package webhook import ( "context" "encoding/json" "net/http" "testing" mock_webhook "github.com/aws/aws-app-mesh-controller-for-k8s/mocks/aws-app-mesh-controller-for-k8s/pkg/webhook" "github.com/golang/mock/gomock" "github.com/pkg/errors" "github.com/stretchr/testify/assert" admissionv1 "k8s.io/api/admission/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) func Test_validatingHandler_InjectDecoder(t *testing.T) { h := validatingHandler{ decoder: nil, } decoder := &admission.Decoder{} h.InjectDecoder(decoder) assert.Equal(t, decoder, h.decoder) } func Test_validatingHandler_Handle(t *testing.T) { schema := runtime.NewScheme() clientgoscheme.AddToScheme(schema) // k8sDecoder knows k8s objects decoder, _ := admission.NewDecoder(schema) initialPod := &corev1.Pod{ TypeMeta: metav1.TypeMeta{ APIVersion: "v1", Kind: "Pod", }, ObjectMeta: metav1.ObjectMeta{ Namespace: "default", Name: "foo", Annotations: map[string]string{ "some-key": "some-value", }, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "bar", Image: "bar:v1", }, }, }, Status: corev1.PodStatus{}, } initialPodRaw, err := json.Marshal(initialPod) assert.NoError(t, err) updatedPod := initialPod.DeepCopy() updatedPod.Spec.Containers[0].Image = "bar:v2" updatedPodRaw, err := json.Marshal(updatedPod) assert.NoError(t, err) type fields struct { validatorPrototype func(req admission.Request) (runtime.Object, error) validatorValidateCreate func(ctx context.Context, obj runtime.Object) error validatorValidateUpdate func(ctx context.Context, obj runtime.Object, oldObj runtime.Object) error validatorValidateDelete func(ctx context.Context, obj runtime.Object) error decoder *admission.Decoder } type args struct { req admission.Request } tests := []struct { name string fields fields args args want admission.Response }{ { name: "[create] approve request", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return &corev1.Pod{}, nil }, validatorValidateCreate: func(ctx context.Context, obj runtime.Object) error { return nil }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: true, Result: &metav1.Status{ Code: http.StatusOK, }, }, }, }, { name: "[create] reject request", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return &corev1.Pod{}, nil }, validatorValidateCreate: func(ctx context.Context, obj runtime.Object) error { return errors.New("oops, some error happened") }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ Patches: nil, AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: http.StatusForbidden, Reason: "oops, some error happened", }, }, }, }, { name: "[create] unexpected object type - prototype returns error ", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return nil, errors.New("oops, unexpected object type") }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Create, Object: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ Patches: nil, AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: http.StatusBadRequest, Message: "oops, unexpected object type", }, }, }, }, { name: "[update] approve request", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return &corev1.Pod{}, nil }, validatorValidateUpdate: func(ctx context.Context, obj runtime.Object, oldObj runtime.Object) error { return nil }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ Raw: updatedPodRaw, }, OldObject: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: true, Result: &metav1.Status{ Code: http.StatusOK, }, }, }, }, { name: "[update] reject request", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return &corev1.Pod{}, nil }, validatorValidateUpdate: func(ctx context.Context, obj runtime.Object, oldObj runtime.Object) error { return errors.New("oops, some error happened") }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ Raw: updatedPodRaw, }, OldObject: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ Patches: nil, AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: http.StatusForbidden, Reason: "oops, some error happened", }, }, }, }, { name: "[update] unexpected object type - prototype returns error", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return nil, errors.New("oops, unexpected object type") }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Update, Object: runtime.RawExtension{ Raw: updatedPodRaw, }, OldObject: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ Patches: nil, AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: http.StatusBadRequest, Message: "oops, unexpected object type", }, }, }, }, { name: "[delete] approve request", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return &corev1.Pod{}, nil }, validatorValidateDelete: func(ctx context.Context, obj runtime.Object) error { return nil }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ Raw: updatedPodRaw, }, }, }, }, want: admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: true, Result: &metav1.Status{ Code: http.StatusOK, }, }, }, }, { name: "[delete] reject request", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return &corev1.Pod{}, nil }, validatorValidateDelete: func(ctx context.Context, obj runtime.Object) error { return errors.New("oops, some error happened") }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ Raw: updatedPodRaw, }, }, }, }, want: admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: http.StatusForbidden, Reason: "oops, some error happened", }, }, }, }, { name: "[delete] unexpected object type - prototype returns error", fields: fields{ validatorPrototype: func(req admission.Request) (runtime.Object, error) { return nil, errors.New("oops, unexpected object type") }, decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Delete, OldObject: runtime.RawExtension{ Raw: updatedPodRaw, }, }, }, }, want: admission.Response{ AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: false, Result: &metav1.Status{ Code: http.StatusBadRequest, Message: "oops, unexpected object type", }, }, }, }, { name: "[connect] methods other than create/update/delete will pass through", fields: fields{ decoder: decoder, }, args: args{ req: admission.Request{ AdmissionRequest: admissionv1.AdmissionRequest{ Operation: admissionv1.Connect, Object: runtime.RawExtension{ Raw: initialPodRaw, }, }, }, }, want: admission.Response{ Patches: nil, AdmissionResponse: admissionv1.AdmissionResponse{ Allowed: true, Result: &metav1.Status{ Code: http.StatusOK, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() ctrl := gomock.NewController(t) defer ctrl.Finish() validator := mock_webhook.NewMockValidator(ctrl) if tt.fields.validatorPrototype != nil { validator.EXPECT().Prototype(gomock.Any()).DoAndReturn(tt.fields.validatorPrototype) } if tt.fields.validatorValidateCreate != nil { validator.EXPECT().ValidateCreate(gomock.Any(), gomock.Any()).DoAndReturn(tt.fields.validatorValidateCreate) } if tt.fields.validatorValidateUpdate != nil { validator.EXPECT().ValidateUpdate(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(tt.fields.validatorValidateUpdate) } if tt.fields.validatorValidateDelete != nil { validator.EXPECT().ValidateDelete(gomock.Any(), gomock.Any()).DoAndReturn(tt.fields.validatorValidateDelete) } h := &validatingHandler{ validator: validator, decoder: tt.fields.decoder, } got := h.Handle(ctx, tt.args.req) assert.Equal(t, tt.want, got) }) } }