diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go b/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go index 52cf3d88cfd..84dc0fe22d9 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/common/common.go @@ -23,6 +23,7 @@ const K8sRecommendedAppAnnotations = "app.kubernetes.io/part-of" const K8sRecommendedManagedByAnnotations = "app.kubernetes.io/managed-by" const PreDeploymentEvaluationAnnotation = "keptn.sh/pre-deployment-evaluations" const PostDeploymentEvaluationAnnotation = "keptn.sh/post-deployment-evaluations" +const SchedulingGateRemoved = "keptn.sh/scheduling-gate-removed" const TaskNameAnnotation = "keptn.sh/task-name" const NamespaceEnabledAnnotation = "keptn.sh/lifecycle-toolkit" const CreateAppTaskSpanName = "create_%s_app_task" @@ -30,8 +31,9 @@ const CreateWorkloadTaskSpanName = "create_%s_deployment_task" const CreateAppEvalSpanName = "create_%s_app_evaluation" const CreateWorkloadEvalSpanName = "create_%s_deployment_evaluation" const AppTypeAnnotation = "keptn.sh/app-type" +const KeptnGate = "keptn-prechecks-gate" -const MinKLTNameLen = 80 +const MinKeptnNameLen = 80 const MaxK8sObjectLength = 253 type AppType string @@ -176,17 +178,17 @@ const ( func GenerateTaskName(checkType CheckType, taskName string) string { randomId := rand.Intn(99_999-10_000) + 10000 - return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKLTNameLen, string(checkType), taskName, strconv.Itoa(randomId)) + return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKeptnNameLen, string(checkType), taskName, strconv.Itoa(randomId)) } func GenerateJobName(taskName string) string { randomId := rand.Intn(99_999-10_000) + 10000 - return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKLTNameLen, taskName, strconv.Itoa(randomId)) + return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKeptnNameLen, taskName, strconv.Itoa(randomId)) } func GenerateEvaluationName(checkType CheckType, evalName string) string { randomId := rand.Intn(99_999-10_000) + 10000 - return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKLTNameLen, string(checkType), evalName, strconv.Itoa(randomId)) + return operatorcommon.CreateResourceName(MaxK8sObjectLength, MinKeptnNameLen, string(checkType), evalName, strconv.Itoa(randomId)) } // MergeMaps merges two maps into a new map. If a key exists in both maps, the diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go index 8e407c8ae11..0e224b7734f 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnapp_types.go @@ -102,7 +102,7 @@ func init() { } func (a KeptnApp) GetAppVersionName() string { - return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKLTNameLen, a.Name, a.Spec.Version, common.Hash(a.Generation)) + return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKeptnNameLen, a.Name, a.Spec.Version, common.Hash(a.Generation)) } func (a KeptnApp) SetSpanAttributes(span trace.Span) { diff --git a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go index 38e031457ec..956debd855d 100644 --- a/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go +++ b/lifecycle-operator/apis/lifecycle/v1alpha3/keptnworkload_types.go @@ -103,7 +103,7 @@ func init() { } func (w KeptnWorkload) GetWorkloadInstanceName() string { - return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKLTNameLen, w.Name, w.Spec.Version) + return operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKeptnNameLen, w.Name, w.Spec.Version) } func (w KeptnWorkload) SetSpanAttributes(span trace.Span) { diff --git a/lifecycle-operator/config/rbac/role.yaml b/lifecycle-operator/config/rbac/role.yaml index 265bef1dc8f..bac0a3df79b 100644 --- a/lifecycle-operator/config/rbac/role.yaml +++ b/lifecycle-operator/config/rbac/role.yaml @@ -85,6 +85,7 @@ rules: verbs: - get - list + - update - watch - apiGroups: - "" diff --git a/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go b/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go new file mode 100644 index 00000000000..bcaa16a4524 --- /dev/null +++ b/lifecycle-operator/controllers/common/fake/schedulinggateshandler_mock.go @@ -0,0 +1,115 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package fake + +import ( + "context" + lfcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + "sync" +) + +// ISchedulingGatesHandlerMock is a mock implementation of common.ISchedulingGatesHandler. +// +// func TestSomethingThatUsesISchedulingGatesHandler(t *testing.T) { +// +// // make and configure a mocked common.ISchedulingGatesHandler +// mockedISchedulingGatesHandler := &ISchedulingGatesHandlerMock{ +// EnabledFunc: func() bool { +// panic("mock out the Enabled method") +// }, +// RemoveGatesFunc: func(ctx context.Context, workloadInstance *lfcv1alpha3.KeptnWorkloadInstance) error { +// panic("mock out the RemoveGates method") +// }, +// } +// +// // use mockedISchedulingGatesHandler in code that requires common.ISchedulingGatesHandler +// // and then make assertions. +// +// } +type ISchedulingGatesHandlerMock struct { + // EnabledFunc mocks the Enabled method. + EnabledFunc func() bool + + // RemoveGatesFunc mocks the RemoveGates method. + RemoveGatesFunc func(ctx context.Context, workloadInstance *lfcv1alpha3.KeptnWorkloadInstance) error + + // calls tracks calls to the methods. + calls struct { + // Enabled holds details about calls to the Enabled method. + Enabled []struct { + } + // RemoveGates holds details about calls to the RemoveGates method. + RemoveGates []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // WorkloadInstance is the workloadInstance argument value. + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance + } + } + lockEnabled sync.RWMutex + lockRemoveGates sync.RWMutex +} + +// Enabled calls EnabledFunc. +func (mock *ISchedulingGatesHandlerMock) Enabled() bool { + if mock.EnabledFunc == nil { + panic("ISchedulingGatesHandlerMock.EnabledFunc: method is nil but ISchedulingGatesHandler.Enabled was just called") + } + callInfo := struct { + }{} + mock.lockEnabled.Lock() + mock.calls.Enabled = append(mock.calls.Enabled, callInfo) + mock.lockEnabled.Unlock() + return mock.EnabledFunc() +} + +// EnabledCalls gets all the calls that were made to Enabled. +// Check the length with: +// +// len(mockedISchedulingGatesHandler.EnabledCalls()) +func (mock *ISchedulingGatesHandlerMock) EnabledCalls() []struct { +} { + var calls []struct { + } + mock.lockEnabled.RLock() + calls = mock.calls.Enabled + mock.lockEnabled.RUnlock() + return calls +} + +// RemoveGates calls RemoveGatesFunc. +func (mock *ISchedulingGatesHandlerMock) RemoveGates(ctx context.Context, workloadInstance *lfcv1alpha3.KeptnWorkloadInstance) error { + if mock.RemoveGatesFunc == nil { + panic("ISchedulingGatesHandlerMock.RemoveGatesFunc: method is nil but ISchedulingGatesHandler.RemoveGates was just called") + } + callInfo := struct { + Ctx context.Context + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance + }{ + Ctx: ctx, + WorkloadInstance: workloadInstance, + } + mock.lockRemoveGates.Lock() + mock.calls.RemoveGates = append(mock.calls.RemoveGates, callInfo) + mock.lockRemoveGates.Unlock() + return mock.RemoveGatesFunc(ctx, workloadInstance) +} + +// RemoveGatesCalls gets all the calls that were made to RemoveGates. +// Check the length with: +// +// len(mockedISchedulingGatesHandler.RemoveGatesCalls()) +func (mock *ISchedulingGatesHandlerMock) RemoveGatesCalls() []struct { + Ctx context.Context + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance +} { + var calls []struct { + Ctx context.Context + WorkloadInstance *lfcv1alpha3.KeptnWorkloadInstance + } + mock.lockRemoveGates.RLock() + calls = mock.calls.RemoveGates + mock.lockRemoveGates.RUnlock() + return calls +} diff --git a/lifecycle-operator/controllers/common/schedulinggateshandler.go b/lifecycle-operator/controllers/common/schedulinggateshandler.go new file mode 100644 index 00000000000..d6fabf49536 --- /dev/null +++ b/lifecycle-operator/controllers/common/schedulinggateshandler.go @@ -0,0 +1,108 @@ +package common + +import ( + "context" + + "github.com/go-logr/logr" + klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + apicommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3/common" + controllererrors "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/errors" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +//go:generate moq -pkg fake -skip-ensure -out ./fake/schedulinggateshandler_mock.go . ISchedulingGatesHandler +type ISchedulingGatesHandler interface { + RemoveGates(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error + Enabled() bool +} + +type RemoveGatesFunc func(ctx context.Context, c client.Client, podName string, podNamespace string) error +type GetPodsFunc func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind string, namespace string) ([]string, error) + +type SchedulingGatesHandler struct { + client.Client + logr.Logger + enabled bool + removeGates RemoveGatesFunc + getPods GetPodsFunc +} + +func NewSchedulingGatesHandler(c client.Client, l logr.Logger, enabled bool) *SchedulingGatesHandler { + return &SchedulingGatesHandler{ + Client: c, + Logger: l, + enabled: enabled, + removeGates: removePodGates, + getPods: getPodsOfOwner, + } +} + +func (h *SchedulingGatesHandler) RemoveGates(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error { + switch workloadInstance.Spec.ResourceReference.Kind { + case "Pod": + return h.removeGates(ctx, h.Client, workloadInstance.Spec.ResourceReference.Name, workloadInstance.Namespace) + case "ReplicaSet", "StatefulSet", "DaemonSet": + podList, err := h.getPods(ctx, h.Client, workloadInstance.Spec.ResourceReference.UID, workloadInstance.Spec.ResourceReference.Kind, workloadInstance.Namespace) + if err != nil { + h.Logger.Error(err, "cannot get pods") + return err + } + for _, pod := range podList { + err := h.removeGates(ctx, h.Client, pod, workloadInstance.Namespace) + if err != nil { + h.Logger.Error(err, "cannot remove gates from pod") + return err + } + } + default: + return controllererrors.ErrUnsupportedWorkloadInstanceResourceReference + } + + return nil +} + +func (h *SchedulingGatesHandler) Enabled() bool { + return h.enabled +} + +func removePodGates(ctx context.Context, c client.Client, podName string, podNamespace string) error { + pod := &v1.Pod{} + err := c.Get(ctx, types.NamespacedName{Namespace: podNamespace, Name: podName}, pod) + if err != nil { + return err + } + + if pod.Annotations[apicommon.SchedulingGateRemoved] != "" { + return nil + } + + if len(pod.Annotations) == 0 { + pod.Annotations = make(map[string]string, 1) + } + pod.Annotations[apicommon.SchedulingGateRemoved] = "true" + pod.Spec.SchedulingGates = nil + return c.Update(ctx, pod) +} + +func getPodsOfOwner(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind string, namespace string) ([]string, error) { + pods := &v1.PodList{} + err := c.List(ctx, pods, client.InNamespace(namespace)) + if err != nil { + return nil, err + } + + var resultPods []string + + for _, pod := range pods.Items { + for _, owner := range pod.OwnerReferences { + if owner.Kind == ownerKind && owner.UID == ownerUID { + resultPods = append(resultPods, pod.Name) + break + } + } + } + + return resultPods, nil +} diff --git a/lifecycle-operator/controllers/common/schedulinggateshandler_test.go b/lifecycle-operator/controllers/common/schedulinggateshandler_test.go new file mode 100644 index 00000000000..0ffe58ef8e8 --- /dev/null +++ b/lifecycle-operator/controllers/common/schedulinggateshandler_test.go @@ -0,0 +1,365 @@ +package common + +import ( + "context" + "fmt" + "testing" + + klcv1alpha3 "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3" + apicommon "github.com/keptn/lifecycle-toolkit/lifecycle-operator/apis/lifecycle/v1alpha3/common" + controllererrors "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/errors" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func Test_RemovePodGates(t *testing.T) { + tests := []struct { + name string + podName string + pod *corev1.Pod + wantError bool + annotations map[string]string + }{ + { + name: "pod does not exist", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + }, + }, + wantError: true, + }, + { + name: "scheduling gates already removed", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod", + Namespace: "default", + Annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + }, + }, + }, + wantError: false, + annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + }, + }, + { + name: "scheduling gates removed - empty annotations", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + SchedulingGates: []corev1.PodSchedulingGate{ + { + Name: "gate", + }, + }, + }, + }, + wantError: false, + annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + }, + }, + { + name: "scheduling gates removed - not empty annotations", + podName: "pod", + pod: &corev1.Pod{ + ObjectMeta: v1.ObjectMeta{ + Name: "pod", + Namespace: "default", + Annotations: map[string]string{ + "test": "test", + }, + }, + Spec: corev1.PodSpec{ + SchedulingGates: []corev1.PodSchedulingGate{ + { + Name: "gate", + }, + }, + }, + }, + wantError: false, + annotations: map[string]string{ + apicommon.SchedulingGateRemoved: "true", + "test": "test", + }, + }, + } + + err := klcv1alpha3.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := fake.NewClientBuilder().WithObjects(tt.pod).WithStatusSubresource(tt.pod).Build() + err := removePodGates(context.TODO(), client, tt.podName, tt.pod.Namespace) + if tt.wantError != (err != nil) { + t.Errorf("want error: %t, got: %v", tt.wantError, err) + } + if !tt.wantError { + pod := &corev1.Pod{} + err := client.Get(context.TODO(), types.NamespacedName{Namespace: tt.pod.Namespace, Name: tt.podName}, pod) + require.Nil(t, err) + require.Equal(t, []corev1.PodSchedulingGate(nil), pod.Spec.SchedulingGates) + require.Equal(t, tt.annotations, pod.Annotations) + } + }) + } +} + +func Test_GetPodsOfOwner(t *testing.T) { + namespace := "default" + tests := []struct { + name string + uid types.UID + kind string + pods *corev1.PodList + result []string + }{ + { + name: "pod list empty", + pods: &corev1.PodList{}, + result: nil, + }, + { + name: "pod list not matching kind or uid", + pods: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + OwnerReferences: []v1.OwnerReference{ + { + Kind: "unknown", + UID: types.UID("uid"), + }, + }, + }, + }, + }, + }, + kind: "unknown2", + uid: types.UID("uid2"), + result: nil, + }, + { + name: "pod list matches one pod of list", + pods: &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "pod1", + Namespace: "default", + OwnerReferences: []v1.OwnerReference{ + { + Kind: "unknown", + UID: types.UID("uid"), + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Name: "pod2", + Namespace: "default", + OwnerReferences: []v1.OwnerReference{ + { + Kind: "unknown2", + UID: types.UID("uid2"), + }, + }, + }, + }, + }, + }, + kind: "unknown", + uid: types.UID("uid"), + result: []string{"pod1"}, + }, + } + + err := klcv1alpha3.AddToScheme(scheme.Scheme) + require.Nil(t, err) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := fake.NewClientBuilder().WithLists(tt.pods).Build() + res, err := getPodsOfOwner(context.TODO(), client, tt.uid, tt.kind, namespace) + require.Nil(t, err) + require.Equal(t, tt.result, res) + }) + } +} + +func Test_SchedulingGatesHandler_IsSchedulingGatesEnabled(t *testing.T) { + h := SchedulingGatesHandler{ + enabled: true, + } + + require.True(t, h.Enabled()) + + h.enabled = false + + require.False(t, h.Enabled()) +} + +func Test_SchedulingGatesHandler_IsSchedulingGatesEnabledRemoveGates(t *testing.T) { + tests := []struct { + name string + handler SchedulingGatesHandler + wi *klcv1alpha3.KeptnWorkloadInstance + wantErr error + }{ + { + name: "unsuported resource ref", + handler: SchedulingGatesHandler{}, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "unsupported", + }, + }, + }, + }, + wantErr: controllererrors.ErrUnsupportedWorkloadInstanceResourceReference, + }, + { + name: "pod - happy path", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "Pod", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "pod - fail path", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return fmt.Errorf("pod") + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "Pod", + }, + }, + }, + }, + wantErr: fmt.Errorf("pod"), + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - happy path", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return nil + }, + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{"podName"}, nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - happy path - no pods found", + handler: SchedulingGatesHandler{ + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{}, nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - happy path - err getPods", + handler: SchedulingGatesHandler{ + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{}, fmt.Errorf("err") + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: fmt.Errorf("err"), + }, + { + name: "ReplicaSet, StatefulSet, DaemonSet - err removeGates", + handler: SchedulingGatesHandler{ + removeGates: func(ctx context.Context, c client.Client, podName, podNamespace string) error { + return fmt.Errorf("err") + }, + getPods: func(ctx context.Context, c client.Client, ownerUID types.UID, ownerKind, namespace string) ([]string, error) { + return []string{"podName"}, nil + }, + }, + wi: &klcv1alpha3.KeptnWorkloadInstance{ + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + ResourceReference: klcv1alpha3.ResourceReference{ + Kind: "ReplicaSet", + }, + }, + }, + }, + wantErr: fmt.Errorf("err"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.handler.RemoveGates(context.TODO(), tt.wi) + require.Equal(t, tt.wantErr, err) + }) + } +} diff --git a/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go b/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go index f38a7a3a279..7804550c7dc 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go +++ b/lifecycle-operator/controllers/lifecycle/keptnapp/controller.go @@ -184,7 +184,7 @@ func (r *KeptnAppReconciler) deprecateAppVersions(ctx context.Context, app *klcv lastResultErr = nil for i := app.Generation - 1; i > 0; i-- { deprecatedAppVersion := &klcv1alpha3.KeptnAppVersion{} - appVersionName := operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKLTNameLen, app.Name, app.Spec.Version, common.Hash(i)) + appVersionName := operatorcommon.CreateResourceName(common.MaxK8sObjectLength, common.MinKeptnNameLen, app.Name, app.Spec.Version, common.Hash(i)) if err := r.Get(ctx, types.NamespacedName{Namespace: app.Namespace, Name: appVersionName}, deprecatedAppVersion); err != nil { if !errors.IsNotFound(err) { r.Log.Error(err, fmt.Sprintf("Could not get KeptnAppVersion: %s", appVersionName)) diff --git a/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go b/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go index 9ac81a6c46a..e314bbebc1c 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go +++ b/lifecycle-operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go @@ -85,5 +85,5 @@ func (r *KeptnAppVersionReconciler) handleUnaccessibleWorkloadInstanceList(ctx c } func getWorkloadInstanceName(appName string, workloadName string, version string) string { - return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKLTNameLen, appName, workloadName, version) + return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKeptnNameLen, appName, workloadName, version) } diff --git a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go index ee887ec7216..ce734b6c612 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go +++ b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller.go @@ -46,12 +46,13 @@ const traceComponentName = "keptn/lifecycle-operator/workloadinstance" // KeptnWorkloadInstanceReconciler reconciles a KeptnWorkloadInstance object type KeptnWorkloadInstanceReconciler struct { client.Client - Scheme *runtime.Scheme - EventSender controllercommon.IEvent - Log logr.Logger - Meters apicommon.KeptnMeters - SpanHandler *telemetry.SpanHandler - TracerFactory telemetry.TracerFactory + Scheme *runtime.Scheme + EventSender controllercommon.IEvent + Log logr.Logger + Meters apicommon.KeptnMeters + SpanHandler *telemetry.SpanHandler + TracerFactory telemetry.TracerFactory + SchedulingGatesHandler controllercommon.ISchedulingGatesHandler } // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptnworkloadinstances,verbs=get;list;watch;create;update;patch;delete @@ -61,7 +62,7 @@ type KeptnWorkloadInstanceReconciler struct { // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptntasks/status,verbs=get;update;patch // +kubebuilder:rbac:groups=lifecycle.keptn.sh,resources=keptntasks/finalizers,verbs=update // +kubebuilder:rbac:groups=core,resources=events,verbs=create;watch;patch -// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update // +kubebuilder:rbac:groups=apps,resources=replicasets;deployments;statefulsets;daemonsets,verbs=get;list;watch // +kubebuilder:rbac:groups=argoproj.io,resources=rollouts,verbs=get;list;watch @@ -71,7 +72,7 @@ type KeptnWorkloadInstanceReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile // -//nolint:gocyclo +//nolint:gocyclo,gocognit func (r *KeptnWorkloadInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { r.Log.Info("Searching for KeptnWorkloadInstance") @@ -138,6 +139,14 @@ func (r *KeptnWorkloadInstanceReconciler) Reconcile(ctx context.Context, req ctr } } + if r.SchedulingGatesHandler.Enabled() { + // pre-evaluation checks done at this moment, we can remove the gate + if err := r.SchedulingGatesHandler.RemoveGates(ctx, workloadInstance); err != nil { + r.Log.Error(err, "could not remove SchedulingGates") + return ctrl.Result{Requeue: true, RequeueAfter: 10 * time.Second}, err + } + } + // Wait for deployment of Workload phase = apicommon.PhaseWorkloadDeployment if !workloadInstance.IsDeploymentSucceeded() { diff --git a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go index 692c437bf83..582ac079f1c 100644 --- a/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go +++ b/lifecycle-operator/controllers/lifecycle/keptnworkloadinstance/controller_test.go @@ -778,6 +778,11 @@ func TestKeptnWorkloadInstanceReconciler_ReconcileReachCompletion(t *testing.T) }, ) r, eventChannel, _ := setupReconciler(wi, app) + r.SchedulingGatesHandler = &fake.ISchedulingGatesHandlerMock{ + EnabledFunc: func() bool { + return false + }, + } req := ctrl.Request{ NamespacedName: types.NamespacedName{ @@ -807,6 +812,168 @@ func TestKeptnWorkloadInstanceReconciler_ReconcileReachCompletion(t *testing.T) } } +func TestKeptnWorkloadInstanceReconciler_ReconcileReachCompletion_SchedulingGates(t *testing.T) { + + testNamespace := "some-ns" + + wi := &klcv1alpha3.KeptnWorkloadInstance{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-wi", + Namespace: testNamespace, + }, + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + AppName: "some-app", + Version: "1.0.0", + }, + WorkloadName: "some-app-some-workload", + PreviousVersion: "", + TraceId: nil, + }, + Status: klcv1alpha3.KeptnWorkloadInstanceStatus{ + DeploymentStatus: apicommon.StateSucceeded, + PreDeploymentStatus: apicommon.StateSucceeded, + PostDeploymentStatus: apicommon.StateSucceeded, + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + CurrentPhase: apicommon.PhaseWorkloadPostEvaluation.ShortName, + Status: apicommon.StateSucceeded, + StartTime: metav1.Time{}, + EndTime: metav1.Time{}, + }, + } + + app := controllercommon.ReturnAppVersion( + testNamespace, + "some-app", + "1.0.0", + []klcv1alpha3.KeptnWorkloadRef{ + { + Name: "some-workload", + Version: "1.0.0", + }, + }, + klcv1alpha3.KeptnAppVersionStatus{ + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + }, + ) + + schedulingGatesMock := &fake.ISchedulingGatesHandlerMock{ + RemoveGatesFunc: func(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error { + return nil + }, + EnabledFunc: func() bool { + return true + }, + } + r, eventChannel, _ := setupReconciler(wi, app) + r.SchedulingGatesHandler = schedulingGatesMock + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: testNamespace, + Name: "some-wi", + }, + } + + result, err := r.Reconcile(context.TODO(), req) + + require.Len(t, schedulingGatesMock.RemoveGatesCalls(), 1) + require.Nil(t, err) + + // do not requeue since we reached completion + require.False(t, result.Requeue) + + // here we do not expect an event about the application preEvaluation being finished since that will have been sent in + // one of the previous reconciliation loops that lead to the first phase being reached + expectedEvents := []string{ + "CompletedFinished", + } + + for _, e := range expectedEvents { + select { + case event := <-eventChannel: + assert.Equal(t, strings.Contains(event, req.Name), true, "wrong workloadinstance") + assert.Equal(t, strings.Contains(event, req.Namespace), true, "wrong namespace") + assert.Equal(t, strings.Contains(event, e), true, fmt.Sprintf("no %s found in %s", e, event)) + case <-time.After(5 * time.Second): + t.Error("Didn't receive the cloud event") + } + } +} + +func TestKeptnWorkloadInstanceReconciler_RemoveGates_fail(t *testing.T) { + + testNamespace := "some-ns" + + wi := &klcv1alpha3.KeptnWorkloadInstance{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: "some-wi", + Namespace: testNamespace, + }, + Spec: klcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha3.KeptnWorkloadSpec{ + AppName: "some-app", + Version: "1.0.0", + }, + WorkloadName: "some-app-some-workload", + PreviousVersion: "", + TraceId: nil, + }, + Status: klcv1alpha3.KeptnWorkloadInstanceStatus{ + DeploymentStatus: apicommon.StateSucceeded, + PreDeploymentStatus: apicommon.StateSucceeded, + PostDeploymentStatus: apicommon.StateSucceeded, + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + PostDeploymentEvaluationStatus: apicommon.StateSucceeded, + CurrentPhase: apicommon.PhaseWorkloadPostEvaluation.ShortName, + Status: apicommon.StateSucceeded, + StartTime: metav1.Time{}, + EndTime: metav1.Time{}, + }, + } + + app := controllercommon.ReturnAppVersion( + testNamespace, + "some-app", + "1.0.0", + []klcv1alpha3.KeptnWorkloadRef{ + { + Name: "some-workload", + Version: "1.0.0", + }, + }, + klcv1alpha3.KeptnAppVersionStatus{ + PreDeploymentEvaluationStatus: apicommon.StateSucceeded, + }, + ) + r, _, _ := setupReconciler(wi, app) + r.SchedulingGatesHandler = &fake.ISchedulingGatesHandlerMock{ + RemoveGatesFunc: func(ctx context.Context, workloadInstance *klcv1alpha3.KeptnWorkloadInstance) error { + return fmt.Errorf("err") + }, + EnabledFunc: func() bool { + return true + }, + } + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: testNamespace, + Name: "some-wi", + }, + } + + result, err := r.Reconcile(context.TODO(), req) + + require.NotNil(t, err) + + // do not requeue since we reached completion + require.True(t, result.Requeue) +} + func TestKeptnWorkloadInstanceReconciler_ReconcileFailed(t *testing.T) { testNamespace := "some-ns" diff --git a/lifecycle-operator/main.go b/lifecycle-operator/main.go index 2696070a626..82a8298eddc 100644 --- a/lifecycle-operator/main.go +++ b/lifecycle-operator/main.go @@ -92,6 +92,8 @@ type envConfig struct { KeptnWorkloadInstanceControllerLogLevel int `envconfig:"KEPTN_WORKLOAD_INSTANCE_CONTROLLER_LOG_LEVEL" default:"0"` KeptnOptionsControllerLogLevel int `envconfig:"OPTIONS_CONTROLLER_LOG_LEVEL" default:"0"` + SchedulingGatesEnabled bool `envconfig:"SCHEDULING_GATES_ENABLED" default:"false"` + KeptnOptionsCollectorURL string `envconfig:"OTEL_COLLECTOR_URL" default:""` } @@ -259,13 +261,14 @@ func main() { workloadInstanceLogger := ctrl.Log.WithName("KeptnWorkloadInstance Controller").V(env.KeptnWorkloadInstanceControllerLogLevel) workloadInstanceRecorder := mgr.GetEventRecorderFor("keptnworkloadinstance-controller") workloadInstanceReconciler := &keptnworkloadinstance.KeptnWorkloadInstanceReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Log: workloadInstanceLogger, - EventSender: controllercommon.NewEventMultiplexer(workloadInstanceLogger, workloadInstanceRecorder, ceClient), - Meters: keptnMeters, - TracerFactory: telemetry.GetOtelInstance(), - SpanHandler: spanHandler, + SchedulingGatesHandler: controllercommon.NewSchedulingGatesHandler(mgr.GetClient(), workloadInstanceLogger, env.SchedulingGatesEnabled), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Log: workloadInstanceLogger, + EventSender: controllercommon.NewEventMultiplexer(workloadInstanceLogger, workloadInstanceRecorder, ceClient), + Meters: keptnMeters, + TracerFactory: telemetry.GetOtelInstance(), + SpanHandler: spanHandler, } if err = (workloadInstanceReconciler).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "KeptnWorkloadInstance") @@ -363,11 +366,12 @@ func main() { webhookBuilder.Register(mgr, map[string]*ctrlWebhook.Admission{ "/mutate-v1-pod": { Handler: &pod_mutator.PodMutatingWebhook{ - Client: mgr.GetClient(), - Tracer: otel.Tracer("keptn/webhook"), - EventSender: controllercommon.NewEventMultiplexer(webhookLogger, webhookRecorder, ceClient), - Decoder: admission.NewDecoder(mgr.GetScheme()), - Log: webhookLogger, + SchedulingGatesEnabled: env.SchedulingGatesEnabled, + Client: mgr.GetClient(), + Tracer: otel.Tracer("keptn/webhook"), + EventSender: controllercommon.NewEventMultiplexer(webhookLogger, webhookRecorder, ceClient), + Decoder: admission.NewDecoder(mgr.GetScheme()), + Log: webhookLogger, }, }, }) diff --git a/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go b/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go index 2d3bb0678ba..111e38ddcac 100644 --- a/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go +++ b/lifecycle-operator/test/component/workloadinstance/workloadinstance_suite_test.go @@ -43,13 +43,14 @@ var _ = BeforeSuite(func() { // //setup controllers here config.Instance().SetDefaultNamespace(KeptnNamespace) controller := &keptnworkloadinstance.KeptnWorkloadInstanceReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - EventSender: controllercommon.NewK8sSender(k8sManager.GetEventRecorderFor("test-workloadinstance-controller")), - Log: GinkgoLogr, - Meters: common.InitKeptnMeters(), - SpanHandler: &telemetry.SpanHandler{}, - TracerFactory: &common.TracerFactory{Tracer: tracer}, + SchedulingGatesHandler: controllercommon.NewSchedulingGatesHandler(nil, GinkgoLogr, false), + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + EventSender: controllercommon.NewK8sSender(k8sManager.GetEventRecorderFor("test-workloadinstance-controller")), + Log: GinkgoLogr, + Meters: common.InitKeptnMeters(), + SpanHandler: &telemetry.SpanHandler{}, + TracerFactory: &common.TracerFactory{Tracer: tracer}, } Eventually(controller.SetupWithManager(k8sManager)).WithTimeout(30 * time.Second).WithPolling(time.Second).Should(Succeed()) close(readyToStart) diff --git a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go index b43a3deae41..ab87375e3ae 100644 --- a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go +++ b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook.go @@ -36,11 +36,12 @@ import ( // PodMutatingWebhook annotates Pods type PodMutatingWebhook struct { - Client client.Client - Tracer trace.Tracer - Decoder *admission.Decoder - EventSender controllercommon.IEvent - Log logr.Logger + Client client.Client + Tracer trace.Tracer + Decoder *admission.Decoder + EventSender controllercommon.IEvent + Log logr.Logger + SchedulingGatesEnabled bool } const InvalidAnnotationMessage = "Invalid annotations" @@ -81,7 +82,7 @@ func (a *PodMutatingWebhook) Handle(ctx context.Context, req admission.Request) return admission.Allowed("namespace is not enabled for lifecycle operator") } - // check the OwnerReference of the pod to see if it is supported and intended to be managed by KLT + // check the OwnerReference of the pod to see if it is supported and intended to be managed by Keptn ownerRef := a.getOwnerReference(pod.ObjectMeta) if ownerRef.Kind == "" { @@ -93,16 +94,28 @@ func (a *PodMutatingWebhook) Handle(ctx context.Context, req admission.Request) logger.Info(fmt.Sprintf("Pod annotations: %v", pod.Annotations)) podIsAnnotated := a.isPodAnnotated(pod) - logger.Info("Checked if pod is annotated.") - if !podIsAnnotated { logger.Info("Pod is not annotated, check for parent annotations...") podIsAnnotated = a.copyAnnotationsIfParentAnnotated(ctx, &req, pod) } if podIsAnnotated { - logger.Info("Resource is annotated with Keptn annotations, using Keptn scheduler") - pod.Spec.SchedulerName = "keptn-scheduler" + logger.Info("Resource is annotated with Keptn annotations") + if a.SchedulingGatesEnabled { + logger.Info("SchedulingGates enabled") + _, gateRemoved := getLabelOrAnnotation(&pod.ObjectMeta, apicommon.SchedulingGateRemoved, "") + if gateRemoved { + return admission.Allowed("gate of the pod already removed") + } + pod.Spec.SchedulingGates = []corev1.PodSchedulingGate{ + { + Name: apicommon.KeptnGate, + }, + } + } else { + logger.Info("SchedulingGates disabled, using keptn-scheduler") + pod.Spec.SchedulerName = "keptn-scheduler" + } logger.Info("Annotations", "annotations", pod.Annotations) isAppAnnotationPresent := a.isAppAnnotationPresent(pod) @@ -438,7 +451,7 @@ func (a *PodMutatingWebhook) generateAppCreationRequest(ctx context.Context, pod func (a *PodMutatingWebhook) getWorkloadName(pod *corev1.Pod) string { workloadName, _ := getLabelOrAnnotation(&pod.ObjectMeta, apicommon.WorkloadAnnotation, apicommon.K8sRecommendedWorkloadAnnotations) applicationName, _ := getLabelOrAnnotation(&pod.ObjectMeta, apicommon.AppAnnotation, apicommon.K8sRecommendedAppAnnotations) - return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKLTNameLen, applicationName, workloadName) + return operatorcommon.CreateResourceName(apicommon.MaxK8sObjectLength, apicommon.MinKeptnNameLen, applicationName, workloadName) } func (a *PodMutatingWebhook) getAppName(pod *corev1.Pod) string { diff --git a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go index 433b5e01304..bd9a8cbdf81 100644 --- a/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go +++ b/lifecycle-operator/webhooks/pod_mutator/pod_mutating_webhook_test.go @@ -14,6 +14,7 @@ import ( fakeclient "github.com/keptn/lifecycle-toolkit/lifecycle-operator/controllers/common/fake" "github.com/stretchr/testify/require" "go.opentelemetry.io/otel/trace" + "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -1207,6 +1208,204 @@ func TestPodMutatingWebhook_Handle_SingleService(t *testing.T) { }, workload.Spec) } +func TestPodMutatingWebhook_Handle_SchedulingGates_GateRemoved(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pod", + Namespace: "default", + Annotations: map[string]string{ + apicommon.WorkloadAnnotation: "my-workload", + apicommon.VersionAnnotation: "0.1", + apicommon.SchedulingGateRemoved: "true", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Deployment", + Name: "my-deployment", + UID: "1234", + }, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "example-container", + Image: "nginx", + }, + }, + }, + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Annotations: map[string]string{ + apicommon.NamespaceEnabledAnnotation: "enabled", + }, + }, + } + fakeClient := fakeclient.NewClient(ns, pod) + + tr := &fakeclient.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return ctx, trace.SpanFromContext(ctx) + }} + + decoder := admission.NewDecoder(runtime.NewScheme()) + + wh := &PodMutatingWebhook{ + SchedulingGatesEnabled: true, + Client: fakeClient, + Tracer: tr, + Decoder: decoder, + EventSender: controllercommon.NewK8sSender(record.NewFakeRecorder(100)), + Log: testr.New(t), + } + + // Convert the Pod object to a byte array + podBytes, err := json.Marshal(pod) + require.Nil(t, err) + + // Create an AdmissionRequest object + request := admissionv1.AdmissionRequest{ + UID: "12345", + Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: podBytes, + }, + Namespace: "default", + } + + resp := wh.Handle(context.TODO(), admission.Request{ + AdmissionRequest: request, + }) + + require.NotNil(t, resp) + require.True(t, resp.Allowed) + + // no changes to the pod are expected + require.Len(t, resp.Patches, 0) +} + +func TestPodMutatingWebhook_Handle_SchedulingGates(t *testing.T) { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pod", + Namespace: "default", + Annotations: map[string]string{ + apicommon.WorkloadAnnotation: "my-workload", + apicommon.VersionAnnotation: "0.1", + }, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "v1", + Kind: "Deployment", + Name: "my-deployment", + UID: "1234", + }, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "example-container", + Image: "nginx", + }, + }, + }, + } + ns := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Annotations: map[string]string{ + apicommon.NamespaceEnabledAnnotation: "enabled", + }, + }, + } + fakeClient := fakeclient.NewClient(ns, pod) + + tr := &fakeclient.ITracerMock{StartFunc: func(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + return ctx, trace.SpanFromContext(ctx) + }} + + decoder := admission.NewDecoder(runtime.NewScheme()) + + wh := &PodMutatingWebhook{ + SchedulingGatesEnabled: true, + Client: fakeClient, + Tracer: tr, + Decoder: decoder, + EventSender: controllercommon.NewK8sSender(record.NewFakeRecorder(100)), + Log: testr.New(t), + } + + // Convert the Pod object to a byte array + podBytes, err := json.Marshal(pod) + require.Nil(t, err) + + // Create an AdmissionRequest object + request := admissionv1.AdmissionRequest{ + UID: "12345", + Kind: metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}, + Operation: admissionv1.Create, + Object: runtime.RawExtension{ + Raw: podBytes, + }, + Namespace: "default", + } + + resp := wh.Handle(context.TODO(), admission.Request{ + AdmissionRequest: request, + }) + + require.NotNil(t, resp) + require.True(t, resp.Allowed) + + op := jsonpatch.Operation{ + Operation: "add", + Path: "/spec/schedulingGates", + Value: []interface{}{map[string]interface{}{"name": apicommon.KeptnGate}}, + } + + require.Len(t, resp.Patches, 2) + if resp.Patches[0].Path == "/spec/schedulingGates" { + require.Equal(t, op, resp.Patches[0]) + } else { + require.Equal(t, op, resp.Patches[1]) + } + + kacr := &klcv1alpha3.KeptnAppCreationRequest{} + + err = fakeClient.Get(context.Background(), types.NamespacedName{ + Namespace: "default", + Name: "my-workload", + }, kacr) + + require.Nil(t, err) + + require.Equal(t, "my-workload", kacr.Spec.AppName) + require.Equal(t, string(apicommon.AppTypeSingleService), kacr.Annotations[apicommon.AppTypeAnnotation]) + + workload := &klcv1alpha3.KeptnWorkload{} + + err = fakeClient.Get(context.TODO(), types.NamespacedName{ + Namespace: "default", + Name: "my-workload-my-workload", + }, workload) + + require.Nil(t, err) + + require.Equal(t, klcv1alpha3.KeptnWorkloadSpec{ + AppName: kacr.Spec.AppName, + Version: "0.1", + ResourceReference: klcv1alpha3.ResourceReference{ + UID: "1234", + Kind: "Deployment", + Name: "my-deployment", + }, + }, workload.Spec) +} + func TestPodMutatingWebhook_Handle_SingleService_AppCreationRequestAlreadyPresent(t *testing.T) { fakeClient := fakeclient.NewClient(&corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{