Skip to content

Commit

Permalink
Merge pull request #138 from arielsepton/feature/last-updated-by-anno…
Browse files Browse the repository at this point in the history
…tation

Feature/last updated by annotation
  • Loading branch information
dana-prow-ci[bot] committed Apr 18, 2024
2 parents 53f52aa + 67e2817 commit a95b1a2
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 35 deletions.
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)
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)
}, 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

0 comments on commit a95b1a2

Please sign in to comment.