diff --git a/common/types.go b/common/types.go index 0cc091ed..0babd14e 100644 --- a/common/types.go +++ b/common/types.go @@ -133,6 +133,7 @@ type BaseComponentCertificate interface { type BaseComponentNetworkPolicy interface { GetNamespaceLabels() map[string]string GetFromLabels() map[string]string + IsDisabled() bool } // BaseComponentMonitoring represents basic service monitoring configuration diff --git a/controllers/runtimecomponent_controller.go b/controllers/runtimecomponent_controller.go index d489a7ab..d3d9a304 100644 --- a/controllers/runtimecomponent_controller.go +++ b/controllers/runtimecomponent_controller.go @@ -20,10 +20,8 @@ import ( "context" "fmt" "os" - "strings" "github.com/application-stacks/runtime-component-operator/common" - "github.com/pkg/errors" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -49,13 +47,15 @@ import ( networkingv1 "k8s.io/api/networking/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" servingv1 "knative.dev/serving/pkg/apis/serving/v1" "sigs.k8s.io/controller-runtime/pkg/client" ) const ( - OperatorName = "runtime-component-operator" + OperatorFullName = "Runtime Component Operator" + OperatorName = "runtime-component-operator" + OperatorShortName = "rco" + APIName = "RuntimeComponent" ) // RuntimeComponentReconciler reconciles a RuntimeComponent object @@ -80,40 +80,20 @@ type RuntimeComponentReconciler struct { // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -func (r *RuntimeComponentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - - reqLogger := r.Log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name) - reqLogger.Info("Reconciling RuntimeComponent") - - ns, err := appstacksutils.GetOperatorNamespace() - // When running the operator locally, `ns` will be empty string - if ns == "" { - // Since this method can be called directly from unit test, populate `watchNamespaces`. - if r.watchNamespaces == nil { - r.watchNamespaces, err = appstacksutils.GetWatchNamespaces() - if err != nil { - reqLogger.Error(err, "Error getting watch namespace") - return reconcile.Result{}, err - } - } - // If the operator is running locally, use the first namespace in the `watchNamespaces` - // `watchNamespaces` must have at least one item - ns = r.watchNamespaces[0] - } +func (r *RuntimeComponentReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) { - configMap, err := r.GetOpConfigMap(OperatorName, ns) - if err != nil { - reqLogger.Info("Failed to find runtime-component-operator config map") - appstacksutils.CreateConfigMap(OperatorName) + reqLogger := r.Log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) + reqLogger.Info("Reconcile " + APIName + " - starting") + + if ns, err := r.CheckOperatorNamespace(r.watchNamespaces); err != nil { + return reconcile.Result{}, err } else { - common.Config.LoadFromConfigMap(configMap) + r.UpdateConfigMap(OperatorName, ns) } // Fetch the RuntimeComponent instance instance := &appstacksv1.RuntimeComponent{} - var ba common.BaseComponent = instance - err = r.GetClient().Get(context.TODO(), req.NamespacedName, instance) - if err != nil { + if err := r.GetClient().Get(context.TODO(), request.NamespacedName, instance); err != nil { if kerrors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. @@ -133,17 +113,16 @@ func (r *RuntimeComponentReconciler) Reconcile(ctx context.Context, req ctrl.Req // Check if there is an existing Deployment, Statefulset or Knative service by this name // not managed by this operator - err = appstacksutils.CheckForNameConflicts("RuntimeComponent", instance.Name, instance.Namespace, r.GetClient(), req, isKnativeSupported) - if err != nil { + if err = appstacksutils.CheckForNameConflicts(APIName, instance.Name, instance.Namespace, r.GetClient(), request, isKnativeSupported); err != nil { return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } // initialize the RuntimeComponent instance instance.Initialize() - _, err = appstacksutils.Validate(instance) + // If there's any validation error, don't bother with requeuing - if err != nil { - reqLogger.Error(err, "Error validating RuntimeComponent") + if _, err = appstacksutils.Validate(instance); err != nil { + reqLogger.Error(err, "Error validating "+APIName) r.ManageError(err, common.StatusConditionTypeReconciled, instance) return reconcile.Result{}, nil } @@ -154,361 +133,92 @@ func (r *RuntimeComponentReconciler) Reconcile(ctx context.Context, req ctrl.Req instance.Annotations = appstacksutils.MergeMaps(instance.Annotations, appstacksutils.GetOpenShiftAnnotations(instance)) } - err = r.GetClient().Update(context.TODO(), instance) - if err != nil { - reqLogger.Error(err, "Error updating RuntimeComponent") + if err = r.GetClient().Update(context.TODO(), instance); err != nil { + reqLogger.Error(err, "Error updating "+APIName) return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - // currentGen := instance.Generation - // if currentGen == 1 { - // return reconcile.Result{RequeueAfter: common.ReconcileInterval * time.Second}, nil - // } - defaultMeta := metav1.ObjectMeta{ Name: instance.Name, Namespace: instance.Namespace, } imageReferenceOld := instance.Status.ImageReference - instance.Status.ImageReference = instance.Spec.ApplicationImage - if r.IsOpenShift() { - image, err := imageutil.ParseDockerImageReference(instance.Spec.ApplicationImage) - if err == nil { - isTag := &imagev1.ImageStreamTag{} - isTagName := imageutil.JoinImageStreamTag(image.Name, image.Tag) - isTagNamespace := image.Namespace - if isTagNamespace == "" { - isTagNamespace = instance.Namespace - } - key := types.NamespacedName{Name: isTagName, Namespace: isTagNamespace} - err = r.GetAPIReader().Get(context.Background(), key, isTag) - // Call ManageError only if the error type is not found or is not forbidden. Forbidden could happen - // when the operator tries to call GET for ImageStreamTags on a namespace that doesn't exists (e.g. - // cannot get imagestreamtags.image.openshift.io in the namespace "navidsh": no RBAC policy matched) - if err == nil { - image := isTag.Image - if image.DockerImageReference != "" { - instance.Status.ImageReference = image.DockerImageReference - } - } else if err != nil && !kerrors.IsNotFound(err) && !kerrors.IsForbidden(err) && !strings.Contains(isTagName, "/") { - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } + if err = r.UpdateImageReference(instance); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } + if imageReferenceOld != instance.Status.ImageReference { reqLogger.Info("Updating status.imageReference", "status.imageReference", instance.Status.ImageReference) err = r.UpdateStatus(instance) if err != nil { - reqLogger.Error(err, "Error updating RuntimeComponent status") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } - - if instance.Spec.ServiceAccountName == nil || *instance.Spec.ServiceAccountName == "" { - serviceAccount := &corev1.ServiceAccount{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(serviceAccount, instance, func() error { - return appstacksutils.CustomizeServiceAccount(serviceAccount, instance, r.GetClient()) - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile ServiceAccount") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - serviceAccount := &corev1.ServiceAccount{ObjectMeta: defaultMeta} - err = r.DeleteResource(serviceAccount) - if err != nil { - reqLogger.Error(err, "Failed to delete ServiceAccount") + reqLogger.Error(err, "Error updating "+APIName+" status") return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } } - // Check if the ServiceAccount has a valid pull secret before creating the deployment/statefulset - // or setting up knative. Otherwise the pods can go into an ImagePullBackOff loop - saErr := appstacksutils.ServiceAccountPullSecretExists(instance, r.GetClient()) - if saErr != nil { - return r.ManageError(saErr, common.StatusConditionTypeReconciled, instance) + if err = r.UpdateServiceAccount(instance, defaultMeta); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - if instance.Spec.CreateKnativeService != nil && *instance.Spec.CreateKnativeService { - // Clean up non-Knative resources - resources := []client.Object{ - &corev1.Service{ObjectMeta: defaultMeta}, - &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: instance.Name + "-headless", Namespace: instance.Namespace}}, - &appsv1.Deployment{ObjectMeta: defaultMeta}, - &appsv1.StatefulSet{ObjectMeta: defaultMeta}, - &autoscalingv1.HorizontalPodAutoscaler{ObjectMeta: defaultMeta}, - &networkingv1.NetworkPolicy{ObjectMeta: defaultMeta}, - } - err = r.DeleteResources(resources) + // If Knative is supported and being used, delete other resources and create/update Knative service + // Otherwise, delete Knative service + err, createKnativeService := r.UpdateKnativeService(instance, defaultMeta, isKnativeSupported) + if createKnativeService { if err != nil { - reqLogger.Error(err, "Failed to clean up non-Knative resources") return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - - if ok, _ := r.IsGroupVersionSupported(networkingv1.SchemeGroupVersion.String(), "Ingress"); ok { - r.DeleteResource(&networkingv1.Ingress{ObjectMeta: defaultMeta}) - } - - if r.IsOpenShift() { - route := &routev1.Route{ObjectMeta: defaultMeta} - err = r.DeleteResource(route) - if err != nil { - reqLogger.Error(err, "Failed to clean up non-Knative resource Route") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } - - if isKnativeSupported { - reqLogger.Info("Knative is supported and Knative Service is enabled") - ksvc := &servingv1.Service{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(ksvc, instance, func() error { - appstacksutils.CustomizeKnativeService(ksvc, instance) - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile Knative Service") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } + } else { instance.Status.Versions.Reconciled = appstacksutils.RCOOperandVersion - reqLogger.Info("Reconcile RuntimeComponent - completed") + reqLogger.Info("Reconcile " + APIName + " - completed") return r.ManageSuccess(common.StatusConditionTypeReconciled, instance) } - return r.ManageError(errors.New("failed to reconcile Knative service as operator could not find Knative CRDs"), common.StatusConditionTypeReconciled, instance) } - if isKnativeSupported { - ksvc := &servingv1.Service{ObjectMeta: defaultMeta} - err = r.DeleteResource(ksvc) - if err != nil { - reqLogger.Error(err, "Failed to delete Knative Service") - r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } - - useCertmanager, err := r.GenerateSvcCertSecret(ba, "rco", "Runtime Component Operator", "runtime-component-operator") + useCertmanager, err := r.UpdateSvcCertSecret(instance, OperatorShortName, OperatorFullName, OperatorName) if err != nil { - reqLogger.Error(err, "Failed to reconcile CertManager Certificate") return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - if ba.GetService().GetCertificateSecretRef() != nil { - ba.GetStatus().SetReference(common.StatusReferenceCertSecretName, *ba.GetService().GetCertificateSecretRef()) + + if err = r.UpdateService(instance, defaultMeta, useCertmanager); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - svc := &corev1.Service{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(svc, instance, func() error { - appstacksutils.CustomizeService(svc, ba) - svc.Annotations = appstacksutils.MergeMaps(svc.Annotations, instance.Spec.Service.Annotations) - if !useCertmanager && r.IsOpenShift() { - appstacksutils.AddOCPCertAnnotation(ba, svc) - } - monitoringEnabledLabelName := getMonitoringEnabledLabelName(ba) - if instance.Spec.Monitoring != nil { - svc.Labels[monitoringEnabledLabelName] = "true" - } else { - delete(svc.Labels, monitoringEnabledLabelName) - } - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile Service") + if err = r.UpdateTLSReference(instance); err != nil { return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - networkPolicy := &networkingv1.NetworkPolicy{ObjectMeta: defaultMeta} - if np := instance.Spec.NetworkPolicy; np == nil || np != nil && !np.IsDisabled() { - err = r.CreateOrUpdate(networkPolicy, instance, func() error { - appstacksutils.CustomizeNetworkPolicy(networkPolicy, r.IsOpenShift(), instance) - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile network policy") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - if err := r.DeleteResource(networkPolicy); err != nil { - reqLogger.Error(err, "Failed to delete network policy") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } + if err = r.UpdateNetworkPolicy(instance, defaultMeta); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - err = r.ReconcileBindings(instance) - if err != nil { - return r.ManageError(err, common.StatusConditionTypeReconciled, ba) + if err = r.ReconcileBindings(instance); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } if instance.Spec.StatefulSet != nil { - // Delete Deployment if exists - deploy := &appsv1.Deployment{ObjectMeta: defaultMeta} - err = r.DeleteResource(deploy) - - if err != nil { - reqLogger.Error(err, "Failed to delete Deployment") + if err = r.UpdateStatefulSet(instance, defaultMeta); err != nil { return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: instance.Name + "-headless", Namespace: instance.Namespace}} - err = r.CreateOrUpdate(svc, instance, func() error { - appstacksutils.CustomizeService(svc, instance) - svc.Spec.ClusterIP = corev1.ClusterIPNone - svc.Spec.Type = corev1.ServiceTypeClusterIP - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile headless Service") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - - statefulSet := &appsv1.StatefulSet{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(statefulSet, instance, func() error { - appstacksutils.CustomizeStatefulSet(statefulSet, instance) - appstacksutils.CustomizePodSpec(&statefulSet.Spec.Template, instance) - if err := appstacksutils.CustomizePodWithSVCCertificate(&statefulSet.Spec.Template, instance, r.GetClient()); err != nil { - return err - } - appstacksutils.CustomizePersistence(statefulSet, instance) - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile StatefulSet") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - // Delete StatefulSet if exists - statefulSet := &appsv1.StatefulSet{ObjectMeta: defaultMeta} - err = r.DeleteResource(statefulSet) - if err != nil { - reqLogger.Error(err, "Failed to delete Statefulset") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - - // Delete StatefulSet if exists - headlesssvc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: instance.Name + "-headless", Namespace: instance.Namespace}} - err = r.DeleteResource(headlesssvc) - - if err != nil { - reqLogger.Error(err, "Failed to delete headless Service") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - deploy := &appsv1.Deployment{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(deploy, instance, func() error { - appstacksutils.CustomizeDeployment(deploy, instance) - appstacksutils.CustomizePodSpec(&deploy.Spec.Template, instance) - if err := appstacksutils.CustomizePodWithSVCCertificate(&deploy.Spec.Template, instance, r.GetClient()); err != nil { - return err - } - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile Deployment") + if err = r.UpdateDeployment(instance, defaultMeta); err != nil { return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - } - if instance.Spec.Autoscaling != nil { - hpa := &autoscalingv1.HorizontalPodAutoscaler{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(hpa, instance, func() error { - appstacksutils.CustomizeHPA(hpa, instance) - return nil - }) - - if err != nil { - reqLogger.Error(err, "Failed to reconcile HorizontalPodAutoscaler") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - hpa := &autoscalingv1.HorizontalPodAutoscaler{ObjectMeta: defaultMeta} - err = r.DeleteResource(hpa) - if err != nil { - reqLogger.Error(err, "Failed to delete HorizontalPodAutoscaler") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } + if err = r.UpdateAutoscaling(instance, defaultMeta); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - if ok, err := r.IsGroupVersionSupported(routev1.SchemeGroupVersion.String(), "Route"); err != nil { - reqLogger.Error(err, fmt.Sprintf("Failed to check if %s is supported", routev1.SchemeGroupVersion.String())) - r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } else if ok { - if instance.Spec.Expose != nil && *instance.Spec.Expose { - route := &routev1.Route{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(route, instance, func() error { - key, cert, caCert, destCACert, err := r.GetRouteTLSValues(ba) - if err != nil { - return err - } - appstacksutils.CustomizeRoute(route, ba, key, cert, caCert, destCACert) - - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile Route") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - route := &routev1.Route{ObjectMeta: defaultMeta} - err = r.DeleteResource(route) - if err != nil { - reqLogger.Error(err, "Failed to delete Route") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } - } else { - - if ok, err := r.IsGroupVersionSupported(networkingv1.SchemeGroupVersion.String(), "Ingress"); err != nil { - reqLogger.Error(err, fmt.Sprintf("Failed to check if %s is supported", networkingv1.SchemeGroupVersion.String())) - r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } else if ok { - if instance.Spec.Expose != nil && *instance.Spec.Expose { - ing := &networkingv1.Ingress{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(ing, instance, func() error { - appstacksutils.CustomizeIngress(ing, instance) - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile Ingress") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - ing := &networkingv1.Ingress{ObjectMeta: defaultMeta} - err = r.DeleteResource(ing) - if err != nil { - reqLogger.Error(err, "Failed to delete Ingress") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } - } + if err = r.UpdateRouteOrIngress(instance, defaultMeta); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } - if ok, err := r.IsGroupVersionSupported(prometheusv1.SchemeGroupVersion.String(), "ServiceMonitor"); err != nil { - reqLogger.Error(err, fmt.Sprintf("Failed to check if %s is supported", prometheusv1.SchemeGroupVersion.String())) - r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } else if ok { - if instance.Spec.Monitoring != nil && (instance.Spec.CreateKnativeService == nil || !*instance.Spec.CreateKnativeService) { - sm := &prometheusv1.ServiceMonitor{ObjectMeta: defaultMeta} - err = r.CreateOrUpdate(sm, instance, func() error { - appstacksutils.CustomizeServiceMonitor(sm, instance) - return nil - }) - if err != nil { - reqLogger.Error(err, "Failed to reconcile ServiceMonitor") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } else { - sm := &prometheusv1.ServiceMonitor{ObjectMeta: defaultMeta} - err = r.DeleteResource(sm) - if err != nil { - reqLogger.Error(err, "Failed to delete ServiceMonitor") - return r.ManageError(err, common.StatusConditionTypeReconciled, instance) - } - } - - } else { - reqLogger.V(1).Info(fmt.Sprintf("%s is not supported", prometheusv1.SchemeGroupVersion.String())) + if err = r.UpdateServiceMonitor(instance, defaultMeta); err != nil { + return r.ManageError(err, common.StatusConditionTypeReconciled, instance) } instance.Status.Versions.Reconciled = appstacksutils.RCOOperandVersion - reqLogger.Info("Reconcile RuntimeComponent - completed") + reqLogger.Info("Reconcile " + APIName + " - completed") return r.ManageSuccess(common.StatusConditionTypeReconciled, instance) } diff --git a/utils/reconciler.go b/utils/reconciler.go index 8fef3114..010c9f47 100644 --- a/utils/reconciler.go +++ b/utils/reconciler.go @@ -175,6 +175,31 @@ func (r *ReconcilerBase) GetOpConfigMap(name string, ns string) (*corev1.ConfigM return configMap, nil } +// CheckOperatorNamespace ... +func (r *ReconcilerBase) CheckOperatorNamespace(watchNamespaces []string) (string, error) { + ns, err := GetOperatorNamespace() + if err != nil { + log.Info("Failed to get operator namespace, error: " + err.Error()) + } + + // When running the operator locally, `ns` will be empty string + if ns == "" { + // Since this method can be called directly from unit test, populate `watchNamespaces`. + if watchNamespaces == nil { + watchNamespaces, err = GetWatchNamespaces() + if err != nil { + log.Error(err, "Error getting watch namespace") + return "", err + } + } + // If the operator is running locally, use the first namespace in the `watchNamespaces` + // `watchNamespaces` must have at least one item + ns = watchNamespaces[0] + } + + return ns, nil +} + // ManageError ... func (r *ReconcilerBase) ManageError(issue error, conditionType common.StatusConditionType, ba common.BaseComponent) (reconcile.Result, error) { s := ba.GetStatus() diff --git a/utils/update.go b/utils/update.go new file mode 100644 index 00000000..2f07af6a --- /dev/null +++ b/utils/update.go @@ -0,0 +1,467 @@ +package utils + +import ( + "context" + "errors" + "fmt" + "strings" + + "github.com/application-stacks/runtime-component-operator/common" + imagev1 "github.com/openshift/api/image/v1" + routev1 "github.com/openshift/api/route/v1" + "github.com/openshift/library-go/pkg/image/imageutil" + prometheusv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + servingv1 "knative.dev/serving/pkg/apis/serving/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// UpdateConfigMap creates or updates ConfigMap resource +func (r *ReconcilerBase) UpdateConfigMap(OperatorName string, ns string) { + configMap, err := r.GetOpConfigMap(OperatorName, ns) + if err != nil { + log.Info("Failed to get " + OperatorName + " config map, error: " + err.Error()) + CreateConfigMap(OperatorName) + } else { + common.Config.LoadFromConfigMap(configMap) + } +} + +// UpdateImageStreamTag updates image reference +func (r *ReconcilerBase) UpdateImageReference(ba common.BaseComponent) error { + status := ba.GetStatus() + metaObj := ba.(metav1.Object) + clientObj := ba.(client.Object) + + imageReferenceOld := status.GetImageReference() + status.SetImageReference(ba.GetApplicationImage()) + + if r.IsOpenShift() { + image, err := imageutil.ParseDockerImageReference(ba.GetApplicationImage()) + if err == nil { + isTag := &imagev1.ImageStreamTag{} + isTagName := imageutil.JoinImageStreamTag(image.Name, image.Tag) + isTagNamespace := image.Namespace + if isTagNamespace == "" { + isTagNamespace = metaObj.GetNamespace() + } + key := types.NamespacedName{Name: isTagName, Namespace: isTagNamespace} + err = r.GetAPIReader().Get(context.Background(), key, isTag) + // Call ManageError only if the error type is not found or is not forbidden. Forbidden could happen + // when the operator tries to call GET for ImageStreamTags on a namespace that doesn't exists (e.g. + // cannot get imagestreamtags.image.openshift.io in the namespace "navidsh": no RBAC policy matched) + if err == nil { + image := isTag.Image + if image.DockerImageReference != "" { + status.SetImageReference(image.DockerImageReference) + } + } else if err != nil && !kerrors.IsNotFound(err) && !kerrors.IsForbidden(err) && !strings.Contains(isTagName, "/") { + return err + } + } + } + + if imageReferenceOld != status.GetImageReference() { + log.Info("Updating status.imageReference", "status.imageReference", status.GetImageReference()) + if err := r.UpdateStatus(clientObj); err != nil { + return err + } + } + + return nil +} + +// UpdateServiceAccount creates or updates Services Account resource +func (r *ReconcilerBase) UpdateServiceAccount(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + if ba.GetServiceAccountName() == nil || *ba.GetServiceAccountName() == "" { + serviceAccount := &corev1.ServiceAccount{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(serviceAccount, metaObj, func() error { + return CustomizeServiceAccount(serviceAccount, ba, r.GetClient()) + }) + if err != nil { + log.Error(err, "Failed to reconcile ServiceAccount") + return err + } + } else { + serviceAccount := &corev1.ServiceAccount{ObjectMeta: defaultMeta} + if err := r.DeleteResource(serviceAccount); err != nil { + log.Error(err, "Failed to delete ServiceAccount") + return err + } + } + + // Check if the ServiceAccount has a valid pull secret before creating the deployment/statefulset + // or setting up knative. Otherwise the pods can go into an ImagePullBackOff loop + if saErr := ServiceAccountPullSecretExists(ba, r.GetClient()); saErr != nil { + return saErr + } + + return nil +} + +// UpdateKnativeService creates or updates Knative Service resource +func (r *ReconcilerBase) UpdateKnativeService(ba common.BaseComponent, defaultMeta metav1.ObjectMeta, isKnativeSupported bool) (error, bool) { + metaObj := ba.(metav1.Object) + createKnativeService := false + + // When Knative is supported and Knative serving is being used + if ba.GetCreateKnativeService() != nil && *ba.GetCreateKnativeService() { + createKnativeService = true + + // Clean up non-Knative resources + resources := []client.Object{ + &corev1.Service{ObjectMeta: defaultMeta}, + &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: metaObj.GetName() + "-headless", Namespace: metaObj.GetNamespace()}}, + &appsv1.Deployment{ObjectMeta: defaultMeta}, + &appsv1.StatefulSet{ObjectMeta: defaultMeta}, + &autoscalingv1.HorizontalPodAutoscaler{ObjectMeta: defaultMeta}, + &networkingv1.NetworkPolicy{ObjectMeta: defaultMeta}, + } + + if err := r.DeleteResources(resources); err != nil { + log.Error(err, "Failed to clean up non-Knative resources") + return err, createKnativeService + } + + if ok, _ := r.IsGroupVersionSupported(networkingv1.SchemeGroupVersion.String(), "Ingress"); ok { + r.DeleteResource(&networkingv1.Ingress{ObjectMeta: defaultMeta}) + } + + if r.IsOpenShift() { + route := &routev1.Route{ObjectMeta: defaultMeta} + if err := r.DeleteResource(route); err != nil { + log.Error(err, "Failed to clean up non-Knative resource Route") + return err, createKnativeService + } + } + + if isKnativeSupported { + log.Info("Knative is supported and Knative Service is enabled") + ksvc := &servingv1.Service{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(ksvc, metaObj, func() error { + CustomizeKnativeService(ksvc, ba) + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile Knative Service") + return err, createKnativeService + } + return nil, createKnativeService + } + + return errors.New("Failed to reconcile Knative service as operator could not find Knative CRDs"), createKnativeService + } + if isKnativeSupported { + ksvc := &servingv1.Service{ObjectMeta: defaultMeta} + if err := r.DeleteResource(ksvc); err != nil { + log.Error(err, "Failed to delete Knative Service") + r.ManageError(err, common.StatusConditionTypeReconciled, ba) + } + } + return nil, createKnativeService +} + +// UpdateSvcCertSecret creates or updates Service Cert secret +func (r *ReconcilerBase) UpdateSvcCertSecret(ba common.BaseComponent, prefix string, CACommonName string, operatorName string) (bool, error) { + useCertmanager, err := r.GenerateSvcCertSecret(ba, prefix, CACommonName, operatorName) + if err != nil { + log.Error(err, "Failed to reconcile CertManager Certificate") + return useCertmanager, err + } + + if ba.GetService().GetCertificateSecretRef() != nil { + ba.GetStatus().SetReference(common.StatusReferenceCertSecretName, *ba.GetService().GetCertificateSecretRef()) + } + + return useCertmanager, nil +} + +// UpdateService creates or updates Service resource +func (r *ReconcilerBase) UpdateService(ba common.BaseComponent, defaultMeta metav1.ObjectMeta, useCertmanager bool) error { + metaObj := ba.(metav1.Object) + + svc := &corev1.Service{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(svc, metaObj, func() error { + CustomizeService(svc, ba) + svc.Annotations = MergeMaps(svc.Annotations, ba.GetAnnotations()) + if !useCertmanager && r.IsOpenShift() { + AddOCPCertAnnotation(ba, svc) + } + monitoringEnabledLabelName := getMonitoringEnabledLabelName(ba) + if ba.GetMonitoring() != nil { + svc.Labels[monitoringEnabledLabelName] = "true" + } else { + delete(svc.Labels, monitoringEnabledLabelName) + } + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile Service") + return err + } + + return nil +} + +// UpdateTLSReference creates or updates TLS reference in status field +func (r *ReconcilerBase) UpdateTLSReference(ba common.BaseComponent) error { + if (ba.GetManageTLS() == nil || *ba.GetManageTLS()) && + ba.GetStatus().GetReferences()[common.StatusReferenceCertSecretName] == "" { + return errors.New("Failed to generate TLS certificate. Ensure cert-manager is installed and running") + } + + return nil +} + +// UpdateNetworkPolicy creates or updates Network Policy resource +func (r *ReconcilerBase) UpdateNetworkPolicy(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + networkPolicy := &networkingv1.NetworkPolicy{ObjectMeta: defaultMeta} + if np := ba.GetNetworkPolicy(); np == nil || np != nil && !np.IsDisabled() { + err := r.CreateOrUpdate(networkPolicy, metaObj, func() error { + CustomizeNetworkPolicy(networkPolicy, r.IsOpenShift(), ba) + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile network policy") + return err + } + } else { + if err := r.DeleteResource(networkPolicy); err != nil { + log.Error(err, "Failed to delete network policy") + return err + } + } + + return nil +} + +// UpdateStatefulSet creates or updates StatefulSet resource +func (r *ReconcilerBase) UpdateStatefulSet(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + // Delete Deployment if exists + deploy := &appsv1.Deployment{ObjectMeta: defaultMeta} + if err := r.DeleteResource(deploy); err != nil { + log.Error(err, "Failed to delete Deployment") + return err + } + + // Create Headless Service for StatefulSet + svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: metaObj.GetName() + "-headless", Namespace: metaObj.GetNamespace()}} + err := r.CreateOrUpdate(svc, metaObj, func() error { + CustomizeService(svc, ba) + svc.Spec.ClusterIP = corev1.ClusterIPNone + svc.Spec.Type = corev1.ServiceTypeClusterIP + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile headless Service") + return err + } + + statefulSet := &appsv1.StatefulSet{ObjectMeta: defaultMeta} + err = r.CreateOrUpdate(statefulSet, metaObj, func() error { + CustomizeStatefulSet(statefulSet, ba) + CustomizePodSpec(&statefulSet.Spec.Template, ba) + if err := CustomizePodWithSVCCertificate(&statefulSet.Spec.Template, ba, r.GetClient()); err != nil { + return err + } + CustomizePersistence(statefulSet, ba) + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile StatefulSet") + return err + } + + return nil +} + +// UpdateDeploymentReq creates or update Deployment resource +func (r *ReconcilerBase) UpdateDeployment(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + // Delete StatefulSet if exists + statefulSet := &appsv1.StatefulSet{ObjectMeta: defaultMeta} + if err := r.DeleteResource(statefulSet); err != nil { + log.Error(err, "Failed to delete Statefulset") + return err + } + + // Delete Headless Service if exists + headlesssvc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Name: metaObj.GetName() + "-headless", Namespace: metaObj.GetNamespace()}} + if err := r.DeleteResource(headlesssvc); err != nil { + log.Error(err, "Failed to delete headless Service") + return err + } + + deploy := &appsv1.Deployment{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(deploy, metaObj, func() error { + CustomizeDeployment(deploy, ba) + CustomizePodSpec(&deploy.Spec.Template, ba) + if err := CustomizePodWithSVCCertificate(&deploy.Spec.Template, ba, r.GetClient()); err != nil { + return err + } + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile Deployment") + return err + } + + return nil +} + +// UpdateAutoscaling creates or updates HPA resource +func (r *ReconcilerBase) UpdateAutoscaling(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + if ba.GetAutoscaling() != nil { + hpa := &autoscalingv1.HorizontalPodAutoscaler{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(hpa, metaObj, func() error { + CustomizeHPA(hpa, ba) + return nil + }) + + if err != nil { + log.Error(err, "Failed to reconcile HorizontalPodAutoscaler") + return err + } + } else { + hpa := &autoscalingv1.HorizontalPodAutoscaler{ObjectMeta: defaultMeta} + if err := r.DeleteResource(hpa); err != nil { + log.Error(err, "Failed to delete HorizontalPodAutoscaler") + return err + } + } + + return nil +} + +// UpdateRouteOrIngress creates or updates Route resource if supported, otherwise Ingress resource +func (r *ReconcilerBase) UpdateRouteOrIngress(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + // Check if Route is supported + if ok, err := r.IsGroupVersionSupported(routev1.SchemeGroupVersion.String(), "Route"); err != nil { + log.Error(err, fmt.Sprintf("Failed to check if %s is supported", routev1.SchemeGroupVersion.String())) + r.ManageError(err, common.StatusConditionTypeReconciled, ba) + } else if ok { + if err = r.UpdateRoute(ba, defaultMeta); err != nil { + return err + } + } else { + // If Route is not supported, check if Ingress is supported + if ok, err := r.IsGroupVersionSupported(networkingv1.SchemeGroupVersion.String(), "Ingress"); err != nil { + log.Error(err, fmt.Sprintf("Failed to check if %s is supported", networkingv1.SchemeGroupVersion.String())) + r.ManageError(err, common.StatusConditionTypeReconciled, ba) + } else if ok { + if err = r.UpdateIngress(ba, defaultMeta); err != nil { + return err + } + } else { + log.V(1).Info(fmt.Sprintf("%s is not supported", networkingv1.SchemeGroupVersion.String())) + } + } + + return nil +} + +// UpdateRoute creates or updates Route resource +func (r *ReconcilerBase) UpdateRoute(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + if ba.GetExpose() != nil && *ba.GetExpose() { + route := &routev1.Route{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(route, metaObj, func() error { + key, cert, caCert, destCACert, err := r.GetRouteTLSValues(ba) + if err != nil { + return err + } + CustomizeRoute(route, ba, key, cert, caCert, destCACert) + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile Route") + return err + } + } else { + route := &routev1.Route{ObjectMeta: defaultMeta} + if err := r.DeleteResource(route); err != nil { + log.Error(err, "Failed to delete Route") + return err + } + } + + return nil +} + +// UpdateIngress creates or updates Ingress resource +func (r *ReconcilerBase) UpdateIngress(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + metaObj := ba.(metav1.Object) + + if ba.GetExpose() != nil && *ba.GetExpose() { + ing := &networkingv1.Ingress{ObjectMeta: defaultMeta} + err := r.CreateOrUpdate(ing, metaObj, func() error { + CustomizeIngress(ing, ba) + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile Ingress") + return err + } + } else { + ing := &networkingv1.Ingress{ObjectMeta: defaultMeta} + if err := r.DeleteResource(ing); err != nil { + log.Error(err, "Failed to delete Ingress") + return err + } + } + + return nil +} + +// UpdateIngress creates or updates Service Monitor resource +func (r *ReconcilerBase) UpdateServiceMonitor(ba common.BaseComponent, defaultMeta metav1.ObjectMeta) error { + + if ok, err := r.IsGroupVersionSupported(prometheusv1.SchemeGroupVersion.String(), "ServiceMonitor"); err != nil { + log.Error(err, fmt.Sprintf("Failed to check if %s is supported", prometheusv1.SchemeGroupVersion.String())) + r.ManageError(err, common.StatusConditionTypeReconciled, ba) + } else if ok { + metaObj := ba.(metav1.Object) + + if ba.GetMonitoring() != nil && (ba.GetCreateKnativeService() == nil || !*ba.GetCreateKnativeService()) { + sm := &prometheusv1.ServiceMonitor{ObjectMeta: defaultMeta} + err = r.CreateOrUpdate(sm, metaObj, func() error { + CustomizeServiceMonitor(sm, ba) + return nil + }) + if err != nil { + log.Error(err, "Failed to reconcile ServiceMonitor") + return err + } + } else { + sm := &prometheusv1.ServiceMonitor{ObjectMeta: defaultMeta} + if err = r.DeleteResource(sm); err != nil { + log.Error(err, "Failed to delete ServiceMonitor") + return err + } + } + } else { + log.V(1).Info(fmt.Sprintf("%s is not supported", prometheusv1.SchemeGroupVersion.String())) + } + + return nil +} + +func getMonitoringEnabledLabelName(ba common.BaseComponent) string { + return "monitor." + ba.GetGroupName() + "/enabled" +}