diff --git a/api/v2/checluster_webhook.go b/api/v2/checluster_webhook.go index 52c7e12d5..47870c38c 100644 --- a/api/v2/checluster_webhook.go +++ b/api/v2/checluster_webhook.go @@ -18,6 +18,7 @@ import ( "strconv" "strings" + "github.com/eclipse-che/che-operator/pkg/common/utils" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -124,6 +125,11 @@ func (r *CheClusterValidator) ValidateCreate(_ context.Context, obj runtime.Obje if err := r.ensureSingletonCheCluster(); err != nil { return []string{}, err } + + if err := r.ensureDevWorkspaceOperatorConfigApiExist(); err != nil { + return []string{}, err + } + return []string{}, r.validate(cheCluster) } @@ -162,6 +168,16 @@ func (r *CheClusterValidator) ensureSingletonCheCluster() error { return nil } +func (r *CheClusterValidator) ensureDevWorkspaceOperatorConfigApiExist() error { + k8sHelper := k8shelper.New() + + if !utils.IsK8SResourceServed(k8sHelper.GetClientset().Discovery(), constants.DevWorkspaceOperatorConfigPlural) { + return fmt.Errorf(constants.DevWorkspaceOperatorNotExistsErrorMsg) + } + + return nil +} + func (r *CheClusterValidator) validate(checluster *CheCluster) error { for _, github := range checluster.Spec.GitServices.GitHub { if err := r.validateOAuthSecret(github.SecretName, "github", github.Endpoint, github.DisableSubdomainIsolation, checluster.Namespace); err != nil { diff --git a/build/scripts/olm/test-catalog-from-sources.sh b/build/scripts/olm/test-catalog-from-sources.sh index 997aaa12b..c57e1a2d6 100755 --- a/build/scripts/olm/test-catalog-from-sources.sh +++ b/build/scripts/olm/test-catalog-from-sources.sh @@ -150,7 +150,7 @@ run() { make create-operatorgroup NAME="eclipse-che" NAMESPACE="${NAMESPACE}" VERBOSE=${VERBOSE} # Install Dev Workspace operator next version - make install-devworkspace CHANNEL="next" VERBOSE=${VERBOSE} OPERATOR_NAMESPACE="${NAMESPACE}" + # make install-devworkspace CHANNEL="next" VERBOSE=${VERBOSE} OPERATOR_NAMESPACE="${NAMESPACE}" exposeOpenShiftRegistry createEclipseCheCatalogFromSources @@ -166,14 +166,14 @@ run() { VERBOSE=${VERBOSE} make wait-pod-running NAMESPACE="${NAMESPACE}" SELECTOR="app.kubernetes.io/component=che-operator" - if [[ $(oc get checluster -n "${NAMESPACE}" --no-headers | wc -l) == 0 ]]; then - getCheClusterCRFromInstalledCSV | oc apply -n "${NAMESPACE}" -f - - if [[ -n ${CR_PATCH_YAML} ]]; then - patch=$(yq -r "." "${CR_PATCH_YAML}" | tr -d "\n" ) - oc patch checluster eclipse-che -n "${NAMESPACE}" --type='merge' -p "${patch}" - fi - fi - make wait-eclipseche-version VERSION="$(getCheVersionFromInstalledCSV)" NAMESPACE="${NAMESPACE}" VERBOSE=${VERBOSE} +# if [[ $(oc get checluster -n "${NAMESPACE}" --no-headers | wc -l) == 0 ]]; then +# getCheClusterCRFromInstalledCSV | oc apply -n "${NAMESPACE}" -f - +# if [[ -n ${CR_PATCH_YAML} ]]; then +# patch=$(yq -r "." "${CR_PATCH_YAML}" | tr -d "\n" ) +# oc patch checluster eclipse-che -n "${NAMESPACE}" --type='merge' -p "${patch}" +# fi +# fi +# make wait-eclipseche-version VERSION="$(getCheVersionFromInstalledCSV)" NAMESPACE="${NAMESPACE}" VERBOSE=${VERBOSE} } init "$@" diff --git a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml index 5bcc9278f..eb74093b5 100644 --- a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml +++ b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml @@ -86,7 +86,7 @@ metadata: categories: Developer Tools certified: "false" containerImage: quay.io/eclipse/che-operator:next - createdAt: "2026-01-16T09:21:06Z" + createdAt: "2026-01-21T12:02:21Z" description: A Kube-native development solution that delivers portable and collaborative developer workspaces. features.operators.openshift.io/cnf: "false" @@ -108,7 +108,7 @@ metadata: operatorframework.io/arch.amd64: supported operatorframework.io/arch.arm64: supported operatorframework.io/os.linux: supported - name: eclipse-che.v7.114.0-951.next + name: eclipse-che.v7.114.0-952.next namespace: placeholder spec: apiservicedefinitions: {} @@ -906,6 +906,14 @@ spec: verbs: - get - create + - apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list serviceAccountName: che-operator deployments: - label: @@ -1141,7 +1149,7 @@ spec: name: gateway-authorization-sidecar-k8s - image: quay.io/che-incubator/header-rewrite-proxy:latest name: gateway-header-sidecar - version: 7.114.0-951.next + version: 7.114.0-952.next webhookdefinitions: - admissionReviewVersions: - v1 diff --git a/bundle/next/eclipse-che/metadata/dependencies.yaml b/bundle/next/eclipse-che/metadata/dependencies.yaml deleted file mode 100644 index b8ba165d6..000000000 --- a/bundle/next/eclipse-che/metadata/dependencies.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright (c) 2019-2023 Red Hat, Inc. -# This program and the accompanying materials are made -# available under the terms of the Eclipse Public License 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0/ -# -# SPDX-License-Identifier: EPL-2.0 -# -# Contributors: -# Red Hat, Inc. - initial API and implementation -# - -dependencies: -- type: olm.package - value: - packageName: devworkspace-operator - version: ">=0.11.0" diff --git a/cmd/main.go b/cmd/main.go index d3afbb978..312570453 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -13,6 +13,7 @@ package main import ( + "context" "flag" "os" "time" @@ -154,6 +155,7 @@ func init() { utilruntime.Must(projectv1.AddToScheme(scheme)) utilruntime.Must(securityv1.Install(scheme)) utilruntime.Must(templatev1.Install(scheme)) + utilruntime.Must(operatorsv1alpha1.AddToScheme(scheme)) } } @@ -187,28 +189,7 @@ func printVersion(logger logr.Logger) { logger.Info("Operator is running on ", "Infrastructure", infra) } -// getWatchNamespace returns the Namespace the operator should be watching for changes -func getWatchNamespace() (string, error) { - // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE - // which specifies the Namespace to watch. - // An empty value means the operator is running with cluster scope. - var watchNamespaceEnvVar = "WATCH_NAMESPACE" - - ns, found := os.LookupEnv(watchNamespaceEnvVar) - if !found { - return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar) - } - - return ns, nil -} - func main() { - watchNamespace, err := getWatchNamespace() - if err != nil { - setupLog.Error(err, "unable to get WatchNamespace, "+ - "the manager will watch and manage resources in all namespaces") - } - config := ctrl.GetConfigOrDie() discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) @@ -252,12 +233,6 @@ func main() { LeaseDuration: &leaseDuration, RenewDeadline: &renewDeadline, NewCache: cacheFunction, - - // NOTE: We CANNOT limit the manager to a single namespace, because that would limit the - // devworkspace routing reconciler to a single namespace, which would make it totally unusable. - // Instead, if some controller wants to limit itself to single namespace, it can do it - // for example using an event filter, as checontroller does. - // Namespace: watchNamespace, }) if err != nil { setupLog.Error(err, "unable to start manager") @@ -270,22 +245,43 @@ func main() { os.Exit(1) } + terminationPeriod := int64(20) + if !test.IsTestMode() { + namespace, err := infrastructure.GetOperatorNamespace() + if err == nil { + terminationPeriod = signal.GetTerminationGracePeriodSeconds(mgr.GetAPIReader(), namespace) + } + } + sigHandler := signal.SetupSignalHandler(terminationPeriod) + // Setup all Controllers - cheReconciler := checontroller.NewReconciler(mgr.GetClient(), nonCachingClient, discoveryClient, mgr.GetScheme(), watchNamespace) + cheReconciler := checontroller.NewReconciler(mgr.GetClient(), nonCachingClient, discoveryClient, mgr.GetScheme()) if err = cheReconciler.SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to set up controller", "controller", "CheCluster") os.Exit(1) } - routing := dwr.DevWorkspaceRoutingReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("DevWorkspaceRouting"), - Scheme: mgr.GetScheme(), - SolverGetter: solver.Getter(mgr.GetScheme()), - } - if err := routing.SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to set up controller", "controller", "DevWorkspaceRouting") - os.Exit(1) + if utils.IsK8SResourceServed(discoveryClient, constants.DevWorkspaceRoutingPlural) { + routing := dwr.DevWorkspaceRoutingReconciler{ + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("DevWorkspaceRouting"), + Scheme: mgr.GetScheme(), + SolverGetter: solver.Getter(mgr.GetScheme()), + } + if err := routing.SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to set up controller", "controller", "DevWorkspaceRouting") + os.Exit(1) + } + } else { + setupLog.Info("DevWorkspaceRouting API is not available.") + go waitUntilDevWorkspaceRoutingApiExists( + discoveryClient, + sigHandler, + func() { + setupLog.Info("DevWorkspaceRouting API is available. Restarting operator ...") + os.Exit(0) + }, + ) } namespacecache := namespacecache.NewNamespaceCache(nonCachingClient) @@ -302,15 +298,6 @@ func main() { os.Exit(1) } - terminationPeriod := int64(20) - if !test.IsTestMode() { - namespace, err := infrastructure.GetOperatorNamespace() - if err == nil { - terminationPeriod = signal.GetTerminationGracePeriodSeconds(mgr.GetAPIReader(), namespace) - } - } - sigHandler := signal.SetupSignalHandler(terminationPeriod) - // we install the devworkspace CheCluster reconciler even if dw is not supported so that it // can write meaningful status messages into the CheCluster CRs. dwChe := devworkspace.CheClusterReconciler{} @@ -413,3 +400,23 @@ func getCacheFunc() (cache.NewCacheFunc, error) { return cache.New(config, opts) }, nil } + +func waitUntilDevWorkspaceRoutingApiExists( + discoveryClient discovery.DiscoveryInterface, + context context.Context, + callback func(), +) { + ticker := time.NewTicker(60 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if utils.IsK8SResourceServed(discoveryClient, constants.DevWorkspaceRoutingPlural) { + callback() + } + case <-context.Done(): + return + } + } +} diff --git a/config/rbac/cluster_role.yaml b/config/rbac/cluster_role.yaml index 109b539ed..d95f5a506 100644 --- a/config/rbac/cluster_role.yaml +++ b/config/rbac/cluster_role.yaml @@ -384,4 +384,12 @@ rules: - servicemonitors verbs: - get - - create \ No newline at end of file + - create + - apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list \ No newline at end of file diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index 975319401..5c8facf42 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -19,6 +19,7 @@ import ( "github.com/eclipse-che/che-operator/pkg/common/constants" k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" "github.com/eclipse-che/che-operator/pkg/common/reconciler" + "github.com/eclipse-che/che-operator/pkg/deploy/devworkspace" "k8s.io/utils/pointer" "sigs.k8s.io/controller-runtime/pkg/controller" @@ -33,7 +34,6 @@ import ( "github.com/devfile/devworkspace-operator/pkg/infrastructure" "github.com/eclipse-che/che-operator/pkg/common/chetypes" - "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/eclipse-che/che-operator/pkg/deploy" "github.com/eclipse-che/che-operator/pkg/deploy/consolelink" "github.com/eclipse-che/che-operator/pkg/deploy/dashboard" @@ -80,8 +80,6 @@ type CheClusterReconciler struct { // in the API Server discoveryClient discovery.DiscoveryInterface reconcilerManager *reconciler.ReconcilerManager - // the namespace to which to limit the reconciliation. If empty, all namespaces are considered - namespace string } // NewReconciler returns a new CheClusterReconciler @@ -89,8 +87,7 @@ func NewReconciler( k8sclient client.Client, noncachedClient client.Client, discoveryClient discovery.DiscoveryInterface, - scheme *k8sruntime.Scheme, - namespace string) *CheClusterReconciler { + scheme *k8sruntime.Scheme) *CheClusterReconciler { reconcilerManager := reconciler.NewReconcilerManager() @@ -99,6 +96,7 @@ func NewReconciler( reconcilerManager.AddReconciler(migration.NewMigrator()) reconcilerManager.AddReconciler(migration.NewCheClusterDefaultsCleaner()) reconcilerManager.AddReconciler(NewCheClusterValidator()) + reconcilerManager.AddReconciler(devworkspace.NewDevWorkspaceVersionValidator(constants.MinimumDevWorkspaceVersion)) } reconcilerManager.AddReconciler(tls.NewCertificatesReconciler()) @@ -133,7 +131,6 @@ func NewReconciler( client: k8sclient, nonCachedClient: noncachedClient, discoveryClient: discoveryClient, - namespace: namespace, reconcilerManager: reconcilerManager, } } @@ -141,7 +138,7 @@ func NewReconciler( // SetupWithManager sets up the controller with the Manager. func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { var toTrustedBundleConfigMapRequestMapper handler.MapFunc = func(ctx context.Context, obj client.Object) []reconcile.Request { - isTrusted, reconcileRequest := IsTrustedBundleConfigMap(r.client, r.namespace, obj) + isTrusted, reconcileRequest := IsTrustedBundleConfigMap(r.client, obj) if isTrusted { return []reconcile.Request{reconcileRequest} } @@ -149,7 +146,7 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { } var toEclipseCheRelatedObjRequestMapper handler.MapFunc = func(ctx context.Context, obj client.Object) []reconcile.Request { - isEclipseCheRelatedObj, reconcileRequest := IsEclipseCheRelatedObj(r.client, r.namespace, obj) + isEclipseCheRelatedObj, reconcileRequest := IsEclipseCheRelatedObj(r.client, obj) if isEclipseCheRelatedObj { return []reconcile.Request{reconcileRequest} } @@ -181,10 +178,6 @@ func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { bld.Owns(&networking.Ingress{}) } - if r.namespace != "" { - bld = bld.WithEventFilter(utils.InNamespaceEventFilter(r.namespace)) - } - // Use controller.TypedOptions to allow to configure 2 controllers for same object being reconciled return bld.WithOptions( controller.TypedOptions[reconcile.Request]{ diff --git a/controllers/che/cheobj_verifier.go b/controllers/che/cheobj_verifier.go index b200e2039..2c60ea5df 100644 --- a/controllers/che/cheobj_verifier.go +++ b/controllers/che/cheobj_verifier.go @@ -21,22 +21,17 @@ import ( ) // IsTrustedBundleConfigMap detects whether given config map is the config map with additional CA certificates to be trusted by Che -func IsTrustedBundleConfigMap(cl client.Client, watchNamespace string, obj client.Object) (bool, ctrl.Request) { +func IsTrustedBundleConfigMap(cl client.Client, obj client.Object) (bool, ctrl.Request) { if obj.GetNamespace() == "" { // ignore cluster scope objects return false, ctrl.Request{} } - checluster, _ := deploy.FindCheClusterCRInNamespace(cl, watchNamespace) + checluster, _ := deploy.FindCheClusterCRInNamespace(cl, obj.GetNamespace()) if checluster == nil { return false, ctrl.Request{} } - if checluster.Namespace != obj.GetNamespace() { - // ignore object in another namespace - return false, ctrl.Request{} - } - // Check for component if value, exists := obj.GetLabels()[constants.KubernetesComponentLabelKey]; !exists || value != constants.CheCABundle { // Labels do not match @@ -60,22 +55,17 @@ func IsTrustedBundleConfigMap(cl client.Client, watchNamespace string, obj clien // isEclipseCheRelatedObj indicates if there is an object in a che namespace with the labels: // - 'app.kubernetes.io/part-of=che.eclipse.org' // - 'app.kubernetes.io/instance=che' -func IsEclipseCheRelatedObj(cl client.Client, watchNamespace string, obj client.Object) (bool, ctrl.Request) { +func IsEclipseCheRelatedObj(cl client.Client, obj client.Object) (bool, ctrl.Request) { if obj.GetNamespace() == "" { // ignore cluster scope objects return false, ctrl.Request{} } - checluster, _ := deploy.FindCheClusterCRInNamespace(cl, watchNamespace) + checluster, _ := deploy.FindCheClusterCRInNamespace(cl, obj.GetNamespace()) if checluster == nil { return false, ctrl.Request{} } - if checluster.Namespace != obj.GetNamespace() { - // ignore object in another namespace - return false, ctrl.Request{} - } - // Check for part-of label if value, exists := obj.GetLabels()[constants.KubernetesPartOfLabelKey]; !exists || value != constants.CheEclipseOrg { return false, ctrl.Request{} diff --git a/controllers/che/cheobj_verifier_test.go b/controllers/che/cheobj_verifier_test.go index 809861a7c..6b05a314e 100644 --- a/controllers/che/cheobj_verifier_test.go +++ b/controllers/che/cheobj_verifier_test.go @@ -123,7 +123,7 @@ func TestIsTrustedBundleConfigMap(t *testing.T) { newTestObject.ObjectMeta.Labels = testCase.objLabels } - isEclipseCheObj, req := IsTrustedBundleConfigMap(ctx.ClusterAPI.Client, testCase.watchNamespace, newTestObject) + isEclipseCheObj, req := IsTrustedBundleConfigMap(ctx.ClusterAPI.Client, newTestObject) assert.Equal(t, testCase.expectedIsEclipseCheObj, isEclipseCheObj) if isEclipseCheObj { @@ -217,7 +217,7 @@ func TestIsEclipseCheRelatedObj(t *testing.T) { ctx := test.NewCtxBuilder().WithObjects(testCase.initObjects...).Build() testObject.ObjectMeta.Namespace = testCase.objNamespace - isEclipseCheObj, req := IsEclipseCheRelatedObj(ctx.ClusterAPI.Client, testCase.watchNamespace, testObject) + isEclipseCheObj, req := IsEclipseCheRelatedObj(ctx.ClusterAPI.Client, testObject) assert.Equal(t, testCase.expectedIsEclipseCheObj, isEclipseCheObj) if isEclipseCheObj { diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index 4aeeb2134..eb40c843b 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -10066,6 +10066,14 @@ rules: verbs: - get - create +- apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml b/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml index 5736411fc..ac1862e74 100644 --- a/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml +++ b/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml @@ -385,3 +385,11 @@ rules: verbs: - get - create +- apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index b2e9262d5..958dd8fa8 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -10066,6 +10066,14 @@ rules: verbs: - get - create +- apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole diff --git a/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml b/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml index 5736411fc..ac1862e74 100644 --- a/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml +++ b/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml @@ -385,3 +385,11 @@ rules: verbs: - get - create +- apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list diff --git a/helmcharts/next/templates/che-operator.ClusterRole.yaml b/helmcharts/next/templates/che-operator.ClusterRole.yaml index 5736411fc..ac1862e74 100644 --- a/helmcharts/next/templates/che-operator.ClusterRole.yaml +++ b/helmcharts/next/templates/che-operator.ClusterRole.yaml @@ -385,3 +385,11 @@ rules: verbs: - get - create +- apiGroups: + - operators.coreos.com + resources: + - subscriptions + verbs: + - get + - watch + - list diff --git a/pkg/common/constants/constants.go b/pkg/common/constants/constants.go index dc1e93529..a141cc1a1 100644 --- a/pkg/common/constants/constants.go +++ b/pkg/common/constants/constants.go @@ -134,6 +134,12 @@ const ( DefaultContainerBuildSccName = "container-build" DefaultContainerRunSccName = "container-run" DefaultDisableContainerRunCapabilities = true + DevWorkspaceOperatorConfigPlural = "devworkspaceoperatorconfigs" + DevWorkspaceRoutingPlural = "devworkspaceroutings" + DevWorkspaceOperatorNotExistsErrorMsg = "DevWorkspace Operator is not installed. Please install it before creating a CheCluster instance" + + // TODO update on each release + MinimumDevWorkspaceVersion = "0.38.0" // Finalizers ContainerBuildFinalizer = "container-build.finalizers.che.eclipse.org" diff --git a/pkg/common/k8s-helper/k8s_helper.go b/pkg/common/k8s-helper/k8s_helper.go index d7f0f4178..169521aae 100644 --- a/pkg/common/k8s-helper/k8s_helper.go +++ b/pkg/common/k8s-helper/k8s_helper.go @@ -17,6 +17,8 @@ import ( "os" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + fakeDiscovery "k8s.io/client-go/discovery/fake" + ctrl "sigs.k8s.io/controller-runtime" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -24,7 +26,6 @@ import ( "k8s.io/client-go/kubernetes/fake" - "github.com/sirupsen/logrus" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client/config" ) @@ -36,6 +37,7 @@ type K8sHelper struct { var ( k8sHelper *K8sHelper + logger = ctrl.Log.WithName("k8sHelper") ) func New() *K8sHelper { @@ -47,7 +49,12 @@ func New() *K8sHelper { return initializeForTesting() } - return initialize() + if err := initialize(); err != nil { + logger.Error(err, "Failed to initialize Kubernetes client") + os.Exit(1) + } + + return k8sHelper } func (cl *K8sHelper) GetClientset() kubernetes.Interface { @@ -74,35 +81,40 @@ func (cl *K8sHelper) GetPodsByComponent(name string, ns string) []string { func initializeForTesting() *K8sHelper { k8sHelper = &K8sHelper{ - clientset: fake.NewSimpleClientset(), + clientset: fake.NewClientset(), client: fakeclient.NewClientBuilder().Build(), } + k8sHelper.clientset.Discovery().(*fakeDiscovery.FakeDiscovery).Fake.Resources = []*metav1.APIResourceList{ + { + APIResources: []metav1.APIResource{ + {Name: "devworkspaceoperatorconfigs"}, + }, + }, + } + return k8sHelper } -func initialize() *K8sHelper { - cfg, err := config.GetConfig() - if err != nil { - logrus.Fatalf("Failed to initialized Kubernetes client: %v", err) - } +func initialize() error { + cfg := config.GetConfigOrDie() - clientSet, err := kubernetes.NewForConfig(cfg) + clientset, err := kubernetes.NewForConfig(cfg) if err != nil { - logrus.Fatalf("Failed to initialized Kubernetes client: %v", err) + return err } client, err := client.New(cfg, client.Options{Scheme: runtime.NewScheme()}) if err != nil { - logrus.Fatalf("Failed to initialized Kubernetes client: %v", err) + return err } k8sHelper = &K8sHelper{ - clientset: clientSet, + clientset: clientset, client: client, } - return k8sHelper + return nil } func isTestMode() bool { return len(os.Getenv("MOCK_API")) != 0 diff --git a/pkg/common/test/test-client/test_client.go b/pkg/common/test/test-client/test_client.go index 319d9cdee..10c6dc640 100644 --- a/pkg/common/test/test-client/test_client.go +++ b/pkg/common/test/test-client/test_client.go @@ -14,6 +14,7 @@ package test_client import ( projectv1 "github.com/openshift/api/project/v1" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" batchv1 "k8s.io/api/batch/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -63,6 +64,16 @@ func GetTestClients(initObjs ...client.Object) (client.Client, *fakeDiscovery.Fa {Name: "consolelinks"}, }, }, + { + APIResources: []metav1.APIResource{ + {Name: "devworkspaceoperatorconfigs"}, + }, + }, + { + APIResources: []metav1.APIResource{ + {Name: "devworkspaceroutings"}, + }, + }, { GroupVersion: "che.eclipse.org/v1alpha1", APIResources: []metav1.APIResource{ @@ -104,6 +115,7 @@ func getScheme() *runtime.Scheme { scheme.AddKnownTypes(networkingv1.SchemeGroupVersion, &networkingv1.Ingress{}, &networkingv1.IngressList{}) scheme.AddKnownTypes(batchv1.SchemeGroupVersion, &batchv1.Job{}, &batchv1.JobList{}) scheme.AddKnownTypes(projectv1.SchemeGroupVersion, &projectv1.Project{}, &projectv1.ProjectList{}) + scheme.AddKnownTypes(operatorsv1alpha1.SchemeGroupVersion, &operatorsv1alpha1.Subscription{}, &operatorsv1alpha1.SubscriptionList{}) return scheme } diff --git a/pkg/common/utils/utils.go b/pkg/common/utils/utils.go index f4264e53c..6f95b79f9 100644 --- a/pkg/common/utils/utils.go +++ b/pkg/common/utils/utils.go @@ -24,8 +24,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/discovery" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/yaml" ) @@ -162,23 +160,6 @@ func GetMapOrDefault(value map[string]string, defaultValue map[string]string) ma return ret } -func InNamespaceEventFilter(namespace string) predicate.Predicate { - return predicate.Funcs{ - CreateFunc: func(ce event.CreateEvent) bool { - return namespace == ce.Object.GetNamespace() - }, - DeleteFunc: func(de event.DeleteEvent) bool { - return namespace == de.Object.GetNamespace() - }, - UpdateFunc: func(ue event.UpdateEvent) bool { - return namespace == ue.ObjectOld.GetNamespace() - }, - GenericFunc: func(ge event.GenericEvent) bool { - return namespace == ge.Object.GetNamespace() - }, - } -} - func ParseMap(src string) map[string]string { if src == "" { return nil diff --git a/pkg/deploy/devworkspace/devworkspace_version_validator.go b/pkg/deploy/devworkspace/devworkspace_version_validator.go new file mode 100644 index 000000000..5c7cda98c --- /dev/null +++ b/pkg/deploy/devworkspace/devworkspace_version_validator.go @@ -0,0 +1,120 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/blang/semver/v4" + "github.com/eclipse-che/che-operator/pkg/common/chetypes" + "github.com/eclipse-che/che-operator/pkg/common/constants" + "github.com/eclipse-che/che-operator/pkg/common/reconciler" + operatorsv1alpha1 "github.com/operator-framework/api/pkg/operators/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type DevWorkspaceVersionValidator struct { + reconciler.Reconcilable + minimumDwVersion string +} + +func NewDevWorkspaceVersionValidator(minimumDwVersion string) *DevWorkspaceVersionValidator { + return &DevWorkspaceVersionValidator{ + minimumDwVersion: minimumDwVersion, + } +} + +func (v *DevWorkspaceVersionValidator) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { + if err := v.ensureDevWorkspaceVersion(ctx); err != nil { + return reconcile.Result{}, false, err + } + + return reconcile.Result{}, true, nil +} + +func (v *DevWorkspaceVersionValidator) Finalize(ctx *chetypes.DeployContext) bool { + return true +} + +func (v *DevWorkspaceVersionValidator) ensureDevWorkspaceVersion(ctx *chetypes.DeployContext) error { + subscriptions := &operatorsv1alpha1.SubscriptionList{} + if err := ctx.ClusterAPI.NonCachingClient.List(context.TODO(), subscriptions); err != nil { + return err + } + + idx := slices.IndexFunc( + subscriptions.Items, + func(subscription operatorsv1alpha1.Subscription) bool { + return subscription.Spec.Package == constants.DevWorkspaceOperatorName + }, + ) + + if idx == -1 { + return fmt.Errorf(constants.DevWorkspaceOperatorNotExistsErrorMsg) + } + + subscription := subscriptions.Items[idx] + + installedCSV := subscription.Status.InstalledCSV + if installedCSV == "" { + return fmt.Errorf("DevWorkspace Operator CSV is not installed yet") + } + + // Extract version from CSV name (e.g., "devworkspace-operator.v0.40.0-dev.3" -> "0.40.0-dev.3") + baseVersion, _, err := extractVersionFromCSV(installedCSV) + if err != nil { + return fmt.Errorf("failed to extract version from CSV %s: %w", installedCSV, err) + } + + // Parse installed and required versions + installedSemver, err := semver.Parse(baseVersion) + if err != nil { + return fmt.Errorf("failed to parse installed DevWorkspace Operator version %s: %w", baseVersion, err) + } + + requiredSemver, err := semver.Parse(v.minimumDwVersion) + if err != nil { + return fmt.Errorf("failed to parse required DevWorkspace Operator version %s: %w", v.minimumDwVersion, err) + } + + // Compare versions + if installedSemver.LT(requiredSemver) { + return fmt.Errorf("DevWorkspace Operator version %s is installed, but Eclipse Che requires version %s or higher. Please upgrade the DevWorkspace Operator", baseVersion, v.minimumDwVersion) + } + + return nil +} + +func extractVersionFromCSV(csvName string) (string, string, error) { + idx := strings.LastIndex(csvName, ".v") + if idx == -1 { + return "", "", fmt.Errorf("CSV name does not contain version prefix '.v': %s", csvName) + } + + version := csvName[idx+2:] // +2 to skip ".v" + if version == "" { + return "", "", fmt.Errorf("CSV name does not contain version after '.v': %s", csvName) + } + + dashIdx := strings.LastIndex(version, "-") + if dashIdx != -1 { + baseVersion := version[:dashIdx] + devVersion := version[dashIdx+1:] + return baseVersion, devVersion, nil + } + + return version, "", nil +} diff --git a/pkg/deploy/devworkspace/devworkspace_version_validator_test.go b/pkg/deploy/devworkspace/devworkspace_version_validator_test.go new file mode 100644 index 000000000..fe65c87d2 --- /dev/null +++ b/pkg/deploy/devworkspace/devworkspace_version_validator_test.go @@ -0,0 +1,62 @@ +// +// Copyright (c) 2019-2026 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExtractVersionFromCSV(t *testing.T) { + tests := []struct { + name string + csvName string + wantVersion string + wantDevVersion string + }{ + { + name: "Valid CSV with dev version", + csvName: "devworkspace-operator.v0.40.0-dev.3", + wantVersion: "0.40.0", + wantDevVersion: "dev.3", + }, + { + name: "Valid CSV without dev version", + csvName: "devworkspace-operator.v0.40.0", + wantVersion: "0.40.0", + wantDevVersion: "", + }, + { + name: "Valid CSV with patch version", + csvName: "devworkspace-operator.v1.2.3", + wantVersion: "1.2.3", + wantDevVersion: "", + }, + { + name: "Valid CSV with complex dev version", + csvName: "devworkspace-operator.v0.40.0-alpha.1", + wantVersion: "0.40.0", + wantDevVersion: "alpha.1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotVersion, gotDevVersion, err := extractVersionFromCSV(tt.csvName) + assert.NoError(t, err) + assert.Equal(t, tt.wantVersion, gotVersion) + assert.Equal(t, tt.wantDevVersion, gotDevVersion) + }) + } +} diff --git a/pkg/deploy/devworkspace/init_test.go b/pkg/deploy/devworkspace/init_test.go new file mode 100644 index 000000000..ecccec9a8 --- /dev/null +++ b/pkg/deploy/devworkspace/init_test.go @@ -0,0 +1,26 @@ +// +// Copyright (c) 2019-2024 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package devworkspace + +import ( + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" + "github.com/eclipse-che/che-operator/pkg/common/test" +) + +func init() { + test.EnableTestMode() + + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + defaults.InitializeForTesting("../../../config/manager/manager.yaml") +}