che-operator/controllers/devworkspace/solver/solver.go

200 lines
7.4 KiB
Go

//
// 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.GetGatewayWorkpaceConfigMapName(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}
}