From 2b66dbb9c3233d675ee4fc322ea3cd435d9c0d37 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Tue, 10 Mar 2026 12:46:45 +0100 Subject: [PATCH 1/7] chore: Remove old devworkspace controller Signed-off-by: Anatolii Bazko --- api/v2/checluster_types.go | 7 +- api/v2/zz_generated.deepcopy.go | 2 +- cmd/main.go | 9 - .../bases/org.eclipse.che_checlusters.yaml | 4 +- config/rbac/cluster_role.yaml | 1 + controllers/che/checluster_controller.go | 1 + controllers/devworkspace/README.adoc | 12 - controllers/devworkspace/controller.go | 381 -------------- controllers/devworkspace/controller_test.go | 469 ------------------ controllers/devworkspace/defaults/defaults.go | 2 - controllers/devworkspace/init_test.go | 26 - .../devworkspace/solver/che_routing.go | 69 +-- .../che_routing_external_tls_config_test.go | 11 +- .../devworkspace/solver/che_routing_test.go | 132 ++--- controllers/devworkspace/solver/solver.go | 56 +-- .../usernamespace_controller_test.go | 21 +- pkg/common/test/deploy_context.go | 8 +- pkg/deploy/server/basedomain_reconciler.go | 105 ++++ 18 files changed, 202 insertions(+), 1114 deletions(-) delete mode 100644 controllers/devworkspace/README.adoc delete mode 100644 controllers/devworkspace/controller.go delete mode 100644 controllers/devworkspace/controller_test.go delete mode 100644 controllers/devworkspace/init_test.go create mode 100644 pkg/deploy/server/basedomain_reconciler.go diff --git a/api/v2/checluster_types.go b/api/v2/checluster_types.go index ecdd5cc824..d52bad8390 100644 --- a/api/v2/checluster_types.go +++ b/api/v2/checluster_types.go @@ -956,12 +956,6 @@ type EditorDownloadUrl struct { // GatewayPhase describes the different phases of the Che gateway lifecycle. type GatewayPhase string -const ( - GatewayPhaseInitializing = "Initializing" - GatewayPhaseEstablished = "Established" - GatewayPhaseInactive = "Inactive" -) - // CheClusterPhase describes the different phases of the Che cluster lifecycle. type CheClusterPhase string @@ -974,6 +968,7 @@ const ( // CheClusterStatus defines the observed state of Che installation. type CheClusterStatus struct { + // Deprecated. // Specifies the current phase of the gateway deployment. // +optional // +operator-sdk:csv:customresourcedefinitions:type=status diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 111b0d42d5..64fb82e9a2 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -19,7 +19,7 @@ package v2 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" - v1 "k8s.io/api/core/v1" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/cmd/main.go b/cmd/main.go index d3afbb9784..d92b90c312 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,6 @@ import ( devworkspaceinfra "github.com/devfile/devworkspace-operator/pkg/infrastructure" "go.uber.org/zap/zapcore" - "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/pkg/common/constants" defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" "github.com/eclipse-che/che-operator/pkg/common/signal" @@ -311,14 +310,6 @@ func main() { } 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{} - if err := dwChe.SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to set up devWorkspace controller", "controller", "DevWorkspaceReconciler") - os.Exit(1) - } - if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err = chev2.SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "CheCluster") diff --git a/config/crd/bases/org.eclipse.che_checlusters.yaml b/config/crd/bases/org.eclipse.che_checlusters.yaml index 42612a5441..b2bff834fe 100644 --- a/config/crd/bases/org.eclipse.che_checlusters.yaml +++ b/config/crd/bases/org.eclipse.che_checlusters.yaml @@ -11122,7 +11122,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why diff --git a/config/rbac/cluster_role.yaml b/config/rbac/cluster_role.yaml index 109b539ed8..71e2e157bb 100644 --- a/config/rbac/cluster_role.yaml +++ b/config/rbac/cluster_role.yaml @@ -200,6 +200,7 @@ rules: verbs: - create - delete + - deletecollection - get - update - patch diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index 975319401a..deb7899f43 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -109,6 +109,7 @@ func NewReconciler( // we have to expose che endpoint independently of syncing other server // resources since che host is used for dashboard deployment and che config map reconcilerManager.AddReconciler(server.NewCheHostReconciler()) + reconcilerManager.AddReconciler(server.NewBaseDomainReconciler()) reconcilerManager.AddReconciler(postgres.NewPostgresReconciler()) if infrastructure.IsOpenShift() { reconcilerManager.AddReconciler(identityprovider.NewIdentityProviderReconciler()) diff --git a/controllers/devworkspace/README.adoc b/controllers/devworkspace/README.adoc deleted file mode 100644 index 82005db069..0000000000 --- a/controllers/devworkspace/README.adoc +++ /dev/null @@ -1,12 +0,0 @@ -= Devworkspace Che controller - -This is an import of originally standalone Devworkspace Che operator. -As such many things, that could be shared or reused with/from the rest -of the che-operator codebase, aren't. - -This situation will hopefully improve over time as we integrate the two -codebases more and more. - -In particular, the `controller/devworkspace/sync` subpackage is more -or less identical to `deploy/sync` and should be replaced by `deploy/sync` -after a careful inspection, if possible. diff --git a/controllers/devworkspace/controller.go b/controllers/devworkspace/controller.go deleted file mode 100644 index 39c2936102..0000000000 --- a/controllers/devworkspace/controller.go +++ /dev/null @@ -1,381 +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 -// - -package devworkspace - -import ( - "context" - "encoding/hex" - stdErrors "errors" - "fmt" - "math/rand" - "strings" - "sync" - "time" - - "k8s.io/utils/pointer" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/devfile/devworkspace-operator/pkg/infrastructure" - chev2 "github.com/eclipse-che/che-operator/api/v2" - "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" - routev1 "github.com/openshift/api/route/v1" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - rbac "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -var ( - log = ctrl.Log.WithName("devworkspace-che") - currentCheInstances = map[client.ObjectKey]chev2.CheCluster{} - cheInstancesAccess = sync.Mutex{} -) - -const ( - // FinalizerName is the name of the finalizer put on the Che Cluster resources by the controller. Public for testing purposes. - FinalizerName = "checluster.che.eclipse.org" -) - -type CheClusterReconciler struct { - client client.Client - scheme *runtime.Scheme -} - -// GetCurrentCheClusterInstances returns a map of all che clusters (keyed by their namespaced name) -// the che cluster controller currently knows of. This returns any meaningful data -// only after reconciliation has taken place. -// -// If this method is called from another controller, it effectively couples that controller -// with the che manager controller. Such controller will therefore have to run in the same -// process as the che manager controller. On the other hand, using this method, and somehow -// tolerating its eventual consistency, makes the other controller more efficient such that -// it doesn't have to find the che managers in the cluster (which is what che manager reconciler -// is doing). -// -// If need be, this method can be replaced by a simply calling client.List to get all the che -// managers in the cluster. -func GetCurrentCheClusterInstances() map[client.ObjectKey]chev2.CheCluster { - cheInstancesAccess.Lock() - defer cheInstancesAccess.Unlock() - - ret := map[client.ObjectKey]chev2.CheCluster{} - - for k, v := range currentCheInstances { - ret[k] = v - } - - return ret -} - -// CleanCheClusterInstancesForTest is a helper function for test code in other packages that needs -// to re-initialize the state of the checluster instance cache. -func CleanCheClusterInstancesForTest() { - cheInstancesAccess.Lock() - defer cheInstancesAccess.Unlock() - - currentCheInstances = map[client.ObjectKey]chev2.CheCluster{} -} - -// New returns a new instance of the Che manager reconciler. This is mainly useful for -// testing because it doesn't set up any watches in the cluster, etc. For that use SetupWithManager. -func New(cl client.Client, scheme *runtime.Scheme) CheClusterReconciler { - return CheClusterReconciler{ - client: cl, - scheme: scheme, - } -} - -func (r *CheClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.client = mgr.GetClient() - r.scheme = mgr.GetScheme() - - bld := ctrl.NewControllerManagedBy(mgr). - For(&chev2.CheCluster{}). - Owns(&corev1.Service{}). - Owns(&corev1.ConfigMap{}). - Owns(&appsv1.Deployment{}). - Owns(&corev1.Pod{}). - Owns(&corev1.ServiceAccount{}). - Owns(&rbac.Role{}). - Owns(&rbac.RoleBinding{}) - if infrastructure.IsOpenShift() { - bld.Owns(&routev1.Route{}) - } else { - bld.Owns(&networkingv1.Ingress{}) - } - - // Use controller.TypedOptions to allow to configure 2 controllers for same object being reconciled - return bld.WithOptions( - controller.TypedOptions[reconcile.Request]{ - SkipNameValidation: pointer.Bool(true), - }).Complete(r) -} - -func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - cheInstancesAccess.Lock() - defer cheInstancesAccess.Unlock() - - // remove the manager from the shared map for the time of the reconciliation - // we'll add it back if it is successfully reconciled. - // The access to the map is locked for the time of reconciliation so that outside - // callers don't witness this intermediate state. - delete(currentCheInstances, req.NamespacedName) - - // make sure we've checked we're in a valid state - cluster := &chev2.CheCluster{} - err := r.client.Get(ctx, req.NamespacedName, cluster) - if err != nil { - if errors.IsNotFound(err) { - // Ok, our current router disappeared... - return ctrl.Result{}, nil - } - // other error - let's requeue - return ctrl.Result{}, err - } - - if cluster.GetDeletionTimestamp() != nil { - return ctrl.Result{}, r.finalize(ctx, cluster) - } - - finalizerUpdated, err := r.ensureFinalizer(ctx, cluster) - if err != nil { - log.Info("Failed to set a finalizer", "object", req.String()) - return ctrl.Result{}, err - } else if finalizerUpdated { - // we've updated the object with a new finalizer, so we will enter another reconciliation loop shortly - // we don't add the manager into the shared map just yet, because we have actually not reconciled it fully. - return ctrl.Result{}, nil - } - - // validate the CR - err = r.validate(cluster) - if err != nil { - log.Info("validation errors", "errors", err.Error()) - res, err := r.updateStatus(ctx, cluster, nil, cluster.Status.WorkspaceBaseDomain, chev2.ClusterPhaseInactive, err.Error()) - if err != nil { - return res, err - } - - return res, nil - } - - // now, finally, the actual reconciliation - var changed bool - - // We are no longer in charge of the gateway, leaving the responsibility for managing it on the che-operator. - // But we need to detect the hostname on which the gateway is exposed so that the rest of our subsystems work. - if cluster.GetCheHost() == "" { - // Wait some time in case the route is not ready yet - return ctrl.Result{RequeueAfter: 2 * time.Second}, err - } - - // setting changed to false, because we jump from inactive directly to established, because we are no longer in - // control of gateway creation - changed = false - - workspaceBaseDomain := cluster.Spec.Networking.Domain - - // to be compatible with CheCluster API v1 - routeDomain := cluster.Spec.Components.CheServer.ExtraProperties["CHE_INFRA_OPENSHIFT_ROUTE_HOST_DOMAIN__SUFFIX"] - if routeDomain != "" { - workspaceBaseDomain = routeDomain - } - - if workspaceBaseDomain == "" { - workspaceBaseDomain, err = r.detectOpenShiftRouteBaseDomain(cluster) - if err != nil { - return ctrl.Result{}, err - } - - if workspaceBaseDomain == "" { - res, err := r.updateStatus(ctx, cluster, nil, cluster.Status.WorkspaceBaseDomain, chev2.ClusterPhaseInactive, "Could not auto-detect the workspaceBaseDomain. Please set it explicitly in the spec.") - if err != nil { - return res, err - } - - return res, nil - } - } - - res, err := r.updateStatus(ctx, cluster, &changed, workspaceBaseDomain, chev2.ClusterPhaseActive, "") - - if err != nil { - return res, err - } - - // everything went fine and the manager exists, put it back in the shared map - currentCheInstances[req.NamespacedName] = *cluster - - return res, nil -} - -func (r *CheClusterReconciler) updateStatus(ctx context.Context, cluster *chev2.CheCluster, changed *bool, workspaceDomain string, phase chev2.CheClusterPhase, phaseMessage string) (ctrl.Result, error) { - currentPhase := cluster.Status.GatewayPhase - - if changed != nil { - if *changed { - cluster.Status.GatewayPhase = chev2.GatewayPhaseInitializing - } else { - cluster.Status.GatewayPhase = chev2.GatewayPhaseEstablished - } - } - - cluster.Status.WorkspaceBaseDomain = workspaceDomain - err := r.client.Status().Update(ctx, cluster) - - requeue := currentPhase == chev2.GatewayPhaseInitializing - return ctrl.Result{Requeue: requeue}, err -} - -func (r *CheClusterReconciler) validate(cluster *chev2.CheCluster) error { - validationErrors := []string{} - - if !infrastructure.IsOpenShift() { - if cluster.Spec.Networking.Domain == "" { - validationErrors = append(validationErrors, "spec.networking.domain must be specified") - } - } - - if len(validationErrors) > 0 { - message := "The following validation errors were detected:\n" - for _, m := range validationErrors { - message += "- " + m + "\n" - } - - return stdErrors.New(message) - } - - return nil -} - -func (r *CheClusterReconciler) finalize(ctx context.Context, cluster *chev2.CheCluster) (err error) { - err = r.gatewayConfigFinalize(ctx, cluster) - - if err == nil { - finalizers := []string{} - for i := range cluster.Finalizers { - if cluster.Finalizers[i] != FinalizerName { - finalizers = append(finalizers, cluster.Finalizers[i]) - } - } - - cluster.Finalizers = finalizers - - err = r.client.Update(ctx, cluster) - } else { - cluster.Status.ChePhase = chev2.ClusterPhasePendingDeletion - cluster.Status.Message = fmt.Sprintf("Finalization has failed: %s", err.Error()) - err = r.client.Status().Update(ctx, cluster) - } - - return err -} - -func (r *CheClusterReconciler) ensureFinalizer(ctx context.Context, cluster *chev2.CheCluster) (updated bool, err error) { - - needsUpdate := true - if cluster.Finalizers != nil { - for i := range cluster.Finalizers { - if cluster.Finalizers[i] == FinalizerName { - needsUpdate = false - break - } - } - } else { - cluster.Finalizers = []string{} - } - - if needsUpdate { - cluster.Finalizers = append(cluster.Finalizers, FinalizerName) - err = r.client.Update(ctx, cluster) - } - - return needsUpdate, err -} - -// Tries to autodetect the route base domain. -func (r *CheClusterReconciler) detectOpenShiftRouteBaseDomain(cluster *chev2.CheCluster) (string, error) { - if !infrastructure.IsOpenShift() { - return "", nil - } - - name := "devworkspace-che-test-" + randomSuffix(8) - testRoute := &routev1.Route{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cluster.Namespace, - Name: name, - }, - Spec: routev1.RouteSpec{ - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: name, - }, - }, - } - - err := r.client.Create(context.TODO(), testRoute) - if err != nil { - return "", err - } - defer r.client.Delete(context.TODO(), testRoute) - host := testRoute.Spec.Host - - prefixToRemove := name + "-" + cluster.Namespace + "." - return strings.TrimPrefix(host, prefixToRemove), nil -} - -func randomSuffix(length int) string { - var rnd = rand.New(rand.NewSource(time.Now().UnixNano())) - - arr := make([]byte, (length+1)/2) // to make even-length array so that it is convertible to hex - rnd.Read(arr) - - return hex.EncodeToString(arr) -} - -// Checks that there are no devworkspace configurations for the gateway (which would mean running devworkspaces). -// If there are some, an error is returned. -func (r *CheClusterReconciler) gatewayConfigFinalize(ctx context.Context, cluster *chev2.CheCluster) error { - // we need to stop the reconcile if there are devworkspaces handled by it. - // we detect that by the presence of the gateway configmaps in the namespace of the manager - list := corev1.ConfigMapList{} - - err := r.client.List(ctx, &list, &client.ListOptions{ - Namespace: cluster.Namespace, - LabelSelector: labels.SelectorFromSet(defaults.GetLabelsForComponent(cluster, "gateway-config")), - }) - if err != nil { - return err - } - - workspaceCount := 0 - - for _, c := range list.Items { - if c.Annotations[defaults.ConfigAnnotationCheManagerName] == cluster.Name && c.Annotations[defaults.ConfigAnnotationCheManagerNamespace] == cluster.Namespace { - workspaceCount++ - } - } - - if workspaceCount > 0 { - return fmt.Errorf("there are %d devworkspaces associated with this Che manager", workspaceCount) - } - - return nil -} diff --git a/controllers/devworkspace/controller_test.go b/controllers/devworkspace/controller_test.go deleted file mode 100644 index 4ff0068c87..0000000000 --- a/controllers/devworkspace/controller_test.go +++ /dev/null @@ -1,469 +0,0 @@ -// -// Copyright (c) 2019-2025 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" - "os" - "testing" - - "github.com/stretchr/testify/assert" - - defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" - "github.com/eclipse-che/che-operator/pkg/common/test" - - "github.com/devfile/devworkspace-operator/pkg/infrastructure" - chev2 "github.com/eclipse-che/che-operator/api/v2" - devworkspacedefaults "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -func TestNoCustomResourceSharedWhenReconcilingNonExistent(t *testing.T) { - infrastructure.InitializeForTesting(infrastructure.Kubernetes) - - // clear the map before the test - for k := range currentCheInstances { - delete(currentCheInstances, k) - } - - managerName := "che" - ns := "default" - - ctx := test.NewCtxBuilder().Build() - scheme := ctx.ClusterAPI.Scheme - cl := ctx.ClusterAPI.Client - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - // there is nothing in our context, so the map should still be empty - managers := GetCurrentCheClusterInstances() - if len(managers) != 0 { - t.Fatalf("There should have been no managers after a reconcile of a non-existent manager.") - } - - // now add some manager and reconcile a non-existent one - cl.Create(context.TODO(), &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: managerName + "-not-me", - Namespace: ns, - Finalizers: []string{FinalizerName}, - }, - Spec: chev2.CheClusterSpec{ - Networking: chev2.CheClusterSpecNetworking{ - Hostname: "over.the.rainbow", - }, - }, - Status: chev2.CheClusterStatus{ - WorkspaceBaseDomain: "down.on.earth", - }, - }) - - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - managers = GetCurrentCheClusterInstances() - if len(managers) != 0 { - t.Fatalf("There should have been no managers after a reconcile of a non-existent manager.") - } -} - -func TestAddsCustomResourceToSharedMapOnCreate(t *testing.T) { - infrastructure.InitializeForTesting(infrastructure.Kubernetes) - - // clear the map before the test - for k := range currentCheInstances { - delete(currentCheInstances, k) - } - - managerName := "che" - ns := "default" - ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: managerName, - Namespace: ns, - Finalizers: []string{FinalizerName}, - }, - Spec: chev2.CheClusterSpec{ - Networking: chev2.CheClusterSpecNetworking{ - Hostname: "over.the.rainbow", - Domain: "down.on.earth", - }, - }, - }).Build() - cl := ctx.ClusterAPI.Client - scheme := ctx.ClusterAPI.Scheme - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - managers := GetCurrentCheClusterInstances() - if len(managers) != 1 { - t.Fatalf("There should have been exactly 1 manager after a reconcile but there is %d.", len(managers)) - } - - mgr, ok := managers[types.NamespacedName{Name: managerName, Namespace: ns}] - if !ok { - t.Fatalf("The map of the current managers doesn't contain the expected one.") - } - - if mgr.Name != managerName { - t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName) - } -} - -func TestUpdatesCustomResourceInSharedMapOnUpdate(t *testing.T) { - infrastructure.InitializeForTesting(infrastructure.Kubernetes) - - // clear the map before the test - for k := range currentCheInstances { - delete(currentCheInstances, k) - } - - managerName := "che" - ns := "default" - - ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: managerName, - Namespace: ns, - Finalizers: []string{FinalizerName}, - }, - Spec: chev2.CheClusterSpec{ - Networking: chev2.CheClusterSpecNetworking{ - Hostname: "over.the.rainbow", - Domain: "down.on.earth", - }, - }, - }).Build() - scheme := ctx.ClusterAPI.Scheme - cl := ctx.ClusterAPI.Client - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - managers := GetCurrentCheClusterInstances() - if len(managers) != 1 { - t.Fatalf("There should have been exactly 1 manager after a reconcile but there is %d.", len(managers)) - } - - mgr, ok := managers[types.NamespacedName{Name: managerName, Namespace: ns}] - if !ok { - t.Fatalf("The map of the current managers doesn't contain the expected one.") - } - - if mgr.Name != managerName { - t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName) - } - - if mgr.GetCheHost() != "over.the.rainbow" { - t.Fatalf("Unexpected host value: expected: over.the.rainbow, actual: %s", mgr.GetCheHost()) - } - - // now update the manager and reconcile again. See that the map contains the updated value - mgrInCluster := chev2.CheCluster{} - cl.Get(context.TODO(), client.ObjectKey{Name: managerName, Namespace: ns}, &mgrInCluster) - - // to be able to update, we need to set the resource version - mgr.SetResourceVersion(mgrInCluster.GetResourceVersion()) - - mgr.Spec.Networking.Hostname = "over.the.shoulder" - err = cl.Update(context.TODO(), &mgr) - if err != nil { - t.Fatalf("Failed to update. Wat? %s", err) - } - - // before the reconcile, the map still should containe the old value - managers = GetCurrentCheClusterInstances() - mgr, ok = managers[types.NamespacedName{Name: managerName, Namespace: ns}] - if !ok { - t.Fatalf("The map of the current managers doesn't contain the expected one.") - } - - if mgr.Name != managerName { - t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName) - } - - if mgr.Spec.Networking.Hostname != "over.the.rainbow" { - t.Fatalf("Unexpected host value: expected: over.the.rainbow, actual: %s", mgr.Spec.Networking.Hostname) - } - - // now reconcile and see that the value in the map is now updated - - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - managers = GetCurrentCheClusterInstances() - mgr, ok = managers[types.NamespacedName{Name: managerName, Namespace: ns}] - if !ok { - t.Fatalf("The map of the current managers doesn't contain the expected one.") - } - - if mgr.Name != managerName { - t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName) - } - - if mgr.Spec.Networking.Hostname != "over.the.shoulder" { - t.Fatalf("Unexpected host value: expected: over.the.shoulder, actual: %s", mgr.Spec.Networking.Hostname) - } -} - -func TestRemovesCustomResourceFromSharedMapOnDelete(t *testing.T) { - infrastructure.InitializeForTesting(infrastructure.Kubernetes) - - // clear the map before the test - for k := range currentCheInstances { - delete(currentCheInstances, k) - } - - managerName := "che" - ns := "default" - - ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: managerName, - Namespace: ns, - Finalizers: []string{FinalizerName}, - }, - Spec: chev2.CheClusterSpec{ - Networking: chev2.CheClusterSpecNetworking{ - Hostname: "over.the.rainbow", - Domain: "down.on.earth", - }, - }, - }).Build() - cl := ctx.ClusterAPI.Client - scheme := ctx.ClusterAPI.Scheme - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - managers := GetCurrentCheClusterInstances() - if len(managers) != 1 { - t.Fatalf("There should have been exactly 1 manager after a reconcile but there is %d.", len(managers)) - } - - mgr, ok := managers[types.NamespacedName{Name: managerName, Namespace: ns}] - if !ok { - t.Fatalf("The map of the current managers doesn't contain the expected one.") - } - - if mgr.Name != managerName { - t.Fatalf("Found a manager that we didn't reconcile. Curious (and buggy). We found %s but should have found %s", mgr.Name, managerName) - } - - cl.Delete(context.TODO(), &mgr) - - // now reconcile and see that the value is no longer in the map - - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - managers = GetCurrentCheClusterInstances() - _, ok = managers[types.NamespacedName{Name: managerName, Namespace: ns}] - if ok { - t.Fatalf("The map of the current managers should no longer contain the manager after it has been deleted.") - } -} - -func TestCustomResourceFinalization(t *testing.T) { - infrastructure.InitializeForTesting(infrastructure.Kubernetes) - - managerName := "che" - ns := "default" - - ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: managerName, - Namespace: ns, - Finalizers: []string{FinalizerName}, - }, - Spec: chev2.CheClusterSpec{ - Networking: chev2.CheClusterSpecNetworking{ - Hostname: "over.the.rainbow", - Domain: "down.on.earth", - }, - }, - }).WithObjects(&corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ws1", - Namespace: ns, - Annotations: map[string]string{ - devworkspacedefaults.ConfigAnnotationCheManagerName: managerName, - devworkspacedefaults.ConfigAnnotationCheManagerNamespace: ns, - }, - Labels: devworkspacedefaults.GetLabelsFromNames(managerName, "gateway-config"), - }, - }).Build() - cl := ctx.ClusterAPI.Client - scheme := ctx.ClusterAPI.Scheme - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - assert.NoError(t, err) - - // check that the reconcile loop added the finalizer - manager := chev2.CheCluster{} - err = cl.Get(context.TODO(), client.ObjectKey{Name: managerName, Namespace: ns}, &manager) - assert.NoError(t, err) - assert.Equal(t, 1, len(manager.Finalizers)) - assert.Equal(t, FinalizerName, manager.Finalizers[0]) - - // try to delete the manager and check that the configmap disallows that and that the status of the manager is updated - err = cl.Delete(context.TODO(), &manager) - assert.NoError(t, err) - - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - assert.NoError(t, err) - - manager = chev2.CheCluster{} - err = cl.Get(context.TODO(), client.ObjectKey{Name: managerName, Namespace: ns}, &manager) - assert.NoError(t, err) - assert.Equal(t, 1, len(manager.Finalizers)) - assert.Equal(t, chev2.ClusterPhasePendingDeletion, string(manager.Status.ChePhase)) - assert.NotEqual(t, 0, len(manager.Status.Message)) - - // now remove the config map and check that the finalization proceeds - err = cl.Delete(context.TODO(), &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "ws1", - Namespace: ns, - }, - }) - assert.NoError(t, err) - - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: managerName, Namespace: ns}}) - assert.NoError(t, err) - - manager = chev2.CheCluster{} - err = cl.Get(context.TODO(), client.ObjectKey{Name: managerName, Namespace: ns}, &manager) - assert.Error(t, err) - assert.True(t, errors.IsNotFound(err)) -} - -// This test should be removed if we are again in charge of gateway creation. -func TestExternalGatewayDetection(t *testing.T) { - origFlavor := defaults.GetCheFlavor() - t.Cleanup(func() { - os.Setenv("CHE_FLAVOR", origFlavor) - }) - - os.Setenv("CHE_FLAVOR", "test-che") - - clusterName := "eclipse-che" - ns := "default" - - cluster := &chev2.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: clusterName, - Namespace: ns, - }, - Status: chev2.CheClusterStatus{ - WorkspaceBaseDomain: "down.on.earth", - CheURL: "https://host", - }, - } - - onKubernetes(func() { - ctx := test.NewCtxBuilder().WithCheCluster(cluster).Build() - cl := ctx.ClusterAPI.Client - scheme := ctx.ClusterAPI.Scheme - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - // first reconcile sets the finalizer, second reconcile actually finishes the process - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - persisted := chev2.CheCluster{} - if err := cl.Get(context.TODO(), types.NamespacedName{Name: clusterName, Namespace: ns}, &persisted); err != nil { - t.Fatal(err) - } - - if persisted.GetCheHost() != "host" { - t.Fatalf("Unexpected gateway host: %v", persisted.GetCheHost()) - } - }) - - onOpenShift(func() { - ctx := test.NewCtxBuilder().WithCheCluster(cluster).Build() - cl := ctx.ClusterAPI.Client - scheme := ctx.ClusterAPI.Scheme - - reconciler := CheClusterReconciler{client: cl, scheme: scheme} - - // first reconcile sets the finalizer, second reconcile actually finishes the process - _, err := reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - _, err = reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName, Namespace: ns}}) - if err != nil { - t.Fatalf("Failed to reconcile che manager with error: %s", err) - } - - persisted := chev2.CheCluster{} - if err := cl.Get(context.TODO(), types.NamespacedName{Name: clusterName, Namespace: ns}, &persisted); err != nil { - t.Fatal(err) - } - - if persisted.GetCheHost() != "host" { - t.Fatalf("Unexpected gateway host: %v", persisted.GetCheHost()) - } - }) -} - -func onKubernetes(f func()) { - infrastructure.InitializeForTesting(infrastructure.Kubernetes) - f() -} - -func onOpenShift(f func()) { - infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) - f() -} diff --git a/controllers/devworkspace/defaults/defaults.go b/controllers/devworkspace/defaults/defaults.go index c0baccefc8..753727214b 100644 --- a/controllers/devworkspace/defaults/defaults.go +++ b/controllers/devworkspace/defaults/defaults.go @@ -19,8 +19,6 @@ import ( const ( configAnnotationPrefix = "che.routing.controller.devfile.io/" - ConfigAnnotationCheManagerName = configAnnotationPrefix + "che-name" - ConfigAnnotationCheManagerNamespace = configAnnotationPrefix + "che-namespace" ConfigAnnotationDevWorkspaceRoutingName = configAnnotationPrefix + "devworkspacerouting-name" ConfigAnnotationDevWorkspaceRoutingNamespace = configAnnotationPrefix + "devworkspacerouting-namespace" ConfigAnnotationEndpointName = configAnnotationPrefix + "endpoint-name" diff --git a/controllers/devworkspace/init_test.go b/controllers/devworkspace/init_test.go deleted file mode 100644 index 03dd65c1ff..0000000000 --- a/controllers/devworkspace/init_test.go +++ /dev/null @@ -1,26 +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 -// - -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") -} diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index c2e137ea79..b2ea6d01be 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -102,10 +102,6 @@ func (c *CheRoutingSolver) provisionServices(objs *solvers.RoutingObjects, cheCl } objs.Services = append(objs.Services, *commonService) - annos := map[string]string{} - annos[dwdefaults.ConfigAnnotationCheManagerName] = cheCluster.Name - annos[dwdefaults.ConfigAnnotationCheManagerNamespace] = cheCluster.Namespace - additionalLabels := dwdefaults.GetLabelsForComponent(cheCluster, "exposure") for i := range objs.Services { @@ -126,13 +122,6 @@ func (c *CheRoutingSolver) provisionServices(objs *solvers.RoutingObjects, cheCl if s.Annotations == nil { s.Annotations = map[string]string{} } - - for k, v := range annos { - - if len(s.Annotations[k]) == 0 { - s.Annotations[k] = v - } - } } return nil @@ -219,15 +208,9 @@ func (c *CheRoutingSolver) provisionPodAdditions(objs *solvers.RoutingObjects, c return nil } -func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, workspaceID string, componentEndpoints map[string]dwo.EndpointList, routingObj solvers.RoutingObjects) (exposedEndpoints map[string]dwo.ExposedEndpointList, ready bool, err error) { - if cheCluster.Status.GatewayPhase == chev2.GatewayPhaseInitializing { - return nil, false, nil - } - +func (c *CheRoutingSolver) cheExposedEndpoints(gatewayHost string, workspaceID string, componentEndpoints map[string]dwo.EndpointList, routingObj solvers.RoutingObjects) (exposedEndpoints map[string]dwo.ExposedEndpointList, ready bool, err error) { exposedEndpoints = map[string]dwo.ExposedEndpointList{} - gatewayHost := cheCluster.GetCheHost() - endpointStrategy := getEndpointPathStrategy(c.client, workspaceID, routingObj.Services[0].Namespace, routingObj.Services[0].ObjectMeta.OwnerReferences[0].Name) for component, endpoints := range componentEndpoints { @@ -786,44 +769,42 @@ func findRouteForEndpoint(componentName string, endpoint dwo.Endpoint, objs *sol return nil } -func (c *CheRoutingSolver) cheRoutingFinalize(cheManager *chev2.CheCluster, routing *dwo.DevWorkspaceRouting) error { - selector, err := labels.Parse(fmt.Sprintf("%s=%s", dwconstants.DevWorkspaceIDLabel, routing.Spec.DevWorkspaceId)) +func (c *CheRoutingSolver) cheRoutingFinalize(routing *dwo.DevWorkspaceRouting) error { + selector := labels.SelectorFromSet( + labels.Set{ + dwconstants.DevWorkspaceIDLabel: routing.Spec.DevWorkspaceId, + }, + ) + + err := c.client.DeleteAllOf( + context.TODO(), + &corev1.ConfigMap{}, + &client.DeleteAllOfOptions{ + ListOptions: client.ListOptions{Namespace: routing.Namespace, LabelSelector: selector}, + }, + ) if err != nil { return err } - // delete configs from che namespace - if deleteErr := c.deleteConfigs(&client.ListOptions{ - Namespace: cheManager.Namespace, - LabelSelector: selector, - }); deleteErr != nil { - return deleteErr - } - - // delete configs from workspace namespace - if deleteErr := c.deleteConfigs(&client.ListOptions{ - Namespace: routing.Namespace, - LabelSelector: selector, - }); deleteErr != nil { - return deleteErr - } - - return nil -} - -func (c *CheRoutingSolver) deleteConfigs(listOpts *client.ListOptions) error { - configs := &corev1.ConfigMapList{} - err := c.client.List(context.TODO(), configs, listOpts) + cheCluster, err := deploy.FindCheClusterCRInNamespace(c.client, "") if err != nil { return err } - for _, cm := range configs.Items { - err = c.client.Delete(context.TODO(), &cm) + if cheCluster != nil { + err = c.client.DeleteAllOf( + context.TODO(), + &corev1.ConfigMap{}, + &client.DeleteAllOfOptions{ + ListOptions: client.ListOptions{Namespace: cheCluster.Namespace, LabelSelector: selector}, + }, + ) if err != nil { return err } } + return nil } diff --git a/controllers/devworkspace/solver/che_routing_external_tls_config_test.go b/controllers/devworkspace/solver/che_routing_external_tls_config_test.go index 54a44e8f72..9befc629c7 100644 --- a/controllers/devworkspace/solver/che_routing_external_tls_config_test.go +++ b/controllers/devworkspace/solver/che_routing_external_tls_config_test.go @@ -17,7 +17,6 @@ import ( "github.com/devfile/devworkspace-operator/pkg/infrastructure" chev2 "github.com/eclipse-che/che-operator/api/v2" - controller "github.com/eclipse-che/che-operator/controllers/devworkspace" routev1 "github.com/openshift/api/route/v1" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -30,9 +29,8 @@ func TestExternalTLSConfigForIngresses(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -104,9 +102,8 @@ func TestExternalTLSConfigForRoutes(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ diff --git a/controllers/devworkspace/solver/che_routing_test.go b/controllers/devworkspace/solver/che_routing_test.go index 3733c3489a..9e74926bd8 100644 --- a/controllers/devworkspace/solver/che_routing_test.go +++ b/controllers/devworkspace/solver/che_routing_test.go @@ -18,18 +18,15 @@ import ( "strings" "testing" - "github.com/stretchr/testify/assert" - dwo "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" "github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers" "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/stretchr/testify/assert" dwCommon "github.com/devfile/devworkspace-operator/pkg/common" dwConstants "github.com/devfile/devworkspace-operator/pkg/constants" "github.com/devfile/devworkspace-operator/pkg/infrastructure" chev2 "github.com/eclipse-che/che-operator/api/v2" - controller "github.com/eclipse-che/che-operator/controllers/devworkspace" - "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" "github.com/eclipse-che/che-operator/pkg/common/constants" "github.com/eclipse-che/che-operator/pkg/deploy/gateway" corev1 "k8s.io/api/core/v1" @@ -39,7 +36,6 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/yaml" ) @@ -48,12 +44,7 @@ var ( ) func getSpecObjectsForManager(t *testing.T, mgr *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, additionalInitialObjects ...client.Object) (client.Client, solvers.RoutingSolver, solvers.RoutingObjects) { - allObjs := []client.Object{mgr, routing} - for i := range additionalInitialObjects { - allObjs = append(allObjs, additionalInitialObjects[i]) - } - - ctx := test.NewCtxBuilder().WithObjects(allObjs...).Build() + ctx := test.NewCtxBuilder().WithCheCluster(mgr).WithObjects(routing).WithObjects(additionalInitialObjects...).Build() scheme := ctx.ClusterAPI.Scheme cl := ctx.ClusterAPI.Client @@ -68,13 +59,6 @@ func getSpecObjectsForManager(t *testing.T, mgr *chev2.CheCluster, routing *dwo. PodSelector: routing.Spec.PodSelector, } - // we need to do 1 round of che manager reconciliation so that the solver gets initialized - cheRecon := controller.New(cl, scheme) - _, err = cheRecon.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: mgr.Name, Namespace: mgr.Namespace}}) - if err != nil { - t.Fatal(err) - } - objs, err := solver.GetSpecObjects(routing, meta) if err != nil { t.Fatal(err) @@ -100,18 +84,14 @@ func getSpecObjectsForManager(t *testing.T, mgr *chev2.CheCluster, routing *dwo. } } - // now we need a second round of che manager reconciliation so that it proclaims the che gateway as established - cheRecon.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "che", Namespace: "ns"}}) - return cl, solver, objs } func getSpecObjects(t *testing.T, routing *dwo.DevWorkspaceRouting) (client.Client, solvers.RoutingSolver, solvers.RoutingObjects) { return getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -327,9 +307,8 @@ func relocatableDevWorkspaceRouting() *dwo.DevWorkspaceRouting { func userProfileSecret(username string) *corev1.Secret { return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "user-profile", - Namespace: "ws", - Finalizers: []string{controller.FinalizerName}, + Name: "user-profile", + Namespace: "ws", }, Data: map[string][]byte{ "name": []byte(username), @@ -380,14 +359,6 @@ func TestCreateRelocatedObjectsK8S(t *testing.T) { for i := range objs.Services { t.Run(fmt.Sprintf("service-%d", i), func(t *testing.T) { svc := &objs.Services[i] - if svc.Annotations[defaults.ConfigAnnotationCheManagerName] != "che" { - t.Errorf("The name of the associated che manager should have been recorded in the service annotation") - } - - if svc.Annotations[defaults.ConfigAnnotationCheManagerNamespace] != "ns" { - t.Errorf("The namespace of the associated che manager should have been recorded in the service annotation") - } - if svc.Labels[dwConstants.DevWorkspaceIDLabel] != "wsid" { t.Errorf("The workspace ID should be recorded in the service labels") } @@ -507,9 +478,8 @@ func TestCreateRelocatedObjectsK8SLegacy(t *testing.T) { cl, _, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -751,9 +721,8 @@ func TestCreateRelocatedObjectsOpenshiftLegacy(t *testing.T) { cl, _, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -979,9 +948,8 @@ func TestUniqueMainEndpointLegacy(t *testing.T) { cl, _, _ := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1053,14 +1021,6 @@ func TestCreateSubDomainObjects(t *testing.T) { for i := range objs.Services { t.Run(fmt.Sprintf("service-%d", i), func(t *testing.T) { svc := &objs.Services[i] - if svc.Annotations[defaults.ConfigAnnotationCheManagerName] != "che" { - t.Errorf("The name of the associated che manager should have been recorded in the service annotation") - } - - if svc.Annotations[defaults.ConfigAnnotationCheManagerNamespace] != "ns" { - t.Errorf("The namespace of the associated che manager should have been recorded in the service annotation") - } - if svc.Labels[dwConstants.DevWorkspaceIDLabel] != "wsid" { t.Errorf("The workspace ID should be recorded in the service labels") } @@ -1112,9 +1072,8 @@ func TestCreateSubDomainObjectsLegacy(t *testing.T) { cl, _, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1136,14 +1095,6 @@ func TestCreateSubDomainObjectsLegacy(t *testing.T) { for i := range objs.Services { t.Run(fmt.Sprintf("service-%d", i), func(t *testing.T) { svc := &objs.Services[i] - if svc.Annotations[defaults.ConfigAnnotationCheManagerName] != "che" { - t.Errorf("The name of the associated che manager should have been recorded in the service annotation") - } - - if svc.Annotations[defaults.ConfigAnnotationCheManagerNamespace] != "ns" { - t.Errorf("The namespace of the associated che manager should have been recorded in the service annotation") - } - if svc.Labels[dwConstants.DevWorkspaceIDLabel] != "wsid" { t.Errorf("The workspace ID should be recorded in the service labels") } @@ -1250,9 +1201,8 @@ func TestReportRelocatableExposedEndpointsLegacy(t *testing.T) { routing := relocatableDevWorkspaceRouting() _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1513,9 +1463,8 @@ func TestExposeEndpointsLegacy(t *testing.T) { _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1614,9 +1563,8 @@ func TestReportSubdomainExposedEndpointsLongUsername(t *testing.T) { routing := subdomainDevWorkspaceRouting() _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1681,9 +1629,8 @@ func TestReportSubdomainExposedEndpointsLegacy(t *testing.T) { routing := subdomainDevWorkspaceRouting() _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1789,9 +1736,8 @@ func TestUsesIngressAnnotationsForWorkspaceEndpointIngresses(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1826,9 +1772,8 @@ func TestUsesEndpointAnnotationsForWorkspaceEndpointIngresses(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1874,9 +1819,8 @@ func TestUsesEndpointAnnotationsForWorkspaceEndpointRoutes(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1918,9 +1862,8 @@ func TestUsesEndpointServiceWithDiscoverableAttributeSetRoutes(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -1962,9 +1905,8 @@ func TestUsesEndpointServiceWithDiscoverableAttributeSetIngresses(t *testing.T) mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -2014,9 +1956,8 @@ func TestUsesCustomCertificateForWorkspaceEndpointIngresses(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ @@ -2098,9 +2039,8 @@ func TestUsesCustomCertificateForWorkspaceEndpointRoutes(t *testing.T) { mgr := &chev2.CheCluster{ ObjectMeta: metav1.ObjectMeta{ - Name: "che", - Namespace: "ns", - Finalizers: []string{controller.FinalizerName}, + Name: "che", + Namespace: "ns", }, Spec: chev2.CheClusterSpec{ Networking: chev2.CheClusterSpecNetworking{ diff --git a/controllers/devworkspace/solver/solver.go b/controllers/devworkspace/solver/solver.go index 37d81b8cbf..a57fe64e12 100644 --- a/controllers/devworkspace/solver/solver.go +++ b/controllers/devworkspace/solver/solver.go @@ -14,16 +14,15 @@ package solver import ( "context" - "fmt" "time" "github.com/devfile/devworkspace-operator/pkg/constants" k8sclient "github.com/eclipse-che/che-operator/pkg/common/k8s-client" + "github.com/eclipse-che/che-operator/pkg/deploy" controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" "github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers" chev2 "github.com/eclipse-che/che-operator/api/v2" - controller "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" @@ -123,22 +122,17 @@ func (c *CheRoutingSolver) FinalizerRequired(routing *controllerv1alpha1.DevWork } func (c *CheRoutingSolver) Finalize(routing *controllerv1alpha1.DevWorkspaceRouting) error { - cheManager, err := cheManagerOfRouting(routing) - if err != nil { - return err - } - - return c.cheRoutingFinalize(cheManager, routing) + return c.cheRoutingFinalize(routing) } // GetSpecObjects constructs cluster routing objects which should be applied on the cluster func (c *CheRoutingSolver) GetSpecObjects(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta solvers.DevWorkspaceMetadata) (solvers.RoutingObjects, error) { - cheManager, err := cheManagerOfRouting(routing) + cheCluster, err := c.getCheClusterReady() if err != nil { return solvers.RoutingObjects{}, err } - return c.cheSpecObjects(cheManager, routing, workspaceMeta) + return c.cheSpecObjects(cheCluster, routing, workspaceMeta) } // GetExposedEndpoints retreives the URL for each endpoint in a devfile spec from a set of RoutingObjects. @@ -150,51 +144,35 @@ func (c *CheRoutingSolver) GetExposedEndpoints(endpoints map[string]controllerv1 return map[string]controllerv1alpha1.ExposedEndpointList{}, true, nil } - managerName := routingObj.Services[0].Annotations[defaults.ConfigAnnotationCheManagerName] - managerNamespace := routingObj.Services[0].Annotations[defaults.ConfigAnnotationCheManagerNamespace] workspaceID := routingObj.Services[0].Labels[constants.DevWorkspaceIDLabel] - manager, err := findCheManager(client.ObjectKey{Name: managerName, Namespace: managerNamespace}) + cheCluster, err := c.getCheClusterReady() if err != nil { return nil, false, err } - return c.cheExposedEndpoints(manager, workspaceID, endpoints, routingObj) + return c.cheExposedEndpoints(cheCluster.GetCheHost(), workspaceID, endpoints, routingObj) } func isSupported(routingClass controllerv1alpha1.DevWorkspaceRoutingClass) bool { return routingClass == "che" } -func cheManagerOfRouting(routing *controllerv1alpha1.DevWorkspaceRouting) (*chev2.CheCluster, error) { - cheName := routing.Annotations[defaults.ConfigAnnotationCheManagerName] - cheNamespace := routing.Annotations[defaults.ConfigAnnotationCheManagerNamespace] - - return findCheManager(client.ObjectKey{Name: cheName, Namespace: cheNamespace}) -} - -func findCheManager(cheManagerKey client.ObjectKey) (*chev2.CheCluster, error) { - managers := controller.GetCurrentCheClusterInstances() - if len(managers) == 0 { - // the CheManager has not been reconciled yet, so let's wait a bit - return &chev2.CheCluster{}, &solvers.RoutingNotReady{Retry: 1 * time.Second} +func (c *CheRoutingSolver) getCheClusterReady() (*chev2.CheCluster, error) { + cheCluster, err := deploy.FindCheClusterCRInNamespace(c.client, "") + if err != nil { + return nil, err } - if len(cheManagerKey.Name) == 0 { - if len(managers) > 1 { - return &chev2.CheCluster{}, &solvers.RoutingInvalid{Reason: fmt.Sprintf("the routing does not specify any Che manager in its configuration but there are %d Che managers in the cluster", len(managers))} - } - for _, m := range managers { - return &m, nil - } - + if cheCluster == nil { + // Not found, let's wait + return nil, &solvers.RoutingNotReady{Retry: 10 * time.Second} } - if m, ok := managers[cheManagerKey]; ok { - return &m, nil + if cheCluster.Status.CheURL == "" || cheCluster.Status.WorkspaceBaseDomain == "" { + // Required status fields are not set + return nil, &solvers.RoutingNotReady{Retry: 5 * time.Second} } - logger.Info("Routing requires a non-existing che manager. Retrying in 10 seconds.", "key", cheManagerKey) - - return &chev2.CheCluster{}, &solvers.RoutingNotReady{Retry: 10 * time.Second} + return cheCluster, nil } diff --git a/controllers/usernamespace/usernamespace_controller_test.go b/controllers/usernamespace/usernamespace_controller_test.go index d9b2b26a64..5ddd7de2f2 100644 --- a/controllers/usernamespace/usernamespace_controller_test.go +++ b/controllers/usernamespace/usernamespace_controller_test.go @@ -29,7 +29,6 @@ import ( dwconstants "github.com/devfile/devworkspace-operator/pkg/constants" "github.com/devfile/devworkspace-operator/pkg/infrastructure" chev2 "github.com/eclipse-che/che-operator/api/v2" - "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/pkg/common/constants" "github.com/eclipse-che/che-operator/pkg/deploy/tls" configv1 "github.com/openshift/api/config/v1" @@ -153,18 +152,9 @@ func setupCheCluster(t *testing.T, ctx context.Context, cl client.Client, scheme t.Fatal(err) } - r := devworkspace.New(cl, scheme) - // the reconciliation needs to run twice for it to be truly finished - we're setting up finalizers etc... - if _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: cheName, Namespace: cheNamespaceName}}); err != nil { - t.Fatal(err) - } - if _, err := r.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: cheName, Namespace: cheNamespaceName}}); err != nil { - t.Fatal(err) - } } func setup(infraType infrastructure.Type, objs ...client.Object) (*runtime.Scheme, client.Client, *CheUserNamespaceReconciler) { - devworkspace.CleanCheClusterInstancesForTest() infrastructure.InitializeForTesting(infraType) ctx := test.NewCtxBuilder().WithObjects(objs...).WithCheCluster(nil).Build() @@ -400,16 +390,7 @@ func TestUpdateSccClusterRoleBinding(t *testing.T) { } allObjs := []client.Object{ns1, pr1, cheCluster} - scheme, cl, usernamespaceReconciler := setup(infrastructure.OpenShiftv4, allObjs...) - - // the reconciliation needs to run twice for it to be truly finished - we're setting up finalizers etc... - devworkspaceReconciler := devworkspace.New(cl, scheme) - if _, err := devworkspaceReconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "eclipse-che", Namespace: "eclipse-che"}}); err != nil { - t.Fatal(err) - } - if _, err := devworkspaceReconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: "eclipse-che", Namespace: "eclipse-che"}}); err != nil { - t.Fatal(err) - } + _, cl, usernamespaceReconciler := setup(infrastructure.OpenShiftv4, allObjs...) _, err := usernamespaceReconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: types.NamespacedName{Name: ns1.GetName()}}) assert.Nil(t, err) diff --git a/pkg/common/test/deploy_context.go b/pkg/common/test/deploy_context.go index 5dcb3cd8b8..75ed051159 100644 --- a/pkg/common/test/deploy_context.go +++ b/pkg/common/test/deploy_context.go @@ -42,11 +42,17 @@ func (f *DeployContextBuild) WithObjects(initObjs ...client.Object) *DeployConte func (f *DeployContextBuild) WithCheCluster(cheCluster *chev2.CheCluster) *DeployContextBuild { f.cheCluster = cheCluster - if f.cheCluster != nil && f.cheCluster.TypeMeta.Kind == "" { + if f.cheCluster != nil { f.cheCluster.TypeMeta = metav1.TypeMeta{ Kind: "CheCluster", APIVersion: chev2.GroupVersion.String(), } + if f.cheCluster.Status.WorkspaceBaseDomain == "" { + f.cheCluster.Status.WorkspaceBaseDomain = f.cheCluster.Spec.Networking.Domain + } + if f.cheCluster.Status.CheURL == "" { + f.cheCluster.Status.CheURL = "https://" + f.cheCluster.Spec.Networking.Hostname + } } return f } diff --git a/pkg/deploy/server/basedomain_reconciler.go b/pkg/deploy/server/basedomain_reconciler.go new file mode 100644 index 0000000000..cc602667d0 --- /dev/null +++ b/pkg/deploy/server/basedomain_reconciler.go @@ -0,0 +1,105 @@ +// +// 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 server + +import ( + "context" + "fmt" + "strings" + + "github.com/devfile/devworkspace-operator/pkg/infrastructure" + "github.com/eclipse-che/che-operator/pkg/common/chetypes" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" + "github.com/eclipse-che/che-operator/pkg/common/reconciler" + "github.com/eclipse-che/che-operator/pkg/common/utils" + "github.com/eclipse-che/che-operator/pkg/deploy" + routev1 "github.com/openshift/api/route/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type BaseDomainReconciler struct { + reconciler.Reconcilable +} + +func NewBaseDomainReconciler() *BaseDomainReconciler { + return &BaseDomainReconciler{} +} + +func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { + baseDomain := utils.GetValue( + ctx.CheCluster.Spec.Components.CheServer.ExtraProperties["CHE_INFRA_OPENSHIFT_ROUTE_HOST_DOMAIN__SUFFIX"], + ctx.CheCluster.Spec.Networking.Domain, // must be set for Kubernetes, see CheClusterValidator + ) + + if baseDomain == "" { + if infrastructure.IsOpenShift() { + openshiftBaseDomain, err := r.detectOpenShiftRouteBaseDomain(ctx) + if err != nil { + return reconcile.Result{}, false, err + } + + baseDomain = openshiftBaseDomain + } + } + + if baseDomain == "" { + return reconcile.Result{}, false, fmt.Errorf("unable to detect base domain") + } + + if ctx.CheCluster.Status.WorkspaceBaseDomain != baseDomain { + ctx.CheCluster.Status.WorkspaceBaseDomain = baseDomain + if err := deploy.UpdateCheCRStatus(ctx, "WorkspaceBaseDomain", baseDomain); err != nil { + return reconcile.Result{}, false, err + } + } + + return reconcile.Result{}, true, nil +} + +func (r *BaseDomainReconciler) Finalize(ctx *chetypes.DeployContext) bool { + return true +} + +// Tries to autodetect the route base domain. +func (r *BaseDomainReconciler) detectOpenShiftRouteBaseDomain(ctx *chetypes.DeployContext) (string, error) { + name := "devworkspace-che-test" + testRoute := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ctx.CheCluster.Namespace, + Name: name, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: name, + }, + }, + } + + if err := ctx.ClusterAPI.ClientWrapper.Create(context.TODO(), testRoute); err != nil { + return "", err + } + + defer func() { + _ = ctx.ClusterAPI.ClientWrapper.DeleteByKeyIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: testRoute.Name, Namespace: testRoute.Namespace}, + &routev1.Route{}) + }() + + items := strings.SplitAfterN(testRoute.Spec.Host, ".", 2) + return items[1], nil +} From 024e1e76f3dcc94776314520fd75047668d26a26 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Tue, 10 Mar 2026 12:54:01 +0100 Subject: [PATCH 2/7] fixes Signed-off-by: Anatolii Bazko --- api/v2/zz_generated.deepcopy.go | 2 +- .../manifests/che-operator.clusterserviceversion.yaml | 11 +++++++---- .../manifests/org.eclipse.che_checlusters.yaml | 4 +++- .../bases/che-operator.clusterserviceversion.yaml | 4 +++- deploy/deployment/kubernetes/combined.yaml | 5 ++++- .../kubernetes/objects/che-operator.ClusterRole.yaml | 1 + ...ters.org.eclipse.che.CustomResourceDefinition.yaml | 4 +++- deploy/deployment/openshift/combined.yaml | 5 ++++- .../openshift/objects/che-operator.ClusterRole.yaml | 1 + ...ters.org.eclipse.che.CustomResourceDefinition.yaml | 4 +++- ...ters.org.eclipse.che.CustomResourceDefinition.yaml | 4 +++- .../next/templates/che-operator.ClusterRole.yaml | 1 + 12 files changed, 34 insertions(+), 12 deletions(-) diff --git a/api/v2/zz_generated.deepcopy.go b/api/v2/zz_generated.deepcopy.go index 64fb82e9a2..111b0d42d5 100644 --- a/api/v2/zz_generated.deepcopy.go +++ b/api/v2/zz_generated.deepcopy.go @@ -19,7 +19,7 @@ package v2 import ( "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" ) diff --git a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml index 384244cf23..7badd51245 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-02-26T12:30:32Z" + createdAt: "2026-03-10T11:53:33Z" 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.115.0-954.next + name: eclipse-che.v7.116.0-955.next namespace: placeholder spec: apiservicedefinitions: {} @@ -247,7 +247,9 @@ spec: path: devfileRegistryURL x-descriptors: - urn:alm:descriptor:org.w3:link - - description: Specifies the current phase of the gateway deployment. + - description: 'Deprecated. + + Specifies the current phase of the gateway deployment.' displayName: Gateway phase path: gatewayPhase x-descriptors: @@ -721,6 +723,7 @@ spec: verbs: - create - delete + - deletecollection - get - update - patch @@ -1141,7 +1144,7 @@ spec: name: gateway-authorization-sidecar-k8s - image: quay.io/che-incubator/header-rewrite-proxy:latest name: gateway-header-sidecar - version: 7.115.0-954.next + version: 7.116.0-955.next webhookdefinitions: - admissionReviewVersions: - v1 diff --git a/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml b/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml index f44a38f8d3..89fb5074ea 100644 --- a/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml +++ b/bundle/next/eclipse-che/manifests/org.eclipse.che_checlusters.yaml @@ -11177,7 +11177,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why diff --git a/config/manifests/bases/che-operator.clusterserviceversion.yaml b/config/manifests/bases/che-operator.clusterserviceversion.yaml index fa27c042db..1af46d044b 100644 --- a/config/manifests/bases/che-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/che-operator.clusterserviceversion.yaml @@ -166,7 +166,9 @@ spec: path: devfileRegistryURL x-descriptors: - urn:alm:descriptor:org.w3:link - - description: Specifies the current phase of the gateway deployment. + - description: |- + Deprecated. + Specifies the current phase of the gateway deployment. displayName: Gateway phase path: gatewayPhase x-descriptors: diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index 7e8e7f7a9a..0635b7a230 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -11143,7 +11143,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why @@ -11413,6 +11415,7 @@ rules: verbs: - create - delete + - deletecollection - get - update - patch diff --git a/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml b/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml index 5736411fcd..cc423e6c81 100644 --- a/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml +++ b/deploy/deployment/kubernetes/objects/che-operator.ClusterRole.yaml @@ -200,6 +200,7 @@ rules: verbs: - create - delete + - deletecollection - get - update - patch diff --git a/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index 102fdcb6b3..8bf11a87d6 100644 --- a/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -11138,7 +11138,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 1c089a9563..cdb19dbbb0 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -11143,7 +11143,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why @@ -11413,6 +11415,7 @@ rules: verbs: - create - delete + - deletecollection - get - update - patch diff --git a/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml b/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml index 5736411fcd..cc423e6c81 100644 --- a/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml +++ b/deploy/deployment/openshift/objects/che-operator.ClusterRole.yaml @@ -200,6 +200,7 @@ rules: verbs: - create - delete + - deletecollection - get - update - patch diff --git a/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index a87f52a934..fdb9eeffcd 100644 --- a/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -11138,7 +11138,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why diff --git a/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml b/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml index 102fdcb6b3..8bf11a87d6 100644 --- a/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml +++ b/helmcharts/next/crds/checlusters.org.eclipse.che.CustomResourceDefinition.yaml @@ -11138,7 +11138,9 @@ spec: description: Deprecated the public URL of the internal devfile registry. type: string gatewayPhase: - description: Specifies the current phase of the gateway deployment. + description: |- + Deprecated. + Specifies the current phase of the gateway deployment. type: string message: description: A human readable message indicating details about why diff --git a/helmcharts/next/templates/che-operator.ClusterRole.yaml b/helmcharts/next/templates/che-operator.ClusterRole.yaml index 5736411fcd..cc423e6c81 100644 --- a/helmcharts/next/templates/che-operator.ClusterRole.yaml +++ b/helmcharts/next/templates/che-operator.ClusterRole.yaml @@ -200,6 +200,7 @@ rules: verbs: - create - delete + - deletecollection - get - update - patch From c86b004308d559626790235454a1153b3c43d29f Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 11 Mar 2026 16:06:35 +0100 Subject: [PATCH 3/7] fixes Signed-off-by: Anatolii Bazko --- pkg/deploy/server/basedomain_reconciler.go | 46 +----- .../server/basedomain_reconciler_test.go | 150 ++++++++++++++++++ pkg/deploy/server/server_reconciler.go | 3 + 3 files changed, 157 insertions(+), 42 deletions(-) create mode 100644 pkg/deploy/server/basedomain_reconciler_test.go diff --git a/pkg/deploy/server/basedomain_reconciler.go b/pkg/deploy/server/basedomain_reconciler.go index cc602667d0..1c4b5c58d0 100644 --- a/pkg/deploy/server/basedomain_reconciler.go +++ b/pkg/deploy/server/basedomain_reconciler.go @@ -13,19 +13,14 @@ package server import ( - "context" "fmt" "strings" "github.com/devfile/devworkspace-operator/pkg/infrastructure" "github.com/eclipse-che/che-operator/pkg/common/chetypes" - defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" "github.com/eclipse-che/che-operator/pkg/common/reconciler" "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/eclipse-che/che-operator/pkg/deploy" - routev1 "github.com/openshift/api/route/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -45,12 +40,11 @@ func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile if baseDomain == "" { if infrastructure.IsOpenShift() { - openshiftBaseDomain, err := r.detectOpenShiftRouteBaseDomain(ctx) - if err != nil { - return reconcile.Result{}, false, err + items := strings.SplitAfterN(ctx.CheHost, ".", 2) + if len(items) != 2 { + return reconcile.Result{}, false, fmt.Errorf("unable to detect base domain") } - - baseDomain = openshiftBaseDomain + baseDomain = items[1] } } @@ -71,35 +65,3 @@ func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile func (r *BaseDomainReconciler) Finalize(ctx *chetypes.DeployContext) bool { return true } - -// Tries to autodetect the route base domain. -func (r *BaseDomainReconciler) detectOpenShiftRouteBaseDomain(ctx *chetypes.DeployContext) (string, error) { - name := "devworkspace-che-test" - testRoute := &routev1.Route{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: ctx.CheCluster.Namespace, - Name: name, - Labels: deploy.GetLabels(defaults.GetCheFlavor()), - }, - Spec: routev1.RouteSpec{ - To: routev1.RouteTargetReference{ - Kind: "Service", - Name: name, - }, - }, - } - - if err := ctx.ClusterAPI.ClientWrapper.Create(context.TODO(), testRoute); err != nil { - return "", err - } - - defer func() { - _ = ctx.ClusterAPI.ClientWrapper.DeleteByKeyIgnoreNotFound( - context.TODO(), - types.NamespacedName{Name: testRoute.Name, Namespace: testRoute.Namespace}, - &routev1.Route{}) - }() - - items := strings.SplitAfterN(testRoute.Spec.Host, ".", 2) - return items[1], nil -} diff --git a/pkg/deploy/server/basedomain_reconciler_test.go b/pkg/deploy/server/basedomain_reconciler_test.go new file mode 100644 index 0000000000..407e6708ff --- /dev/null +++ b/pkg/deploy/server/basedomain_reconciler_test.go @@ -0,0 +1,150 @@ +// +// 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 server + +import ( + "context" + "testing" + + chev2 "github.com/eclipse-che/che-operator/api/v2" + "github.com/eclipse-che/che-operator/pkg/common/test" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestBaseDomainFromNetworkingDomain(t *testing.T) { + ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "my-domain.com", + }, + }, + }).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "my-domain.com", ctx.CheCluster.Status.WorkspaceBaseDomain) +} + +func TestBaseDomainFromExtraProperties(t *testing.T) { + ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "default-domain.com", + }, + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ExtraProperties: map[string]string{ + "CHE_INFRA_OPENSHIFT_ROUTE_HOST_DOMAIN__SUFFIX": "custom-domain.com", + }, + }, + }, + }, + }).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "custom-domain.com", ctx.CheCluster.Status.WorkspaceBaseDomain) +} + +func TestBaseDomainExtraPropertiesOverridesNetworkingDomain(t *testing.T) { + ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "networking-domain.com", + }, + Components: chev2.CheClusterComponents{ + CheServer: chev2.CheServer{ + ExtraProperties: map[string]string{ + "CHE_INFRA_OPENSHIFT_ROUTE_HOST_DOMAIN__SUFFIX": "extra-domain.com", + }, + }, + }, + }, + }).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "extra-domain.com", ctx.CheCluster.Status.WorkspaceBaseDomain) +} + +func TestBaseDomainStatusUpdated(t *testing.T) { + ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "new-domain.com", + }, + }, + Status: chev2.CheClusterStatus{ + WorkspaceBaseDomain: "old-domain.com", + }, + }).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "new-domain.com", ctx.CheCluster.Status.WorkspaceBaseDomain) + + // Verify status was persisted + cheCluster := &chev2.CheCluster{} + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: "eclipse-che", Namespace: "eclipse-che"}, cheCluster) + assert.Nil(t, err) + assert.Equal(t, "new-domain.com", cheCluster.Status.WorkspaceBaseDomain) +} + +func TestBaseDomainFromCheHost(t *testing.T) { + ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Status: chev2.CheClusterStatus{ + CheURL: "https://che.apps.cluster.example.com", + }, + }).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "apps.cluster.example.com", ctx.CheCluster.Status.WorkspaceBaseDomain) +} diff --git a/pkg/deploy/server/server_reconciler.go b/pkg/deploy/server/server_reconciler.go index fe05feeaf4..58f2e847e7 100644 --- a/pkg/deploy/server/server_reconciler.go +++ b/pkg/deploy/server/server_reconciler.go @@ -24,6 +24,7 @@ import ( "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -33,6 +34,8 @@ const ( configMapName = "che" ) +var log = ctrl.Log.WithName("server") + type CheServerReconciler struct { reconciler.Reconcilable } From ea0d3c765f744f495fa7c428c67b8c76595a2fa9 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 11 Mar 2026 17:14:04 +0100 Subject: [PATCH 4/7] fixes Signed-off-by: Anatolii Bazko --- pkg/common/k8s-client/k8s_client.go | 17 ++++ pkg/common/k8s-client/k8s_client_types.go | 3 + pkg/deploy/server/basedomain_reconciler.go | 80 ++++++++++++++++--- .../server/basedomain_reconciler_test.go | 21 +++-- 4 files changed, 104 insertions(+), 17 deletions(-) diff --git a/pkg/common/k8s-client/k8s_client.go b/pkg/common/k8s-client/k8s_client.go index c4777baedc..7d5ca24bc6 100644 --- a/pkg/common/k8s-client/k8s_client.go +++ b/pkg/common/k8s-client/k8s_client.go @@ -92,6 +92,23 @@ func (k K8sClientWrapper) Create( return k.doCreate(ctx, obj, false, opts...) } +func (k K8sClientWrapper) CreateIgnoreIfAlreadyExists( + ctx context.Context, + obj client.Object, + opts ...client.CreateOption, +) error { + defer func() { + // ensure GVK is set (for original object) when function returns + _ = k.ensureGVK(obj) + }() + + if err := k.ensureGVK(obj); err != nil { + return err + } + + return k.doCreate(ctx, obj, true, opts...) +} + func (k K8sClientWrapper) GetIgnoreNotFound( ctx context.Context, key client.ObjectKey, diff --git a/pkg/common/k8s-client/k8s_client_types.go b/pkg/common/k8s-client/k8s_client_types.go index 378193156a..97994b59ba 100644 --- a/pkg/common/k8s-client/k8s_client_types.go +++ b/pkg/common/k8s-client/k8s_client_types.go @@ -28,6 +28,9 @@ type K8sClient interface { // Create creates object. // Returns nil if object is created otherwise returns error. Create(ctx context.Context, blueprint client.Object, opts ...client.CreateOption) error + // CreateIgnoreIfAlreadyExists creates object. + // Returns nil if object is created or already exists otherwise returns error. + CreateIgnoreIfAlreadyExists(ctx context.Context, blueprint client.Object, opts ...client.CreateOption) error // GetIgnoreNotFound gets object. // Returns true if object exists otherwise returns false. // Returns nil if object is retrieved or not found otherwise returns error. diff --git a/pkg/deploy/server/basedomain_reconciler.go b/pkg/deploy/server/basedomain_reconciler.go index 1c4b5c58d0..94c4cc2715 100644 --- a/pkg/deploy/server/basedomain_reconciler.go +++ b/pkg/deploy/server/basedomain_reconciler.go @@ -13,14 +13,19 @@ package server import ( + "context" "fmt" "strings" "github.com/devfile/devworkspace-operator/pkg/infrastructure" "github.com/eclipse-che/che-operator/pkg/common/chetypes" + defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults" "github.com/eclipse-che/che-operator/pkg/common/reconciler" "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/eclipse-che/che-operator/pkg/deploy" + routev1 "github.com/openshift/api/route/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) @@ -33,28 +38,32 @@ func NewBaseDomainReconciler() *BaseDomainReconciler { } func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { - baseDomain := utils.GetValue( + workspaceBaseDomain := utils.GetValue( ctx.CheCluster.Spec.Components.CheServer.ExtraProperties["CHE_INFRA_OPENSHIFT_ROUTE_HOST_DOMAIN__SUFFIX"], ctx.CheCluster.Spec.Networking.Domain, // must be set for Kubernetes, see CheClusterValidator ) - if baseDomain == "" { + if workspaceBaseDomain == "" { if infrastructure.IsOpenShift() { - items := strings.SplitAfterN(ctx.CheHost, ".", 2) - if len(items) != 2 { - return reconcile.Result{}, false, fmt.Errorf("unable to detect base domain") + openshiftBaseDomain, err := r.detectOpenShiftRouteBaseDomain(ctx) + if err != nil { + return reconcile.Result{}, false, err } - baseDomain = items[1] + if openshiftBaseDomain == "" { + return reconcile.Result{}, false, nil + } + + workspaceBaseDomain = openshiftBaseDomain } } - if baseDomain == "" { + if workspaceBaseDomain == "" { return reconcile.Result{}, false, fmt.Errorf("unable to detect base domain") } - if ctx.CheCluster.Status.WorkspaceBaseDomain != baseDomain { - ctx.CheCluster.Status.WorkspaceBaseDomain = baseDomain - if err := deploy.UpdateCheCRStatus(ctx, "WorkspaceBaseDomain", baseDomain); err != nil { + if ctx.CheCluster.Status.WorkspaceBaseDomain != workspaceBaseDomain { + ctx.CheCluster.Status.WorkspaceBaseDomain = workspaceBaseDomain + if err := deploy.UpdateCheCRStatus(ctx, "WorkspaceBaseDomain", workspaceBaseDomain); err != nil { return reconcile.Result{}, false, err } } @@ -65,3 +74,54 @@ func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile func (r *BaseDomainReconciler) Finalize(ctx *chetypes.DeployContext) bool { return true } + +// Tries to autodetect the route base domain. +func (r *BaseDomainReconciler) detectOpenShiftRouteBaseDomain(ctx *chetypes.DeployContext) (string, error) { + name := "devworkspace-che-test" + testRoute := &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ctx.CheCluster.Namespace, + Name: name, + Labels: deploy.GetLabels(defaults.GetCheFlavor()), + }, + Spec: routev1.RouteSpec{ + To: routev1.RouteTargetReference{ + Kind: "Service", + Name: name, + }, + }, + } + + if err := ctx.ClusterAPI.ClientWrapper.CreateIgnoreIfAlreadyExists(context.TODO(), testRoute); err != nil { + return "", err + } + + defer func() { + if err := ctx.ClusterAPI.ClientWrapper.DeleteByKeyIgnoreNotFound( + context.TODO(), + types.NamespacedName{Namespace: testRoute.Namespace, Name: testRoute.Name}, + testRoute, + ); err != nil { + log.Error(err, "unable to delete test route") + } + }() + + // Re-read the route to get the Host field populated by the OpenShift router + existedRoute := &routev1.Route{} + if exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( + context.TODO(), + types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace}, + existedRoute, + ); err != nil { + return "", err + } else if !exists { + return "", nil + } + + items := strings.SplitN(existedRoute.Spec.Host, ".", 2) + if len(items) != 2 { + return "", fmt.Errorf("unable to detect base domain") + } + + return items[1], nil +} diff --git a/pkg/deploy/server/basedomain_reconciler_test.go b/pkg/deploy/server/basedomain_reconciler_test.go index 407e6708ff..a5053eef7b 100644 --- a/pkg/deploy/server/basedomain_reconciler_test.go +++ b/pkg/deploy/server/basedomain_reconciler_test.go @@ -18,6 +18,7 @@ import ( chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/pkg/common/test" + routev1 "github.com/openshift/api/route/v1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -130,21 +131,27 @@ func TestBaseDomainStatusUpdated(t *testing.T) { assert.Equal(t, "new-domain.com", cheCluster.Status.WorkspaceBaseDomain) } -func TestBaseDomainFromCheHost(t *testing.T) { - ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ +func TestBaseDomainFromRoute(t *testing.T) { + route := &routev1.Route{ + TypeMeta: metav1.TypeMeta{ + Kind: "Route", + APIVersion: routev1.GroupVersion.String(), + }, ObjectMeta: metav1.ObjectMeta{ + Name: "devworkspace-che-test", Namespace: "eclipse-che", - Name: "eclipse-che", }, - Status: chev2.CheClusterStatus{ - CheURL: "https://che.apps.cluster.example.com", + Spec: routev1.RouteSpec{ + Host: "devworkspace-che-test.eclipse.org", }, - }).Build() + } + + ctx := test.NewCtxBuilder().WithObjects(route).Build() reconciler := NewBaseDomainReconciler() _, done, err := reconciler.Reconcile(ctx) assert.True(t, done) assert.Nil(t, err) - assert.Equal(t, "apps.cluster.example.com", ctx.CheCluster.Status.WorkspaceBaseDomain) + assert.Equal(t, "eclipse.org", ctx.CheCluster.Status.WorkspaceBaseDomain) } From a17eea75825a409314c1c70dd8add2d05d37868b Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Thu, 12 Mar 2026 09:59:43 +0100 Subject: [PATCH 5/7] fixes Signed-off-by: Anatolii Bazko --- pkg/common/k8s-client/k8s_client.go | 17 --------- pkg/common/k8s-client/k8s_client_types.go | 3 -- pkg/deploy/server/basedomain_reconciler.go | 41 +++++++++++----------- 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/pkg/common/k8s-client/k8s_client.go b/pkg/common/k8s-client/k8s_client.go index 7d5ca24bc6..c4777baedc 100644 --- a/pkg/common/k8s-client/k8s_client.go +++ b/pkg/common/k8s-client/k8s_client.go @@ -92,23 +92,6 @@ func (k K8sClientWrapper) Create( return k.doCreate(ctx, obj, false, opts...) } -func (k K8sClientWrapper) CreateIgnoreIfAlreadyExists( - ctx context.Context, - obj client.Object, - opts ...client.CreateOption, -) error { - defer func() { - // ensure GVK is set (for original object) when function returns - _ = k.ensureGVK(obj) - }() - - if err := k.ensureGVK(obj); err != nil { - return err - } - - return k.doCreate(ctx, obj, true, opts...) -} - func (k K8sClientWrapper) GetIgnoreNotFound( ctx context.Context, key client.ObjectKey, diff --git a/pkg/common/k8s-client/k8s_client_types.go b/pkg/common/k8s-client/k8s_client_types.go index 97994b59ba..378193156a 100644 --- a/pkg/common/k8s-client/k8s_client_types.go +++ b/pkg/common/k8s-client/k8s_client_types.go @@ -28,9 +28,6 @@ type K8sClient interface { // Create creates object. // Returns nil if object is created otherwise returns error. Create(ctx context.Context, blueprint client.Object, opts ...client.CreateOption) error - // CreateIgnoreIfAlreadyExists creates object. - // Returns nil if object is created or already exists otherwise returns error. - CreateIgnoreIfAlreadyExists(ctx context.Context, blueprint client.Object, opts ...client.CreateOption) error // GetIgnoreNotFound gets object. // Returns true if object exists otherwise returns false. // Returns nil if object is retrieved or not found otherwise returns error. diff --git a/pkg/deploy/server/basedomain_reconciler.go b/pkg/deploy/server/basedomain_reconciler.go index 94c4cc2715..2ce885e4fc 100644 --- a/pkg/deploy/server/basedomain_reconciler.go +++ b/pkg/deploy/server/basedomain_reconciler.go @@ -24,6 +24,7 @@ import ( "github.com/eclipse-che/che-operator/pkg/common/utils" "github.com/eclipse-che/che-operator/pkg/deploy" routev1 "github.com/openshift/api/route/v1" + "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" @@ -92,35 +93,33 @@ func (r *BaseDomainReconciler) detectOpenShiftRouteBaseDomain(ctx *chetypes.Depl }, } - if err := ctx.ClusterAPI.ClientWrapper.CreateIgnoreIfAlreadyExists(context.TODO(), testRoute); err != nil { - return "", err + // We don't use ClientWrapper here not to print logs (improve in the future) + if err := ctx.ClusterAPI.Client.Create(context.TODO(), testRoute); err != nil { + if !errors.IsAlreadyExists(err) { + return "", err + } } - defer func() { - if err := ctx.ClusterAPI.ClientWrapper.DeleteByKeyIgnoreNotFound( - context.TODO(), - types.NamespacedName{Namespace: testRoute.Namespace, Name: testRoute.Name}, - testRoute, - ); err != nil { - log.Error(err, "unable to delete test route") + // Re-read the route to get the Host field populated by the OpenShift router + route := &routev1.Route{} + routeKey := types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace} + if err := ctx.ClusterAPI.Client.Get(context.TODO(), routeKey, route); err != nil { + if errors.IsNotFound(err) { + return "", nil } - }() - // Re-read the route to get the Host field populated by the OpenShift router - existedRoute := &routev1.Route{} - if exists, err := ctx.ClusterAPI.ClientWrapper.GetIgnoreNotFound( - context.TODO(), - types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace}, - existedRoute, - ); err != nil { return "", err - } else if !exists { - return "", nil } - items := strings.SplitN(existedRoute.Spec.Host, ".", 2) + defer func() { + if err := ctx.ClusterAPI.Client.Delete(context.TODO(), route); err != nil { + log.Error(err, "unable to delete test route %s", name) + } + }() + + items := strings.SplitN(route.Spec.Host, ".", 2) if len(items) != 2 { - return "", fmt.Errorf("unable to detect base domain") + return "", fmt.Errorf("unable to detect workspace base domain from %s", route.Spec.Host) } return items[1], nil From 1027a87c77a70c9a8f7aab342b885a9f8bd0417a Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Tue, 17 Mar 2026 12:02:29 +0100 Subject: [PATCH 6/7] fixup Signed-off-by: Anatolii Bazko --- pkg/deploy/server/basedomain_reconciler.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/deploy/server/basedomain_reconciler.go b/pkg/deploy/server/basedomain_reconciler.go index 2ce885e4fc..f7bbef37f3 100644 --- a/pkg/deploy/server/basedomain_reconciler.go +++ b/pkg/deploy/server/basedomain_reconciler.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/devfile/devworkspace-operator/pkg/infrastructure" "github.com/eclipse-che/che-operator/pkg/common/chetypes" @@ -51,7 +52,7 @@ func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile return reconcile.Result{}, false, err } if openshiftBaseDomain == "" { - return reconcile.Result{}, false, nil + return reconcile.Result{RequeueAfter: time.Second}, false, nil } workspaceBaseDomain = openshiftBaseDomain @@ -105,6 +106,7 @@ func (r *BaseDomainReconciler) detectOpenShiftRouteBaseDomain(ctx *chetypes.Depl routeKey := types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace} if err := ctx.ClusterAPI.Client.Get(context.TODO(), routeKey, route); err != nil { if errors.IsNotFound(err) { + // Route is not ready return "", nil } @@ -117,6 +119,11 @@ func (r *BaseDomainReconciler) detectOpenShiftRouteBaseDomain(ctx *chetypes.Depl } }() + if route.Spec.Host == "" { + // Route is not ready + return "", nil + } + items := strings.SplitN(route.Spec.Host, ".", 2) if len(items) != 2 { return "", fmt.Errorf("unable to detect workspace base domain from %s", route.Spec.Host) From d3265853089167b37b8e3eb5148ccd401bfbae35 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Thu, 19 Mar 2026 10:01:21 +0100 Subject: [PATCH 7/7] Added tests Signed-off-by: Anatolii Bazko --- .../server/basedomain_reconciler_test.go | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/pkg/deploy/server/basedomain_reconciler_test.go b/pkg/deploy/server/basedomain_reconciler_test.go index a5053eef7b..979b591f9a 100644 --- a/pkg/deploy/server/basedomain_reconciler_test.go +++ b/pkg/deploy/server/basedomain_reconciler_test.go @@ -16,6 +16,7 @@ import ( "context" "testing" + "github.com/devfile/devworkspace-operator/pkg/infrastructure" chev2 "github.com/eclipse-che/che-operator/api/v2" "github.com/eclipse-che/che-operator/pkg/common/test" routev1 "github.com/openshift/api/route/v1" @@ -131,6 +132,74 @@ func TestBaseDomainStatusUpdated(t *testing.T) { assert.Equal(t, "new-domain.com", cheCluster.Status.WorkspaceBaseDomain) } +func TestBaseDomainIdempotency(t *testing.T) { + ctx := test.NewCtxBuilder().WithCheCluster(&chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "my-domain.com", + }, + }, + Status: chev2.CheClusterStatus{ + WorkspaceBaseDomain: "my-domain.com", + }, + }).Build() + + reconciler := NewBaseDomainReconciler() + + // First reconcile + _, done, err := reconciler.Reconcile(ctx) + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "my-domain.com", ctx.CheCluster.Status.WorkspaceBaseDomain) + + // Second reconcile should produce the same result + _, done, err = reconciler.Reconcile(ctx) + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "my-domain.com", ctx.CheCluster.Status.WorkspaceBaseDomain) +} + +func TestBaseDomainFailsWhenNoDomainResolved(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + defer infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + + ctx := test.NewCtxBuilder().Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.False(t, done) + assert.NotNil(t, err) +} + +func TestBaseDomainFailsWhenRouteHostMalformed(t *testing.T) { + route := &routev1.Route{ + TypeMeta: metav1.TypeMeta{ + Kind: "Route", + APIVersion: routev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "devworkspace-che-test", + Namespace: "eclipse-che", + }, + Spec: routev1.RouteSpec{ + Host: "nodots", + }, + } + + ctx := test.NewCtxBuilder().WithObjects(route).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.False(t, done) + assert.NotNil(t, err) +} + func TestBaseDomainFromRoute(t *testing.T) { route := &routev1.Route{ TypeMeta: metav1.TypeMeta{