From 52843e38c37774a9440a616f39ba8b04d7bc67b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sch=C3=BCnemann?= Date: Wed, 7 May 2025 11:23:22 +0200 Subject: [PATCH 1/3] add generic kubernetest resource management api --- pkg/resources/clusterrole.go | 38 ++++++++++++++++++ pkg/resources/clusterrole_test.go | 1 + pkg/resources/clusterrolebinding.go | 51 ++++++++++++++++++++++++ pkg/resources/clusterrolebinding_test.go | 1 + pkg/resources/configmap.go | 1 + pkg/resources/configmap_test.go | 1 + pkg/resources/crd.go | 39 ++++++++++++++++++ pkg/resources/crd_test.go | 1 + pkg/resources/metadata.go | 20 ++++++++++ pkg/resources/mutator.go | 42 +++++++++++++++++++ pkg/resources/mutator_test.go | 1 + pkg/resources/namespace.go | 38 ++++++++++++++++++ pkg/resources/namespace_test.go | 1 + pkg/resources/resources_suite_test.go | 1 + pkg/resources/role.go | 1 + pkg/resources/role_test.go | 1 + pkg/resources/rolebinding.go | 1 + pkg/resources/rolebinding_test.go | 1 + pkg/resources/secret.go | 1 + pkg/resources/secret_test.go | 1 + pkg/resources/serviceaccount.go | 38 ++++++++++++++++++ pkg/resources/serviceaccount_test.go | 1 + pkg/resources/util.go | 1 + 23 files changed, 282 insertions(+) create mode 100644 pkg/resources/clusterrole.go create mode 100644 pkg/resources/clusterrole_test.go create mode 100644 pkg/resources/clusterrolebinding.go create mode 100644 pkg/resources/clusterrolebinding_test.go create mode 100644 pkg/resources/configmap.go create mode 100644 pkg/resources/configmap_test.go create mode 100644 pkg/resources/crd.go create mode 100644 pkg/resources/crd_test.go create mode 100644 pkg/resources/metadata.go create mode 100644 pkg/resources/mutator.go create mode 100644 pkg/resources/mutator_test.go create mode 100644 pkg/resources/namespace.go create mode 100644 pkg/resources/namespace_test.go create mode 100644 pkg/resources/resources_suite_test.go create mode 100644 pkg/resources/role.go create mode 100644 pkg/resources/role_test.go create mode 100644 pkg/resources/rolebinding.go create mode 100644 pkg/resources/rolebinding_test.go create mode 100644 pkg/resources/secret.go create mode 100644 pkg/resources/secret_test.go create mode 100644 pkg/resources/serviceaccount.go create mode 100644 pkg/resources/serviceaccount_test.go create mode 100644 pkg/resources/util.go diff --git a/pkg/resources/clusterrole.go b/pkg/resources/clusterrole.go new file mode 100644 index 0000000..15545c2 --- /dev/null +++ b/pkg/resources/clusterrole.go @@ -0,0 +1,38 @@ +package resources + +import ( + "fmt" + + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ClusterRoleMutator struct { + Name string + Labels map[string]string + Rules []v1.PolicyRule +} + +var _ Mutator[*v1.ClusterRole] = &ClusterRoleMutator{} + +func (m *ClusterRoleMutator) String() string { + return fmt.Sprintf("clusterrole %s", m.Name) +} + +func (m *ClusterRoleMutator) Empty() *v1.ClusterRole { + return &v1.ClusterRole{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "ClusterRole", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + }, + } +} + +func (m *ClusterRoleMutator) Mutate(r *v1.ClusterRole) error { + r.ObjectMeta.Labels = m.Labels + r.Rules = m.Rules + return nil +} diff --git a/pkg/resources/clusterrole_test.go b/pkg/resources/clusterrole_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/clusterrole_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/clusterrolebinding.go b/pkg/resources/clusterrolebinding.go new file mode 100644 index 0000000..3ca9fe5 --- /dev/null +++ b/pkg/resources/clusterrolebinding.go @@ -0,0 +1,51 @@ +package resources + +import ( + "fmt" + + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ClusterRoleBindingMutator struct { + ClusterRoleBindingName string + ClusterRoleName string + ServiceAccountName string + ServiceAccountNamespace string + Labels map[string]string +} + +var _ Mutator[*v1.ClusterRoleBinding] = &ClusterRoleBindingMutator{} + +func (m *ClusterRoleBindingMutator) String() string { + return fmt.Sprintf("clusterrolebinding %s", m.ClusterRoleBindingName) +} + +func (m *ClusterRoleBindingMutator) Empty() *v1.ClusterRoleBinding { + return &v1.ClusterRoleBinding{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "ClusterRoleBinding", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.ClusterRoleBindingName, + }, + } +} + +func (m *ClusterRoleBindingMutator) Mutate(r *v1.ClusterRoleBinding) error { + r.ObjectMeta.Labels = m.Labels + r.RoleRef = v1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: m.ClusterRoleName, + } + r.Subjects = []v1.Subject{ + { + Kind: "ServiceAccount", + Name: m.ServiceAccountName, + Namespace: m.ServiceAccountNamespace, + }, + } + return nil +} diff --git a/pkg/resources/clusterrolebinding_test.go b/pkg/resources/clusterrolebinding_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/clusterrolebinding_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/configmap.go b/pkg/resources/configmap.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/configmap.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/configmap_test.go b/pkg/resources/configmap_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/configmap_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/crd.go b/pkg/resources/crd.go new file mode 100644 index 0000000..f56c204 --- /dev/null +++ b/pkg/resources/crd.go @@ -0,0 +1,39 @@ +package resources + +import ( + "fmt" + + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type crdMutator struct { + crd *apiextv1.CustomResourceDefinition +} + +var _ Mutator[*apiextv1.CustomResourceDefinition] = &crdMutator{} + +func NewCRDMutator(crd *apiextv1.CustomResourceDefinition) Mutator[*apiextv1.CustomResourceDefinition] { + return &crdMutator{crd: crd} +} + +func (m *crdMutator) String() string { + return fmt.Sprintf("crd %s", m.crd.Name) +} + +func (m *crdMutator) Empty() *apiextv1.CustomResourceDefinition { + return &apiextv1.CustomResourceDefinition{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "apiextensions.k8s.io/v1", + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.crd.Name, + }, + } +} + +func (m *crdMutator) Mutate(r *apiextv1.CustomResourceDefinition) error { + m.crd.Spec.DeepCopyInto(&r.Spec) + return nil +} diff --git a/pkg/resources/crd_test.go b/pkg/resources/crd_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/crd_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/metadata.go b/pkg/resources/metadata.go new file mode 100644 index 0000000..6839916 --- /dev/null +++ b/pkg/resources/metadata.go @@ -0,0 +1,20 @@ +package resources + +import ( + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type baseMutator struct { + Labels map[string]string + Annotations map[string]string +} + +var _ Mutator[client.Object] = &baseMutator{} + +func NewBaseMutator(labels, annotations map[string]string) Mutator[client.Object] { + return &baseMutator{ + Labels: labels, + Annotations: annotations, + } +} diff --git a/pkg/resources/mutator.go b/pkg/resources/mutator.go new file mode 100644 index 0000000..7aeb3ce --- /dev/null +++ b/pkg/resources/mutator.go @@ -0,0 +1,42 @@ +package resources + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type Mutator[K client.Object] interface { + Empty() K + Mutate(res K) error + String() string +} + +func GetResource[K client.Object](ctx context.Context, clt client.Client, m Mutator[K]) (K, error) { + res := m.Empty() + if err := clt.Get(ctx, client.ObjectKeyFromObject(res), res); err != nil { + return res, fmt.Errorf("failed to get %s: %w", m.String(), err) + } + return res, nil +} + +func CreateOrUpdateResource[K client.Object](ctx context.Context, clt client.Client, m Mutator[K]) error { + res := m.Empty() + _, err := controllerutil.CreateOrUpdate(ctx, clt, res, func() error { + return m.Mutate(res) + }) + if err != nil { + return fmt.Errorf("failed to create or update %s: %w", m.String(), err) + } + return nil +} + +func DeleteResource[K client.Object](ctx context.Context, clt client.Client, m Mutator[K]) error { + res := m.Empty() + if err := clt.Delete(ctx, res); client.IgnoreNotFound(err) != nil { + return fmt.Errorf("failed to delete %s: %w", m.String(), err) + } + return nil +} diff --git a/pkg/resources/mutator_test.go b/pkg/resources/mutator_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/mutator_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/namespace.go b/pkg/resources/namespace.go new file mode 100644 index 0000000..3fa8c8b --- /dev/null +++ b/pkg/resources/namespace.go @@ -0,0 +1,38 @@ +package resources + +import ( + "fmt" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type namespaceMutator struct { + name string +} + +var _ Mutator[*v1.Namespace] = &namespaceMutator{} + +func NewNamespaceMutator(name string) Mutator[*v1.Namespace] { + return &namespaceMutator{name: name} +} + +func (m *namespaceMutator) String() string { + return fmt.Sprintf("namespace %s", m.name) +} + +func (m *namespaceMutator) Empty() *v1.Namespace { + return &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.name, + }, + } +} + +func (*namespaceMutator) Mutate(r *v1.Namespace) error { + return nil +} diff --git a/pkg/resources/namespace_test.go b/pkg/resources/namespace_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/namespace_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/resources_suite_test.go b/pkg/resources/resources_suite_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/resources_suite_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/role.go b/pkg/resources/role.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/role.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/role_test.go b/pkg/resources/role_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/role_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/rolebinding.go b/pkg/resources/rolebinding.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/rolebinding.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/rolebinding_test.go b/pkg/resources/rolebinding_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/rolebinding_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/secret.go b/pkg/resources/secret.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/secret.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/secret_test.go b/pkg/resources/secret_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/secret_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/serviceaccount.go b/pkg/resources/serviceaccount.go new file mode 100644 index 0000000..9e3da59 --- /dev/null +++ b/pkg/resources/serviceaccount.go @@ -0,0 +1,38 @@ +package resources + +import ( + "fmt" + + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ServiceAccountMutator struct { + Name string + Namespace string + Labels map[string]string +} + +var _ Mutator[*core.ServiceAccount] = &ServiceAccountMutator{} + +func (m *ServiceAccountMutator) String() string { + return fmt.Sprintf("service account %s/%s", m.Namespace, m.Name) +} + +func (m *ServiceAccountMutator) Empty() *core.ServiceAccount { + return &core.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + } +} + +func (m *ServiceAccountMutator) Mutate(s *core.ServiceAccount) error { + s.ObjectMeta.Labels = m.Labels + return nil +} diff --git a/pkg/resources/serviceaccount_test.go b/pkg/resources/serviceaccount_test.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/serviceaccount_test.go @@ -0,0 +1 @@ +package resources diff --git a/pkg/resources/util.go b/pkg/resources/util.go new file mode 100644 index 0000000..18d6395 --- /dev/null +++ b/pkg/resources/util.go @@ -0,0 +1 @@ +package resources From 1b9dfe7f66dfc502a918bb80ed9138280f73fa64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sch=C3=BCnemann?= Date: Wed, 7 May 2025 12:29:29 +0200 Subject: [PATCH 2/3] format --- pkg/resources/clusterrole.go | 19 +++-- pkg/resources/clusterrole_test.go | 85 +++++++++++++++++++- pkg/resources/clusterrolebinding.go | 37 +++++---- pkg/resources/clusterrolebinding_test.go | 88 ++++++++++++++++++++- pkg/resources/configmap.go | 54 +++++++++++++ pkg/resources/configmap_test.go | 82 +++++++++++++++++++- pkg/resources/crd.go | 11 ++- pkg/resources/crd_test.go | 99 +++++++++++++++++++++++- pkg/resources/metadata.go | 39 ++++++++-- pkg/resources/mutator_test.go | 67 +++++++++++++++- pkg/resources/namespace.go | 11 ++- pkg/resources/namespace_test.go | 90 ++++++++++++++++++++- pkg/resources/resources_suite_test.go | 14 +++- pkg/resources/role.go | 49 ++++++++++++ pkg/resources/role_test.go | 86 +++++++++++++++++++- pkg/resources/rolebinding.go | 52 +++++++++++++ pkg/resources/rolebinding_test.go | 89 ++++++++++++++++++++- pkg/resources/secret.go | 57 ++++++++++++++ pkg/resources/secret_test.go | 86 +++++++++++++++++++- pkg/resources/serviceaccount.go | 15 +++- pkg/resources/serviceaccount_test.go | 79 ++++++++++++++++++- pkg/resources/util.go | 20 +++++ 22 files changed, 1178 insertions(+), 51 deletions(-) diff --git a/pkg/resources/clusterrole.go b/pkg/resources/clusterrole.go index 15545c2..d67dbbf 100644 --- a/pkg/resources/clusterrole.go +++ b/pkg/resources/clusterrole.go @@ -3,18 +3,28 @@ package resources import ( "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ClusterRoleMutator struct { - Name string - Labels map[string]string - Rules []v1.PolicyRule + Name string + Rules []v1.PolicyRule + meta Mutator[client.Object] } var _ Mutator[*v1.ClusterRole] = &ClusterRoleMutator{} +func NewClusterRoleMutator(name string, rules []v1.PolicyRule, labels map[string]string, annotations map[string]string) Mutator[*v1.ClusterRole] { + return &ClusterRoleMutator{ + Name: name, + Rules: rules, + meta: NewMetadataMutator(labels, annotations), + } +} + func (m *ClusterRoleMutator) String() string { return fmt.Sprintf("clusterrole %s", m.Name) } @@ -32,7 +42,6 @@ func (m *ClusterRoleMutator) Empty() *v1.ClusterRole { } func (m *ClusterRoleMutator) Mutate(r *v1.ClusterRole) error { - r.ObjectMeta.Labels = m.Labels r.Rules = m.Rules - return nil + return m.meta.Mutate(r) } diff --git a/pkg/resources/clusterrole_test.go b/pkg/resources/clusterrole_test.go index 18d6395..9e4efcf 100644 --- a/pkg/resources/clusterrole_test.go +++ b/pkg/resources/clusterrole_test.go @@ -1 +1,84 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("ClusterRoleMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + rules []v1.PolicyRule + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*v1.ClusterRole] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the rbac/v1 API + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define rules, labels, and annotations + rules = []v1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list"}, + }, + } + labels = map[string]string{"key1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a cluster role mutator + mutator = resources.NewClusterRoleMutator("test-clusterrole", rules, labels, annotations) + }) + + It("should create an empty cluster role with correct metadata", func() { + clusterRole := mutator.Empty() + + Expect(clusterRole.Name).To(Equal("test-clusterrole")) + Expect(clusterRole.APIVersion).To(Equal("rbac.authorization.k8s.io/v1")) + Expect(clusterRole.Kind).To(Equal("ClusterRole")) + }) + + It("should apply rules using Mutate", func() { + clusterRole := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(clusterRole)).To(Succeed()) + + // Verify that the rules are applied + Expect(clusterRole.Rules).To(Equal(rules)) + }) + + It("should create and retrieve the cluster role using the fake client", func() { + clusterRole := mutator.Empty() + Expect(mutator.Mutate(clusterRole)).To(Succeed()) + + // Create the cluster role in the fake client + Expect(fakeClient.Create(ctx, clusterRole)).To(Succeed()) + + // Retrieve the cluster role from the fake client and verify it + retrievedClusterRole := &v1.ClusterRole{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-clusterrole"}, retrievedClusterRole)).To(Succeed()) + Expect(retrievedClusterRole).To(Equal(clusterRole)) + }) +}) diff --git a/pkg/resources/clusterrolebinding.go b/pkg/resources/clusterrolebinding.go index 3ca9fe5..36daf2b 100644 --- a/pkg/resources/clusterrolebinding.go +++ b/pkg/resources/clusterrolebinding.go @@ -3,20 +3,30 @@ package resources import ( "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type ClusterRoleBindingMutator struct { - ClusterRoleBindingName string - ClusterRoleName string - ServiceAccountName string - ServiceAccountNamespace string - Labels map[string]string + ClusterRoleBindingName string + RoleRef v1.RoleRef + Subjects []v1.Subject + meta Mutator[client.Object] } var _ Mutator[*v1.ClusterRoleBinding] = &ClusterRoleBindingMutator{} +func NewClusterRoleBindingMutator(clusterRoleBindingName string, subjects []v1.Subject, roleRef v1.RoleRef, labels map[string]string, annotations map[string]string) Mutator[*v1.ClusterRoleBinding] { + return &ClusterRoleBindingMutator{ + ClusterRoleBindingName: clusterRoleBindingName, + RoleRef: roleRef, + Subjects: subjects, + meta: NewMetadataMutator(labels, annotations), + } +} + func (m *ClusterRoleBindingMutator) String() string { return fmt.Sprintf("clusterrolebinding %s", m.ClusterRoleBindingName) } @@ -34,18 +44,7 @@ func (m *ClusterRoleBindingMutator) Empty() *v1.ClusterRoleBinding { } func (m *ClusterRoleBindingMutator) Mutate(r *v1.ClusterRoleBinding) error { - r.ObjectMeta.Labels = m.Labels - r.RoleRef = v1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "ClusterRole", - Name: m.ClusterRoleName, - } - r.Subjects = []v1.Subject{ - { - Kind: "ServiceAccount", - Name: m.ServiceAccountName, - Namespace: m.ServiceAccountNamespace, - }, - } - return nil + r.RoleRef = m.RoleRef + r.Subjects = m.Subjects + return m.meta.Mutate(r) } diff --git a/pkg/resources/clusterrolebinding_test.go b/pkg/resources/clusterrolebinding_test.go index 18d6395..9f6b041 100644 --- a/pkg/resources/clusterrolebinding_test.go +++ b/pkg/resources/clusterrolebinding_test.go @@ -1 +1,87 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("ClusterRoleBindingMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + subjects []v1.Subject + roleRef v1.RoleRef + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*v1.ClusterRoleBinding] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the rbac/v1 API + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define subjects, roleRef, labels, and annotations + subjects = []v1.Subject{ + { + Kind: "User", + Name: "test-user", + Namespace: "test-namespace", + }, + } + roleRef = resources.NewClusterRoleRef("test-role") + labels = map[string]string{"key1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a cluster role binding mutator + mutator = resources.NewClusterRoleBindingMutator("test-clusterrolebinding", subjects, roleRef, labels, annotations) + }) + + It("should create an empty cluster role binding with correct metadata", func() { + clusterRoleBinding := mutator.Empty() + + Expect(clusterRoleBinding.Name).To(Equal("test-clusterrolebinding")) + Expect(clusterRoleBinding.APIVersion).To(Equal("rbac.authorization.k8s.io/v1")) + Expect(clusterRoleBinding.Kind).To(Equal("ClusterRoleBinding")) + }) + + It("should apply subjects and roleRef using Mutate", func() { + clusterRoleBinding := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(clusterRoleBinding)).To(Succeed()) + + // Verify that the subjects and roleRef are applied + Expect(clusterRoleBinding.Subjects).To(Equal(subjects)) + Expect(clusterRoleBinding.RoleRef).To(Equal(roleRef)) + }) + + It("should create and retrieve the cluster role binding using the fake client", func() { + clusterRoleBinding := mutator.Empty() + Expect(mutator.Mutate(clusterRoleBinding)).To(Succeed()) + + // Create the cluster role binding in the fake client + Expect(fakeClient.Create(ctx, clusterRoleBinding)).To(Succeed()) + + // Retrieve the cluster role binding from the fake client and verify it + retrievedClusterRoleBinding := &v1.ClusterRoleBinding{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-clusterrolebinding"}, retrievedClusterRoleBinding)).To(Succeed()) + Expect(retrievedClusterRoleBinding).To(Equal(clusterRoleBinding)) + }) +}) diff --git a/pkg/resources/configmap.go b/pkg/resources/configmap.go index 18d6395..baea795 100644 --- a/pkg/resources/configmap.go +++ b/pkg/resources/configmap.go @@ -1 +1,55 @@ package resources + +import ( + "fmt" + + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ConfigMapMutator struct { + Name string + Namespace string + Data map[string]string + meta Mutator[client.Object] +} + +var _ Mutator[*core.ConfigMap] = &ConfigMapMutator{} + +func NewConfigMapMutator(name, namespace string, data map[string]string, labels map[string]string, annotations map[string]string) Mutator[*core.ConfigMap] { + return &ConfigMapMutator{ + Name: name, + Namespace: namespace, + Data: data, + meta: NewMetadataMutator(labels, annotations), + } +} + +func (m *ConfigMapMutator) String() string { + return fmt.Sprintf("configmap %s/%s", m.Namespace, m.Name) +} + +func (m *ConfigMapMutator) Empty() *core.ConfigMap { + return &core.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + Data: m.Data, + } +} + +func (m *ConfigMapMutator) Mutate(cm *core.ConfigMap) error { + if cm.Data == nil { + cm.Data = make(map[string]string) + } + for key, value := range m.Data { + cm.Data[key] = value + } + return m.meta.Mutate(cm) +} diff --git a/pkg/resources/configmap_test.go b/pkg/resources/configmap_test.go index 18d6395..06ed8a3 100644 --- a/pkg/resources/configmap_test.go +++ b/pkg/resources/configmap_test.go @@ -1 +1,81 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("ConfigMapMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + data map[string]string + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*v1.ConfigMap] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the core/v1 API + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define data, labels, and annotations + data = map[string]string{"key1": "value1", "key2": "value2"} + labels = map[string]string{"label1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a ConfigMap mutator + mutator = resources.NewConfigMapMutator("test-configmap", "test-namespace", data, labels, annotations) + }) + + It("should create an empty ConfigMap with correct metadata", func() { + configMap := mutator.Empty() + + Expect(configMap.Name).To(Equal("test-configmap")) + Expect(configMap.Namespace).To(Equal("test-namespace")) + Expect(configMap.APIVersion).To(Equal("v1")) + Expect(configMap.Kind).To(Equal("ConfigMap")) + }) + + It("should apply data, labels, and annotations using Mutate", func() { + configMap := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(configMap)).To(Succeed()) + + // Verify that the data, labels, and annotations are applied + Expect(configMap.Data).To(Equal(data)) + Expect(configMap.Labels).To(Equal(labels)) + Expect(configMap.Annotations).To(Equal(annotations)) + }) + + It("should create and retrieve the ConfigMap using the fake client", func() { + configMap := mutator.Empty() + Expect(mutator.Mutate(configMap)).To(Succeed()) + + // Create the ConfigMap in the fake client + Expect(fakeClient.Create(ctx, configMap)).To(Succeed()) + + // Retrieve the ConfigMap from the fake client and verify it + retrievedConfigMap := &v1.ConfigMap{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-configmap", Namespace: "test-namespace"}, retrievedConfigMap)).To(Succeed()) + Expect(retrievedConfigMap).To(Equal(configMap)) + }) +}) diff --git a/pkg/resources/crd.go b/pkg/resources/crd.go index f56c204..5fcabd2 100644 --- a/pkg/resources/crd.go +++ b/pkg/resources/crd.go @@ -3,18 +3,21 @@ package resources import ( "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type crdMutator struct { - crd *apiextv1.CustomResourceDefinition + crd *apiextv1.CustomResourceDefinition + meta Mutator[client.Object] } var _ Mutator[*apiextv1.CustomResourceDefinition] = &crdMutator{} -func NewCRDMutator(crd *apiextv1.CustomResourceDefinition) Mutator[*apiextv1.CustomResourceDefinition] { - return &crdMutator{crd: crd} +func NewCRDMutator(crd *apiextv1.CustomResourceDefinition, labels map[string]string, annotations map[string]string) Mutator[*apiextv1.CustomResourceDefinition] { + return &crdMutator{crd: crd, meta: NewMetadataMutator(labels, annotations)} } func (m *crdMutator) String() string { @@ -35,5 +38,5 @@ func (m *crdMutator) Empty() *apiextv1.CustomResourceDefinition { func (m *crdMutator) Mutate(r *apiextv1.CustomResourceDefinition) error { m.crd.Spec.DeepCopyInto(&r.Spec) - return nil + return m.meta.Mutate(r) } diff --git a/pkg/resources/crd_test.go b/pkg/resources/crd_test.go index 18d6395..7f86959 100644 --- a/pkg/resources/crd_test.go +++ b/pkg/resources/crd_test.go @@ -1 +1,98 @@ -package resources +package resources_test + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("CRDMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + labels map[string]string + annotations map[string]string + crd *apiextensionsv1.CustomResourceDefinition + mutator resources.Mutator[*apiextensionsv1.CustomResourceDefinition] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the apiextensions/v1 API + scheme = runtime.NewScheme() + Expect(apiextensionsv1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define labels and annotations + labels = map[string]string{"label1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Define a CRD object + crd = &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-crd", + Labels: labels, + Annotations: annotations, + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "example.com", + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "tests", + Singular: "test", + Kind: "Test", + }, + Scope: apiextensionsv1.NamespaceScoped, + }, + } + + // Create a CRD mutator + mutator = resources.NewCRDMutator(crd, labels, annotations) + }) + + It("should create an empty CRD with correct metadata", func() { + crd := mutator.Empty() + + Expect(crd.Name).To(Equal("test-crd")) + Expect(crd.APIVersion).To(Equal("apiextensions.k8s.io/v1")) + Expect(crd.Kind).To(Equal("CustomResourceDefinition")) + }) + + It("should apply labels and annotations using Mutate", func() { + crd := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(crd)).To(Succeed()) + + // Verify that the labels and annotations are applied + Expect(crd.Labels).To(Equal(labels)) + Expect(crd.Annotations).To(Equal(annotations)) + }) + + It("should create and retrieve the CRD using the fake client", func() { + crd := mutator.Empty() + Expect(mutator.Mutate(crd)).To(Succeed()) + + // Create the CRD in the fake client + Expect(fakeClient.Create(ctx, crd)).To(Succeed()) + + // Retrieve the CRD from the fake client and verify it + retrievedCRD := &apiextensionsv1.CustomResourceDefinition{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-crd"}, retrievedCRD)).To(Succeed()) + Expect(retrievedCRD).To(Equal(crd)) + }) +}) diff --git a/pkg/resources/metadata.go b/pkg/resources/metadata.go index 6839916..2c6c7d5 100644 --- a/pkg/resources/metadata.go +++ b/pkg/resources/metadata.go @@ -1,20 +1,49 @@ package resources import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) -type baseMutator struct { +type metadataMutator struct { Labels map[string]string Annotations map[string]string } -var _ Mutator[client.Object] = &baseMutator{} +var _ Mutator[client.Object] = &metadataMutator{} -func NewBaseMutator(labels, annotations map[string]string) Mutator[client.Object] { - return &baseMutator{ +func NewMetadataMutator(labels map[string]string, annotations map[string]string) Mutator[client.Object] { + return &metadataMutator{ Labels: labels, Annotations: annotations, } } + +func (m *metadataMutator) String() string { + return "metadata" +} + +func (m *metadataMutator) Empty() client.Object { + return nil +} + +func (m *metadataMutator) Mutate(res client.Object) error { + if m.Labels != nil { + if res.GetLabels() == nil { + res.SetLabels(make(map[string]string)) + } + for k, v := range m.Labels { + res.GetLabels()[k] = v + } + } + + if m.Annotations != nil { + if res.GetAnnotations() == nil { + res.SetAnnotations(make(map[string]string)) + } + + for k, v := range m.Annotations { + res.GetAnnotations()[k] = v + } + } + return nil +} diff --git a/pkg/resources/mutator_test.go b/pkg/resources/mutator_test.go index 18d6395..1d496d5 100644 --- a/pkg/resources/mutator_test.go +++ b/pkg/resources/mutator_test.go @@ -1 +1,66 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("Resource Functions", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + labels map[string]string + annotations map[string]string + data map[string]string + mutator resources.Mutator[*corev1.ConfigMap] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the core/v1 API + scheme = runtime.NewScheme() + Expect(corev1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define labels, annotations, and data + labels = map[string]string{"label1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + data = map[string]string{"key1": "value1", "key2": "value2"} + + // Create a ConfigMap mutator + mutator = resources.NewConfigMapMutator("test-configmap", "test-namespace", data, labels, annotations) + }) + + It("should get, create or update, and delete a resource", func() { + // Test CreateOrUpdateResource + Expect(resources.CreateOrUpdateResource(ctx, fakeClient, mutator)).To(Succeed()) + + // Test GetResource + retrievedConfigMap, err := resources.GetResource(ctx, fakeClient, mutator) + Expect(err).ToNot(HaveOccurred()) + Expect(retrievedConfigMap.Data).To(Equal(data)) + Expect(retrievedConfigMap.Labels).To(Equal(labels)) + Expect(retrievedConfigMap.Annotations).To(Equal(annotations)) + + // Test DeleteResource + Expect(resources.DeleteResource(ctx, fakeClient, mutator)).To(Succeed()) + + // Verify the resource is deleted + _, err = resources.GetResource(ctx, fakeClient, mutator) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/pkg/resources/namespace.go b/pkg/resources/namespace.go index 3fa8c8b..23a0c24 100644 --- a/pkg/resources/namespace.go +++ b/pkg/resources/namespace.go @@ -3,18 +3,21 @@ package resources import ( "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) type namespaceMutator struct { name string + meta Mutator[client.Object] } var _ Mutator[*v1.Namespace] = &namespaceMutator{} -func NewNamespaceMutator(name string) Mutator[*v1.Namespace] { - return &namespaceMutator{name: name} +func NewNamespaceMutator(name string, labels map[string]string, annotations map[string]string) Mutator[*v1.Namespace] { + return &namespaceMutator{name: name, meta: NewMetadataMutator(labels, annotations)} } func (m *namespaceMutator) String() string { @@ -33,6 +36,6 @@ func (m *namespaceMutator) Empty() *v1.Namespace { } } -func (*namespaceMutator) Mutate(r *v1.Namespace) error { - return nil +func (m *namespaceMutator) Mutate(r *v1.Namespace) error { + return m.meta.Mutate(r) } diff --git a/pkg/resources/namespace_test.go b/pkg/resources/namespace_test.go index 18d6395..26a28bc 100644 --- a/pkg/resources/namespace_test.go +++ b/pkg/resources/namespace_test.go @@ -1 +1,89 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("NamespaceMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*v1.Namespace] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the core/v1 API + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define labels and annotations + labels = map[string]string{"key1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a namespace mutator + mutator = resources.NewNamespaceMutator("test-namespace", labels, annotations) + }) + + It("should create an empty namespace with correct metadata", func() { + namespace := mutator.Empty() + + Expect(namespace.Name).To(Equal("test-namespace")) + Expect(namespace.APIVersion).To(Equal("v1")) + Expect(namespace.Kind).To(Equal("Namespace")) + }) + + It("should apply labels and annotations using Mutate", func() { + namespace := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(namespace)).To(Succeed()) + + // Verify that the labels and annotations are applied + Expect(namespace.Labels).To(Equal(labels)) + Expect(namespace.Annotations).To(Equal(annotations)) + + // Add additional labels and annotations + additionalLabels := map[string]string{"key2": "value2"} + additionalAnnotations := map[string]string{"annotation2": "value2"} + namespace.SetLabels(additionalLabels) + namespace.SetAnnotations(additionalAnnotations) + + // Apply the mutator's Mutate method again + Expect(mutator.Mutate(namespace)).To(Succeed()) + // Verify that the original and additional labels and annotations are merged + Expect(namespace.Labels).To(Equal(map[string]string{"key1": "value1", "key2": "value2"})) + Expect(namespace.Annotations).To(Equal(map[string]string{"annotation1": "value1", "annotation2": "value2"})) + }) + + It("should create and retrieve the namespace using the fake client", func() { + namespace := mutator.Empty() + Expect(mutator.Mutate(namespace)).To(Succeed()) + + // Create the namespace in the fake client + Expect(fakeClient.Create(ctx, namespace)).To(Succeed()) + + // Retrieve the namespace from the fake client and verify it + retrievedNamespace := &v1.Namespace{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-namespace"}, retrievedNamespace)).To(Succeed()) + Expect(retrievedNamespace).To(Equal(namespace)) + }) +}) diff --git a/pkg/resources/resources_suite_test.go b/pkg/resources/resources_suite_test.go index 18d6395..dd8d7c3 100644 --- a/pkg/resources/resources_suite_test.go +++ b/pkg/resources/resources_suite_test.go @@ -1 +1,13 @@ -package resources +package resources_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestResources(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Resources Suite") +} diff --git a/pkg/resources/role.go b/pkg/resources/role.go index 18d6395..30ae583 100644 --- a/pkg/resources/role.go +++ b/pkg/resources/role.go @@ -1 +1,50 @@ package resources + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RoleMutator struct { + Name string + Namespace string + Rules []v1.PolicyRule + meta Mutator[client.Object] +} + +var _ Mutator[*v1.Role] = &RoleMutator{} + +func NewRoleMutator(name, namespace string, rules []v1.PolicyRule, labels map[string]string, annotations map[string]string) Mutator[*v1.Role] { + return &RoleMutator{ + Name: name, + Namespace: namespace, + Rules: rules, + meta: NewMetadataMutator(labels, annotations), + } +} + +func (m *RoleMutator) String() string { + return fmt.Sprintf("role %s/%s", m.Namespace, m.Name) +} + +func (m *RoleMutator) Empty() *v1.Role { + return &v1.Role{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "Role", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + } +} + +func (m *RoleMutator) Mutate(r *v1.Role) error { + r.Rules = m.Rules + return m.meta.Mutate(r) +} diff --git a/pkg/resources/role_test.go b/pkg/resources/role_test.go index 18d6395..5b2f17e 100644 --- a/pkg/resources/role_test.go +++ b/pkg/resources/role_test.go @@ -1 +1,85 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("RoleMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + rules []v1.PolicyRule + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*v1.Role] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the rbac/v1 API + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define rules, labels, and annotations + rules = []v1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list"}, + }, + } + labels = map[string]string{"key1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a role mutator + mutator = resources.NewRoleMutator("test-role", "test-namespace", rules, labels, annotations) + }) + + It("should create an empty role with correct metadata", func() { + role := mutator.Empty() + + Expect(role.Name).To(Equal("test-role")) + Expect(role.Namespace).To(Equal("test-namespace")) + Expect(role.APIVersion).To(Equal("rbac.authorization.k8s.io/v1")) + Expect(role.Kind).To(Equal("Role")) + }) + + It("should apply rules using Mutate", func() { + role := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(role)).To(Succeed()) + + // Verify that the rules are applied + Expect(role.Rules).To(Equal(rules)) + }) + + It("should create and retrieve the role using the fake client", func() { + role := mutator.Empty() + Expect(mutator.Mutate(role)).To(Succeed()) + + // Create the role in the fake client + Expect(fakeClient.Create(ctx, role)).To(Succeed()) + + // Retrieve the role from the fake client and verify it + retrievedRole := &v1.Role{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-role", Namespace: "test-namespace"}, retrievedRole)).To(Succeed()) + Expect(retrievedRole).To(Equal(role)) + }) +}) diff --git a/pkg/resources/rolebinding.go b/pkg/resources/rolebinding.go index 18d6395..a6b4f86 100644 --- a/pkg/resources/rolebinding.go +++ b/pkg/resources/rolebinding.go @@ -1 +1,53 @@ package resources + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + v1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RoleBindingMutator struct { + Name string + Namespace string + Subjects []v1.Subject + RoleRef v1.RoleRef + meta Mutator[client.Object] +} + +var _ Mutator[*v1.RoleBinding] = &RoleBindingMutator{} + +func NewRoleBindingMutator(name, namespace string, subjects []v1.Subject, roleRef v1.RoleRef, labels map[string]string, annotations map[string]string) Mutator[*v1.RoleBinding] { + return &RoleBindingMutator{ + Name: name, + Namespace: namespace, + Subjects: subjects, + RoleRef: roleRef, + meta: NewMetadataMutator(labels, annotations), + } +} + +func (m *RoleBindingMutator) String() string { + return fmt.Sprintf("rolebinding %s/%s", m.Namespace, m.Name) +} + +func (m *RoleBindingMutator) Empty() *v1.RoleBinding { + return &v1.RoleBinding{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "rbac.authorization.k8s.io/v1", + Kind: "RoleBinding", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + } +} + +func (m *RoleBindingMutator) Mutate(rb *v1.RoleBinding) error { + rb.Subjects = m.Subjects + rb.RoleRef = m.RoleRef + return m.meta.Mutate(rb) +} diff --git a/pkg/resources/rolebinding_test.go b/pkg/resources/rolebinding_test.go index 18d6395..6c633c6 100644 --- a/pkg/resources/rolebinding_test.go +++ b/pkg/resources/rolebinding_test.go @@ -1 +1,88 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("RoleBindingMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + subjects []v1.Subject + roleRef v1.RoleRef + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*v1.RoleBinding] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the rbac/v1 API + scheme = runtime.NewScheme() + Expect(v1.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define subjects, roleRef, labels, and annotations + subjects = []v1.Subject{ + { + Kind: "User", + Name: "test-user", + Namespace: "test-namespace", + }, + } + roleRef = resources.NewRoleRef("test-role") + labels = map[string]string{"key1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a role binding mutator + mutator = resources.NewRoleBindingMutator("test-rolebinding", "test-namespace", subjects, roleRef, labels, annotations) + }) + + It("should create an empty role binding with correct metadata", func() { + roleBinding := mutator.Empty() + + Expect(roleBinding.Name).To(Equal("test-rolebinding")) + Expect(roleBinding.Namespace).To(Equal("test-namespace")) + Expect(roleBinding.APIVersion).To(Equal("rbac.authorization.k8s.io/v1")) + Expect(roleBinding.Kind).To(Equal("RoleBinding")) + }) + + It("should apply subjects and roleRef using Mutate", func() { + roleBinding := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(roleBinding)).To(Succeed()) + + // Verify that the subjects and roleRef are applied + Expect(roleBinding.Subjects).To(Equal(subjects)) + Expect(roleBinding.RoleRef).To(Equal(roleRef)) + }) + + It("should create and retrieve the role binding using the fake client", func() { + roleBinding := mutator.Empty() + Expect(mutator.Mutate(roleBinding)).To(Succeed()) + + // Create the role binding in the fake client + Expect(fakeClient.Create(ctx, roleBinding)).To(Succeed()) + + // Retrieve the role binding from the fake client and verify it + retrievedRoleBinding := &v1.RoleBinding{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-rolebinding", Namespace: "test-namespace"}, retrievedRoleBinding)).To(Succeed()) + Expect(retrievedRoleBinding).To(Equal(roleBinding)) + }) +}) diff --git a/pkg/resources/secret.go b/pkg/resources/secret.go index 18d6395..b6866c2 100644 --- a/pkg/resources/secret.go +++ b/pkg/resources/secret.go @@ -1 +1,58 @@ package resources + +import ( + "fmt" + + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type SecretMutator struct { + Name string + Namespace string + Data map[string][]byte + Type core.SecretType + meta Mutator[client.Object] +} + +var _ Mutator[*core.Secret] = &SecretMutator{} + +func NewSecretMutator(name, namespace string, data map[string][]byte, secretType core.SecretType, labels map[string]string, annotations map[string]string) Mutator[*core.Secret] { + return &SecretMutator{ + Name: name, + Namespace: namespace, + Data: data, + Type: secretType, + meta: NewMetadataMutator(labels, annotations), + } +} + +func (m *SecretMutator) String() string { + return fmt.Sprintf("secret %s/%s", m.Namespace, m.Name) +} + +func (m *SecretMutator) Empty() *core.Secret { + return &core.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: m.Name, + Namespace: m.Namespace, + }, + Data: m.Data, + Type: m.Type, + } +} + +func (m *SecretMutator) Mutate(s *core.Secret) error { + if s.Data == nil { + s.Data = make(map[string][]byte) + } + for key, value := range m.Data { + s.Data[key] = value + } + return m.meta.Mutate(s) +} diff --git a/pkg/resources/secret_test.go b/pkg/resources/secret_test.go index 18d6395..b351132 100644 --- a/pkg/resources/secret_test.go +++ b/pkg/resources/secret_test.go @@ -1 +1,85 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("SecretMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + data map[string][]byte + labels map[string]string + annotations map[string]string + secretType core.SecretType + mutator resources.Mutator[*core.Secret] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the core/v1 API + scheme = runtime.NewScheme() + Expect(core.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define data, labels, annotations, and secret type + data = map[string][]byte{"key1": []byte("value1"), "key2": []byte("value2")} + labels = map[string]string{"label1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + secretType = core.SecretTypeOpaque + + // Create a Secret mutator + mutator = resources.NewSecretMutator("test-secret", "test-namespace", data, secretType, labels, annotations) + }) + + It("should create an empty Secret with correct metadata", func() { + secret := mutator.Empty() + + Expect(secret.Name).To(Equal("test-secret")) + Expect(secret.Namespace).To(Equal("test-namespace")) + Expect(secret.APIVersion).To(Equal("v1")) + Expect(secret.Kind).To(Equal("Secret")) + Expect(secret.Type).To(Equal(secretType)) + Expect(secret.Data).To(Equal(data)) + }) + + It("should apply data, labels, and annotations using Mutate", func() { + secret := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(secret)).To(Succeed()) + + // Verify that the data, labels, and annotations are applied + Expect(secret.Data).To(Equal(data)) + Expect(secret.Labels).To(Equal(labels)) + Expect(secret.Annotations).To(Equal(annotations)) + }) + + It("should create and retrieve the Secret using the fake client", func() { + secret := mutator.Empty() + Expect(mutator.Mutate(secret)).To(Succeed()) + + // Create the Secret in the fake client + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + // Retrieve the Secret from the fake client and verify it + retrievedSecret := &core.Secret{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-secret", Namespace: "test-namespace"}, retrievedSecret)).To(Succeed()) + Expect(retrievedSecret).To(Equal(secret)) + }) +}) diff --git a/pkg/resources/serviceaccount.go b/pkg/resources/serviceaccount.go index 9e3da59..be14eb8 100644 --- a/pkg/resources/serviceaccount.go +++ b/pkg/resources/serviceaccount.go @@ -3,6 +3,8 @@ package resources import ( "fmt" + "sigs.k8s.io/controller-runtime/pkg/client" + core "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -10,11 +12,19 @@ import ( type ServiceAccountMutator struct { Name string Namespace string - Labels map[string]string + meta Mutator[client.Object] } var _ Mutator[*core.ServiceAccount] = &ServiceAccountMutator{} +func NewServiceAccountMutator(name, namespace string, labels map[string]string, annotations map[string]string) Mutator[*core.ServiceAccount] { + return &ServiceAccountMutator{ + Name: name, + Namespace: namespace, + meta: NewMetadataMutator(labels, annotations), + } +} + func (m *ServiceAccountMutator) String() string { return fmt.Sprintf("service account %s/%s", m.Namespace, m.Name) } @@ -33,6 +43,5 @@ func (m *ServiceAccountMutator) Empty() *core.ServiceAccount { } func (m *ServiceAccountMutator) Mutate(s *core.ServiceAccount) error { - s.ObjectMeta.Labels = m.Labels - return nil + return m.meta.Mutate(s) } diff --git a/pkg/resources/serviceaccount_test.go b/pkg/resources/serviceaccount_test.go index 18d6395..4d5ebc1 100644 --- a/pkg/resources/serviceaccount_test.go +++ b/pkg/resources/serviceaccount_test.go @@ -1 +1,78 @@ -package resources +package resources_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/resources" + "github.com/openmcp-project/controller-utils/pkg/testing" +) + +var _ = Describe("ServiceAccountMutator", func() { + var ( + ctx context.Context + fakeClient client.WithWatch + scheme *runtime.Scheme + labels map[string]string + annotations map[string]string + mutator resources.Mutator[*core.ServiceAccount] + ) + + BeforeEach(func() { + ctx = context.TODO() + + // Create a scheme and register the core/v1 API + scheme = runtime.NewScheme() + Expect(core.AddToScheme(scheme)).To(Succeed()) + + // Initialize the fake client + var err error + fakeClient, err = testing.GetFakeClient(scheme) + Expect(err).ToNot(HaveOccurred()) + + // Define labels and annotations + labels = map[string]string{"label1": "value1"} + annotations = map[string]string{"annotation1": "value1"} + + // Create a ServiceAccount mutator + mutator = resources.NewServiceAccountMutator("test-serviceaccount", "test-namespace", labels, annotations) + }) + + It("should create an empty ServiceAccount with correct metadata", func() { + serviceAccount := mutator.Empty() + + Expect(serviceAccount.Name).To(Equal("test-serviceaccount")) + Expect(serviceAccount.Namespace).To(Equal("test-namespace")) + Expect(serviceAccount.APIVersion).To(Equal("v1")) + Expect(serviceAccount.Kind).To(Equal("ServiceAccount")) + }) + + It("should apply labels and annotations using Mutate", func() { + serviceAccount := mutator.Empty() + + // Apply the mutator's Mutate method + Expect(mutator.Mutate(serviceAccount)).To(Succeed()) + + // Verify that the labels and annotations are applied + Expect(serviceAccount.Labels).To(Equal(labels)) + Expect(serviceAccount.Annotations).To(Equal(annotations)) + }) + + It("should create and retrieve the ServiceAccount using the fake client", func() { + serviceAccount := mutator.Empty() + Expect(mutator.Mutate(serviceAccount)).To(Succeed()) + + // Create the ServiceAccount in the fake client + Expect(fakeClient.Create(ctx, serviceAccount)).To(Succeed()) + + // Retrieve the ServiceAccount from the fake client and verify it + retrievedServiceAccount := &core.ServiceAccount{} + Expect(fakeClient.Get(ctx, client.ObjectKey{Name: "test-serviceaccount", Namespace: "test-namespace"}, retrievedServiceAccount)).To(Succeed()) + Expect(retrievedServiceAccount).To(Equal(serviceAccount)) + }) +}) diff --git a/pkg/resources/util.go b/pkg/resources/util.go index 18d6395..121c290 100644 --- a/pkg/resources/util.go +++ b/pkg/resources/util.go @@ -1 +1,21 @@ package resources + +import v1 "k8s.io/api/rbac/v1" + +// NewClusterRoleRef creates a RoleRef for a ClusterRole. +func NewClusterRoleRef(name string) v1.RoleRef { + return v1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: name, + } +} + +// NewRoleRef creates a RoleRef for a Role. +func NewRoleRef(name string) v1.RoleRef { + return v1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: name, + } +} From acd1b3f2715052a6262d8f537a567139f2460634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Sch=C3=BCnemann?= Date: Wed, 7 May 2025 13:45:28 +0200 Subject: [PATCH 3/3] add readme section --- README.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/README.md b/README.md index cdeb7d3..da702cf 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,82 @@ env := testing.NewEnvironmentBuilder(). env.ShouldReconcile(testing.RequestFromStrings("testresource")) ``` +### Kubernetes resource management + +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. +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. + +#### Examples + +Create or update a `ConfigMap`, a `ServiceAccount` and a `Deployment` using the `Mutator` interface: + +```go +type myDeploymentMutator struct { +} + +var _ resource.Mutator[*appsv1.Deployment] = &myDeploymentMutator{} + +func newDeploymentMutator() resources.Mutator[*appsv1.Deployment] { + return &MyDeploymentMutator{} +} + +func (m *MyDeploymentMutator) String() string { + return "deployment default/test" +} + +func (m *MyDeploymentMutator) Empty() *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "default", + }, + } +} + +func (m *MyDeploymentMutator) Mutate(deployment *appsv1.Deployment) error { + // create one container with an image + deployment.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: "test", + Image: "test-image:latest", + }, + } + return nil +} + + +func ReconcileResources(ctx context.Context, client client.Client) error { + configMapResource := resource.NewConfigMap("my-configmap", "my-namespace", map[string]string{) + "label1": "value1", + "label2": "value2", + }, nil) + + serviceAccountResource := resource.NewServiceAccount("my-serviceaccount", "my-namespace", nil, nil) + + myDeploymentMutator := newDeploymentMutator() + + var err error + + err = resources.CreateOrUpdateResource(ctx, client, configMapResource) + if err != nil { + return err + } + + resources.CreateOrUpdateResource(ctx, client, serviceAccountResource) + if err != nil { + return err + } + + err = resources.CreateOrUpdateResource(ctx, client, myDeploymentMutator) + if err != nil { + return err + } + + return nil +} + +``` + ## Support, Feedback, Contributing 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).