diff --git a/Makefile b/Makefile index 23cf7b6a..64a3294c 100644 --- a/Makefile +++ b/Makefile @@ -65,7 +65,7 @@ else endif # Default image repo -QUAY_REGISTRY ?= quay.io/opencloudio +QUAY_REGISTRY ?= quay.io/luzarragaben ICR_REIGSTRY ?= icr.io/cpopen ifeq ($(BUILD_LOCALLY),0) @@ -268,8 +268,8 @@ build-operator-image: $(CONFIG_DOCKER_TARGET) ## Build the operator image. --build-arg GOARCH=$(LOCAL_ARCH) -f Dockerfile . build-operator-dev-image: ## Build the operator dev image. - @echo "Building the $(DEV_REGISTRY)/$(OPERATOR_IMAGE_NAME) docker image..." - @docker build -t $(DEV_REGISTRY)/$(OPERATOR_IMAGE_NAME):$(VERSION) \ + @echo "Building the $(QUAY_REGISTRY)/$(OPERATOR_IMAGE_NAME) docker image..." + @docker build -t $(QUAY_REGISTRY)/$(OPERATOR_IMAGE_NAME):$(VERSION) \ --build-arg VCS_REF=$(VCS_REF) --build-arg RELEASE_VERSION=$(RELEASE_VERSION) \ --build-arg GOARCH=$(LOCAL_ARCH) -f Dockerfile . diff --git a/controllers/operandconfignoolm/operandconfig_controller_test.go b/controllers/operandconfignoolm/operandconfig_controller_test.go new file mode 100644 index 00000000..94b894c4 --- /dev/null +++ b/controllers/operandconfignoolm/operandconfig_controller_test.go @@ -0,0 +1,173 @@ +// +// Copyright 2022 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package operandconfignoolm + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "k8s.io/apimachinery/pkg/types" + + operatorv1alpha1 "github.com/IBM/operand-deployment-lifecycle-manager/v4/api/v1alpha1" + testutil "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/testutil" +) + +// +kubebuilder:docs-gen:collapse=Imports + +var _ = Describe("OperandConfig controller", func() { + const ( + name = "common-service" + namespace = "ibm-common-services" + requestName = "ibm-cloudpak-name" + requestNamespace = "ibm-cloudpak" + operatorNamespace = "ibm-operators" + ) + + var ( + ctx context.Context + + namespaceName string + operatorNamespaceName string + requestNamespaceName string + + registry *operatorv1alpha1.OperandRegistry + config *operatorv1alpha1.OperandConfig + request *operatorv1alpha1.OperandRequest + catalogSource *olmv1alpha1.CatalogSource + configKey types.NamespacedName + ) + + BeforeEach(func() { + ctx = context.Background() + namespaceName = testutil.CreateNSName(namespace) + operatorNamespaceName = testutil.CreateNSName(operatorNamespace) + requestNamespaceName = testutil.CreateNSName(requestNamespace) + registry = testutil.OperandRegistryObj(name, namespaceName, operatorNamespaceName) + config = testutil.OperandConfigObj(name, namespaceName) + request = testutil.OperandRequestObj(name, namespaceName, requestName, requestNamespaceName) + catalogSource = testutil.CatalogSource("community-operators", "openshift-marketplace") + configKey = types.NamespacedName{Name: name, Namespace: namespaceName} + + By("Creating the Namespace") + Expect(k8sClient.Create(ctx, testutil.NamespaceObj(namespaceName))).Should(Succeed()) + Expect(k8sClient.Create(ctx, testutil.NamespaceObj(operatorNamespaceName))).Should(Succeed()) + Expect(k8sClient.Create(ctx, testutil.NamespaceObj(requestNamespaceName))).Should(Succeed()) + Expect(k8sClient.Create(ctx, testutil.NamespaceObj("openshift-marketplace"))) + + By("Creating the CatalogSource") + Expect(k8sClient.Create(ctx, catalogSource)).Should(Succeed()) + catalogSource.Status = testutil.CatalogSourceStatus() + Expect(k8sClient.Status().Update(ctx, catalogSource)).Should(Succeed()) + By("Creating the OperandRegistry") + Expect(k8sClient.Create(ctx, registry)).Should(Succeed()) + By("Creating the OperandConfig") + Expect(k8sClient.Create(ctx, config)).Should(Succeed()) + }) + + AfterEach(func() { + By("Deleting the CatalogSource") + Expect(k8sClient.Delete(ctx, catalogSource)).Should(Succeed()) + By("Deleting the OperandRequest") + Expect(k8sClient.Delete(ctx, request)).Should(Succeed()) + By("Deleting the OperandConfig") + Expect(k8sClient.Delete(ctx, config)).Should(Succeed()) + By("Deleting the OperandRegistry") + Expect(k8sClient.Delete(ctx, registry)).Should(Succeed()) + }) + + Context("Initializing OperandConfig Status", func() { + It("Should the status of OperandConfig be Running", func() { + + By("Checking status of the OperandConfig") + Eventually(func() operatorv1alpha1.ServicePhase { + configInstance := &operatorv1alpha1.OperandConfig{} + Expect(k8sClient.Get(ctx, configKey, configInstance)).Should(Succeed()) + + return configInstance.Status.Phase + }, timeout, interval).Should(Equal(operatorv1alpha1.ServiceInit)) + + By("Creating the OperandRequest") + Expect(k8sClient.Create(ctx, request)).Should(Succeed()) + + By("Setting status of the Subscriptions") + jaegerSub := testutil.Subscription("jaeger", operatorNamespaceName) + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: "jaeger", Namespace: operatorNamespaceName}, jaegerSub) + jaegerSub.Status = testutil.SubscriptionStatus("jaeger", operatorNamespaceName, "0.0.1") + return k8sClient.Status().Update(ctx, jaegerSub) + }, timeout, interval).Should(Succeed()) + + mongodbSub := testutil.Subscription("mongodb-atlas-kubernetes", operatorNamespaceName) + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: "mongodb-atlas-kubernetes", Namespace: operatorNamespaceName}, mongodbSub) + mongodbSub.Status = testutil.SubscriptionStatus("mongodb-atlas-kubernetes", operatorNamespaceName, "0.0.1") + return k8sClient.Status().Update(ctx, mongodbSub) + }, timeout, interval).Should(Succeed()) + + By("Creating and Setting status of the ClusterServiceVersions") + jaegerCSV := testutil.ClusterServiceVersion("jaeger-csv.v0.0.1", "jaeger", operatorNamespaceName, testutil.JaegerExample) + Expect(k8sClient.Create(ctx, jaegerCSV)).Should(Succeed()) + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: "jaeger-csv.v0.0.1", Namespace: operatorNamespaceName}, jaegerCSV) + jaegerCSV.Status = testutil.ClusterServiceVersionStatus() + return k8sClient.Status().Update(ctx, jaegerCSV) + }, timeout, interval).Should(Succeed()) + + mongodbCSV := testutil.ClusterServiceVersion("mongodb-atlas-kubernetes-csv.v0.0.1", "mongodb-atlas-kubernetes", operatorNamespaceName, testutil.MongodbExample) + Expect(k8sClient.Create(ctx, mongodbCSV)).Should(Succeed()) + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: "mongodb-atlas-kubernetes-csv.v0.0.1", Namespace: operatorNamespaceName}, mongodbCSV) + mongodbCSV.Status = testutil.ClusterServiceVersionStatus() + return k8sClient.Status().Update(ctx, mongodbCSV) + }, timeout, interval).Should(Succeed()) + + By("Creating and Setting status of the InstallPlan") + jaegerIP := testutil.InstallPlan("jaeger-install-plan", operatorNamespaceName) + Expect(k8sClient.Create(ctx, jaegerIP)).Should(Succeed()) + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: "jaeger-install-plan", Namespace: operatorNamespaceName}, jaegerIP) + jaegerIP.Status = testutil.InstallPlanStatus() + return k8sClient.Status().Update(ctx, jaegerIP) + }, timeout, interval).Should(Succeed()) + + mongodbIP := testutil.InstallPlan("mongodb-atlas-kubernetes-install-plan", operatorNamespaceName) + Expect(k8sClient.Create(ctx, mongodbIP)).Should(Succeed()) + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: "mongodb-atlas-kubernetes-install-plan", Namespace: operatorNamespaceName}, mongodbIP) + mongodbIP.Status = testutil.InstallPlanStatus() + return k8sClient.Status().Update(ctx, mongodbIP) + }, timeout, interval).Should(Succeed()) + + By("Checking status of the OperandConfig") + Eventually(func() operatorv1alpha1.ServicePhase { + configInstance := &operatorv1alpha1.OperandConfig{} + Expect(k8sClient.Get(ctx, configKey, configInstance)).Should(Succeed()) + return configInstance.Status.Phase + }, timeout, interval).Should(Equal(operatorv1alpha1.ServiceRunning)) + + By("Cleaning up olm resources") + Expect(k8sClient.Delete(ctx, jaegerSub)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, mongodbSub)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, jaegerCSV)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, mongodbCSV)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, jaegerIP)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, mongodbIP)).Should(Succeed()) + }) + }) +}) diff --git a/controllers/operandconfignoolm/operandconfig_suite_test.go b/controllers/operandconfignoolm/operandconfig_suite_test.go new file mode 100644 index 00000000..2a831313 --- /dev/null +++ b/controllers/operandconfignoolm/operandconfig_suite_test.go @@ -0,0 +1,153 @@ +// +// Copyright 2022 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package operandconfignoolm + +import ( + "os" + "path/filepath" + "testing" + "time" + + jaegerv1 "github.com/jaegertracing/jaeger-operator/apis/v1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/onsi/gomega/gexec" + olmv1 "github.com/operator-framework/api/pkg/operators/v1" + olmv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + operatorsv1 "github.com/operator-framework/operator-lifecycle-manager/pkg/package-server/apis/operators/v1" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + nssv1 "github.com/IBM/ibm-namespace-scope-operator/api/v1" + + apiv1alpha1 "github.com/IBM/operand-deployment-lifecycle-manager/v4/api/v1alpha1" + "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandregistry" + "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandrequest" + deploy "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operator" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +const useExistingCluster = "USE_EXISTING_CLUSTER" + +var ( + cfg *rest.Config + k8sClient client.Client + testEnv *envtest.Environment + // scheme = runtime.NewScheme() + + timeout = time.Second * 900 + interval = time.Second * 5 +) + +func TestOperandConfig(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, + "OperandConfig Controller Suite") +} + +var _ = BeforeSuite(func(ctx SpecContext) { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + UseExistingCluster: UseExistingCluster(), + CRDDirectoryPaths: []string{filepath.Join("../..", "config", "crd", "bases"), filepath.Join("../..", "testcrds")}, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = apiv1alpha1.AddToScheme(clientgoscheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme + + err = nssv1.AddToScheme(clientgoscheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = olmv1alpha1.AddToScheme(clientgoscheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = olmv1.AddToScheme(clientgoscheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = jaegerv1.AddToScheme(clientgoscheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + err = operatorsv1.AddToScheme(clientgoscheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: clientgoscheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + // Start your controllers test logic + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: clientgoscheme.Scheme, + Metrics: server.Options{ + BindAddress: "0", + }, + }) + Expect(err).ToNot(HaveOccurred()) + + // Setup Manager with OperandRegistry Controller + err = (&operandregistry.Reconciler{ + ODLMOperator: deploy.NewODLMOperator(k8sManager, "OperandRegistry"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + // Setup Manager with OperandConfig Controller + err = (&Reconciler{ + ODLMOperator: deploy.NewODLMOperator(k8sManager, "OperandConfig"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + // Setup Manager with OperandRequest Controller + err = (&operandrequest.Reconciler{ + ODLMOperator: deploy.NewODLMOperator(k8sManager, "OperandRequest"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + // End your controllers test logic + +}, NodeTimeout(timeout)) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + gexec.KillAndWait(5 * time.Second) + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func UseExistingCluster() *bool { + use := false + if os.Getenv(useExistingCluster) != "" && os.Getenv(useExistingCluster) == "true" { + use = true + } + return &use +} diff --git a/controllers/operandconfignoolm/operandconfignoolm_controller.go b/controllers/operandconfignoolm/operandconfignoolm_controller.go new file mode 100644 index 00000000..cc736c85 --- /dev/null +++ b/controllers/operandconfignoolm/operandconfignoolm_controller.go @@ -0,0 +1,404 @@ +// +// Copyright 2022 IBM Corporation +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package operandconfignoolm + +import ( + "context" + "encoding/json" + "fmt" + "reflect" + "regexp" + "strings" + + "github.com/mohae/deepcopy" + "github.com/pkg/errors" + appsv1 "k8s.io/api/apps/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + operatorv1alpha1 "github.com/IBM/operand-deployment-lifecycle-manager/v4/api/v1alpha1" + "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/constant" + deploy "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operator" + "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/util" +) + +// Reconciler reconciles a OperandConfig object +type Reconciler struct { + *deploy.ODLMOperator +} + +//+kubebuilder:rbac:groups=operator.ibm.com,namespace="placeholder",resources=operandconfigs;operandconfigs/status;operandconfigs/finalizers,verbs=get;list;watch;create;update;patch;delete + +// Reconcile reads that state of the cluster for a OperandConfig object and makes changes based on the state read +// and what is in the OperandConfig.Spec +// Note: +// The Controller will requeue the Request to be processed again if the returned error is non-nil or +// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reconcileErr error) { + // Fetch the OperandConfig instance + instance := &operatorv1alpha1.OperandConfig{} + if err := r.Client.Get(ctx, req.NamespacedName, instance); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + klog.V(2).Infof("Reconciling OperandConfig: %s", req.NamespacedName) + + originalInstance := instance.DeepCopy() + + // Always attempt to patch the status after each reconciliation. + defer func() { + if reflect.DeepEqual(originalInstance.Status, instance.Status) { + return + } + if err := r.Client.Status().Patch(ctx, instance, client.MergeFrom(originalInstance)); err != nil { + reconcileErr = utilerrors.NewAggregate([]error{reconcileErr, fmt.Errorf("error while patching OperandConfig.Status: %v", err)}) + } + }() + + // Update status of OperandConfig by checking CRs + if err := r.updateStatus(ctx, instance); err != nil { + klog.Errorf("failed to update the status for OperandConfig %s : %v", req.NamespacedName.String(), err) + return ctrl.Result{}, err + } + + // Check if all the services are deployed + if instance.Status.Phase != operatorv1alpha1.ServiceInit && + instance.Status.Phase != operatorv1alpha1.ServiceRunning { + klog.V(2).Info("Waiting for all the services being deployed ...") + return ctrl.Result{RequeueAfter: constant.DefaultRequeueDuration}, nil + } + + klog.V(2).Infof("Finished reconciling OperandConfig: %s", req.NamespacedName) + return ctrl.Result{}, nil +} + +func (r *Reconciler) updateStatus(ctx context.Context, instance *operatorv1alpha1.OperandConfig) error { + // Create an empty ServiceStatus map + klog.V(3).Info("Initializing OperandConfig status") + + // Set the init status for OperandConfig instance + if instance.Status.Phase == "" { + instance.Status.Phase = operatorv1alpha1.ServiceInit + } + + originalStatus := deepcopy.Copy(instance.Status.ServiceStatus) + instance.Status.ServiceStatus = make(map[string]operatorv1alpha1.CrStatus) + + registryInstance, err := r.GetOperandRegistry(ctx, types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}) + if err != nil { + return err + } + + for _, op := range registryInstance.Spec.Operators { + + op := op + + service := instance.GetService(op.Name, op.ConfigName) + if service == nil { + continue + } + + // Check if the operator is request in the OperandRegistry + if !checkRegistryStatus(op.Name, registryInstance) { + continue + } + + namespace := r.GetOperatorNamespace(op.InstallMode, op.Namespace) + + klog.V(1).Info("Looking for deployment for the operator: ", op.Name) + var deployment *appsv1.Deployment + deploymentList, err := r.GetDeploymentListFromPackage(ctx, op.PackageName, op.Namespace) + deployment = deploymentList[0] + + if deployment == nil { + klog.Warningf("Deployment for the Operator %s/%s doesn't exist, retry...", namespace, op.Name) + continue + } + + _, ok := instance.Status.ServiceStatus[op.Name] + + if !ok { + instance.Status.ServiceStatus[op.Name] = operatorv1alpha1.CrStatus{} + } + + if instance.Status.ServiceStatus[op.Name].CrStatus == nil { + tmp := instance.Status.ServiceStatus[op.Name] + tmp.CrStatus = make(map[string]operatorv1alpha1.ServicePhase) + instance.Status.ServiceStatus[op.Name] = tmp + } + + merr := &util.MultiErr{} + + // handle the deletion of k8s resources + k8sError := r.deleteK8sReousceFromStatus(ctx, originalStatus.(map[string]operatorv1alpha1.CrStatus), service, &op) + if k8sError != nil { + merr.Add(k8sError) + } + + // update the status for kubernetes resources + k8sResources := service.Resources + for _, resource := range k8sResources { + var k8sUnstruct unstructured.Unstructured + k8sAPIVersion := resource.APIVersion + k8sKind := resource.Kind + k8sName := resource.Name + k8sNamespace := instance.Namespace + if resource.Namespace != "" { + k8sNamespace = resource.Namespace + } + resourceKey := k8sAPIVersion + "@" + k8sKind + "@" + k8sNamespace + "@" + k8sName + + k8sUnstruct.SetAPIVersion(k8sAPIVersion) + k8sUnstruct.SetKind(k8sKind) + k8sGetError := r.Client.Get(ctx, types.NamespacedName{ + Name: k8sName, + Namespace: k8sNamespace, + }, &k8sUnstruct) + + if k8sGetError != nil && !apierrors.IsNotFound(k8sGetError) { + instance.Status.ServiceStatus[op.Name].CrStatus[resourceKey] = operatorv1alpha1.ServiceFailed + } else if apierrors.IsNotFound(k8sGetError) { + instance.Status.ServiceStatus[op.Name].CrStatus[resourceKey] = operatorv1alpha1.ServiceCreating + } else { + instance.Status.ServiceStatus[op.Name].CrStatus[resourceKey] = operatorv1alpha1.ServiceRunning + } + } + + // update the status for custom resources + almExamples := deployment.GetAnnotations()["alm-examples"] + if almExamples == "" { + klog.Warningf("Not found alm-examples in the Deployment %s/%s", &deployment.Namespace, &deployment.Name) + continue + } + // Create a slice for crTemplates + var crTemplates []interface{} + + // Convert CR template string to slice + err = json.Unmarshal([]byte(almExamples), &crTemplates) + if err != nil { + return errors.Wrapf(err, "failed to convert alm-examples in the Deployment %s/%s to slice", &deployment.Namespace, &deployment.Name) + } + + // Merge OperandConfig and ClusterServiceVersion alm-examples + for _, crTemplate := range crTemplates { + + // Create an unstruct object for CR and request its value to CR template + var unstruct unstructured.Unstructured + unstruct.Object = crTemplate.(map[string]interface{}) + + kind := unstruct.Object["kind"].(string) + + existinConfig := false + for crName := range service.Spec { + // Compare the name of OperandConfig and CRD name + if strings.EqualFold(kind, crName) { + existinConfig = true + } + } + + if !existinConfig { + continue + } + + name := unstruct.GetName() + if name == "" { + continue + } + + getError := r.Client.Get(ctx, types.NamespacedName{ + Name: name, + Namespace: instance.Namespace, + }, &unstruct) + + if getError != nil && !apierrors.IsNotFound(getError) { + instance.Status.ServiceStatus[op.Name].CrStatus[kind] = operatorv1alpha1.ServiceFailed + } else if apierrors.IsNotFound(getError) { + instance.Status.ServiceStatus[op.Name].CrStatus[kind] = operatorv1alpha1.ServiceCreating + } else { + instance.Status.ServiceStatus[op.Name].CrStatus[kind] = operatorv1alpha1.ServiceRunning + } + } + if len(merr.Errors) != 0 { + return merr + } + } + + klog.V(2).Info("Updating OperandConfig status") + instance.UpdateOperandPhase() + + return nil +} + +// deleteK8sReousceFromStatus deletes the k8s resources from OperandConfig Status when they are not defined in OperandConfig Spec anymore +func (r *Reconciler) deleteK8sReousceFromStatus(ctx context.Context, serviceStatus map[string]operatorv1alpha1.CrStatus, service *operatorv1alpha1.ConfigService, op *operatorv1alpha1.Operator) error { + merr := &util.MultiErr{} + reg, _ := regexp.Compile(`^(.*)\@(.*)\@(.*)\@(.*)`) + var existingResList []string + for key := range serviceStatus[op.Name].CrStatus { + if reg.MatchString((key)) { + existingResList = append(existingResList, key) + } + } + + for _, resKey := range existingResList { + separateRes := strings.Split(resKey, "@") + + // based on the regexp "regexp.Compile(`^(.*)\@(.*)\@(.*)\@(.*)`)" above, check if resKey can be split properly to a length 4 Slice + if len(separateRes) != 4 { + err := fmt.Errorf("%s cannot be split to a length 4 Slice by @", resKey) + merr.Add(err) + continue + } + k8sAPIVersion := separateRes[0] + k8sKind := separateRes[1] + k8sNamespace := separateRes[2] + k8sName := separateRes[3] + + k8sResources := service.Resources + isInConfig := false + for _, resource := range k8sResources { + if resource.Name == k8sName && resource.Kind == k8sKind { + isInConfig = true + } + } + // start the deletion if the resource found in status but not in config spec + if !isInConfig { + err := r.deleteK8sReousce(ctx, k8sAPIVersion, k8sKind, k8sName, k8sNamespace) + if err != nil { + merr.Add(err) + } + } + } + if len(merr.Errors) != 0 { + return merr + } + return nil +} + +func (r *Reconciler) deleteK8sReousce(ctx context.Context, k8sAPIVersion, k8sKind, k8sName, k8sNamespace string) error { + var k8sUnstruct unstructured.Unstructured + k8sUnstruct.SetAPIVersion(k8sAPIVersion) + k8sUnstruct.SetKind(k8sKind) + k8sGetError := r.Client.Get(ctx, types.NamespacedName{ + Name: k8sName, + Namespace: k8sNamespace, + }, &k8sUnstruct) + + if k8sGetError != nil && !apierrors.IsNotFound(k8sGetError) { + return errors.Wrapf(k8sGetError, "failed to get k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + } else if apierrors.IsNotFound(k8sGetError) { + klog.V(3).Infof("There is no k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + } else { + if r.CheckLabel(k8sUnstruct, map[string]string{constant.OpreqLabel: "true"}) { + klog.V(3).Infof("Deleting k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + k8sDeleteError := r.Delete(ctx, &k8sUnstruct, client.PropagationPolicy(metav1.DeletePropagationBackground)) + if k8sDeleteError != nil && !apierrors.IsNotFound(k8sDeleteError) { + return errors.Wrapf(k8sDeleteError, "failed to delete k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + } + waitErr := wait.PollUntilContextTimeout(ctx, constant.DefaultCRDeletePeriod, constant.DefaultCRDeleteTimeout, true, func(ctx context.Context) (bool, error) { + klog.V(3).Infof("Waiting for k8s resource -- Kind: %s, NamespacedName: %s/%s removed ...", k8sKind, k8sNamespace, k8sName) + err := r.Client.Get(ctx, types.NamespacedName{ + Name: k8sName, + Namespace: k8sNamespace, + }, &k8sUnstruct) + if apierrors.IsNotFound(err) { + return true, nil + } + if err != nil { + return false, errors.Wrapf(err, "failed to get k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + } + return false, nil + }) + if waitErr != nil { + return errors.Wrapf(waitErr, "failed to delete k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + } + klog.V(1).Infof("Finish deleting k8s resource -- Kind: %s, NamespacedName: %s/%s", k8sKind, k8sNamespace, k8sName) + } + } + return nil +} + +func checkRegistryStatus(opName string, registryInstance *operatorv1alpha1.OperandRegistry) bool { + status := registryInstance.Status.OperatorsStatus + for opRegistryName := range status { + if opName == opRegistryName { + return true + } + } + return false +} + +func (r *Reconciler) getRequestToConfigMapper(ctx context.Context, obj client.Object) []reconcile.Request { + opreqInstance := &operatorv1alpha1.OperandRequest{} + requests := []reconcile.Request{} + // If the OperandRequest has been deleted, reconcile all the OperandConfig in the cluster + if err := r.Client.Get(ctx, types.NamespacedName{Name: obj.GetName(), Namespace: obj.GetNamespace()}, opreqInstance); apierrors.IsNotFound(err) { + configList := &operatorv1alpha1.OperandConfigList{} + _ = r.Client.List(ctx, configList) + for _, config := range configList.Items { + namespaceName := types.NamespacedName{Name: config.Name, Namespace: config.Namespace} + req := reconcile.Request{NamespacedName: namespaceName} + requests = append(requests, req) + } + return requests + } + + // If the OperandRequest exist, reconcile OperandConfigs specific in the OperandRequest instance. + for _, request := range opreqInstance.Spec.Requests { + registryKey := opreqInstance.GetRegistryKey(request) + req := reconcile.Request{NamespacedName: registryKey} + requests = append(requests, req) + } + return requests +} + +// SetupWithManager adds OperandConfig controller to the manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + options := controller.Options{ + MaxConcurrentReconciles: r.MaxConcurrentReconciles, // Set the desired value for max concurrent reconciles. + } + return ctrl.NewControllerManagedBy(mgr). + WithOptions(options). + For(&operatorv1alpha1.OperandConfig{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Watches(&operatorv1alpha1.OperandRequest{}, handler.EnqueueRequestsFromMapFunc(r.getRequestToConfigMapper), builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Evaluates to false if the object has been confirmed deleted. + return !e.DeleteStateUnknown + }, + UpdateFunc: func(e event.UpdateEvent) bool { + oldObject := e.ObjectOld.(*operatorv1alpha1.OperandRequest) + newObject := e.ObjectNew.(*operatorv1alpha1.OperandRequest) + return !reflect.DeepEqual(oldObject.Status, newObject.Status) + }, + })).Complete(r) +} diff --git a/main.go b/main.go index ff547f42..795bd4ea 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,7 @@ import ( "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/namespacescope" "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandbindinfo" "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandconfig" + "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandconfignoolm" "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandregistry" "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandrequest" "github.com/IBM/operand-deployment-lifecycle-manager/v4/controllers/operandrequestnoolm" @@ -116,6 +117,12 @@ func main() { klog.Errorf("unable to create controller OperandRequestNoOLM: %v", err) os.Exit(1) } + if err = (&operandconfignoolm.Reconciler{ + ODLMOperator: deploy.NewODLMOperator(mgr, "OperandConfig"), + }).SetupWithManager(mgr); err != nil { + klog.Errorf("unable to create controller OperandConfigNoOLM: %v", err) + os.Exit(1) + } } else { if err = (&operandrequest.Reconciler{ ODLMOperator: deploy.NewODLMOperator(mgr, "OperandRequest"), @@ -124,12 +131,12 @@ func main() { klog.Errorf("unable to create controller OperandRequest: %v", err) os.Exit(1) } - } - if err = (&operandconfig.Reconciler{ - ODLMOperator: deploy.NewODLMOperator(mgr, "OperandConfig"), - }).SetupWithManager(mgr); err != nil { - klog.Errorf("unable to create controller OperandConfig: %v", err) - os.Exit(1) + if err = (&operandconfig.Reconciler{ + ODLMOperator: deploy.NewODLMOperator(mgr, "OperandConfig"), + }).SetupWithManager(mgr); err != nil { + klog.Errorf("unable to create controller OperandConfig: %v", err) + os.Exit(1) + } } if err = (&operandbindinfo.Reconciler{ ODLMOperator: deploy.NewODLMOperator(mgr, "OperandBindInfo"),