Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,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).
Expand Down
47 changes: 47 additions & 0 deletions pkg/resources/clusterrole.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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
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)
}

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.Rules = m.Rules
return m.meta.Mutate(r)
}
84 changes: 84 additions & 0 deletions pkg/resources/clusterrole_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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))
})
})
50 changes: 50 additions & 0 deletions pkg/resources/clusterrolebinding.go
Original file line number Diff line number Diff line change
@@ -0,0 +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 ClusterRoleBindingMutator struct {
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)
}

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.RoleRef = m.RoleRef
r.Subjects = m.Subjects
return m.meta.Mutate(r)
}
87 changes: 87 additions & 0 deletions pkg/resources/clusterrolebinding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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))
})
})
Loading