Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/last updated by annotation #138

Merged
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
6 changes: 5 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,15 @@ func main() {

hookServer := mgr.GetWebhookServer()
decoder := admission.NewDecoder(scheme)
hookServer.Register(rcswebhooks.ServingPath, &webhook.Admission{Handler: &rcswebhooks.CappValidator{
hookServer.Register(rcswebhooks.ValidatorServingPath, &webhook.Admission{Handler: &rcswebhooks.CappValidator{
Client: mgr.GetClient(),
Decoder: decoder,
}})

hookServer.Register(rcswebhooks.MutatorServingPath, &webhook.Admission{Handler: &rcswebhooks.CappMutator{
Decoder: decoder,
}})

//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
27 changes: 27 additions & 0 deletions config/webhook/manifests.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
- v1
- v1beta1
clientConfig:
service:
name: webhook-service
namespace: system
path: /mutate-capp
failurePolicy: Fail
name: capp.dana.io
rules:
- apiGroups:
- rcs.dana.io
apiVersions:
- v1alpha1
operations:
- CREATE
- UPDATE
resources:
- capps
sideEffects: NoneOnDryRun
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: validating-webhook-configuration
Expand Down
51 changes: 51 additions & 0 deletions internal/webhooks/capp_mutator_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package webhooks

import (
"context"
"encoding/json"
"net/http"

cappv1alpha1 "github.com/dana-team/container-app-operator/api/v1alpha1"

"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

type CappMutator struct {
Decoder *admission.Decoder
}

// +kubebuilder:webhook:path=/mutate-capp,mutating=true,sideEffects=NoneOnDryRun,failurePolicy=fail,groups=rcs.dana.io,resources=capps,verbs=create;update,versions=v1alpha1,name=capp.dana.io,admissionReviewVersions=v1;v1beta1

const (
LastUpdatedByAnnotationKey = "rcs.dana.io/last-updated-by"
MutatorServingPath = "/mutate-capp"
)

// Handle implements the mutation webhook.
func (c *CappMutator) Handle(ctx context.Context, req admission.Request) admission.Response {
logger := log.FromContext(ctx).WithValues("mutation webhook", "capp mutation Webhook", "Name", req.Name)
capp := cappv1alpha1.Capp{}
if err := c.Decoder.DecodeRaw(req.Object, &capp); err != nil {
logger.Error(err, "could not decode capp object")
return admission.Errored(http.StatusBadRequest, err)
}

c.handleInner(&capp, req)

marshaledCapp, err := json.Marshal(capp)
arielsepton marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}

return admission.PatchResponseFromRaw(req.Object.Raw, marshaledCapp)
}

// handleInner sets an annotation on the Capp object to track the last user who updated it.
func (c *CappMutator) handleInner(capp *cappv1alpha1.Capp, req admission.Request) {
if capp.ObjectMeta.Annotations == nil {
capp.ObjectMeta.Annotations = make(map[string]string)
}

capp.ObjectMeta.Annotations[LastUpdatedByAnnotationKey] = req.UserInfo.Username
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type CappValidator struct {

// +kubebuilder:webhook:path=/validate-capp,mutating=false,sideEffects=NoneOnDryRun,failurePolicy=fail,groups="rcs.dana.io",resources=capps,verbs=create;update,versions=v1alpha1,name=capp.validate.rcs.dana.io,admissionReviewVersions=v1;v1beta1

const ServingPath = "/validate-capp"
const ValidatorServingPath = "/validate-capp"

func (c *CappValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
logger := log.FromContext(ctx).WithValues("webhook", "capp Webhook", "Name", req.Name)
Expand Down
86 changes: 56 additions & 30 deletions test/e2e_tests/mocks/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,47 +63,73 @@ func CreateSecret() *corev1.Secret {
}
}

// CreateRole creates a role with basic permissions for pod logs.
func CreateRole() *rbacv1.Role {
// CreateCappRole creates a role with basic permissions for pod logs.
func CreateCappRole() *rbacv1.Role {
rules := []rbacv1.PolicyRule{
{
Resources: []string{
"pod/logs",
},
APIGroups: []string{
"",
},
Verbs: []string{
"get", "watch", "list",
},
},
}

return CreateRole(CappAdmin+"-role", rules)
}

// CreateCappRoleBinding creates a binding for the pod reader role.
func CreateCappRoleBinding() *rbacv1.RoleBinding {
roleRef := rbacv1.RoleRef{
Name: CappAdmin + "-role",
Kind: "Role",
APIGroup: "rbac.authorization.k8s.io",
}

subjects := []rbacv1.Subject{
{
Kind: "User",
Name: CappAdmin,
APIGroup: "rbac.authorization.k8s.io",
},
}

return CreateRoleBinding(CappAdmin+"-role-binding", roleRef, subjects)
}

// CreateRole creates a role with the specified name and rules.
func CreateRole(name string, rules []rbacv1.PolicyRule) *rbacv1.Role {
return &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: CappAdmin + "-role",
Name: name,
Namespace: NSName,
},
Rules: []rbacv1.PolicyRule{
{
Resources: []string{
"pod/logs",
},
APIGroups: []string{
"",
},
Verbs: []string{
"get", "watch", "list",
},
},
},
Rules: rules,
}
}

// CreateRoleBinding creates a binding for the pod reader role.
func CreateRoleBinding() *rbacv1.RoleBinding {
// CreateRoleBinding creates a role binding with the specified name, role reference, and subjects.
func CreateRoleBinding(name string, roleRef rbacv1.RoleRef, subjects []rbacv1.Subject) *rbacv1.RoleBinding {
return &rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: CappAdmin + "-role-binding",
Name: name,
Namespace: NSName,
},
RoleRef: rbacv1.RoleRef{
Name: CappAdmin + "-role",
Kind: "Role",
APIGroup: "rbac.authorization.k8s.io",
},
Subjects: []rbacv1.Subject{
{
Kind: "User",
Name: CappAdmin,
APIGroup: "rbac.authorization.k8s.io",
},
RoleRef: roleRef,
Subjects: subjects,
}
}

// CreateServiceAccount creates a service account with the specified name.
func CreateServiceAccount(name string) *corev1.ServiceAccount {
return &corev1.ServiceAccount{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: NSName,
},
}
}
49 changes: 49 additions & 0 deletions test/e2e_tests/mutating.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package e2e_tests

import (
"fmt"

"github.com/dana-team/rcs-ocm-deployer/internal/webhooks"
mock "github.com/dana-team/rcs-ocm-deployer/test/e2e_tests/mocks"
utilst "github.com/dana-team/rcs-ocm-deployer/test/e2e_tests/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

const (
adminAnnotationValue = "kubernetes-admin"
)

var _ = Describe("Validate the mutating webhook", func() {
It("Should add annotation on create", func() {
baseCapp := mock.CreateBaseCapp()
capp := utilst.CreateCapp(k8sClient, baseCapp)

annotation := capp.ObjectMeta.Annotations[webhooks.LastUpdatedByAnnotationKey]
Expect(annotation).To(Equal(adminAnnotationValue))
})

It("Should add annotation on update", func() {
baseCapp := mock.CreateBaseCapp()
capp := utilst.CreateCapp(k8sClient, baseCapp)

annotation := capp.ObjectMeta.Annotations[webhooks.LastUpdatedByAnnotationKey]
Expect(annotation).To(Equal(adminAnnotationValue))

utilst.SwitchUser(&k8sClient, cfg, mock.NSName, newScheme(), true)
capp = utilst.GetCapp(k8sClient, capp.Name, capp.Namespace)
capp.ObjectMeta.Annotations["test"] = "test"
utilst.UpdateCapp(k8sClient, capp)

updatedCapp := utilst.GetCapp(k8sClient, capp.Name, capp.Namespace)

// Check if the annotation has changed
updatedAnnotation := updatedCapp.ObjectMeta.Annotations[webhooks.LastUpdatedByAnnotationKey]
Expect(updatedAnnotation).To(Equal(fmt.Sprintf(utilst.ServiceAccountNameFormat, mock.NSName, utilst.ServiceAccountName)))
})

AfterEach(func() {
// Revert k8sClient back to use the original configuration
utilst.SwitchUser(&k8sClient, cfg, mock.NSName, newScheme(), false)
})
})
7 changes: 6 additions & 1 deletion test/e2e_tests/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
knativev1 "knative.dev/serving/pkg/apis/serving/v1"
knativev1alphav1 "knative.dev/serving/pkg/apis/serving/v1alpha1"
clusterv1 "open-cluster-management.io/api/cluster/v1"
Expand All @@ -29,6 +30,7 @@ import (
)

var k8sClient client.Client
var cfg *rest.Config

func newScheme() *runtime.Scheme {
s := runtime.NewScheme()
Expand Down Expand Up @@ -59,17 +61,20 @@ var _ = SynchronizedBeforeSuite(func() {
initClient()
cleanUp()
createE2ETestNamespace()
utilst.CreateTestUser(k8sClient, mock.NSName)
arielsepton marked this conversation as resolved.
Show resolved Hide resolved
}, func() {
initClient()
})

var _ = SynchronizedAfterSuite(func() {}, func() {
utilst.DeleteTestUser(k8sClient, mock.NSName)
cleanUp()
})

// initClient initializes a k8s client.
func initClient() {
cfg, err := config.GetConfig()
var err error
cfg, err = config.GetConfig()
Expect(err).NotTo(HaveOccurred())

k8sClient, err = client.New(cfg, client.Options{Scheme: newScheme()})
Expand Down
4 changes: 2 additions & 2 deletions test/e2e_tests/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,9 @@ var _ = Describe("Validate the placement sync controller", func() {
})

It("Should copy the rolebindings from capp's namespace to ManifestWork ", func() {
baseRole := mock.CreateRole()
baseRole := mock.CreateCappRole()
role := utilst.CreateRole(k8sClient, baseRole)
baseRoleBinding := mock.CreateRoleBinding()
baseRoleBinding := mock.CreateCappRoleBinding()
roleBinding := utilst.CreateRoleBinding(k8sClient, baseRoleBinding)
baseCapp := mock.CreateBaseCapp()
desiredCapp := utilst.CreateCapp(k8sClient, baseCapp)
Expand Down
5 changes: 5 additions & 0 deletions test/e2e_tests/utils/capp_adpater.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ func GetCapp(k8sClient client.Client, name string, namespace string) *cappv1alph
return capp
}

// UpdateCapp updates the provided Capp instance in the Kubernetes cluster, and returns it.
func UpdateCapp(k8sClient client.Client, capp *cappv1alpha1.Capp) {
Expect(k8sClient.Update(context.Background(), capp)).To(Succeed())
}

// DoesFinalizerExist checks if a finalizer exists on a Capp.
func DoesFinalizerExist(k8sClient client.Client, cappName string, cappNamespace string, finalizerName string) bool {
capp := GetCapp(k8sClient, cappName, cappNamespace)
Expand Down
Loading
Loading