Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl.
return reconcile.Result{}, r.markRoutingFailed(instance, "DevWorkspaceRouting requires field routingClass to be set")
}

solver, err := r.SolverGetter.GetSolver(r.Client, instance.Spec.RoutingClass)
solver, err := r.SolverGetter.GetSolver(r.Client, reqLogger, instance.Spec.RoutingClass)
if err != nil {
if errors.Is(err, solvers.RoutingNotSupported) {
reqLogger.Info("Routing class not supported by this controller, skipping reconciliation", "routingClass", instance.Spec.RoutingClass)
return reconcile.Result{}, nil
}
return reconcile.Result{}, r.markRoutingFailed(instance, fmt.Sprintf("Invalid routingClass for DevWorkspace: %s", err))
Expand Down Expand Up @@ -125,9 +126,10 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl.
}

workspaceMeta := solvers.DevWorkspaceMetadata{
DevWorkspaceId: instance.Spec.DevWorkspaceId,
Namespace: instance.Namespace,
PodSelector: instance.Spec.PodSelector,
DevWorkspaceId: instance.Spec.DevWorkspaceId,
DevWorkspaceName: instance.Name,
Namespace: instance.Namespace,
PodSelector: instance.Spec.PodSelector,
}
Comment on lines 128 to 133
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm how DevWorkspaceRouting objects are named relative to the parent DevWorkspace.
# Look for routing creation sites in the workspace controller / provision packages.
rg -nP --type=go -C5 '\bDevWorkspaceRouting\s*\{' -g '!**/vendor/**' -g '!**/zz_generated*'
rg -nP --type=go -C3 '\bObjectMeta:\s*metav1\.ObjectMeta\s*\{' -g '*routing*'

Repository: devfile/devworkspace-operator

Length of output: 26087


🏁 Script executed:

#!/bin/bash
# Find the common.DevWorkspaceRoutingName function definition
rg -nP --type=go "func.*DevWorkspaceRoutingName" -g '!**/vendor/**'

# Show the context around where workspaceMeta is created in the controller
sed -n '120,140p' controllers/controller/devworkspacerouting/devworkspacerouting_controller.go

# Check if there's a reference to parent DevWorkspace in the routing controller
rg -nP --type=go "DevWorkspace\s*\{" controllers/controller/devworkspacerouting/devworkspacerouting_controller.go -C 3

Repository: devfile/devworkspace-operator

Length of output: 879


🏁 Script executed:

#!/bin/bash
# Get the full implementation of DevWorkspaceRoutingName
cat -n pkg/common/naming.go | head -40

# Check if DevWorkspaceRouting object has parent reference or ownerRef pointing to DevWorkspace
rg -nP --type=go "OwnerReferences|ownerRef" controllers/controller/devworkspacerouting/devworkspacerouting_controller.go

# Search for webhook code that validates workspace names
fd webhook -t f --type go | head -5
rg -nP --type=go "otherWorkspace|DevWorkspaceName" --type=go -A 3 -B 3 | head -50

Repository: devfile/devworkspace-operator

Length of output: 6551


🏁 Script executed:

#!/bin/bash
# Find webhook validation code that uses DevWorkspaceName
rg -nP --type=go "DevWorkspaceName|validateEndpoints" webhook/ --type go -A 5 -B 2 | head -100

# Check how DevWorkspaceRouting relates to parent DevWorkspace (ownerReferences)
rg -nP --type=go "OwnerReferences" controllers/workspace/ -A 3 -B 1

# Look at the full reconcile method context around line 128-133
sed -n '100,150p' controllers/controller/devworkspacerouting/devworkspacerouting_controller.go

Repository: devfile/devworkspace-operator

Length of output: 13643


🏁 Script executed:

#!/bin/bash
# Find where ServiceConflictError is created and how it uses DevWorkspaceName
rg -nP --type=go "ServiceConflictError" pkg/ --type go -B 5 -A 5

# Check how solvers use DevWorkspaceMetadata.DevWorkspaceName
rg -nP --type=go "DevWorkspaceName" pkg/ --type go -C 3

# Find what information is available in the routing controller about the parent workspace
rg -nP --type=go "getDevWorkspace\|FindOwner" controllers/controller/devworkspacerouting/ --type go -C 3

Repository: devfile/devworkspace-operator

Length of output: 6411


🏁 Script executed:

#!/bin/bash
# Find ServiceConflictError in solvers package
rg -nP --type=go "type ServiceConflictError\|ServiceConflictError\s*struct" pkg/solvers/ -A 10

# Check solvers package for DevWorkspaceMetadata usage
rg -nP --type=go "DevWorkspaceMetadata" pkg/solvers/ -B 2 -A 8 | head -100

# Look for conflict reporting with workspace names
rg -nP --type=go "OtherWorkspaceName\|ConflictingWorkspaceName\|conflict.*workspace" pkg/ --type go -C 3

Repository: devfile/devworkspace-operator

Length of output: 179


🏁 Script executed:

#!/bin/bash
# Find where solvers package is located
find . -name "*.go" -type f | xargs grep -l "type ServiceConflictError" | head -5

# Find solvers imports
rg -nP --type=go "solvers\." controllers/controller/devworkspacerouting/ --type go | head -10

# Search for ServiceConflictError definition
rg -nP --type=go "ServiceConflictError" --type go -B 3 -A 10 | head -100

Repository: devfile/devworkspace-operator

Length of output: 11248


🏁 Script executed:

#!/bin/bash
# Check where DevWorkspaceName is used in solvers
rg -nP --type=go "DevWorkspaceName\|OtherWorkspaceName" controllers/controller/devworkspacerouting/solvers/ --type go -B 3 -A 3

# Check if solvers checks for conflicts by comparing workspace metadata names
rg -nP --type=go "metadata\.DevWorkspaceName\|workspaceMeta\.DevWorkspaceName" controllers/controller/devworkspacerouting/solvers/ --type go -B 5 -A 5

# Look at the full errors.go to understand ServiceConflictError
cat -n controllers/controller/devworkspacerouting/solvers/errors.go

Repository: devfile/devworkspace-operator

Length of output: 2871


🏁 Script executed:

#!/bin/bash
# Find where ServiceConflictError is instantiated in solvers
rg -nP --type=go "ServiceConflictError\s*\{" controllers/controller/devworkspacerouting/solvers/ --type go -B 5 -A 3

# Check the common_test.go file more fully to understand the concern
sed -n '70,100p' controllers/controller/devworkspacerouting/solvers/common_test.go

Repository: devfile/devworkspace-operator

Length of output: 4589


🏁 Script executed:

#!/bin/bash
# Find all usages of DevWorkspaceName in the solvers code
rg -nP --type=go "DevWorkspaceName\|meta\.DevWorkspaceName" controllers/controller/devworkspacerouting/solvers/ --type go -C 3

# Check if DevWorkspaceName is compared with any service labels or workspace names
rg -nP --type=go "\.DevWorkspaceName\s*==" controllers/controller/devworkspacerouting/ --type go -C 5

Repository: devfile/devworkspace-operator

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Let's trace how DevWorkspaceName is actually used by checking all references to it
rg -nP --type=go "DevWorkspaceName" controllers/controller/devworkspacerouting/solvers/ --type go

# Check what parameters GetSpecObjects and GetDiscoverableServicesForEndpoints use
rg -nP --type=go "GetSpecObjects\|GetDiscoverableServicesForEndpoints" controllers/controller/devworkspacerouting/solvers/ --type go -B 2 -A 8 | head -60

# Check the full common.go file around the conflict check
sed -n '50,75p' controllers/controller/devworkspacerouting/solvers/common.go

Repository: devfile/devworkspace-operator

Length of output: 2144


🏁 Script executed:

#!/bin/bash
# Check if DevWorkspaceRouting has ownerReferences pointing to parent DevWorkspace
sed -n '75,200p' controllers/controller/devworkspacerouting/devworkspacerouting_controller.go | head -80

# Check if we can get parent workspace name from ownerReferences
rg -nP --type=go "GetOwnerReferences\|OwnerReferences" controllers/controller/devworkspacerouting/devworkspacerouting_controller.go --type go -B 2 -A 5

# Check how workspace controller creates the routing with ownerReferences
sed -n '100,120p' pkg/provision/workspace/routing.go

Repository: devfile/devworkspace-operator

Length of output: 4073


🏁 Script executed:

#!/bin/bash
# Verify: Check if DevWorkspaceRouting object has ownerReferences that we can use to look up parent workspace
sed -n '105,130p' pkg/provision/workspace/routing.go

# Check if there's any code that looks up the parent DevWorkspace from the routing object
rg -nP --type=go "ownerRef.*DevWorkspace\|GetOwner" controllers/controller/devworkspacerouting/ --type go -C 3

# Verify test data - what are the expected workspace names in tests
sed -n '40,60p' controllers/controller/devworkspacerouting/solvers/common_test.go

Repository: devfile/devworkspace-operator

Length of output: 1446


🏁 Script executed:

#!/bin/bash
# Check if there's a way to get parent workspace name from ownerReferences in the routing controller
rg -nP --type=go "for.*ownerRef\|OwnerReferences\|GetOwnerReferences" controllers/controller/devworkspacerouting/devworkspacerouting_controller.go --type go -B 2 -A 8

# Verify: Can we access parent workspace from routing object?
sed -n '1,80p' controllers/controller/devworkspacerouting/devworkspacerouting_controller.go | tail -30

Repository: devfile/devworkspace-operator

Length of output: 1583


🏁 Script executed:

#!/bin/bash
# Final verification: check if the parent workspace name is accessible via ownerReferences
# and confirm the exact issue with how names diverge

# Show where SetControllerReference is used
rg -nP --type=go "SetControllerReference" pkg/provision/workspace/routing.go --type go -B 3 -A 3

# Check what happens after ownerReferences are set
sed -n '120,135p' pkg/provision/workspace/routing.go

# Verify the actual behavior: Get parent workspace name from ownerReference
rg -nP --type=go "ownerReferences\|OwnerReferences" apis/controller/v1alpha1/devworkspacerouting_types.go --type go -C 5

Repository: devfile/devworkspace-operator

Length of output: 604


🏁 Script executed:

#!/bin/bash
# Final confirmation: Check if we can get parent workspace name from ownerReferences in the routing reconciler
# and verify the actual issue scenario

# Get the full reconcile method to see all available information
sed -n '72,180p' controllers/controller/devworkspacerouting/devworkspacerouting_controller.go | grep -A 5 "ownerRef\|OwnerReferences" || echo "No ownerReferences used in reconciler"

# Check test to confirm expected behavior
rg -nP --type=go "DevWorkspaceName.*routing-\|routing-.*DevWorkspaceName" controllers/controller/devworkspacerouting/ --type go

# Verify the actual naming mismatch by checking what parent workspace name should be
sed -n '40,70p' controllers/workspace/devworkspace_controller_test.go

Repository: devfile/devworkspace-operator

Length of output: 1038


🏁 Script executed:

#!/bin/bash
# Let me verify the actual problem: check if parent workspace name is accessible via ownerReferences
# and whether this causes a real issue

# Get the type definition for DevWorkspaceRouting to see ownerReferences field
sed -n '1,100p' apis/controller/v1alpha1/devworkspacerouting_types.go

# Check if there's documentation on how DevWorkspaceName should be set
rg -nP --type=go "DevWorkspaceMetadata" apis/controller/v1alpha1/ --type go -B 5 -A 10

Repository: devfile/devworkspace-operator

Length of output: 3967


Use the parent DevWorkspace.Name instead of instance.Name for DevWorkspaceName.

DevWorkspaceRouting objects are created with names like "routing-{workspaceId}" (based on the workspace ID), but the parent DevWorkspace can have any arbitrary name. The code currently sets workspaceMeta.DevWorkspaceName = instance.Name, which passes the routing object's name to solvers for service labeling. This causes a mismatch: services get labeled with the routing name (e.g., "routing-abc123"), but the webhook's conflict reporting uses the actual DevWorkspace.Name (e.g., "my-workspace"). When users see conflict errors, the workspace name attributed to the service owner will not match the actual workspace name, making error messages confusing.

Since DevWorkspaceRouting has an ownerReferences to its parent DevWorkspace, the parent workspace name should be retrieved from there and used for DevWorkspaceName instead of instance.Name.

🤖 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 128 - 133, The code sets workspaceMeta.DevWorkspaceName =
instance.Name but must use the parent DevWorkspace's name from
instance.OwnerReferences; update the DevWorkspaceMetadata population so
DevWorkspaceName is assigned from the owner reference whose Kind (or
APIVersion+Kind) indicates the parent DevWorkspace (e.g., iterate
instance.OwnerReferences, find ownerRef.Kind == "DevWorkspace" and use
ownerRef.Name), and fall back to instance.Name only if no such ownerRef exists;
change the assignment near workspaceMeta (DevWorkspaceMetadata,
workspaceMeta.DevWorkspaceName) to use that ownerRef-derived name.


restrictedAccess, setRestrictedAccess := instance.Annotations[constants.DevWorkspaceRestrictedAccessAnnotation]
Expand All @@ -149,6 +151,12 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl.
return reconcile.Result{}, r.markRoutingFailed(instance, fmt.Sprintf("Unable to provision networking for DevWorkspace: %s", invalid))
}

var conflict *solvers.ServiceConflictError
if errors.As(err, &conflict) {
reqLogger.Error(conflict, "Routing controller detected a service conflict", "endpointName", conflict.EndpointName, "workspaceName", conflict.WorkspaceName)
return reconcile.Result{}, r.markRoutingFailed(instance, fmt.Sprintf("Unable to provision networking for DevWorkspace: %s", conflict))
}

// generic error, just fail the reconciliation
return reconcile.Result{}, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
)

var routeAnnotations = func(endpointName string, endpointAnnotations map[string]string) map[string]string {
Expand Down Expand Up @@ -47,10 +49,21 @@ var nginxIngressAnnotations = func(endpointName string, endpointAnnotations map[
// According to the current cluster there is different behavior:
// Kubernetes: use Ingresses without TLS
// OpenShift: use Routes with TLS enabled
type BasicSolver struct{}
type BasicSolver struct {
client client.Client
logger logr.Logger
}

var _ RoutingSolver = (*BasicSolver)(nil)

// NewBasicSolver creates a new BasicSolver with the provided dependencies
func NewBasicSolver(client client.Client, logger logr.Logger) *BasicSolver {
return &BasicSolver{
client: client,
logger: logger,
}
}

func (s *BasicSolver) FinalizerRequired(*controllerv1alpha1.DevWorkspaceRouting) bool {
return false
}
Expand All @@ -70,7 +83,11 @@ func (s *BasicSolver) GetSpecObjects(routing *controllerv1alpha1.DevWorkspaceRou

spec := routing.Spec
services := getServicesForEndpoints(spec.Endpoints, workspaceMeta)
services = append(services, GetDiscoverableServicesForEndpoints(spec.Endpoints, workspaceMeta)...)
discoverableServices, err := GetDiscoverableServicesForEndpoints(spec.Endpoints, workspaceMeta, s.client, s.logger)
if err != nil {
return RoutingObjects{}, err
}
services = append(services, discoverableServices...)
routingObjects.Services = services
if infrastructure.IsOpenShift() {
routingObjects.Routes = getRoutesForSpec(routingSuffix, spec.Endpoints, workspaceMeta)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,31 @@ import (
corev1 "k8s.io/api/core/v1"

controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
)

const (
serviceServingCertAnnot = "service.beta.openshift.io/serving-cert-secret-name"
)

type ClusterSolver struct {
TLS bool
TLS bool
client client.Client
logger logr.Logger
}

var _ RoutingSolver = (*ClusterSolver)(nil)

// NewClusterSolver creates a new ClusterSolver with the provided dependencies
func NewClusterSolver(client client.Client, logger logr.Logger, tls bool) *ClusterSolver {
return &ClusterSolver{
TLS: tls,
client: client,
logger: logger,
}
}

func (s *ClusterSolver) FinalizerRequired(*controllerv1alpha1.DevWorkspaceRouting) bool {
return false
}
Expand All @@ -47,6 +60,11 @@ func (s *ClusterSolver) Finalize(*controllerv1alpha1.DevWorkspaceRouting) error
func (s *ClusterSolver) GetSpecObjects(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta DevWorkspaceMetadata) (RoutingObjects, error) {
spec := routing.Spec
services := getServicesForEndpoints(spec.Endpoints, workspaceMeta)
discoverableServices, err := GetDiscoverableServicesForEndpoints(spec.Endpoints, workspaceMeta, s.client, s.logger)
if err != nil {
return RoutingObjects{}, err
}
services = append(services, discoverableServices...)
podAdditions := &controllerv1alpha1.PodAdditions{}
if s.TLS {
readOnlyMode := int32(420)
Expand Down
43 changes: 32 additions & 11 deletions controllers/controller/devworkspacerouting/solvers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@
package solvers

import (
"context"

controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/devfile/devworkspace-operator/pkg/common"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"

routeV1 "github.com/openshift/api/route/v1"
corev1 "k8s.io/api/core/v1"
Expand All @@ -29,14 +34,15 @@ import (
)

type DevWorkspaceMetadata struct {
DevWorkspaceId string
Namespace string
PodSelector map[string]string
DevWorkspaceId string
DevWorkspaceName string
Namespace string
PodSelector map[string]string
}

// GetDiscoverableServicesForEndpoints converts the endpoint list into a set of services, each corresponding to a single discoverable
// endpoint from the list. Endpoints with the NoneEndpointExposure are ignored.
func GetDiscoverableServicesForEndpoints(endpoints map[string]controllerv1alpha1.EndpointList, meta DevWorkspaceMetadata) []corev1.Service {
func GetDiscoverableServicesForEndpoints(endpoints map[string]controllerv1alpha1.EndpointList, meta DevWorkspaceMetadata, cl client.Client, log logr.Logger) ([]corev1.Service, error) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@akurinnoy is there a reason for implementing duplicate endpiont detection in both the webhook and the GetDiscoverableServicesForEndpoints function?

Copy link
Copy Markdown
Collaborator Author

@akurinnoy akurinnoy Nov 17, 2025

Choose a reason for hiding this comment

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

While the webhook prevents the DevWorkspaces (which will cause endpoint conflicts) from being created, the controller checks other cases:

  • two workspaces created simultaneously - both of them may pass the webhook validation
  • services can be created from other sources
  • old workspaces, created before this validation

Is this the answer to your question?

var services []corev1.Service
for _, machineEndpoints := range endpoints {
for _, endpoint := range machineEndpoints {
Expand All @@ -45,21 +51,36 @@ func GetDiscoverableServicesForEndpoints(endpoints map[string]controllerv1alpha1
}

if endpoint.Attributes.GetBoolean(string(controllerv1alpha1.DiscoverableAttribute), nil) {
// Create service with name matching endpoint
// TODO: This could cause a reconcile conflict if multiple workspaces define the same discoverable endpoint
// Also endpoint names may not be valid as service names
serviceName := common.EndpointName(endpoint.Name)
existingService := &corev1.Service{}
err := cl.Get(context.TODO(), client.ObjectKey{Name: serviceName, Namespace: meta.Namespace}, existingService)
if err != nil {
if !errors.IsNotFound(err) {
log.Error(err, "Failed to get service from cluster", "serviceName", serviceName)
return nil, err
}
} else {
if existingService.Labels[constants.DevWorkspaceIDLabel] != meta.DevWorkspaceId {
return nil, &ServiceConflictError{
EndpointName: endpoint.Name,
WorkspaceName: existingService.Labels[constants.DevWorkspaceNameLabel],
}
}
}

servicePort := corev1.ServicePort{
Name: common.EndpointName(endpoint.Name),
Name: serviceName,
Protocol: corev1.ProtocolTCP,
Port: int32(endpoint.TargetPort),
TargetPort: intstr.FromInt(endpoint.TargetPort),
}
services = append(services, corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: common.EndpointName(endpoint.Name),
Name: serviceName,
Namespace: meta.Namespace,
Labels: map[string]string{
constants.DevWorkspaceIDLabel: meta.DevWorkspaceId,
constants.DevWorkspaceIDLabel: meta.DevWorkspaceId,
constants.DevWorkspaceNameLabel: meta.DevWorkspaceName,
},
Annotations: map[string]string{
constants.DevWorkspaceDiscoverableServiceAnnotation: "true",
Expand All @@ -74,7 +95,7 @@ func GetDiscoverableServicesForEndpoints(endpoints map[string]controllerv1alpha1
}
}
}
return services
return services, nil
}

// GetServiceForEndpoints returns a single service that exposes all endpoints of given exposure types, possibly also including the discoverable types.
Expand Down
Loading
Loading