Skip to content

Commit 326ef9b

Browse files
authored
feat: add generic kubernetest resource management api (#42)
1 parent acdeca4 commit 326ef9b

24 files changed

+1485
-0
lines changed

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,82 @@ env := testing.NewEnvironmentBuilder().
339339
env.ShouldReconcile(testing.RequestFromStrings("testresource"))
340340
```
341341

342+
### Kubernetes resource management
343+
344+
The `pkg/resource` package contains some useful functions for working with Kubernetes resources. The `Mutator` interface can be used to modify resources in a generic way. It is used by the `Mutate` function, which takes a resource and a mutator and applies the mutator to the resource.
345+
The package also contains convenience types for the most common resource types, e.g. `ConfigMap`, `Secret`, `ClusterRole`, `ClusterRoleBinding`, etc. These types implement the `Mutator` interface and can be used to modify the corresponding resources.
346+
347+
#### Examples
348+
349+
Create or update a `ConfigMap`, a `ServiceAccount` and a `Deployment` using the `Mutator` interface:
350+
351+
```go
352+
type myDeploymentMutator struct {
353+
}
354+
355+
var _ resource.Mutator[*appsv1.Deployment] = &myDeploymentMutator{}
356+
357+
func newDeploymentMutator() resources.Mutator[*appsv1.Deployment] {
358+
return &MyDeploymentMutator{}
359+
}
360+
361+
func (m *MyDeploymentMutator) String() string {
362+
return "deployment default/test"
363+
}
364+
365+
func (m *MyDeploymentMutator) Empty() *appsv1.Deployment {
366+
return &appsv1.Deployment{
367+
ObjectMeta: metav1.ObjectMeta{
368+
Name: "test",
369+
Namespace: "default",
370+
},
371+
}
372+
}
373+
374+
func (m *MyDeploymentMutator) Mutate(deployment *appsv1.Deployment) error {
375+
// create one container with an image
376+
deployment.Spec.Template.Spec.Containers = []corev1.Container{
377+
{
378+
Name: "test",
379+
Image: "test-image:latest",
380+
},
381+
}
382+
return nil
383+
}
384+
385+
386+
func ReconcileResources(ctx context.Context, client client.Client) error {
387+
configMapResource := resource.NewConfigMap("my-configmap", "my-namespace", map[string]string{)
388+
"label1": "value1",
389+
"label2": "value2",
390+
}, nil)
391+
392+
serviceAccountResource := resource.NewServiceAccount("my-serviceaccount", "my-namespace", nil, nil)
393+
394+
myDeploymentMutator := newDeploymentMutator()
395+
396+
var err error
397+
398+
err = resources.CreateOrUpdateResource(ctx, client, configMapResource)
399+
if err != nil {
400+
return err
401+
}
402+
403+
resources.CreateOrUpdateResource(ctx, client, serviceAccountResource)
404+
if err != nil {
405+
return err
406+
}
407+
408+
err = resources.CreateOrUpdateResource(ctx, client, myDeploymentMutator)
409+
if err != nil {
410+
return err
411+
}
412+
413+
return nil
414+
}
415+
416+
```
417+
342418
## Support, Feedback, Contributing
343419

344420
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/openmcp-project/controller-utils/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).

pkg/resources/clusterrole.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package resources
2+
3+
import (
4+
"fmt"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
8+
v1 "k8s.io/api/rbac/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
type ClusterRoleMutator struct {
13+
Name string
14+
Rules []v1.PolicyRule
15+
meta Mutator[client.Object]
16+
}
17+
18+
var _ Mutator[*v1.ClusterRole] = &ClusterRoleMutator{}
19+
20+
func NewClusterRoleMutator(name string, rules []v1.PolicyRule, labels map[string]string, annotations map[string]string) Mutator[*v1.ClusterRole] {
21+
return &ClusterRoleMutator{
22+
Name: name,
23+
Rules: rules,
24+
meta: NewMetadataMutator(labels, annotations),
25+
}
26+
}
27+
28+
func (m *ClusterRoleMutator) String() string {
29+
return fmt.Sprintf("clusterrole %s", m.Name)
30+
}
31+
32+
func (m *ClusterRoleMutator) Empty() *v1.ClusterRole {
33+
return &v1.ClusterRole{
34+
TypeMeta: metav1.TypeMeta{
35+
APIVersion: "rbac.authorization.k8s.io/v1",
36+
Kind: "ClusterRole",
37+
},
38+
ObjectMeta: metav1.ObjectMeta{
39+
Name: m.Name,
40+
},
41+
}
42+
}
43+
44+
func (m *ClusterRoleMutator) Mutate(r *v1.ClusterRole) error {
45+
r.Rules = m.Rules
46+
return m.meta.Mutate(r)
47+
}

pkg/resources/clusterrole_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package resources_test
2+
3+
import (
4+
"context"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
v1 "k8s.io/api/rbac/v1"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
"github.com/openmcp-project/controller-utils/pkg/resources"
13+
"github.com/openmcp-project/controller-utils/pkg/testing"
14+
)
15+
16+
var _ = Describe("ClusterRoleMutator", func() {
17+
var (
18+
ctx context.Context
19+
fakeClient client.WithWatch
20+
scheme *runtime.Scheme
21+
rules []v1.PolicyRule
22+
labels map[string]string
23+
annotations map[string]string
24+
mutator resources.Mutator[*v1.ClusterRole]
25+
)
26+
27+
BeforeEach(func() {
28+
ctx = context.TODO()
29+
30+
// Create a scheme and register the rbac/v1 API
31+
scheme = runtime.NewScheme()
32+
Expect(v1.AddToScheme(scheme)).To(Succeed())
33+
34+
// Initialize the fake client
35+
var err error
36+
fakeClient, err = testing.GetFakeClient(scheme)
37+
Expect(err).ToNot(HaveOccurred())
38+
39+
// Define rules, labels, and annotations
40+
rules = []v1.PolicyRule{
41+
{
42+
APIGroups: []string{""},
43+
Resources: []string{"pods"},
44+
Verbs: []string{"get", "list"},
45+
},
46+
}
47+
labels = map[string]string{"key1": "value1"}
48+
annotations = map[string]string{"annotation1": "value1"}
49+
50+
// Create a cluster role mutator
51+
mutator = resources.NewClusterRoleMutator("test-clusterrole", rules, labels, annotations)
52+
})
53+
54+
It("should create an empty cluster role with correct metadata", func() {
55+
clusterRole := mutator.Empty()
56+
57+
Expect(clusterRole.Name).To(Equal("test-clusterrole"))
58+
Expect(clusterRole.APIVersion).To(Equal("rbac.authorization.k8s.io/v1"))
59+
Expect(clusterRole.Kind).To(Equal("ClusterRole"))
60+
})
61+
62+
It("should apply rules using Mutate", func() {
63+
clusterRole := mutator.Empty()
64+
65+
// Apply the mutator's Mutate method
66+
Expect(mutator.Mutate(clusterRole)).To(Succeed())
67+
68+
// Verify that the rules are applied
69+
Expect(clusterRole.Rules).To(Equal(rules))
70+
})
71+
72+
It("should create and retrieve the cluster role using the fake client", func() {
73+
clusterRole := mutator.Empty()
74+
Expect(mutator.Mutate(clusterRole)).To(Succeed())
75+
76+
// Create the cluster role in the fake client
77+
Expect(fakeClient.Create(ctx, clusterRole)).To(Succeed())
78+
79+
// Retrieve the cluster role from the fake client and verify it
80+
retrievedClusterRole := &v1.ClusterRole{}
81+
Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-clusterrole"}, retrievedClusterRole)).To(Succeed())
82+
Expect(retrievedClusterRole).To(Equal(clusterRole))
83+
})
84+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package resources
2+
3+
import (
4+
"fmt"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
8+
v1 "k8s.io/api/rbac/v1"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
type ClusterRoleBindingMutator struct {
13+
ClusterRoleBindingName string
14+
RoleRef v1.RoleRef
15+
Subjects []v1.Subject
16+
meta Mutator[client.Object]
17+
}
18+
19+
var _ Mutator[*v1.ClusterRoleBinding] = &ClusterRoleBindingMutator{}
20+
21+
func NewClusterRoleBindingMutator(clusterRoleBindingName string, subjects []v1.Subject, roleRef v1.RoleRef, labels map[string]string, annotations map[string]string) Mutator[*v1.ClusterRoleBinding] {
22+
return &ClusterRoleBindingMutator{
23+
ClusterRoleBindingName: clusterRoleBindingName,
24+
RoleRef: roleRef,
25+
Subjects: subjects,
26+
meta: NewMetadataMutator(labels, annotations),
27+
}
28+
}
29+
30+
func (m *ClusterRoleBindingMutator) String() string {
31+
return fmt.Sprintf("clusterrolebinding %s", m.ClusterRoleBindingName)
32+
}
33+
34+
func (m *ClusterRoleBindingMutator) Empty() *v1.ClusterRoleBinding {
35+
return &v1.ClusterRoleBinding{
36+
TypeMeta: metav1.TypeMeta{
37+
APIVersion: "rbac.authorization.k8s.io/v1",
38+
Kind: "ClusterRoleBinding",
39+
},
40+
ObjectMeta: metav1.ObjectMeta{
41+
Name: m.ClusterRoleBindingName,
42+
},
43+
}
44+
}
45+
46+
func (m *ClusterRoleBindingMutator) Mutate(r *v1.ClusterRoleBinding) error {
47+
r.RoleRef = m.RoleRef
48+
r.Subjects = m.Subjects
49+
return m.meta.Mutate(r)
50+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package resources_test
2+
3+
import (
4+
"context"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
v1 "k8s.io/api/rbac/v1"
9+
"k8s.io/apimachinery/pkg/runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
11+
12+
"github.com/openmcp-project/controller-utils/pkg/resources"
13+
"github.com/openmcp-project/controller-utils/pkg/testing"
14+
)
15+
16+
var _ = Describe("ClusterRoleBindingMutator", func() {
17+
var (
18+
ctx context.Context
19+
fakeClient client.WithWatch
20+
scheme *runtime.Scheme
21+
subjects []v1.Subject
22+
roleRef v1.RoleRef
23+
labels map[string]string
24+
annotations map[string]string
25+
mutator resources.Mutator[*v1.ClusterRoleBinding]
26+
)
27+
28+
BeforeEach(func() {
29+
ctx = context.TODO()
30+
31+
// Create a scheme and register the rbac/v1 API
32+
scheme = runtime.NewScheme()
33+
Expect(v1.AddToScheme(scheme)).To(Succeed())
34+
35+
// Initialize the fake client
36+
var err error
37+
fakeClient, err = testing.GetFakeClient(scheme)
38+
Expect(err).ToNot(HaveOccurred())
39+
40+
// Define subjects, roleRef, labels, and annotations
41+
subjects = []v1.Subject{
42+
{
43+
Kind: "User",
44+
Name: "test-user",
45+
Namespace: "test-namespace",
46+
},
47+
}
48+
roleRef = resources.NewClusterRoleRef("test-role")
49+
labels = map[string]string{"key1": "value1"}
50+
annotations = map[string]string{"annotation1": "value1"}
51+
52+
// Create a cluster role binding mutator
53+
mutator = resources.NewClusterRoleBindingMutator("test-clusterrolebinding", subjects, roleRef, labels, annotations)
54+
})
55+
56+
It("should create an empty cluster role binding with correct metadata", func() {
57+
clusterRoleBinding := mutator.Empty()
58+
59+
Expect(clusterRoleBinding.Name).To(Equal("test-clusterrolebinding"))
60+
Expect(clusterRoleBinding.APIVersion).To(Equal("rbac.authorization.k8s.io/v1"))
61+
Expect(clusterRoleBinding.Kind).To(Equal("ClusterRoleBinding"))
62+
})
63+
64+
It("should apply subjects and roleRef using Mutate", func() {
65+
clusterRoleBinding := mutator.Empty()
66+
67+
// Apply the mutator's Mutate method
68+
Expect(mutator.Mutate(clusterRoleBinding)).To(Succeed())
69+
70+
// Verify that the subjects and roleRef are applied
71+
Expect(clusterRoleBinding.Subjects).To(Equal(subjects))
72+
Expect(clusterRoleBinding.RoleRef).To(Equal(roleRef))
73+
})
74+
75+
It("should create and retrieve the cluster role binding using the fake client", func() {
76+
clusterRoleBinding := mutator.Empty()
77+
Expect(mutator.Mutate(clusterRoleBinding)).To(Succeed())
78+
79+
// Create the cluster role binding in the fake client
80+
Expect(fakeClient.Create(ctx, clusterRoleBinding)).To(Succeed())
81+
82+
// Retrieve the cluster role binding from the fake client and verify it
83+
retrievedClusterRoleBinding := &v1.ClusterRoleBinding{}
84+
Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-clusterrolebinding"}, retrievedClusterRoleBinding)).To(Succeed())
85+
Expect(retrievedClusterRoleBinding).To(Equal(clusterRoleBinding))
86+
})
87+
})

0 commit comments

Comments
 (0)