Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,25 @@ type BackupCronJobConfig struct {
BackoffLimit *int32 `json:"backoffLimit,omitempty"`
}

// GatewayReference defines a reference to a Gateway API Gateway resource
// that HTTPRoutes should attach to via parentRefs.
type GatewayReference struct {
// Name is the name of the Gateway resource
// +kubebuilder:validation:Required
Name string `json:"name"`
// Namespace is the namespace of the Gateway resource.
// If not specified, HTTPRoutes will reference a Gateway in the same namespace
// as the DevWorkspace.
// +kubebuilder:validation:Optional
Namespace *string `json:"namespace,omitempty"`
// GatewayClassName is the name of the GatewayClass to use.
// This is used for validation and informational purposes.
// Defaults to "nginx" if not specified.
// +kubebuilder:validation:Optional
// +kubebuilder:default:="nginx"
GatewayClassName string `json:"gatewayClassName,omitempty"`
}

type RoutingConfig struct {
// DefaultRoutingClass specifies the routingClass to be used when a DevWorkspace
// specifies an empty `.spec.routingClass`. Supported routingClasses can be defined
Expand All @@ -144,6 +163,12 @@ type RoutingConfig struct {
// TLSCertificateConfigmapRef defines the name and namespace of the configmap with a certificate to inject into the
// HTTP client.
TLSCertificateConfigmapRef *ConfigmapReference `json:"tlsCertificateConfigmapRef,omitempty"`
// GatewayRef defines a reference to a Gateway API Gateway resource that HTTPRoutes
// should attach to when using the 'gateway-api' routing class. This field is required
// when routingClass is set to 'gateway-api'. The referenced Gateway must be provisioned
// by the cluster administrator or Che Operator before workspaces can use Gateway API routing.
// +kubebuilder:validation:Optional
GatewayRef *GatewayReference `json:"gatewayRef,omitempty"`
}

type WorkspaceConfig struct {
Expand Down
1 change: 1 addition & 0 deletions apis/controller/v1alpha1/devworkspacerouting_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const (
DevWorkspaceRoutingCluster DevWorkspaceRoutingClass = "cluster"
DevWorkspaceRoutingClusterTLS DevWorkspaceRoutingClass = "cluster-tls"
DevWorkspaceRoutingWebTerminal DevWorkspaceRoutingClass = "web-terminal"
DevWorkspaceRoutingGatewayAPI DevWorkspaceRoutingClass = "gateway-api"
)

// DevWorkspaceRoutingStatus defines the observed state of DevWorkspaceRouting
Expand Down
27 changes: 26 additions & 1 deletion apis/controller/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"

controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
)
Expand Down Expand Up @@ -68,6 +69,7 @@ type DevWorkspaceRoutingReconciler struct {
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=*
// +kubebuidler:rbac:groups=route.openshift.io,resources=routes/status,verbs=get,list,watch
// +kubebuilder:rbac:groups=route.openshift.io,resources=routes/custom-host,verbs=create
// +kubebuilder:rbac:groups=gateway.networking.k8s.io,resources=httproutes,verbs=*

func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
reqLogger := r.Log.WithValues("Request.Namespace", req.Namespace, "Request.Name", req.Name)
Expand Down Expand Up @@ -183,6 +185,16 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl.
routes[idx].Annotations = maputils.Append(routes[idx].Annotations, constants.DevWorkspaceRestrictedAccessAnnotation, restrictedAccess)
}
}
httpRoutes := routingObjects.HTTPRoutes
for idx := range httpRoutes {
err := controllerutil.SetControllerReference(instance, &httpRoutes[idx], r.Scheme)
if err != nil {
return reconcile.Result{}, err
}
if setRestrictedAccess {
httpRoutes[idx].Annotations = maputils.Append(httpRoutes[idx].Annotations, constants.DevWorkspaceRestrictedAccessAnnotation, restrictedAccess)
}
}

servicesInSync, clusterServices, err := r.syncServices(instance, services)
if err != nil {
Expand Down Expand Up @@ -231,12 +243,34 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl.
clusterRoutingObj.Ingresses = clusterIngresses
}

// Sync HTTPRoutes if using gateway-api routing class
if len(httpRoutes) > 0 {
httpRoutesInSync, clusterHTTPRoutes, err := r.syncHTTPRoutes(instance, httpRoutes)
if err != nil {
failError := &sync.UnrecoverableSyncError{}
if errors.As(err, &failError) {
return reconcile.Result{}, r.markRoutingFailed(instance, err.Error())
}
reqLogger.Error(err, "Error syncing HTTPRoutes")
return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
} else if !httpRoutesInSync {
reqLogger.Info("HTTPRoutes not in sync")
return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
}
clusterRoutingObj.HTTPRoutes = clusterHTTPRoutes
}

Comment on lines +246 to +262

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

HTTPRoute sync/cleanup is skipped when the desired list becomes empty, unlike routes/ingresses.

Wrapping the sync in if len(httpRoutes) > 0 means syncHTTPRoutes (and its getHTTPRoutesToDelete cleanup) never runs once the solver produces zero desired HTTPRoutes for this CR — e.g. if a workspace's exposed endpoints drop to zero while gateway-api routing is still selected. Any previously-created HTTPRoutes stay live in the cluster (still routable) until the whole DevWorkspaceRouting CR is deleted. The routes/ingresses sync calls just above (lines 217-244) run unconditionally for exactly this reason — to let their delete-diffing logic clean up stale objects even when the desired list is empty.

🐛 Proposed fix — always call syncHTTPRoutes
-	// Sync HTTPRoutes if using gateway-api routing class
-	if len(httpRoutes) > 0 {
-		httpRoutesInSync, clusterHTTPRoutes, err := r.syncHTTPRoutes(instance, httpRoutes)
-		if err != nil {
-			failError := &sync.UnrecoverableSyncError{}
-			if errors.As(err, &failError) {
-				return reconcile.Result{}, r.markRoutingFailed(instance, err.Error())
-			}
-			reqLogger.Error(err, "Error syncing HTTPRoutes")
-			return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
-		} else if !httpRoutesInSync {
-			reqLogger.Info("HTTPRoutes not in sync")
-			return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
-		}
-		clusterRoutingObj.HTTPRoutes = clusterHTTPRoutes
-	}
+	// Sync HTTPRoutes (always, so stale HTTPRoutes get cleaned up even when 0 are desired)
+	httpRoutesInSync, clusterHTTPRoutes, err := r.syncHTTPRoutes(instance, httpRoutes)
+	if err != nil {
+		failError := &sync.UnrecoverableSyncError{}
+		if errors.As(err, &failError) {
+			return reconcile.Result{}, r.markRoutingFailed(instance, err.Error())
+		}
+		reqLogger.Error(err, "Error syncing HTTPRoutes")
+		return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
+	} else if !httpRoutesInSync {
+		reqLogger.Info("HTTPRoutes not in sync")
+		return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
+	}
+	clusterRoutingObj.HTTPRoutes = clusterHTTPRoutes
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Sync HTTPRoutes if using gateway-api routing class
if len(httpRoutes) > 0 {
httpRoutesInSync, clusterHTTPRoutes, err := r.syncHTTPRoutes(instance, httpRoutes)
if err != nil {
failError := &sync.UnrecoverableSyncError{}
if errors.As(err, &failError) {
return reconcile.Result{}, r.markRoutingFailed(instance, err.Error())
}
reqLogger.Error(err, "Error syncing HTTPRoutes")
return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
} else if !httpRoutesInSync {
reqLogger.Info("HTTPRoutes not in sync")
return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
}
clusterRoutingObj.HTTPRoutes = clusterHTTPRoutes
}
// Sync HTTPRoutes (always, so stale HTTPRoutes get cleaned up even when 0 are desired)
httpRoutesInSync, clusterHTTPRoutes, err := r.syncHTTPRoutes(instance, httpRoutes)
if err != nil {
failError := &sync.UnrecoverableSyncError{}
if errors.As(err, &failError) {
return reconcile.Result{}, r.markRoutingFailed(instance, err.Error())
}
reqLogger.Error(err, "Error syncing HTTPRoutes")
return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
} else if !httpRoutesInSync {
reqLogger.Info("HTTPRoutes not in sync")
return reconcile.Result{Requeue: true}, r.reconcileStatus(instance, nil, nil, false, "Preparing HTTPRoutes")
}
clusterRoutingObj.HTTPRoutes = clusterHTTPRoutes
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controllers/controller/devworkspacerouting/devworkspacerouting_controller.go`
around lines 246 - 262, Call syncHTTPRoutes unconditionally in the HTTPRoute
reconciliation block, including when httpRoutes is empty, so its
deletion-diffing cleanup removes stale cluster HTTPRoutes. Preserve the existing
error handling, requeue/status behavior, and assignment of
clusterRoutingObj.HTTPRoutes for successful synchronization.

exposedEndpoints, endpointsAreReady, err := solver.GetExposedEndpoints(instance.Spec.Endpoints, clusterRoutingObj)
if err != nil {
reqLogger.Error(err, "Could not get exposed endpoints for devworkspace")
return reconcile.Result{}, r.markRoutingFailed(instance, fmt.Sprintf("Could not get exposed endpoints for DevWorkspace: %s", err))
}

if !endpointsAreReady {
reqLogger.Info("Endpoints not ready, requeuing to check HTTPRoute status")
return reconcile.Result{RequeueAfter: 3 * time.Second}, r.reconcileStatus(instance, nil, nil, false, "Waiting for HTTPRoute endpoints to be ready")
}

return reconcile.Result{}, r.reconcileStatus(instance, &routingObjects, exposedEndpoints, endpointsAreReady, "")
}

Expand Down Expand Up @@ -343,7 +377,8 @@ func (r *DevWorkspaceRoutingReconciler) SetupWithManager(mgr ctrl.Manager) error
WithOptions(controller.Options{MaxConcurrentReconciles: maxConcurrentReconciles}).
For(&controllerv1alpha1.DevWorkspaceRouting{}).
Owns(&corev1.Service{}).
Owns(&networkingv1.Ingress{})
Owns(&networkingv1.Ingress{}).
Owns(&gwapiv1.HTTPRoute{})
if infrastructure.IsOpenShift() {
bld.Owns(&routeV1.Route{})
}
Expand Down
Loading