Skip to content
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,
}

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) {
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