Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make workspace routes more user-friendly #1672

Merged
merged 8 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
Expand Down Expand Up @@ -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
Expand Down
155 changes: 113 additions & 42 deletions controllers/devworkspace/solver/che_routing.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"context"
"fmt"
"path"
"strconv"
"regexp"
"strings"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -279,7 +281,7 @@ func (c *CheRoutingSolver) cheExposedEndpoints(cheCluster *chev2.CheCluster, wor
return map[string]dwo.ExposedEndpointList{}, false, nil
}

publicURLPrefix := getPublicURLPrefixForEndpoint(workspaceID, component, endpoint)
publicURLPrefix := getPublicURLPrefixForEndpoint(component, endpoint, endpointStrategy)
endpointURL = path.Join(gatewayHost, publicURLPrefix, endpoint.Path)
}

Expand Down Expand Up @@ -329,34 +331,124 @@ func (c *CheRoutingSolver) getGatewayConfigsAndFillRoutingObjects(cheCluster *ch
}

configs := make([]corev1.ConfigMap, 0)
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); 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); 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)
}
}

return configs, nil
}

func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster, routing *dwo.DevWorkspaceRouting, objs *solvers.RoutingObjects) (func(info *EndpointInfo), error) {
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)
Copy link
Contributor Author

@dkwon17 dkwon17 May 18, 2023

Choose a reason for hiding this comment

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

The username is also being read from the namespace as well, in the case that the admin has pre-provisioned the namespaces following the doc, since in this case, the user-profile secret would not be created


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 {
return "", err
}
username := string(secret.Data["name"])
return normalize(username), nil
Copy link
Contributor Author

@dkwon17 dkwon17 May 4, 2023

Choose a reason for hiding this comment

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

The username is being read from the user-profile secret created by che-server, which is why this PR depends on eclipse-che/che-server#508

Another way to read the username could be to read the che.eclipse.org/username annotation from the user namespace

Copy link
Contributor

Choose a reason for hiding this comment

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

BTW, che.eclipse.org/username annotation might not exist

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have a backup plan instead of throwing an error if Secret doesn't exist ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm afraid there is no backup plan right now if that secret does not exist, are there other ways to detect the username?

Copy link
Contributor

Choose a reason for hiding this comment

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

What about using the legacy route name if username is unknown?

Copy link
Member

Choose a reason for hiding this comment

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

whereas a backup plan could be nice to have, using user-profile section should be robust in general e.g. if it is deleted from the namespace it would be re-created next time user access the dashboard

Copy link
Contributor Author

@dkwon17 dkwon17 May 11, 2023

Choose a reason for hiding this comment

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

After more investigation, I think the minikube tests are failing because the user-profile secret is not created when the user namespace and workspace is created by applying templates (ie, without using the dashboard)

As a result, workspaces cannot start

Copy link
Member

Choose a reason for hiding this comment

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

@dkwon17 thanks for the investigation, so in this case the flow when devworkspace is created using oc apply command is going to also fail if there is no user-profile secret in the namespace, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ibuziuk yes, that's right

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am currently working on the legacy routes in this case, as suggested by @tolusha

}

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)
if err != nil {
return "", err
}
return normalize(routing.ObjectMeta.OwnerReferences[0].Name), nil
}

func normalize(username string) string {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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
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) {
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, endpointStrategy)
objs.Routes = append(objs.Routes, route)
}, nil
} else {
Expand All @@ -365,7 +457,7 @@ func (c *CheRoutingSolver) getInfraSpecificExposer(cheCluster *chev2.CheCluster,
return nil, err
}
return func(info *EndpointInfo) {
ingress := exposer.getIngressForService(info)
ingress := exposer.getIngressForService(info, endpointStrategy)
objs.Ingresses = append(objs.Ingresses, ingress)
}, nil
}
Expand All @@ -381,7 +473,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)
Expand Down Expand Up @@ -412,7 +504,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,
Expand Down Expand Up @@ -473,16 +565,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, cmLabels map[string]string, endpointStrategy EndpointStrategy) (*corev1.ConfigMap, error) {
dwId := routing.Spec.DevWorkspaceId
dwNamespace := routing.Namespace

cfg := gateway.CreateCommonTraefikConfig(
dwId,
fmt.Sprintf("PathPrefix(`/%s`)", dwId),
fmt.Sprintf("PathPrefix(`%s`)", endpointStrategy.getMainWorkspacePathPrefix()),
100,
getServiceURL(wsGatewayPort, dwId, dwNamespace),
[]string{"/" + dwId})
[]string{endpointStrategy.getMainWorkspacePathPrefix()})

if cheCluster.IsAccessTokenConfigured() {
cfg.AddAuthHeaderRewrite(dwId)
Expand All @@ -494,7 +586,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, dwId, routing.Spec.Endpoints, endpointStrategy)

if contents, err := yaml.Marshal(cfg); err != nil {
return nil, err
Expand Down Expand Up @@ -540,18 +632,18 @@ 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, 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) {
middlewares := []string{dwId + gateway.StripPrefixMiddlewareSuffix}
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/healthz`)", dwId, endpointPath),
Rule: fmt.Sprintf("Path(`%s/healthz`)", endpointStrategy.getEndpointPathPrefix(endpointPath)),
Service: dwId,
Middlewares: middlewares,
Priority: 101,
Expand All @@ -561,8 +653,8 @@ func routeForHealthzEndpoint(cfg *gateway.TraefikConfig, dwId string, endpoints
}
}

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
Expand Down Expand Up @@ -594,19 +686,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/<endpoint-name>
routeName = e.Name
} else {
// if endpoint is NOT unique, we're exposing on /componentName/<port-number>
routeName = strconv.Itoa(e.TargetPort)
}
path = fmt.Sprintf("/%s/%s", componentName, routeName)

return routeName, path
}

func findServiceForPort(port int32, objs *solvers.RoutingObjects) *corev1.Service {
for i := range objs.Services {
svc := &objs.Services[i]
Expand Down Expand Up @@ -713,20 +792,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(workspaceID string, machineName string, endpoint dwo.Endpoint) string {
func getPublicURLPrefixForEndpoint(componentName string, endpoint dwo.Endpoint, endpointStrategy EndpointStrategy) string {
endpointName := ""
if endpoint.Attributes.GetString(uniqueEndpointAttributeName, nil) == "true" {
endpointName = endpoint.Name
}

return getPublicURLPrefix(workspaceID, machineName, int32(endpoint.TargetPort), endpointName)
}

func getPublicURLPrefix(workspaceID string, machineName string, port int32, uniqueEndpointName string) string {
if uniqueEndpointName == "" {
return fmt.Sprintf(endpointURLPrefixPattern, workspaceID, machineName, port)
}
return fmt.Sprintf(uniqueEndpointURLPrefixPattern, workspaceID, machineName, uniqueEndpointName)
return endpointStrategy.getPublicURLPrefix(int32(endpoint.TargetPort), endpointName, componentName)
}

func determineEndpointScheme(e dwo.Endpoint) string {
Expand Down
Loading