Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)

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
}

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