// // Copyright (c) 2019-2020 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" "time" "github.com/devfile/devworkspace-operator/pkg/constants" controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" "github.com/devfile/devworkspace-operator/controllers/controller/devworkspacerouting/solvers" "github.com/eclipse-che/che-operator/api/v2alpha1" controller "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/controllers/devworkspace/defaults" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) var ( logger = ctrl.Log.WithName("solver") ) // CheRoutingSolver is a struct representing the routing solver for Che specific routing of devworkspaces type CheRoutingSolver struct { client client.Client scheme *runtime.Scheme } // Magic to ensure we get compile time error right here if our struct doesn't support the interface. var _ solvers.RoutingSolverGetter = (*CheRouterGetter)(nil) var _ solvers.RoutingSolver = (*CheRoutingSolver)(nil) // CheRouterGetter negotiates the solver with the calling code type CheRouterGetter struct { scheme *runtime.Scheme } // Getter creates a new CheRouterGetter func Getter(scheme *runtime.Scheme) *CheRouterGetter { return &CheRouterGetter{ scheme: scheme, } } func (g *CheRouterGetter) HasSolver(routingClass controllerv1alpha1.DevWorkspaceRoutingClass) bool { return isSupported(routingClass) } func (g *CheRouterGetter) GetSolver(client client.Client, routingClass controllerv1alpha1.DevWorkspaceRoutingClass) (solver solvers.RoutingSolver, err error) { if !isSupported(routingClass) { return nil, solvers.RoutingNotSupported } return &CheRoutingSolver{client: client, scheme: g.scheme}, nil } func (g *CheRouterGetter) SetupControllerManager(mgr *builder.Builder) error { // We want to watch configmaps and re-map the reconcile on the devworkspace routing, if possible // This way we can react on changes of the gateway configmap changes by re-reconciling the corresponding // devworkspace routing and thus keeping the devworkspace routing in a functional state // TODO is this going to be performant enough in a big cluster with very many configmaps? mgr.Watches(&source.Kind{Type: &corev1.ConfigMap{}}, &handler.EnqueueRequestsFromMapFunc{ToRequests: handler.ToRequestsFunc(func(mo handler.MapObject) []reconcile.Request { applicable, key := isGatewayWorkspaceConfig(mo.Meta) if applicable { // cool, we can trigger the reconcile of the routing so that we can update the configmap that has just changed under our hands return []reconcile.Request{ { NamespacedName: key, }, } } else { return []reconcile.Request{} } })}) return nil } func isGatewayWorkspaceConfig(obj metav1.Object) (bool, types.NamespacedName) { workspaceID := obj.GetLabels()[constants.DevWorkspaceIDLabel] objectName := obj.GetName() // bail out quickly if we're not dealing with a configmap with an expected name if objectName != defaults.GetGatewayWorkspaceConfigMapName(workspaceID) { return false, types.NamespacedName{} } routingName := obj.GetAnnotations()[defaults.ConfigAnnotationDevWorkspaceRoutingName] routingNamespace := obj.GetAnnotations()[defaults.ConfigAnnotationDevWorkspaceRoutingNamespace] // if there is no annotation for the routing, we're out of luck.. this should not happen though if routingName == "" { return false, types.NamespacedName{} } // cool, we found a configmap belonging to a concrete devworkspace routing return true, types.NamespacedName{Name: routingName, Namespace: routingNamespace} } func (c *CheRoutingSolver) FinalizerRequired(routing *controllerv1alpha1.DevWorkspaceRouting) bool { return true } func (c *CheRoutingSolver) Finalize(routing *controllerv1alpha1.DevWorkspaceRouting) error { cheManager, err := cheManagerOfRouting(routing) if err != nil { return err } return c.cheRoutingFinalize(cheManager, routing) } // GetSpecObjects constructs cluster routing objects which should be applied on the cluster func (c *CheRoutingSolver) GetSpecObjects(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta solvers.DevWorkspaceMetadata) (solvers.RoutingObjects, error) { cheManager, err := cheManagerOfRouting(routing) if err != nil { return solvers.RoutingObjects{}, err } return c.cheSpecObjects(cheManager, routing, workspaceMeta) } // GetExposedEndpoints retreives the URL for each endpoint in a devfile spec from a set of RoutingObjects. // Returns is a map from component ids (as defined in the devfile) to the list of endpoints for that component // Return value "ready" specifies if all endpoints are resolved on the cluster; if false it is necessary to retry, as // URLs will be undefined. func (c *CheRoutingSolver) GetExposedEndpoints(endpoints map[string]controllerv1alpha1.EndpointList, routingObj solvers.RoutingObjects) (exposedEndpoints map[string]controllerv1alpha1.ExposedEndpointList, ready bool, err error) { if len(routingObj.Services) == 0 { return map[string]controllerv1alpha1.ExposedEndpointList{}, true, nil } managerName := routingObj.Services[0].Annotations[defaults.ConfigAnnotationCheManagerName] managerNamespace := routingObj.Services[0].Annotations[defaults.ConfigAnnotationCheManagerNamespace] workspaceID := routingObj.Services[0].Labels[constants.DevWorkspaceIDLabel] manager, err := findCheManager(client.ObjectKey{Name: managerName, Namespace: managerNamespace}) if err != nil { return nil, false, err } return c.cheExposedEndpoints(manager, workspaceID, endpoints, routingObj) } func isSupported(routingClass controllerv1alpha1.DevWorkspaceRoutingClass) bool { return routingClass == "che" } func cheManagerOfRouting(routing *controllerv1alpha1.DevWorkspaceRouting) (*v2alpha1.CheCluster, error) { cheName := routing.Annotations[defaults.ConfigAnnotationCheManagerName] cheNamespace := routing.Annotations[defaults.ConfigAnnotationCheManagerNamespace] return findCheManager(client.ObjectKey{Name: cheName, Namespace: cheNamespace}) } func findCheManager(cheManagerKey client.ObjectKey) (*v2alpha1.CheCluster, error) { managers := controller.GetCurrentCheClusterInstances() if len(managers) == 0 { // the CheManager has not been reconciled yet, so let's wait a bit return &v2alpha1.CheCluster{}, &solvers.RoutingNotReady{Retry: 1 * time.Second} } if len(cheManagerKey.Name) == 0 { if len(managers) > 1 { return &v2alpha1.CheCluster{}, &solvers.RoutingInvalid{Reason: fmt.Sprintf("the routing does not specify any Che manager in its configuration but there are %d Che managers in the cluster", len(managers))} } for _, m := range managers { return &m, nil } } if m, ok := managers[cheManagerKey]; ok { return &m, nil } logger.Info("Routing requires a non-existing che manager. Retrying in 10 seconds.", "key", cheManagerKey) return &v2alpha1.CheCluster{}, &solvers.RoutingNotReady{Retry: 10 * time.Second} }