From f4996a9dd550aeaa282d2e3edd3d82630d4fcfbb Mon Sep 17 00:00:00 2001 From: jackdelahunt Date: Mon, 4 Nov 2024 20:35:40 +0000 Subject: [PATCH] refactor: datasciencepipelines component refactor --- .../v1/datasciencepipelines_types.go | 47 +-- apis/components/v1/zz_generated.deepcopy.go | 36 ++- .../v1/datasciencecluster_types.go | 3 +- ...s.opendatahub.io_datasciencepipelines.yaml | 33 +- ...atahub-operator.clusterserviceversion.yaml | 4 +- .../datasciencepipelines.go | 176 ----------- .../zz_generated.deepcopy.go | 39 --- ...s.opendatahub.io_datasciencepipelines.yaml | 33 +- .../datasciencepipelines.go | 70 +++++ .../datasciencepipelines_controller.go | 88 ++++-- ...datasciencepipelines_controller_actions.go | 115 +++++++ .../datasciencecluster_controller.go | 30 +- .../datasciencecluster/kubebuilder_rbac.go | 5 + controllers/status/status.go | 6 + controllers/webhook/webhook_suite_test.go | 5 +- docs/api-overview.md | 67 ++-- main.go | 10 + pkg/upgrade/upgrade.go | 5 +- tests/e2e/controller_test.go | 11 +- tests/e2e/dashboard_test.go | 1 - tests/e2e/datasciencepipelines_test.go | 288 ++++++++++++++++++ tests/e2e/helper_test.go | 7 +- tests/e2e/odh_manager_test.go | 6 + 23 files changed, 750 insertions(+), 335 deletions(-) delete mode 100644 components/datasciencepipelines/datasciencepipelines.go delete mode 100644 components/datasciencepipelines/zz_generated.deepcopy.go create mode 100644 controllers/components/datasciencepipelines/datasciencepipelines.go create mode 100644 controllers/components/datasciencepipelines/datasciencepipelines_controller_actions.go create mode 100644 tests/e2e/datasciencepipelines_test.go diff --git a/apis/components/v1/datasciencepipelines_types.go b/apis/components/v1/datasciencepipelines_types.go index a490c35fe3f..8964c348976 100644 --- a/apis/components/v1/datasciencepipelines_types.go +++ b/apis/components/v1/datasciencepipelines_types.go @@ -21,26 +21,17 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - -// DataSciencePipelinesSpec defines the desired state of DataSciencePipelines -type DataSciencePipelinesSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - - // Foo is an example field of DataSciencePipelines. Edit datasciencepipelines_types.go to remove/update - Foo string `json:"foo,omitempty"` -} - -// DataSciencePipelinesStatus defines the observed state of DataSciencePipelines -type DataSciencePipelinesStatus struct { - components.Status `json:",inline"` -} +const ( + DataSciencePipelinesComponentName = "data-science-pipelines-operator" + // value should match whats set in the XValidation below + DataSciencePipelinesInstanceName = "default-datasciencepipelines" + DataSciencePipelinesKind = "DataSciencePipelines" +) // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster +// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'default-datasciencepipelines'",message="DataSciencePipelines name must be default-datasciencepipelines" // DataSciencePipelines is the Schema for the datasciencepipelines API type DataSciencePipelines struct { @@ -51,8 +42,22 @@ type DataSciencePipelines struct { Status DataSciencePipelinesStatus `json:"status,omitempty"` } +// DataSciencePipelinesSpec defines the desired state of DataSciencePipelines +type DataSciencePipelinesSpec struct { + DataSciencePipelinesCommonSpec `json:",inline"` +} + +type DataSciencePipelinesCommonSpec struct { + components.DevFlagsSpec `json:",inline"` +} + +// DataSciencePipelinesStatus defines the observed state of DataSciencePipelines +type DataSciencePipelinesStatus struct { + components.Status `json:",inline"` +} + func (c *DataSciencePipelines) GetDevFlags() *components.DevFlags { - return nil + return c.Spec.DevFlags } func (c *DataSciencePipelines) GetStatus() *components.Status { @@ -71,3 +76,11 @@ type DataSciencePipelinesList struct { func init() { SchemeBuilder.Register(&DataSciencePipelines{}, &DataSciencePipelinesList{}) } + +// DSCDataSciencePipelines contains all the configuration exposed in DSC instance for DataSciencePipelines component +type DSCDataSciencePipelines struct { + // configuration fields common across components + components.ManagementSpec `json:",inline"` + // datasciencepipelines specific field + DataSciencePipelinesCommonSpec `json:",inline"` +} diff --git a/apis/components/v1/zz_generated.deepcopy.go b/apis/components/v1/zz_generated.deepcopy.go index 11e0f992750..c87ef7b58a9 100644 --- a/apis/components/v1/zz_generated.deepcopy.go +++ b/apis/components/v1/zz_generated.deepcopy.go @@ -131,6 +131,23 @@ func (in *DSCDashboard) DeepCopy() *DSCDashboard { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DSCDataSciencePipelines) DeepCopyInto(out *DSCDataSciencePipelines) { + *out = *in + out.ManagementSpec = in.ManagementSpec + in.DataSciencePipelinesCommonSpec.DeepCopyInto(&out.DataSciencePipelinesCommonSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DSCDataSciencePipelines. +func (in *DSCDataSciencePipelines) DeepCopy() *DSCDataSciencePipelines { + if in == nil { + return nil + } + out := new(DSCDataSciencePipelines) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DSCKueue) DeepCopyInto(out *DSCKueue) { *out = *in @@ -326,7 +343,7 @@ func (in *DataSciencePipelines) DeepCopyInto(out *DataSciencePipelines) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -348,6 +365,22 @@ func (in *DataSciencePipelines) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataSciencePipelinesCommonSpec) DeepCopyInto(out *DataSciencePipelinesCommonSpec) { + *out = *in + in.DevFlagsSpec.DeepCopyInto(&out.DevFlagsSpec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSciencePipelinesCommonSpec. +func (in *DataSciencePipelinesCommonSpec) DeepCopy() *DataSciencePipelinesCommonSpec { + if in == nil { + return nil + } + out := new(DataSciencePipelinesCommonSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DataSciencePipelinesList) DeepCopyInto(out *DataSciencePipelinesList) { *out = *in @@ -383,6 +416,7 @@ func (in *DataSciencePipelinesList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DataSciencePipelinesSpec) DeepCopyInto(out *DataSciencePipelinesSpec) { *out = *in + in.DataSciencePipelinesCommonSpec.DeepCopyInto(&out.DataSciencePipelinesCommonSpec) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSciencePipelinesSpec. diff --git a/apis/datasciencecluster/v1/datasciencecluster_types.go b/apis/datasciencecluster/v1/datasciencecluster_types.go index e07464c1bef..93fe1236901 100644 --- a/apis/datasciencecluster/v1/datasciencecluster_types.go +++ b/apis/datasciencecluster/v1/datasciencecluster_types.go @@ -27,7 +27,6 @@ import ( componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" "github.com/opendatahub-io/opendatahub-operator/v2/components" "github.com/opendatahub-io/opendatahub-operator/v2/components/codeflare" - "github.com/opendatahub-io/opendatahub-operator/v2/components/datasciencepipelines" "github.com/opendatahub-io/opendatahub-operator/v2/components/kserve" "github.com/opendatahub-io/opendatahub-operator/v2/components/modelmeshserving" "github.com/opendatahub-io/opendatahub-operator/v2/components/trainingoperator" @@ -55,7 +54,7 @@ type Components struct { // DataServicePipeline component configuration. // Require OpenShift Pipelines Operator to be installed before enable component - DataSciencePipelines datasciencepipelines.DataSciencePipelines `json:"datasciencepipelines,omitempty"` + DataSciencePipelines componentsv1.DSCDataSciencePipelines `json:"datasciencepipelines,omitempty"` // Kserve component configuration. // Require OpenShift Serverless and OpenShift Service Mesh Operators to be installed before enable component diff --git a/bundle/manifests/components.opendatahub.io_datasciencepipelines.yaml b/bundle/manifests/components.opendatahub.io_datasciencepipelines.yaml index d077513ff49..be11c8e4d30 100644 --- a/bundle/manifests/components.opendatahub.io_datasciencepipelines.yaml +++ b/bundle/manifests/components.opendatahub.io_datasciencepipelines.yaml @@ -40,10 +40,32 @@ spec: spec: description: DataSciencePipelinesSpec defines the desired state of DataSciencePipelines properties: - foo: - description: Foo is an example field of DataSciencePipelines. Edit - datasciencepipelines_types.go to remove/update - type: string + devFlags: + description: Add developer fields + properties: + manifests: + description: List of custom manifests for the given component + items: + properties: + contextDir: + default: manifests + description: contextDir is the relative path to the folder + containing manifests in a repository, default value "manifests" + type: string + sourcePath: + default: "" + description: 'sourcePath is the subpath within contextDir + where kustomize builds start. Examples include any sub-folder + or path: `base`, `overlays/dev`, `default`, `odh` etc.' + type: string + uri: + default: "" + description: uri is the URI point to a git repo with tag/branch. + e.g. https://github.com/org/repo/tarball/ + type: string + type: object + type: array + type: object type: object status: description: DataSciencePipelinesStatus defines the observed state of @@ -112,6 +134,9 @@ spec: type: string type: object type: object + x-kubernetes-validations: + - message: DataSciencePipelines name must be default-datasciencepipelines + rule: self.metadata.name == 'default-datasciencepipelines' served: true storage: true subresources: diff --git a/bundle/manifests/opendatahub-operator.clusterserviceversion.yaml b/bundle/manifests/opendatahub-operator.clusterserviceversion.yaml index 1989bdc167f..66e6c63812a 100644 --- a/bundle/manifests/opendatahub-operator.clusterserviceversion.yaml +++ b/bundle/manifests/opendatahub-operator.clusterserviceversion.yaml @@ -103,7 +103,7 @@ metadata: categories: AI/Machine Learning, Big Data certified: "False" containerImage: quay.io/opendatahub/opendatahub-operator:v2.19.0 - createdAt: "2024-11-01T10:08:53Z" + createdAt: "2024-11-12T13:04:02Z" olm.skipRange: '>=1.0.0 <2.19.0' operators.operatorframework.io/builder: operator-sdk-v1.31.0 operators.operatorframework.io/internal-objects: '["featuretrackers.features.opendatahub.io", @@ -1220,7 +1220,7 @@ spec: value: /opt/manifests - name: ODH_PLATFORM_TYPE value: OpenDataHub - image: REPLACE_IMAGE:latest + image: quay.io/opendatahub/opendatahub-operator:latest imagePullPolicy: Always livenessProbe: httpGet: diff --git a/components/datasciencepipelines/datasciencepipelines.go b/components/datasciencepipelines/datasciencepipelines.go deleted file mode 100644 index f0066a6c544..00000000000 --- a/components/datasciencepipelines/datasciencepipelines.go +++ /dev/null @@ -1,176 +0,0 @@ -// Package datasciencepipelines provides utility functions to config Data Science Pipelines: -// Pipeline solution for end to end MLOps workflows that support the Kubeflow Pipelines SDK, Tekton and Argo Workflows. -// +groupName=datasciencecluster.opendatahub.io -package datasciencepipelines - -import ( - "context" - "fmt" - "path/filepath" - - "github.com/go-logr/logr" - operatorv1 "github.com/openshift/api/operator/v1" - conditionsv1 "github.com/openshift/custom-resource-status/conditions/v1" - corev1 "k8s.io/api/core/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - k8serr "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - logf "sigs.k8s.io/controller-runtime/pkg/log" - - dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" - "github.com/opendatahub-io/opendatahub-operator/v2/components" - "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/deploy" - "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" -) - -var ( - ComponentName = "data-science-pipelines-operator" - Path = deploy.DefaultManifestPath + "/" + ComponentName + "/base" - OverlayPath = deploy.DefaultManifestPath + "/" + ComponentName + "/overlays" - ArgoWorkflowCRD = "workflows.argoproj.io" -) - -// Verifies that Dashboard implements ComponentInterface. -var _ components.ComponentInterface = (*DataSciencePipelines)(nil) - -// DataSciencePipelines struct holds the configuration for the DataSciencePipelines component. -// +kubebuilder:object:generate=true -type DataSciencePipelines struct { - components.Component `json:""` -} - -func (d *DataSciencePipelines) Init(ctx context.Context, _ cluster.Platform) error { - log := logf.FromContext(ctx).WithName(ComponentName) - - var imageParamMap = map[string]string{ - // v1 - "IMAGES_APISERVER": "RELATED_IMAGE_ODH_ML_PIPELINES_API_SERVER_IMAGE", - "IMAGES_ARTIFACT": "RELATED_IMAGE_ODH_ML_PIPELINES_ARTIFACT_MANAGER_IMAGE", - "IMAGES_PERSISTENTAGENT": "RELATED_IMAGE_ODH_ML_PIPELINES_PERSISTENCEAGENT_IMAGE", - "IMAGES_SCHEDULEDWORKFLOW": "RELATED_IMAGE_ODH_ML_PIPELINES_SCHEDULEDWORKFLOW_IMAGE", - "IMAGES_CACHE": "RELATED_IMAGE_ODH_ML_PIPELINES_CACHE_IMAGE", - "IMAGES_DSPO": "RELATED_IMAGE_ODH_DATA_SCIENCE_PIPELINES_OPERATOR_CONTROLLER_IMAGE", - // v2 - "IMAGESV2_ARGO_APISERVER": "RELATED_IMAGE_ODH_ML_PIPELINES_API_SERVER_V2_IMAGE", - "IMAGESV2_ARGO_PERSISTENCEAGENT": "RELATED_IMAGE_ODH_ML_PIPELINES_PERSISTENCEAGENT_V2_IMAGE", - "IMAGESV2_ARGO_SCHEDULEDWORKFLOW": "RELATED_IMAGE_ODH_ML_PIPELINES_SCHEDULEDWORKFLOW_V2_IMAGE", - "IMAGESV2_ARGO_ARGOEXEC": "RELATED_IMAGE_ODH_DATA_SCIENCE_PIPELINES_ARGO_ARGOEXEC_IMAGE", - "IMAGESV2_ARGO_WORKFLOWCONTROLLER": "RELATED_IMAGE_ODH_DATA_SCIENCE_PIPELINES_ARGO_WORKFLOWCONTROLLER_IMAGE", - "V2_DRIVER_IMAGE": "RELATED_IMAGE_ODH_ML_PIPELINES_DRIVER_IMAGE", - "V2_LAUNCHER_IMAGE": "RELATED_IMAGE_ODH_ML_PIPELINES_LAUNCHER_IMAGE", - "IMAGESV2_ARGO_MLMDGRPC": "RELATED_IMAGE_ODH_MLMD_GRPC_SERVER_IMAGE", - } - - if err := deploy.ApplyParams(Path, imageParamMap); err != nil { - log.Error(err, "failed to update image", "path", Path) - } - - return nil -} - -func (d *DataSciencePipelines) OverrideManifests(ctx context.Context, _ cluster.Platform) error { - // If devflags are set, update default manifests path - if len(d.DevFlags.Manifests) != 0 { - manifestConfig := d.DevFlags.Manifests[0] - if err := deploy.DownloadManifests(ctx, ComponentName, manifestConfig); err != nil { - return err - } - // If overlay is defined, update paths - defaultKustomizePath := "base" - if manifestConfig.SourcePath != "" { - defaultKustomizePath = manifestConfig.SourcePath - } - Path = filepath.Join(deploy.DefaultManifestPath, ComponentName, defaultKustomizePath) - } - - return nil -} - -func (d *DataSciencePipelines) GetComponentName() string { - return ComponentName -} - -func (d *DataSciencePipelines) ReconcileComponent(ctx context.Context, - cli client.Client, - l logr.Logger, - owner metav1.Object, - dscispec *dsciv1.DSCInitializationSpec, - platform cluster.Platform, - _ bool, -) error { - enabled := d.GetManagementState() == operatorv1.Managed - monitoringEnabled := dscispec.Monitoring.ManagementState == operatorv1.Managed - - if enabled { - if d.DevFlags != nil { - // Download manifests and update paths - if err := d.OverrideManifests(ctx, platform); err != nil { - return err - } - } - // skip check if the dependent operator has beeninstalled, this is done in dashboard - // Check for existing Argo Workflows - if err := UnmanagedArgoWorkFlowExists(ctx, cli); err != nil { - return err - } - } - - // new overlay - manifestsPath := filepath.Join(OverlayPath, "rhoai") - if platform == cluster.OpenDataHub || platform == "" { - manifestsPath = filepath.Join(OverlayPath, "odh") - } - if err := deploy.DeployManifestsFromPath(ctx, cli, owner, manifestsPath, dscispec.ApplicationsNamespace, ComponentName, enabled); err != nil { - return err - } - l.Info("apply manifests done") - - // Wait for deployment available - if enabled { - if err := cluster.WaitForDeploymentAvailable(ctx, cli, ComponentName, dscispec.ApplicationsNamespace, 20, 2); err != nil { - return fmt.Errorf("deployment for %s is not ready to server: %w", ComponentName, err) - } - } - - // CloudService Monitoring handling - if platform == cluster.ManagedRhods { - if err := d.UpdatePrometheusConfig(cli, l, enabled && monitoringEnabled, ComponentName); err != nil { - return err - } - if err := deploy.DeployManifestsFromPath(ctx, cli, owner, - filepath.Join(deploy.DefaultManifestPath, "monitoring", "prometheus", "apps"), - dscispec.Monitoring.Namespace, - "prometheus", true); err != nil { - return err - } - l.Info("updating SRE monitoring done") - } - - return nil -} - -func UnmanagedArgoWorkFlowExists(ctx context.Context, - cli client.Client) error { - workflowCRD := &apiextensionsv1.CustomResourceDefinition{} - if err := cli.Get(ctx, client.ObjectKey{Name: ArgoWorkflowCRD}, workflowCRD); err != nil { - if k8serr.IsNotFound(err) { - return nil - } - return fmt.Errorf("failed to get existing Workflow CRD : %w", err) - } - // Verify if existing workflow is deployed by ODH with label - odhLabelValue, odhLabelExists := workflowCRD.Labels[labels.ODH.Component(ComponentName)] - if odhLabelExists && odhLabelValue == "true" { - return nil - } - return fmt.Errorf("%s CRD already exists but not deployed by this operator. "+ - "Remove existing Argo workflows or set `spec.components.datasciencepipelines.managementState` to Removed to proceed ", ArgoWorkflowCRD) -} - -func SetExistingArgoCondition(conditions *[]conditionsv1.Condition, reason, message string) { - status.SetCondition(conditions, string(status.CapabilityDSPv2Argo), reason, message, corev1.ConditionFalse) - status.SetComponentCondition(conditions, ComponentName, status.ReconcileFailed, message, corev1.ConditionFalse) -} diff --git a/components/datasciencepipelines/zz_generated.deepcopy.go b/components/datasciencepipelines/zz_generated.deepcopy.go deleted file mode 100644 index 11c4e758555..00000000000 --- a/components/datasciencepipelines/zz_generated.deepcopy.go +++ /dev/null @@ -1,39 +0,0 @@ -//go:build !ignore_autogenerated - -/* -Copyright 2023. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Code generated by controller-gen. DO NOT EDIT. - -package datasciencepipelines - -import () - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSciencePipelines) DeepCopyInto(out *DataSciencePipelines) { - *out = *in - in.Component.DeepCopyInto(&out.Component) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSciencePipelines. -func (in *DataSciencePipelines) DeepCopy() *DataSciencePipelines { - if in == nil { - return nil - } - out := new(DataSciencePipelines) - in.DeepCopyInto(out) - return out -} diff --git a/config/crd/bases/components.opendatahub.io_datasciencepipelines.yaml b/config/crd/bases/components.opendatahub.io_datasciencepipelines.yaml index 8783503a55f..4a9ec8bbfd5 100644 --- a/config/crd/bases/components.opendatahub.io_datasciencepipelines.yaml +++ b/config/crd/bases/components.opendatahub.io_datasciencepipelines.yaml @@ -40,10 +40,32 @@ spec: spec: description: DataSciencePipelinesSpec defines the desired state of DataSciencePipelines properties: - foo: - description: Foo is an example field of DataSciencePipelines. Edit - datasciencepipelines_types.go to remove/update - type: string + devFlags: + description: Add developer fields + properties: + manifests: + description: List of custom manifests for the given component + items: + properties: + contextDir: + default: manifests + description: contextDir is the relative path to the folder + containing manifests in a repository, default value "manifests" + type: string + sourcePath: + default: "" + description: 'sourcePath is the subpath within contextDir + where kustomize builds start. Examples include any sub-folder + or path: `base`, `overlays/dev`, `default`, `odh` etc.' + type: string + uri: + default: "" + description: uri is the URI point to a git repo with tag/branch. + e.g. https://github.com/org/repo/tarball/ + type: string + type: object + type: array + type: object type: object status: description: DataSciencePipelinesStatus defines the observed state of @@ -112,6 +134,9 @@ spec: type: string type: object type: object + x-kubernetes-validations: + - message: DataSciencePipelines name must be default-datasciencepipelines + rule: self.metadata.name == 'default-datasciencepipelines' served: true storage: true subresources: diff --git a/controllers/components/datasciencepipelines/datasciencepipelines.go b/controllers/components/datasciencepipelines/datasciencepipelines.go new file mode 100644 index 00000000000..e914ce7a7c9 --- /dev/null +++ b/controllers/components/datasciencepipelines/datasciencepipelines.go @@ -0,0 +1,70 @@ +package datasciencepipelines + +import ( + "fmt" + + operatorv1 "github.com/openshift/api/operator/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" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/cluster" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/deploy" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/annotations" +) + +const ( + ArgoWorkflowCRD = "workflows.argoproj.io" +) + +func Init(platform cluster.Platform) error { + var imageParamMap = map[string]string{ + // v1 + "IMAGES_APISERVER": "RELATED_IMAGE_ODH_ML_PIPELINES_API_SERVER_IMAGE", + "IMAGES_ARTIFACT": "RELATED_IMAGE_ODH_ML_PIPELINES_ARTIFACT_MANAGER_IMAGE", + "IMAGES_PERSISTENTAGENT": "RELATED_IMAGE_ODH_ML_PIPELINES_PERSISTENCEAGENT_IMAGE", + "IMAGES_SCHEDULEDWORKFLOW": "RELATED_IMAGE_ODH_ML_PIPELINES_SCHEDULEDWORKFLOW_IMAGE", + "IMAGES_CACHE": "RELATED_IMAGE_ODH_ML_PIPELINES_CACHE_IMAGE", + "IMAGES_DSPO": "RELATED_IMAGE_ODH_DATA_SCIENCE_PIPELINES_OPERATOR_CONTROLLER_IMAGE", + // v2 + "IMAGESV2_ARGO_APISERVER": "RELATED_IMAGE_ODH_ML_PIPELINES_API_SERVER_V2_IMAGE", + "IMAGESV2_ARGO_PERSISTENCEAGENT": "RELATED_IMAGE_ODH_ML_PIPELINES_PERSISTENCEAGENT_V2_IMAGE", + "IMAGESV2_ARGO_SCHEDULEDWORKFLOW": "RELATED_IMAGE_ODH_ML_PIPELINES_SCHEDULEDWORKFLOW_V2_IMAGE", + "IMAGESV2_ARGO_ARGOEXEC": "RELATED_IMAGE_ODH_DATA_SCIENCE_PIPELINES_ARGO_ARGOEXEC_IMAGE", + "IMAGESV2_ARGO_WORKFLOWCONTROLLER": "RELATED_IMAGE_ODH_DATA_SCIENCE_PIPELINES_ARGO_WORKFLOWCONTROLLER_IMAGE", + "V2_DRIVER_IMAGE": "RELATED_IMAGE_ODH_ML_PIPELINES_DRIVER_IMAGE", + "V2_LAUNCHER_IMAGE": "RELATED_IMAGE_ODH_ML_PIPELINES_LAUNCHER_IMAGE", + "IMAGESV2_ARGO_MLMDGRPC": "RELATED_IMAGE_ODH_MLMD_GRPC_SERVER_IMAGE", + } + + if err := deploy.ApplyParams(DefaultPath, imageParamMap); err != nil { + return fmt.Errorf("failed to update images on path %s: %w", DefaultPath, err) + } + + return nil +} + +func GetComponentCR(dsc *dscv1.DataScienceCluster) *componentsv1.DataSciencePipelines { + dataSciencePipelinesAnnotations := make(map[string]string) + + switch dsc.Spec.Components.DataSciencePipelines.ManagementState { + case operatorv1.Managed, operatorv1.Removed: + dataSciencePipelinesAnnotations[annotations.ManagementStateAnnotation] = string(dsc.Spec.Components.DataSciencePipelines.ManagementState) + default: // Force and Unmanaged case for unknown values, we do not support these yet + dataSciencePipelinesAnnotations[annotations.ManagementStateAnnotation] = "Unknown" + } + + return &componentsv1.DataSciencePipelines{ + TypeMeta: metav1.TypeMeta{ + Kind: componentsv1.DataSciencePipelinesKind, + APIVersion: componentsv1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: componentsv1.DataSciencePipelinesInstanceName, + Annotations: dataSciencePipelinesAnnotations, + }, + Spec: componentsv1.DataSciencePipelinesSpec{ + DataSciencePipelinesCommonSpec: dsc.Spec.Components.DataSciencePipelines.DataSciencePipelinesCommonSpec, + }, + } +} diff --git a/controllers/components/datasciencepipelines/datasciencepipelines_controller.go b/controllers/components/datasciencepipelines/datasciencepipelines_controller.go index 7422bfe5eb1..e7410ce9d4b 100644 --- a/controllers/components/datasciencepipelines/datasciencepipelines_controller.go +++ b/controllers/components/datasciencepipelines/datasciencepipelines_controller.go @@ -19,40 +19,70 @@ package datasciencepipelines import ( "context" - "k8s.io/apimachinery/pkg/runtime" + securityv1 "github.com/openshift/api/security/v1" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/builder" componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions/deploy" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions/render/kustomize" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions/updatestatus" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/predicates/resources" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/reconciler" + odhdeploy "github.com/opendatahub-io/opendatahub-operator/v2/pkg/deploy" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" ) -// DataSciencePipelinesReconciler reconciles a DataSciencePipelines object. -type DataSciencePipelinesReconciler struct { - client.Client - Scheme *runtime.Scheme -} +var ( + DefaultPath = odhdeploy.DefaultManifestPath + "/" + componentsv1.DataSciencePipelinesComponentName + "/base" +) -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the DataSciencePipelines object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.2/pkg/reconcile -func (r *DataSciencePipelinesReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - - // TODO(user): your logic here - - return ctrl.Result{}, nil -} +func NewComponentReconciler(ctx context.Context, mgr ctrl.Manager) error { + _, err := reconciler.ComponentReconcilerFor( + mgr, + componentsv1.DataSciencePipelinesInstanceName, + &componentsv1.DataSciencePipelines{}, + ). + // customized Owns() for Component with new predicates + Owns(&corev1.ConfigMap{}). + Owns(&corev1.Secret{}). + Owns(&rbacv1.ClusterRoleBinding{}). + Owns(&rbacv1.ClusterRole{}). + Owns(&rbacv1.Role{}). + Owns(&rbacv1.RoleBinding{}). + Owns(&corev1.ServiceAccount{}). + Owns(&corev1.Service{}). + Owns(&monitoringv1.ServiceMonitor{}). + Owns(&appsv1.Deployment{}, builder.WithPredicates(resources.NewDeploymentPredicate())). + Owns(&securityv1.SecurityContextConstraints{}). + Watches(&extv1.CustomResourceDefinition{}). // call ForLabel() + new predicates + // Add datasciencepipelines-specific actions + WithAction(checkPreConditions). + WithAction(initialize). + WithAction(devFlags). + WithAction(kustomize.NewAction( + kustomize.WithCache(kustomize.DefaultCachingKeyFn), + kustomize.WithLabel(labels.ODH.Component(componentsv1.DataSciencePipelinesComponentName), "true"), + kustomize.WithLabel(labels.K8SCommon.PartOf, componentsv1.DataSciencePipelinesComponentName), + )). + WithAction(deploy.NewAction( + deploy.WithCache(), + deploy.WithFieldOwner(componentsv1.DataSciencePipelinesInstanceName), + deploy.WithLabel(labels.ComponentPartOf, componentsv1.DataSciencePipelinesInstanceName), + )). + WithAction(updatestatus.NewAction( + updatestatus.WithSelectorLabel(labels.ComponentPartOf, componentsv1.DataSciencePipelinesInstanceName), + )). + Build(ctx) + + if err != nil { + return err // no need customize error, it is done in the caller main + } -// SetupWithManager sets up the controller with the Manager. -func (r *DataSciencePipelinesReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&componentsv1.DataSciencePipelines{}). - Complete(r) + return nil } diff --git a/controllers/components/datasciencepipelines/datasciencepipelines_controller_actions.go b/controllers/components/datasciencepipelines/datasciencepipelines_controller_actions.go new file mode 100644 index 00000000000..e9e2cf38906 --- /dev/null +++ b/controllers/components/datasciencepipelines/datasciencepipelines_controller_actions.go @@ -0,0 +1,115 @@ +/* +Copyright 2023. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package datasciencepipelines + +import ( + "context" + "fmt" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + k8serr "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/controllers/status" + odherrors "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/actions/errors" + odhtypes "github.com/opendatahub-io/opendatahub-operator/v2/pkg/controller/types" + odhdeploy "github.com/opendatahub-io/opendatahub-operator/v2/pkg/deploy" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" +) + +func checkPreConditions(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { + dsp, ok := rr.Instance.(*componentsv1.DataSciencePipelines) + if !ok { + return fmt.Errorf("resource instance %v is not a componentsv1.DataSciencePipelines", rr.Instance) + } + + // Check preconditions if this is an upgrade + if rr.Instance.GetStatus().Phase != status.PhaseReady { + return nil + } + + workflowCRD := &apiextensionsv1.CustomResourceDefinition{} + if err := rr.Client.Get(ctx, client.ObjectKey{Name: ArgoWorkflowCRD}, workflowCRD); err != nil { + if k8serr.IsNotFound(err) { + return nil + } + return odherrors.NewStopError("failed to get existing Workflow CRD : %v", err) + } + + // Verify if existing workflow is deployed by ODH with label + // if not then set Argo capability status condition to false + odhLabelValue, odhLabelExists := workflowCRD.Labels[labels.ODH.Component(componentsv1.DataSciencePipelinesComponentName)] + if !odhLabelExists || odhLabelValue != "true" { + s := dsp.GetStatus() + s.Phase = "NotReady" + + meta.SetStatusCondition(&s.Conditions, metav1.Condition{ + Type: status.ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: status.DataSciencePipelinesDoesntOwnArgoCRDReason, + Message: status.DataSciencePipelinesDoesntOwnArgoCRDMessage, + ObservedGeneration: s.ObservedGeneration, + }) + + return odherrors.NewStopError(status.DataSciencePipelinesDoesntOwnArgoCRDMessage) + } + + return nil +} + +func initialize(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { + rr.Manifests = append(rr.Manifests, odhtypes.ManifestInfo{ + Path: DefaultPath, + ContextDir: "", + SourcePath: "", + }) + if err := odhdeploy.ApplyParams(DefaultPath, nil, map[string]string{"namespace": rr.DSCI.Spec.ApplicationsNamespace}); err != nil { + return fmt.Errorf("failed to update params.env from %s : %w", rr.Manifests[0], err) + } + return nil +} + +func devFlags(ctx context.Context, rr *odhtypes.ReconciliationRequest) error { + dsp, ok := rr.Instance.(*componentsv1.DataSciencePipelines) + if !ok { + return fmt.Errorf("resource instance %v is not a componentsv1.DataSciencePipelines)", rr.Instance) + } + + if dsp.Spec.DevFlags == nil { + return nil + } + + // Implement devflags support logic + // If dev flags are set, update default manifests path + if len(dsp.Spec.DevFlags.Manifests) != 0 { + manifestConfig := dsp.Spec.DevFlags.Manifests[0] + if err := odhdeploy.DownloadManifests(ctx, componentsv1.DataSciencePipelinesComponentName, manifestConfig); err != nil { + return err + } + + if manifestConfig.SourcePath != "" { + rr.Manifests[0].Path = odhdeploy.DefaultManifestPath + rr.Manifests[0].ContextDir = componentsv1.DataSciencePipelinesComponentName + rr.Manifests[0].SourcePath = manifestConfig.SourcePath + } + } + + return nil +} diff --git a/controllers/datasciencecluster/datasciencecluster_controller.go b/controllers/datasciencecluster/datasciencecluster_controller.go index 7b139862e55..136b4726f4a 100644 --- a/controllers/datasciencecluster/datasciencecluster_controller.go +++ b/controllers/datasciencecluster/datasciencecluster_controller.go @@ -52,8 +52,8 @@ import ( 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" dashboardctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/dashboard" + datasciencepipelinesctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/datasciencepipelines" kueuectrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/kueue" modelregistryctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/modelregistry" rayctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/ray" @@ -210,21 +210,6 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R return ctrl.Result{}, nil } - // Check preconditions if this is an upgrade - if instance.Status.Phase == status.PhaseReady { - // Check for existence of Argo Workflows if DSP is - if instance.Status.InstalledComponents[datasciencepipelines.ComponentName] { - if err := datasciencepipelines.UnmanagedArgoWorkFlowExists(ctx, r.Client); err != nil { - message := fmt.Sprintf("Failed upgrade: %v ", err.Error()) - _, err = status.UpdateWithRetry(ctx, r.Client, instance, func(saved *dscv1.DataScienceCluster) { - datasciencepipelines.SetExistingArgoCondition(&saved.Status.Conditions, status.ArgoWorkflowExist, message) - status.SetErrorCondition(&saved.Status.Conditions, status.ArgoWorkflowExist, message) - saved.Status.Phase = status.PhaseError - }) - return ctrl.Result{}, err - } - } - } // Start reconciling if instance.Status.Conditions == nil { @@ -287,6 +272,14 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R componentErrors = multierror.Append(componentErrors, err) } + // Deploy DataSciencePipelines + if instance, err = r.ReconcileComponent(ctx, instance, componentsv1.DataSciencePipelinesComponentName, func() (error, bool) { + dsp := datasciencepipelinesctrl.GetComponentCR(instance) + return r.apply(ctx, instance, dsp), instance.Spec.Components.DataSciencePipelines.ManagementState == operatorv1.Managed + }); err != nil { + componentErrors = multierror.Append(componentErrors, err) + } + // Process errors for components if componentErrors != nil { log.Info("DataScienceCluster Deployment Incomplete.") @@ -555,6 +548,7 @@ func (r *DataScienceClusterReconciler) SetupWithManager(ctx context.Context, mgr Owns(&componentsv1.ModelRegistry{}). Owns(&componentsv1.TrustyAI{}). Owns(&componentsv1.Kueue{}). + Owns(&componentsv1.DataSciencePipelines{}). Owns( &corev1.ServiceAccount{}, builder.WithPredicates(saPredicates), @@ -652,10 +646,10 @@ func (r *DataScienceClusterReconciler) getRequestName(ctx context.Context) (stri // argoWorkflowCRDPredicates filters the delete events to trigger reconcile when Argo Workflow CRD is deleted. var argoWorkflowCRDPredicates = predicate.Funcs{ DeleteFunc: func(e event.DeleteEvent) bool { - if e.Object.GetName() == datasciencepipelines.ArgoWorkflowCRD { + if e.Object.GetName() == datasciencepipelinesctrl.ArgoWorkflowCRD { labelList := e.Object.GetLabels() // CRD to be deleted with label "app.opendatahub.io/datasciencepipeline":"true", should not trigger reconcile - if value, exist := labelList[labels.ODH.Component(datasciencepipelines.ComponentName)]; exist && value == "true" { + if value, exist := labelList[labels.ODH.Component(componentsv1.DataSciencePipelinesComponentName)]; exist && value == "true" { return false } } diff --git a/controllers/datasciencecluster/kubebuilder_rbac.go b/controllers/datasciencecluster/kubebuilder_rbac.go index 8ff61c5fbf9..ce6f80bf79d 100644 --- a/controllers/datasciencecluster/kubebuilder_rbac.go +++ b/controllers/datasciencecluster/kubebuilder_rbac.go @@ -115,6 +115,11 @@ package datasciencecluster // +kubebuilder:rbac:groups="user.openshift.io",resources=users,verbs=list;watch;patch;delete;get // +kubebuilder:rbac:groups="console.openshift.io",resources=consolelinks,verbs=create;get;patch;delete +// DataSciencePipelines +// +kubebuilder:rbac:groups=components.opendatahub.io,resources=datasciencepipelines,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=components.opendatahub.io,resources=datasciencepipelines/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=components.opendatahub.io,resources=datasciencepipelines/finalizers,verbs=update + // Ray // +kubebuilder:rbac:groups=components.opendatahub.io,resources=rays,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=components.opendatahub.io,resources=rays/status,verbs=get;update;patch diff --git a/controllers/status/status.go b/controllers/status/status.go index 71558191eab..f7b5d533752 100644 --- a/controllers/status/status.go +++ b/controllers/status/status.go @@ -94,6 +94,12 @@ const ( ServiceMeshNotConfiguredMessage = "ServiceMesh needs to be set to 'Managed' in DSCI CR" ) +const ( + DataSciencePipelinesDoesntOwnArgoCRDReason = "DataSciencePipelinesDoesntOwnArgoCRD" + DataSciencePipelinesDoesntOwnArgoCRDMessage = "Failed upgrade: workflows.argoproj.io CRD already exists but not deployed by this operator " + + "remove existing Argo workflows or set `spec.components.datasciencepipelines.managementState` to Removed to proceed" +) + // SetProgressingCondition sets the ProgressingCondition to True and other conditions to false or // Unknown. Used when we are just starting to reconcile, and there are no existing conditions. func SetProgressingCondition(conditions *[]conditionsv1.Condition, reason string, message string) { diff --git a/controllers/webhook/webhook_suite_test.go b/controllers/webhook/webhook_suite_test.go index d30996ae901..e6a282c4fc1 100644 --- a/controllers/webhook/webhook_suite_test.go +++ b/controllers/webhook/webhook_suite_test.go @@ -46,7 +46,6 @@ import ( dsciv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1" componentsold "github.com/opendatahub-io/opendatahub-operator/v2/components" "github.com/opendatahub-io/opendatahub-operator/v2/components/codeflare" - "github.com/opendatahub-io/opendatahub-operator/v2/components/datasciencepipelines" "github.com/opendatahub-io/opendatahub-operator/v2/components/kserve" "github.com/opendatahub-io/opendatahub-operator/v2/components/modelmeshserving" "github.com/opendatahub-io/opendatahub-operator/v2/components/workbenches" @@ -275,8 +274,8 @@ func newDSC(name string, namespace string) *dscv1.DataScienceCluster { ManagementState: operatorv1.Removed, }, }, - DataSciencePipelines: datasciencepipelines.DataSciencePipelines{ - Component: componentsold.Component{ + DataSciencePipelines: componentsv1.DSCDataSciencePipelines{ + ManagementSpec: components.ManagementSpec{ ManagementState: operatorv1.Removed, }, }, diff --git a/docs/api-overview.md b/docs/api-overview.md index 60d81e90ee7..07f85986b86 100644 --- a/docs/api-overview.md +++ b/docs/api-overview.md @@ -120,6 +120,23 @@ DSCDashboard contains all the configuration exposed in DSC instance for Dashboar +_Appears in:_ +- [Components](#components) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `managementState` _[ManagementState](#managementstate)_ | Set to one of the following values:

- "Managed" : the operator is actively managing the component and trying to keep it active.
It will only upgrade the component if it is safe to do so

- "Removed" : the operator is actively managing the component and will not install it,
or if it is installed, the operator will try to remove it | | Enum: [Managed Removed]
| +| `devFlags` _[DevFlags](#devflags)_ | Add developer fields | | | + + +#### DSCDataSciencePipelines + + + +DSCDataSciencePipelines contains all the configuration exposed in DSC instance for DataSciencePipelines component + + + _Appears in:_ - [Components](#components) @@ -331,6 +348,23 @@ _Appears in:_ | `status` _[DataSciencePipelinesStatus](#datasciencepipelinesstatus)_ | | | | +#### DataSciencePipelinesCommonSpec + + + + + + + +_Appears in:_ +- [DSCDataSciencePipelines](#dscdatasciencepipelines) +- [DataSciencePipelinesSpec](#datasciencepipelinesspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `devFlags` _[DevFlags](#devflags)_ | Add developer fields | | | + + #### DataSciencePipelinesList @@ -364,7 +398,7 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `foo` _string_ | Foo is an example field of DataSciencePipelines. Edit datasciencepipelines_types.go to remove/update | | | +| `devFlags` _[DevFlags](#devflags)_ | Add developer fields | | | #### DataSciencePipelinesStatus @@ -1104,7 +1138,6 @@ Component struct defines the basis for each OpenDataHub component configuration. _Appears in:_ - [CodeFlare](#codeflare) -- [DataSciencePipelines](#datasciencepipelines) - [Kserve](#kserve) - [ModelMeshServing](#modelmeshserving) - [TrainingOperator](#trainingoperator) @@ -1148,12 +1181,15 @@ DevFlagsSpec struct defines the component's dev flags configuration. _Appears in:_ - [Component](#component) - [DSCDashboard](#dscdashboard) +- [DSCDataSciencePipelines](#dscdatasciencepipelines) - [DSCKueue](#dsckueue) - [DSCModelRegistry](#dscmodelregistry) - [DSCRay](#dscray) - [DSCTrustyAI](#dsctrustyai) - [DashboardCommonSpec](#dashboardcommonspec) - [DashboardSpec](#dashboardspec) +- [DataSciencePipelinesCommonSpec](#datasciencepipelinescommonspec) +- [DataSciencePipelinesSpec](#datasciencepipelinesspec) - [KueueCommonSpec](#kueuecommonspec) - [KueueSpec](#kueuespec) - [ModelRegistryCommonSpec](#modelregistrycommonspec) @@ -1179,6 +1215,7 @@ ManagementSpec struct defines the component's management configuration. _Appears in:_ - [Component](#component) - [DSCDashboard](#dscdashboard) +- [DSCDataSciencePipelines](#dscdatasciencepipelines) - [DSCKueue](#dsckueue) - [DSCModelRegistry](#dscmodelregistry) - [DSCRay](#dscray) @@ -1224,30 +1261,6 @@ _Appears in:_ -## datasciencecluster.opendatahub.io/datasciencepipelines - -Package datasciencepipelines provides utility functions to config Data Science Pipelines: -Pipeline solution for end to end MLOps workflows that support the Kubeflow Pipelines SDK, Tekton and Argo Workflows. - - - -#### DataSciencePipelines - - - -DataSciencePipelines struct holds the configuration for the DataSciencePipelines component. - - - -_Appears in:_ -- [Components](#components) - -| Field | Description | Default | Validation | -| --- | --- | --- | --- | -| `Component` _[Component](#component)_ | | | | - - - ## datasciencecluster.opendatahub.io/kserve Package kserve provides utility functions to config Kserve as the Controller for serving ML models on arbitrary frameworks @@ -1411,7 +1424,7 @@ _Appears in:_ | `dashboard` _[DSCDashboard](#dscdashboard)_ | Dashboard component configuration. | | | | `workbenches` _[Workbenches](#workbenches)_ | Workbenches component configuration. | | | | `modelmeshserving` _[ModelMeshServing](#modelmeshserving)_ | ModelMeshServing component configuration.
Does not support enabled Kserve at the same time | | | -| `datasciencepipelines` _[DataSciencePipelines](#datasciencepipelines)_ | DataServicePipeline component configuration.
Require OpenShift Pipelines Operator to be installed before enable component | | | +| `datasciencepipelines` _[DSCDataSciencePipelines](#dscdatasciencepipelines)_ | DataServicePipeline component configuration.
Require OpenShift Pipelines Operator to be installed before enable component | | | | `kserve` _[Kserve](#kserve)_ | Kserve component configuration.
Require OpenShift Serverless and OpenShift Service Mesh Operators to be installed before enable component
Does not support enabled ModelMeshServing at the same time | | | | `kueue` _[DSCKueue](#dsckueue)_ | Kueue component configuration. | | | | `codeflare` _[CodeFlare](#codeflare)_ | CodeFlare component configuration.
If CodeFlare Operator has been installed in the cluster, it should be uninstalled first before enabled component. | | | diff --git a/main.go b/main.go index d6372e57fa4..72a8b8e719d 100644 --- a/main.go +++ b/main.go @@ -64,6 +64,7 @@ import ( featurev1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/features/v1" "github.com/opendatahub-io/opendatahub-operator/v2/controllers/certconfigmapgenerator" dashboardctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/dashboard" + datasciencepipelinesctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/datasciencepipelines" kueuectrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/kueue" modelregistryctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/modelregistry" rayctrl "github.com/opendatahub-io/opendatahub-operator/v2/controllers/components/ray" @@ -128,6 +129,9 @@ func initComponents(_ context.Context, p cluster.Platform) error { if err := modelregistryctrl.Init(p); err != nil { return err } + if err := datasciencepipelinesctrl.Init(p); err != nil { + multiErr = multierror.Append(multiErr, err) + } if err := trustyaictrl.Init(p); err != nil { return err @@ -432,6 +436,7 @@ func CreateComponentReconcilers(ctx context.Context, mgr manager.Manager) error setupLog.Error(err, "unable to create controller", "controller", "DashboardReconciler") return err } + if err := rayctrl.NewComponentReconciler(ctx, mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "RayReconciler") return err @@ -449,5 +454,10 @@ func CreateComponentReconcilers(ctx context.Context, mgr manager.Manager) error return err } + if err := datasciencepipelinesctrl.NewComponentReconciler(ctx, mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "DataSciencePipelinesReconciler") + return err + } + return nil } diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go index ec5490f89af..a14900aa34e 100644 --- a/pkg/upgrade/upgrade.go +++ b/pkg/upgrade/upgrade.go @@ -33,7 +33,6 @@ import ( infrav1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/infrastructure/v1" componentsold "github.com/opendatahub-io/opendatahub-operator/v2/components" "github.com/opendatahub-io/opendatahub-operator/v2/components/codeflare" - "github.com/opendatahub-io/opendatahub-operator/v2/components/datasciencepipelines" "github.com/opendatahub-io/opendatahub-operator/v2/components/kserve" "github.com/opendatahub-io/opendatahub-operator/v2/components/modelmeshserving" "github.com/opendatahub-io/opendatahub-operator/v2/components/trainingoperator" @@ -74,8 +73,8 @@ func CreateDefaultDSC(ctx context.Context, cli client.Client) error { ModelMeshServing: modelmeshserving.ModelMeshServing{ Component: componentsold.Component{ManagementState: operatorv1.Managed}, }, - DataSciencePipelines: datasciencepipelines.DataSciencePipelines{ - Component: componentsold.Component{ManagementState: operatorv1.Managed}, + DataSciencePipelines: componentsv1.DSCDataSciencePipelines{ + ManagementSpec: components.ManagementSpec{ManagementState: operatorv1.Managed}, }, Kserve: kserve.Kserve{ Component: componentsold.Component{ManagementState: operatorv1.Managed}, diff --git a/tests/e2e/controller_test.go b/tests/e2e/controller_test.go index 6e86741fd69..6bac41ce345 100644 --- a/tests/e2e/controller_test.go +++ b/tests/e2e/controller_test.go @@ -39,11 +39,12 @@ var ( scheme = runtime.NewScheme() componentsTestSuites = map[string]TestFn{ - "dashboard": dashboardTestSuite, - "ray": rayTestSuite, - "modelregistry": modelRegistryTestSuite, - "trustyai": trustyAITestSuite, - "kueue": kueueTestSuite, + "dashboard": dashboardTestSuite, + "ray": rayTestSuite, + "modelregistry": modelRegistryTestSuite, + "trustyai": trustyAITestSuite, + "kueue": kueueTestSuite, + "datasciencepipelienes": dataSciencePipelinesTestSuite, } ) diff --git a/tests/e2e/dashboard_test.go b/tests/e2e/dashboard_test.go index cde11a36859..e79cd29110e 100644 --- a/tests/e2e/dashboard_test.go +++ b/tests/e2e/dashboard_test.go @@ -127,7 +127,6 @@ func (tc *DashboardTestCtx) testOwnerReferences() error { } // Test Dashboard resources - appDeployments, err := tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).List(tc.testCtx.ctx, metav1.ListOptions{ LabelSelector: labels.ODH.Component("dashboard"), }) diff --git a/tests/e2e/datasciencepipelines_test.go b/tests/e2e/datasciencepipelines_test.go new file mode 100644 index 00000000000..85bec1b796f --- /dev/null +++ b/tests/e2e/datasciencepipelines_test.go @@ -0,0 +1,288 @@ +package e2e_test + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + "time" + + operatorv1 "github.com/openshift/api/operator/v1" + "github.com/stretchr/testify/require" + autoscalingv1 "k8s.io/api/autoscaling/v1" + k8serr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + componentsv1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/components/v1" + "github.com/opendatahub-io/opendatahub-operator/v2/pkg/metadata/labels" +) + +type DataSciencePipelinesTestCtx struct { + testCtx *testContext + testDataSciencePipelinesInstance componentsv1.DataSciencePipelines +} + +func dataSciencePipelinesTestSuite(t *testing.T) { + t.Helper() + + dspCtx := DataSciencePipelinesTestCtx{} + var err error + dspCtx.testCtx, err = NewTestContext() + require.NoError(t, err) + + testCtx := dspCtx.testCtx + + t.Run(testCtx.testDsc.Name, func(t *testing.T) { + // creation + t.Run("Creation of DataSciencePipelines CR", func(t *testing.T) { + err = dspCtx.testDSPCreation() + require.NoError(t, err, "error creating DataSciencePipelines CR") + }) + + t.Run("Validate DataSciencePipelines instance", func(t *testing.T) { + err = dspCtx.validateDataSciencePipelines() + require.NoError(t, err, "error validating DataSciencePipelines instance") + }) + + t.Run("Validate Ownerrefrences exist", func(t *testing.T) { + err = dspCtx.testOwnerReferences() + require.NoError(t, err, "error getting all DataSciencePipeline's Ownerrefrences") + }) + + t.Run("Validate DataSciencePipelines Ready", func(t *testing.T) { + err = dspCtx.validateDataSciencePipelinesReady() + require.NoError(t, err, "DataSciencePipelines instance is not Ready") + }) + + // reconcile + t.Run("Validate Controller reconcile", func(t *testing.T) { + err = dspCtx.testUpdateOnDataSciencePipelinesResources() + require.NoError(t, err, "error testing updates for DataSciencePipeline's managed resources") + }) + + // disable + t.Run("Validate Disabling DataSciencePipelines Component", func(t *testing.T) { + err = dspCtx.testUpdateDataSciencePipelinesComponentDisabled() + require.NoError(t, err, "error testing DataSciencePipelines component enabled field") + }) + }) +} + +func (tc *DataSciencePipelinesTestCtx) testDSPCreation() error { + if tc.testCtx.testDsc.Spec.Components.DataSciencePipelines.ManagementState != operatorv1.Managed { + return nil + } + + err := tc.testCtx.wait(func(ctx context.Context) (bool, error) { + existingDSPList := &componentsv1.DataSciencePipelinesList{} + + if err := tc.testCtx.customClient.List(ctx, existingDSPList); err != nil { + return false, err + } + + switch { + case len(existingDSPList.Items) == 1: + tc.testDataSciencePipelinesInstance = existingDSPList.Items[0] + return true, nil + case len(existingDSPList.Items) > 1: + return false, fmt.Errorf( + "unexpected DataSciencePipelines CR instances. Expected 1 , Found %v instance", len(existingDSPList.Items)) + default: + return false, nil + } + }) + + if err != nil { + return fmt.Errorf("unable to find Ray CR instance: %w", err) + } + + return nil +} + +func (tc *DataSciencePipelinesTestCtx) validateDataSciencePipelines() error { + // DataSciencePipeline spec should match the spec of DSP component in DSC + if !reflect.DeepEqual( + tc.testCtx.testDsc.Spec.Components.DataSciencePipelines.DataSciencePipelinesCommonSpec, + tc.testDataSciencePipelinesInstance.Spec.DataSciencePipelinesCommonSpec) { + err := fmt.Errorf("expected .spec for DataSciencePipelines %v, got %v", + tc.testCtx.testDsc.Spec.Components.DataSciencePipelines.DataSciencePipelinesCommonSpec, tc.testDataSciencePipelinesInstance.Spec.DataSciencePipelinesCommonSpec) + return err + } + return nil +} + +func (tc *DataSciencePipelinesTestCtx) testOwnerReferences() error { + if len(tc.testDataSciencePipelinesInstance.OwnerReferences) != 1 { + return errors.New("expect CR has ownerreferences set") + } + + // Test CR ownerref + if tc.testDataSciencePipelinesInstance.OwnerReferences[0].Kind != "DataScienceCluster" { + return fmt.Errorf("expected ownerreference DataScienceCluster not found. Got ownereferrence: %v", + tc.testDataSciencePipelinesInstance.OwnerReferences[0].Kind) + } + + // Test DataSciencePipelines resources + appDeployments, err := tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).List(tc.testCtx.ctx, metav1.ListOptions{ + LabelSelector: labels.ODH.Component(componentsv1.DataSciencePipelinesComponentName), + }) + if err != nil { + return fmt.Errorf("error listing component deployments %w", err) + } + + // test any one deployment for ownerreference + if len(appDeployments.Items) != 0 && appDeployments.Items[0].OwnerReferences[0].Kind != componentsv1.DataSciencePipelinesKind { + return fmt.Errorf("expected ownerreference not found. Got ownereferrence: %v", + appDeployments.Items[0].OwnerReferences) + } + + return nil +} + +// Verify DataSciencePipelines instance is in Ready phase when dsp deployments are up and running. +func (tc *DataSciencePipelinesTestCtx) validateDataSciencePipelinesReady() error { + err := wait.PollUntilContextTimeout(tc.testCtx.ctx, generalRetryInterval, componentReadyTimeout, true, func(ctx context.Context) (bool, error) { + key := types.NamespacedName{Name: tc.testDataSciencePipelinesInstance.Name} + dsp := &componentsv1.DataSciencePipelines{} + + err := tc.testCtx.customClient.Get(ctx, key, dsp) + if err != nil { + return false, err + } + return dsp.Status.Phase == readyStatus, nil + }) + + if err != nil { + return fmt.Errorf("error waiting Ready state for DataSciencePipelines %v: %w", tc.testDataSciencePipelinesInstance.Name, err) + } + + return nil +} + +func (tc *DataSciencePipelinesTestCtx) testUpdateOnDataSciencePipelinesResources() error { + appDeployments, err := tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).List(tc.testCtx.ctx, metav1.ListOptions{ + LabelSelector: labels.ComponentPartOf + "=" + tc.testDataSciencePipelinesInstance.Name, + }) + if err != nil { + return err + } + + if len(appDeployments.Items) != 1 { + return fmt.Errorf("error getting deployment for component %s", tc.testDataSciencePipelinesInstance.Name) + } + + const expectedReplica int32 = 2 // from 1 to 2 + + testDeployment := appDeployments.Items[0] + patchedReplica := &autoscalingv1.Scale{ + ObjectMeta: metav1.ObjectMeta{ + Name: testDeployment.Name, + Namespace: testDeployment.Namespace, + }, + Spec: autoscalingv1.ScaleSpec{ + Replicas: expectedReplica, + }, + Status: autoscalingv1.ScaleStatus{}, + } + + updatedDep, err := tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).UpdateScale(tc.testCtx.ctx, + testDeployment.Name, patchedReplica, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("error patching component resources : %w", err) + } + + if updatedDep.Spec.Replicas != patchedReplica.Spec.Replicas { + return fmt.Errorf("failed to patch replicas : expect to be %v but got %v", patchedReplica.Spec.Replicas, updatedDep.Spec.Replicas) + } + + // Sleep for 20 seconds to allow the operator to reconcile + // we expect it should not revert back to original value because of AllowList + time.Sleep(2 * generalRetryInterval) + reconciledDep, err := tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).Get(tc.testCtx.ctx, testDeployment.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("error getting component resource after reconcile: %w", err) + } + + if *reconciledDep.Spec.Replicas != expectedReplica { + return fmt.Errorf("failed to revert back replicas : expect to be %v but got %v", expectedReplica, *reconciledDep.Spec.Replicas) + } + + return nil +} + +func (tc *DataSciencePipelinesTestCtx) testUpdateDataSciencePipelinesComponentDisabled() error { + // Test Updating DSP to be disabled + var dspDeploymentName string + + if tc.testCtx.testDsc.Spec.Components.DataSciencePipelines.ManagementState == operatorv1.Managed { + appDeployments, err := tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).List(tc.testCtx.ctx, metav1.ListOptions{ + LabelSelector: labels.ODH.Component(componentsv1.DataSciencePipelinesComponentName), + }) + if err != nil { + return fmt.Errorf("error getting enabled component %v", componentsv1.DataSciencePipelinesComponentName) + } + + if len(appDeployments.Items) > 0 { + dspDeploymentName = appDeployments.Items[0].Name + if appDeployments.Items[0].Status.ReadyReplicas == 0 { + return fmt.Errorf("error getting enabled component: %s its deployment 'ReadyReplicas'", dspDeploymentName) + } + } + } else { + return errors.New("datasciencepipelines spec should be in 'enabled: true' state in order to perform test") + } + + // Disable component DataSciencePipelines + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // refresh DSC instance in case it was updated during the reconcile + err := tc.testCtx.customClient.Get(tc.testCtx.ctx, types.NamespacedName{Name: tc.testCtx.testDsc.Name}, tc.testCtx.testDsc) + if err != nil { + return fmt.Errorf("error getting resource %w", err) + } + + // Disable the Component + tc.testCtx.testDsc.Spec.Components.DataSciencePipelines.ManagementState = operatorv1.Removed + + // Try to update + err = tc.testCtx.customClient.Update(tc.testCtx.ctx, tc.testCtx.testDsc) + + // Return err itself here (not wrapped inside another error) + // so that RetryOnConflict can identify it correctly. + if err != nil { + return fmt.Errorf("error updating component from 'enabled: true' to 'enabled: false': %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("error after retry %w", err) + } + + if err = tc.testCtx.wait(func(ctx context.Context) (bool, error) { + // Verify ray CR is deleted + dsp := &componentsv1.DataSciencePipelines{} + err = tc.testCtx.customClient.Get(ctx, client.ObjectKey{Name: tc.testDataSciencePipelinesInstance.Name}, dsp) + return k8serr.IsNotFound(err), nil + }); err != nil { + return fmt.Errorf("component datasciencepipelines is disabled, should not get the DataSciencePipelines CR %v", tc.testDataSciencePipelinesInstance.Name) + } + + // Sleep for 20 seconds to allow the operator to reconcile + time.Sleep(2 * generalRetryInterval) + _, err = tc.testCtx.kubeClient.AppsV1().Deployments(tc.testCtx.applicationsNamespace).Get(tc.testCtx.ctx, dspDeploymentName, metav1.GetOptions{}) + if err != nil { + if k8serr.IsNotFound(err) { + return nil // correct result: should not find deployment after we disable it already + } + return fmt.Errorf("error getting component resource after reconcile: %w", err) + } + return fmt.Errorf("component %v is disabled, should not get its deployment %v from NS %v any more", + componentsv1.DataSciencePipelinesKind, + dspDeploymentName, + tc.testCtx.applicationsNamespace) +} diff --git a/tests/e2e/helper_test.go b/tests/e2e/helper_test.go index 354bd4efbcc..d9c11c8509a 100644 --- a/tests/e2e/helper_test.go +++ b/tests/e2e/helper_test.go @@ -28,7 +28,6 @@ import ( infrav1 "github.com/opendatahub-io/opendatahub-operator/v2/apis/infrastructure/v1" componentsold "github.com/opendatahub-io/opendatahub-operator/v2/components" "github.com/opendatahub-io/opendatahub-operator/v2/components/codeflare" - "github.com/opendatahub-io/opendatahub-operator/v2/components/datasciencepipelines" "github.com/opendatahub-io/opendatahub-operator/v2/components/kserve" "github.com/opendatahub-io/opendatahub-operator/v2/components/modelmeshserving" "github.com/opendatahub-io/opendatahub-operator/v2/components/trainingoperator" @@ -132,9 +131,9 @@ func setupDSCInstance(name string) *dscv1.DataScienceCluster { ManagementState: operatorv1.Removed, }, }, - DataSciencePipelines: datasciencepipelines.DataSciencePipelines{ - Component: componentsold.Component{ - ManagementState: operatorv1.Removed, + DataSciencePipelines: componentsv1.DSCDataSciencePipelines{ + ManagementSpec: components.ManagementSpec{ + ManagementState: operatorv1.Managed, }, }, Kserve: kserve.Kserve{ diff --git a/tests/e2e/odh_manager_test.go b/tests/e2e/odh_manager_test.go index f408955de11..a39a229f01f 100644 --- a/tests/e2e/odh_manager_test.go +++ b/tests/e2e/odh_manager_test.go @@ -68,4 +68,10 @@ func (tc *testContext) validateOwnedCRDs(t *testing.T) { require.NoErrorf(t, tc.validateCRD("kueues.components.opendatahub.io"), "error in validating CRD : kueues.components.opendatahub.io") }) + + t.Run("Validate DataSciencePipelines CRD", func(t *testing.T) { + t.Parallel() + require.NoErrorf(t, tc.validateCRD("datasciencepipelines.components.opendatahub.io"), + "error in validating CRD : datasciencepipelines.components.opendatahub.io") + }) }