diff --git a/operator/controllers/common/fake/fakeclient.go b/operator/controllers/common/fake/fakeclient.go index dd4dc356a1..0039bd514b 100644 --- a/operator/controllers/common/fake/fakeclient.go +++ b/operator/controllers/common/fake/fakeclient.go @@ -15,16 +15,14 @@ import ( // NewClient returns a new controller-runtime fake Client configured with the Operator's scheme, and initialized with objs. func NewClient(objs ...client.Object) client.Client { - setupSchemes() + SetupSchemes() return fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(objs...).Build() } -func setupSchemes() { +func SetupSchemes() { utilruntime.Must(clientgoscheme.AddToScheme(scheme.Scheme)) utilruntime.Must(corev1.AddToScheme(scheme.Scheme)) utilruntime.Must(apiv1.AddToScheme(scheme.Scheme)) - // utilruntime.Must(lfcv1alpha1.AddToScheme(scheme.Scheme)) - // utilruntime.Must(lfcv1alpha2.AddToScheme(scheme.Scheme)) utilruntime.Must(lfcv1alpha3.AddToScheme(scheme.Scheme)) utilruntime.Must(optionsv1alpha1.AddToScheme(scheme.Scheme)) utilruntime.Must(metricsapi.AddToScheme(scheme.Scheme)) diff --git a/operator/controllers/lifecycle/keptnappversion/controller_test.go b/operator/controllers/lifecycle/keptnappversion/controller_test.go index 2b022a25f6..85cb565d05 100644 --- a/operator/controllers/lifecycle/keptnappversion/controller_test.go +++ b/operator/controllers/lifecycle/keptnappversion/controller_test.go @@ -19,6 +19,7 @@ import ( "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + k8sfake "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) @@ -160,7 +161,7 @@ func setupReconcilerWithMeters() *KeptnAppVersionReconciler { return r } -func setupReconciler() (*KeptnAppVersionReconciler, chan string, *fake.ITracerMock, *fake.ISpanHandlerMock) { +func setupReconciler(objs ...client.Object) (*KeptnAppVersionReconciler, chan string, *fake.ITracerMock, *fake.ISpanHandlerMock) { //setup logger opts := zap.Options{ Development: true, @@ -185,7 +186,13 @@ func setupReconciler() (*KeptnAppVersionReconciler, chan string, *fake.ITracerMo UnbindSpanFunc: func(reconcileObject client.Object, phase string) error { return nil }, } - fakeClient := fake.NewClient() + workloadInstanceIndexer := func(obj client.Object) []string { + workloadInstance, _ := obj.(*lfcv1alpha3.KeptnWorkloadInstance) + return []string{workloadInstance.Spec.AppName} + } + + fake.SetupSchemes() + fakeClient := k8sfake.NewClientBuilder().WithObjects(objs...).WithScheme(scheme.Scheme).WithObjects().WithIndex(&lfcv1alpha3.KeptnWorkloadInstance{}, "spec.app", workloadInstanceIndexer).Build() recorder := record.NewFakeRecorder(100) r := &KeptnAppVersionReconciler{ diff --git a/operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go b/operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go index 1ca2a9ca3f..6b303e86d6 100644 --- a/operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go +++ b/operator/controllers/lifecycle/keptnappversion/reconcile_workloadsstate.go @@ -6,8 +6,7 @@ import ( klcv1alpha3 "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3" apicommon "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3/common" controllercommon "github.com/keptn/lifecycle-toolkit/operator/controllers/common" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) func (r *KeptnAppVersionReconciler) reconcileWorkloads(ctx context.Context, appVersion *klcv1alpha3.KeptnAppVersion) (apicommon.KeptnState, error) { @@ -20,18 +19,33 @@ func (r *KeptnAppVersionReconciler) reconcileWorkloads(ctx context.Context, appV LongName: "Reconcile Workloads", } - var newStatus []klcv1alpha3.WorkloadStatus + workloadInstanceList, err := r.getWorkloadInstanceList(ctx, appVersion.Namespace, appVersion.Spec.AppName) + if err != nil { + r.Log.Error(err, "Could not get workloads") + return apicommon.StateUnknown, r.handleUnaccessibleWorkloadInstanceList(ctx, appVersion) + } + + newStatus := make([]klcv1alpha3.WorkloadStatus, 0, len(appVersion.Spec.Workloads)) for _, w := range appVersion.Spec.Workloads { r.Log.Info("Reconciling workload " + w.Name) - workload, err := r.getWorkloadInstance(ctx, getWorkloadInstanceName(appVersion.Namespace, appVersion.Spec.AppName, w.Name, w.Version)) - if err != nil && errors.IsNotFound(err) { + workloadStatus := apicommon.StatePending + found := false + instanceName := getWorkloadInstanceName(appVersion.Spec.AppName, w.Name, w.Version) + for _, i := range workloadInstanceList.Items { + r.Log.Info("No WorkloadInstance found for KeptnApp " + appVersion.Spec.AppName) + // additional filtering of the retrieved WIs is needed, as the List() method retrieves all + // WIs for a specific KeptnApp. The result can contain also WIs, that are not part of the + // latest KeptnAppVersion, so it's needed to double check them + // no need to compare version, as it is part of WI name + if instanceName == i.Name { + found = true + workloadStatus = i.Status.Status + } + } + + if !found { controllercommon.RecordEvent(r.Recorder, phase, "Warning", appVersion, "NotFound", "workloadInstance not found", appVersion.GetVersion()) - workload.Status.Status = apicommon.StatePending - } else if err != nil { - r.Log.Error(err, "Could not get workload") - workload.Status.Status = apicommon.StateUnknown } - workloadStatus := workload.Status.Status newStatus = append(newStatus, klcv1alpha3.WorkloadStatus{ Workload: w, @@ -48,16 +62,31 @@ func (r *KeptnAppVersionReconciler) reconcileWorkloads(ctx context.Context, appV r.Log.Info("Workload status", "status", appVersion.Status.WorkloadStatus) // Write Status Field - err := r.Client.Status().Update(ctx, appVersion) + err = r.Client.Status().Update(ctx, appVersion) return overallState, err } -func (r *KeptnAppVersionReconciler) getWorkloadInstance(ctx context.Context, workload types.NamespacedName) (klcv1alpha3.KeptnWorkloadInstance, error) { - workloadInstance := &klcv1alpha3.KeptnWorkloadInstance{} - err := r.Get(ctx, workload, workloadInstance) - return *workloadInstance, err +func (r *KeptnAppVersionReconciler) getWorkloadInstanceList(ctx context.Context, namespace string, appName string) (*klcv1alpha3.KeptnWorkloadInstanceList, error) { + workloadInstanceList := &klcv1alpha3.KeptnWorkloadInstanceList{} + err := r.Client.List(ctx, workloadInstanceList, client.InNamespace(namespace), client.MatchingFields{ + "spec.app": appName, + }) + return workloadInstanceList, err +} + +func (r *KeptnAppVersionReconciler) handleUnaccessibleWorkloadInstanceList(ctx context.Context, appVersion *klcv1alpha3.KeptnAppVersion) error { + newStatus := make([]klcv1alpha3.WorkloadStatus, 0, len(appVersion.Spec.Workloads)) + for _, w := range appVersion.Spec.Workloads { + newStatus = append(newStatus, klcv1alpha3.WorkloadStatus{ + Workload: w, + Status: apicommon.StateUnknown, + }) + } + appVersion.Status.WorkloadOverallStatus = apicommon.StateUnknown + appVersion.Status.WorkloadStatus = newStatus + return r.Client.Status().Update(ctx, appVersion) } -func getWorkloadInstanceName(namespace string, appName string, workloadName string, version string) types.NamespacedName { - return types.NamespacedName{Namespace: namespace, Name: appName + "-" + workloadName + "-" + version} +func getWorkloadInstanceName(appName string, workloadName string, version string) string { + return appName + "-" + workloadName + "-" + version } diff --git a/operator/controllers/lifecycle/keptnappversion/reconcile_workloadstate_test.go b/operator/controllers/lifecycle/keptnappversion/reconcile_workloadstate_test.go new file mode 100644 index 0000000000..40edc38a48 --- /dev/null +++ b/operator/controllers/lifecycle/keptnappversion/reconcile_workloadstate_test.go @@ -0,0 +1,220 @@ +package keptnappversion + +import ( + "context" + "testing" + + lfcv1alpha3 "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3" + apicommon "github.com/keptn/lifecycle-toolkit/operator/apis/lifecycle/v1alpha3/common" + "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +//nolint:dogsled +func TestKeptnAppVersionReconciler_reconcileWorkloads_noWorkloads(t *testing.T) { + appVersion := &lfcv1alpha3.KeptnAppVersion{ + ObjectMeta: v1.ObjectMeta{ + Name: "appversion", + Namespace: "default", + }, + Spec: lfcv1alpha3.KeptnAppVersionSpec{ + AppName: "app", + }, + } + r, _, _, _ := setupReconciler(appVersion) + + state, err := r.reconcileWorkloads(context.TODO(), appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateSucceeded, state) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: appVersion.Namespace, Name: appVersion.Name}, appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateSucceeded, appVersion.Status.WorkloadOverallStatus) + require.Len(t, appVersion.Status.WorkloadStatus, 0) +} + +//nolint:dogsled +func TestKeptnAppVersionReconciler_reconcileWorkloads(t *testing.T) { + appVersion := &lfcv1alpha3.KeptnAppVersion{ + ObjectMeta: v1.ObjectMeta{ + Name: "appversion", + Namespace: "default", + }, + Spec: lfcv1alpha3.KeptnAppVersionSpec{ + KeptnAppSpec: lfcv1alpha3.KeptnAppSpec{ + Workloads: []lfcv1alpha3.KeptnWorkloadRef{ + { + Name: "workload", + Version: "ver1", + }, + }, + }, + AppName: "app", + }, + } + r, _, _, _ := setupReconciler(appVersion) + + // No workloadInstances are created yet, should stay in Pending state + + state, err := r.reconcileWorkloads(context.TODO(), appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StatePending, state) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: appVersion.Namespace, Name: appVersion.Name}, appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StatePending, appVersion.Status.WorkloadOverallStatus) + require.Len(t, appVersion.Status.WorkloadStatus, 1) + require.Equal(t, []lfcv1alpha3.WorkloadStatus{ + { + Workload: lfcv1alpha3.KeptnWorkloadRef{ + Name: "workload", + Version: "ver1", + }, + Status: apicommon.StatePending, + }, + }, appVersion.Status.WorkloadStatus) + + // Creating WorkloadInstace that is not part of the App -> should stay Pending + + wi1 := &lfcv1alpha3.KeptnWorkloadInstance{ + ObjectMeta: v1.ObjectMeta{ + Name: "workload", + Namespace: "default", + }, + Spec: lfcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: lfcv1alpha3.KeptnWorkloadSpec{ + AppName: "app2", + }, + }, + } + + err = r.Client.Create(context.TODO(), wi1) + require.Nil(t, err) + + state, err = r.reconcileWorkloads(context.TODO(), appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StatePending, state) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: appVersion.Namespace, Name: appVersion.Name}, appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StatePending, appVersion.Status.WorkloadOverallStatus) + require.Len(t, appVersion.Status.WorkloadStatus, 1) + require.Equal(t, []lfcv1alpha3.WorkloadStatus{ + { + Workload: lfcv1alpha3.KeptnWorkloadRef{ + Name: "workload", + Version: "ver1", + }, + Status: apicommon.StatePending, + }, + }, appVersion.Status.WorkloadStatus) + + // Creating WorkloadInstance of App with progressing state -> appVersion should be Progressing + + wi2 := &lfcv1alpha3.KeptnWorkloadInstance{ + ObjectMeta: v1.ObjectMeta{ + Name: "app-workload-ver1", + Namespace: "default", + }, + Spec: lfcv1alpha3.KeptnWorkloadInstanceSpec{ + KeptnWorkloadSpec: lfcv1alpha3.KeptnWorkloadSpec{ + AppName: "app", + }, + }, + } + + err = r.Client.Create(context.TODO(), wi2) + require.Nil(t, err) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: wi2.Namespace, Name: wi2.Name}, wi2) + require.Nil(t, err) + + wi2.Status.Status = apicommon.StateProgressing + err = r.Client.Update(context.TODO(), wi2) + require.Nil(t, err) + + state, err = r.reconcileWorkloads(context.TODO(), appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateProgressing, state) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: appVersion.Namespace, Name: appVersion.Name}, appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateProgressing, appVersion.Status.WorkloadOverallStatus) + require.Len(t, appVersion.Status.WorkloadStatus, 1) + require.Equal(t, []lfcv1alpha3.WorkloadStatus{ + { + Workload: lfcv1alpha3.KeptnWorkloadRef{ + Name: "workload", + Version: "ver1", + }, + Status: apicommon.StateProgressing, + }, + }, appVersion.Status.WorkloadStatus) + + // Updating WorkloadInstance of App with succeeded state -> appVersion should be Succeeded + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: wi2.Namespace, Name: wi2.Name}, wi2) + require.Nil(t, err) + + wi2.Status.Status = apicommon.StateSucceeded + err = r.Client.Update(context.TODO(), wi2) + require.Nil(t, err) + + state, err = r.reconcileWorkloads(context.TODO(), appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateSucceeded, state) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: appVersion.Namespace, Name: appVersion.Name}, appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateSucceeded, appVersion.Status.WorkloadOverallStatus) + require.Len(t, appVersion.Status.WorkloadStatus, 1) + require.Equal(t, []lfcv1alpha3.WorkloadStatus{ + { + Workload: lfcv1alpha3.KeptnWorkloadRef{ + Name: "workload", + Version: "ver1", + }, + Status: apicommon.StateSucceeded, + }, + }, appVersion.Status.WorkloadStatus) +} + +//nolint:dogsled +func TestKeptnAppVersionReconciler_handleUnaccessibleWorkloadInstanceList(t *testing.T) { + appVersion := &lfcv1alpha3.KeptnAppVersion{ + ObjectMeta: v1.ObjectMeta{ + Name: "appversion", + Namespace: "default", + }, + Spec: lfcv1alpha3.KeptnAppVersionSpec{ + KeptnAppSpec: lfcv1alpha3.KeptnAppSpec{ + Workloads: []lfcv1alpha3.KeptnWorkloadRef{ + { + Name: "workload", + Version: "ver1", + }, + }, + }, + AppName: "app", + }, + } + r, _, _, _ := setupReconciler(appVersion) + + err := r.handleUnaccessibleWorkloadInstanceList(context.TODO(), appVersion) + require.Nil(t, err) + + err = r.Client.Get(context.TODO(), types.NamespacedName{Namespace: appVersion.Namespace, Name: appVersion.Name}, appVersion) + require.Nil(t, err) + require.Equal(t, apicommon.StateUnknown, appVersion.Status.WorkloadOverallStatus) + require.Len(t, appVersion.Status.WorkloadStatus, 1) + require.Equal(t, []lfcv1alpha3.WorkloadStatus{ + { + Workload: lfcv1alpha3.KeptnWorkloadRef{ + Name: "workload", + Version: "ver1", + }, + Status: apicommon.StateUnknown, + }, + }, appVersion.Status.WorkloadStatus) +} diff --git a/operator/controllers/lifecycle/keptnworkloadinstance/controller.go b/operator/controllers/lifecycle/keptnworkloadinstance/controller.go index 72dfc0513d..4548caf6fa 100644 --- a/operator/controllers/lifecycle/keptnworkloadinstance/controller.go +++ b/operator/controllers/lifecycle/keptnworkloadinstance/controller.go @@ -212,6 +212,12 @@ func (r *KeptnWorkloadInstanceReconciler) finishKeptnWorkloadInstanceReconcile(c // SetupWithManager sets up the controller with the Manager. func (r *KeptnWorkloadInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &klcv1alpha3.KeptnWorkloadInstance{}, "spec.app", func(rawObj client.Object) []string { + workloadInstance := rawObj.(*klcv1alpha3.KeptnWorkloadInstance) + return []string{workloadInstance.Spec.AppName} + }); err != nil { + return err + } return ctrl.NewControllerManagedBy(mgr). // predicate disabling the auto reconciliation after updating the object status For(&klcv1alpha3.KeptnWorkloadInstance{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).