diff --git a/.golangci.yaml b/.golangci.yaml index 987dfe36..8f2eab56 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -16,3 +16,6 @@ linters: issues: max-issues-per-linter: 0 max-same-issues: 0 + exclude: + - "^SA1019: pkg.NamespaceValidationEnabled is deprecated:" + - "^SA1019: warden.NamespaceValidationEnabled is deprecated:" diff --git a/cmd/admission/main.go b/cmd/admission/main.go index 90d471c5..1a6b6282 100644 --- a/cmd/admission/main.go +++ b/cmd/admission/main.go @@ -4,13 +4,13 @@ import ( "context" "flag" "fmt" - "os" - "github.com/kyma-project/warden/internal/env" "github.com/kyma-project/warden/internal/logging" + "github.com/kyma-project/warden/internal/validate" "github.com/kyma-project/warden/internal/webhook" "go.uber.org/zap/zapcore" "k8s.io/apimachinery/pkg/fields" + "os" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -18,7 +18,6 @@ import ( "github.com/go-logr/zapr" "github.com/kyma-project/warden/internal/admission" "github.com/kyma-project/warden/internal/config" - "github.com/kyma-project/warden/internal/validate" "github.com/kyma-project/warden/internal/webhook/certs" "go.uber.org/zap" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" @@ -151,15 +150,8 @@ func main() { os.Exit(5) } - repoFactory := validate.NotaryRepoFactory{Timeout: appConfig.Notary.Timeout} - allowedRegistries := validate.ParseAllowedRegistries(appConfig.Notary.AllowedRegistries) - - validatorSvcConfig := validate.ServiceConfig{ - NotaryConfig: validate.NotaryConfig{Url: appConfig.Notary.URL}, - AllowedRegistries: allowedRegistries, - } - podValidatorSvc := validate.NewImageValidator(&validatorSvcConfig, repoFactory) - validatorSvc := validate.NewPodValidator(podValidatorSvc) + validatorSvc := validate.NewValidatorSvcFactory().NewValidatorSvc( + appConfig.Notary.URL, appConfig.Notary.AllowedRegistries, appConfig.Notary.Timeout) logger.Info("setting up webhook server") // webhook server setup @@ -170,7 +162,10 @@ func main() { }) whs.Register(admission.DefaultingPath, &ctrlwebhook.Admission{ - Handler: admission.NewDefaultingWebhook(mgr.GetClient(), validatorSvc, appConfig.Admission.Timeout, appConfig.Admission.StrictMode, decoder, logger.With("webhook", "defaulting")), + Handler: admission.NewDefaultingWebhook(mgr.GetClient(), + validatorSvc, validate.NewValidatorSvcFactory(), + appConfig.Admission.Timeout, appConfig.Admission.StrictMode, + decoder, logger.With("webhook", "defaulting")), }) logger.Info("starting the controller-manager") diff --git a/cmd/operator/main.go b/cmd/operator/main.go index 9d83af44..28763b0d 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -132,6 +132,7 @@ func main() { mgr.GetClient(), mgr.GetScheme(), podValidator, + validate.NewValidatorSvcFactory(), controllers.PodReconcilerConfig{RequeueAfter: appConfig.Operator.PodReconcilerRequeueAfter}, logger.Named("pod-controller"), )).SetupWithManager(mgr); err != nil { diff --git a/internal/admission/defaulting.go b/internal/admission/defaulting.go index 44669192..8c98a705 100644 --- a/internal/admission/defaulting.go +++ b/internal/admission/defaulting.go @@ -27,22 +27,27 @@ const ( const PodType = "Pod" type DefaultingWebHook struct { - validationSvc validate.PodValidator - timeout time.Duration - client k8sclient.Client - decoder *admission.Decoder - baseLogger *zap.SugaredLogger - strictMode bool + systemValidator validate.PodValidator + userValidationSvcFactory validate.ValidatorSvcFactory + timeout time.Duration + client k8sclient.Client + decoder *admission.Decoder + baseLogger *zap.SugaredLogger + strictMode bool } -func NewDefaultingWebhook(client k8sclient.Client, ValidationSvc validate.PodValidator, timeout time.Duration, strictMode bool, decoder *admission.Decoder, logger *zap.SugaredLogger) *DefaultingWebHook { +func NewDefaultingWebhook(client k8sclient.Client, + systemValidator validate.PodValidator, userValidationSvcFactory validate.ValidatorSvcFactory, + timeout time.Duration, strictMode bool, + decoder *admission.Decoder, logger *zap.SugaredLogger) *DefaultingWebHook { return &DefaultingWebHook{ - client: client, - validationSvc: ValidationSvc, - baseLogger: logger, - timeout: timeout, - strictMode: strictMode, - decoder: decoder, + client: client, + systemValidator: systemValidator, + userValidationSvcFactory: userValidationSvcFactory, + baseLogger: logger, + timeout: timeout, + strictMode: strictMode, + decoder: decoder, } } @@ -76,19 +81,28 @@ func (w *DefaultingWebHook) handle(ctx context.Context, req admission.Request) a return result } - result, err := w.validationSvc.ValidatePod(ctx, pod, ns) + validator := w.systemValidator + if validate.IsUserValidationForNS(ns) { + var err error + validator, err = validate.NewUserValidationSvc(ns, w.userValidationSvcFactory) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + } + + result, err := validator.ValidatePod(ctx, pod, ns) if err != nil { return admission.Errored(http.StatusInternalServerError, err) } if result.Status == validate.NoAction { return admission.Allowed("validation is not enabled for pod") } - res := w.createResponse(ctx, req, result, pod, logger) + res := w.createResponse(ctx, req, result, pod, ns, logger) return res } func cleanAnnotationIfNeeded(ctx context.Context, pod *corev1.Pod, ns *corev1.Namespace, req admission.Request) admission.Response { - if enabled := isValidationEnabledForNS(ns); !enabled { + if enabled := validate.IsValidationEnabledForNS(ns); !enabled { return admission.Allowed("validation is not needed for pod") } if removed := removeInternalAnnotation(ctx, pod.ObjectMeta.Annotations); removed { @@ -110,13 +124,31 @@ func (w DefaultingWebHook) handleTimeout(ctx context.Context, timeoutErr error, msg := fmt.Sprintf("request exceeded desired timeout: %s, reason: %s", w.timeout.String(), timeoutErr.Error()) logger := helpers.LoggerFromCtx(ctx) logger.Info(msg) - res := w.createResponse(ctx, req, validate.ValidationResult{Status: validate.ServiceUnavailable}, pod, logger) + + ns := &corev1.Namespace{} + if err := w.client.Get(ctx, k8sclient.ObjectKey{Name: pod.Namespace}, ns); err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + res := w.createResponse(ctx, req, validate.ValidationResult{Status: validate.ServiceUnavailable}, pod, ns, logger) res.Result = &metav1.Status{Message: msg} return res } -func (w *DefaultingWebHook) createResponse(ctx context.Context, req admission.Request, result validate.ValidationResult, pod *corev1.Pod, logger *zap.SugaredLogger) admission.Response { - markedPod := markPod(ctx, result, pod, w.strictMode) +func (w *DefaultingWebHook) createResponse(ctx context.Context, + req admission.Request, result validate.ValidationResult, + pod *corev1.Pod, ns *corev1.Namespace, logger *zap.SugaredLogger) admission.Response { + + strictMode := w.strictMode + if validate.IsUserValidationForNS(ns) { + var err error + strictMode, err = helpers.GetUserValidationStrictMode(ns) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + } + + markedPod := markPod(ctx, result, pod, strictMode) fBytes, err := json.Marshal(markedPod) if err != nil { return admission.Errored(http.StatusInternalServerError, err) @@ -128,7 +160,7 @@ func (w *DefaultingWebHook) createResponse(ctx context.Context, req admission.Re func isValidationNeeded(ctx context.Context, pod *corev1.Pod, ns *corev1.Namespace, operation admissionv1.Operation) bool { logger := helpers.LoggerFromCtx(ctx) - if enabled := isValidationEnabledForNS(ns); !enabled { + if enabled := validate.IsValidationEnabledForNS(ns); !enabled { logger.Debugw("pod validation skipped because validation for namespace is not enabled") return false } @@ -146,10 +178,6 @@ func IsValidationNeededForOperation(operation admissionv1.Operation) bool { return operation == admissionv1.Create } -func isValidationEnabledForNS(ns *corev1.Namespace) bool { - return ns.GetLabels()[pkg.NamespaceValidationLabel] == pkg.NamespaceValidationEnabled -} - func isValidationEnabledForPodValidationLabel(pod *corev1.Pod) bool { validationLabelValue := getPodValidationLabelValue(pod) if validationLabelValue == pkg.ValidationStatusFailed || validationLabelValue == pkg.ValidationStatusPending { diff --git a/internal/admission/defaulting_test.go b/internal/admission/defaulting_test.go index ae715f46..a4b1ead8 100644 --- a/internal/admission/defaulting_test.go +++ b/internal/admission/defaulting_test.go @@ -4,8 +4,11 @@ import ( "context" "encoding/json" "errors" + "github.com/kyma-project/warden/internal/helpers" + "k8s.io/utils/ptr" "net/http" "net/http/httptest" + "strconv" "testing" "time" @@ -41,22 +44,19 @@ func TestTimeout(t *testing.T) { timeout := time.Millisecond * 100 testNs := "test-namespace" - ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNs, Labels: map[string]string{ - pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled, - }}} + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNs, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Image: "test:test"}}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, } raw, err := json.Marshal(pod) require.NoError(t, err) - req := admission.Request{ - AdmissionRequest: admissionv1.AdmissionRequest{ - Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, - Object: runtime.RawExtension{Raw: raw}, - }} + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() t.Run("Success", func(t *testing.T) { @@ -66,7 +66,8 @@ func TestTimeout(t *testing.T) { After(timeout/2). Return(validate.ValidationResult{Status: validate.Valid}, nil).Once() defer validationSvc.AssertExpectations(t) - webhook := NewDefaultingWebhook(client, validationSvc, timeout, StrictModeOff, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + validationSvc, nil, timeout, StrictModeOff, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -83,7 +84,8 @@ func TestTimeout(t *testing.T) { After(timeout*2). Return(validate.ValidationResult{Status: validate.Valid}, nil).Once() defer validationSvc.AssertExpectations(t) - webhook := NewDefaultingWebhook(client, validationSvc, timeout, StrictModeOff, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + validationSvc, nil, timeout, StrictModeOff, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -101,7 +103,8 @@ func TestTimeout(t *testing.T) { After(timeout*2). Return(validate.ValidationResult{Status: validate.Valid}, nil).Once() defer validationSvc.AssertExpectations(t) - webhook := NewDefaultingWebhook(client, validationSvc, timeout, StrictModeOn, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + validationSvc, nil, timeout, StrictModeOn, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -124,7 +127,8 @@ func TestTimeout(t *testing.T) { validateImage := validate.NewImageValidator(&validate.ServiceConfig{NotaryConfig: validate.NotaryConfig{Url: srv.URL}}, validate.NotaryRepoFactory{}) validationSvc := validate.NewPodValidator(validateImage) - webhook := NewDefaultingWebhook(client, validationSvc, timeout, StrictModeOff, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + validationSvc, nil, timeout, StrictModeOff, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -147,21 +151,20 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { timeout := time.Second nsName := "test-namespace" - ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, Labels: map[string]string{ - pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled, - }}} t.Run("when valid image should return success", func(t *testing.T) { //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} mockImageValidator := mocks.ImageValidatorService{} - mockImageValidator.Mock.On("Validate", mock.Anything, "test:test"). - Return(nil) + mockImageValidator.Mock.On("Validate", mock.Anything, "test:test").Return(nil) mockPodValidator := validate.NewPodValidator(&mockImageValidator) pod := newPodFix(nsName, nil) req := newRequestFix(t, pod, admissionv1.Create) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() - webhook := NewDefaultingWebhook(client, mockPodValidator, timeout, false, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + mockPodValidator, nil, timeout, false, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -175,16 +178,18 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { t.Run("when valid image with annotation reject should return success and remove the annotation", func(t *testing.T) { //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} mockImageValidator := mocks.ImageValidatorService{} - mockImageValidator.Mock.On("Validate", mock.Anything, "test:test"). - Return(nil) + mockImageValidator.Mock.On("Validate", mock.Anything, "test:test").Return(nil) mockPodValidator := validate.NewPodValidator(&mockImageValidator) pod := newPodFix(nsName, nil) pod.ObjectMeta.Annotations = map[string]string{annotations.PodValidationRejectAnnotation: annotations.ValidationReject} req := newRequestFix(t, pod, admissionv1.Create) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() - webhook := NewDefaultingWebhook(client, mockPodValidator, timeout, false, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + mockPodValidator, nil, timeout, false, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -198,11 +203,14 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { t.Run("when pod labeled by ns controller with pending label and annotation reject should remove the annotation", func(t *testing.T) { //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} pod := newPodFix(nsName, map[string]string{pkg.PodValidationLabel: pkg.ValidationStatusPending}) pod.ObjectMeta.Annotations = map[string]string{annotations.PodValidationRejectAnnotation: annotations.ValidationReject} req := newRequestFix(t, pod, admissionv1.Update) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() - webhook := NewDefaultingWebhook(client, nil, timeout, false, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + nil, nil, timeout, false, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -215,6 +223,8 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { t.Run("when invalid image should return failed and annotation reject with images list", func(t *testing.T) { //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} mockImageValidator := mocks.ImageValidatorService{} mockImageValidator.Mock.On("Validate", mock.Anything, "test:test"). Return(pkg.NewValidationFailedErr(errors.New("validation failed"))) @@ -223,7 +233,8 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { pod := newPodFix(nsName, nil) req := newRequestFix(t, pod, admissionv1.Create) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() - webhook := NewDefaultingWebhook(client, mockPodValidator, timeout, false, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + mockPodValidator, nil, timeout, false, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -237,6 +248,8 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { t.Run("when service unavailable and strict mode on should return pending and annotation reject", func(t *testing.T) { //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} mockPodValidator := mocks.NewPodValidator(t) mockPodValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). Return(validate.ValidationResult{Status: validate.ServiceUnavailable}, nil) @@ -245,7 +258,8 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { pod := newPodFix(nsName, nil) req := newRequestFix(t, pod, admissionv1.Create) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() - webhook := NewDefaultingWebhook(client, mockPodValidator, timeout, StrictModeOn, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + mockPodValidator, nil, timeout, StrictModeOn, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -259,6 +273,8 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { t.Run("when service unavailable and strict mode off should return pending", func(t *testing.T) { //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} mockPodValidator := mocks.NewPodValidator(t) mockPodValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). Return(validate.ValidationResult{Status: validate.ServiceUnavailable}, nil) @@ -267,7 +283,88 @@ func TestFlow_OutputStatuses_ForPodValidationResult(t *testing.T) { pod := newPodFix(nsName, nil) req := newRequestFix(t, pod, admissionv1.Create) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() - webhook := NewDefaultingWebhook(client, mockPodValidator, timeout, StrictModeOff, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + mockPodValidator, nil, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + require.Nil(t, res.Result) + require.True(t, res.AdmissionResponse.Allowed) + require.ElementsMatch(t, patchWithAddLabel(pkg.ValidationStatusPending), res.Patches) + }) + + t.Run("when service unavailable and strict mode on for user validation should return pending and annotation reject", func(t *testing.T) { + //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser}, + Annotations: map[string]string{ + pkg.NamespaceStrictModeAnnotation: strconv.FormatBool(true), + pkg.NamespaceNotaryURLAnnotation: "notary"}, + }} + + systemValidator := mocks.NewPodValidator(t) + systemValidator.AssertNotCalled(t, "ValidatePod") + defer systemValidator.AssertExpectations(t) + + userValidator := mocks.NewPodValidator(t) + userValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.ServiceUnavailable}, nil).Once() + defer userValidator.AssertExpectations(t) + + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.On("NewValidatorSvc", mock.Anything, mock.Anything, mock.Anything). + Return(userValidator).Once() + defer userValidatorFactory.AssertExpectations(t) + + pod := newPodFix(nsName, nil) + req := newRequestFix(t, pod, admissionv1.Create) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + webhook := NewDefaultingWebhook(client, + systemValidator, userValidatorFactory, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + require.Nil(t, res.Result) + require.True(t, res.AdmissionResponse.Allowed) + require.ElementsMatch(t, withAddRejectAnnotation(patchWithAddLabel(pkg.ValidationStatusPending)), res.Patches) + }) + + t.Run("when service unavailable and strict mode off for user validation should return pending", func(t *testing.T) { + //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser}, + Annotations: map[string]string{ + pkg.NamespaceStrictModeAnnotation: strconv.FormatBool(false), + pkg.NamespaceNotaryURLAnnotation: "notary"}, + }} + + systemValidator := mocks.NewPodValidator(t) + systemValidator.AssertNotCalled(t, "ValidatePod") + defer systemValidator.AssertExpectations(t) + + userValidator := mocks.NewPodValidator(t) + userValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.ServiceUnavailable}, nil).Once() + defer userValidator.AssertExpectations(t) + + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.On("NewValidatorSvc", mock.Anything, mock.Anything, mock.Anything). + Return(userValidator).Once() + defer userValidatorFactory.AssertExpectations(t) + + pod := newPodFix(nsName, nil) + req := newRequestFix(t, pod, admissionv1.Create) + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + webhook := NewDefaultingWebhook(client, + systemValidator, userValidatorFactory, timeout, StrictModeOn, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -288,9 +385,8 @@ func TestFlow_SomeInputStatuses_ShouldCallPodValidation(t *testing.T) { decoder := admission.NewDecoder(scheme) nsName := "test-namespace" - ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, Labels: map[string]string{ - pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled, - }}} + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: nsName, + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}} type want struct { shouldCallValidate bool @@ -404,7 +500,8 @@ func TestFlow_SomeInputStatuses_ShouldCallPodValidation(t *testing.T) { req := newRequestFix(t, pod, tt.operation) client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() timeout := time.Second - webhook := NewDefaultingWebhook(client, mockPodValidator, timeout, false, decoder, logger.Sugar()) + webhook := NewDefaultingWebhook(client, + mockPodValidator, nil, timeout, false, decoder, logger.Sugar()) //WHEN res := webhook.Handle(context.TODO(), req) @@ -413,16 +510,499 @@ func TestFlow_SomeInputStatuses_ShouldCallPodValidation(t *testing.T) { require.NotNil(t, res) require.True(t, res.AdmissionResponse.Allowed) + expectedValidateCalls := 0 if tt.want.shouldCallValidate { - mockImageValidator.AssertNumberOfCalls(t, "Validate", 1) - } else { - mockImageValidator.AssertNumberOfCalls(t, "Validate", 0) + expectedValidateCalls = 1 } + mockImageValidator.AssertNumberOfCalls(t, "Validate", expectedValidateCalls) require.Equal(t, tt.want.patches, res.Patches) }) } } +func TestFlow_NamespaceLabelsValidation(t *testing.T) { + //GIVEN + logger := zap.NewNop() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + decoder := admission.NewDecoder(scheme) + timeout := time.Millisecond * 100 + + testNs := "test-namespace" + + testsSkipValidation := []struct { + name string + namespaceLabels map[string]string + }{ + { + name: "Namespace without labels - validation is not needed", + namespaceLabels: map[string]string{}, + }, + { + name: "Namespace with unknown value of validation label - validation is not needed", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: "unknown"}, + }, + } + for _, tt := range testsSkipValidation { + t.Run(tt.name, func(t *testing.T) { + //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNs, Labels: tt.namespaceLabels}} + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + } + + raw, err := json.Marshal(pod) + require.NoError(t, err) + + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + + validationSvc := mocks.NewPodValidator(t) + validationSvc.AssertNotCalled(t, "ValidatePod") + defer validationSvc.AssertExpectations(t) + webhook := NewDefaultingWebhook(client, + validationSvc, nil, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + require.NotNil(t, res.Result) + assert.Contains(t, res.Result.Message, "validation is not needed for pod") + assert.True(t, res.Allowed) + }) + } + + testsWithValidation := []struct { + name string + namespaceValidationLabelValue string + }{ + { + name: "Namespace with enabled validation - validation is needed", + namespaceValidationLabelValue: pkg.NamespaceValidationEnabled, + }, + { + name: "Namespace with enabled (system) validation - validation is needed", + namespaceValidationLabelValue: pkg.NamespaceValidationSystem, + }, + { + name: "Namespace with enabled (user) validation - validation is needed", + namespaceValidationLabelValue: pkg.NamespaceValidationUser, + }, + } + for _, tt := range testsWithValidation { + t.Run(tt.name, func(t *testing.T) { + //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: testNs, + Labels: map[string]string{pkg.NamespaceValidationLabel: tt.namespaceValidationLabelValue}, + Annotations: map[string]string{pkg.NamespaceNotaryURLAnnotation: "notary"}, + }} + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + } + + raw, err := json.Marshal(pod) + require.NoError(t, err) + + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + + podValidatorCallCount := 0 + + systemValidator := mocks.NewPodValidator(t) + systemValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.Valid}, nil). + Run(func(args mock.Arguments) { + podValidatorCallCount++ + }).Maybe() + defer systemValidator.AssertExpectations(t) + + userValidator := mocks.NewPodValidator(t) + userValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.Valid}, nil). + Run(func(args mock.Arguments) { + podValidatorCallCount++ + }).Maybe() + defer userValidator.AssertExpectations(t) + + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.On("NewValidatorSvc", mock.Anything, mock.Anything, mock.Anything). + Return(userValidator).Maybe() + defer userValidatorFactory.AssertExpectations(t) + + webhook := NewDefaultingWebhook(client, + systemValidator, userValidatorFactory, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + require.Nil(t, res.Result) + assert.True(t, res.Allowed) + // we expect that only one of the validators will be called + assert.Equal(t, 1, podValidatorCallCount) + }) + } +} + +func TestFlow_UseSystemOrUserValidator(t *testing.T) { + //GIVEN + logger := zap.NewNop() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + decoder := admission.NewDecoder(scheme) + timeout := time.Millisecond * 100 + + testNs := "test-namespace" + + t.Run("Namespace with system validation", func(t *testing.T) { + //GIVEN + namespaceLabels := map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationSystem} + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNs, Labels: namespaceLabels}} + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + } + + raw, err := json.Marshal(pod) + require.NoError(t, err) + + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + + // system validator should be called + validationSvc := mocks.NewPodValidator(t) + validationSvc.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.Valid}, nil).Once() + defer validationSvc.AssertExpectations(t) + + // user validator (factory) should not be called + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.AssertNotCalled(t, "NewValidatorSvc") + defer userValidatorFactory.AssertExpectations(t) + + webhook := NewDefaultingWebhook(client, + validationSvc, userValidatorFactory, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + require.Nil(t, res.Result) + assert.True(t, res.Allowed) + }) + + t.Run("Namespace with user validation", func(t *testing.T) { + //GIVEN + namespaceLabels := map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser} + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNs, + Labels: namespaceLabels, + Annotations: map[string]string{pkg.NamespaceNotaryURLAnnotation: "notary"}}, + } + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + } + + raw, err := json.Marshal(pod) + require.NoError(t, err) + + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + + // system validator should not be called + systemValidator := mocks.NewPodValidator(t) + systemValidator.AssertNotCalled(t, "ValidatePod") + defer systemValidator.AssertExpectations(t) + + // user validator and its factory should be called exactly once + userValidator := mocks.NewPodValidator(t) + userValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.Valid}, nil).Once() + defer userValidator.AssertExpectations(t) + + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.On("NewValidatorSvc", mock.Anything, mock.Anything, mock.Anything). + Return(userValidator).Once() + defer userValidatorFactory.AssertExpectations(t) + + webhook := NewDefaultingWebhook(client, + systemValidator, userValidatorFactory, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + require.Nil(t, res.Result) + assert.True(t, res.Allowed) + }) +} + +func TestFlow_UserValidatorGetValuesFromNamespaceAnnotations(t *testing.T) { + //GIVEN + logger := zap.NewNop() + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + decoder := admission.NewDecoder(scheme) + timeout := time.Millisecond * 100 + + testNs := "test-namespace" + + tests := []struct { + name string + notaryUrl *string + allowedRegistries *string + notaryTimeout *string + success bool + errorMessage string + }{ + { + name: "User validation get notary url from namespace annotation", + notaryUrl: ptr.To("http://test.notary.url"), + success: true, + }, + { + name: "User validation get allowed registries from namespace annotation", + notaryUrl: ptr.To("http://test.notary.url"), + allowedRegistries: ptr.To("ala,ma, \nkota"), + success: true, + }, + { + name: "User validation get notary timeout from namespace annotation", + notaryUrl: ptr.To("http://test.notary.url"), + notaryTimeout: ptr.To("22s"), + success: true, + }, + { + name: "User validation get all params from namespace annotation", + notaryUrl: ptr.To("http://another.test.notary.url"), + allowedRegistries: ptr.To("maka,paka"), + notaryTimeout: ptr.To("77h"), + success: true, + }, + { + name: "User validation return error for namespace without notary url annotation", + allowedRegistries: ptr.To("maka,paka"), + notaryTimeout: ptr.To("77h"), + success: false, + errorMessage: "notary URL is not set", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + //GIVEN + namespaceLabels := map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser} + namespaceAnnotations := map[string]string{} + expectedNotaryURL := "" + expectedAllowedRegistries := helpers.DefaultUserAllowedRegistries + expectedNotaryTimeout, _ := time.ParseDuration(helpers.DefaultUserNotaryTimeoutString) + if tt.notaryUrl != nil { + expectedNotaryURL = *tt.notaryUrl + namespaceAnnotations[pkg.NamespaceNotaryURLAnnotation] = *tt.notaryUrl + } + if tt.allowedRegistries != nil { + expectedAllowedRegistries = *tt.allowedRegistries + namespaceAnnotations[pkg.NamespaceAllowedRegistriesAnnotation] = *tt.allowedRegistries + } + if tt.notaryTimeout != nil { + expectedNotaryTimeout, _ = time.ParseDuration(*tt.notaryTimeout) + namespaceAnnotations[pkg.NamespaceNotaryTimeoutAnnotation] = *tt.notaryTimeout + } + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: testNs, Labels: namespaceLabels, Annotations: namespaceAnnotations}} + pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + } + + raw, err := json.Marshal(pod) + require.NoError(t, err) + + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + + // system validator should not be called + systemValidator := mocks.NewPodValidator(t) + systemValidator.AssertNotCalled(t, "ValidatePod") + defer systemValidator.AssertExpectations(t) + + userValidator := mocks.NewPodValidator(t) + userValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.Valid}, nil).Maybe() + defer userValidator.AssertExpectations(t) + + // user validator factory should be called with proper data + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.On("NewValidatorSvc", mock.Anything, mock.Anything, mock.Anything). + Return(userValidator). + Run(func(args mock.Arguments) { + argNotaryURL := args.Get(0) + argAllowedRegistries := args.Get(1) + argNotaryTimeout := args.Get(2) + require.Equal(t, expectedNotaryURL, argNotaryURL) + require.Equal(t, expectedAllowedRegistries, argAllowedRegistries) + require.Equal(t, expectedNotaryTimeout, argNotaryTimeout) + }).Maybe() + defer userValidatorFactory.AssertExpectations(t) + + webhook := NewDefaultingWebhook(client, + systemValidator, userValidatorFactory, timeout, StrictModeOff, decoder, logger.Sugar()) + + //WHEN + res := webhook.Handle(context.TODO(), req) + + //THEN + require.NotNil(t, res) + if tt.success { + assert.True(t, res.Allowed) + require.Nil(t, res.Result) + } else { + assert.False(t, res.Allowed) + require.NotNil(t, res.Result) + require.Contains(t, res.Result.Message, tt.errorMessage) + } + }) + } +} + +func TestHandleTimeout(t *testing.T) { + //GIVEN + logger := zap.NewNop() + ctxLogger := helpers.LoggerToContext(context.TODO(), logger.Sugar()) + scheme := runtime.NewScheme() + require.NoError(t, corev1.AddToScheme(scheme)) + decoder := admission.NewDecoder(scheme) + timeout := time.Millisecond * 100 + + testNs := "test-namespace" + + tests := []struct { + name string + systemStrictMode bool + userStrictMode bool // opposite value of system strict mode for the test if we get proper value + namespaceValidationLabelValue string + expectedPatches []jsonpatch.Operation + }{ + { + name: "Handle timeout for system validation and strict mode off", + namespaceValidationLabelValue: pkg.NamespaceValidationSystem, + systemStrictMode: StrictModeOff, + userStrictMode: StrictModeOn, + expectedPatches: []jsonpatch.Operation{ + { + Operation: "add", + Path: "/metadata/labels", + Value: map[string]interface{}{"pods.warden.kyma-project.io/validate": "pending"}, + }}, + }, + { + name: "Handle timeout for system validation and strict mode on", + namespaceValidationLabelValue: pkg.NamespaceValidationSystem, + systemStrictMode: StrictModeOn, + userStrictMode: StrictModeOff, + expectedPatches: []jsonpatch.Operation{ + { + Operation: "add", + Path: "/metadata/labels", + Value: map[string]interface{}{"pods.warden.kyma-project.io/validate": "pending"}, + }, + { + Operation: "add", + Path: "/metadata/annotations", + Value: map[string]interface{}{"pods.warden.kyma-project.io/validate-reject": "reject"}, + }}, + }, + { + name: "Handle timeout for user validation and strict mode off", + namespaceValidationLabelValue: pkg.NamespaceValidationUser, + systemStrictMode: StrictModeOn, + userStrictMode: StrictModeOff, + expectedPatches: []jsonpatch.Operation{ + { + Operation: "add", + Path: "/metadata/labels", + Value: map[string]interface{}{"pods.warden.kyma-project.io/validate": "pending"}, + }}, + }, + { + name: "Handle timeout for user validation and strict mode on", + namespaceValidationLabelValue: pkg.NamespaceValidationUser, + systemStrictMode: StrictModeOff, + userStrictMode: StrictModeOn, + expectedPatches: []jsonpatch.Operation{ + { + Operation: "add", + Path: "/metadata/labels", + Value: map[string]interface{}{"pods.warden.kyma-project.io/validate": "pending"}, + }, + { + Operation: "add", + Path: "/metadata/annotations", + Value: map[string]interface{}{"pods.warden.kyma-project.io/validate-reject": "reject"}, + }}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + //GIVEN + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: testNs, + Labels: map[string]string{pkg.NamespaceValidationLabel: tt.namespaceValidationLabelValue}, + Annotations: map[string]string{pkg.NamespaceStrictModeAnnotation: strconv.FormatBool(tt.userStrictMode)}, + }} + pod := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: testNs}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, + } + + raw, err := json.Marshal(pod) + require.NoError(t, err) + + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Object: runtime.RawExtension{Raw: raw}, + }} + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&ns).Build() + + webhook := NewDefaultingWebhook(client, + nil, nil, timeout, tt.systemStrictMode, decoder, logger.Sugar()) + + //WHEN + res := webhook.handleTimeout(ctxLogger, errors.New(""), req) + + //THEN + require.NotNil(t, res) + assert.True(t, res.Allowed) + require.NotNil(t, res.Result) + require.Contains(t, res.Result.Message, "request exceeded desired timeout") + require.NotNil(t, res.Patches) + require.ElementsMatch(t, tt.expectedPatches, res.Patches) + }) + } +} + func setupValidatorMock() *mocks.ImageValidatorService { mockValidator := mocks.ImageValidatorService{} mockValidator.Mock.On("Validate", mock.Anything, "test:test"). @@ -434,12 +1014,11 @@ func newRequestFix(t *testing.T, pod corev1.Pod, operation admissionv1.Operation raw, err := json.Marshal(pod) require.NoError(t, err) - req := admission.Request{ - AdmissionRequest: admissionv1.AdmissionRequest{ - Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, - Operation: operation, - Object: runtime.RawExtension{Raw: raw}, - }} + req := admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{Kind: PodType, Version: corev1.SchemeGroupVersion.Version}, + Operation: operation, + Object: runtime.RawExtension{Raw: raw}, + }} return req } @@ -449,8 +1028,7 @@ func newPodFix(nsName string, labels map[string]string) corev1.Pod { Name: "pod", Namespace: nsName, Labels: labels, }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{Image: "test:test"}}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "test:test"}}}, } return pod } diff --git a/internal/controllers/namespace/controller.go b/internal/controllers/namespace/controller.go index dfb51349..b90ae22c 100644 --- a/internal/controllers/namespace/controller.go +++ b/internal/controllers/namespace/controller.go @@ -2,6 +2,7 @@ package namespace import ( "context" + "github.com/kyma-project/warden/internal/validate" "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/predicate" @@ -26,7 +27,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&corev1.Namespace{}). WithEventFilter(predicate.And( - newWardenLabelsAdded(predicateOps{logger: r.Log}), + wardenPredicate(predicateOps{logger: r.Log}), )). Complete(r) } @@ -50,10 +51,10 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu }, client.IgnoreNotFound(err) } - if !nsValidationLabelSet(instance.Labels) { + if !validate.IsSupportedValidationLabelValue(instance.Labels[warden.NamespaceValidationLabel]) { var result ctrl.Result logger.With("result", result). - Debugf("validation lable: %s not found, omitting update namespace event", warden.NamespaceValidationLabel) + Debugf("validation label: %s not found or not supported value, omitting update namespace event", warden.NamespaceValidationLabel) return result, nil } diff --git a/internal/controllers/namespace/predicate.go b/internal/controllers/namespace/predicate.go index 1be212a6..ea235c4e 100644 --- a/internal/controllers/namespace/predicate.go +++ b/internal/controllers/namespace/predicate.go @@ -1,6 +1,7 @@ package namespace import ( + "github.com/kyma-project/warden/internal/validate" warden "github.com/kyma-project/warden/pkg" "go.uber.org/zap" "sigs.k8s.io/controller-runtime/pkg/event" @@ -36,8 +37,8 @@ func buildNsGenericReject(ops predicateOps) func(event.GenericEvent) bool { } } -// newWardenLabelsAdded creates predicate to check if validation label was added -func newWardenLabelsAdded(ops predicateOps) predicate.Funcs { +// wardenPredicate creates predicate to check if validation label was added +func wardenPredicate(ops predicateOps) predicate.Funcs { return predicate.Funcs{ CreateFunc: buildNsCreateReject(ops), DeleteFunc: buildNsDeleteReject(ops), @@ -46,34 +47,63 @@ func newWardenLabelsAdded(ops predicateOps) predicate.Funcs { } } -func nsValidationLabelSet(labels map[string]string) bool { - value, found := labels[warden.NamespaceValidationLabel] - if found && value == warden.NamespaceValidationEnabled { - return true - } - return false -} - // buildNsUpdated creates function to check if validation label was added func buildNsUpdated(ops predicateOps) func(event.UpdateEvent) bool { return func(evt event.UpdateEvent) bool { + oldLabels := evt.ObjectOld.GetLabels() + newLabels := evt.ObjectNew.GetLabels() + oldAnnotations := evt.ObjectOld.GetAnnotations() + newAnnotations := evt.ObjectNew.GetAnnotations() ops.logger. - With("oldLabels", evt.ObjectOld.GetLabels()). - With("newLabels", evt.ObjectNew.GetLabels()). + With("oldLabels", oldLabels). + With("newLabels", newLabels). + With("oldAnnotations", oldAnnotations). + With("newAnnotations", newAnnotations). Debug("incoming update namespace event") - if nsValidationLabelSet(evt.ObjectOld.GetLabels()) { - ops.logger.Debugf("validation label '%s' already exists, omitting update namespace event", - warden.NamespaceValidationLabel) - return false + reconciliationNeeded := validationLabelUpdated(oldLabels, newLabels, ops.logger) || + userValidationAnnotationsUpdated(oldAnnotations, newAnnotations, newLabels, ops.logger) + if !reconciliationNeeded { + ops.logger.Debugf("omitting update namespace event") } - if !nsValidationLabelSet(evt.ObjectNew.GetLabels()) { - ops.logger.Debugf("validation label: %s not found, omitting update namespace event", - warden.NamespaceValidationLabel) - return false + return reconciliationNeeded + } +} + +func userValidationAnnotationsUpdated(oldAnnotations, newAnnotations, newLabels map[string]string, log *zap.SugaredLogger) bool { + if newLabels[warden.NamespaceValidationLabel] != warden.NamespaceValidationUser { + return false + } + for _, key := range []string{ + warden.NamespaceNotaryURLAnnotation, + warden.NamespaceAllowedRegistriesAnnotation, + warden.NamespaceNotaryTimeoutAnnotation, + warden.NamespaceStrictModeAnnotation, + } { + oldValue := oldAnnotations[key] + newValue := newAnnotations[key] + if oldValue != newValue { + log.Debugf("validation annotation: %s was changed and reconciliation is needed", key) + return true } + } + return false +} + +func validationLabelUpdated(oldLabels, newLabels map[string]string, log *zap.SugaredLogger) bool { + oldValue := oldLabels[warden.NamespaceValidationLabel] + newValue := newLabels[warden.NamespaceValidationLabel] - return true + if !validate.IsSupportedValidationLabelValue(newValue) { + log.Debugf("validation label: %s is removed or unsupported", warden.NamespaceValidationLabel) + return false + } + + if !validate.IsChangedSupportedValidationLabelValue(oldValue, newValue) { + log.Debugf("validation label: %s value was not changed", warden.NamespaceValidationLabel) + return false } + log.Debugf("validation label: %s was changed and reconciliation is needed", warden.NamespaceValidationLabel) + return true } diff --git a/internal/controllers/namespace/predicate_test.go b/internal/controllers/namespace/predicate_test.go index 2ce85821..9b5cfef4 100644 --- a/internal/controllers/namespace/predicate_test.go +++ b/internal/controllers/namespace/predicate_test.go @@ -11,129 +11,275 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" ) -func Test_nsValidationLabelSet(t *testing.T) { - type args struct { - labels map[string]string - } +func Test_nsUpdated(t *testing.T) { tests := []struct { - name string - args args - want bool + name string + event event.UpdateEvent + want bool }{ { - name: "namespace validation label not found - empty key", - args: args{ - labels: map[string]string{ - warden.NamespaceValidationLabel: "", - }, - }, - want: false, + name: "ns updated - added validation label", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled}}}}, + want: true, }, { - name: "namespace validation label not found - no key", - args: args{ - labels: map[string]string{ - "some": "label", - }, - }, + name: "ns not updated - added validation label with unsupported value", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: "disable"}}}}, want: false, }, { - name: "namespace validation label not found - nil map", - args: args{ - labels: nil, - }, + name: "ns not updated - removed validation label", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{}}}, want: false, }, { - name: `namespace validation label not found - non "enabled" key value`, - args: args{ - labels: map[string]string{ - warden.NamespaceValidationLabel: "disabled", - }, - }, + name: "ns updated - changed validation label value (both supported)", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}}}}, + want: true, + }, + { + name: "ns updated - changed validation label value from unsupported to supported", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: "disable"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}}}}, + want: true, + }, + { + name: "ns not updated - changed validation label value from supported to unsupported", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: "disable"}}}}, want: false, }, { - name: "namespace validation label found", - args: args{ - labels: map[string]string{ - warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled, - }, - }, + name: "ns updated - changed user validation annotations (notary url) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryURLAnnotation: "notary-url"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryURLAnnotation: "changed-notary-url"}}}}, + want: true, + }, + { + name: "ns updated - changed user validation annotations (allowed registries) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceAllowedRegistriesAnnotation: "another,allowed,registries"}}}}, + want: true, + }, + { + name: "ns updated - changed user validation annotations (notary timeout) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryTimeoutAnnotation: "33s"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryTimeoutAnnotation: "44s"}}}}, want: true, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := nsValidationLabelSet(tt.args.labels); got != tt.want { - t.Errorf("nsValidationLabelSet() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_nsUpdated(t *testing.T) { - type args struct { - event event.UpdateEvent - } - tests := []struct { - name string - args args - want bool - }{ { - name: "ns updated", - args: args{ - event: event.UpdateEvent{ - ObjectOld: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{}, - }, - ObjectNew: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled, - }, - }, - }, - }, - }, + name: "ns updated - changed user validation annotations (strict mode) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceStrictModeAnnotation: "true"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceStrictModeAnnotation: "false"}}}}, want: true, }, { - name: "ns not updated - new obj has no validation label", - args: args{ - event: event.UpdateEvent{ - ObjectOld: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{}, - }, - ObjectNew: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - warden.NamespaceValidationLabel: "disable", - }, - }, - }, - }, - }, + name: "ns updated - added user validation annotations (notary url) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryURLAnnotation: "notary-url"}}}}, + want: true, + }, + { + name: "ns updated - added user validation annotations (allowed registries) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries"}}}}, + want: true, + }, + { + name: "ns updated - added user validation annotations (notary timeout) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryTimeoutAnnotation: "33s"}}}}, + want: true, + }, + { + name: "ns updated - added user validation annotations (strict mode) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceStrictModeAnnotation: "true"}}}}, + want: true, + }, + { + name: "ns updated - removed user validation annotations (notary url) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryURLAnnotation: "notary-url"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}}, + want: true, + }, + { + name: "ns updated - removed user validation annotations (allowed registries) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}}, + want: true, + }, + { + name: "ns updated - removed user validation annotations (notary timeout) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceNotaryTimeoutAnnotation: "33s"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}}, + want: true, + }, + { + name: "ns updated - removed user validation annotations (strict mode) value for user validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}, + Annotations: map[string]string{warden.NamespaceStrictModeAnnotation: "true"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationUser}}}}, + want: true, + }, + { + name: "ns not updated - changed user validation annotations value for system validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}, + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "33s", + warden.NamespaceStrictModeAnnotation: "true"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}, + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "another-notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "another,allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "44s", + warden.NamespaceStrictModeAnnotation: "false"}}}}, + want: false, + }, + { + name: "ns not updated - removed user validation annotations value for system validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled}, + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "33s", + warden.NamespaceStrictModeAnnotation: "true"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled}}}}, + want: false, + }, + { + name: "ns not updated - added user validation annotations value for system validation", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: warden.NamespaceValidationSystem}, + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "33s", + warden.NamespaceStrictModeAnnotation: "true"}}}}, + want: false, + }, + { + name: "ns not updated - changed user validation annotations value for no validation label", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "33s", + warden.NamespaceStrictModeAnnotation: "true"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "another-notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "another,allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "44s", + warden.NamespaceStrictModeAnnotation: "false"}}}}, + want: false, + }, + { + name: "ns not updated - removed user validation annotations value for no validation label", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: "disabled"}, + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "33s", + warden.NamespaceStrictModeAnnotation: "true"}}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{warden.NamespaceValidationLabel: "unsupported"}}}}, want: false, }, { - name: "ns not updated - old obj has validation label", - args: args{ - event: event.UpdateEvent{ - ObjectOld: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Labels: map[string]string{ - warden.NamespaceValidationLabel: warden.NamespaceValidationEnabled, - }, - }, - }, - ObjectNew: &corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{}, - }, - }, - }, + name: "ns not updated - added user validation annotations value for no validation label", + event: event.UpdateEvent{ + ObjectOld: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{}}, + ObjectNew: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + warden.NamespaceNotaryURLAnnotation: "notary-url", + warden.NamespaceAllowedRegistriesAnnotation: "allowed,registries", + warden.NamespaceNotaryTimeoutAnnotation: "33s", + warden.NamespaceStrictModeAnnotation: "true"}}}}, want: false, }, } @@ -145,7 +291,7 @@ func Test_nsUpdated(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - shouldBeTriggered := nsUpdate(tt.args.event) + shouldBeTriggered := nsUpdate(tt.event) require.Equal(t, tt.want, shouldBeTriggered) }) } diff --git a/internal/controllers/pod_controller.go b/internal/controllers/pod_controller.go index 3ff7c6ea..4948cfa2 100644 --- a/internal/controllers/pod_controller.go +++ b/internal/controllers/pod_controller.go @@ -18,11 +18,12 @@ package controllers import ( "context" - "github.com/google/uuid" - "github.com/kyma-project/warden/internal/helpers" "sort" "time" + "github.com/google/uuid" + "github.com/kyma-project/warden/internal/helpers" + "github.com/kyma-project/warden/internal/validate" "github.com/kyma-project/warden/pkg" "go.uber.org/zap" @@ -40,20 +41,24 @@ type PodReconcilerConfig struct { // PodReconciler reconciles a Pod object type PodReconciler struct { - client client.Client - scheme *runtime.Scheme - validator validate.PodValidator - baseLogger *zap.SugaredLogger + client client.Client + scheme *runtime.Scheme + systemValidator validate.PodValidator + userValidationSvcFactory validate.ValidatorSvcFactory + baseLogger *zap.SugaredLogger PodReconcilerConfig } -func NewPodReconciler(client client.Client, scheme *runtime.Scheme, validator validate.PodValidator, reconcileCfg PodReconcilerConfig, logger *zap.SugaredLogger) *PodReconciler { +func NewPodReconciler(client client.Client, scheme *runtime.Scheme, + validator validate.PodValidator, userValidationSvcFactory validate.ValidatorSvcFactory, + reconcileCfg PodReconcilerConfig, logger *zap.SugaredLogger) *PodReconciler { return &PodReconciler{ - client: client, - scheme: scheme, - validator: validator, - baseLogger: logger, - PodReconcilerConfig: reconcileCfg, + client: client, + scheme: scheme, + systemValidator: validator, + userValidationSvcFactory: userValidationSvcFactory, + baseLogger: logger, + PodReconcilerConfig: reconcileCfg, } } @@ -104,6 +109,7 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R reqUUID := uuid.New().String() logger := r.baseLogger.With("req", req).With("req-id", reqUUID) ctxLogger := helpers.LoggerToContext(ctx, logger) + logger.Debugf("reconciliation started") var pod corev1.Pod if err := r.client.Get(ctxLogger, req.NamespacedName, &pod); err != nil { @@ -137,7 +143,16 @@ func (r *PodReconciler) checkPod(ctx context.Context, pod *corev1.Pod) (validate return validate.NoAction, err } - result, err := r.validator.ValidatePod(ctx, pod, &ns) + validator := r.systemValidator + if validate.IsUserValidationForNS(&ns) { + var err error + validator, err = validate.NewUserValidationSvc(&ns, r.userValidationSvcFactory) + if err != nil { + return validate.NoAction, err + } + } + + result, err := validator.ValidatePod(ctx, pod, &ns) if err != nil { return validate.NoAction, err } diff --git a/internal/controllers/pod_controller_test.go b/internal/controllers/pod_controller_test.go index a5f3bc7a..46603a3d 100644 --- a/internal/controllers/pod_controller_test.go +++ b/internal/controllers/pod_controller_test.go @@ -51,7 +51,7 @@ func Test_PodReconcile(t *testing.T) { requeueTime := 60 * time.Minute testLogger := test_helpers.NewTestZapLogger(t) - ctrl := NewPodReconciler(k8sClient, scheme.Scheme, podValidator, PodReconcilerConfig{ + ctrl := NewPodReconciler(k8sClient, scheme.Scheme, podValidator, nil, PodReconcilerConfig{ RequeueAfter: requeueTime, }, testLogger.Sugar()) @@ -130,10 +130,147 @@ func Test_PodReconcile(t *testing.T) { } } +func Test_PodReconcileForSystemOrUserValidation(t *testing.T) { + testEnv, k8sClient := test_suite.Setup(t) + defer test_suite.TearDown(t, testEnv) + + requeueTime := 60 * time.Minute + testLogger := test_helpers.NewTestZapLogger(t) + + testCases := []struct { + name string + namespace corev1.Namespace + }{ + { + name: "Reconcile pod with system (enabled) validator", + namespace: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "warden-enabled", + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}}}, + }, + { + name: "Reconcile pod with system (system) validator", + namespace: corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "warden-system", + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationSystem}}}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + //GIVEN + // system validator should be called + systemImageValidator := mocks.NewImageValidatorService(t) + systemImageValidator.On("Validate", mock.Anything, mock.Anything). + Return(nil).Once() + systemPodValidator := validate.NewPodValidator(systemImageValidator) + + // user validator (factory) should not be called + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.AssertNotCalled(t, "NewValidatorSvc") + defer userValidatorFactory.AssertExpectations(t) + + require.NoError(t, k8sClient.Create(context.TODO(), &tc.namespace)) + defer deleteNamespace(t, k8sClient, &tc.namespace) + + pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{ + Namespace: tc.namespace.GetName(), + Name: "valid-pod"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "image", Name: "container"}}}, + } + require.NoError(t, k8sClient.Create(context.TODO(), &pod)) + defer deletePod(t, k8sClient, &pod) + req := reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: pod.GetNamespace(), + Name: pod.GetName()}, + } + + ctrl := NewPodReconciler(k8sClient, scheme.Scheme, systemPodValidator, userValidatorFactory, + PodReconcilerConfig{RequeueAfter: requeueTime}, testLogger.Sugar()) + + //WHEN + res, err := ctrl.Reconcile(context.TODO(), req) + + //THEN + require.NoError(t, err) + assert.Equal(t, reconcile.Result{}, res) + + key := ctrlclient.ObjectKeyFromObject(&pod) + + finalPod := corev1.Pod{} + require.NoError(t, k8sClient.Get(context.TODO(), key, &finalPod)) + + labelValue, found := finalPod.Labels[pkg.PodValidationLabel] + require.True(t, found) + require.Equal(t, pkg.ValidationStatusSuccess, labelValue) + }) + } + + t.Run("Reconcile pod with user validator", func(t *testing.T) { + //GIVEN + // system validator should not be called + systemImageValidator := mocks.NewImageValidatorService(t) + systemImageValidator.AssertNotCalled(t, "Validate") + systemPodValidator := validate.NewPodValidator(systemImageValidator) + + // user validator should be called + userValidator := mocks.NewPodValidator(t) + userValidator.On("ValidatePod", mock.Anything, mock.Anything, mock.Anything). + Return(validate.ValidationResult{Status: validate.Valid}, nil).Once() + defer userValidator.AssertExpectations(t) + + userValidatorFactory := mocks.NewValidatorSvcFactory(t) + userValidatorFactory.On("NewValidatorSvc", mock.Anything, mock.Anything, mock.Anything). + Return(userValidator).Once() + defer userValidatorFactory.AssertExpectations(t) + + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: "warden-user", + Labels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser}, + Annotations: map[string]string{pkg.NamespaceNotaryURLAnnotation: "notary"}, + }} + require.NoError(t, k8sClient.Create(context.TODO(), &ns)) + defer deleteNamespace(t, k8sClient, &ns) + + pod := corev1.Pod{ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.GetName(), + Name: "valid-pod"}, + Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: "image", Name: "container"}}}, + } + require.NoError(t, k8sClient.Create(context.TODO(), &pod)) + defer deletePod(t, k8sClient, &pod) + req := reconcile.Request{NamespacedName: types.NamespacedName{ + Namespace: pod.GetNamespace(), + Name: pod.GetName()}, + } + + ctrl := NewPodReconciler(k8sClient, scheme.Scheme, systemPodValidator, userValidatorFactory, + PodReconcilerConfig{RequeueAfter: requeueTime}, testLogger.Sugar()) + + //WHEN + res, err := ctrl.Reconcile(context.TODO(), req) + + //THEN + require.NoError(t, err) + assert.Equal(t, reconcile.Result{}, res) + + key := ctrlclient.ObjectKeyFromObject(&pod) + + finalPod := corev1.Pod{} + require.NoError(t, k8sClient.Get(context.TODO(), key, &finalPod)) + + labelValue, found := finalPod.Labels[pkg.PodValidationLabel] + require.True(t, found) + require.Equal(t, pkg.ValidationStatusSuccess, labelValue) + }) +} + func deletePod(t *testing.T, k8sClient ctrlclient.Client, pod *corev1.Pod) { require.NoError(t, k8sClient.Delete(context.TODO(), pod)) } +func deleteNamespace(t *testing.T, k8sClient ctrlclient.Client, ns *corev1.Namespace) { + require.NoError(t, k8sClient.Delete(context.TODO(), ns)) +} + type MockK8sClient struct { ctrlclient.Client called bool @@ -176,7 +313,7 @@ func TestReconcile_K8sOperationFails(t *testing.T) { Spec: corev1.PodSpec{Containers: []corev1.Container{{Image: validImage, Name: "container"}}}} require.NoError(t, mockK8Client.Create(context.TODO(), &pod)) - ctrl := NewPodReconciler(mockK8Client, scheme.Scheme, podValidator, PodReconcilerConfig{ + ctrl := NewPodReconciler(mockK8Client, scheme.Scheme, podValidator, nil, PodReconcilerConfig{ RequeueAfter: requeueTime, }, testLogger.Sugar()) req := reconcile.Request{NamespacedName: types.NamespacedName{ diff --git a/internal/helpers/user_notary_cfg.go b/internal/helpers/user_notary_cfg.go new file mode 100644 index 00000000..5abe60b5 --- /dev/null +++ b/internal/helpers/user_notary_cfg.go @@ -0,0 +1,57 @@ +package helpers + +import ( + "github.com/kyma-project/warden/pkg" + "github.com/pkg/errors" + corev1 "k8s.io/api/core/v1" + "strconv" + "time" +) + +const ( + DefaultUserAllowedRegistries = "" + DefaultUserNotaryTimeoutString = "30s" + DefaultUserStrictMode = true +) + +type UserValidationNotaryConfig struct { + NotaryURL string + AllowedRegistries string + NotaryTimeout time.Duration +} + +func GetUserValidationNotaryConfig(ns *corev1.Namespace) (UserValidationNotaryConfig, error) { + userNotaryURL, okNotaryURL := ns.GetAnnotations()[pkg.NamespaceNotaryURLAnnotation] + if !okNotaryURL { + return UserValidationNotaryConfig{}, errors.New("notary URL is not set") + } + userAllowedRegistries, okAllowedRegistries := ns.GetAnnotations()[pkg.NamespaceAllowedRegistriesAnnotation] + if !okAllowedRegistries { + userAllowedRegistries = DefaultUserAllowedRegistries + } + userNotaryTimeoutString, okUserNotaryTimeoutString := ns.GetAnnotations()[pkg.NamespaceNotaryTimeoutAnnotation] + if !okUserNotaryTimeoutString { + userNotaryTimeoutString = DefaultUserNotaryTimeoutString + } + userNotaryTimeout, errNotaryTimeoutParse := time.ParseDuration(userNotaryTimeoutString) + if errNotaryTimeoutParse != nil { + return UserValidationNotaryConfig{}, errNotaryTimeoutParse + } + return UserValidationNotaryConfig{ + NotaryURL: userNotaryURL, + AllowedRegistries: userAllowedRegistries, + NotaryTimeout: userNotaryTimeout, + }, nil +} + +func GetUserValidationStrictMode(ns *corev1.Namespace) (bool, error) { + strictModeString, ok := ns.GetAnnotations()[pkg.NamespaceStrictModeAnnotation] + if !ok { + return DefaultUserStrictMode, nil + } + strictMode, err := strconv.ParseBool(strictModeString) + if err != nil { + return true, errors.Wrapf(err, "failed to parse %s annotation", pkg.NamespaceStrictModeAnnotation) + } + return strictMode, nil +} diff --git a/internal/validate/mocks/ImageValidatorService.go b/internal/validate/mocks/ImageValidatorService.go index 1111081e..471d2330 100644 --- a/internal/validate/mocks/ImageValidatorService.go +++ b/internal/validate/mocks/ImageValidatorService.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.39.0. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks diff --git a/internal/validate/mocks/NotaryRepoClient.go b/internal/validate/mocks/NotaryRepoClient.go index c9c0c4a0..0748dfe1 100644 --- a/internal/validate/mocks/NotaryRepoClient.go +++ b/internal/validate/mocks/NotaryRepoClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.39.0. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks diff --git a/internal/validate/mocks/PodValidator.go b/internal/validate/mocks/PodValidator.go index dcfe67ef..68173f2b 100644 --- a/internal/validate/mocks/PodValidator.go +++ b/internal/validate/mocks/PodValidator.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.39.0. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks diff --git a/internal/validate/mocks/RepoFactory.go b/internal/validate/mocks/RepoFactory.go index 42055b1f..f2665b26 100644 --- a/internal/validate/mocks/RepoFactory.go +++ b/internal/validate/mocks/RepoFactory.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.39.0. DO NOT EDIT. +// Code generated by mockery v2.46.0. DO NOT EDIT. package mocks diff --git a/internal/validate/mocks/ValidatorSvcFactory.go b/internal/validate/mocks/ValidatorSvcFactory.go new file mode 100644 index 00000000..3346b830 --- /dev/null +++ b/internal/validate/mocks/ValidatorSvcFactory.go @@ -0,0 +1,50 @@ +// Code generated by mockery v2.46.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" + + validate "github.com/kyma-project/warden/internal/validate" +) + +// ValidatorSvcFactory is an autogenerated mock type for the ValidatorSvcFactory type +type ValidatorSvcFactory struct { + mock.Mock +} + +// NewValidatorSvc provides a mock function with given fields: notaryURL, notaryAllowedRegistries, notaryTimeout +func (_m *ValidatorSvcFactory) NewValidatorSvc(notaryURL string, notaryAllowedRegistries string, notaryTimeout time.Duration) validate.PodValidator { + ret := _m.Called(notaryURL, notaryAllowedRegistries, notaryTimeout) + + if len(ret) == 0 { + panic("no return value specified for NewValidatorSvc") + } + + var r0 validate.PodValidator + if rf, ok := ret.Get(0).(func(string, string, time.Duration) validate.PodValidator); ok { + r0 = rf(notaryURL, notaryAllowedRegistries, notaryTimeout) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(validate.PodValidator) + } + } + + return r0 +} + +// NewValidatorSvcFactory creates a new instance of ValidatorSvcFactory. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewValidatorSvcFactory(t interface { + mock.TestingT + Cleanup(func()) +}) *ValidatorSvcFactory { + mock := &ValidatorSvcFactory{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/validate/namespace.go b/internal/validate/namespace.go new file mode 100644 index 00000000..951f793d --- /dev/null +++ b/internal/validate/namespace.go @@ -0,0 +1,33 @@ +package validate + +import ( + "github.com/kyma-project/warden/pkg" + corev1 "k8s.io/api/core/v1" +) + +func IsValidationEnabledForNS(ns *corev1.Namespace) bool { + value := ns.GetLabels()[pkg.NamespaceValidationLabel] + return IsSupportedValidationLabelValue(value) +} + +func IsSupportedValidationLabelValue(value string) bool { + return value == pkg.NamespaceValidationEnabled || + value == pkg.NamespaceValidationSystem || + value == pkg.NamespaceValidationUser +} + +func IsUserValidationForNS(ns *corev1.Namespace) bool { + value := ns.GetLabels()[pkg.NamespaceValidationLabel] + return value == pkg.NamespaceValidationUser +} + +func IsChangedSupportedValidationLabelValue(oldValue, newValue string) bool { + if !IsSupportedValidationLabelValue(oldValue) && !IsSupportedValidationLabelValue(newValue) { + return false + } + if (oldValue == pkg.NamespaceValidationEnabled || oldValue == pkg.NamespaceValidationSystem) && + (newValue == pkg.NamespaceValidationEnabled || newValue == pkg.NamespaceValidationSystem) { + return false + } + return oldValue != newValue +} diff --git a/internal/validate/namespace_test.go b/internal/validate/namespace_test.go new file mode 100644 index 00000000..62d0aaa6 --- /dev/null +++ b/internal/validate/namespace_test.go @@ -0,0 +1,185 @@ +package validate_test + +import ( + "testing" + + "github.com/kyma-project/warden/internal/validate" + "github.com/kyma-project/warden/pkg" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestNamespaceLabelsValidation(t *testing.T) { + testNs := "test-namespace" + + testCases := []struct { + name string + namespaceLabels map[string]string + success bool + }{ + { + name: "namespace has validation enabled", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled}, + success: true, + }, + { + name: "namespace has validation enabled (system)", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationSystem}, + success: true, + }, + { + name: "namespace has validation enabled (user)", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser}, + success: true, + }, + { + name: "namespace has validation disabled (invalid)", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: "invalid"}, + success: false, + }, + { + name: "namespace has no validation label", + namespaceLabels: map[string]string{}, + success: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + //GIVEN + ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: testNs, + Labels: testCase.namespaceLabels, + }} + + //WHEN + enabled := validate.IsValidationEnabledForNS(ns) + + //THEN + require.Equal(t, testCase.success, enabled) + }) + } +} + +func TestUserNamespaceLabelsValidation(t *testing.T) { + testNs := "test-namespace" + + testCases := []struct { + name string + namespaceLabels map[string]string + success bool + }{ + { + name: "namespace has user validation enabled", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationUser}, + success: true, + }, + { + name: "namespace has not user validation enabled (is set to system)", + namespaceLabels: map[string]string{pkg.NamespaceValidationLabel: pkg.NamespaceValidationSystem}, + success: false, + }, + { + name: "namespace has no validation label", + namespaceLabels: map[string]string{}, + success: false, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + //GIVEN + ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: testNs, + Labels: testCase.namespaceLabels, + }} + + //WHEN + enabled := validate.IsUserValidationForNS(ns) + + //THEN + require.Equal(t, testCase.success, enabled) + }) + } +} + +func TestIsChangedSupportedValidationLabelValue(t *testing.T) { + tests := []struct { + name string + oldValue string + newValue string + want bool + }{ + { + name: "changed (System->User)", + oldValue: pkg.NamespaceValidationSystem, + newValue: pkg.NamespaceValidationUser, + want: true, + }, + { + name: "changed (User->System)", + oldValue: pkg.NamespaceValidationUser, + newValue: pkg.NamespaceValidationSystem, + want: true, + }, + { + name: "changed (Enabled->User)", + oldValue: pkg.NamespaceValidationEnabled, + newValue: pkg.NamespaceValidationUser, + want: true, + }, + { + name: "changed (User->Enabled)", + oldValue: pkg.NamespaceValidationUser, + newValue: pkg.NamespaceValidationEnabled, + want: true, + }, + { + name: "not changed (System->System)", + oldValue: pkg.NamespaceValidationSystem, + newValue: pkg.NamespaceValidationSystem, + want: false, + }, + { + name: "not changed (System->Enabled - equivalent values)", + oldValue: pkg.NamespaceValidationSystem, + newValue: pkg.NamespaceValidationEnabled, + want: false, + }, + { + name: "not changed (Enabled->System - equivalent values)", + oldValue: pkg.NamespaceValidationEnabled, + newValue: pkg.NamespaceValidationSystem, + want: false, + }, + { + name: "changed (System->unsupported)", + oldValue: pkg.NamespaceValidationSystem, + newValue: "unsupported", + want: true, + }, + { + name: "changed (unsupported->User)", + oldValue: "unsupported", + newValue: pkg.NamespaceValidationUser, + want: true, + }, + { + name: "not changed (unsupported->unsupported)", + oldValue: "unsupported", + newValue: "unsupported", + want: false, + }, + { + name: "not changed (unsupported->another-unsupported)", + oldValue: "blekota", + newValue: "mlekota", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := validate.IsChangedSupportedValidationLabelValue(tt.oldValue, tt.newValue) + require.Equal(t, tt.want, result) + }) + } +} diff --git a/internal/validate/pod.go b/internal/validate/pod.go index 6e516348..682821aa 100644 --- a/internal/validate/pod.go +++ b/internal/validate/pod.go @@ -3,6 +3,8 @@ package validate import ( "context" "errors" + "time" + "github.com/kyma-project/warden/internal/helpers" "github.com/kyma-project/warden/pkg" corev1 "k8s.io/api/core/v1" @@ -22,15 +24,50 @@ const ( NoAction ValidationStatus = "NoAction" ) +//go:generate mockery --name ValidatorSvcFactory +type ValidatorSvcFactory interface { + NewValidatorSvc(notaryURL string, notaryAllowedRegistries string, notaryTimeout time.Duration) PodValidator +} + +var _ ValidatorSvcFactory = &validatorSvcFactory{} + +type validatorSvcFactory struct { +} + +func NewValidatorSvcFactory() ValidatorSvcFactory { + return &validatorSvcFactory{} +} + +func (_ validatorSvcFactory) NewValidatorSvc(notaryURL string, notaryAllowedRegistries string, notaryTimeout time.Duration) PodValidator { + repoFactory := NotaryRepoFactory{Timeout: notaryTimeout} + allowedRegistries := ParseAllowedRegistries(notaryAllowedRegistries) + + validatorSvcConfig := ServiceConfig{ + NotaryConfig: NotaryConfig{Url: notaryURL}, + AllowedRegistries: allowedRegistries, + } + podValidatorSvc := NewImageValidator(&validatorSvcConfig, repoFactory) + validatorSvc := NewPodValidator(podValidatorSvc) + return validatorSvc +} + +func NewUserValidationSvc(ns *corev1.Namespace, validatorFactory ValidatorSvcFactory) (PodValidator, error) { + userValidationConfig, errGetUserValidation := helpers.GetUserValidationNotaryConfig(ns) + if errGetUserValidation != nil { + return nil, errGetUserValidation + } + validationSvc := validatorFactory.NewValidatorSvc( + userValidationConfig.NotaryURL, + userValidationConfig.AllowedRegistries, + userValidationConfig.NotaryTimeout) + return validationSvc, nil +} + //go:generate mockery --name PodValidator type PodValidator interface { ValidatePod(ctx context.Context, pod *corev1.Pod, ns *corev1.Namespace) (ValidationResult, error) } -type NamespaceChecker interface { - IsValidationEnabledForNS(namespace string) bool -} - var _ PodValidator = &podValidator{} type podValidator struct { @@ -69,10 +106,6 @@ func (a *podValidator) ValidatePod(ctx context.Context, pod *corev1.Pod, ns *cor return ValidationResult{admitResult, invalidImages}, nil } -func IsValidationEnabledForNS(ns *corev1.Namespace) bool { - return ns.GetLabels()[pkg.NamespaceValidationLabel] == pkg.NamespaceValidationEnabled -} - func (a *podValidator) validateImage(ctx context.Context, image string) (ValidationStatus, error) { err := a.Validator.Validate(ctx, image) if err != nil { diff --git a/internal/validate/pod_test.go b/internal/validate/pod_test.go index 5fc073e2..321a5353 100644 --- a/internal/validate/pod_test.go +++ b/internal/validate/pod_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/kyma-project/warden/internal/validate" "github.com/kyma-project/warden/internal/validate/mocks" @@ -167,3 +168,14 @@ func TestValidatePod(t *testing.T) { }) } } + +func TestNewValidatorSvc(t *testing.T) { + t.Run("create new validator svc", func(t *testing.T) { + validatorSvc := validate.NewValidatorSvcFactory(). + NewValidatorSvc("notaryURL", "allowed,registries", time.Second) + result, err := validatorSvc.ValidatePod(context.Background(), &v1.Pod{}, &v1.Namespace{}) + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, validate.Valid, result.Status) + }) +} diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go index ad774036..a7f74faf 100644 --- a/internal/webhook/webhook.go +++ b/internal/webhook/webhook.go @@ -128,8 +128,17 @@ func getFunctionMutatingWebhookCfg(config WebhookConfig) admissionregistrationv1 SideEffects: &sideEffects, TimeoutSeconds: ptr.To[int32](MutationWebhookTimeout), NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled, + MatchExpressions: []metav1.LabelSelectorRequirement{ + // match system and user values in pkg.NamespaceValidationLabel + { + Key: pkg.NamespaceValidationLabel, + Operator: metav1.LabelSelectorOpIn, + Values: []string{ + pkg.NamespaceValidationEnabled, + pkg.NamespaceValidationSystem, + pkg.NamespaceValidationUser, + }, + }, }, }, } @@ -182,8 +191,17 @@ func createValidatingWebhookConfiguration(config WebhookConfig) *admissionregist SideEffects: &sideEffects, TimeoutSeconds: ptr.To[int32](ValidationWebhookTimeout), NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - pkg.NamespaceValidationLabel: pkg.NamespaceValidationEnabled, + MatchExpressions: []metav1.LabelSelectorRequirement{ + // match system and user values in pkg.NamespaceValidationLabel + { + Key: pkg.NamespaceValidationLabel, + Operator: metav1.LabelSelectorOpIn, + Values: []string{ + pkg.NamespaceValidationEnabled, + pkg.NamespaceValidationSystem, + pkg.NamespaceValidationUser, + }, + }, }, }, }, diff --git a/pkg/labels.go b/pkg/labels.go index e33394f7..a855860b 100644 --- a/pkg/labels.go +++ b/pkg/labels.go @@ -1,8 +1,15 @@ package pkg const ( - NamespaceValidationLabel = "namespaces.warden.kyma-project.io/validate" - NamespaceValidationEnabled = "enabled" + NamespaceValidationLabel = "namespaces.warden.kyma-project.io/validate" + // Deprecated: use "system" instead + NamespaceValidationEnabled = "enabled" + NamespaceValidationSystem = "system" + NamespaceValidationUser = "user" + NamespaceNotaryURLAnnotation = "namespaces.warden.kyma-project.io/notary-url" + NamespaceAllowedRegistriesAnnotation = "namespaces.warden.kyma-project.io/allowed-registries" + NamespaceNotaryTimeoutAnnotation = "namespaces.warden.kyma-project.io/notary-timeout" + NamespaceStrictModeAnnotation = "namespaces.warden.kyma-project.io/strict-mode" ) const (