che-operator/pkg/deploy/tls/certificates.go

230 lines
8.6 KiB
Go

//
// Copyright (c) 2019-2023 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 tls
import (
"context"
"reflect"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/eclipse-che/che-operator/pkg/common/chetypes"
"github.com/eclipse-che/che-operator/pkg/common/constants"
defaults "github.com/eclipse-che/che-operator/pkg/common/operator-defaults"
"github.com/eclipse-che/che-operator/pkg/deploy"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
const (
injector = "config.openshift.io/inject-trusted-cabundle"
// CheCACertsConfigMapLabelKey is the label value which marks config map with additional CA certificates
CheCACertsConfigMapLabelValue = "ca-bundle"
// CheAllCACertsConfigMapName is the name of config map which contains all additional trusted by Che TLS CA certificates
CheAllCACertsConfigMapName = "ca-certs-merged"
// CheMergedCAConfigMapRevisionsAnnotationKey is annotation name which holds versions of included config maps in format: cm-name1=ver1,cm-name2=ver2
CheMergedCAConfigMapRevisionsAnnotationKey = "che.eclipse.org/included-configmaps"
KubernetesRootCertificateConfigMapName = "kube-root-ca.crt"
// Local constants
// labelEqualSign constant is used as a replacement for '=' symbol in labels because '=' is not allowed there
labelEqualSign = "-"
// labelCommaSign constant is used as a replacement for ',' symbol in labels because ',' is not allowed there
labelCommaSign = "."
)
type CertificatesReconciler struct {
deploy.Reconcilable
}
func NewCertificatesReconciler() *CertificatesReconciler {
return &CertificatesReconciler{}
}
func (c *CertificatesReconciler) Reconcile(ctx *chetypes.DeployContext) (reconcile.Result, bool, error) {
if ctx.Proxy.TrustedCAMapName != "" {
if done, err := c.syncTrustStoreConfigMapToCluster(ctx); !done {
return reconcile.Result{}, false, err
}
}
if done, err := c.syncKubernetesRootCertificates(ctx); !done {
return reconcile.Result{}, false, err
}
if done, err := c.syncAdditionalCACertsConfigMapToCluster(ctx); !done {
return reconcile.Result{}, false, err
}
return reconcile.Result{}, true, nil
}
func (c *CertificatesReconciler) Finalize(ctx *chetypes.DeployContext) bool {
return true
}
func (c *CertificatesReconciler) syncTrustStoreConfigMapToCluster(ctx *chetypes.DeployContext) (bool, error) {
configMapSpec := deploy.GetConfigMapSpec(ctx, constants.DefaultServerTrustStoreConfigMapName, map[string]string{}, defaults.GetCheFlavor())
// OpenShift will automatically injects all certs into the configmap
configMapSpec.ObjectMeta.Labels[injector] = "true"
configMapSpec.ObjectMeta.Labels[constants.KubernetesPartOfLabelKey] = constants.CheEclipseOrg
configMapSpec.ObjectMeta.Labels[constants.KubernetesComponentLabelKey] = CheCACertsConfigMapLabelValue
actual := &corev1.ConfigMap{}
exists, err := deploy.GetNamespacedObject(ctx, constants.DefaultServerTrustStoreConfigMapName, actual)
if err != nil {
return false, err
}
if !exists {
// We have to create an empty config map with the specific labels
return deploy.CreateIgnoreIfExists(ctx, configMapSpec)
}
if actual.ObjectMeta.Labels[injector] != "true" ||
actual.ObjectMeta.Labels[constants.KubernetesPartOfLabelKey] != constants.CheEclipseOrg ||
actual.ObjectMeta.Labels[constants.KubernetesComponentLabelKey] != CheCACertsConfigMapLabelValue {
actual.ObjectMeta.Labels[injector] = "true"
actual.ObjectMeta.Labels[constants.KubernetesPartOfLabelKey] = constants.CheEclipseOrg
actual.ObjectMeta.Labels[constants.KubernetesComponentLabelKey] = CheCACertsConfigMapLabelValue
logrus.Infof("Updating existed object: %s, name: %s", configMapSpec.Kind, configMapSpec.Name)
if err := ctx.ClusterAPI.Client.Update(context.TODO(), actual); err != nil {
return false, err
}
}
return true, nil
}
// syncAdditionalCACertsConfigMapToCluster adds labels to ConfigMap `kube-root-ca.crt` to propagate
// Kubernetes root certificates to Che components. It is needed to use NonCachingClient because the map
// initially is not in the cache.
func (c *CertificatesReconciler) syncKubernetesRootCertificates(ctx *chetypes.DeployContext) (bool, error) {
kubeRootCertsConfigMap := &corev1.ConfigMap{}
if err := ctx.ClusterAPI.NonCachingClient.Get(
context.TODO(),
types.NamespacedName{
Name: KubernetesRootCertificateConfigMapName,
Namespace: ctx.CheCluster.Namespace,
},
kubeRootCertsConfigMap); err != nil {
if errors.IsNotFound(err) {
return true, nil
} else {
return false, err
}
}
if kubeRootCertsConfigMap.GetLabels() == nil {
kubeRootCertsConfigMap.SetLabels(map[string]string{})
}
kubeRootCertsConfigMap.Labels[constants.KubernetesPartOfLabelKey] = constants.CheEclipseOrg
kubeRootCertsConfigMap.Labels[constants.KubernetesComponentLabelKey] = CheCACertsConfigMapLabelValue
// Set TypeMeta to avoid "cause: no version "" has been registered in scheme" error
kubeRootCertsConfigMap.TypeMeta = metav1.TypeMeta{
Kind: "ConfigMap",
APIVersion: "v1",
}
return deploy.SyncWithClient(ctx.ClusterAPI.NonCachingClient, ctx, kubeRootCertsConfigMap, deploy.ConfigMapDiffOpts)
}
func (c *CertificatesReconciler) syncAdditionalCACertsConfigMapToCluster(ctx *chetypes.DeployContext) (bool, error) {
// Get all source config maps, if any
caConfigMaps, err := GetCACertsConfigMaps(ctx.ClusterAPI.Client, ctx.CheCluster.GetNamespace())
if err != nil {
return false, err
}
mergedCAConfigMap := &corev1.ConfigMap{}
err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: CheAllCACertsConfigMapName}, mergedCAConfigMap)
if err == nil {
// Merged config map exists. Check if it is up to date.
caConfigMapsCurrentRevisions := make(map[string]string)
for _, cm := range caConfigMaps {
caConfigMapsCurrentRevisions[cm.Name] = cm.ResourceVersion
}
caConfigMapsCachedRevisions := make(map[string]string)
if mergedCAConfigMap.ObjectMeta.Annotations != nil {
if revisions, exists := mergedCAConfigMap.ObjectMeta.Annotations[CheMergedCAConfigMapRevisionsAnnotationKey]; exists {
for _, cmNameRevision := range strings.Split(revisions, labelCommaSign) {
nameRevision := strings.Split(cmNameRevision, labelEqualSign)
if len(nameRevision) != 2 {
// The label value is invalid, recreate merged config map
break
}
caConfigMapsCachedRevisions[nameRevision[0]] = nameRevision[1]
}
}
}
if reflect.DeepEqual(caConfigMapsCurrentRevisions, caConfigMapsCachedRevisions) {
// Existing merged config map is up to date, do nothing
return true, nil
}
} else {
if !errors.IsNotFound(err) {
return false, err
}
// Merged config map doesn't exist. Create it.
}
// Merged config map is out of date or doesn't exist
// Merge all config maps into single one to mount inside Che components and workspaces
data := make(map[string]string)
revisions := ""
for _, cm := range caConfigMaps {
// Copy data
for key, dataRecord := range cm.Data {
data[cm.ObjectMeta.Name+"."+key] = dataRecord
}
// Save source config map revision
if revisions != "" {
revisions += labelCommaSign
}
revisions += cm.ObjectMeta.Name + labelEqualSign + cm.ObjectMeta.ResourceVersion
}
// Add SelfSigned certificate for a git repository to the bundle
if ctx.CheCluster.Spec.DevEnvironments.TrustedCerts != nil && ctx.CheCluster.Spec.DevEnvironments.TrustedCerts.GitTrustedCertsConfigMapName != "" {
gitTrustedCertsConfig := &corev1.ConfigMap{}
exists, err := deploy.GetNamespacedObject(ctx, ctx.CheCluster.Spec.DevEnvironments.TrustedCerts.GitTrustedCertsConfigMapName, gitTrustedCertsConfig)
if err != nil {
return false, err
} else if exists && gitTrustedCertsConfig.Data["ca.crt"] != "" {
if revisions != "" {
revisions += labelCommaSign
}
revisions += gitTrustedCertsConfig.Name + labelEqualSign + gitTrustedCertsConfig.ResourceVersion
data[gitTrustedCertsConfig.Name+".ca.crt"] = gitTrustedCertsConfig.Data["ca.crt"]
}
}
mergedCAConfigMapSpec := deploy.GetConfigMapSpec(ctx, CheAllCACertsConfigMapName, data, defaults.GetCheFlavor())
mergedCAConfigMapSpec.ObjectMeta.Labels[constants.KubernetesPartOfLabelKey] = constants.CheEclipseOrg
mergedCAConfigMapSpec.ObjectMeta.Annotations[CheMergedCAConfigMapRevisionsAnnotationKey] = revisions
return deploy.SyncConfigMapSpecToCluster(ctx, mergedCAConfigMapSpec)
}