diff --git a/apis/components/v1/groupversion_info.go b/apis/components/v1/groupversion_info.go index d066ed50e21..727c3cab6c8 100644 --- a/apis/components/v1/groupversion_info.go +++ b/apis/components/v1/groupversion_info.go @@ -15,8 +15,8 @@ limitations under the License. */ // Package v1 contains API Schema definitions for the components v1 API group -//+kubebuilder:object:generate=true -//+groupName=components.opendatahub.io +// +kubebuilder:object:generate=true +// +groupName=components.opendatahub.io package v1 import ( diff --git a/components/component.go b/components/component.go index 58927c6d0db..7b53477d541 100644 --- a/components/component.go +++ b/components/component.go @@ -3,7 +3,6 @@ package components import ( "context" - "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" "os" "path/filepath" "strings" @@ -14,6 +13,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" ) diff --git a/components/dashboard/zz_generated.deepcopy.go b/components/dashboard/zz_generated.deepcopy.go index a0833e96bb2..69b406105a1 100644 --- a/components/dashboard/zz_generated.deepcopy.go +++ b/components/dashboard/zz_generated.deepcopy.go @@ -19,6 +19,7 @@ limitations under the License. // Code generated by controller-gen. DO NOT EDIT. package dashboard + // //import () // diff --git a/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go b/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go index a3ce257dcb3..a9565eb6d19 100644 --- a/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go +++ b/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go @@ -20,13 +20,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" annotation "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/annotations" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/trustedcabundle" ) // CertConfigmapGeneratorReconciler holds the controller configuration. type CertConfigmapGeneratorReconciler struct { - Client client.Client + *odhClient.Client Scheme *runtime.Scheme Log logr.Logger } diff --git a/controllers/components/base_reconciler_actions.go b/controllers/components/base_reconciler_actions.go deleted file mode 100644 index c1f3f56ebb0..00000000000 --- a/controllers/components/base_reconciler_actions.go +++ /dev/null @@ -1,35 +0,0 @@ -package components - -import ( - "context" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -type DeleteResourcesAction struct { - BaseAction - Types []client.Object - Labels map[string]string -} - -func (r *DeleteResourcesAction) Execute(ctx context.Context, rr *ReconciliationRequest) error { - for i := range r.Types { - opts := make([]client.DeleteAllOfOption, 1) - opts = append(opts, client.MatchingLabels(r.Labels)) - - namespaced, err := rr.Client.IsObjectNamespaced(r.Types[i]) - if err != nil { - return err - } - - if namespaced { - opts = append(opts, client.InNamespace(rr.DSCI.Spec.ApplicationsNamespace)) - } - - err = rr.Client.DeleteAllOf(ctx, r.Types[i], opts...) - if err != nil { - return err - } - } - - return nil -} diff --git a/controllers/components/dashboard_controller.go b/controllers/components/dashboard_controller.go index 6579872eb4f..f226353f13c 100644 --- a/controllers/components/dashboard_controller.go +++ b/controllers/components/dashboard_controller.go @@ -20,16 +20,19 @@ import ( "context" "errors" "fmt" - "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" - "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk" - "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" + "k8s.io/utils/pointer" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + k8serr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -37,16 +40,20 @@ import ( dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions" + odhrec "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/reconciler" + odhtypes "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/deploy" - k8serr "k8s.io/apimachinery/pkg/api/errors" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" ) -var ( - DashboardInstanceName = "default-dashboard" +const ( ComponentName = "dashboard" + DashboardInstanceName = "default-dashboard" +) + +var ( ComponentNameUpstream = ComponentName PathUpstream = deploy.DefaultManifestPath + "/" + ComponentNameUpstream + "/odh" @@ -58,34 +65,27 @@ var ( DefaultPath = "" ) -func NewDashboardReconciler(mgr ctrl.Manager) error { - r := NewBaseReconciler[*componentsv1.Dashboard](mgr, ComponentName) +func NewDashboardReconciler(ctx context.Context, mgr ctrl.Manager) error { + r, err := odhrec.NewComponentReconciler[*componentsv1.Dashboard](ctx, mgr, ComponentName) + if err != nil { + return err + } + actionCtx := logf.IntoContext(ctx, r.Log) // Add Dashboard-specific actions - r.AddAction(&InitializeAction{BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("initialize")}}) - r.AddAction(&SupportDevFlagsAction{BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("devFlags")}}) - r.AddAction(&CleanupOAuthClientAction{BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("cleanup")}}) - r.AddAction(&DeployComponentAction{BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("deploy")}}) - r.AddAction(&UpdateStatusAction{BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("update-status")}}) - - r.AddFinalizer(&DeleteResourcesAction{ - BaseAction: BaseAction{ - Log: mgr.GetLogger().WithName("finalizers").WithName("cleanup"), - }, - // include only the types that must be deleted - Types: []client.Object{ - &corev1.Secret{}, - }, - Labels: map[string]string{ - "app.opendatahub.io/dashboard": "true", - }, - }) + r.AddAction(&InitializeAction{actions.BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("initialize")}}) + r.AddAction(&SupportDevFlagsAction{actions.BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("devFlags")}}) + r.AddAction(&CleanupOAuthClientAction{actions.BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("cleanup")}}) + r.AddAction(&DeployComponentAction{actions.BaseAction{Log: mgr.GetLogger().WithName("actions").WithName("deploy")}}) - eh := handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, a client.Object) []reconcile.Request { - return watchDashboardResources(ctx, a) - }) + r.AddAction(actions.NewUpdateStatusAction( + actionCtx, + actions.WithUpdateStatusLabel(labels.ComponentName, ComponentName), + )) + + eh := handler.EnqueueRequestsFromMapFunc(watchDashboardResources) - err := ctrl.NewControllerManagedBy(mgr). + err = ctrl.NewControllerManagedBy(mgr). For(&componentsv1.Dashboard{}). // dependants Watches(&appsv1.Deployment{}, eh). @@ -119,12 +119,7 @@ func CreateDashboardInstance(dsc *dscv1.DataScienceCluster) *componentsv1.Dashbo Name: DashboardInstanceName, }, Spec: componentsv1.DashboardSpec{ - DSCDashboard: componentsv1.DSCDashboard{ - Component: components.Component{ - ManagementState: dsc.Spec.Components.Dashboard.ManagementState, - DevFlags: dsc.Spec.Components.Dashboard.DevFlags, - }, - }, + DSCDashboard: dsc.Spec.Components.Dashboard, }, } } @@ -169,8 +164,7 @@ func CreateDashboardInstance(dsc *dscv1.DataScienceCluster) *componentsv1.Dashbo // +kubebuilder:rbac:groups="*",resources=replicasets,verbs=* -func watchDashboardResources(ctx context.Context, a client.Object) []reconcile.Request { - +func watchDashboardResources(_ context.Context, a client.Object) []reconcile.Request { if a.GetLabels()["app.opendatahub.io/dashboard"] == "true" { return []reconcile.Request{{ NamespacedName: types.NamespacedName{Name: DashboardInstanceName}, @@ -245,10 +239,10 @@ func updateKustomizeVariable(ctx context.Context, cli client.Client, platform cl // Action implementations type InitializeAction struct { - BaseAction + actions.BaseAction } -func (a *InitializeAction) Execute(ctx context.Context, rr *ReconciliationRequest) error { +func (a *InitializeAction) Execute(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { // Implement initialization logic log := logf.FromContext(ctx).WithName(ComponentNameUpstream) @@ -263,7 +257,7 @@ func (a *InitializeAction) Execute(ctx context.Context, rr *ReconciliationReques } DefaultPath = manifestMap[rr.Platform] - rr.Manifests = Manifests{ + rr.Manifests = odhtypes.Manifests{ Paths: manifestMap, } @@ -275,11 +269,15 @@ func (a *InitializeAction) Execute(ctx context.Context, rr *ReconciliationReques } type SupportDevFlagsAction struct { - BaseAction + actions.BaseAction } -func (a *SupportDevFlagsAction) Execute(ctx context.Context, rr *ReconciliationRequest) error { - dashboard := rr.Instance.(*componentsv1.Dashboard) +func (a *SupportDevFlagsAction) Execute(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { + dashboard, ok := rr.Instance.(*componentsv1.Dashboard) + if !ok { + return fmt.Errorf("resource instance %v is not a componentsv1.Dashboard)", rr.Instance) + } + if dashboard.Spec.DevFlags == nil { return nil } @@ -298,10 +296,10 @@ func (a *SupportDevFlagsAction) Execute(ctx context.Context, rr *ReconciliationR } type CleanupOAuthClientAction struct { - BaseAction + actions.BaseAction } -func (a *CleanupOAuthClientAction) Execute(ctx context.Context, rr *ReconciliationRequest) error { +func (a *CleanupOAuthClientAction) Execute(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { // Remove previous oauth-client secrets // Check if component is going from state of `Not Installed --> Installed` // Assumption: Component is currently set to enabled @@ -329,10 +327,10 @@ func (a *CleanupOAuthClientAction) Execute(ctx context.Context, rr *Reconciliati } type DeployComponentAction struct { - BaseAction + actions.BaseAction } -func (a *DeployComponentAction) Execute(ctx context.Context, rr *ReconciliationRequest) error { +func (a *DeployComponentAction) Execute(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { // Implement component deployment logic // 1. platform specific RBAC if rr.Platform == cluster.OpenDataHub || rr.Platform == "" { @@ -356,44 +354,51 @@ func (a *DeployComponentAction) Execute(ctx context.Context, rr *ReconciliationR // return fmt.Errorf("failed to update params.env from %s : %w", r.entryPath, err) // } + path := rr.Manifests.Paths[rr.Platform] + name := ComponentNameUpstream + // common: Deploy odh-dashboard manifests // TODO: check if we can have the same component name odh-dashboard for both, or still keep rhods-dashboard for RHOAI switch rr.Platform { case cluster.SelfManagedRhods, cluster.ManagedRhods: // anaconda - if err := cluster.CreateSecret(ctx, rr.Client, "anaconda-ce-access", rr.DSCI.Spec.ApplicationsNamespace); err != nil { + err := cluster.CreateSecret( + ctx, + rr.Client, + "anaconda-ce-access", + rr.DSCI.Spec.ApplicationsNamespace, + // set owner reference so it gets deleted when the Dashboard resource get deleted as well + cluster.WithOwnerReference(metav1.OwnerReference{ + APIVersion: rr.Instance.GetObjectKind().GroupVersionKind().GroupVersion().String(), + Kind: rr.Instance.GetObjectKind().GroupVersionKind().Kind, + Name: rr.Instance.GetName(), + UID: rr.Instance.GetUID(), + Controller: pointer.Bool(true), + BlockOwnerDeletion: pointer.Bool(true), + }), + cluster.WithLabels( + labels.ComponentName, ComponentName, + labels.ODH.Component(name), "true", + labels.K8SCommon.PartOf, name, + ), + ) + + if err != nil { return fmt.Errorf("failed to create access-secret for anaconda: %w", err) } - // Deploy RHOAI manifests - if err := deploy.DeployManifestsFromPath(ctx, rr.Client, rr.Instance, rr.Manifests.Paths[rr.Platform], rr.DSCI.Spec.ApplicationsNamespace, ComponentNameDownstream, true); err != nil { - return fmt.Errorf("failed to apply manifests from %s: %w", PathDownstream, err) - } - a.Log.Info("apply manifests done") - - if err := cluster.WaitForDeploymentAvailable(ctx, rr.Client, ComponentNameDownstream, rr.DSCI.Spec.ApplicationsNamespace, 20, 3); err != nil { - return fmt.Errorf("deployment for %s is not ready to server: %w", ComponentNameDownstream, err) - } - return nil + name = ComponentNameDownstream + } - default: - // Deploy ODH manifests - if err := deploy.DeployManifestsFromPath(ctx, rr.Client, rr.Instance, rr.Manifests.Paths[cluster.OpenDataHub], rr.DSCI.Spec.ApplicationsNamespace, ComponentNameUpstream, true); err != nil { - return err - } - a.Log.Info("apply manifests done") + err = deploy.DeployManifestsFromPathWithLabels(ctx, rr.Client, rr.Instance, path, rr.DSCI.Spec.ApplicationsNamespace, name, true, map[string]string{ + labels.ComponentName: ComponentName, + }) - if err := cluster.WaitForDeploymentAvailable(ctx, rr.Client, ComponentNameUpstream, rr.DSCI.Spec.ApplicationsNamespace, 20, 3); err != nil { - return fmt.Errorf("deployment for %s is not ready to server: %w", ComponentNameUpstream, err) - } + if err != nil { + return fmt.Errorf("failed to apply manifests from %s: %w", name, err) } - return nil -} -type UpdateStatusAction struct { - BaseAction -} + a.Log.Info("apply manifests done") -func (a *UpdateStatusAction) Execute(ctx context.Context, rr *ReconciliationRequest) error { return nil } diff --git a/controllers/components/suite_test.go b/controllers/components/suite_test.go index fec070e0b0e..f27bb88f16b 100644 --- a/controllers/components/suite_test.go +++ b/controllers/components/suite_test.go @@ -20,9 +20,6 @@ import ( "path/filepath" "testing" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" @@ -31,7 +28,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" - //+kubebuilder:scaffold:imports + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to diff --git a/controllers/datasciencecluster/datasciencecluster_controller.go b/controllers/datasciencecluster/datasciencecluster_controller.go index 5cf44c545f1..c03a67e9579 100644 --- a/controllers/datasciencecluster/datasciencecluster_controller.go +++ b/controllers/datasciencecluster/datasciencecluster_controller.go @@ -21,7 +21,6 @@ import ( "context" "errors" "fmt" - componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" "reflect" "strings" "time" @@ -51,12 +50,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" "github.com/opendatahub-io/opendatahub-operator/v2/components/datasciencepipelines" componentsctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components" "github.com/opendatahub-io/opendatahub-operator/v2/controllers/status" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" ctrlogger "github.com/opendatahub-io/opendatahub-operator/v2/pkg/logger" annotations "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/annotations" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" @@ -65,7 +66,7 @@ import ( // DataScienceClusterReconciler reconciles a DataScienceCluster object. type DataScienceClusterReconciler struct { - client.Client + *odhClient.Client Scheme *runtime.Scheme Log logr.Logger // Recorder to generate events @@ -115,10 +116,10 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R instance := &instances.Items[0] - //allComponents, err := instance.GetComponents() - //if err != nil { + // allComponents, err := instance.GetComponents() + // if err != nil { // return ctrl.Result{}, err - //} + // } // If DSC CR exist and deletion CM exist // delete DSC CR and let reconcile requeue @@ -138,11 +139,11 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R return reconcile.Result{}, err } } - //for _, component := range allComponents { + // for _, component := range allComponents { // if err := component.Cleanup(ctx, r.Client, instance, r.DataScienceCluster.DSCISpec); err != nil { // return ctrl.Result{}, err // } - //} + // } return reconcile.Result{Requeue: true}, nil } @@ -189,11 +190,11 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R } } else { log.Info("Finalization DataScienceCluster start deleting instance", "name", instance.Name, "finalizer", finalizerName) - //for _, component := range allComponents { + // for _, component := range allComponents { // if err := component.Cleanup(ctx, r.Client, instance, r.DataScienceCluster.DSCISpec); err != nil { // return ctrl.Result{}, err // } - //} + // } if controllerutil.ContainsFinalizer(instance, finalizerName) { controllerutil.RemoveFinalizer(instance, finalizerName) if err := r.Update(ctx, instance); err != nil { @@ -386,7 +387,7 @@ func (r *DataScienceClusterReconciler) apply(ctx context.Context, dsc *dscv1.Dat return err } - if err := r.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(dsc.Name), client.ForceOwnership); err != nil { + if err := r.Client.Apply(ctx, obj, client.FieldOwner(dsc.Name), client.ForceOwnership); err != nil { return err } diff --git a/controllers/dscinitialization/dscinitialization_controller.go b/controllers/dscinitialization/dscinitialization_controller.go index 37991131cb5..437f25ea620 100644 --- a/controllers/dscinitialization/dscinitialization_controller.go +++ b/controllers/dscinitialization/dscinitialization_controller.go @@ -46,6 +46,7 @@ import ( dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" "github.com/opendatahub-io/opendatahub-operator/v2/controllers/status" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/deploy" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/trustedcabundle" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/upgrade" @@ -61,7 +62,7 @@ var managementStateChangeTrustedCA = false // DSCInitializationReconciler reconciles a DSCInitialization object. type DSCInitializationReconciler struct { - client.Client + *odhClient.Client Scheme *runtime.Scheme Log logr.Logger Recorder record.EventRecorder diff --git a/controllers/dscinitialization/suite_test.go b/controllers/dscinitialization/suite_test.go index f3ef428f878..618b9a62e91 100644 --- a/controllers/dscinitialization/suite_test.go +++ b/controllers/dscinitialization/suite_test.go @@ -46,6 +46,7 @@ import ( dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" dscictrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/dscinitialization" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" "github.com/opendatahub-io/opendatahub-operator/v2/tests/envtestutil" . "github.com/onsi/ginkgo/v2" @@ -122,6 +123,10 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) + odhClient, err := odhClient.New(gCtx, cfg, k8sClient) + Expect(err).NotTo(HaveOccurred()) + Expect(odhClient).NotTo(BeNil()) + webhookInstallOptions := &testEnv.WebhookInstallOptions mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: testScheme, @@ -135,7 +140,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = (&dscictrl.DSCInitializationReconciler{ - Client: k8sClient, + Client: odhClient, Scheme: testScheme, Log: ctrl.Log.WithName("controllers").WithName("DSCInitialization"), Recorder: mgr.GetEventRecorderFor("dscinitialization-controller"), diff --git a/controllers/secretgenerator/secretgenerator_controller.go b/controllers/secretgenerator/secretgenerator_controller.go index 957e02fe4ae..ac57125050c 100644 --- a/controllers/secretgenerator/secretgenerator_controller.go +++ b/controllers/secretgenerator/secretgenerator_controller.go @@ -40,6 +40,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" annotation "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/annotations" ) @@ -50,7 +51,7 @@ const ( // SecretGeneratorReconciler holds the controller configuration. type SecretGeneratorReconciler struct { - Client client.Client + *odhClient.Client Scheme *runtime.Scheme Log logr.Logger } diff --git a/controllers/status/status.go b/controllers/status/status.go index 808cfee2f7b..b47c23a037e 100644 --- a/controllers/status/status.go +++ b/controllers/status/status.go @@ -21,6 +21,10 @@ package status import ( conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" ) // These constants represent the overall Phase as used by .Status.Phase. @@ -63,6 +67,8 @@ const ( // ConditionReconcileComplete represents extra Condition Type, used by .Condition.Type. ConditionReconcileComplete conditionsv1.ConditionType = "ReconcileComplete" + + ConditionTypeReady string = "Ready" ) const ( @@ -214,3 +220,8 @@ func RemoveComponentCondition(conditions *[]conditionsv1.Condition, component st type ModelRegistryStatus struct { RegistriesNamespace string `json:"registriesNamespace,omitempty"` } + +func SetStatusCondition(obj components.WithStatus, condition metav1.Condition) bool { + s := obj.GetStatus() + return meta.SetStatusCondition(&s.Conditions, condition) +} diff --git a/controllers/webhook/webhook_suite_test.go b/controllers/webhook/webhook_suite_test.go index 32d08e2c71e..51c6526ce2d 100644 --- a/controllers/webhook/webhook_suite_test.go +++ b/controllers/webhook/webhook_suite_test.go @@ -20,7 +20,6 @@ import ( "context" "crypto/tls" "fmt" - componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" "net" "path/filepath" "testing" @@ -42,6 +41,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" componentsold "github.com/opendatahub-io/opendatahub-operator/v2/components" diff --git a/go.mod b/go.mod index 9b86d8e92c2..cba7ae80440 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/blang/semver/v4 v4.0.0 github.com/go-logr/logr v1.4.1 github.com/hashicorp/go-multierror v1.1.1 + github.com/onsi/ginkgo v1.16.4 github.com/onsi/ginkgo/v2 v2.14.0 github.com/onsi/gomega v1.30.0 github.com/openshift/addon-operator/apis v0.0.0-20230919043633-820afed15881 @@ -14,6 +15,7 @@ require ( github.com/operator-framework/api v0.18.0 github.com/pkg/errors v0.9.1 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.68.0 + github.com/rs/xid v1.6.0 github.com/spf13/afero v1.10.0 github.com/stretchr/testify v1.8.4 go.uber.org/zap v1.26.0 @@ -63,6 +65,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -87,6 +90,7 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/component-base v0.29.2 // indirect k8s.io/klog/v2 v2.110.1 // indirect diff --git a/go.sum b/go.sum index 3b193384846..4605b86184c 100644 --- a/go.sum +++ b/go.sum @@ -261,11 +261,13 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= @@ -332,6 +334,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= @@ -798,6 +802,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index ee15ce4c9f9..8441bf09de5 100644 --- a/main.go +++ b/main.go @@ -19,7 +19,6 @@ package main import ( "context" "flag" - componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" "os" "github.com/hashicorp/go-multierror" @@ -57,6 +56,7 @@ import ( ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics/server" ctrlwebhook "sigs.k8s.io/controller-runtime/pkg/webhook" + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" featurev1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/features/v1" @@ -68,6 +68,7 @@ import ( "github.com/opendatahub-io/opendatahub-operator/v2/controllers/secretgenerator" "github.com/opendatahub-io/opendatahub-operator/v2/controllers/webhook" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/logger" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/upgrade" ) @@ -240,8 +241,14 @@ func main() { //nolint:funlen,maintidx webhook.Init(mgr) + oc, err := odhClient.NewFromManager(ctx, mgr) + if err != nil { + setupLog.Error(err, "unable to create client") + os.Exit(1) + } + if err = (&dscictrl.DSCInitializationReconciler{ - Client: mgr.GetClient(), + Client: oc, Scheme: mgr.GetScheme(), Log: logger.LogWithLevel(ctrl.Log.WithName(operatorName).WithName("controllers").WithName("DSCInitialization"), logmode), Recorder: mgr.GetEventRecorderFor("dscinitialization-controller"), @@ -252,7 +259,7 @@ func main() { //nolint:funlen,maintidx } if err = (&dscctrl.DataScienceClusterReconciler{ - Client: mgr.GetClient(), + Client: oc, Scheme: mgr.GetScheme(), Log: logger.LogWithLevel(ctrl.Log.WithName(operatorName).WithName("controllers").WithName("DataScienceCluster"), logmode), DataScienceCluster: &dscctrl.DataScienceClusterConfig{ @@ -267,7 +274,7 @@ func main() { //nolint:funlen,maintidx } if err = (&secretgenerator.SecretGeneratorReconciler{ - Client: mgr.GetClient(), + Client: oc, Scheme: mgr.GetScheme(), Log: logger.LogWithLevel(ctrl.Log.WithName(operatorName).WithName("controllers").WithName("SecretGenerator"), logmode), }).SetupWithManager(mgr); err != nil { @@ -276,7 +283,7 @@ func main() { //nolint:funlen,maintidx } if err = (&certconfigmapgenerator.CertConfigmapGeneratorReconciler{ - Client: mgr.GetClient(), + Client: oc, Scheme: mgr.GetScheme(), Log: logger.LogWithLevel(ctrl.Log.WithName(operatorName).WithName("controllers").WithName("CertConfigmapGenerator"), logmode), }).SetupWithManager(mgr); err != nil { @@ -284,7 +291,7 @@ func main() { //nolint:funlen,maintidx os.Exit(1) } - if err = componentsctrl.NewDashboardReconciler(mgr); err != nil { + if err = componentsctrl.NewDashboardReconciler(ctx, mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "DashboardReconciler") os.Exit(1) } diff --git a/pkg/controller/actions/action_delete_resources.go b/pkg/controller/actions/action_delete_resources.go new file mode 100644 index 00000000000..aa21da284e9 --- /dev/null +++ b/pkg/controller/actions/action_delete_resources.go @@ -0,0 +1,84 @@ +package actions + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" +) + +const ( + DeleteResourcesActionName = "delete-resources" +) + +type DeleteResourcesAction struct { + BaseAction + types []client.Object + labels map[string]string +} + +type DeleteResourcesActionOpts func(*DeleteResourcesAction) + +func WithDeleteResourcesTypes(values ...client.Object) DeleteResourcesActionOpts { + return func(action *DeleteResourcesAction) { + action.types = append(action.types, values...) + } +} + +func WithDeleteResourcesLabel(k string, v string) DeleteResourcesActionOpts { + return func(action *DeleteResourcesAction) { + action.labels[k] = v + } +} + +func WithDeleteResourcesLabels(values map[string]string) DeleteResourcesActionOpts { + return func(action *DeleteResourcesAction) { + for k, v := range values { + action.labels[k] = v + } + } +} + +func (r *DeleteResourcesAction) Execute(ctx context.Context, rr *types.ReconciliationRequest) error { + for i := range r.types { + opts := make([]client.DeleteAllOfOption, 0) + + if len(r.labels) > 0 { + opts = append(opts, client.MatchingLabels(r.labels)) + } + + namespaced, err := rr.Client.IsObjectNamespaced(r.types[i]) + if err != nil { + return err + } + + if namespaced { + opts = append(opts, client.InNamespace(rr.DSCI.Spec.ApplicationsNamespace)) + } + + err = rr.Client.DeleteAllOf(ctx, r.types[i], opts...) + if err != nil { + return err + } + } + + return nil +} + +func NewDeleteResourcesAction(ctx context.Context, opts ...DeleteResourcesActionOpts) *DeleteResourcesAction { + action := DeleteResourcesAction{ + BaseAction: BaseAction{ + Log: log.FromContext(ctx).WithName(ActionGroup).WithName(DeleteResourcesActionName), + }, + types: make([]client.Object, 0), + labels: map[string]string{}, + } + + for _, opt := range opts { + opt(&action) + } + + return &action +} diff --git a/pkg/controller/actions/action_delete_resources_test.go b/pkg/controller/actions/action_delete_resources_test.go new file mode 100644 index 00000000000..faff2714802 --- /dev/null +++ b/pkg/controller/actions/action_delete_resources_test.go @@ -0,0 +1,85 @@ +package actions_test + +import ( + "context" + "testing" + + "github.com/onsi/gomega/gstruct" + "github.com/rs/xid" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" + dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" + + . "github.com/onsi/gomega" +) + +func TestDeleteResourcesAction(t *testing.T) { + g := NewWithT(t) + + ctx := context.Background() + ns := xid.New().String() + + client := NewFakeClient( + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvk.Deployment.GroupVersion().String(), + Kind: gvk.Deployment.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-deployment", + Namespace: ns, + Labels: map[string]string{ + labels.K8SCommon.PartOf: "foo", + }, + }, + }, + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvk.Deployment.GroupVersion().String(), + Kind: gvk.Deployment.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-deployment-2", + Namespace: ns, + Labels: map[string]string{ + labels.K8SCommon.PartOf: "baz", + }, + }, + }, + ) + + action := actions.NewDeleteResourcesAction( + ctx, + actions.WithDeleteResourcesTypes(&appsv1.Deployment{}), + actions.WithDeleteResourcesLabel(labels.K8SCommon.PartOf, "foo")) + + err := action.Execute(ctx, &types.ReconciliationRequest{ + Client: client, + Instance: nil, + DSCI: &dsciv1.DSCInitialization{Spec: dsciv1.DSCInitializationSpec{ApplicationsNamespace: ns}}, + DSC: &dscv1.DataScienceCluster{}, + Platform: cluster.OpenDataHub, + }) + + g.Expect(err).ShouldNot(HaveOccurred()) + + deployments := appsv1.DeploymentList{} + err = client.List(ctx, &deployments) + + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(deployments.Items).Should(HaveLen(1)) + g.Expect(deployments.Items[0]).To( + gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "ObjectMeta": gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Name": Equal("my-deployment-2"), + }), + }), + ) +} diff --git a/pkg/controller/actions/action_test.go b/pkg/controller/actions/action_test.go new file mode 100644 index 00000000000..75dd098fde9 --- /dev/null +++ b/pkg/controller/actions/action_test.go @@ -0,0 +1,46 @@ +package actions_test + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" +) + +func NewFakeClient(objs ...ctrlClient.Object) ctrlClient.WithWatch { + scheme := runtime.NewScheme() + utilruntime.Must(corev1.AddToScheme(scheme)) + utilruntime.Must(appsv1.AddToScheme(scheme)) + + fakeMapper := meta.NewDefaultRESTMapper(scheme.PreferredVersionAllGroups()) + for gvk := range scheme.AllKnownTypes() { + switch { + // TODO: add cases for cluster scoped + default: + fakeMapper.Add(gvk, meta.RESTScopeNamespace) + } + } + + return fake.NewClientBuilder(). + WithScheme(scheme). + WithRESTMapper(fakeMapper). + WithObjects(objs...). + Build() +} + +func ExtractStatusCondition(conditionType string) func(in types.ResourceObject) metav1.Condition { + return func(in types.ResourceObject) metav1.Condition { + c := meta.FindStatusCondition(in.GetStatus().Conditions, conditionType) + if c == nil { + return metav1.Condition{} + } + + return *c + } +} diff --git a/pkg/controller/actions/action_update_status.go b/pkg/controller/actions/action_update_status.go new file mode 100644 index 00000000000..721f52e95d6 --- /dev/null +++ b/pkg/controller/actions/action_update_status.go @@ -0,0 +1,103 @@ +package actions + +import ( + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/opendatahub-io/opendatahub-operator/v2/controllers/status" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" +) + +const ( + UpdateStatusActionName = "update-status" + DeploymentsNotReadyReason = "DeploymentsNotReady" + ReadyReason = "Ready" +) + +type UpdateStatusAction struct { + BaseAction + labels map[string]string +} + +type UpdateStatusActionOpts func(*UpdateStatusAction) + +func WithUpdateStatusLabel(k string, v string) UpdateStatusActionOpts { + return func(action *UpdateStatusAction) { + action.labels[k] = v + } +} + +func WithUpdateStatusLabels(values map[string]string) UpdateStatusActionOpts { + return func(action *UpdateStatusAction) { + for k, v := range values { + action.labels[k] = v + } + } +} + +func (a *UpdateStatusAction) Execute(ctx context.Context, rr *types.ReconciliationRequest) error { + if len(a.labels) == 0 { + return nil + } + + obj, ok := rr.Instance.(types.ResourceObject) + if !ok { + return fmt.Errorf("resource instance %v is not a ResourceObject", rr.Instance) + } + + deployments := &appsv1.DeploymentList{} + + err := rr.Client.List( + ctx, + deployments, + client.InNamespace(rr.DSCI.Spec.ApplicationsNamespace), + client.MatchingLabels(a.labels), + ) + + if err != nil { + return fmt.Errorf("error fetching list of deployments: %w", err) + } + + ready := 0 + for _, deployment := range deployments.Items { + if deployment.Status.ReadyReplicas == deployment.Status.Replicas { + ready++ + } + } + + conditionReady := metav1.Condition{ + Type: status.ConditionTypeReady, + Status: metav1.ConditionTrue, + Reason: ReadyReason, + Message: fmt.Sprintf("%d/%d deployments ready", ready, len(deployments.Items)), + } + + if len(deployments.Items) > 0 && ready != len(deployments.Items) { + conditionReady.Status = metav1.ConditionFalse + conditionReady.Reason = DeploymentsNotReadyReason + } + + status.SetStatusCondition(obj, conditionReady) + + return nil +} + +func NewUpdateStatusAction(ctx context.Context, opts ...UpdateStatusActionOpts) *UpdateStatusAction { + action := UpdateStatusAction{ + BaseAction: BaseAction{ + Log: log.FromContext(ctx).WithName(ActionGroup).WithName(UpdateStatusActionName), + }, + labels: map[string]string{}, + } + + for _, opt := range opts { + opt(&action) + } + + return &action +} diff --git a/pkg/controller/actions/action_update_status_test.go b/pkg/controller/actions/action_update_status_test.go new file mode 100644 index 00000000000..2e3a38f639d --- /dev/null +++ b/pkg/controller/actions/action_update_status_test.go @@ -0,0 +1,163 @@ +package actions_test + +import ( + "context" + "testing" + + "github.com/onsi/gomega/gstruct" + "github.com/rs/xid" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" + dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" + dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/controllers/status" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster/gvk" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" + + . "github.com/onsi/gomega" +) + +func TestUpdateStatusActionNotReady(t *testing.T) { + g := NewWithT(t) + + ctx := context.Background() + ns := xid.New().String() + + client := NewFakeClient( + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvk.Deployment.GroupVersion().String(), + Kind: gvk.Deployment.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-deployment", + Namespace: ns, + Labels: map[string]string{ + labels.K8SCommon.PartOf: "foo", + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + ReadyReplicas: 0, + }, + }, + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvk.Deployment.GroupVersion().String(), + Kind: gvk.Deployment.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-deployment-2", + Namespace: ns, + Labels: map[string]string{ + labels.K8SCommon.PartOf: "foo", + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + ReadyReplicas: 1, + }, + }, + ) + + action := actions.NewUpdateStatusAction( + ctx, + actions.WithUpdateStatusLabel(labels.K8SCommon.PartOf, "foo")) + + rr := types.ReconciliationRequest{ + Client: client, + Instance: &componentsv1.Dashboard{}, + DSCI: &dsciv1.DSCInitialization{Spec: dsciv1.DSCInitializationSpec{ApplicationsNamespace: ns}}, + DSC: &dscv1.DataScienceCluster{}, + Platform: cluster.OpenDataHub, + } + + err := action.Execute(ctx, &rr) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(rr.Instance).Should( + WithTransform( + ExtractStatusCondition(status.ConditionTypeReady), + gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Status": Equal(metav1.ConditionFalse), + "Reason": Equal(actions.DeploymentsNotReadyReason), + }), + ), + ) +} + +func TestUpdateStatusActionReady(t *testing.T) { + g := NewWithT(t) + + ctx := context.Background() + ns := xid.New().String() + + client := NewFakeClient( + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvk.Deployment.GroupVersion().String(), + Kind: gvk.Deployment.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-deployment", + Namespace: ns, + Labels: map[string]string{ + labels.K8SCommon.PartOf: "foo", + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + ReadyReplicas: 1, + }, + }, + &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + APIVersion: gvk.Deployment.GroupVersion().String(), + Kind: gvk.Deployment.Kind, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-deployment-2", + Namespace: ns, + Labels: map[string]string{ + labels.K8SCommon.PartOf: "foo", + }, + }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + ReadyReplicas: 1, + }, + }, + ) + + action := actions.NewUpdateStatusAction( + ctx, + actions.WithUpdateStatusLabel(labels.K8SCommon.PartOf, "foo")) + + rr := types.ReconciliationRequest{ + Client: client, + Instance: &componentsv1.Dashboard{}, + DSCI: &dsciv1.DSCInitialization{Spec: dsciv1.DSCInitializationSpec{ApplicationsNamespace: ns}}, + DSC: &dscv1.DataScienceCluster{}, + Platform: cluster.OpenDataHub, + } + + err := action.Execute(ctx, &rr) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(rr.Instance).Should( + WithTransform( + ExtractStatusCondition(status.ConditionTypeReady), + gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{ + "Status": Equal(metav1.ConditionTrue), + "Reason": Equal(actions.ReadyReason), + }), + ), + ) +} diff --git a/pkg/controller/actions/actions.go b/pkg/controller/actions/actions.go new file mode 100644 index 00000000000..bf1739e9f0b --- /dev/null +++ b/pkg/controller/actions/actions.go @@ -0,0 +1,25 @@ +package actions + +import ( + "context" + + "github.com/go-logr/logr" + + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" +) + +// +// Common +// + +const ( + ActionGroup = "action" +) + +type Action interface { + Execute(ctx context.Context, rr *types.ReconciliationRequest) error +} + +type BaseAction struct { + Log logr.Logger +} diff --git a/pkg/controller/client/client.go b/pkg/controller/client/client.go new file mode 100644 index 00000000000..9b4cf5b8adf --- /dev/null +++ b/pkg/controller/client/client.go @@ -0,0 +1,50 @@ +package client + +import ( + "context" + "fmt" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + ctrlCli "sigs.k8s.io/controller-runtime/pkg/client" +) + +func NewFromManager(ctx context.Context, mgr ctrl.Manager) (*Client, error) { + return New(ctx, mgr.GetConfig(), mgr.GetClient()) +} + +func New(_ context.Context, cfg *rest.Config, client ctrlCli.Client) (*Client, error) { + k8sCli, err := kubernetes.NewForConfig(cfg) + if err != nil { + return nil, fmt.Errorf("unable to construct a playground client: %w", err) + } + + return &Client{ + Client: client, + K: k8sCli, + }, nil +} + +type Client struct { + ctrlCli.Client + K kubernetes.Interface +} + +func (c *Client) Apply(ctx context.Context, obj ctrlCli.Object, opts ...ctrlCli.PatchOption) error { + err := c.Client.Patch(ctx, obj, ctrlCli.Apply, opts...) + if err != nil { + return fmt.Errorf("unable to pactch object %s: %w", obj, err) + } + + return nil +} + +func (c *Client) ApplyStatus(ctx context.Context, obj ctrlCli.Object, opts ...ctrlCli.SubResourcePatchOption) error { + err := c.Client.Status().Patch(ctx, obj, ctrlCli.Apply, opts...) + if err != nil { + return fmt.Errorf("unable to pactch object %s: %w", obj, err) + } + + return nil +} diff --git a/controllers/components/base_reconciler_type.go b/pkg/controller/reconciler/component_reconciler.go similarity index 63% rename from controllers/components/base_reconciler_type.go rename to pkg/controller/reconciler/component_reconciler.go index c22327e6b0d..22bc74ee006 100644 --- a/controllers/components/base_reconciler_type.go +++ b/pkg/controller/reconciler/component_reconciler.go @@ -1,55 +1,33 @@ -package components +package types import ( "context" "errors" "fmt" + "reflect" + "github.com/go-logr/logr" - "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" - dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" - dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" - "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" - "reflect" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" -) - -type Action interface { - Execute(ctx context.Context, rr *ReconciliationRequest) error -} - -type BaseAction struct { - Log logr.Logger -} - -type ResourceObject interface { - client.Object - components.WithStatus -} - -type ReconciliationRequest struct { - client.Client - Instance client.Object - DSC *dscv1.DataScienceCluster - DSCI *dsciv1.DSCInitialization - Platform cluster.Platform - Manifests Manifests -} -type Manifests struct { - Paths map[cluster.Platform]string -} + dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" + dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions" + odhClient "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/client" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" +) -type BaseReconciler[T ResourceObject] struct { - Client client.Client +type ComponentReconciler[T types.ResourceObject] struct { + Client *odhClient.Client Scheme *runtime.Scheme - Actions []Action - Finalizer []Action + Actions []actions.Action + Finalizer []actions.Action Log logr.Logger Manager manager.Manager Controller controller.Controller @@ -57,29 +35,44 @@ type BaseReconciler[T ResourceObject] struct { Platform cluster.Platform } -func NewBaseReconciler[T ResourceObject](mgr manager.Manager, name string) *BaseReconciler[T] { - return &BaseReconciler[T]{ - Client: mgr.GetClient(), +func NewComponentReconciler[T types.ResourceObject](ctx context.Context, mgr manager.Manager, name string) (*ComponentReconciler[T], error) { + oc, err := odhClient.NewFromManager(ctx, mgr) + if err != nil { + return nil, err + } + + cc := ComponentReconciler[T]{ + Client: oc, Scheme: mgr.GetScheme(), Log: ctrl.Log.WithName("controllers").WithName(name), Manager: mgr, Recorder: mgr.GetEventRecorderFor(name), Platform: cluster.GetRelease().Name, } + + return &cc, nil } -func (r *BaseReconciler[T]) AddAction(action Action) { +func (r *ComponentReconciler[T]) GetLogger() logr.Logger { + return r.Log +} + +func (r *ComponentReconciler[T]) AddAction(action actions.Action) { r.Actions = append(r.Actions, action) } -func (r *BaseReconciler[T]) AddFinalizer(action Action) { +func (r *ComponentReconciler[T]) AddFinalizer(action actions.Action) { r.Finalizer = append(r.Finalizer, action) } -func (r *BaseReconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *ComponentReconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx) - res := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(T) + t := reflect.TypeOf(*new(T)).Elem() + res, ok := reflect.New(t).Interface().(T) + if !ok { + return ctrl.Result{}, fmt.Errorf("unable to construct instance of %v", t) + } if err := r.Client.Get(ctx, client.ObjectKey{Name: req.Name}, res); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } @@ -102,13 +95,13 @@ func (r *BaseReconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (ct return ctrl.Result{}, errors.New("unable to find DSCInitialization") } - rr := ReconciliationRequest{ + rr := types.ReconciliationRequest{ Client: r.Client, Instance: res, DSC: &dscl.Items[0], DSCI: &dscil.Items[0], Platform: r.Platform, - Manifests: Manifests{ + Manifests: types.Manifests{ Paths: make(map[cluster.Platform]string), }, } @@ -134,5 +127,17 @@ func (r *BaseReconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (ct } } + // update status + err := r.Client.ApplyStatus( + ctx, + rr.Instance, + client.FieldOwner(rr.Instance.GetName()), + client.ForceOwnership, + ) + + if err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil } diff --git a/pkg/controller/types/types.go b/pkg/controller/types/types.go new file mode 100644 index 00000000000..9610cf82b46 --- /dev/null +++ b/pkg/controller/types/types.go @@ -0,0 +1,33 @@ +package types + +import ( + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" + dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" + dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" +) + +type ResourceObject interface { + client.Object + components.WithStatus +} + +type WithLogger interface { + GetLogger() logr.Logger +} + +type ReconciliationRequest struct { + client.Client + Instance client.Object + DSC *dscv1.DataScienceCluster + DSCI *dsciv1.DSCInitialization + Platform cluster.Platform + Manifests Manifests +} + +type Manifests struct { + Paths map[cluster.Platform]string +} diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go index 5929d3046dc..e8d7feafa2f 100644 --- a/pkg/deploy/deploy.go +++ b/pkg/deploy/deploy.go @@ -155,6 +155,28 @@ func DeployManifestsFromPath( namespace string, componentName string, componentEnabled bool, +) error { + return DeployManifestsFromPathWithLabels( + ctx, + cli, + owner, + manifestPath, + namespace, + componentName, + componentEnabled, map[string]string{}, + ) +} + +func DeployManifestsFromPathWithLabels( + ctx context.Context, + cli client.Client, + owner metav1.Object, + manifestPath string, + namespace string, + componentName string, + componentEnabled bool, + // TODO: this method must be refactored, left it just to avoid breaking compatibility + additionalLabels map[string]string, ) error { // Render the Kustomize manifests k := krusty.MakeKustomizer(krusty.MakeDefaultOptions()) @@ -180,7 +202,22 @@ func DeployManifestsFromPath( return fmt.Errorf("failed applying namespace plugin when preparing Kustomize resources. %w", err) } - labelsPlugin := plugins.CreateAddLabelsPlugin(componentName) + resourceLabels := map[string]string{ + labels.ODH.Component(componentName): "true", + labels.K8SCommon.PartOf: componentName, + } + + for k, v := range additionalLabels { + _, ok := resourceLabels[k] + if ok { + // don't override default labels + continue + } + + resourceLabels[k] = v + } + + labelsPlugin := plugins.CreateSetLabelsPlugin(resourceLabels) if err := labelsPlugin.Transform(resMap); err != nil { return fmt.Errorf("failed applying labels plugin when preparing Kustomize resources. %w", err) } diff --git a/pkg/metadata/labels/types.go b/pkg/metadata/labels/types.go index 382c2e38db3..f966bb8f884 100644 --- a/pkg/metadata/labels/types.go +++ b/pkg/metadata/labels/types.go @@ -2,6 +2,7 @@ package labels const ( ODHAppPrefix = "app.opendatahub.io" + ComponentName = "components.opendatahub.io/name" InjectTrustCA = "config.openshift.io/inject-trusted-cabundle" SecurityEnforce = "pod-security.kubernetes.io/enforce" ClusterMonitoring = "openshift.io/cluster-monitoring" diff --git a/pkg/plugins/addLabelsplugin.go b/pkg/plugins/addLabelsplugin.go index 13ecada5a93..5c79d2cb613 100644 --- a/pkg/plugins/addLabelsplugin.go +++ b/pkg/plugins/addLabelsplugin.go @@ -16,11 +16,15 @@ import ( // - It adds labels to the "spec/template/metadata/labels" and "spec/selector/matchLabels" paths // for resources of kind "Deployment". func CreateAddLabelsPlugin(componentName string) *builtins.LabelTransformerPlugin { + return CreateSetLabelsPlugin(map[string]string{ + labels.ODH.Component(componentName): "true", + labels.K8SCommon.PartOf: componentName, + }) +} + +func CreateSetLabelsPlugin(labels map[string]string) *builtins.LabelTransformerPlugin { return &builtins.LabelTransformerPlugin{ - Labels: map[string]string{ - labels.ODH.Component(componentName): "true", - labels.K8SCommon.PartOf: componentName, - }, + Labels: labels, FieldSpecs: []types.FieldSpec{ { Gvk: resid.Gvk{Kind: "Deployment"}, diff --git a/tests/e2e/helper_test.go b/tests/e2e/helper_test.go index ac13a3d7c01..4cf760664da 100644 --- a/tests/e2e/helper_test.go +++ b/tests/e2e/helper_test.go @@ -3,7 +3,6 @@ package e2e_test import ( "context" "fmt" - componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" "log" "strings" "testing" @@ -23,6 +22,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/opendatahub-io/opendatahub-operator/v2/apis/components" + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" dscv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" infrav1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/infrastructure/v1"