che-operator/controllers/usernamespace/controller.go

479 lines
14 KiB
Go

//
// 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 usernamespace
import (
"context"
"github.com/eclipse-che/che-operator/pkg/deploy/tls"
"github.com/eclipse-che/che-operator/pkg/util"
"github.com/devfile/devworkspace-operator/pkg/constants"
org "github.com/eclipse-che/che-operator/api"
v1 "github.com/eclipse-che/che-operator/api/v1"
"github.com/eclipse-che/che-operator/api/v2alpha1"
"github.com/eclipse-che/che-operator/controllers/che"
"github.com/eclipse-che/che-operator/controllers/devworkspace"
"github.com/eclipse-che/che-operator/controllers/devworkspace/defaults"
"github.com/eclipse-che/che-operator/pkg/deploy"
projectv1 "github.com/openshift/api/project/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
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/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)
const (
userSettingsComponentLabelValue = "user-settings"
)
type CheUserNamespaceReconciler struct {
client client.Client
scheme *runtime.Scheme
namespaceCache namespaceCache
}
type eventRule struct {
check func(metav1.Object) bool
namespaces func(metav1.Object) []string
}
var _ reconcile.Reconciler = (*CheUserNamespaceReconciler)(nil)
func NewReconciler() *CheUserNamespaceReconciler {
return &CheUserNamespaceReconciler{namespaceCache: *NewNamespaceCache()}
}
func (r *CheUserNamespaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
r.scheme = mgr.GetScheme()
r.client = mgr.GetClient()
r.namespaceCache.client = r.client
var obj client.Object
if util.IsOpenShift4 {
obj = &projectv1.Project{}
} else {
obj = &corev1.Namespace{}
}
ctx := context.Background()
bld := ctrl.NewControllerManagedBy(mgr).
For(obj).
Watches(&source.Kind{Type: &corev1.Secret{}}, r.watchRulesForSecrets(ctx)).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, r.watchRulesForConfigMaps(ctx)).
Watches(&source.Kind{Type: &v1.CheCluster{}}, r.triggerAllNamespaces())
return bld.Complete(r)
}
func (r *CheUserNamespaceReconciler) watchRulesForSecrets(ctx context.Context) handler.EventHandler {
rules := r.commonRules(ctx, deploy.CheTLSSelfSignedCertificateSecretName)
return handler.EnqueueRequestsFromMapFunc(
handler.MapFunc(func(obj client.Object) []reconcile.Request {
return asReconcileRequestsForNamespaces(obj, rules)
}))
}
func asReconcileRequestsForNamespaces(obj metav1.Object, rules []eventRule) []reconcile.Request {
for _, r := range rules {
if r.check(obj) {
nss := r.namespaces(obj)
ret := make([]reconcile.Request, len(nss))
for i, n := range nss {
ret[i] = reconcile.Request{
NamespacedName: types.NamespacedName{
Name: n,
},
}
}
return ret
}
}
return []reconcile.Request{}
}
func (r *CheUserNamespaceReconciler) commonRules(ctx context.Context, namesInCheClusterNamespace ...string) []eventRule {
return []eventRule{
{
check: func(o metav1.Object) bool {
return isLabeledAsUserSettings(o) && r.isInManagedNamespace(ctx, o)
},
namespaces: func(o metav1.Object) []string { return []string{o.GetNamespace()} },
},
{
check: func(o metav1.Object) bool {
return r.hasNameAndIsCollocatedWithCheCluster(ctx, o, namesInCheClusterNamespace...)
},
namespaces: func(o metav1.Object) []string { return r.namespaceCache.GetAllKnownNamespaces() },
},
}
}
func (r *CheUserNamespaceReconciler) watchRulesForConfigMaps(ctx context.Context) handler.EventHandler {
rules := r.commonRules(ctx, tls.CheAllCACertsConfigMapName)
return handler.EnqueueRequestsFromMapFunc(
handler.MapFunc(func(obj client.Object) []reconcile.Request {
return asReconcileRequestsForNamespaces(obj, rules)
}))
}
func (r *CheUserNamespaceReconciler) hasNameAndIsCollocatedWithCheCluster(ctx context.Context, obj metav1.Object, names ...string) bool {
for _, n := range names {
if obj.GetName() == n && r.hasCheCluster(ctx, obj.GetNamespace()) {
return true
}
}
return false
}
func isLabeledAsUserSettings(obj metav1.Object) bool {
return obj.GetLabels()["app.kubernetes.io/component"] == userSettingsComponentLabelValue
}
func (r *CheUserNamespaceReconciler) isInManagedNamespace(ctx context.Context, obj metav1.Object) bool {
info, err := r.namespaceCache.GetNamespaceInfo(ctx, obj.GetNamespace())
return err == nil && info != nil && info.OwnerUid != ""
}
func (r *CheUserNamespaceReconciler) triggerAllNamespaces() handler.EventHandler {
return handler.EnqueueRequestsFromMapFunc(
handler.MapFunc(func(obj client.Object) []reconcile.Request {
nss := r.namespaceCache.GetAllKnownNamespaces()
ret := make([]reconcile.Request, len(nss))
for _, ns := range nss {
ret = append(ret, reconcile.Request{
NamespacedName: types.NamespacedName{Name: ns},
})
}
return ret
}),
)
}
func (r *CheUserNamespaceReconciler) hasCheCluster(ctx context.Context, namespace string) bool {
list := v1.CheClusterList{}
if err := r.client.List(ctx, &list, client.InNamespace(namespace)); err != nil {
return false
}
return len(list.Items) > 0
}
func (r *CheUserNamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
info, err := r.namespaceCache.ExamineNamespace(ctx, req.Name)
if err != nil {
logrus.Errorf("Failed to examine namespace %s for presence of Che user info labels: %v", req.Name, err)
return ctrl.Result{}, err
}
if info == nil || info.OwnerUid == "" {
// we're not handling this namespace
return ctrl.Result{}, nil
}
checluster := findManagingCheCluster(*info.CheCluster)
if checluster == nil {
return ctrl.Result{Requeue: true}, nil
}
if devworkspace.GetDevWorkspaceState(r.scheme, checluster) != devworkspace.EnabledState {
return ctrl.Result{}, nil
}
// let's construct the deployContext to be able to use methods from v1 operator
deployContext := &deploy.DeployContext{
CheCluster: org.AsV1(checluster),
ClusterAPI: deploy.ClusterAPI{
Client: r.client,
NonCachingClient: r.client,
DiscoveryClient: nil,
Scheme: r.scheme,
},
}
if err = r.reconcileSelfSignedCert(ctx, deployContext, req.Name, checluster); err != nil {
logrus.Errorf("Failed to reconcile self-signed certificate into namespace '%s': %v", req.Name, err)
return ctrl.Result{}, err
}
if err = r.reconcileTrustedCerts(ctx, deployContext, req.Name, checluster); err != nil {
logrus.Errorf("Failed to reconcile self-signed certificate into namespace '%s': %v", req.Name, err)
return ctrl.Result{}, err
}
if err = r.reconcileProxySettings(ctx, req.Name, checluster, deployContext); err != nil {
logrus.Errorf("Failed to reconcile proxy settings into namespace '%s': %v", req.Name, err)
return ctrl.Result{}, err
}
if err = r.reconcileGitTlsCertificate(ctx, req.Name, checluster, deployContext); err != nil {
logrus.Errorf("Failed to reconcile Che git TLS certificate into namespace '%s': %v", req.Name, err)
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
func findManagingCheCluster(key types.NamespacedName) *v2alpha1.CheCluster {
instances := devworkspace.GetCurrentCheClusterInstances()
if len(instances) == 0 {
return nil
}
if len(instances) == 1 {
for k, v := range instances {
if key.Name == "" || (key.Name == k.Name && key.Namespace == k.Namespace) {
return &v
}
return nil
}
}
ret, ok := instances[key]
if ok {
return &ret
} else {
return nil
}
}
func (r *CheUserNamespaceReconciler) reconcileSelfSignedCert(ctx context.Context, deployContext *deploy.DeployContext, targetNs string, checluster *v2alpha1.CheCluster) error {
targetCertName := prefixedName(checluster, "server-cert")
delSecret := func() error {
_, err := deploy.Delete(deployContext, client.ObjectKey{Name: targetCertName, Namespace: targetNs}, &corev1.Secret{})
return err
}
cheCert := &corev1.Secret{}
if err := r.client.Get(ctx, client.ObjectKey{Name: deploy.CheTLSSelfSignedCertificateSecretName, Namespace: checluster.Namespace}, cheCert); err != nil {
if !errors.IsNotFound(err) {
return err
}
// There is not self-signed cert in the namespace of the checluster, so we have nothing to copy around
return delSecret()
}
if _, ok := cheCert.Data["ca.crt"]; !ok {
// the secret doesn't contain the certificate. bail out.
return delSecret()
}
targetCert := &corev1.Secret{
TypeMeta: metav1.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: targetCertName,
Namespace: targetNs,
Labels: defaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{
constants.DevWorkspaceMountLabel: "true",
constants.DevWorkspaceWatchSecretLabel: "true",
}),
Annotations: map[string]string{
constants.DevWorkspaceMountAsAnnotation: "file",
constants.DevWorkspaceMountPathAnnotation: "/tmp/che/secret/",
},
},
Data: map[string][]byte{
"ca.crt": cheCert.Data["ca.crt"],
},
Type: cheCert.Type,
Immutable: cheCert.Immutable,
}
_, err := deploy.DoSync(deployContext, targetCert, deploy.SecretDiffOpts)
return err
}
func (r *CheUserNamespaceReconciler) reconcileTrustedCerts(ctx context.Context, deployContext *deploy.DeployContext, targetNs string, checluster *v2alpha1.CheCluster) error {
targetConfigMapName := prefixedName(checluster, "trusted-ca-certs")
delConfigMap := func() error {
_, err := deploy.Delete(deployContext, client.ObjectKey{Name: targetConfigMapName, Namespace: targetNs}, &corev1.Secret{})
return err
}
sourceMap := &corev1.ConfigMap{}
if err := r.client.Get(ctx, client.ObjectKey{Name: tls.CheAllCACertsConfigMapName, Namespace: checluster.Namespace}, sourceMap); err != nil {
if !errors.IsNotFound(err) {
return err
}
return delConfigMap()
}
targetMap := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: targetConfigMapName,
Namespace: targetNs,
Labels: defaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{
constants.DevWorkspaceMountLabel: "true",
constants.DevWorkspaceWatchConfigMapLabel: "true",
}),
Annotations: addToFirst(sourceMap.Annotations, map[string]string{
constants.DevWorkspaceMountAsAnnotation: "file",
constants.DevWorkspaceMountPathAnnotation: "/public-certs",
}),
},
Data: sourceMap.Data,
}
_, err := deploy.DoSync(deployContext, targetMap, deploy.ConfigMapDiffOpts)
return err
}
func addToFirst(first map[string]string, second map[string]string) map[string]string {
if first == nil {
first = map[string]string{}
}
for k, v := range second {
first[k] = v
}
return first
}
func (r *CheUserNamespaceReconciler) reconcileProxySettings(ctx context.Context, targetNs string, checluster *v2alpha1.CheCluster, deployContext *deploy.DeployContext) error {
proxyConfig, err := che.GetProxyConfiguration(deployContext)
if err != nil {
return err
}
if proxyConfig == nil {
return nil
}
proxySettings := map[string]string{}
if proxyConfig.HttpProxy != "" {
proxySettings["HTTP_PROXY"] = proxyConfig.HttpProxy
}
if proxyConfig.HttpsProxy != "" {
proxySettings["HTTPS_PROXY"] = proxyConfig.HttpsProxy
}
if proxyConfig.NoProxy != "" {
proxySettings["NO_PROXY"] = proxyConfig.NoProxy
}
key := client.ObjectKey{Name: prefixedName(checluster, "proxy-settings"), Namespace: targetNs}
cfg := &corev1.ConfigMap{}
exists := true
if err := r.client.Get(ctx, key, cfg); err != nil {
if errors.IsNotFound(err) {
exists = false
} else {
return err
}
}
if len(proxySettings) == 0 {
if exists {
if err := r.client.Delete(ctx, cfg); err != nil {
return err
}
}
return nil
}
requiredLabels := defaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{
constants.DevWorkspaceMountLabel: "true",
constants.DevWorkspaceWatchConfigMapLabel: "true",
})
requiredAnnos := map[string]string{
constants.DevWorkspaceMountAsAnnotation: "env",
}
cfg = &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: prefixedName(checluster, "proxy-settings"),
Namespace: targetNs,
Labels: requiredLabels,
Annotations: requiredAnnos,
},
Data: proxySettings,
}
_, err = deploy.DoSync(deployContext, cfg, deploy.ConfigMapDiffOpts)
return err
}
func (r *CheUserNamespaceReconciler) reconcileGitTlsCertificate(ctx context.Context, targetNs string, checluster *v2alpha1.CheCluster, deployContext *deploy.DeployContext) error {
targetName := prefixedName(checluster, "git-tls-creds")
delConfigMap := func() error {
_, err := deploy.Delete(deployContext, client.ObjectKey{Name: targetName, Namespace: targetNs}, &corev1.Secret{})
return err
}
clusterv1 := org.AsV1(checluster)
if !clusterv1.Spec.Server.GitSelfSignedCert {
return delConfigMap()
}
gitCert := &corev1.ConfigMap{}
if err := deployContext.ClusterAPI.Client.Get(ctx, client.ObjectKey{Name: deploy.GitSelfSignedCertsConfigMapName, Namespace: checluster.Namespace}, gitCert); err != nil {
if !errors.IsNotFound(err) {
return err
}
return delConfigMap()
}
target := corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: targetName,
Namespace: targetNs,
Labels: defaults.AddStandardLabelsForComponent(checluster, userSettingsComponentLabelValue, map[string]string{
constants.DevWorkspaceGitTLSLabel: "true",
constants.DevWorkspaceWatchConfigMapLabel: "true",
}),
},
Data: map[string]string{
"host": gitCert.Data["githost"],
"certificate": gitCert.Data["ca.crt"],
},
}
_, err := deploy.DoSync(deployContext, &target, deploy.ConfigMapDiffOpts)
return err
}
func prefixedName(checluster *v2alpha1.CheCluster, name string) string {
return checluster.Name + "-" + checluster.Namespace + "-" + name
}