diff --git a/Makefile b/Makefile index b2357975a45..430584e9cbd 100644 --- a/Makefile +++ b/Makefile @@ -138,9 +138,7 @@ endef .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. -# TODO: enable below when we do webhook -# $(CONTROLLER_GEN) rbac:roleName=controller-manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases - $(CONTROLLER_GEN) rbac:roleName=controller-manager-role crd:ignoreUnexportedFields=true paths="./..." output:crd:artifacts:config=config/crd/bases + $(CONTROLLER_GEN) rbac:roleName=controller-manager-role crd:ignoreUnexportedFields=true webhook paths="./..." output:crd:artifacts:config=config/crd/bases $(call fetch-external-crds,github.com/openshift/api,route/v1) $(call fetch-external-crds,github.com/openshift/api,user/v1) diff --git a/PROJECT b/PROJECT index cbb40d6a5b2..4bfc6c97792 100644 --- a/PROJECT +++ b/PROJECT @@ -21,6 +21,9 @@ resources: kind: DSCInitialization path: github.com/opendatahub-io/opendatahub-operator/v2/apis/dscinitialization/v1 version: v1 + webhooks: + validation: true + webhookVersion: v1 - api: crdVersion: v1 namespaced: false @@ -30,4 +33,7 @@ resources: kind: DataScienceCluster path: github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1 version: v1 + webhooks: + validation: true + webhookVersion: v1 version: "3" diff --git a/bundle/manifests/datasciencecluster.opendatahub.io_datascienceclusters.yaml b/bundle/manifests/datasciencecluster.opendatahub.io_datascienceclusters.yaml index 457c39ddf59..45c687adb48 100644 --- a/bundle/manifests/datasciencecluster.opendatahub.io_datascienceclusters.yaml +++ b/bundle/manifests/datasciencecluster.opendatahub.io_datascienceclusters.yaml @@ -62,12 +62,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -105,12 +105,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -149,12 +149,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -195,12 +195,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -298,12 +298,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -342,12 +342,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -385,12 +385,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -428,12 +428,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array @@ -471,12 +471,12 @@ spec: description: 'sourcePath is the subpath within contextDir where kustomize builds start. Examples include any sub-folder or path: `base`, `overlays/dev`, - `default`, `odh` etc' + `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/ + with tag/branch. e.g. https://github.com/org/repo/tarball/ type: string type: object type: array diff --git a/bundle/manifests/dscinitialization.opendatahub.io_dscinitializations.yaml b/bundle/manifests/dscinitialization.opendatahub.io_dscinitializations.yaml index 8ae3397ba85..6b9514bb1e5 100644 --- a/bundle/manifests/dscinitialization.opendatahub.io_dscinitializations.yaml +++ b/bundle/manifests/dscinitialization.opendatahub.io_dscinitializations.yaml @@ -131,6 +131,7 @@ spec: field. properties: customCABundle: + default: "" description: A custom CA bundle that will be available for all components in the Data Science Cluster(DSC). This bundle will be stored in odh-trusted-ca-bundle ConfigMap .data.odh-ca-bundle.crt . @@ -146,6 +147,7 @@ spec: pattern: ^(Managed|Unmanaged|Force|Removed)$ type: string required: + - customCABundle - managementState type: object required: diff --git a/bundle/manifests/redhat-ods-operator-webhook-service_v1_service.yaml b/bundle/manifests/redhat-ods-operator-webhook-service_v1_service.yaml new file mode 100644 index 00000000000..218c45fefcc --- /dev/null +++ b/bundle/manifests/redhat-ods-operator-webhook-service_v1_service.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Service +metadata: + annotations: + service.beta.openshift.io/inject-cabundle: "true" + service.beta.openshift.io/serving-cert-secret-name: redhat-ods-operator-controller-webhook-cert + creationTimestamp: null + labels: + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: rhods-operator + app.kubernetes.io/instance: webhook-service + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/name: service + app.kubernetes.io/part-of: rhods-operator + name: redhat-ods-operator-webhook-service +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager +status: + loadBalancer: {} diff --git a/bundle/manifests/rhods-operator.clusterserviceversion.yaml b/bundle/manifests/rhods-operator.clusterserviceversion.yaml index 9417d9431be..4817d94ac40 100644 --- a/bundle/manifests/rhods-operator.clusterserviceversion.yaml +++ b/bundle/manifests/rhods-operator.clusterserviceversion.yaml @@ -106,9 +106,9 @@ metadata: capabilities: Full Lifecycle categories: AI/Machine Learning, Big Data certified: "False" - containerImage: quay.io/opendatahub/opendatahub-operator:v2.4.0 + containerImage: quay.io/opendatahub/opendatahub-operator:v2.7.0 createdAt: "2023-8-23T00:00:00Z" - olm.skipRange: '>=1.0.0 <2.0.0' + olm.skipRange: '>=1.0.0 <2.7.0' operatorframework.io/initialization-resource: |- { "apiVersion": "datasciencecluster.opendatahub.io/v1", @@ -157,7 +157,7 @@ metadata: }, "workbenches": { "managementState": "Managed" - } + }, } } } @@ -165,7 +165,7 @@ metadata: operators.operatorframework.io/internal-objects: '[dscinitialization.opendatahub.io]' operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/red-hat-data-services/rhods-operator - name: rhods-operator.v2.4.0 + name: rhods-operator.v2.8.0 namespace: placeholder spec: apiservicedefinitions: {} @@ -218,28 +218,7 @@ spec: - kind: FeatureTracker name: featuretrackers.features.opendatahub.io version: v1 - description: |- - Red Hat OpenShift AI is a complete platform for the entire lifecycle of your AI/ML projects. - - When using Red Hat OpenShift AI, your users will find all the tools they would expect from a modern AI/ML platform in an interface that is intuitive, requires no local install, and is backed by the power of your OpenShift cluster. - - Your Data Scientists will feel right at home with quick and simple access to the Notebook interface they are used to. They can leverage the default Notebook Images (Including PyTorch, tensorflow, and CUDA), or add custom ones. Your MLOps engineers will be able to leverage Data Science Pipelines to easily parallelize and/or schedule the required workloads. They can then quickly serve, monitor, and update the created AI/ML models. They can do that by either using the provided out-of-the-box OpenVino Server Model Runtime or by adding their own custom serving runtime instead. These activities are tied together with the concept of Data Science Projects, simplifying both organization and collaboration. - - But beyond the individual features, one of the key aspects of this platform is its flexibility. Not only can you augment it with your own Customer Workbench Image and Custom Model Serving Runtime Images, but you will also have a consistent experience across any infrastructure footprint. Be it in the public cloud, private cloud, on-premises, and even in disconnected clusters. Red Hat OpenShift AI can be installed on any supported OpenShift. It can scale out or in depending on the size of your team and its computing requirements. - - Finally, thanks to the operator-driven deployment and updates, the administrative load of the platform is very light, leaving everyone more time to focus on the work that makes a difference. - - ### Components - * Dashboard - * Curated Workbench Images (incl CUDA, PyTorch, Tensorflow, VScode) - * Ability to add Custom Images - * Ability to leverage accelerators (such as NVIDIA GPU) - * Data Science Pipelines. (including Elyra notebook interface, and based on standard OpenShift Pipelines) - * Model Serving using ModelMesh and Kserve. - * Ability to use other runtimes for serving - * Model Monitoring - * Distributed workloads (KubeRay, CodeFlare, Kueue) - * XAI explanations of predictive models (TrustyAI) + description: This will be replaced by Kustomize displayName: Red Hat OpenShift AI icon: - base64data: PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI3LjQuMSwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxvZ29zIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgODM1LjkgMjQ0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA4MzUuOSAyNDQ7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojRkZGRkZGO30KCS5zdDF7ZmlsbDojRUUwMDAwO30KPC9zdHlsZT4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNMzAxLjMsMTgzLjFjMCw1LjItMSwxMC4xLTMsMTQuN2MtMiw0LjYtNC43LDguNi04LjEsMTJjLTMuNCwzLjQtNy40LDYtMTIuMSw4Yy00LjYsMS45LTkuNiwyLjktMTQuOSwyLjkKCQkJYy01LjMsMC0xMC4yLTEtMTQuOS0yLjljLTQuNi0xLjktOC43LTQuNi0xMi04Yy0zLjQtMy40LTYtNy4zLTgtMTJjLTItNC42LTMtOS41LTMtMTQuN2MwLTUuMiwxLTEwLjEsMy0xNC43YzItNC42LDQuNi04LjYsOC0xMgoJCQljMy40LTMuNCw3LjQtNiwxMi04YzQuNi0xLjksOS42LTIuOSwxNC45LTIuOWM1LjMsMCwxMC4yLDEsMTQuOSwyLjljNC42LDEuOSw4LjcsNC42LDEyLjEsOGMzLjQsMy40LDYuMSw3LjQsOC4xLDEyCgkJCUMzMDAuNCwxNzMsMzAxLjMsMTc3LjksMzAxLjMsMTgzLjF6IE0yOTAuMiwxODMuMWMwLTMuOS0wLjctNy41LTIuMS0xMC45Yy0xLjQtMy40LTMuMy02LjMtNS43LTguN2MtMi40LTIuNS01LjItNC40LTguNS01LjgKCQkJYy0zLjMtMS40LTYuOC0yLjEtMTAuNi0yLjFzLTcuMiwwLjctMTAuNSwyLjFjLTMuMywxLjQtNi4xLDMuMy04LjUsNS44Yy0yLjQsMi41LTQuMyw1LjQtNS43LDguN2MtMS40LDMuNC0yLjEsNy0yLjEsMTAuOQoJCQljMCwzLjksMC43LDcuNSwyLjEsMTAuOWMxLjQsMy40LDMuMyw2LjMsNS43LDguN2MyLjQsMi40LDUuMiw0LjQsOC41LDUuOGMzLjMsMS40LDYuOCwyLjEsMTAuNSwyLjFzNy4zLTAuNywxMC42LTIuMQoJCQljMy4zLTEuNCw2LjEtMy4zLDguNS01LjhjMi40LTIuNCw0LjMtNS4zLDUuNy04LjdDMjg5LjUsMTkwLjYsMjkwLjIsMTg3LDI5MC4yLDE4My4xeiIvPgoJCTxwYXRoIGQ9Ik0zMTEuNiwyNDEuMXYtNzQuNWgxMC4zdjVjMi4yLTEuOSw0LjctMy4zLDcuNS00LjNjMi44LTEsNS43LTEuNSw4LjctMS41YzMuNywwLDcuMiwwLjcsMTAuNSwyLjEKCQkJYzMuMywxLjQsNi4xLDMuNCw4LjUsNS44YzIuNCwyLjUsNC4zLDUuNCw1LjcsOC43YzEuNCwzLjMsMi4xLDYuOSwyLjEsMTAuNmMwLDMuOC0wLjcsNy40LTIuMSwxMC43Yy0xLjQsMy4zLTMuMyw2LjItNS43LDguNwoJCQljLTIuNCwyLjUtNS4zLDQuNC04LjYsNS44Yy0zLjMsMS40LTYuOSwyLjEtMTAuNywyLjFjLTMsMC01LjgtMC41LTguNS0xLjRjLTIuNy0wLjktNS4xLTIuMi03LjMtMy44djI1LjlIMzExLjZ6IE0zMzYuNywxNzQuOAoJCQljLTMuMSwwLTUuOCwwLjYtOC4zLDEuN2MtMi41LDEuMS00LjYsMi42LTYuMyw0LjZ2MjQuMWMxLjcsMS45LDMuOCwzLjQsNi4zLDQuNWMyLjYsMS4xLDUuMywxLjcsOC4zLDEuN2M1LjEsMCw5LjQtMS44LDEyLjgtNS4zCgkJCWMzLjQtMy41LDUuMS03LjgsNS4xLTEyLjljMC01LjItMS44LTkuNi01LjMtMTMuMUMzNDUuOSwxNzYuNiwzNDEuNywxNzQuOCwzMzYuNywxNzQuOHoiLz4KCQk8cGF0aCBkPSJNMzcyLjQsMTkzYzAtMy43LDAuNy03LjMsMi0xMC42YzEuNC0zLjMsMy4yLTYuMiw1LjYtOC43YzIuNC0yLjUsNS4yLTQuNCw4LjQtNS44YzMuMi0xLjQsNi43LTIuMSwxMC41LTIuMQoJCQljMy42LDAsNywwLjcsMTAuMSwyLjFjMy4yLDEuNCw1LjksMy40LDguMSw1LjhjMi4zLDIuNSw0LDUuNCw1LjQsOC44YzEuMywzLjQsMiw3LDIsMTAuOXYzaC00MS44YzAuNyw0LjQsMi43LDgsNiwxMC45CgkJCWMzLjMsMi45LDcuMyw0LjMsMTEuOSw0LjNjMi42LDAsNS0wLjQsNy40LTEuMmMyLjQtMC44LDQuNC0yLDYtMy40bDYuNyw2LjZjLTMuMSwyLjQtNi4zLDQuMi05LjYsNS4zYy0zLjMsMS4xLTYuOSwxLjctMTAuOSwxLjcKCQkJYy0zLjksMC03LjUtMC43LTEwLjktMi4xYy0zLjQtMS40LTYuMy0zLjMtOC44LTUuOGMtMi41LTIuNC00LjUtNS4zLTUuOS04LjdDMzczLjEsMjAwLjUsMzcyLjQsMTk2LjksMzcyLjQsMTkzeiBNMzk4LjcsMTc0LjUKCQkJYy00LDAtNy41LDEuMy0xMC40LDRjLTIuOSwyLjYtNC44LDYtNS41LDEwLjJoMzEuNGMtMC43LTQtMi41LTcuNC01LjQtMTAuMUM0MDUuOSwxNzUuOSw0MDIuNSwxNzQuNSwzOTguNywxNzQuNXoiLz4KCQk8cGF0aCBkPSJNNDM0LjMsMjE5LjV2LTUyLjloMTAuNHY1LjNjMi4xLTIuMSw0LjUtMy43LDcuMS00LjdjMi43LTEuMSw1LjYtMS42LDguOC0xLjZjNiwwLDExLDEuOSwxNC44LDUuOAoJCQljMy44LDMuOSw1LjgsOC44LDUuOCwxNC45djMzLjNoLTEwLjNWMTg4YzAtNC4xLTEuMi03LjMtMy41LTkuOGMtMi40LTIuNC01LjYtMy42LTkuNy0zLjZjLTIuOCwwLTUuMywwLjYtNy41LDEuOAoJCQljLTIuMiwxLjItNC4xLDIuOS01LjUsNS4xdjM4LjFINDM0LjN6Ii8+CgkJPHBhdGggZD0iTTQ4OCwyMDcuNWw2LjctNy43YzMuOSwzLjgsNy45LDYuNywxMiw4LjZjNC4xLDEuOSw4LjUsMi45LDEzLjEsMi45YzUuMywwLDkuNS0xLjEsMTIuOC0zLjRjMy4zLTIuMyw0LjktNS4yLDQuOS04LjcKCQkJYzAtMy4yLTEuMS01LjctMy4zLTcuNGMtMi4yLTEuOC02LTMuMS0xMS40LTRsLTEyLjItMmMtNi43LTEuMS0xMS43LTMuMy0xNS02LjRjLTMuMy0zLjItNC45LTcuMy00LjktMTIuNQoJCQljMC02LjIsMi40LTExLjMsNy4zLTE1LjFjNC45LTMuOCwxMS40LTUuOCwxOS41LTUuOGM1LjEsMCwxMC4zLDAuOCwxNS40LDIuNWM1LjEsMS43LDkuOCw0LjEsMTMuOSw3LjNsLTYsOC4zCgkJCWMtNC0zLTcuOS01LjItMTEuOS02LjdjLTQtMS41LTgtMi4yLTExLjktMi4yYy00LjcsMC04LjUsMS0xMS40LDNjLTIuOSwyLTQuNCw0LjYtNC40LDcuOGMwLDIuOSwxLDUuMiwzLDYuOAoJCQljMiwxLjYsNS4zLDIuOCwxMCwzLjVsMTEuOCwxLjljNy43LDEuMiwxMy4zLDMuNSwxNyw2LjhjMy42LDMuMyw1LjQsNy43LDUuNCwxMy40YzAsMy4zLTAuNyw2LjQtMi4xLDkuMQoJCQljLTEuNCwyLjctMy4zLDUuMS01LjksNy4xYy0yLjUsMi01LjYsMy41LTkuMiw0LjZjLTMuNiwxLjEtNy42LDEuNi0xMS45LDEuNmMtNS44LDAtMTEuNC0xLjEtMTYuOC0zLjQKCQkJQzQ5NywyMTQuOSw0OTIuMiwyMTEuNiw0ODgsMjA3LjV6Ii8+CgkJPHBhdGggZD0iTTU2Ny40LDE0NC41djc1LjFINTU3di03Mi44TDU2Ny40LDE0NC41eiBNNTU3LDIxOS41di01Mi45aDEwLjR2NS4zYzIuMS0yLjEsNC41LTMuNyw3LjEtNC43YzIuNy0xLjEsNS42LTEuNiw4LjgtMS42CgkJCWM2LDAsMTEsMS45LDE0LjgsNS44YzMuOCwzLjksNS44LDguOCw1LjgsMTQuOXYzMy4zaC0xMC4zVjE4OGMwLTQuMS0xLjItNy4zLTMuNS05LjhjLTIuNC0yLjQtNS42LTMuNi05LjctMy42CgkJCWMtMi44LDAtNS4zLDAuNi03LjUsMS44Yy0yLjIsMS4yLTQuMSwyLjktNS41LDUuMXYzOC4xSDU1N3oiLz4KCQk8cGF0aCBkPSJNNjIxLjEsMTU4LjFjLTEuNywwLTMuMi0wLjYtNC41LTEuOWMtMS4zLTEuMy0xLjktMi44LTEuOS00LjVjMC0xLjcsMC42LTMuMiwxLjktNC41YzEuMy0xLjMsMi44LTEuOSw0LjUtMS45CgkJCWMxLjcsMCwzLjIsMC42LDQuNSwxLjljMS4yLDEuMywxLjksMi44LDEuOSw0LjVjMCwxLjctMC42LDMuMi0xLjksNC41QzYyNC40LDE1Ny40LDYyMi45LDE1OC4xLDYyMS4xLDE1OC4xeiBNNjI2LjMsMTY2LjZ2NTIuOQoJCQloLTEwLjR2LTUyLjlINjI2LjN6Ii8+CgkJPHBhdGggZD0iTTYzNC4xLDE2Ni42aDEyLjN2LThjMC01LjMsMS41LTkuNSw0LjYtMTIuNWMzLTMsNy41LTQuNSwxMy40LTQuNWMxLjMsMCwyLjYsMC4xLDMuOSwwLjNjMS4zLDAuMiwyLjUsMC40LDMuNiwwLjd2OQoJCQljLTEuMi0wLjMtMi4zLTAuNi0zLjItMC43Yy0xLTAuMS0yLjEtMC4yLTMuMy0wLjJjLTIuOSwwLTUuMSwwLjctNi41LDJjLTEuNCwxLjMtMi4xLDMuNC0yLjEsNi4ydjcuOGgxNS4ydjguN2gtMTUuMnY0NC4yaC0xMC4zCgkJCXYtNDQuMmgtMTIuM1YxNjYuNnoiLz4KCQk8cGF0aCBkPSJNNjg3LjcsMjA2LjR2LTMxLjFoLTExLjJ2LTguN2gxMS4ydi0xMy41bDEwLjMtMi41djE2aDE1LjZ2OC43SDY5OFYyMDRjMCwyLjcsMC42LDQuNiwxLjgsNS43YzEuMiwxLjEsMy4yLDEuNyw2LDEuNwoJCQljMS41LDAsMi44LTAuMSw0LTAuM2MxLjEtMC4yLDIuMy0wLjUsMy42LTF2OC43Yy0xLjUsMC41LTMuMSwwLjktNC45LDEuMWMtMS44LDAuMy0zLjUsMC40LTUsMC40Yy01LjEsMC05LTEuMi0xMS44LTMuNgoJCQlTNjg3LjcsMjExLDY4Ny43LDIwNi40eiIvPgoJCTxwYXRoIGQ9Ik03MzcsMjE5LjVsMzAuMi03Mi44aDEyLjhsMjkuNyw3Mi44aC0xMS45bC04LjQtMjEuM2gtMzIuNmwtOC41LDIxLjNINzM3eiBNNzYwLjUsMTg5LjJoMjUuNGwtMTIuNy0zMS45TDc2MC41LDE4OS4yeiIvPgoJCTxwYXRoIGQ9Ik04MTcuMiwyMTkuNXYtNzIuOGgxMC45djcyLjhIODE3LjJ6Ii8+Cgk8L2c+Cgk8Zz4KCQk8Zz4KCQkJPHBhdGggY2xhc3M9InN0MSIgZD0iTTEyOSw4NWMxMi41LDAsMzAuNi0yLjYsMzAuNi0xNy41YzAtMS4yLDAtMi4zLTAuMy0zLjRsLTcuNC0zMi40Yy0xLjctNy4xLTMuMi0xMC4zLTE1LjctMTYuNgoJCQkJQzEyNi40LDEwLjIsMTA1LjMsMiw5OSwyYy01LjgsMC03LjUsNy41LTE0LjQsNy41Yy02LjcsMC0xMS42LTUuNi0xNy45LTUuNmMtNiwwLTkuOSw0LjEtMTIuOSwxMi41YzAsMC04LjQsMjMuNy05LjUsMjcuMgoJCQkJQzQ0LDQ0LjMsNDQsNDUsNDQsNDUuNUM0NCw1NC44LDgwLjMsODUsMTI5LDg1IE0xNjEuNSw3My42YzEuNyw4LjIsMS43LDkuMSwxLjcsMTAuMWMwLDE0LTE1LjcsMjEuOC0zNi40LDIxLjgKCQkJCUM4MCwxMDUuNSwzOS4xLDc4LjEsMzkuMSw2MGMwLTIuOCwwLjYtNS40LDEuNS03LjNDMjMuOCw1My41LDIsNTYuNSwyLDc1LjdDMiwxMDcuMiw3Ni42LDE0NiwxMzUuNywxNDYKCQkJCWM0NS4zLDAsNTYuNy0yMC41LDU2LjctMzYuNkMxOTIuMyw5Ni42LDE4MS40LDgyLjIsMTYxLjUsNzMuNiIvPgoJCQk8cGF0aCBkPSJNMTYxLjUsNzMuNmMxLjcsOC4yLDEuNyw5LjEsMS43LDEwLjFjMCwxNC0xNS43LDIxLjgtMzYuNCwyMS44QzgwLDEwNS41LDM5LjEsNzguMSwzOS4xLDYwYzAtMi44LDAuNi01LjQsMS41LTcuMwoJCQkJbDMuNy05LjFDNDQsNDQuMyw0NCw0NSw0NCw0NS41QzQ0LDU0LjgsODAuMyw4NSwxMjksODVjMTIuNSwwLDMwLjYtMi42LDMwLjYtMTcuNWMwLTEuMiwwLTIuMy0wLjMtMy40TDE2MS41LDczLjZ6Ii8+CgkJPC9nPgoJCTxwYXRoIGQ9Ik01ODEuMiw5NC4zYzAsMTEuOSw3LjIsMTcuNywyMC4yLDE3LjdjMy4yLDAsOC42LTAuNywxMS45LTEuN1Y5Ni41Yy0yLjgsMC44LTQuOSwxLjItNy43LDEuMmMtNS40LDAtNy40LTEuNy03LjQtNi43CgkJCVY2OS44aDE1LjZWNTUuNmgtMTUuNnYtMThsLTE3LDMuN3YxNC4zSDU3MHYxNC4yaDExLjNWOTQuM3ogTTUyOC4zLDk0LjZjMC0zLjcsMy43LTUuNSw5LjMtNS41YzMuNywwLDcsMC41LDEwLjEsMS4zdjcuMgoJCQljLTMuMiwxLjgtNi44LDIuNi0xMC42LDIuNkM1MzEuNiwxMDAuMiw1MjguMyw5OC4xLDUyOC4zLDk0LjYgTTUzMy41LDExMi4yYzYsMCwxMC44LTEuMywxNS40LTQuM3YzLjRoMTYuOFY3NS42CgkJCWMwLTEzLjYtOS4xLTIxLTI0LjQtMjFjLTguNSwwLTE2LjksMi0yNiw2LjFsNi4xLDEyLjVjNi41LTIuNywxMi00LjQsMTYuOC00LjRjNywwLDEwLjYsMi43LDEwLjYsOC4zdjIuNwoJCQljLTQtMS4xLTguMi0xLjYtMTIuNi0xLjZjLTE0LjMsMC0yMi45LDYtMjIuOSwxNi43QzUxMy4zLDEwNC43LDUyMS4xLDExMi4yLDUzMy41LDExMi4yIE00NDEuMSwxMTEuMmgxOC4xVjgyLjRoMzAuM3YyOC44aDE4LjEKCQkJVjM3LjZoLTE4LjF2MjguM2gtMzAuM1YzNy42aC0xOC4xVjExMS4yeiBNMzcyLjEsODMuNGMwLTgsNi4zLTE0LjEsMTQuNi0xNC4xYzQuNiwwLDguOCwxLjYsMTEuOCw0LjNWOTNjLTMsMi45LTcsNC40LTExLjgsNC40CgkJCUMzNzguNSw5Ny41LDM3Mi4xLDkxLjQsMzcyLjEsODMuNCBNMzk4LjcsMTExLjJoMTYuOFYzMy45bC0xNywzLjd2MjAuOWMtNC4yLTIuNC05LTMuNy0xNC4yLTMuN2MtMTYuMiwwLTI4LjksMTIuNS0yOC45LDI4LjUKCQkJYzAsMTYsMTIuNSwyOC42LDI4LjQsMjguNmM1LjUsMCwxMC42LTEuNywxNC45LTQuOFYxMTEuMnogTTMyMS41LDY4LjVjNS40LDAsOS45LDMuNSwxMS43LDguOEgzMTAKCQkJQzMxMS43LDcxLjgsMzE1LjksNjguNSwzMjEuNSw2OC41IE0yOTIuOCw4My41YzAsMTYuMiwxMy4zLDI4LjgsMzAuMywyOC44YzkuNCwwLDE2LjItMi41LDIzLjItOC40bC0xMS4zLTEwCgkJCWMtMi42LDIuNy02LjUsNC4yLTExLjEsNC4yYy02LjMsMC0xMS41LTMuNS0xMy43LTguOGgzOS42Vjg1YzAtMTcuNy0xMS45LTMwLjQtMjguMS0zMC40QzMwNS42LDU0LjcsMjkyLjgsNjcuMywyOTIuOCw4My41CgkJCSBNMjYzLjUsNTMuMWM2LDAsOS40LDMuOCw5LjQsOC4zcy0zLjQsOC4zLTkuNCw4LjNoLTE3LjlWNTMuMUgyNjMuNXogTTIyNy41LDExMS4yaDE4LjFWODQuNGgxMy44bDEzLjksMjYuOGgyMC4ybC0xNi4yLTI5LjQKCQkJYzguNy0zLjgsMTMuOS0xMS43LDEzLjktMjAuN2MwLTEzLjMtMTAuNC0yMy41LTI2LTIzLjVoLTM3LjdWMTExLjJ6Ii8+Cgk8L2c+CjwvZz4KPC9zdmc+Cg== @@ -1744,18 +1723,6 @@ spec: - patch - update - watch - - apiGroups: - - authentication.k8s.io - resources: - - tokenreviews - verbs: - - create - - apiGroups: - - authorization.k8s.io - resources: - - subjectaccessreviews - verbs: - - create serviceAccountName: redhat-ods-operator-controller-manager deployments: - label: @@ -1790,6 +1757,10 @@ spec: initialDelaySeconds: 15 periodSeconds: 20 name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP readinessProbe: httpGet: path: /readyz @@ -1803,44 +1774,24 @@ spec: requests: cpu: 500m memory: 256Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true securityContext: runAsNonRoot: true serviceAccountName: redhat-ods-operator-controller-manager terminationGracePeriodSeconds: 10 - permissions: - - rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - coordination.k8s.io - resources: - - leases - verbs: - - get - - list - - watch - - create - - update - - patch - - delete - - apiGroups: - - "" - resources: - - events - verbs: - - create - - patch - serviceAccountName: redhat-ods-operator-controller-manager + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: redhat-ods-operator-controller-webhook-cert strategy: deployment installModes: - supported: false @@ -1861,11 +1812,34 @@ spec: - training - kserve - distributed-workloads - - trustyai links: - name: Red Hat OpenShift AI url: https://www.redhat.com/en/technologies/cloud-computing/openshift/openshift-ai minKubeVersion: 1.22.0 provider: name: Red Hat - version: 2.4.0 + replaces: rhods-operator.v2.6.0 + version: 2.8.0 + webhookdefinitions: + - admissionReviewVersions: + - v1 + containerPort: 443 + deploymentName: redhat-ods-operator-controller-manager + failurePolicy: Fail + generateName: operator.opendatahub.io + rules: + - apiGroups: + - datasciencecluster.opendatahub.io + - dscinitialization.opendatahub.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - datascienceclusters + - dscinitializations + sideEffects: None + targetPort: 9443 + type: ValidatingAdmissionWebhook + webhookPath: /validate-opendatahub-io-v1 diff --git a/config/crd/bases/dscinitialization.opendatahub.io_dscinitializations.yaml b/config/crd/bases/dscinitialization.opendatahub.io_dscinitializations.yaml index 48621d88cc7..a821a61ff36 100644 --- a/config/crd/bases/dscinitialization.opendatahub.io_dscinitializations.yaml +++ b/config/crd/bases/dscinitialization.opendatahub.io_dscinitializations.yaml @@ -132,6 +132,7 @@ spec: field. properties: customCABundle: + default: "" description: A custom CA bundle that will be available for all components in the Data Science Cluster(DSC). This bundle will be stored in odh-trusted-ca-bundle ConfigMap .data.odh-ca-bundle.crt . @@ -147,6 +148,7 @@ spec: pattern: ^(Managed|Unmanaged|Force|Removed)$ type: string required: + - customCABundle - managementState type: object required: diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index fc1b6912725..1a7fe00e8c9 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -21,7 +21,7 @@ resources: - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -#- ../webhook +- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. @@ -37,6 +37,7 @@ resources: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml +# Moved below to patches #- manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. @@ -76,3 +77,5 @@ resources: patches: - path: manager_auth_proxy_patch.yaml +# [WEBHOOK] +- path: manager_webhook_patch.yaml diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 00000000000..d76a0133d0d --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: redhat-ods-operator-controller-webhook-cert diff --git a/config/manifests/bases/rhods-operator.clusterserviceversion.yaml b/config/manifests/bases/rhods-operator.clusterserviceversion.yaml index 6da19c778bf..18f7ec09815 100644 --- a/config/manifests/bases/rhods-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/rhods-operator.clusterserviceversion.yaml @@ -97,6 +97,12 @@ spec: e.g. it provides unified authentication giving a Single Sign On experience. displayName: Service Mesh path: serviceMesh + - description: When set to `Managed`, adds odh-trusted-ca-bundle Configmap to + all namespaces that includes cluster-wide Trusted CA Bundle in .data["ca-bundle.crt"]. + Additionally, this fields allows admins to add custom CA bundles to the + configmap using the .CustomCABundle field. + displayName: Trusted CABundle + path: trustedCABundle - description: Internal development useful field to test customizations. This is not recommended to be used in production environment. displayName: Dev Flags diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 00000000000..8428859f524 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,9 @@ +resources: +- manifests.yaml +- service.yaml + +commonAnnotations: + service.beta.openshift.io/inject-cabundle: "true" + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 00000000000..25e21e3c963 --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,25 @@ +# the following config is for teaching kustomize where to look at when substituting vars. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true + +varReference: +- path: metadata/annotations diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 00000000000..5595314bffb --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + creationTimestamp: null + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-opendatahub-io-v1 + failurePolicy: Fail + name: operator.opendatahub.io + rules: + - apiGroups: + - datasciencecluster.opendatahub.io + - dscinitialization.opendatahub.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - datascienceclusters + - dscinitializations + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 00000000000..72c652ddaae --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,22 @@ + +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + app.kubernetes.io/instance: webhook-service + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: rhods-operator + app.kubernetes.io/part-of: rhods-operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system + annotations: + service.beta.openshift.io/serving-cert-secret-name: redhat-ods-operator-controller-webhook-cert +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go b/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go index 17732592d39..2b9c94dc182 100644 --- a/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go +++ b/controllers/certconfigmapgenerator/certconfigmapgenerator_controller.go @@ -49,7 +49,7 @@ func (r *CertConfigmapGeneratorReconciler) SetupWithManager(mgr ctrl.Manager) er // ca bundle in every new namespace created. func (r *CertConfigmapGeneratorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // Request includes namespace that is newly created or where odh-trusted-ca-bundle configmap is updated. - r.Log.Info("Reconciling certConfigMapGenerator.", " CertConfigMapGenerator Request.Namespace", req.NamespacedName) + r.Log.Info("Reconciling certConfigMapGenerator.", "CertConfigMapGenerator Request.Namespace", req.NamespacedName) // Get namespace instance userNamespace := &corev1.Namespace{} err := r.Client.Get(ctx, client.ObjectKey{Name: req.Namespace}, userNamespace) @@ -71,9 +71,6 @@ func (r *CertConfigmapGeneratorReconciler) Reconcile(ctx context.Context, req ct return ctrl.Result{}, nil case 1: dsciInstance = &dsciInstances.Items[0] - default: - message := "only one instance of DSCInitialization object is allowed" - return ctrl.Result{}, errors.New(message) } if dsciInstance.Spec.TrustedCABundle.ManagementState != operatorv1.Managed { diff --git a/controllers/datasciencecluster/datasciencecluster_controller.go b/controllers/datasciencecluster/datasciencecluster_controller.go index 3010faa80f8..72e119636f4 100644 --- a/controllers/datasciencecluster/datasciencecluster_controller.go +++ b/controllers/datasciencecluster/datasciencecluster_controller.go @@ -82,7 +82,7 @@ const ( // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //nolint:gocyclo,maintidx +func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { //nolint:gocyclo r.Log.Info("Reconciling DataScienceCluster resources", "Request.Name", req.Name) instances := &dsc.DataScienceClusterList{} @@ -138,19 +138,6 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R return reconcile.Result{Requeue: true}, nil } - if len(instances.Items) > 1 { - message := fmt.Sprintf("only one instance of DataScienceCluster object is allowed. Update existing instance %s", req.Name) - err := errors.New(message) - _ = r.reportError(err, instance, message) - - _, _ = r.updateStatus(ctx, instance, func(saved *dsc.DataScienceCluster) { - status.SetErrorCondition(&saved.Status.Conditions, status.DuplicateDataScienceCluster, message) - saved.Status.Phase = status.PhaseError - }) - - return ctrl.Result{}, err - } - // Verify a valid DSCInitialization instance is created dsciInstances := &dsci.DSCInitializationList{} err = r.Client.List(ctx, dsciInstances) @@ -161,7 +148,7 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R } // Update phase to error state if DataScienceCluster is created without valid DSCInitialization - switch len(dsciInstances.Items) { + switch len(dsciInstances.Items) { // only handle number as 0 or 1, others won't be existed since webhook block creation case 0: reason := status.ReconcileFailed message := "Failed to get a valid DSCInitialization instance" @@ -177,13 +164,6 @@ func (r *DataScienceClusterReconciler) Reconcile(ctx context.Context, req ctrl.R case 1: dscInitializationSpec := dsciInstances.Items[0].Spec dscInitializationSpec.DeepCopyInto(r.DataScienceCluster.DSCISpec) - default: - message := "only one instance of DSCInitialization object is allowed" - _, _ = r.updateStatus(ctx, instance, func(saved *dsc.DataScienceCluster) { - status.SetErrorCondition(&saved.Status.Conditions, status.DuplicateDSCInitialization, message) - saved.Status.Phase = status.PhaseError - }) - return ctrl.Result{}, errors.New(message) } if instance.ObjectMeta.DeletionTimestamp.IsZero() { diff --git a/controllers/dscinitialization/dscinitialization_controller.go b/controllers/dscinitialization/dscinitialization_controller.go index be34f146816..cd076d5cee2 100644 --- a/controllers/dscinitialization/dscinitialization_controller.go +++ b/controllers/dscinitialization/dscinitialization_controller.go @@ -19,8 +19,6 @@ package dscinitialization import ( "context" - "errors" - "fmt" "path/filepath" "reflect" @@ -89,33 +87,11 @@ func (r *DSCInitializationReconciler) Reconcile(ctx context.Context, req ctrl.Re } var instance *dsciv1.DSCInitialization - switch { + switch { // only handle number as 0 or 1, others won't be existed since webhook block creation case len(instances.Items) == 0: return ctrl.Result{}, nil case len(instances.Items) == 1: instance = &instances.Items[0] - case len(instances.Items) > 1: - // find out the one by created timestamp and use it as the default one - earliestDSCI := &instances.Items[0] - for _, instance := range instances.Items { - currentDSCI := instance - if currentDSCI.CreationTimestamp.Before(&earliestDSCI.CreationTimestamp) { - earliestDSCI = ¤tDSCI - } - } - message := fmt.Sprintf("only one instance of DSCInitialization object is allowed. Please delete other instances than %s", earliestDSCI.Name) - // update all instances Message and Status - for _, deletionInstance := range instances.Items { - deletionInstance := deletionInstance - if deletionInstance.Name != earliestDSCI.Name { - _, _ = r.updateStatus(ctx, &deletionInstance, func(saved *dsciv1.DSCInitialization) { - status.SetErrorCondition(&saved.Status.Conditions, status.DuplicateDSCInitialization, message) - saved.Status.Phase = status.PhaseError - }) - } - } - - return ctrl.Result{}, errors.New(message) } if instance.ObjectMeta.DeletionTimestamp.IsZero() { diff --git a/controllers/dscinitialization/dscinitialization_test.go b/controllers/dscinitialization/dscinitialization_test.go index 6a726a1998b..17b510c4d2c 100644 --- a/controllers/dscinitialization/dscinitialization_test.go +++ b/controllers/dscinitialization/dscinitialization_test.go @@ -20,6 +20,7 @@ import ( const ( workingNamespace = "test-operator-ns" + applicationName = "default-dsci" applicationNamespace = "test-application-ns" configmapName = "odh-common-config" monitoringNamespace = "test-monitoring-ns" @@ -29,10 +30,10 @@ const ( var _ = Describe("DataScienceCluster initialization", func() { Context("Creation of related resources", func() { // must be default as instance name, or it will break - const applicationName = "default-dsci" + BeforeEach(func() { // when - desiredDsci := createDSCI(applicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace) + desiredDsci := createDSCI(operatorv1.Managed, operatorv1.Managed, monitoringNamespace) Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) foundDsci := &dsci.DSCInitialization{} Eventually(dscInitializationIsReady(applicationName, workingNamespace, foundDsci)). @@ -119,7 +120,7 @@ var _ = Describe("DataScienceCluster initialization", func() { const applicationName = "default-dsci" It("Should not create monitoring namespace if monitoring is disabled", func() { // when - desiredDsci := createDSCI(applicationName, operatorv1.Removed, operatorv1.Managed, monitoringNamespace2) + desiredDsci := createDSCI(operatorv1.Removed, operatorv1.Managed, monitoringNamespace2) Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) foundDsci := &dsci.DSCInitialization{} Eventually(dscInitializationIsReady(applicationName, workingNamespace, foundDsci)). @@ -135,7 +136,7 @@ var _ = Describe("DataScienceCluster initialization", func() { }) It("Should create default monitoring namespace if monitoring enabled", func() { // when - desiredDsci := createDSCI(applicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace2) + desiredDsci := createDSCI(operatorv1.Managed, operatorv1.Managed, monitoringNamespace2) Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) foundDsci := &dsci.DSCInitialization{} Eventually(dscInitializationIsReady(applicationName, workingNamespace, foundDsci)). @@ -154,22 +155,6 @@ var _ = Describe("DataScienceCluster initialization", func() { Context("Handling existing resources", func() { AfterEach(cleanupResources) - const applicationName = "default-dsci" - - It("Should not have more than one DSCI instance in the cluster", func() { - - anotherApplicationName := "default2" - // given - desiredDsci := createDSCI(applicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace) - Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) - // when - desiredDsci2 := createDSCI(anotherApplicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace) - // then - Eventually(dscInitializationIsReady(anotherApplicationName, workingNamespace, desiredDsci2)). - WithTimeout(timeout). - WithPolling(interval). - Should(BeFalse()) - }) It("Should not update rolebinding if it exists", func() { applicationName := envtestutil.AppendRandomNameTo("rolebinding-test") @@ -199,7 +184,7 @@ var _ = Describe("DataScienceCluster initialization", func() { Should(BeTrue()) // when - desiredDsci := createDSCI(applicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace) + desiredDsci := createDSCI(operatorv1.Managed, operatorv1.Managed, monitoringNamespace) Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) foundDsci := &dsci.DSCInitialization{} Eventually(dscInitializationIsReady(applicationName, workingNamespace, foundDsci)). @@ -240,7 +225,7 @@ var _ = Describe("DataScienceCluster initialization", func() { Should(BeTrue()) // when - desiredDsci := createDSCI(applicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace) + desiredDsci := createDSCI(operatorv1.Managed, operatorv1.Managed, monitoringNamespace) Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) foundDsci := &dsci.DSCInitialization{} Eventually(dscInitializationIsReady(applicationName, workingNamespace, foundDsci)). @@ -277,7 +262,7 @@ var _ = Describe("DataScienceCluster initialization", func() { Should(BeTrue()) // when - desiredDsci := createDSCI(applicationName, operatorv1.Managed, operatorv1.Managed, monitoringNamespace) + desiredDsci := createDSCI(operatorv1.Managed, operatorv1.Managed, monitoringNamespace) Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) foundDsci := &dsci.DSCInitialization{} Eventually(dscInitializationIsReady(applicationName, workingNamespace, foundDsci)). @@ -349,14 +334,14 @@ func objectExists(ns string, name string, obj client.Object) func() bool { //nol } } -func createDSCI(appName string, enableMonitoring operatorv1.ManagementState, enableTrustedCABundle operatorv1.ManagementState, monitoringNS string) *dsci.DSCInitialization { +func createDSCI(enableMonitoring operatorv1.ManagementState, enableTrustedCABundle operatorv1.ManagementState, monitoringNS string) *dsci.DSCInitialization { return &dsci.DSCInitialization{ TypeMeta: metav1.TypeMeta{ Kind: "DSCInitialization", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: appName, + Name: applicationName, Namespace: workingNamespace, }, Spec: dsci.DSCInitializationSpec{ diff --git a/controllers/webhook/webhook.go b/controllers/webhook/webhook.go new file mode 100644 index 00000000000..f730dc78703 --- /dev/null +++ b/controllers/webhook/webhook.go @@ -0,0 +1,111 @@ +/* +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 webhook + +import ( + "context" + "fmt" + "net/http" + + admissionv1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var log = ctrl.Log.WithName("odh-controller-webhook") + +//+kubebuilder:webhook:path=/validate-opendatahub-io-v1,mutating=false,failurePolicy=fail,sideEffects=None,groups=datasciencecluster.opendatahub.io;dscinitialization.opendatahub.io,resources=datascienceclusters;dscinitializations,verbs=create;update,versions=v1,name=operator.opendatahub.io,admissionReviewVersions=v1 +//nolint:lll + +type OpenDataHubWebhook struct { + client client.Client + decoder *admission.Decoder +} + +func (w *OpenDataHubWebhook) SetupWithManager(mgr ctrl.Manager) { + hookServer := mgr.GetWebhookServer() + odhWebhook := &webhook.Admission{ + Handler: w, + } + hookServer.Register("/validate-opendatahub-io-v1", odhWebhook) +} + +func (w *OpenDataHubWebhook) InjectDecoder(d *admission.Decoder) error { + w.decoder = d + return nil +} + +func (w *OpenDataHubWebhook) InjectClient(c client.Client) error { + w.client = c + return nil +} + +func (w *OpenDataHubWebhook) checkDupCreation(ctx context.Context, req admission.Request) admission.Response { + if req.Operation != admissionv1.Create { + return admission.Allowed(fmt.Sprintf("duplication check: skipping %v request", req.Operation)) + } + + switch req.Kind.Kind { + case "DataScienceCluster": + case "DSCInitialization": + default: + log.Info("Got wrong kind", "kind", req.Kind.Kind) + return admission.Errored(http.StatusBadRequest, nil) + } + + gvk := schema.GroupVersionKind{ + Group: req.Kind.Group, + Version: req.Kind.Version, + Kind: req.Kind.Kind, + } + + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + + if err := w.client.List(ctx, list); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + // if len == 1 now creation of #2 is being handled + if len(list.Items) > 0 { + return admission.Denied(fmt.Sprintf("Only one instance of %s object is allowed", req.Kind.Kind)) + } + + return admission.Allowed(fmt.Sprintf("%s duplication check passed", req.Kind.Kind)) +} + +func (w *OpenDataHubWebhook) Handle(ctx context.Context, req admission.Request) admission.Response { + var resp admission.Response + + // Handle only Create and Update + if req.Operation == admissionv1.Delete || req.Operation == admissionv1.Connect { + msg := fmt.Sprintf("ODH skipping %v request", req.Operation) + log.Info(msg) + return admission.Allowed(msg) + } + + resp = w.checkDupCreation(ctx, req) + if !resp.Allowed { + return resp + } + + return admission.Allowed(fmt.Sprintf("%s allowed", req.Kind.Kind)) +} diff --git a/controllers/webhook/webhook_suite_test.go b/controllers/webhook/webhook_suite_test.go new file mode 100644 index 00000000000..95ce437c10c --- /dev/null +++ b/controllers/webhook/webhook_suite_test.go @@ -0,0 +1,244 @@ +/* +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 webhook_test + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + operatorv1 "github.com/openshift/api/operator/v1" + admissionv1beta1 "k8s.io/api/admission/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + dsc "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" + dsci "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/components/codeflare" + "github.com/opendatahub-io/opendatahub-operator/v2/components/dashboard" + "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/ray" + "github.com/opendatahub-io/opendatahub-operator/v2/components/trustyai" + "github.com/opendatahub-io/opendatahub-operator/v2/components/workbenches" + "github.com/opendatahub-io/opendatahub-operator/v2/controllers/webhook" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + namespace = "webhook-test-ns" + nameBase = "webhook-test" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + // DSCI + err = dsci.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + // DSC + err = dsc.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + // Webhook + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + (&webhook.OpenDataHubWebhook{}).SetupWithManager(mgr) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) //nolint:gosec + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = Describe("DSC/DSCI webhook", func() { + It("Should not have more than one DSCI instance in the cluster", func() { + desiredDsci := newDSCI(nameBase + "-dsci-1") + Expect(k8sClient.Create(context.Background(), desiredDsci)).Should(Succeed()) + desiredDsci2 := newDSCI(nameBase + "-dsci-2") + Expect(k8sClient.Create(context.Background(), desiredDsci2)).ShouldNot(Succeed()) + }) + + It("Should block creation of second DSC instance", func() { + dscSpec := newDSC(nameBase+"-dsc-1", namespace) + Expect(k8sClient.Create(context.Background(), dscSpec)).Should(Succeed()) + dscSpec = newDSC(nameBase+"-dsc-2", namespace) + Expect(k8sClient.Create(context.Background(), dscSpec)).ShouldNot(Succeed()) + }) +}) + +func newDSCI(appName string) *dsci.DSCInitialization { + monitoringNS := "monitoring-namespace" + return &dsci.DSCInitialization{ + TypeMeta: metav1.TypeMeta{ + Kind: "DSCInitialization", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: appName, + Namespace: namespace, + }, + Spec: dsci.DSCInitializationSpec{ + ApplicationsNamespace: namespace, + Monitoring: dsci.Monitoring{ + Namespace: monitoringNS, + ManagementState: operatorv1.Managed, + }, + TrustedCABundle: dsci.TrustedCABundleSpec{ + ManagementState: operatorv1.Managed, + }, + }, + } +} +func newDSC(name string, namespace string) *dsc.DataScienceCluster { + return &dsc.DataScienceCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: dsc.DataScienceClusterSpec{ + Components: dsc.Components{ + Dashboard: dashboard.Dashboard{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + Workbenches: workbenches.Workbenches{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + ModelMeshServing: modelmeshserving.ModelMeshServing{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + DataSciencePipelines: datasciencepipelines.DataSciencePipelines{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + Kserve: kserve.Kserve{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + CodeFlare: codeflare.CodeFlare{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + Ray: ray.Ray{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + TrustyAI: trustyai.TrustyAI{ + Component: components.Component{ + ManagementState: operatorv1.Removed, + }, + }, + }, + }, + } +} diff --git a/main.go b/main.go index 67f46d3b772..96ba5af5a94 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ limitations under the License. package main import ( + "context" "flag" "os" @@ -45,6 +46,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" kfdefv1 "github.com/opendatahub-io/opendatahub-operator/apis/kfdef.apps.kubeflow.org/v1" dsc "github.com/opendatahub-io/opendatahub-operator/v2/apis/datasciencecluster/v1" @@ -54,6 +56,7 @@ import ( datascienceclustercontrollers "github.com/opendatahub-io/opendatahub-operator/v2/controllers/datasciencecluster" dscicontr "github.com/opendatahub-io/opendatahub-operator/v2/controllers/dscinitialization" "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/deploy" "github.com/opendatahub-io/opendatahub-operator/v2/pkg/upgrade" ) @@ -137,6 +140,8 @@ func main() { os.Exit(1) } + (&webhook.OpenDataHubWebhook{}).SetupWithManager(mgr) + if err = (&dscicontr.DSCInitializationReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -203,8 +208,18 @@ func main() { // Check if user opted for disabling DSC configuration _, disableDSCConfig := os.LookupEnv("DISABLE_DSC_CONFIG") if !disableDSCConfig { - if err = upgrade.CreateDefaultDSCI(setupClient, platform, dscApplicationsNamespace, dscMonitoringNamespace); err != nil { - setupLog.Error(err, "unable to create initial setup for the operator") + var createDefaultDSCIFunc manager.RunnableFunc = func(ctx context.Context) error { + err := upgrade.CreateDefaultDSCI(setupClient, platform, dscApplicationsNamespace, dscMonitoringNamespace) + if err != nil { + setupLog.Error(err, "unable to create initial setup for the operator") + } + return err + } + + err := mgr.Add(createDefaultDSCIFunc) + if err != nil { + setupLog.Error(err, "error scheduling DSCI creation") + os.Exit(1) } } diff --git a/tests/e2e/controller_setup_test.go b/tests/e2e/controller_setup_test.go index bb005142fce..b5f058c4957 100644 --- a/tests/e2e/controller_setup_test.go +++ b/tests/e2e/controller_setup_test.go @@ -76,9 +76,9 @@ func NewTestContext() (*testContext, error) { //nolint:golint,revive // Only use } // setup DSCI CR since we do not create automatically by operator - testDSCI := setupDSCICR() + testDSCI := setupDSCICR("e2e-test-dsci") // Setup DataScienceCluster CR - testDSC := setupDSCInstance() + testDSC := setupDSCInstance("e2e-test") return &testContext{ cfg: config, diff --git a/tests/e2e/dsc_creation_test.go b/tests/e2e/dsc_creation_test.go index b5fcc71b888..5c2e078f78c 100644 --- a/tests/e2e/dsc_creation_test.go +++ b/tests/e2e/dsc_creation_test.go @@ -13,6 +13,9 @@ import ( autoscalingv1 "k8s.io/api/autoscaling/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" @@ -34,10 +37,16 @@ func creationTestSuite(t *testing.T) { err = testCtx.testDSCICreation() require.NoError(t, err, "error creating DSCI CR") }) + t.Run("Creation of more than one of DSCInitialization instance", func(t *testing.T) { + testCtx.testDSCIDuplication(t) + }) t.Run("Creation of DataScienceCluster instance", func(t *testing.T) { err = testCtx.testDSCCreation() require.NoError(t, err, "error creating DataScienceCluster instance") }) + t.Run("Creation of more than one of DataScienceCluster instance", func(t *testing.T) { + testCtx.testDSCDuplication(t) + }) t.Run("Validate all deployed components", func(t *testing.T) { err = testCtx.testAllApplicationCreation(t) require.NoError(t, err, "error testing deployments for DataScienceCluster: "+testCtx.testDsc.Name) @@ -132,6 +141,56 @@ func (tc *testContext) testDSCCreation() error { return nil } +func (tc *testContext) requireInstalled(t *testing.T, gvk schema.GroupVersionKind) { + t.Helper() + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(gvk) + + err := tc.customClient.List(tc.ctx, list) + require.NoErrorf(t, err, "Could not get %s list", gvk.Kind) + + require.Greaterf(t, len(list.Items), 0, "%s has not been installed", gvk.Kind) +} + +func (tc *testContext) testDuplication(t *testing.T, gvk schema.GroupVersionKind, o any) { + t.Helper() + tc.requireInstalled(t, gvk) + + u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o) + require.NoErrorf(t, err, "Could not unstructure %s", gvk.Kind) + + obj := &unstructured.Unstructured{ + Object: u, + } + obj.SetGroupVersionKind(gvk) + + err = tc.customClient.Create(tc.ctx, obj) + + require.Errorf(t, err, "Could create second %s", gvk.Kind) +} + +func (tc *testContext) testDSCIDuplication(t *testing.T) { //nolint:thelper + gvk := schema.GroupVersionKind{ + Group: "dscinitialization.opendatahub.io", + Version: "v1", + Kind: "DSCInitialization", + } + dup := setupDSCICR("e2e-test-dsci-dup") + + tc.testDuplication(t, gvk, dup) +} + +func (tc *testContext) testDSCDuplication(t *testing.T) { //nolint:thelper + gvk := schema.GroupVersionKind{ + Group: "datasciencecluster.opendatahub.io", + Version: "v1", + Kind: "DataScienceCluster", + } + dup := setupDSCInstance("e2e-test-dup") + + tc.testDuplication(t, gvk, dup) +} + func (tc *testContext) testAllApplicationCreation(t *testing.T) error { //nolint:funlen,thelper // Validate test instance is in Ready state diff --git a/tests/e2e/helper_test.go b/tests/e2e/helper_test.go index 592a95cff4b..c426d13feeb 100644 --- a/tests/e2e/helper_test.go +++ b/tests/e2e/helper_test.go @@ -53,10 +53,10 @@ func (tc *testContext) waitForControllerDeployment(name string, replicas int32) return err } -func setupDSCICR() *dsci.DSCInitialization { +func setupDSCICR(name string) *dsci.DSCInitialization { dsciTest := &dsci.DSCInitialization{ ObjectMeta: metav1.ObjectMeta{ - Name: "e2e-test-dsci", + Name: name, }, Spec: dsci.DSCInitializationSpec{ ApplicationsNamespace: "opendatahub", @@ -73,10 +73,10 @@ func setupDSCICR() *dsci.DSCInitialization { return dsciTest } -func setupDSCInstance() *dsc.DataScienceCluster { +func setupDSCInstance(name string) *dsc.DataScienceCluster { dscTest := &dsc.DataScienceCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "e2e-test-dsc", + Name: name, }, Spec: dsc.DataScienceClusterSpec{ Components: dsc.Components{