From e83daf2f911bcae1bf2db7359a9de48c8bf27ac5 Mon Sep 17 00:00:00 2001 From: David Kwon Date: Fri, 21 Apr 2023 15:38:21 -0400 Subject: [PATCH 1/8] Make workspace routes more user-friendly Signed-off-by: David Kwon --- .../devworkspace/solver/che_routing.go | 85 ++++++++--- .../devworkspace/solver/che_routing_test.go | 136 +++++++++++++----- .../devworkspace/solver/endpoint_exposer.go | 12 +- 3 files changed, 174 insertions(+), 59 deletions(-) diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index 74d4ac315e..ebc3fa519e 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -16,6 +16,7 @@ import ( "context" "fmt" "path" + "regexp" "strconv" "strings" @@ -279,7 +280,17 @@ func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, wor return map[string]dwo.ExposedEndpointList{}, false, nil } - publicURLPrefix := getPublicURLPrefixForEndpoint(workspaceID, component, endpoint) + username, err := getNormalizedUsername(c.client, routingObj.Services[0].Namespace) + if err != nil { + return map[string]dwo.ExposedEndpointList{}, false, err + } + + dwName, err := getNormalizedWkspName(c.client, routingObj.Services[0].Namespace, routingObj.Services[0].ObjectMeta.OwnerReferences[0].Name) + if err != nil { + return map[string]dwo.ExposedEndpointList{}, false, err + } + + publicURLPrefix := getPublicURLPrefixForEndpoint(dwName, username, endpoint) endpointURL = path.Join(gatewayHost, publicURLPrefix, endpoint.Path) } @@ -329,16 +340,25 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch } configs := make([]corev1.ConfigMap, 0) + username, err := getNormalizedUsername(c.client, routing.Namespace) + if err != nil { + return nil, err + } + + dwName, err := getNormalizedWkspName(c.client, routing.Namespace, routing.Name) + if err != nil { + return nil, err + } // first do routing from main che-gateway into workspace service - if mainWsRouteConfig, err := provisionMainWorkspaceRoute(cheCluster, routing, cmLabels); err != nil { + if mainWsRouteConfig, err := provisionMainWorkspaceRoute(cheCluster, routing, username, dwName, cmLabels); err != nil { return nil, err } else { configs = append(configs, *mainWsRouteConfig) } // then expose the endpoints - if infraExposer, err := c.getInfraSpecificExposer(cheCluster, routing, objs); err != nil { + if infraExposer, err := c.getInfraSpecificExposer(cheCluster, routing, objs, username, dwName); err != nil { return nil, err } else { if workspaceConfig := exposeAllEndpoints(cheCluster, routing, objs, infraExposer); workspaceConfig != nil { @@ -349,14 +369,43 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch return configs, nil } -func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects) (func(info *EndpointInfo), error) { +func getNormalizedUsername(c client.Client, namespace string) (string, error) { + secret := &corev1.Secret{} + err := c.Get(context.TODO(), client.ObjectKey{Name: "user-profile", Namespace: namespace}, secret) + if err != nil { + return "", err + } + username := string(secret.Data["name"]) + return normalize(username), nil +} + +func getNormalizedWkspName(c client.Client, namespace string, routingName string) (string, error) { + routing := &dwo.DevWorkspaceRouting{} + err := c.Get(context.TODO(), client.ObjectKey{Name: routingName, Namespace: namespace}, routing) + if err != nil { + return "", err + } + return normalize(routing.ObjectMeta.OwnerReferences[0].Name), nil +} + +func normalize(username string) string { + r1 := regexp.MustCompile(`[^-a-zA-Z0-9]`) + r2 := regexp.MustCompile(`-+`) + r3 := regexp.MustCompile(`^-|-$`) + + result := r1.ReplaceAllString(username, "-") // replace invalid chars with '-' + result = r2.ReplaceAllString(result, "-") // replace multiple '-' with single ones + return r3.ReplaceAllString(result, "") // trim dashes at beginning/end +} + +func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects, username string, dwName string) (func(info *EndpointInfo), error) { if infrastructure.IsOpenShift() { exposer := &RouteExposer{} if err := exposer.initFrom(context.TODO(), c.client, cheCluster, routing); err != nil { return nil, err } return func(info *EndpointInfo) { - route := exposer.getRouteForService(info) + route := exposer.getRouteForService(info, username, dwName) objs.Routes = append(objs.Routes, route) }, nil } else { @@ -365,7 +414,7 @@ func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, return nil, err } return func(info *EndpointInfo) { - ingress := exposer.getIngressForService(info) + ingress := exposer.getIngressForService(info, username, dwName) objs.Ingresses = append(objs.Ingresses, ingress) }, nil } @@ -473,16 +522,16 @@ func containPort(service *corev1.Service, port int32) bool { return false } -func provisionMainWorkspaceRoute(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, cmLabels map[string]string) (*corev1.ConfigMap, error) { +func provisionMainWorkspaceRoute(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, username string, dwName string, cmLabels map[string]string) (*corev1.ConfigMap, error) { dwId := routing.Spec.DevWorkspaceId dwNamespace := routing.Namespace cfg := gateway.CreateCommonTraefikConfig( dwId, - fmt.Sprintf("PathPrefix(`/%s`)", dwId), + fmt.Sprintf("PathPrefix(`/%s/%s`)", username, dwName), 100, getServiceURL(wsGatewayPort, dwId, dwNamespace), - []string{"/" + dwId}) + []string{fmt.Sprintf("/%s/%s", username, dwName)}) if cheCluster.IsAccessTokenConfigured() { cfg.AddAuthHeaderRewrite(dwId) @@ -494,7 +543,7 @@ func provisionMainWorkspaceRoute(cheCluster *chev2.CheCluster, routing *dwo.DevW add5XXErrorHandling(cfg, dwId) // make '/healthz' path of main endpoints reachable from outside - routeForHealthzEndpoint(cfg, dwId, routing.Spec.Endpoints) + routeForHealthzEndpoint(cfg, username, dwName, dwId, routing.Spec.Endpoints) if contents, err := yaml.Marshal(cfg); err != nil { return nil, err @@ -540,7 +589,7 @@ func add5XXErrorHandling(cfg *gateway.TraefikConfig, dwId string) { } // makes '/healthz' path of main endpoints reachable from the outside -func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, dwId string, endpoints map[string]dwo.EndpointList) { +func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, username string, dwName string, 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) { @@ -551,7 +600,7 @@ func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, dwId string, endpoints routeName, endpointPath := createEndpointPath(&e, componentName) routerName := fmt.Sprintf("%s-%s-healthz", dwId, routeName) cfg.HTTP.Routers[routerName] = &gateway.TraefikConfigRouter{ - Rule: fmt.Sprintf("Path(`/%s%s/healthz`)", dwId, endpointPath), + Rule: fmt.Sprintf("Path(`/%s/%s%s/healthz`)", username, dwName, endpointPath), Service: dwId, Middlewares: middlewares, Priority: 101, @@ -602,7 +651,7 @@ func createEndpointPath(e *dwo.Endpoint, componentName string) (routeName string // if endpoint is NOT unique, we're exposing on /componentName/ routeName = strconv.Itoa(e.TargetPort) } - path = fmt.Sprintf("/%s/%s", componentName, routeName) + path = fmt.Sprintf("/%s", routeName) return routeName, path } @@ -713,20 +762,20 @@ func getServiceURL(port int32, workspaceID string, workspaceNamespace string) st return fmt.Sprintf("http://%s.%s.svc:%d", common.ServiceName(workspaceID), workspaceNamespace, port) } -func getPublicURLPrefixForEndpoint(workspaceID string, machineName string, endpoint dwo.Endpoint) string { +func getPublicURLPrefixForEndpoint(dwName string, username string, endpoint dwo.Endpoint) string { endpointName := "" if endpoint.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" { endpointName = endpoint.Name } - return getPublicURLPrefix(workspaceID, machineName, int32(endpoint.TargetPort), endpointName) + return getPublicURLPrefix(dwName, username, int32(endpoint.TargetPort), endpointName) } -func getPublicURLPrefix(workspaceID string, machineName string, port int32, uniqueEndpointName string) string { +func getPublicURLPrefix(dwName string, username string, port int32, uniqueEndpointName string) string { if uniqueEndpointName == "" { - return fmt.Sprintf(endpointURLPrefixPattern, workspaceID, machineName, port) + return fmt.Sprintf(endpointURLPrefixPattern, username, dwName, port) } - return fmt.Sprintf(uniqueEndpointURLPrefixPattern, workspaceID, machineName, uniqueEndpointName) + return fmt.Sprintf(uniqueEndpointURLPrefixPattern, username, dwName, uniqueEndpointName) } func determineEndpointScheme(e dwo.Endpoint) string { diff --git a/controllers/devworkspace/solver/che_routing_test.go b/controllers/devworkspace/solver/che_routing_test.go index a147cd2055..8f29039aec 100644 --- a/controllers/devworkspace/solver/che_routing_test.go +++ b/controllers/devworkspace/solver/che_routing_test.go @@ -45,6 +45,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/yaml" ) @@ -66,7 +67,7 @@ func createTestScheme() *runtime.Scheme { func getSpecObjectsForManager(t *testing.T, mgr *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, additionalInitialObjects ...runtime.Object) (client.Client, solvers.RoutingSolver, solvers.RoutingObjects) { scheme := createTestScheme() - allObjs := []runtime.Object{mgr} + allObjs := []runtime.Object{mgr, routing} for i := range additionalInitialObjects { allObjs = append(allObjs, additionalInitialObjects[i]) } @@ -95,6 +96,26 @@ func getSpecObjectsForManager(t *testing.T, mgr *chev2.CheCluster, routing *dwo. t.Fatal(err) } + // set owner references for the routing objects + for idx := range objs.Services { + err := controllerutil.SetControllerReference(routing, &objs.Services[idx], scheme) + if err != nil { + t.Fatal(err) + } + } + for idx := range objs.Ingresses { + err := controllerutil.SetControllerReference(routing, &objs.Ingresses[idx], scheme) + if err != nil { + t.Fatal(err) + } + } + for idx := range objs.Routes { + err := controllerutil.SetControllerReference(routing, &objs.Routes[idx], scheme) + if err != nil { + t.Fatal(err) + } + } + // 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"}}) @@ -114,7 +135,7 @@ func getSpecObjects(t *testing.T, routing *dwo.DevWorkspaceRouting) (client.Clie Hostname: "over.the.rainbow", }, }, - }, routing) + }, routing, userProfileSecret()) } func subdomainDevWorkspaceRouting() *dwo.DevWorkspaceRouting { @@ -122,6 +143,14 @@ func subdomainDevWorkspaceRouting() *dwo.DevWorkspaceRouting { ObjectMeta: metav1.ObjectMeta{ Name: "routing", Namespace: "ws", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "workspace.devfile.io/v1alpha2", + Kind: "DevWorkspace", + Name: "my-workspace", + UID: "uid", + }, + }, }, Spec: dwo.DevWorkspaceRoutingSpec{ DevWorkspaceId: "wsid", @@ -159,6 +188,14 @@ func relocatableDevWorkspaceRouting() *dwo.DevWorkspaceRouting { ObjectMeta: metav1.ObjectMeta{ Name: "routing", Namespace: "ws", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "workspace.devfile.io/v1alpha2", + Kind: "DevWorkspace", + Name: "my-workspace", + UID: "uid", + }, + }, }, Spec: dwo.DevWorkspaceRoutingSpec{ DevWorkspaceId: "wsid", @@ -201,6 +238,19 @@ func relocatableDevWorkspaceRouting() *dwo.DevWorkspaceRouting { } } +func userProfileSecret() *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-profile", + Namespace: "ws", + Finalizers: []string{controller.FinalizerName}, + }, + Data: map[string][]byte{ + "name": []byte("username"), + }, + } +} + func TestCreateRelocatedObjectsK8S(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) cl, _, objs := getSpecObjects(t, relocatableDevWorkspaceRouting()) @@ -342,7 +392,7 @@ func TestCreateRelocatedObjectsK8S(t *testing.T) { healthzName := "wsid-9999-healthz" assert.Contains(t, workspaceMainConfig.HTTP.Routers, healthzName) assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Service, wsid) - assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/wsid/m1/9999/healthz`)") + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/username/my-workspace/9999/healthz`)") assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.AuthMiddlewareSuffix) assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.StripPrefixMiddlewareSuffix) assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.HeaderRewriteMiddlewareSuffix) @@ -352,7 +402,7 @@ func TestCreateRelocatedObjectsK8S(t *testing.T) { healthzName := "wsid-m1-9999-healthz" assert.Contains(t, workspaceConfig.HTTP.Routers, healthzName) assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Service, healthzName) - assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/m1/9999/healthz`)") + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/9999/healthz`)") assert.NotContains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.AuthMiddlewareSuffix) assert.Contains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.StripPrefixMiddlewareSuffix) }) @@ -447,7 +497,7 @@ func TestCreateRelocatedObjectsOpenshift(t *testing.T) { healthzName := "wsid-9999-healthz" assert.Contains(t, workspaceMainConfig.HTTP.Routers, healthzName) assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Service, wsid) - assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/wsid/m1/9999/healthz`)") + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/username/my-workspace/9999/healthz`)") assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.AuthMiddlewareSuffix) assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.StripPrefixMiddlewareSuffix) assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.HeaderRewriteMiddlewareSuffix) @@ -457,7 +507,7 @@ func TestCreateRelocatedObjectsOpenshift(t *testing.T) { healthzName := "wsid-m1-9999-healthz" assert.Contains(t, workspaceConfig.HTTP.Routers, healthzName) assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Service, healthzName) - assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/m1/9999/healthz`)") + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/9999/healthz`)") assert.NotContains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.AuthMiddlewareSuffix) assert.Contains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.StripPrefixMiddlewareSuffix) }) @@ -472,6 +522,14 @@ func TestUniqueMainEndpoint(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "routing", Namespace: "ws", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "workspace.devfile.io/v1alpha2", + Kind: "DevWorkspace", + Name: "my-workspace", + UID: "uid", + }, + }, }, Spec: dwo.DevWorkspaceRoutingSpec{ DevWorkspaceId: wsid, @@ -523,7 +581,7 @@ func TestUniqueMainEndpoint(t *testing.T) { healthzName := wsid + "-e1-healthz" assert.Contains(t, workspaceMainConfig.HTTP.Routers, healthzName) assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Service, wsid) - assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/"+wsid+"/m1/e1/healthz`)") + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/username/my-workspace/e1/healthz`)") assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, wsid+gateway.AuthMiddlewareSuffix) assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, wsid+gateway.StripPrefixMiddlewareSuffix) assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, wsid+gateway.HeaderRewriteMiddlewareSuffix) @@ -533,7 +591,7 @@ func TestUniqueMainEndpoint(t *testing.T) { healthzName := wsid + "-m1-e1-healthz" assert.Contains(t, workspaceConfig.HTTP.Routers, healthzName) assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Service, healthzName) - assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/m1/e1/healthz`)") + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/e1/healthz`)") assert.NotContains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.AuthMiddlewareSuffix) assert.Contains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.StripPrefixMiddlewareSuffix) }) @@ -588,14 +646,14 @@ func TestCreateSubDomainObjects(t *testing.T) { if len(objs.Ingresses) != 3 { t.Error("Expected 3 ingress, found ", len(objs.Ingresses)) } - if objs.Ingresses[0].Spec.Rules[0].Host != "wsid-1.down.on.earth" { - t.Error("Expected Ingress host 'wsid-1.down.on.earth', but got ", objs.Ingresses[0].Spec.Rules[0].Host) + if objs.Ingresses[0].Spec.Rules[0].Host != "username.my-workspace.e1.down.on.earth" { + t.Error("Expected Ingress host 'username.my-workspace.e1.down.on.earth', but got ", objs.Ingresses[0].Spec.Rules[0].Host) } - if objs.Ingresses[1].Spec.Rules[0].Host != "wsid-2.down.on.earth" { - t.Error("Expected Ingress host 'wsid-2.down.on.earth', but got ", objs.Ingresses[1].Spec.Rules[0].Host) + if objs.Ingresses[1].Spec.Rules[0].Host != "username.my-workspace.e2.down.on.earth" { + t.Error("Expected Ingress host 'username.my-workspace.e2.down.on.earth', but got ", objs.Ingresses[1].Spec.Rules[0].Host) } - if objs.Ingresses[2].Spec.Rules[0].Host != "wsid-3.down.on.earth" { - t.Error("Expected Ingress host 'wsid-3.down.on.earth', but got ", objs.Ingresses[2].Spec.Rules[0].Host) + if objs.Ingresses[2].Spec.Rules[0].Host != "username.my-workspace.e3.down.on.earth" { + t.Error("Expected Ingress host 'username.my-workspace.e3.down.on.earth', but got ", objs.Ingresses[2].Spec.Rules[0].Host) } }) @@ -604,8 +662,8 @@ func TestCreateSubDomainObjects(t *testing.T) { if len(objs.Routes) != 3 { t.Error("Expected 3 Routes, found ", len(objs.Routes)) } - if objs.Routes[0].Spec.Host != "wsid-1.down.on.earth" { - t.Error("Expected Route host 'wsid-1.down.on.earth', but got ", objs.Routes[0].Spec.Host) + if objs.Routes[0].Spec.Host != "username.my-workspace.e1.down.on.earth" { + t.Error("Expected Route host 'username.my-workspace.e1.down.on.earth', but got ", objs.Routes[0].Spec.Host) } }) } @@ -643,24 +701,24 @@ func TestReportRelocatableExposedEndpoints(t *testing.T) { if e1.Name != "e1" { t.Errorf("The first endpoint should have been e1 but is %s", e1.Name) } - if e1.Url != "https://over.the.rainbow/wsid/m1/9999/1/" { - t.Errorf("The e1 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/wsid/m1/9999/1/", e1.Url) + if e1.Url != "https://over.the.rainbow/username/my-workspace/9999/1/" { + t.Errorf("The e1 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/username/my-workspace/9999/1/", e1.Url) } e2 := m1[1] if e2.Name != "e2" { t.Errorf("The second endpoint should have been e2 but is %s", e1.Name) } - if e2.Url != "https://over.the.rainbow/wsid/m1/9999/2.js" { - t.Errorf("The e2 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/wsid/m1/9999/2.js", e2.Url) + if e2.Url != "https://over.the.rainbow/username/my-workspace/9999/2.js" { + t.Errorf("The e2 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/username/my-workspace/9999/2.js", e2.Url) } e3 := m1[2] if e3.Name != "e3" { t.Errorf("The third endpoint should have been e3 but is %s", e1.Name) } - if e3.Url != "https://over.the.rainbow/wsid/m1/9999/" { - t.Errorf("The e3 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/wsid/m1/9999/", e3.Url) + if e3.Url != "https://over.the.rainbow/username/my-workspace/9999/" { + t.Errorf("The e3 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/username/my-workspace/9999/", e3.Url) } } @@ -671,6 +729,14 @@ func TestExposeEndpoints(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "routing", Namespace: "ws", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "workspace.devfile.io/v1alpha2", + Kind: "DevWorkspace", + Name: "my-workspace", + UID: "uid", + }, + }, }, Spec: dwo.DevWorkspaceRoutingSpec{ DevWorkspaceId: "wsid", @@ -765,13 +831,13 @@ func TestExposeEndpoints(t *testing.T) { assert.True(t, ok) assert.Equal(t, 1, len(sp)) assert.Equal(t, "server-pub", sp[0].Name) - assert.Equal(t, "https://over.the.rainbow/wsid/server-public/8082/", sp[0].Url) + assert.Equal(t, "https://over.the.rainbow/username/my-workspace/8082/", sp[0].Url) spnr, ok := exposed["server-public-no-rewrite"] assert.True(t, ok) assert.Equal(t, 1, len(spnr)) assert.Equal(t, "server-pub-nr", spnr[0].Name) - assert.Equal(t, "http://wsid-1.down.on.earth/", spnr[0].Url) + assert.Equal(t, "http://username.my-workspace.server-pub-nr.down.on.earth/", spnr[0].Url) } func TestReportSubdomainExposedEndpoints(t *testing.T) { @@ -805,24 +871,24 @@ func TestReportSubdomainExposedEndpoints(t *testing.T) { if e1.Name != "e1" { t.Errorf("The first endpoint should have been e1 but is %s", e1.Name) } - if e1.Url != "https://wsid-1.down.on.earth/1/" { - t.Errorf("The e1 endpoint should have the following URL: '%s' but has '%s'.", "https://wsid-1.down.on.earth/1/", e1.Url) + if e1.Url != "https://username.my-workspace.e1.down.on.earth/1/" { + t.Errorf("The e1 endpoint should have the following URL: '%s' but has '%s'.", "https://username.my-workspace.e1.down.on.earth/1/", e1.Url) } e2 := m1[1] if e2.Name != "e2" { t.Errorf("The second endpoint should have been e2 but is %s", e1.Name) } - if e2.Url != "https://wsid-2.down.on.earth/2.js" { - t.Errorf("The e2 endpoint should have the following URL: '%s' but has '%s'.", "https://wsid-2.down.on.earth/2.js", e2.Url) + if e2.Url != "https://username.my-workspace.e2.down.on.earth/2.js" { + t.Errorf("The e2 endpoint should have the following URL: '%s' but has '%s'.", "https://username.my-workspace.e2.down.on.earth/2.js", e2.Url) } e3 := m1[2] if e3.Name != "e3" { t.Errorf("The third endpoint should have been e3 but is %s", e1.Name) } - if e3.Url != "http://wsid-3.down.on.earth/" { - t.Errorf("The e3 endpoint should have the following URL: '%s' but has '%s'.", "https://wsid-3.down.on.earth/", e3.Url) + if e3.Url != "http://username.my-workspace.e3.down.on.earth/" { + t.Errorf("The e3 endpoint should have the following URL: '%s' but has '%s'.", "http://username.my-workspace.e3.down.on.earth/", e3.Url) } } @@ -890,7 +956,7 @@ func TestUsesIngressAnnotationsForWorkspaceEndpointIngresses(t *testing.T) { }, } - _, _, objs := getSpecObjectsForManager(t, mgr, subdomainDevWorkspaceRouting()) + _, _, objs := getSpecObjectsForManager(t, mgr, subdomainDevWorkspaceRouting(), userProfileSecret()) if len(objs.Ingresses) != 3 { t.Fatalf("Unexpected number of generated ingresses: %d", len(objs.Ingresses)) @@ -925,7 +991,7 @@ func TestUsesCustomCertificateForWorkspaceEndpointIngresses(t *testing.T) { }, } - _, _, objs := getSpecObjectsForManager(t, mgr, subdomainDevWorkspaceRouting(), &corev1.Secret{ + _, _, objs := getSpecObjectsForManager(t, mgr, subdomainDevWorkspaceRouting(), userProfileSecret(), &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "tlsSecret", Namespace: "ns", @@ -954,7 +1020,7 @@ func TestUsesCustomCertificateForWorkspaceEndpointIngresses(t *testing.T) { t.Fatalf("Unexpected number of host records on the TLS spec: %d", len(ingress.Spec.TLS[0].Hosts)) } - if ingress.Spec.TLS[0].Hosts[0] != "wsid-1.almost.trivial" { + if ingress.Spec.TLS[0].Hosts[0] != "username.my-workspace.e1.almost.trivial" { t.Errorf("Unexpected host name of the TLS spec: %s", ingress.Spec.TLS[0].Hosts[0]) } @@ -972,7 +1038,7 @@ func TestUsesCustomCertificateForWorkspaceEndpointIngresses(t *testing.T) { t.Fatalf("Unexpected number of host records on the TLS spec: %d", len(ingress.Spec.TLS[0].Hosts)) } - if ingress.Spec.TLS[0].Hosts[0] != "wsid-2.almost.trivial" { + if ingress.Spec.TLS[0].Hosts[0] != "username.my-workspace.e2.almost.trivial" { t.Errorf("Unexpected host name of the TLS spec: %s", ingress.Spec.TLS[0].Hosts[0]) } @@ -1001,7 +1067,7 @@ func TestUsesCustomCertificateForWorkspaceEndpointRoutes(t *testing.T) { }, } - _, _, objs := getSpecObjectsForManager(t, mgr, subdomainDevWorkspaceRouting(), &corev1.Secret{ + _, _, objs := getSpecObjectsForManager(t, mgr, subdomainDevWorkspaceRouting(), userProfileSecret(), &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "tlsSecret", Namespace: "ns", diff --git a/controllers/devworkspace/solver/endpoint_exposer.go b/controllers/devworkspace/solver/endpoint_exposer.go index 6ee93062a8..dd8ce06aa6 100644 --- a/controllers/devworkspace/solver/endpoint_exposer.go +++ b/controllers/devworkspace/solver/endpoint_exposer.go @@ -142,7 +142,7 @@ func (e *IngressExposer) initFrom(ctx context.Context, cl client.Client, cluster return nil } -func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo) routev1.Route { +func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo, username string, dwName string) routev1.Route { targetEndpoint := intstr.FromInt(int(endpoint.port)) labels := labels.Merge( e.labels, @@ -159,7 +159,7 @@ func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo) routev1.Route OwnerReferences: endpoint.service.OwnerReferences, }, Spec: routev1.RouteSpec{ - Host: hostName(endpoint.order, e.devWorkspaceID, e.baseDomain), + Host: hostName(username, dwName, endpoint.endpointName, e.baseDomain), To: routev1.RouteTargetReference{ Kind: "Service", Name: endpoint.service.Name, @@ -185,8 +185,8 @@ func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo) routev1.Route return route } -func (e *IngressExposer) getIngressForService(endpoint *EndpointInfo) networkingv1.Ingress { - hostname := hostName(endpoint.order, e.devWorkspaceID, e.baseDomain) +func (e *IngressExposer) getIngressForService(endpoint *EndpointInfo, username string, dwName string) networkingv1.Ingress { + hostname := hostName(username, dwName, endpoint.endpointName, e.baseDomain) ingressPathType := networkingv1.PathTypeImplementationSpecific ingress := networkingv1.Ingress{ @@ -239,8 +239,8 @@ func (e *IngressExposer) getIngressForService(endpoint *EndpointInfo) networking return ingress } -func hostName(order int, workspaceID string, baseDomain string) string { - return fmt.Sprintf("%s-%d.%s", workspaceID, order, baseDomain) +func hostName(username string, workspaceName string, endpointName string, baseDomain string) string { + return fmt.Sprintf("%s.%s.%s.%s", username, workspaceName, endpointName, baseDomain) } func routeAnnotations(machineName string, endpointName string) map[string]string { From 878a08ad83182ef571bf839b56d43f4a4edad84a Mon Sep 17 00:00:00 2001 From: David Kwon Date: Thu, 18 May 2023 14:59:18 -0400 Subject: [PATCH 2/8] Add update verb for ingress in che-operator CR Signed-off-by: David Kwon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 820898d381..e3d3c4bc00 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - [Builds](#Builds) - [License](#License) - +d ## Description Che/Red Hat OpenShift Dev Spaces operator uses [Operator SDK](https://github.com/operator-framework/operator-sdk) and [Go Kube client](https://github.com/kubernetes/client-go) to deploy, update and manage K8S/OpenShift resources that constitute a multi-user Eclipse Che/Red Hat OpenShift Dev Spaces cluster. From 40748285e78e260c5eb0d652f9dc8ecd92be1e43 Mon Sep 17 00:00:00 2001 From: David Kwon Date: Thu, 18 May 2023 14:59:58 -0400 Subject: [PATCH 3/8] Legacy support Signed-off-by: David Kwon --- .../devworkspace/solver/che_routing.go | 122 +-- .../devworkspace/solver/che_routing_test.go | 712 +++++++++++++++++- .../devworkspace/solver/endpoint_exposer.go | 12 +- .../devworkspace/solver/endpoint_strategy.go | 114 +++ 4 files changed, 896 insertions(+), 64 deletions(-) create mode 100644 controllers/devworkspace/solver/endpoint_strategy.go diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index ebc3fa519e..2843ea879a 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -17,7 +17,6 @@ import ( "fmt" "path" "regexp" - "strconv" "strings" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" @@ -280,17 +279,25 @@ func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, wor return map[string]dwo.ExposedEndpointList{}, false, nil } + useLegacyRouting := false username, err := getNormalizedUsername(c.client, routingObj.Services[0].Namespace) if err != nil { - return map[string]dwo.ExposedEndpointList{}, false, err + useLegacyRouting = true } dwName, err := getNormalizedWkspName(c.client, routingObj.Services[0].Namespace, routingObj.Services[0].ObjectMeta.OwnerReferences[0].Name) if err != nil { - return map[string]dwo.ExposedEndpointList{}, false, err + useLegacyRouting = true } - publicURLPrefix := getPublicURLPrefixForEndpoint(dwName, username, endpoint) + var endpointStrategy EndpointStrategy + if useLegacyRouting { + endpointStrategy = Legacy{workspaceID: workspaceID, componentName: component} + } else { + endpointStrategy = UsernameWkspName{username: username, workspaceName: dwName} + } + + publicURLPrefix := getPublicURLPrefixForEndpoint(endpoint, endpointStrategy) endpointURL = path.Join(gatewayHost, publicURLPrefix, endpoint.Path) } @@ -340,28 +347,37 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch } configs := make([]corev1.ConfigMap, 0) + + useLegacyRouting := false username, err := getNormalizedUsername(c.client, routing.Namespace) if err != nil { - return nil, err + useLegacyRouting = true } dwName, err := getNormalizedWkspName(c.client, routing.Namespace, routing.Name) if err != nil { - return nil, err + useLegacyRouting = true + } + + var endpointStrategy EndpointStrategy + if useLegacyRouting { + endpointStrategy = Legacy{workspaceID: workspaceID} + } else { + endpointStrategy = UsernameWkspName{username: username, workspaceName: dwName} } // first do routing from main che-gateway into workspace service - if mainWsRouteConfig, err := provisionMainWorkspaceRoute(cheCluster, routing, username, dwName, cmLabels); err != nil { + if mainWsRouteConfig, err := provisionMainWorkspaceRoute(cheCluster, routing, cmLabels, endpointStrategy); err != nil { return nil, err } else { configs = append(configs, *mainWsRouteConfig) } // then expose the endpoints - if infraExposer, err := c.getInfraSpecificExposer(cheCluster, routing, objs, username, dwName); err != nil { + if infraExposer, err := c.getInfraSpecificExposer(cheCluster, routing, objs, endpointStrategy); err != nil { return nil, err } else { - if workspaceConfig := exposeAllEndpoints(cheCluster, routing, objs, infraExposer); workspaceConfig != nil { + if workspaceConfig := exposeAllEndpoints(cheCluster, routing, objs, infraExposer, endpointStrategy); workspaceConfig != nil { configs = append(configs, *workspaceConfig) } } @@ -370,6 +386,19 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch } func getNormalizedUsername(c client.Client, namespace string) (string, error) { + username, err := getUsernameFromNamespace(c, namespace) + + if err != nil { + username, err = getUsernameFromSecret(c, namespace) + } + + if err != nil { + return "", err + } + return normalize(username), nil +} + +func getUsernameFromSecret(c client.Client, namespace string) (string, error) { secret := &corev1.Secret{} err := c.Get(context.TODO(), client.ObjectKey{Name: "user-profile", Namespace: namespace}, secret) if err != nil { @@ -379,6 +408,28 @@ func getNormalizedUsername(c client.Client, namespace string) (string, error) { return normalize(username), nil } +func getUsernameFromNamespace(c client.Client, namespace string) (string, error) { + nsInfo := &corev1.Namespace{} + err := c.Get(context.TODO(), client.ObjectKey{Name: namespace}, nsInfo) + if err != nil { + return "", err + } + + notFoundError := fmt.Errorf("username not found in namespace %s", namespace) + + if nsInfo.GetAnnotations() == nil { + return "", notFoundError + } + + username, exists := nsInfo.GetAnnotations()["che.eclipse.org/username"] + + if exists { + return username, nil + } + + return "", notFoundError +} + func getNormalizedWkspName(c client.Client, namespace string, routingName string) (string, error) { routing := &dwo.DevWorkspaceRouting{} err := c.Get(context.TODO(), client.ObjectKey{Name: routingName, Namespace: namespace}, routing) @@ -398,14 +449,14 @@ func normalize(username string) string { return r3.ReplaceAllString(result, "") // trim dashes at beginning/end } -func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects, username string, dwName string) (func(info *EndpointInfo), error) { +func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects, endpointStrategy EndpointStrategy) (func(info *EndpointInfo), error) { if infrastructure.IsOpenShift() { exposer := &RouteExposer{} if err := exposer.initFrom(context.TODO(), c.client, cheCluster, routing); err != nil { return nil, err } return func(info *EndpointInfo) { - route := exposer.getRouteForService(info, username, dwName) + route := exposer.getRouteForService(info, endpointStrategy) objs.Routes = append(objs.Routes, route) }, nil } else { @@ -414,7 +465,7 @@ func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, return nil, err } return func(info *EndpointInfo) { - ingress := exposer.getIngressForService(info, username, dwName) + ingress := exposer.getIngressForService(info, endpointStrategy) objs.Ingresses = append(objs.Ingresses, ingress) }, nil } @@ -430,7 +481,7 @@ func getCommonService(objs *solvers.RoutingObjects, dwId string) *corev1.Service return nil } -func exposeAllEndpoints(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects, ingressExpose func(*EndpointInfo)) *corev1.ConfigMap { +func exposeAllEndpoints(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects, ingressExpose func(*EndpointInfo), endpointStrategy EndpointStrategy) *corev1.ConfigMap { wsRouteConfig := gateway.CreateEmptyTraefikConfig() commonService := getCommonService(objs, routing.Spec.DevWorkspaceId) @@ -461,7 +512,7 @@ func exposeAllEndpoints(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceR } if e.Attributes.GetString(urlRewriteSupportedEndpointAttributeName, nil) == "true" { - addEndpointToTraefikConfig(componentName, e, wsRouteConfig, cheCluster, routing) + addEndpointToTraefikConfig(componentName, e, wsRouteConfig, cheCluster, routing, endpointStrategy) } else { ingressExpose(&EndpointInfo{ order: order, @@ -522,16 +573,16 @@ func containPort(service *corev1.Service, port int32) bool { return false } -func provisionMainWorkspaceRoute(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, username string, dwName string, cmLabels map[string]string) (*corev1.ConfigMap, error) { +func provisionMainWorkspaceRoute(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, cmLabels map[string]string, endpointStrategy EndpointStrategy) (*corev1.ConfigMap, error) { dwId := routing.Spec.DevWorkspaceId dwNamespace := routing.Namespace cfg := gateway.CreateCommonTraefikConfig( dwId, - fmt.Sprintf("PathPrefix(`/%s/%s`)", username, dwName), + fmt.Sprintf("PathPrefix(`%s`)", endpointStrategy.getMainWorkspacePathPrefix()), 100, getServiceURL(wsGatewayPort, dwId, dwNamespace), - []string{fmt.Sprintf("/%s/%s", username, dwName)}) + []string{endpointStrategy.getMainWorkspacePathPrefix()}) if cheCluster.IsAccessTokenConfigured() { cfg.AddAuthHeaderRewrite(dwId) @@ -543,7 +594,7 @@ func provisionMainWorkspaceRoute(cheCluster *chev2.CheCluster, routing *dwo.DevW add5XXErrorHandling(cfg, dwId) // make '/healthz' path of main endpoints reachable from outside - routeForHealthzEndpoint(cfg, username, dwName, dwId, routing.Spec.Endpoints) + routeForHealthzEndpoint(cfg, dwId, routing.Spec.Endpoints, endpointStrategy) if contents, err := yaml.Marshal(cfg); err != nil { return nil, err @@ -589,7 +640,7 @@ func add5XXErrorHandling(cfg *gateway.TraefikConfig, dwId string) { } // makes '/healthz' path of main endpoints reachable from the outside -func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, username string, dwName string, dwId string, endpoints map[string]dwo.EndpointList) { +func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, dwId string, endpoints map[string]dwo.EndpointList, endpointStrategy EndpointStrategy) { for componentName, endpoints := range endpoints { for _, e := range endpoints { if e.Attributes.GetString(string(dwo.TypeEndpointAttribute), nil) == string(dwo.MainEndpointType) { @@ -597,10 +648,10 @@ func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, username string, dwName if infrastructure.IsOpenShift() { middlewares = append(middlewares, dwId+gateway.HeaderRewriteMiddlewareSuffix) } - routeName, endpointPath := createEndpointPath(&e, componentName) + routeName, endpointPath := endpointStrategy.getEndpointPath(&e, componentName) routerName := fmt.Sprintf("%s-%s-healthz", dwId, routeName) cfg.HTTP.Routers[routerName] = &gateway.TraefikConfigRouter{ - Rule: fmt.Sprintf("Path(`/%s/%s%s/healthz`)", username, dwName, endpointPath), + Rule: fmt.Sprintf("Path(`%s/healthz`)", endpointStrategy.getEndpointPathPrefix(endpointPath)), Service: dwId, Middlewares: middlewares, Priority: 101, @@ -610,8 +661,8 @@ func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, username string, dwName } } -func addEndpointToTraefikConfig(componentName string, e dwo.Endpoint, cfg *gateway.TraefikConfig, cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting) { - routeName, prefix := createEndpointPath(&e, componentName) +func addEndpointToTraefikConfig(componentName string, e dwo.Endpoint, cfg *gateway.TraefikConfig, cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, endpointStrategy EndpointStrategy) { + routeName, prefix := endpointStrategy.getEndpointPath(&e, componentName) rulePrefix := fmt.Sprintf("PathPrefix(`%s`)", prefix) // skip if exact same route is already exposed @@ -643,19 +694,6 @@ func addEndpointToTraefikConfig(componentName string, e dwo.Endpoint, cfg *gatew } } -func createEndpointPath(e *dwo.Endpoint, componentName string) (routeName string, path string) { - if e.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" { - // if endpoint is unique, we're exposing on /componentName/ - routeName = e.Name - } else { - // if endpoint is NOT unique, we're exposing on /componentName/ - routeName = strconv.Itoa(e.TargetPort) - } - path = fmt.Sprintf("/%s", routeName) - - return routeName, path -} - func findServiceForPort(port int32, objs *solvers.RoutingObjects) *corev1.Service { for i := range objs.Services { svc := &objs.Services[i] @@ -762,20 +800,12 @@ func getServiceURL(port int32, workspaceID string, workspaceNamespace string) st return fmt.Sprintf("http://%s.%s.svc:%d", common.ServiceName(workspaceID), workspaceNamespace, port) } -func getPublicURLPrefixForEndpoint(dwName string, username string, endpoint dwo.Endpoint) string { +func getPublicURLPrefixForEndpoint(endpoint dwo.Endpoint, endpointStrategy EndpointStrategy) string { endpointName := "" if endpoint.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" { endpointName = endpoint.Name } - - return getPublicURLPrefix(dwName, username, int32(endpoint.TargetPort), endpointName) -} - -func getPublicURLPrefix(dwName string, username string, port int32, uniqueEndpointName string) string { - if uniqueEndpointName == "" { - return fmt.Sprintf(endpointURLPrefixPattern, username, dwName, port) - } - return fmt.Sprintf(uniqueEndpointURLPrefixPattern, username, dwName, uniqueEndpointName) + return endpointStrategy.getPublicURLPrefix(int32(endpoint.TargetPort), endpointName) } func determineEndpointScheme(e dwo.Endpoint) string { diff --git a/controllers/devworkspace/solver/che_routing_test.go b/controllers/devworkspace/solver/che_routing_test.go index 8f29039aec..b207bd2134 100644 --- a/controllers/devworkspace/solver/che_routing_test.go +++ b/controllers/devworkspace/solver/che_routing_test.go @@ -377,6 +377,12 @@ func TestCreateRelocatedObjectsK8S(t *testing.T) { assert.Truef(t, found, "traefik config route doesn't set middleware '%s'", mware) } + t.Run("testEndpointInMainWorkspaceRoute", func(t *testing.T) { + assert.Contains(t, workspaceMainConfig.HTTP.Routers, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[wsid].Service, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[wsid].Rule, "PathPrefix(`/username/my-workspace`)") + }) + t.Run("testServerTransportInMainWorkspaceRoute", func(t *testing.T) { serverTransportName := wsid @@ -410,6 +416,143 @@ func TestCreateRelocatedObjectsK8S(t *testing.T) { }) } +func TestCreateRelocatedObjectsK8SLegacy(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + + cl, _, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, relocatableDevWorkspaceRouting()) + + t.Run("noIngresses", func(t *testing.T) { + if len(objs.Ingresses) != 0 { + t.Error() + } + }) + + t.Run("noRoutes", func(t *testing.T) { + if len(objs.Routes) != 0 { + t.Error() + } + }) + + t.Run("traefikConfig", func(t *testing.T) { + cms := &corev1.ConfigMapList{} + cl.List(context.TODO(), cms) + + assert.Len(t, cms.Items, 2) + + var workspaceMainCfg *corev1.ConfigMap + var workspaceCfg *corev1.ConfigMap + for _, cfg := range cms.Items { + if cfg.Name == "wsid-route" && cfg.Namespace == "ns" { + workspaceMainCfg = cfg.DeepCopy() + } + if cfg.Name == "wsid-route" && cfg.Namespace == "ws" { + workspaceCfg = cfg.DeepCopy() + } + } + + assert.NotNil(t, workspaceMainCfg) + + traefikMainWorkspaceConfig := workspaceMainCfg.Data["wsid.yml"] + assert.NotEmpty(t, traefikMainWorkspaceConfig) + + traefikWorkspaceConfig := workspaceCfg.Data["workspace.yml"] + assert.NotEmpty(t, traefikWorkspaceConfig) + + workspaceConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikWorkspaceConfig), &workspaceConfig)) + assert.Len(t, workspaceConfig.HTTP.Routers, 2) + + wsid := "wsid-m1-9999" + assert.Contains(t, workspaceConfig.HTTP.Routers, wsid) + assert.Len(t, workspaceConfig.HTTP.Routers[wsid].Middlewares, 2) + assert.Len(t, workspaceConfig.HTTP.Middlewares, 3) + + mwares := []string{wsid + gateway.StripPrefixMiddlewareSuffix} + for _, mware := range mwares { + assert.Contains(t, workspaceConfig.HTTP.Middlewares, mware) + found := false + for _, r := range workspaceConfig.HTTP.Routers[wsid].Middlewares { + if r == mware { + found = true + } + } + assert.True(t, found) + } + + workspaceMainConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikMainWorkspaceConfig), &workspaceMainConfig)) + assert.Len(t, workspaceMainConfig.HTTP.Middlewares, 5) + + wsid = "wsid" + mwares = []string{ + wsid + gateway.AuthMiddlewareSuffix, + wsid + gateway.StripPrefixMiddlewareSuffix, + wsid + gateway.HeadersMiddlewareSuffix, + wsid + gateway.ErrorsMiddlewareSuffix, + wsid + gateway.RetryMiddlewareSuffix} + for _, mware := range mwares { + assert.Contains(t, workspaceMainConfig.HTTP.Middlewares, mware) + + found := false + for _, r := range workspaceMainConfig.HTTP.Routers[wsid].Middlewares { + if r == mware { + found = true + } + } + assert.Truef(t, found, "traefik config route doesn't set middleware '%s'", mware) + } + + t.Run("testEndpointInMainWorkspaceRoute", func(t *testing.T) { + assert.Contains(t, workspaceMainConfig.HTTP.Routers, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[wsid].Service, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[wsid].Rule, fmt.Sprintf("PathPrefix(`/%s`)", wsid)) + }) + + t.Run("testServerTransportInMainWorkspaceRoute", func(t *testing.T) { + serverTransportName := wsid + + assert.Len(t, workspaceMainConfig.HTTP.ServersTransports, 1) + assert.Contains(t, workspaceMainConfig.HTTP.ServersTransports, serverTransportName) + + assert.Len(t, workspaceMainConfig.HTTP.Services, 1) + assert.Contains(t, workspaceMainConfig.HTTP.Services, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Services[wsid].LoadBalancer.ServersTransport, serverTransportName) + }) + + t.Run("testHealthzEndpointInMainWorkspaceRoute", func(t *testing.T) { + healthzName := "wsid-9999-healthz" + assert.Contains(t, workspaceMainConfig.HTTP.Routers, healthzName) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Service, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/wsid/m1/9999/healthz`)") + assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.AuthMiddlewareSuffix) + assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.StripPrefixMiddlewareSuffix) + assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.HeaderRewriteMiddlewareSuffix) + }) + + t.Run("testHealthzEndpointInWorkspaceRoute", func(t *testing.T) { + healthzName := "wsid-m1-9999-healthz" + assert.Contains(t, workspaceConfig.HTTP.Routers, healthzName) + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Service, healthzName) + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/m1/9999/healthz`)") + assert.NotContains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.AuthMiddlewareSuffix) + assert.Contains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.StripPrefixMiddlewareSuffix) + }) + + }) +} + func TestCreateRelocatedObjectsOpenshift(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) @@ -514,6 +657,114 @@ func TestCreateRelocatedObjectsOpenshift(t *testing.T) { }) } +func TestCreateRelocatedObjectsOpenshiftLegacy(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + + cl, _, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, relocatableDevWorkspaceRouting()) + + assert.Empty(t, objs.Ingresses) + assert.Empty(t, objs.Routes) + + t.Run("traefikConfig", func(t *testing.T) { + cms := &corev1.ConfigMapList{} + cl.List(context.TODO(), cms) + + assert.Len(t, cms.Items, 2) + + var workspaceMainCfg *corev1.ConfigMap + var workspaceCfg *corev1.ConfigMap + for _, cfg := range cms.Items { + if cfg.Name == "wsid-route" && cfg.Namespace == "ns" { + workspaceMainCfg = cfg.DeepCopy() + } + if cfg.Name == "wsid-route" && cfg.Namespace == "ws" { + workspaceCfg = cfg.DeepCopy() + } + } + + assert.NotNil(t, workspaceMainCfg, "traefik configuration for the workspace not found") + + traefikMainWorkspaceConfig := workspaceMainCfg.Data["wsid.yml"] + assert.NotEmpty(t, traefikMainWorkspaceConfig, "No traefik config file found in the main workspace config configmap") + + traefikWorkspaceConfig := workspaceCfg.Data["workspace.yml"] + assert.NotEmpty(t, traefikWorkspaceConfig, "No traefik config file found in the workspace config configmap") + + workspaceConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikWorkspaceConfig), &workspaceConfig)) + + wsid := "wsid-m1-9999" + assert.Contains(t, workspaceConfig.HTTP.Routers, wsid) + assert.Len(t, workspaceConfig.HTTP.Routers[wsid].Middlewares, 2) + + workspaceMainConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikMainWorkspaceConfig), &workspaceMainConfig)) + assert.Len(t, workspaceMainConfig.HTTP.Middlewares, 6) + + wsid = "wsid" + mwares := []string{ + wsid + gateway.AuthMiddlewareSuffix, + wsid + gateway.StripPrefixMiddlewareSuffix, + wsid + gateway.HeaderRewriteMiddlewareSuffix, + wsid + gateway.HeadersMiddlewareSuffix, + wsid + gateway.ErrorsMiddlewareSuffix, + wsid + gateway.RetryMiddlewareSuffix} + for _, mware := range mwares { + assert.Contains(t, workspaceMainConfig.HTTP.Middlewares, mware) + + found := false + for _, r := range workspaceMainConfig.HTTP.Routers[wsid].Middlewares { + if r == mware { + found = true + } + } + assert.Truef(t, found, "traefik config route doesn't set middleware '%s'", mware) + } + + t.Run("testServerTransportInMainWorkspaceRoute", func(t *testing.T) { + serverTransportName := wsid + + assert.Len(t, workspaceMainConfig.HTTP.ServersTransports, 1) + assert.Contains(t, workspaceMainConfig.HTTP.ServersTransports, serverTransportName) + + assert.Len(t, workspaceMainConfig.HTTP.Services, 1) + assert.Contains(t, workspaceMainConfig.HTTP.Services, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Services[wsid].LoadBalancer.ServersTransport, serverTransportName) + }) + + t.Run("testHealthzEndpointInMainWorkspaceRoute", func(t *testing.T) { + healthzName := "wsid-9999-healthz" + assert.Contains(t, workspaceMainConfig.HTTP.Routers, healthzName) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Service, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/wsid/m1/9999/healthz`)") + assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.AuthMiddlewareSuffix) + assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.StripPrefixMiddlewareSuffix) + assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, "wsid"+gateway.HeaderRewriteMiddlewareSuffix) + }) + + t.Run("testHealthzEndpointInWorkspaceRoute", func(t *testing.T) { + healthzName := "wsid-m1-9999-healthz" + assert.Contains(t, workspaceConfig.HTTP.Routers, healthzName) + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Service, healthzName) + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/m1/9999/healthz`)") + assert.NotContains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.AuthMiddlewareSuffix) + assert.Contains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.StripPrefixMiddlewareSuffix) + }) + }) +} + func TestUniqueMainEndpoint(t *testing.T) { wsid := "wsid123" @@ -597,11 +848,192 @@ func TestUniqueMainEndpoint(t *testing.T) { }) } -func TestCreateSubDomainObjects(t *testing.T) { +func TestUniqueMainEndpointLegacy(t *testing.T) { + wsid := "wsid123" + + infrastructure.InitializeForTesting(infrastructure.OpenShiftv4) + + routing := &dwo.DevWorkspaceRouting{ + ObjectMeta: metav1.ObjectMeta{ + Name: "routing", + Namespace: "ws", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "workspace.devfile.io/v1alpha2", + Kind: "DevWorkspace", + Name: "my-workspace", + UID: "uid", + }, + }, + }, + Spec: dwo.DevWorkspaceRoutingSpec{ + DevWorkspaceId: wsid, + RoutingClass: "che", + Endpoints: map[string]dwo.EndpointList{ + "m1": { + { + Name: "e1", + TargetPort: 9999, + Exposure: dwo.PublicEndpointExposure, + Protocol: "https", + Path: "/1/", + Attributes: dwo.Attributes{ + urlRewriteSupportedEndpointAttributeName: apiext.JSON{Raw: []byte("\"true\"")}, + string(dwo.TypeEndpointAttribute): apiext.JSON{Raw: []byte("\"main\"")}, + uniqueEndpointAttributeName: apiext.JSON{Raw: []byte("\"true\"")}, + }, + }, + }, + }, + }, + } + + cl, _, _ := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, routing) + + cms := &corev1.ConfigMapList{} + cl.List(context.TODO(), cms) + + assert.Len(t, cms.Items, 2) + + var workspaceMainCfg *corev1.ConfigMap + var workspaceCfg *corev1.ConfigMap + for _, cfg := range cms.Items { + if cfg.Name == wsid+"-route" && cfg.Namespace == "ns" { + workspaceMainCfg = cfg.DeepCopy() + } + if cfg.Name == wsid+"-route" && cfg.Namespace == "ws" { + workspaceCfg = cfg.DeepCopy() + } + } + + traefikWorkspaceConfig := workspaceCfg.Data["workspace.yml"] + workspaceConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikWorkspaceConfig), &workspaceConfig)) + + traefikMainWorkspaceConfig := workspaceMainCfg.Data[wsid+".yml"] + workspaceMainConfig := gateway.TraefikConfig{} + assert.NoError(t, yaml.Unmarshal([]byte(traefikMainWorkspaceConfig), &workspaceMainConfig)) + + t.Run("testHealthzEndpointInMainWorkspaceRoute", func(t *testing.T) { + healthzName := wsid + "-e1-healthz" + assert.Contains(t, workspaceMainConfig.HTTP.Routers, healthzName) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Service, wsid) + assert.Equal(t, workspaceMainConfig.HTTP.Routers[healthzName].Rule, "Path(`/"+wsid+"/m1/e1/healthz`)") + assert.NotContains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, wsid+gateway.AuthMiddlewareSuffix) + assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, wsid+gateway.StripPrefixMiddlewareSuffix) + assert.Contains(t, workspaceMainConfig.HTTP.Routers[healthzName].Middlewares, wsid+gateway.HeaderRewriteMiddlewareSuffix) + }) + + t.Run("testHealthzEndpointInWorkspaceRoute", func(t *testing.T) { + healthzName := wsid + "-m1-e1-healthz" + assert.Contains(t, workspaceConfig.HTTP.Routers, healthzName) + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Service, healthzName) + assert.Equal(t, workspaceConfig.HTTP.Routers[healthzName].Rule, "Path(`/m1/e1/healthz`)") + assert.NotContains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.AuthMiddlewareSuffix) + assert.Contains(t, workspaceConfig.HTTP.Routers[healthzName].Middlewares, healthzName+gateway.StripPrefixMiddlewareSuffix) + }) +} + +func TestCreateSubDomainObjects(t *testing.T) { + testCommon := func(infra infrastructure.Type) solvers.RoutingObjects { + infrastructure.InitializeForTesting(infra) + + cl, _, objs := getSpecObjects(t, subdomainDevWorkspaceRouting()) + + t.Run("testPodAdditions", func(t *testing.T) { + if len(objs.PodAdditions.Containers) != 1 || objs.PodAdditions.Containers[0].Name != wsGatewayName { + t.Error("expected Container pod addition with Workspace Gateway. Got ", objs.PodAdditions) + } + if len(objs.PodAdditions.Volumes) != 1 || objs.PodAdditions.Volumes[0].Name != wsGatewayName { + t.Error("expected Volume pod addition for workspace gateway. Got ", objs.PodAdditions) + } + }) + + 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") + } + }) + } + + t.Run("noWorkspaceTraefikConfig", func(t *testing.T) { + cms := &corev1.ConfigMapList{} + cl.List(context.TODO(), cms) + + if len(cms.Items) != 2 { + t.Errorf("there should be 2 configmaps create but found: %d", len(cms.Items)) + } + }) + + return objs + } + + t.Run("expectedIngresses", func(t *testing.T) { + objs := testCommon(infrastructure.Kubernetes) + if len(objs.Ingresses) != 3 { + t.Error("Expected 3 ingress, found ", len(objs.Ingresses)) + } + if objs.Ingresses[0].Spec.Rules[0].Host != "username.my-workspace.e1.down.on.earth" { + t.Error("Expected Ingress host 'username.my-workspace.e1.down.on.earth', but got ", objs.Ingresses[0].Spec.Rules[0].Host) + } + if objs.Ingresses[1].Spec.Rules[0].Host != "username.my-workspace.e2.down.on.earth" { + t.Error("Expected Ingress host 'username.my-workspace.e2.down.on.earth', but got ", objs.Ingresses[1].Spec.Rules[0].Host) + } + if objs.Ingresses[2].Spec.Rules[0].Host != "username.my-workspace.e3.down.on.earth" { + t.Error("Expected Ingress host 'username.my-workspace.e3.down.on.earth', but got ", objs.Ingresses[2].Spec.Rules[0].Host) + } + }) + + t.Run("expectedRoutes", func(t *testing.T) { + objs := testCommon(infrastructure.OpenShiftv4) + if len(objs.Routes) != 3 { + t.Error("Expected 3 Routes, found ", len(objs.Routes)) + } + if objs.Routes[0].Spec.Host != "username.my-workspace.e1.down.on.earth" { + t.Error("Expected Route host 'username.my-workspace.e1.down.on.earth', but got ", objs.Routes[0].Spec.Host) + } + }) +} + +func TestCreateSubDomainObjectsLegacy(t *testing.T) { testCommon := func(infra infrastructure.Type) solvers.RoutingObjects { infrastructure.InitializeForTesting(infra) - cl, _, objs := getSpecObjects(t, subdomainDevWorkspaceRouting()) + cl, _, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, subdomainDevWorkspaceRouting()) t.Run("testPodAdditions", func(t *testing.T) { if len(objs.PodAdditions.Containers) != 1 || objs.PodAdditions.Containers[0].Name != wsGatewayName { @@ -646,14 +1078,14 @@ func TestCreateSubDomainObjects(t *testing.T) { if len(objs.Ingresses) != 3 { t.Error("Expected 3 ingress, found ", len(objs.Ingresses)) } - if objs.Ingresses[0].Spec.Rules[0].Host != "username.my-workspace.e1.down.on.earth" { - t.Error("Expected Ingress host 'username.my-workspace.e1.down.on.earth', but got ", objs.Ingresses[0].Spec.Rules[0].Host) + if objs.Ingresses[0].Spec.Rules[0].Host != "wsid-1.down.on.earth" { + t.Error("Expected Ingress host 'wsid-1.down.on.earth', but got ", objs.Ingresses[0].Spec.Rules[0].Host) } - if objs.Ingresses[1].Spec.Rules[0].Host != "username.my-workspace.e2.down.on.earth" { - t.Error("Expected Ingress host 'username.my-workspace.e2.down.on.earth', but got ", objs.Ingresses[1].Spec.Rules[0].Host) + if objs.Ingresses[1].Spec.Rules[0].Host != "wsid-2.down.on.earth" { + t.Error("Expected Ingress host 'wsid-2.down.on.earth', but got ", objs.Ingresses[1].Spec.Rules[0].Host) } - if objs.Ingresses[2].Spec.Rules[0].Host != "username.my-workspace.e3.down.on.earth" { - t.Error("Expected Ingress host 'username.my-workspace.e3.down.on.earth', but got ", objs.Ingresses[2].Spec.Rules[0].Host) + if objs.Ingresses[2].Spec.Rules[0].Host != "wsid-3.down.on.earth" { + t.Error("Expected Ingress host 'wsid-3.down.on.earth', but got ", objs.Ingresses[2].Spec.Rules[0].Host) } }) @@ -662,8 +1094,8 @@ func TestCreateSubDomainObjects(t *testing.T) { if len(objs.Routes) != 3 { t.Error("Expected 3 Routes, found ", len(objs.Routes)) } - if objs.Routes[0].Spec.Host != "username.my-workspace.e1.down.on.earth" { - t.Error("Expected Route host 'username.my-workspace.e1.down.on.earth', but got ", objs.Routes[0].Spec.Host) + if objs.Routes[0].Spec.Host != "wsid-1.down.on.earth" { + t.Error("Expected Route host 'wsid-1.down.on.earth', but got ", objs.Routes[0].Spec.Host) } }) } @@ -722,6 +1154,72 @@ func TestReportRelocatableExposedEndpoints(t *testing.T) { } } +func TestReportRelocatableExposedEndpointsLegacy(t *testing.T) { + // kubernetes + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + + routing := relocatableDevWorkspaceRouting() + _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, routing) + + exposed, ready, err := solver.GetExposedEndpoints(routing.Spec.Endpoints, objs) + if err != nil { + t.Fatal(err) + } + + if !ready { + t.Errorf("The exposed endpoints should have been ready.") + } + + if len(exposed) != 1 { + t.Errorf("There should have been 1 exposed endpoins but found %d", len(exposed)) + } + + m1, ok := exposed["m1"] + if !ok { + t.Errorf("The exposed endpoints should have been defined on the m1 component.") + } + + if len(m1) != 3 { + t.Fatalf("There should have been 3 endpoints for m1.") + } + + e1 := m1[0] + if e1.Name != "e1" { + t.Errorf("The first endpoint should have been e1 but is %s", e1.Name) + } + if e1.Url != "https://over.the.rainbow/wsid/m1/9999/1/" { + t.Errorf("The e1 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/wsid/m1/9999/1/", e1.Url) + } + + e2 := m1[1] + if e2.Name != "e2" { + t.Errorf("The second endpoint should have been e2 but is %s", e1.Name) + } + if e2.Url != "https://over.the.rainbow/wsid/m1/9999/2.js" { + t.Errorf("The e2 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/wsid/m1/9999/2.js", e2.Url) + } + + e3 := m1[2] + if e3.Name != "e3" { + t.Errorf("The third endpoint should have been e3 but is %s", e1.Name) + } + if e3.Url != "https://over.the.rainbow/wsid/m1/9999/" { + t.Errorf("The e3 endpoint should have the following URL: '%s' but has '%s'.", "https://over.the.rainbow/wsid/m1/9999/", e3.Url) + } +} + func TestExposeEndpoints(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) @@ -840,6 +1338,136 @@ func TestExposeEndpoints(t *testing.T) { assert.Equal(t, "http://username.my-workspace.server-pub-nr.down.on.earth/", spnr[0].Url) } +func TestExposeEndpointsLegacy(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + + routing := &dwo.DevWorkspaceRouting{ + ObjectMeta: metav1.ObjectMeta{ + Name: "routing", + Namespace: "ws", + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "workspace.devfile.io/v1alpha2", + Kind: "DevWorkspace", + Name: "my-workspace", + UID: "uid", + }, + }, + }, + Spec: dwo.DevWorkspaceRoutingSpec{ + DevWorkspaceId: "wsid", + RoutingClass: "che", + Endpoints: map[string]dwo.EndpointList{ + "server-internal": { + { + Name: "server-int", + TargetPort: 8081, + Exposure: dwo.InternalEndpointExposure, + Protocol: "http", + Attributes: map[string]apiext.JSON{ + "urlRewriteSupported": apiext.JSON{Raw: []byte("\"true\"")}, + "cookiesAuthEnabled": apiext.JSON{Raw: []byte("\"true\"")}, + }, + }, + }, + "server-internal-no-rewrite": { + { + Name: "server-int-nr", + TargetPort: 8084, + Exposure: dwo.InternalEndpointExposure, + Protocol: "http", + }, + }, + "server-none": { + { + Name: "server-int", + TargetPort: 8080, + Exposure: dwo.NoneEndpointExposure, + Protocol: "http", + Attributes: map[string]apiext.JSON{ + "urlRewriteSupported": apiext.JSON{Raw: []byte("\"true\"")}, + "cookiesAuthEnabled": apiext.JSON{Raw: []byte("\"true\"")}, + }, + }, + }, + "server-none-no-rewrite": { + { + Name: "server-none-nr", + TargetPort: 8083, + Exposure: dwo.NoneEndpointExposure, + Protocol: "http", + }, + }, + "server-public": { + { + Name: "server-pub", + TargetPort: 8082, + Exposure: dwo.PublicEndpointExposure, + Protocol: "http", + Attributes: map[string]apiext.JSON{ + "urlRewriteSupported": apiext.JSON{Raw: []byte("\"true\"")}, + "cookiesAuthEnabled": apiext.JSON{Raw: []byte("\"true\"")}, + }, + }, + }, + "server-public-no-rewrite": { + { + Name: "server-pub-nr", + TargetPort: 8085, + Exposure: dwo.PublicEndpointExposure, + Protocol: "http", + }, + }, + }, + }, + } + + _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, routing) + + assert.Equal(t, 1, len(objs.Ingresses)) + + exposed, ready, err := solver.GetExposedEndpoints(routing.Spec.Endpoints, objs) + assert.Nil(t, err) + assert.True(t, ready) + assert.Equal(t, 4, len(exposed)) + + si, ok := exposed["server-internal"] + assert.True(t, ok) + assert.Equal(t, 1, len(si)) + assert.Equal(t, "server-int", si[0].Name) + assert.Equal(t, "http://wsid-service.ws.svc:8081", si[0].Url) + + sinr, ok := exposed["server-internal-no-rewrite"] + assert.True(t, ok) + assert.Equal(t, 1, len(sinr)) + assert.Equal(t, "server-int-nr", sinr[0].Name) + assert.Equal(t, "http://wsid-service.ws.svc:8084", sinr[0].Url) + + sp, ok := exposed["server-public"] + assert.True(t, ok) + assert.Equal(t, 1, len(sp)) + assert.Equal(t, "server-pub", sp[0].Name) + assert.Equal(t, "https://over.the.rainbow/wsid/server-public/8082/", sp[0].Url) + + spnr, ok := exposed["server-public-no-rewrite"] + assert.True(t, ok) + assert.Equal(t, 1, len(spnr)) + assert.Equal(t, "server-pub-nr", spnr[0].Name) + assert.Equal(t, "http://wsid-1.down.on.earth/", spnr[0].Url) +} + func TestReportSubdomainExposedEndpoints(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) routing := subdomainDevWorkspaceRouting() @@ -892,6 +1520,70 @@ func TestReportSubdomainExposedEndpoints(t *testing.T) { } } +func TestReportSubdomainExposedEndpointsLegacy(t *testing.T) { + infrastructure.InitializeForTesting(infrastructure.Kubernetes) + routing := subdomainDevWorkspaceRouting() + _, solver, objs := getSpecObjectsForManager(t, &chev2.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "che", + Namespace: "ns", + Finalizers: []string{controller.FinalizerName}, + }, + Spec: chev2.CheClusterSpec{ + Networking: chev2.CheClusterSpecNetworking{ + Domain: "down.on.earth", + Hostname: "over.the.rainbow", + }, + }, + }, routing) + + exposed, ready, err := solver.GetExposedEndpoints(routing.Spec.Endpoints, objs) + if err != nil { + t.Fatal(err) + } + + if !ready { + t.Errorf("The exposed endpoints should have been ready.") + } + + if len(exposed) != 1 { + t.Errorf("There should have been 1 exposed endpoins but found %d", len(exposed)) + } + + m1, ok := exposed["m1"] + if !ok { + t.Errorf("The exposed endpoints should have been defined on the m1 component.") + } + + if len(m1) != 3 { + t.Fatalf("There should have been 3 endpoints for m1.") + } + + e1 := m1[0] + if e1.Name != "e1" { + t.Errorf("The first endpoint should have been e1 but is %s", e1.Name) + } + if e1.Url != "https://wsid-1.down.on.earth/1/" { + t.Errorf("The e1 endpoint should have the following URL: '%s' but has '%s'.", "https://wsid-1.down.on.earth/1/", e1.Url) + } + + e2 := m1[1] + if e2.Name != "e2" { + t.Errorf("The second endpoint should have been e2 but is %s", e1.Name) + } + if e2.Url != "https://wsid-2.down.on.earth/2.js" { + t.Errorf("The e2 endpoint should have the following URL: '%s' but has '%s'.", "https://wsid-2.down.on.earth/2.js", e2.Url) + } + + e3 := m1[2] + if e3.Name != "e3" { + t.Errorf("The third endpoint should have been e3 but is %s", e1.Name) + } + if e3.Url != "http://wsid-3.down.on.earth/" { + t.Errorf("The e3 endpoint should have the following URL: '%s' but has '%s'.", "https://wsid-3.down.on.earth/", e3.Url) + } +} + func TestFinalize(t *testing.T) { infrastructure.InitializeForTesting(infrastructure.Kubernetes) routing := relocatableDevWorkspaceRouting() diff --git a/controllers/devworkspace/solver/endpoint_exposer.go b/controllers/devworkspace/solver/endpoint_exposer.go index dd8ce06aa6..11067c9a63 100644 --- a/controllers/devworkspace/solver/endpoint_exposer.go +++ b/controllers/devworkspace/solver/endpoint_exposer.go @@ -142,7 +142,7 @@ func (e *IngressExposer) initFrom(ctx context.Context, cl client.Client, cluster return nil } -func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo, username string, dwName string) routev1.Route { +func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo, endpointStrategy EndpointStrategy) routev1.Route { targetEndpoint := intstr.FromInt(int(endpoint.port)) labels := labels.Merge( e.labels, @@ -159,7 +159,7 @@ func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo, username strin OwnerReferences: endpoint.service.OwnerReferences, }, Spec: routev1.RouteSpec{ - Host: hostName(username, dwName, endpoint.endpointName, e.baseDomain), + Host: endpointStrategy.getHostname(endpoint, e.baseDomain), To: routev1.RouteTargetReference{ Kind: "Service", Name: endpoint.service.Name, @@ -185,8 +185,8 @@ func (e *RouteExposer) getRouteForService(endpoint *EndpointInfo, username strin return route } -func (e *IngressExposer) getIngressForService(endpoint *EndpointInfo, username string, dwName string) networkingv1.Ingress { - hostname := hostName(username, dwName, endpoint.endpointName, e.baseDomain) +func (e *IngressExposer) getIngressForService(endpoint *EndpointInfo, endpointStrategy EndpointStrategy) networkingv1.Ingress { + hostname := endpointStrategy.getHostname(endpoint, e.baseDomain) ingressPathType := networkingv1.PathTypeImplementationSpecific ingress := networkingv1.Ingress{ @@ -239,10 +239,6 @@ func (e *IngressExposer) getIngressForService(endpoint *EndpointInfo, username s return ingress } -func hostName(username string, workspaceName string, endpointName string, baseDomain string) string { - return fmt.Sprintf("%s.%s.%s.%s", username, workspaceName, endpointName, baseDomain) -} - func routeAnnotations(machineName string, endpointName string) map[string]string { return map[string]string{ defaults.ConfigAnnotationEndpointName: endpointName, diff --git a/controllers/devworkspace/solver/endpoint_strategy.go b/controllers/devworkspace/solver/endpoint_strategy.go new file mode 100644 index 0000000000..67c95f2e67 --- /dev/null +++ b/controllers/devworkspace/solver/endpoint_strategy.go @@ -0,0 +1,114 @@ +// +// Copyright (c) 2019-2021 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 solver + +import ( + "fmt" + "strconv" + + dwo "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" +) + +// Interface for different workspace and endpoint url path strategies +type EndpointStrategy interface { + // get url paths for traefik config + getPublicURLPrefix(port int32, uniqueEndpointName string) string + getMainWorkspacePathPrefix() string + getEndpointPath(e *dwo.Endpoint, componentName string) (routeName string, path string) + getEndpointPathPrefix(endpointPath string) string + + // get hostname for routes / ingress + getHostname(endpointInfo *EndpointInfo, baseDomain string) string +} + +// Main workspace URL is exposed on the following path: +// //// + +// Public endpoints defined in the devfile are exposed on the following path via route or ingress: +// .../ +type UsernameWkspName struct { + username string + workspaceName string +} + +func (u UsernameWkspName) getPublicURLPrefix(port int32, uniqueEndpointName string) string { + if uniqueEndpointName == "" { + return fmt.Sprintf(endpointURLPrefixPattern, u.username, u.workspaceName, port) + } + return fmt.Sprintf(uniqueEndpointURLPrefixPattern, u.username, u.workspaceName, uniqueEndpointName) +} + +func (u UsernameWkspName) getMainWorkspacePathPrefix() string { + return fmt.Sprintf("/%s/%s", u.username, u.workspaceName) +} + +func (u UsernameWkspName) getEndpointPath(e *dwo.Endpoint, componentName string) (routeName string, path string) { + if e.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" { + routeName = e.Name + } else { + routeName = strconv.Itoa(e.TargetPort) + } + path = fmt.Sprintf("/%s", routeName) + + return routeName, path +} + +func (u UsernameWkspName) getEndpointPathPrefix(endpointPath string) string { + return fmt.Sprintf("/%s/%s%s", u.username, u.workspaceName, endpointPath) +} + +func (u UsernameWkspName) getHostname(endpointInfo *EndpointInfo, baseDomain string) string { + return fmt.Sprintf("%s.%s.%s.%s", u.username, u.workspaceName, endpointInfo.endpointName, baseDomain) +} + +// Main workspace URL is exposed on the following path: +// //// + +// Public endpoints defined in the devfile are exposed on the following path via route or ingress: +// -./ +type Legacy struct { + workspaceID string + componentName string +} + +func (l Legacy) getPublicURLPrefix(port int32, uniqueEndpointName string) string { + if uniqueEndpointName == "" { + return fmt.Sprintf(endpointURLPrefixPattern, l.workspaceID, l.componentName, port) + } + return fmt.Sprintf(uniqueEndpointURLPrefixPattern, l.workspaceID, l.componentName, uniqueEndpointName) +} + +func (l Legacy) getMainWorkspacePathPrefix() string { + return fmt.Sprintf("/%s", l.workspaceID) +} + +func (l Legacy) getEndpointPath(e *dwo.Endpoint, componentName string) (routeName string, path string) { + if e.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" { + // if endpoint is unique, we're exposing on /componentName/ + routeName = e.Name + } else { + // if endpoint is NOT unique, we're exposing on /componentName/ + routeName = strconv.Itoa(e.TargetPort) + } + path = fmt.Sprintf("/%s/%s", componentName, routeName) + + return routeName, path +} + +func (l Legacy) getEndpointPathPrefix(endpointPath string) string { + return fmt.Sprintf("/%s%s", l.workspaceID, endpointPath) +} + +func (l Legacy) getHostname(endpointInfo *EndpointInfo, baseDomain string) string { + return fmt.Sprintf("%s-%d.%s", l.workspaceID, endpointInfo.order, baseDomain) +} From 42139830fc2cb5ab80e4957174abd3591a01a812 Mon Sep 17 00:00:00 2001 From: David Kwon Date: Tue, 23 May 2023 14:16:24 -0400 Subject: [PATCH 4/8] Remove componentName field from from legacy strategy Signed-off-by: David Kwon --- controllers/devworkspace/solver/che_routing.go | 8 ++++---- .../devworkspace/solver/endpoint_strategy.go | 13 ++++++------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index 2843ea879a..1fecc2f823 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -292,12 +292,12 @@ func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, wor var endpointStrategy EndpointStrategy if useLegacyRouting { - endpointStrategy = Legacy{workspaceID: workspaceID, componentName: component} + endpointStrategy = Legacy{workspaceID: workspaceID} } else { endpointStrategy = UsernameWkspName{username: username, workspaceName: dwName} } - publicURLPrefix := getPublicURLPrefixForEndpoint(endpoint, endpointStrategy) + publicURLPrefix := getPublicURLPrefixForEndpoint(component, endpoint, endpointStrategy) endpointURL = path.Join(gatewayHost, publicURLPrefix, endpoint.Path) } @@ -800,12 +800,12 @@ func getServiceURL(port int32, workspaceID string, workspaceNamespace string) st return fmt.Sprintf("http://%s.%s.svc:%d", common.ServiceName(workspaceID), workspaceNamespace, port) } -func getPublicURLPrefixForEndpoint(endpoint dwo.Endpoint, endpointStrategy EndpointStrategy) string { +func getPublicURLPrefixForEndpoint(componentName string, endpoint dwo.Endpoint, endpointStrategy EndpointStrategy) string { endpointName := "" if endpoint.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" { endpointName = endpoint.Name } - return endpointStrategy.getPublicURLPrefix(int32(endpoint.TargetPort), endpointName) + return endpointStrategy.getPublicURLPrefix(int32(endpoint.TargetPort), endpointName, componentName) } func determineEndpointScheme(e dwo.Endpoint) string { diff --git a/controllers/devworkspace/solver/endpoint_strategy.go b/controllers/devworkspace/solver/endpoint_strategy.go index 67c95f2e67..35643a559f 100644 --- a/controllers/devworkspace/solver/endpoint_strategy.go +++ b/controllers/devworkspace/solver/endpoint_strategy.go @@ -22,7 +22,7 @@ import ( // Interface for different workspace and endpoint url path strategies type EndpointStrategy interface { // get url paths for traefik config - getPublicURLPrefix(port int32, uniqueEndpointName string) string + getPublicURLPrefix(port int32, uniqueEndpointName string, componentName string) string getMainWorkspacePathPrefix() string getEndpointPath(e *dwo.Endpoint, componentName string) (routeName string, path string) getEndpointPathPrefix(endpointPath string) string @@ -41,7 +41,7 @@ type UsernameWkspName struct { workspaceName string } -func (u UsernameWkspName) getPublicURLPrefix(port int32, uniqueEndpointName string) string { +func (u UsernameWkspName) getPublicURLPrefix(port int32, uniqueEndpointName string, componentName string) string { if uniqueEndpointName == "" { return fmt.Sprintf(endpointURLPrefixPattern, u.username, u.workspaceName, port) } @@ -77,15 +77,14 @@ func (u UsernameWkspName) getHostname(endpointInfo *EndpointInfo, baseDomain str // Public endpoints defined in the devfile are exposed on the following path via route or ingress: // -./ type Legacy struct { - workspaceID string - componentName string + workspaceID string } -func (l Legacy) getPublicURLPrefix(port int32, uniqueEndpointName string) string { +func (l Legacy) getPublicURLPrefix(port int32, uniqueEndpointName string, componentName string) string { if uniqueEndpointName == "" { - return fmt.Sprintf(endpointURLPrefixPattern, l.workspaceID, l.componentName, port) + return fmt.Sprintf(endpointURLPrefixPattern, l.workspaceID, componentName, port) } - return fmt.Sprintf(uniqueEndpointURLPrefixPattern, l.workspaceID, l.componentName, uniqueEndpointName) + return fmt.Sprintf(uniqueEndpointURLPrefixPattern, l.workspaceID, componentName, uniqueEndpointName) } func (l Legacy) getMainWorkspacePathPrefix() string { From cf3e3102d6d16179ee5152ca3e117f5963b9b5a7 Mon Sep 17 00:00:00 2001 From: David Kwon Date: Tue, 23 May 2023 15:10:20 -0400 Subject: [PATCH 5/8] Determine path strategy in different function Signed-off-by: David Kwon --- .../devworkspace/solver/che_routing.go | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index 1fecc2f823..dac180b024 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -232,6 +232,8 @@ func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, wor gatewayHost := cheCluster.GetCheHost() + endpointStrategy := getEndpointPathStrategy(c.client, workspaceID, routingObj.Services[0].Namespace, routingObj.Services[0].ObjectMeta.OwnerReferences[0].Name) + for component, endpoints := range componentEndpoints { for _, endpoint := range endpoints { if dw.EndpointExposure(endpoint.Exposure) == dw.NoneEndpointExposure { @@ -279,24 +281,6 @@ func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, wor return map[string]dwo.ExposedEndpointList{}, false, nil } - useLegacyRouting := false - username, err := getNormalizedUsername(c.client, routingObj.Services[0].Namespace) - if err != nil { - useLegacyRouting = true - } - - dwName, err := getNormalizedWkspName(c.client, routingObj.Services[0].Namespace, routingObj.Services[0].ObjectMeta.OwnerReferences[0].Name) - if err != nil { - useLegacyRouting = true - } - - var endpointStrategy EndpointStrategy - if useLegacyRouting { - endpointStrategy = Legacy{workspaceID: workspaceID} - } else { - endpointStrategy = UsernameWkspName{username: username, workspaceName: dwName} - } - publicURLPrefix := getPublicURLPrefixForEndpoint(component, endpoint, endpointStrategy) endpointURL = path.Join(gatewayHost, publicURLPrefix, endpoint.Path) } @@ -347,24 +331,7 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch } configs := make([]corev1.ConfigMap, 0) - - useLegacyRouting := false - username, err := getNormalizedUsername(c.client, routing.Namespace) - if err != nil { - useLegacyRouting = true - } - - dwName, err := getNormalizedWkspName(c.client, routing.Namespace, routing.Name) - if err != nil { - useLegacyRouting = true - } - - var endpointStrategy EndpointStrategy - if useLegacyRouting { - endpointStrategy = Legacy{workspaceID: workspaceID} - } else { - endpointStrategy = UsernameWkspName{username: username, workspaceName: dwName} - } + endpointStrategy := getEndpointPathStrategy(c.client, workspaceID, routing.Namespace, routing.Name) // first do routing from main che-gateway into workspace service if mainWsRouteConfig, err := provisionMainWorkspaceRoute(cheCluster, routing, cmLabels, endpointStrategy); err != nil { @@ -385,6 +352,30 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch return configs, nil } +func getEndpointPathStrategy(c client.Client, workspaceId string, namespace string, dwRoutingName string) EndpointStrategy { + useLegacyPaths := false + username, err := getNormalizedUsername(c, namespace) + if err != nil { + useLegacyPaths = true + } + + dwName, err := getNormalizedWkspName(c, namespace, dwRoutingName) + if err != nil { + useLegacyPaths = true + } + + if useLegacyPaths { + strategy := new(Legacy) + strategy.workspaceID = workspaceId + return strategy + } else { + strategy := new(UsernameWkspName) + strategy.username = username + strategy.workspaceName = dwName + return strategy + } +} + func getNormalizedUsername(c client.Client, namespace string) (string, error) { username, err := getUsernameFromNamespace(c, namespace) From b2597600dbd75f089369afa3b992ed28c348a916 Mon Sep 17 00:00:00 2001 From: David Kwon Date: Wed, 24 May 2023 17:21:08 -0400 Subject: [PATCH 6/8] Enforce lowercase for username and dw name Signed-off-by: David Kwon --- controllers/devworkspace/solver/che_routing.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/devworkspace/solver/che_routing.go b/controllers/devworkspace/solver/che_routing.go index dac180b024..1a6dfbfe90 100644 --- a/controllers/devworkspace/solver/che_routing.go +++ b/controllers/devworkspace/solver/che_routing.go @@ -437,7 +437,8 @@ func normalize(username string) string { result := r1.ReplaceAllString(username, "-") // replace invalid chars with '-' result = r2.ReplaceAllString(result, "-") // replace multiple '-' with single ones - return r3.ReplaceAllString(result, "") // trim dashes at beginning/end + result = r3.ReplaceAllString(result, "") // trim dashes at beginning/end + return strings.ToLower(result) } func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects, endpointStrategy EndpointStrategy) (func(info *EndpointInfo), error) { From 7dfb2fe89980356f50de7273cb41a5a01ef93ecc Mon Sep 17 00:00:00 2001 From: David Kwon Date: Thu, 25 May 2023 09:56:36 -0400 Subject: [PATCH 7/8] Run make update-dev-resources' Signed-off-by: David Kwon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e3d3c4bc00..820898d381 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - [Builds](#Builds) - [License](#License) -d + ## Description Che/Red Hat OpenShift Dev Spaces operator uses [Operator SDK](https://github.com/operator-framework/operator-sdk) and [Go Kube client](https://github.com/kubernetes/client-go) to deploy, update and manage K8S/OpenShift resources that constitute a multi-user Eclipse Che/Red Hat OpenShift Dev Spaces cluster. From 2d0d278e36cc0216d6d2e3e96d75212b79b9a87d Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Wed, 31 May 2023 11:31:44 +0300 Subject: [PATCH 8/8] Update dev resources Signed-off-by: Anatolii Bazko --- .../manifests/che-operator.clusterserviceversion.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml index bcd830a613..7a31da2828 100644 --- a/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml +++ b/bundle/next/eclipse-che/manifests/che-operator.clusterserviceversion.yaml @@ -77,7 +77,7 @@ metadata: operators.operatorframework.io/project_layout: go.kubebuilder.io/v3 repository: https://github.com/eclipse-che/che-operator support: Eclipse Foundation - name: eclipse-che.v7.68.0-797.next + name: eclipse-che.v7.68.0-798.next namespace: placeholder spec: apiservicedefinitions: {} @@ -1231,7 +1231,7 @@ spec: minKubeVersion: 1.19.0 provider: name: Eclipse Foundation - version: 7.68.0-797.next + version: 7.68.0-798.next webhookdefinitions: - admissionReviewVersions: - v1