diff --git a/operator/api/v1alpha1/keptnappversion_types.go b/operator/api/v1alpha1/keptnappversion_types.go index a7e21a7ae0..0695dbb9f0 100644 --- a/operator/api/v1alpha1/keptnappversion_types.go +++ b/operator/api/v1alpha1/keptnappversion_types.go @@ -355,3 +355,7 @@ func (a KeptnAppVersion) GetSpanAttributes() []attribute.KeyValue { common.AppNamespace.String(a.Namespace), } } + +func (v KeptnAppVersion) GetWorkloadNameOfApp(workloadName string) string { + return fmt.Sprintf("%s-%s", v.Spec.AppName, workloadName) +} diff --git a/operator/api/v1alpha1/keptnappversion_types_test.go b/operator/api/v1alpha1/keptnappversion_types_test.go new file mode 100644 index 0000000000..68f4c47396 --- /dev/null +++ b/operator/api/v1alpha1/keptnappversion_types_test.go @@ -0,0 +1,41 @@ +package v1alpha1 + +import ( + "testing" +) + +func TestKeptnAppVersion_GetWorkloadNameOfApp(t *testing.T) { + type fields struct { + Spec KeptnAppVersionSpec + } + type args struct { + workloadName string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "", + fields: fields{ + Spec: KeptnAppVersionSpec{AppName: "my-app"}, + }, + args: args{ + workloadName: "my-workload", + }, + want: "my-app-my-workload", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := KeptnAppVersion{ + Spec: tt.fields.Spec, + } + if got := v.GetWorkloadNameOfApp(tt.args.workloadName); got != tt.want { + t.Errorf("GetWorkloadNameOfApp() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/operator/controllers/keptnworkloadinstance/controller.go b/operator/controllers/keptnworkloadinstance/controller.go index 918f0443bf..9f7dbe773e 100644 --- a/operator/controllers/keptnworkloadinstance/controller.go +++ b/operator/controllers/keptnworkloadinstance/controller.go @@ -19,7 +19,6 @@ package keptnworkloadinstance import ( "context" "fmt" - "reflect" "time" "go.opentelemetry.io/otel" @@ -274,35 +273,45 @@ func (r *KeptnWorkloadInstanceReconciler) getAppVersion(ctx context.Context, app func (r *KeptnWorkloadInstanceReconciler) getAppVersionForWorkloadInstance(ctx context.Context, wli *klcv1alpha1.KeptnWorkloadInstance) (bool, klcv1alpha1.KeptnAppVersion, error) { apps := &klcv1alpha1.KeptnAppVersionList{} + // TODO add label selector for looking up by name? if err := r.Client.List(ctx, apps, client.InNamespace(wli.Namespace)); err != nil { return false, klcv1alpha1.KeptnAppVersion{}, err } + + workloadFound, latestVersion, err := getLatestAppVersion(apps, wli) + if err != nil { + r.Log.Error(err, "could not look up KeptnAppVersion for WorkloadInstance") + return false, latestVersion, err + } + + if latestVersion.Spec.Version == "" || !workloadFound { + return false, klcv1alpha1.KeptnAppVersion{}, nil + } + return true, latestVersion, nil +} + +func getLatestAppVersion(apps *klcv1alpha1.KeptnAppVersionList, wli *klcv1alpha1.KeptnWorkloadInstance) (bool, klcv1alpha1.KeptnAppVersion, error) { latestVersion := klcv1alpha1.KeptnAppVersion{} + // ignore the potential error since this can not return an error with 0.0.0 + oldVersion, _ := version.NewVersion("0.0.0") + + workloadFound := false for _, app := range apps.Items { if app.Spec.AppName == wli.Spec.AppName { - for _, appWorkload := range app.Spec.Workloads { - if !reflect.DeepEqual(latestVersion, app) { - latestVersion = app - } else if appWorkload.Version == wli.Spec.Version && fmt.Sprintf("%s-%s", app.Spec.AppName, appWorkload.Name) == wli.Spec.WorkloadName { - oldVersion, err := version.NewVersion(app.Spec.Version) - if err != nil { - r.Log.Error(err, "could not parse version") - } - newVersion, err := version.NewVersion(latestVersion.Spec.Version) + if appWorkload.Version == wli.Spec.Version && app.GetWorkloadNameOfApp(appWorkload.Name) == wli.Spec.WorkloadName { + workloadFound = true + newVersion, err := version.NewVersion(app.Spec.Version) if err != nil { - r.Log.Error(err, "could not parse version") + return false, klcv1alpha1.KeptnAppVersion{}, err } - if oldVersion.LessThan(newVersion) { + if newVersion.GreaterThan(oldVersion) { latestVersion = app + oldVersion = newVersion } } } } } - - if latestVersion.Spec.Version == "" { - return false, klcv1alpha1.KeptnAppVersion{}, nil - } - return true, latestVersion, nil + return workloadFound, latestVersion, nil } diff --git a/operator/controllers/keptnworkloadinstance/controller_test.go b/operator/controllers/keptnworkloadinstance/controller_test.go index 375b7a34da..7b1206b66c 100644 --- a/operator/controllers/keptnworkloadinstance/controller_test.go +++ b/operator/controllers/keptnworkloadinstance/controller_test.go @@ -2,14 +2,14 @@ package keptnworkloadinstance import ( "context" - "testing" - - "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" + klcv1alpha1 "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" + "github.com/stretchr/testify/require" testrequire "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "testing" ) func TestKeptnWorkloadInstanceReconciler_IsPodRunning(t *testing.T) { @@ -20,7 +20,7 @@ func TestKeptnWorkloadInstanceReconciler_IsPodRunning(t *testing.T) { r := &KeptnWorkloadInstanceReconciler{ Client: fake.NewClientBuilder().WithLists(podList).Build(), } - isPodRunning, err := r.isPodRunning(context.TODO(), v1alpha1.ResourceReference{UID: types.UID("pod1")}, "node1") + isPodRunning, err := r.isPodRunning(context.TODO(), klcv1alpha1.ResourceReference{UID: types.UID("pod1")}, "node1") testrequire.Nil(t, err) if !isPodRunning { t.Errorf("Wrong!") @@ -29,7 +29,7 @@ func TestKeptnWorkloadInstanceReconciler_IsPodRunning(t *testing.T) { r2 := &KeptnWorkloadInstanceReconciler{ Client: fake.NewClientBuilder().WithLists(podList2).Build(), } - isPodRunning, err = r2.isPodRunning(context.TODO(), v1alpha1.ResourceReference{UID: types.UID("pod1")}, "node1") + isPodRunning, err = r2.isPodRunning(context.TODO(), klcv1alpha1.ResourceReference{UID: types.UID("pod1")}, "node1") testrequire.Nil(t, err) if isPodRunning { t.Errorf("Wrong!") @@ -50,3 +50,214 @@ func makeNominatedPod(podName string, nodeName string, phase v1.PodPhase) v1.Pod }, } } + +func Test_getLatestAppVersion(t *testing.T) { + type args struct { + apps *klcv1alpha1.KeptnAppVersionList + wli *klcv1alpha1.KeptnWorkloadInstance + } + tests := []struct { + name string + args args + wantFound bool + wantAppVersion klcv1alpha1.KeptnAppVersion + wantErr bool + }{ + { + name: "app version found", + args: args{ + apps: &klcv1alpha1.KeptnAppVersionList{ + Items: []klcv1alpha1.KeptnAppVersion{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnAppVersionSpec{ + KeptnAppSpec: klcv1alpha1.KeptnAppSpec{ + Version: "1.0", + Workloads: []klcv1alpha1.KeptnWorkloadRef{ + { + Name: "my-workload", + Version: "1.0", + }, + }, + }, + AppName: "my-app", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnAppVersionSpec{ + KeptnAppSpec: klcv1alpha1.KeptnAppSpec{ + Version: "2.0", + Workloads: []klcv1alpha1.KeptnWorkloadRef{ + { + Name: "my-workload", + Version: "1.0", + }, + }, + }, + AppName: "my-app", + }, + }, + }, + }, + wli: &klcv1alpha1.KeptnWorkloadInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workloadinstance", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha1.KeptnWorkloadSpec{ + AppName: "my-app", + Version: "1.0", + }, + WorkloadName: "my-app-my-workload", + }, + }, + }, + wantFound: true, + wantAppVersion: klcv1alpha1.KeptnAppVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnAppVersionSpec{ + KeptnAppSpec: klcv1alpha1.KeptnAppSpec{ + Version: "2.0", + Workloads: []klcv1alpha1.KeptnWorkloadRef{ + { + Name: "my-workload", + Version: "1.0", + }, + }, + }, + AppName: "my-app", + }, + }, + wantErr: false, + }, + { + name: "app version not found", + args: args{ + apps: &klcv1alpha1.KeptnAppVersionList{ + Items: []klcv1alpha1.KeptnAppVersion{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnAppVersionSpec{ + KeptnAppSpec: klcv1alpha1.KeptnAppSpec{ + Version: "1.0", + Workloads: []klcv1alpha1.KeptnWorkloadRef{ + { + Name: "my-other-workload", + Version: "1.0", + }, + }, + }, + AppName: "my-app", + }, + }, + }, + }, + wli: &klcv1alpha1.KeptnWorkloadInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workloadinstance", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha1.KeptnWorkloadSpec{ + AppName: "my-app", + Version: "1.0", + }, + WorkloadName: "my-app-my-workload", + }, + }, + }, + wantFound: false, + wantAppVersion: klcv1alpha1.KeptnAppVersion{}, + wantErr: false, + }, + { + name: "app version with invalid version", + args: args{ + apps: &klcv1alpha1.KeptnAppVersionList{ + Items: []klcv1alpha1.KeptnAppVersion{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-app", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnAppVersionSpec{ + KeptnAppSpec: klcv1alpha1.KeptnAppSpec{ + Version: "", + Workloads: []klcv1alpha1.KeptnWorkloadRef{ + { + Name: "my-workload", + Version: "1.0", + }, + }, + }, + AppName: "my-app", + }, + }, + }, + }, + wli: &klcv1alpha1.KeptnWorkloadInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workloadinstance", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha1.KeptnWorkloadSpec{ + AppName: "my-app", + Version: "1.0", + }, + WorkloadName: "my-app-my-workload", + }, + }, + }, + wantFound: false, + wantAppVersion: klcv1alpha1.KeptnAppVersion{}, + wantErr: true, + }, + { + name: "app version list empty", + args: args{ + apps: &klcv1alpha1.KeptnAppVersionList{ + Items: []klcv1alpha1.KeptnAppVersion{}, + }, + wli: &klcv1alpha1.KeptnWorkloadInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-workloadinstance", + Namespace: "default", + }, + Spec: klcv1alpha1.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha1.KeptnWorkloadSpec{ + AppName: "my-app", + Version: "1.0", + }, + WorkloadName: "my-app-my-workload", + }, + }, + }, + wantFound: false, + wantAppVersion: klcv1alpha1.KeptnAppVersion{}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + found, gotAppVersion, err := getLatestAppVersion(tt.args.apps, tt.args.wli) + require.Equal(t, tt.wantErr, err != nil) + require.Equal(t, tt.wantFound, found) + require.Equal(t, tt.wantAppVersion, gotAppVersion) + }) + } +} diff --git a/operator/test/component/common.go b/operator/test/component/common.go new file mode 100644 index 0000000000..b1a4addef1 --- /dev/null +++ b/operator/test/component/common.go @@ -0,0 +1,33 @@ +package component + +import ( + "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1/common" + "go.opentelemetry.io/otel/metric/instrument" + "go.opentelemetry.io/otel/metric/unit" + "go.opentelemetry.io/otel/sdk/metric" +) + +func initKeptnMeters() common.KeptnMeters { + provider := metric.NewMeterProvider() + meter := provider.Meter("keptn/task") + deploymentCount, _ := meter.SyncInt64().Counter("keptn.deployment.count", instrument.WithDescription("a simple counter for Keptn Deployments")) + deploymentDuration, _ := meter.SyncFloat64().Histogram("keptn.deployment.duration", instrument.WithDescription("a histogram of duration for Keptn Deployments"), instrument.WithUnit(unit.Unit("s"))) + taskCount, _ := meter.SyncInt64().Counter("keptn.task.count", instrument.WithDescription("a simple counter for Keptn Tasks")) + taskDuration, _ := meter.SyncFloat64().Histogram("keptn.task.duration", instrument.WithDescription("a histogram of duration for Keptn Tasks"), instrument.WithUnit(unit.Unit("s"))) + appCount, _ := meter.SyncInt64().Counter("keptn.app.count", instrument.WithDescription("a simple counter for Keptn Apps")) + appDuration, _ := meter.SyncFloat64().Histogram("keptn.app.duration", instrument.WithDescription("a histogram of duration for Keptn Apps"), instrument.WithUnit(unit.Unit("s"))) + evaluationCount, _ := meter.SyncInt64().Counter("keptn.evaluation.count", instrument.WithDescription("a simple counter for Keptn Evaluations")) + evaluationDuration, _ := meter.SyncFloat64().Histogram("keptn.evaluation.duration", instrument.WithDescription("a histogram of duration for Keptn Evaluations"), instrument.WithUnit(unit.Unit("s"))) + + meters := common.KeptnMeters{ + TaskCount: taskCount, + TaskDuration: taskDuration, + DeploymentCount: deploymentCount, + DeploymentDuration: deploymentDuration, + AppCount: appCount, + AppDuration: appDuration, + EvaluationCount: evaluationCount, + EvaluationDuration: evaluationDuration, + } + return meters +} diff --git a/operator/test/component/workloadinstancecontroller_test.go b/operator/test/component/workloadinstancecontroller_test.go new file mode 100644 index 0000000000..ec4ac39576 --- /dev/null +++ b/operator/test/component/workloadinstancecontroller_test.go @@ -0,0 +1,148 @@ +package component + +import ( + "context" + klcv1alpha1 "github.com/keptn/lifecycle-toolkit/operator/api/v1alpha1" + keptncontroller "github.com/keptn/lifecycle-toolkit/operator/controllers/common" + "github.com/keptn/lifecycle-toolkit/operator/controllers/keptnworkloadinstance" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + otelsdk "go.opentelemetry.io/otel/sdk/trace" + sdktest "go.opentelemetry.io/otel/sdk/trace/tracetest" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// clean example of component test (E2E test/ integration test can be achieved adding a real cluster) +// App controller creates AppVersion when a new App CRD is added +// span for creation and reconcile are correct +// container must be ordered to have the before all setup +// this way the container spec check is not randomized, so we can make +// assertions on spans number and traces +var _ = Describe("KeptnWorkloadInstanceController", Ordered, func() { + var ( + name string + namespace string + version string + spanRecorder *sdktest.SpanRecorder + tracer *otelsdk.TracerProvider + ) + + BeforeAll(func() { + //setup once + By("Waiting for Manager") + Eventually(func() bool { + return k8sManager != nil + }).Should(Equal(true)) + + By("Creating the Controller") + + spanRecorder = sdktest.NewSpanRecorder() + tracer = otelsdk.NewTracerProvider(otelsdk.WithSpanProcessor(spanRecorder)) + + ////setup controllers here + controllers := []keptncontroller.Controller{&keptnworkloadinstance.KeptnWorkloadInstanceReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: k8sManager.GetEventRecorderFor("test-app-controller"), + Log: GinkgoLogr, + Meters: initKeptnMeters(), + Tracer: tracer.Tracer("test-app-tracer"), + }} + setupManager(controllers) // we can register multiple time the same controller + // so that they have a different span/trace + + //for a fake controller you can also use + //controller, err := controller.New("app-controller", cm, controller.Options{ + // Reconciler: reconcile.Func( + // func(_ context.Context, request reconcile.Request) (reconcile.Result, error) { + // reconciled <- request + // return reconcile.Result{}, nil + // }), + //}) + //Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func() { // list var here they will be copied for every spec + name = "test-app" + namespace = "default" // namespaces are not deleted in the api so be careful + // when creating you can use ignoreAlreadyExists(err error) + version = "1.0.0" + }) + Describe("Creation of WorkloadInstance", func() { + var ( + appVersion *klcv1alpha1.KeptnAppVersion + wi *klcv1alpha1.KeptnWorkloadInstance + ) + Context("with a new AppVersions CRD", func() { + + BeforeEach(func() { + appVersion = createAppVersionInCluster(name, namespace, version) + }) + + It("should fail if Workload not found in AppVersion", func() { + wiName := "not-found" + wi = &klcv1alpha1.KeptnWorkloadInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: klcv1alpha1.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: klcv1alpha1.KeptnWorkloadSpec{}, + WorkloadName: "wi-test-app-wname-" + wiName, + TraceId: map[string]string{"traceparent": "00-0f89f15e562489e2e171eca1cf9ba958-d2fa6dbbcbf7e29a-01"}, + }, + } + By("Creating WorkloadInstance") + err := k8sClient.Create(context.TODO(), wi) + Expect(err).To(BeNil()) + + By("Ensuring WorkloadInstance does not progress to next phase") + wiNameObj := types.NamespacedName{ + Namespace: wi.Namespace, + Name: wi.Name, + } + Consistently(func(g Gomega) { + wi := &klcv1alpha1.KeptnWorkloadInstance{} + err := k8sClient.Get(ctx, wiNameObj, wi) + g.Expect(err).To(BeNil()) + g.Expect(wi).To(Not(BeNil())) + g.Expect(wi.Status.CurrentPhase).To(BeEmpty()) + }, "3s").Should(Succeed()) + }) + AfterEach(func() { + // Remember to clean up the cluster after each test + k8sClient.Delete(ctx, appVersion) + k8sClient.Delete(ctx, wi) + // Reset span recorder after each spec + resetSpanRecords(tracer, spanRecorder) + }) + + }) + + }) +}) + +func createAppVersionInCluster(name string, namespace string, version string) *klcv1alpha1.KeptnAppVersion { + instance := &klcv1alpha1.KeptnAppVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: klcv1alpha1.KeptnAppVersionSpec{ + KeptnAppSpec: klcv1alpha1.KeptnAppSpec{ + Version: version, + Workloads: []klcv1alpha1.KeptnWorkloadRef{ + { + Name: "wi-test-app-wname", + Version: "2.0", + }, + }, + }, + }, + } + By("Invoking Reconciling for Create") + + Expect(ignoreAlreadyExists(k8sClient.Create(ctx, instance))).Should(Succeed()) + return instance +}