From a8cbf103af1a108fab4d10d53b8fd1255dbaa7e2 Mon Sep 17 00:00:00 2001 From: David Kwon Date: Tue, 10 May 2022 16:55:48 -0400 Subject: [PATCH] feat: reroute main urls to the dashboard when workspace is stopped Signed-off-by: David Kwon --- .../devworkspace/solver/che_routing.go | 67 ++++++++++++++----- .../devworkspace/solver/che_routing_test.go | 43 ++++++++++++ controllers/devworkspace/solver/solver.go | 8 +++ pkg/deploy/gateway/traefik_config.go | 6 ++ pkg/deploy/gateway/traefik_config_util.go | 32 ++++++++- .../gateway/traefik_config_util_test.go | 7 ++ .../v1alpha1/devworkspacerouting_types.go | 5 ++ .../devworkspacerouting_controller.go | 17 ++--- .../solvers/basic_solver.go | 4 ++ .../solvers/cluster_solver.go | 4 ++ .../devworkspacerouting/solvers/solver.go | 2 + 11 files changed, 168 insertions(+), 27 deletions(-) diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index ab596c9157..d325aae53e 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -439,24 +439,30 @@ func provisionMainWorkspaceRoute(cheCluster *v2alpha1.CheCluster, routing *dwo.D dwId := routing.Spec.DevWorkspaceId dwNamespace := routing.Namespace - cfg := gateway.CreateCommonTraefikConfig( - dwId, - fmt.Sprintf("PathPrefix(`/%s`)", dwId), - 100, - getServiceURL(wsGatewayPort, dwId, dwNamespace), - []string{"/" + dwId}) - - if util.IsOpenShift4 { - // on OpenShift, we need to set authorization header. - // This MUST come before Auth, because Auth needs Authorization header to be properly set. - cfg.AddAuthHeaderRewrite(dwId) - } + cfg := gateway.CreateEmptyTraefikConfig() - // authorize against kube-rbac-proxy in che-gateway. This will be needed for k8s native auth as well. - cfg.AddAuth(dwId, "http://127.0.0.1:8089?namespace="+dwNamespace) + if (routing.IsWorkspaceStopped()) { + routeMainUrlsToDashboardService(cfg, dwId, routing.Spec.Endpoints) + } else { + cfg = gateway.CreateCommonTraefikConfig( + dwId, + fmt.Sprintf("PathPrefix(`/%s`)", dwId), + 100, + getServiceURL(wsGatewayPort, dwId, dwNamespace), + []string{"/" + dwId}) + + if util.IsOpenShift4 { + // on OpenShift, we need to set authorization header. + // This MUST come before Auth, because Auth needs Authorization header to be properly set. + cfg.AddAuthHeaderRewrite(dwId) + } + + // authorize against kube-rbac-proxy in che-gateway. This will be needed for k8s native auth as well. + cfg.AddAuth(dwId, "http://127.0.0.1:8089?namespace="+dwNamespace) - // make '/healthz' path of main endpoints reachable from outside - routeForHealthzEndpoint(cfg, dwId, routing.Spec.Endpoints) + // make '/healthz' path of main endpoints reachable from outside + routeForHealthzEndpoint(cfg, dwId, routing.Spec.Endpoints) + } if contents, err := yaml.Marshal(cfg); err != nil { return nil, err @@ -476,6 +482,35 @@ func provisionMainWorkspaceRoute(cheCluster *v2alpha1.CheCluster, routing *dwo.D } } +func routeMainUrlsToDashboardService(cfg *gateway.TraefikConfig, dwId string, endpoints map[string]dwo.EndpointList) { + for componentName, endpoints := range endpoints { + for _, e := range endpoints { + if e.Attributes.GetString(string(dwo.TypeEndpointAttribute), nil) == string(dwo.MainEndpointType) { + middlewares := []string{} + routeName, endpointPath := createEndpointPath(&e, componentName) + routerName := fmt.Sprintf("%s-%s", dwId, routeName) + + dashboardService := dwId+"-dashboard" + cfg.AddService(dashboardService, "http://che-dashboard:8080") // TODO: need to get che flavor instead of hardcoding `che` + + cfg.HTTP.Routers[routerName] = &gateway.TraefikConfigRouter{ + Rule: fmt.Sprintf("PathPrefix(`/%s%s`)", dwId, endpointPath), + Service: dashboardService, + Middlewares: middlewares, + Priority: 100, + } + + cfg.AddReplacePathRegex(routerName, fmt.Sprintf("^/%s%s/(.*)", dwId, endpointPath), "/dashboard/$1") + + if util.IsOpenShift4 { + cfg.AddAuthHeaderRewrite(routerName) + } + } + } + } + +} + // makes '/healthz' path of main endpoints reachable from the outside func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, dwId string, endpoints map[string]dwo.EndpointList) { for componentName, endpoints := range endpoints { diff --git a/controllers/devworkspace/solver/che_routing_test.go b/controllers/devworkspace/solver/che_routing_test.go index 9bb7dccb58..cd65f1684d 100644 --- a/controllers/devworkspace/solver/che_routing_test.go +++ b/controllers/devworkspace/solver/che_routing_test.go @@ -720,6 +720,49 @@ func TestFinalize(t *testing.T) { } } +func TestWorkspaceStopped(t *testing.T) { + util.IsOpenShift = false + util.IsOpenShift4 = false + routing := relocatableDevWorkspaceRouting() + routing.Annotations = map[string]string{constants.DevWorkspaceStartedStatusAnnotation: "false"} + + cl, slv, _ := getSpecObjects(t, routing) + + meta := solvers.DevWorkspaceMetadata{ + DevWorkspaceId: routing.Spec.DevWorkspaceId, + Namespace: routing.GetNamespace(), + PodSelector: routing.Spec.PodSelector, + } + + if err := slv.WorkspaceStopped(routing, meta); err != nil { + t.Fatal(err) + } + + cms := &corev1.ConfigMapList{} + cl.List(context.TODO(), cms) + + var workspaceMainCfg *corev1.ConfigMap + for _, cfg := range cms.Items { + if cfg.Name == "wsid-route" && cfg.Namespace == "ns" { + workspaceMainCfg = cfg.DeepCopy() + } + } + assert.NotNil(t, workspaceMainCfg) + traefikMainConfig := workspaceMainCfg.Data["wsid.yml"] + assert.NotEmpty(t, traefikMainConfig) + + workspaceMainConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikMainConfig), &workspaceMainConfig)) + assert.Len(t, workspaceMainConfig.HTTP.Routers, 1) + + wsid := "wsid-9999" + assert.Contains(t, workspaceMainConfig.HTTP.Routers, wsid) + assert.Len(t, workspaceMainConfig.HTTP.Routers[wsid].Middlewares, 1) + assert.Len(t, workspaceMainConfig.HTTP.Middlewares, 1) + assert.Len(t, workspaceMainConfig.HTTP.Services, 1) + assert.Equal(t, "http://che-dashboard:8080", workspaceMainConfig.HTTP.Services["wsid-dashboard"].LoadBalancer.Servers[0].URL) +} + func TestEndpointsAlwaysOnSecureProtocolsWhenExposedThroughGateway(t *testing.T) { util.IsOpenShift = false util.IsOpenShift4 = false diff --git a/controllers/devworkspace/solver/solver.go b/controllers/devworkspace/solver/solver.go index abb81e451e..2a116da487 100644 --- a/controllers/devworkspace/solver/solver.go +++ b/controllers/devworkspace/solver/solver.go @@ -196,3 +196,11 @@ func findCheManager(cheManagerKey client.ObjectKey) (*v2alpha1.CheCluster, error return &v2alpha1.CheCluster{}, &solvers.RoutingNotReady{Retry: 10 * time.Second} } + +func (c *CheRoutingSolver) WorkspaceStopped(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta solvers.DevWorkspaceMetadata) error { + cheManager, err := cheManagerOfRouting(routing) + if err != nil { + return err + } + return c.provisionRouting(&solvers.RoutingObjects{}, cheManager, routing, workspaceMeta) +} diff --git a/pkg/deploy/gateway/traefik_config.go b/pkg/deploy/gateway/traefik_config.go index 308d25da70..7ee5a1dca0 100644 --- a/pkg/deploy/gateway/traefik_config.go +++ b/pkg/deploy/gateway/traefik_config.go @@ -36,6 +36,7 @@ type TraefikConfigService struct { type TraefikConfigMiddleware struct { StripPrefix *TraefikConfigStripPrefix `json:"stripPrefix,omitempty"` ForwardAuth *TraefikConfigForwardAuth `json:"forwardAuth,omitempty"` + ReplacePathRegex *TraefikConfigReplacePathRegex `json:"replacePathRegex,omitempty"` Plugin *TraefikPlugin `json:"plugin,omitempty"` } @@ -57,6 +58,11 @@ type TraefikConfigForwardAuth struct { TLS *TraefikConfigTLS `json:"tls,omitempty"` } +type TraefikConfigReplacePathRegex struct { + Regex string `json:"regex"` + Replacement string `json:"replacement"` +} + type TraefikPlugin struct { HeaderRewrite *TraefikPluginHeaderRewrite `json:"header-rewrite,omitempty"` } diff --git a/pkg/deploy/gateway/traefik_config_util.go b/pkg/deploy/gateway/traefik_config_util.go index c660c29bdc..280542a525 100644 --- a/pkg/deploy/gateway/traefik_config_util.go +++ b/pkg/deploy/gateway/traefik_config_util.go @@ -12,9 +12,10 @@ package gateway const ( - StripPrefixMiddlewareSuffix = "-strip-prefix" - HeaderRewriteMiddlewareSuffix = "-header-rewrite" - AuthMiddlewareSuffix = "-auth" + StripPrefixMiddlewareSuffix = "-strip-prefix" + HeaderRewriteMiddlewareSuffix = "-header-rewrite" + AuthMiddlewareSuffix = "-auth" + ReplacePathRegexMiddlewareSuffix = "-replace-path-regex" ) func CreateEmptyTraefikConfig() *TraefikConfig { @@ -49,11 +50,25 @@ func (cfg *TraefikConfig) AddComponent(componentName string, rule string, priori }, }, } + cfg.AddService(componentName, serviceAddr) + if len(stripPrefixes) > 0 { cfg.AddStripPrefix(componentName, stripPrefixes) } } +func (cfg *TraefikConfig) AddService(componentName string, serviceAddr string) { + cfg.HTTP.Services[componentName] = &TraefikConfigService{ + LoadBalancer: TraefikConfigLoadbalancer{ + Servers: []TraefikConfigLoadbalancerServer{ + { + URL: serviceAddr, + }, + }, + }, + } +} + func (cfg *TraefikConfig) AddStripPrefix(componentName string, prefixes []string) { middlewareName := componentName + StripPrefixMiddlewareSuffix cfg.HTTP.Routers[componentName].Middlewares = append(cfg.HTTP.Routers[componentName].Middlewares, middlewareName) @@ -101,3 +116,14 @@ func (cfg *TraefikConfig) AddAuth(componentName string, authAddress string) { }, } } + +func (cfg *TraefikConfig) AddReplacePathRegex(componentName string, regex string, replacement string) { + middlewareName := componentName + ReplacePathRegexMiddlewareSuffix + cfg.HTTP.Routers[componentName].Middlewares = append(cfg.HTTP.Routers[componentName].Middlewares, middlewareName) + cfg.HTTP.Middlewares[middlewareName] = &TraefikConfigMiddleware{ + ReplacePathRegex: &TraefikConfigReplacePathRegex{ + Regex: regex, + Replacement: replacement, + }, + } +} diff --git a/pkg/deploy/gateway/traefik_config_util_test.go b/pkg/deploy/gateway/traefik_config_util_test.go index 23d8c9b3db..aa9c76f0ad 100644 --- a/pkg/deploy/gateway/traefik_config_util_test.go +++ b/pkg/deploy/gateway/traefik_config_util_test.go @@ -39,6 +39,13 @@ func TestTraefikConfig_AddComponent(t *testing.T) { assert.Empty(t, cfg.HTTP.Middlewares) } +func TestAddService(t *testing.T) { + cfg := CreateEmptyTraefikConfig() + cfg.AddService(testComponentName, "http://svc") + assert.Contains(t, cfg.HTTP.Services, testComponentName) + assert.Empty(t, cfg.HTTP.Middlewares) +} + func TestStripPrefixesWhenCreating(t *testing.T) { check := func(cfg *TraefikConfig) { assert.Contains(t, cfg.HTTP.Routers[testComponentName].Middlewares, testComponentName+StripPrefixMiddlewareSuffix) diff --git a/vendor/github.com/devfile/devworkspace-operator/apis/controller/v1alpha1/devworkspacerouting_types.go b/vendor/github.com/devfile/devworkspace-operator/apis/controller/v1alpha1/devworkspacerouting_types.go index d27b4cf894..a96376d545 100644 --- a/vendor/github.com/devfile/devworkspace-operator/apis/controller/v1alpha1/devworkspacerouting_types.go +++ b/vendor/github.com/devfile/devworkspace-operator/apis/controller/v1alpha1/devworkspacerouting_types.go @@ -18,6 +18,7 @@ package v1alpha1 import ( apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/devfile/devworkspace-operator/pkg/constants" ) // DevWorkspaceRoutingSpec defines the desired state of DevWorkspaceRouting @@ -205,3 +206,7 @@ type DevWorkspaceRoutingList struct { func init() { SchemeBuilder.Register(&DevWorkspaceRouting{}, &DevWorkspaceRoutingList{}) } + +func (d *DevWorkspaceRouting) IsWorkspaceStopped() bool { + return d.Annotations != nil && d.Annotations[constants.DevWorkspaceStartedStatusAnnotation] == "false" +} diff --git a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go index 533ad7e87b..ae4a7669b1 100644 --- a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go +++ b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/devworkspacerouting_controller.go @@ -107,8 +107,15 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl. return reconcile.Result{}, r.finalize(solver, instance) } - if instance.Annotations != nil && instance.Annotations[constants.DevWorkspaceStartedStatusAnnotation] == "false" { - return reconcile.Result{}, nil + workspaceMeta := solvers.DevWorkspaceMetadata{ + DevWorkspaceId: instance.Spec.DevWorkspaceId, + Namespace: instance.Namespace, + PodSelector: instance.Spec.PodSelector, + } + + if instance.IsWorkspaceStopped() { + err := solver.WorkspaceStopped(instance, workspaceMeta) + return reconcile.Result{}, err } if instance.Status.Phase == controllerv1alpha1.RoutingFailed { @@ -120,12 +127,6 @@ func (r *DevWorkspaceRoutingReconciler) Reconcile(ctx context.Context, req ctrl. return reconcile.Result{}, err } - workspaceMeta := solvers.DevWorkspaceMetadata{ - DevWorkspaceId: instance.Spec.DevWorkspaceId, - Namespace: instance.Namespace, - PodSelector: instance.Spec.PodSelector, - } - restrictedAccess, setRestrictedAccess := instance.Annotations[constants.DevWorkspaceRestrictedAccessAnnotation] routingObjects, err := solver.GetSpecObjects(instance, workspaceMeta) if err != nil { diff --git a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/basic_solver.go b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/basic_solver.go index 23beeccc3e..c90c8c5750 100644 --- a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/basic_solver.go +++ b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/basic_solver.go @@ -80,3 +80,7 @@ func (s *BasicSolver) GetExposedEndpoints( routingObj RoutingObjects) (exposedEndpoints map[string]controllerv1alpha1.ExposedEndpointList, ready bool, err error) { return getExposedEndpoints(endpoints, routingObj) } + +func (s *BasicSolver) WorkspaceStopped(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta DevWorkspaceMetadata) error { + return nil +} diff --git a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/cluster_solver.go b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/cluster_solver.go index 193fa2dc75..a4e75e0951 100644 --- a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/cluster_solver.go +++ b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/cluster_solver.go @@ -126,3 +126,7 @@ func getHostnameFromService(service corev1.Service, port int32) string { } return fmt.Sprintf("%s://%s.%s.svc:%d", scheme, service.Name, service.Namespace, port) } + +func (s *ClusterSolver) WorkspaceStopped(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta DevWorkspaceMetadata) error { + return nil +} diff --git a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/solver.go b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/solver.go index 06823e2cc1..8da24c2c07 100644 --- a/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/solver.go +++ b/vendor/github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers/solver.go @@ -59,6 +59,8 @@ type RoutingSolver interface { // Return value "ready" specifies if all endpoints are resolved on the cluster; if false it is necessary to retry, as // URLs will be undefined. GetExposedEndpoints(endpoints map[string]controllerv1alpha1.EndpointList, routingObj RoutingObjects) (exposedEndpoints map[string]controllerv1alpha1.ExposedEndpointList, ready bool, err error) + + WorkspaceStopped(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta DevWorkspaceMetadata) error } type RoutingSolverGetter interface {