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/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/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/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/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/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 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..f7bbef37f3 --- /dev/null +++ b/pkg/deploy/server/basedomain_reconciler.go @@ -0,0 +1,133 @@ +// +// 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" + "time" + + "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" + "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" +) + +type BaseDomainReconciler struct { + reconciler.Reconcilable +} + +func NewBaseDomainReconciler() *BaseDomainReconciler { + return &BaseDomainReconciler{} +} + +func (r *BaseDomainReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) { + 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 workspaceBaseDomain == "" { + if infrastructure.IsOpenShift() { + openshiftBaseDomain, err := r.detectOpenShiftRouteBaseDomain(ctx) + if err != nil { + return reconcile.Result{}, false, err + } + if openshiftBaseDomain == "" { + return reconcile.Result{RequeueAfter: time.Second}, false, nil + } + + workspaceBaseDomain = openshiftBaseDomain + } + } + + if workspaceBaseDomain == "" { + return reconcile.Result{}, false, fmt.Errorf("unable to detect base domain") + } + + 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 + } + } + + 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, + }, + }, + } + + // 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 + } + } + + // 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) { + // Route is not ready + return "", nil + } + + return "", err + } + + defer func() { + if err := ctx.ClusterAPI.Client.Delete(context.TODO(), route); err != nil { + log.Error(err, "unable to delete test route %s", name) + } + }() + + 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) + } + + 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..979b591f9a --- /dev/null +++ b/pkg/deploy/server/basedomain_reconciler_test.go @@ -0,0 +1,226 @@ +// +// 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" + + "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" + "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 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{ + Kind: "Route", + APIVersion: routev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "devworkspace-che-test", + Namespace: "eclipse-che", + }, + Spec: routev1.RouteSpec{ + Host: "devworkspace-che-test.eclipse.org", + }, + } + + ctx := test.NewCtxBuilder().WithObjects(route).Build() + + reconciler := NewBaseDomainReconciler() + _, done, err := reconciler.Reconcile(ctx) + + assert.True(t, done) + assert.Nil(t, err) + assert.Equal(t, "eclipse.org", 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 }