From 1e9fa6a0782eb55e7a52238f99378f86dc378038 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Fri, 26 Nov 2021 13:50:59 +0200 Subject: [PATCH] chore: Refactor certificates reconsiler (#1171) * chore: Refactor certificates reconsiler Signed-off-by: Anatolii Bazko --- controllers/che/checluster_controller.go | 107 ++---- controllers/che/cheobj_verifier.go | 3 +- controllers/che/configmap_cert.go | 54 ---- controllers/che/configmap_cert_test.go | 119 ------- controllers/che/proxy.go | 11 - .../checlusterbackup/backup_data_collector.go | 3 +- controllers/usernamespace/controller.go | 5 +- controllers/usernamespace/controller_test.go | 5 +- pkg/deploy/dashboard/deployment_dashboard.go | 5 +- pkg/deploy/data_types.go | 9 +- pkg/deploy/defaults.go | 3 + .../identity-provider/deployment_keycloak.go | 5 +- .../openshift-oauth/openshiftoauth_user.go | 8 +- pkg/deploy/role.go | 19 -- pkg/deploy/secret.go | 24 -- pkg/deploy/server/server_configmap.go | 3 +- pkg/deploy/server/server_deployment.go | 7 +- pkg/deploy/tls/certificates.go | 188 +++++++++++ pkg/deploy/tls/certificates_test.go | 140 ++++++++ pkg/deploy/tls/init_test.go | 21 ++ pkg/deploy/tls/tls_secret.go | 78 +++++ pkg/deploy/{tls.go => tls/tls_utils.go} | 306 ++++++++---------- pkg/deploy/tls_test.go | 109 ------- 23 files changed, 611 insertions(+), 621 deletions(-) delete mode 100644 controllers/che/configmap_cert.go delete mode 100644 controllers/che/configmap_cert_test.go create mode 100644 pkg/deploy/tls/certificates.go create mode 100644 pkg/deploy/tls/certificates_test.go create mode 100644 pkg/deploy/tls/init_test.go create mode 100644 pkg/deploy/tls/tls_secret.go rename pkg/deploy/{tls.go => tls/tls_utils.go} (55%) delete mode 100644 pkg/deploy/tls_test.go diff --git a/controllers/che/checluster_controller.go b/controllers/che/checluster_controller.go index 07e36f7cd..0dee52578 100644 --- a/controllers/che/checluster_controller.go +++ b/controllers/che/checluster_controller.go @@ -28,6 +28,7 @@ import ( "github.com/eclipse-che/che-operator/pkg/deploy/pluginregistry" "github.com/eclipse-che/che-operator/pkg/deploy/postgres" "github.com/eclipse-che/che-operator/pkg/deploy/server" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" identity_provider "github.com/eclipse-che/che-operator/pkg/deploy/identity-provider" "github.com/eclipse-che/che-operator/pkg/util" @@ -96,6 +97,8 @@ func NewReconciler( openShiftOAuthUser := openshiftoauth.NewOpenShiftOAuthUser() reconcileManager.RegisterReconciler(openShiftOAuthUser) reconcileManager.RegisterReconciler(openshiftoauth.NewOpenShiftOAuth(openShiftOAuthUser)) + reconcileManager.RegisterReconciler(tls.NewCertificatesReconciler()) + reconcileManager.RegisterReconciler(tls.NewTlsSecretReconciler()) return &CheClusterReconciler{ Scheme: scheme, @@ -249,6 +252,22 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) CheCluster: checluster, } + // Read proxy configuration + proxy, err := GetProxyConfiguration(deployContext) + if err != nil { + r.Log.Error(err, "Error on reading proxy configuration") + return ctrl.Result{}, err + } + deployContext.Proxy = proxy + + // Detect whether self-signed certificate is used + isSelfSignedCertificate, err := tls.IsSelfSignedCertificateUsed(deployContext) + if err != nil { + r.Log.Error(err, "Failed to detect if self-signed certificate used.") + return ctrl.Result{}, err + } + deployContext.IsSelfSignedCertificate = isSelfSignedCertificate + if isCheGoingToBeUpdated(checluster) { // Current operator is newer than deployed Che backupCR, err := getBackupCRForUpdate(deployContext) @@ -299,94 +318,6 @@ func (r *CheClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{RequeueAfter: time.Second}, err } - // Read proxy configuration - proxy, err := GetProxyConfiguration(deployContext) - if err != nil { - r.Log.Error(err, "Error on reading proxy configuration") - return ctrl.Result{}, err - } - // Assign Proxy to the deploy context - deployContext.Proxy = proxy - - if proxy.TrustedCAMapName != "" { - provisioned, err := r.putOpenShiftCertsIntoConfigMap(deployContext) - if !provisioned { - configMapName := checluster.Spec.Server.ServerTrustStoreConfigMapName - if err != nil { - r.Log.Error(err, "Error on provisioning", "config map", configMapName) - } else { - r.Log.Error(err, "Waiting on provisioning", "config map", configMapName) - } - return ctrl.Result{}, err - } - } - - // Detect whether self-signed certificate is used - selfSignedCertUsed, err := deploy.IsSelfSignedCertificateUsed(deployContext) - if err != nil { - r.Log.Error(err, "Failed to detect if self-signed certificate used.") - return ctrl.Result{}, err - } - - if util.IsOpenShift { - // create a secret with router tls cert when on OpenShift infra and router is configured with a self signed certificate - if selfSignedCertUsed || - // To use Openshift v4 OAuth, the OAuth endpoints are served from a namespace - // and NOT from the Openshift API Master URL (as in v3) - // So we also need the self-signed certificate to access them (same as the Che server) - (util.IsOpenShift4 && checluster.IsOpenShiftOAuthEnabled() && !checluster.Spec.Server.TlsSupport) { - if err := deploy.CreateTLSSecretFromEndpoint(deployContext, "", deploy.CheTLSSelfSignedCertificateSecretName); err != nil { - return ctrl.Result{}, err - } - } - - if util.IsOpenShift && checluster.IsOpenShiftOAuthEnabled() { - // create a secret with OpenShift API crt to be added to keystore that RH SSO will consume - apiUrl, apiInternalUrl, err := util.GetOpenShiftAPIUrls() - if err != nil { - logrus.Errorf("Failed to get OpenShift cluster public hostname. A secret with API crt will not be created and consumed by RH-SSO/Keycloak") - } else { - baseURL := map[bool]string{true: apiInternalUrl, false: apiUrl}[apiInternalUrl != ""] - if err := deploy.CreateTLSSecretFromEndpoint(deployContext, baseURL, "openshift-api-crt"); err != nil { - return ctrl.Result{}, err - } - } - } - } else { - // Handle Che TLS certificates on Kubernetes infrastructure - if checluster.Spec.Server.TlsSupport { - if checluster.Spec.K8s.TlsSecretName != "" { - // Self-signed certificate should be created to secure Che ingresses - result, err := deploy.K8sHandleCheTLSSecrets(deployContext) - if result.Requeue || result.RequeueAfter > 0 { - if err != nil { - logrus.Error(err) - } - if !tests { - return result, err - } - } - } else if selfSignedCertUsed { - // Use default self-signed ingress certificate - if err := deploy.CreateTLSSecretFromEndpoint(deployContext, "", deploy.CheTLSSelfSignedCertificateSecretName); err != nil { - return ctrl.Result{}, err - } - } - } - } - - // Make sure that CA certificates from all marked config maps are merged into single config map to be propageted to Che components - done, err = deploy.SyncAdditionalCACertsConfigMapToCluster(deployContext) - if err != nil { - r.Log.Error(err, "Error updating additional CA config map") - return ctrl.Result{}, err - } - if !done && !tests { - // Config map update is in progress - // Return and do not force reconcile. When update finishes it will trigger reconcile loop. - return ctrl.Result{}, err - } - // Create service account "che" for che-server component. // "che" is the one which token is used to create workspace objects. // Notice: Also we have on more "che-workspace" SA used by plugins like exec, terminal, metrics with limited privileges. diff --git a/controllers/che/cheobj_verifier.go b/controllers/che/cheobj_verifier.go index 0c02523c7..2b6970dca 100644 --- a/controllers/che/cheobj_verifier.go +++ b/controllers/che/cheobj_verifier.go @@ -14,6 +14,7 @@ package che import ( "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" "github.com/eclipse-che/che-operator/pkg/util" "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/types" @@ -46,7 +47,7 @@ func IsTrustedBundleConfigMap(cl client.Client, watchNamespace string, obj clien // No, it is not form CR // Check for component - if value, exists := obj.GetLabels()[deploy.KubernetesComponentLabelKey]; !exists || value != deploy.CheCACertsConfigMapLabelValue { + if value, exists := obj.GetLabels()[deploy.KubernetesComponentLabelKey]; !exists || value != tls.CheCACertsConfigMapLabelValue { // Labels do not match return false, ctrl.Request{} } diff --git a/controllers/che/configmap_cert.go b/controllers/che/configmap_cert.go deleted file mode 100644 index b682584a4..000000000 --- a/controllers/che/configmap_cert.go +++ /dev/null @@ -1,54 +0,0 @@ -// -// 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 che - -import ( - "context" - - "github.com/eclipse-che/che-operator/pkg/deploy" - - "github.com/sirupsen/logrus" - corev1 "k8s.io/api/core/v1" -) - -const ( - injector = "config.openshift.io/inject-trusted-cabundle" -) - -func SyncTrustStoreConfigMapToCluster(deployContext *deploy.DeployContext) (bool, error) { - name := deployContext.CheCluster.Spec.Server.ServerTrustStoreConfigMapName - configMapSpec := deploy.GetConfigMapSpec(deployContext, name, map[string]string{}, deploy.DefaultCheFlavor(deployContext.CheCluster)) - - // OpenShift will automatically injects all certs into the configmap - configMapSpec.ObjectMeta.Labels[injector] = "true" - - actual := &corev1.ConfigMap{} - exists, err := deploy.GetNamespacedObject(deployContext, name, actual) - if err != nil { - return false, err - } - - if !exists { - // We have to create an empty config map with the specific labels - return deploy.Create(deployContext, configMapSpec) - } - - if actual.ObjectMeta.Labels[injector] != "true" { - actual.ObjectMeta.Labels[injector] = "true" - logrus.Infof("Updating existed object: %s, name: %s", configMapSpec.Kind, configMapSpec.Name) - err := deployContext.ClusterAPI.Client.Update(context.TODO(), actual) - return true, err - } - - return true, nil -} diff --git a/controllers/che/configmap_cert_test.go b/controllers/che/configmap_cert_test.go deleted file mode 100644 index d830b3648..000000000 --- a/controllers/che/configmap_cert_test.go +++ /dev/null @@ -1,119 +0,0 @@ -// -// 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 che - -import ( - "context" - - "testing" - - orgv1 "github.com/eclipse-che/che-operator/api/v1" - "github.com/eclipse-che/che-operator/pkg/deploy" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestSyncTrustStoreConfigMapToCluster(t *testing.T) { - orgv1.SchemeBuilder.AddToScheme(scheme.Scheme) - corev1.SchemeBuilder.AddToScheme(scheme.Scheme) - cli := fake.NewFakeClientWithScheme(scheme.Scheme) - deployContext := &deploy.DeployContext{ - CheCluster: &orgv1.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: orgv1.CheClusterSpec{ - Server: orgv1.CheClusterSpecServer{ - ServerTrustStoreConfigMapName: "trust", - }, - }, - }, - ClusterAPI: deploy.ClusterAPI{ - Client: cli, - NonCachingClient: cli, - Scheme: scheme.Scheme, - }, - } - - done, err := SyncTrustStoreConfigMapToCluster(deployContext) - if !done || err != nil { - t.Fatalf("Failed to sync config map: %v", err) - } - - actual := &corev1.ConfigMap{} - err = cli.Get(context.TODO(), types.NamespacedName{Name: "trust", Namespace: "eclipse-che"}, actual) - if err != nil { - t.Fatalf("Failed to get config map: %v", err) - } - if actual.ObjectMeta.Labels[injector] != "true" { - t.Fatalf("Failed to sync config map") - } -} - -func TestSyncExistedTrustStoreConfigMapToCluster(t *testing.T) { - orgv1.SchemeBuilder.AddToScheme(scheme.Scheme) - corev1.SchemeBuilder.AddToScheme(scheme.Scheme) - cli := fake.NewFakeClientWithScheme(scheme.Scheme) - deployContext := &deploy.DeployContext{ - CheCluster: &orgv1.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - Spec: orgv1.CheClusterSpec{ - Server: orgv1.CheClusterSpecServer{ - ServerTrustStoreConfigMapName: "trust", - }, - }, - }, - ClusterAPI: deploy.ClusterAPI{ - Client: cli, - NonCachingClient: cli, - Scheme: scheme.Scheme, - }, - } - - cm := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "trust", - Namespace: "eclipse-che", - Labels: map[string]string{"a": "b"}, - }, - Data: map[string]string{"d": "c"}, - } - err := cli.Create(context.TODO(), cm) - if err != nil { - t.Fatalf("Failed to create config map: %v", err) - } - - done, err := SyncTrustStoreConfigMapToCluster(deployContext) - if !done || err != nil { - t.Fatalf("Failed to sync config map: %v", err) - } - - actual := &corev1.ConfigMap{} - err = cli.Get(context.TODO(), types.NamespacedName{Name: "trust", Namespace: "eclipse-che"}, actual) - if err != nil { - t.Fatalf("Failed to get config map: %v", err) - } - if actual.ObjectMeta.Labels[injector] != "true" || actual.ObjectMeta.Labels["a"] != "b" { - t.Fatalf("Failed to sync config map") - } - if actual.Data["d"] != "c" { - t.Fatalf("Failed to sync config map") - } -} diff --git a/controllers/che/proxy.go b/controllers/che/proxy.go index c3af9e96b..1edae0b07 100644 --- a/controllers/che/proxy.go +++ b/controllers/che/proxy.go @@ -66,14 +66,3 @@ func GetProxyConfiguration(deployContext *deploy.DeployContext) (*deploy.Proxy, } return cheClusterProxyConf, nil } - -func (r *CheClusterReconciler) putOpenShiftCertsIntoConfigMap(deployContext *deploy.DeployContext) (bool, error) { - if deployContext.CheCluster.Spec.Server.ServerTrustStoreConfigMapName == "" { - deployContext.CheCluster.Spec.Server.ServerTrustStoreConfigMapName = deploy.DefaultServerTrustStoreConfigMapName() - if err := deploy.UpdateCheCRSpec(deployContext, "truststore configmap", deploy.DefaultServerTrustStoreConfigMapName()); err != nil { - return false, err - } - } - - return SyncTrustStoreConfigMapToCluster(deployContext) -} diff --git a/controllers/checlusterbackup/backup_data_collector.go b/controllers/checlusterbackup/backup_data_collector.go index 3eb79d257..811b6c22f 100644 --- a/controllers/checlusterbackup/backup_data_collector.go +++ b/controllers/checlusterbackup/backup_data_collector.go @@ -22,6 +22,7 @@ import ( orgv1 "github.com/eclipse-che/che-operator/api/v1" "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" "github.com/eclipse-che/che-operator/pkg/util" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -220,7 +221,7 @@ func backupConfigMaps(bctx *BackupContext, destDir string) (bool, error) { return true, err } - caBundlesConfigmaps, err := deploy.GetCACertsConfigMaps(bctx.r.nonCachingClient, bctx.cheCR.GetNamespace()) + caBundlesConfigmaps, err := tls.GetCACertsConfigMaps(bctx.r.nonCachingClient, bctx.cheCR.GetNamespace()) if err != nil { return false, err } diff --git a/controllers/usernamespace/controller.go b/controllers/usernamespace/controller.go index b52af0b38..d9f3c2bae 100644 --- a/controllers/usernamespace/controller.go +++ b/controllers/usernamespace/controller.go @@ -15,6 +15,7 @@ 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" @@ -128,7 +129,7 @@ func (r *CheUserNamespaceReconciler) commonRules(ctx context.Context, namesInChe } func (r *CheUserNamespaceReconciler) watchRulesForConfigMaps(ctx context.Context) handler.EventHandler { - rules := r.commonRules(ctx, deploy.CheAllCACertsConfigMapName) + rules := r.commonRules(ctx, tls.CheAllCACertsConfigMapName) return handler.EnqueueRequestsFromMapFunc( handler.MapFunc(func(obj client.Object) []reconcile.Request { return asReconcileRequestsForNamespaces(obj, rules) @@ -312,7 +313,7 @@ func (r *CheUserNamespaceReconciler) reconcileTrustedCerts(ctx context.Context, } sourceMap := &corev1.ConfigMap{} - if err := r.client.Get(ctx, client.ObjectKey{Name: deploy.CheAllCACertsConfigMapName, Namespace: checluster.Namespace}, sourceMap); err != nil { + if err := r.client.Get(ctx, client.ObjectKey{Name: tls.CheAllCACertsConfigMapName, Namespace: checluster.Namespace}, sourceMap); err != nil { if !errors.IsNotFound(err) { return err } diff --git a/controllers/usernamespace/controller_test.go b/controllers/usernamespace/controller_test.go index 8f9b211ce..0f05ea1a6 100644 --- a/controllers/usernamespace/controller_test.go +++ b/controllers/usernamespace/controller_test.go @@ -22,6 +22,7 @@ import ( v1 "github.com/eclipse-che/che-operator/api/v1" "github.com/eclipse-che/che-operator/controllers/devworkspace" "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" "github.com/eclipse-che/che-operator/pkg/util" configv1 "github.com/openshift/api/config/v1" projectv1 "github.com/openshift/api/project/v1" @@ -93,7 +94,7 @@ func setupCheCluster(t *testing.T, ctx context.Context, cl client.Client, scheme caCerts := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: deploy.CheAllCACertsConfigMapName, + Name: tls.CheAllCACertsConfigMapName, Namespace: cheNamespaceName, }, Data: map[string]string{ @@ -483,7 +484,7 @@ func TestWatchRulesForConfigMapsInOtherNamespaces(t *testing.T) { APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ - Name: deploy.CheAllCACertsConfigMapName, + Name: tls.CheAllCACertsConfigMapName, Namespace: "eclipse-che", }, } diff --git a/pkg/deploy/dashboard/deployment_dashboard.go b/pkg/deploy/dashboard/deployment_dashboard.go index 8cfbcfbcc..4b8678e22 100644 --- a/pkg/deploy/dashboard/deployment_dashboard.go +++ b/pkg/deploy/dashboard/deployment_dashboard.go @@ -21,6 +21,7 @@ import ( "k8s.io/apimachinery/pkg/types" "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" "github.com/eclipse-che/che-operator/pkg/util" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -38,7 +39,7 @@ func (d *Dashboard) getDashboardDeploymentSpec() (*appsv1.Deployment, error) { volumes, volumeMounts = d.provisionCustomPublicCA(volumes, volumeMounts) - selfSignedCertSecretExist, err := deploy.IsSelfSignedCASecretExists(d.deployContext) + selfSignedCertSecretExist, err := tls.IsSelfSignedCASecretExists(d.deployContext) if err != nil { return nil, err } @@ -243,7 +244,7 @@ func (d *Dashboard) provisionCustomPublicCA(volumes []corev1.Volume, volumeMount VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: deploy.CheAllCACertsConfigMapName, + Name: tls.CheAllCACertsConfigMapName, }, }, }, diff --git a/pkg/deploy/data_types.go b/pkg/deploy/data_types.go index 1eaad441b..7d8ace315 100644 --- a/pkg/deploy/data_types.go +++ b/pkg/deploy/data_types.go @@ -26,10 +26,11 @@ type ProvisioningStatus struct { } type DeployContext struct { - CheCluster *orgv1.CheCluster - ClusterAPI ClusterAPI - Proxy *Proxy - DefaultCheHost string + CheCluster *orgv1.CheCluster + ClusterAPI ClusterAPI + Proxy *Proxy + DefaultCheHost string + IsSelfSignedCertificate bool } type ClusterAPI struct { diff --git a/pkg/deploy/defaults.go b/pkg/deploy/defaults.go index 9da0b9d49..4378157e5 100644 --- a/pkg/deploy/defaults.go +++ b/pkg/deploy/defaults.go @@ -128,6 +128,9 @@ const ( // Name of the secret that holds self-signed certificate for git connections GitSelfSignedCertsConfigMapName = "che-git-self-signed-cert" + CheTLSSelfSignedCertificateSecretName = "self-signed-certificate" + DefaultCheTLSSecretName = "che-tls" + // limits DefaultDashboardMemoryLimit = "256Mi" DefaultDashboardMemoryRequest = "32Mi" diff --git a/pkg/deploy/identity-provider/deployment_keycloak.go b/pkg/deploy/identity-provider/deployment_keycloak.go index 4c7726392..752ea209f 100644 --- a/pkg/deploy/identity-provider/deployment_keycloak.go +++ b/pkg/deploy/identity-provider/deployment_keycloak.go @@ -20,6 +20,7 @@ import ( orgv1 "github.com/eclipse-che/che-operator/api/v1" "github.com/eclipse-che/che-operator/pkg/deploy" "github.com/eclipse-che/che-operator/pkg/deploy/postgres" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" "github.com/eclipse-che/che-operator/pkg/util" "github.com/google/go-cmp/cmp" "github.com/sirupsen/logrus" @@ -100,7 +101,7 @@ func GetSpecKeycloakDeployment( } } - cmResourceVersions := deploy.GetAdditionalCACertsConfigMapVersion(deployContext) + cmResourceVersions := tls.GetAdditionalCACertsConfigMapVersion(deployContext) terminationGracePeriodSeconds := int64(30) cheCertSecretVersion := getSecretResourceVersion("self-signed-certificate", deployContext.CheCluster.Namespace, deployContext.ClusterAPI) openshiftApiCertSecretVersion := getSecretResourceVersion("openshift-api-crt", deployContext.CheCluster.Namespace, deployContext.ClusterAPI) @@ -136,7 +137,7 @@ func GetSpecKeycloakDeployment( customPublicCertsVolumeSource = corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: deploy.CheAllCACertsConfigMapName, + Name: tls.CheAllCACertsConfigMapName, }, }, } diff --git a/pkg/deploy/openshift-oauth/openshiftoauth_user.go b/pkg/deploy/openshift-oauth/openshiftoauth_user.go index c3b373723..30554d3f6 100644 --- a/pkg/deploy/openshift-oauth/openshiftoauth_user.go +++ b/pkg/deploy/openshift-oauth/openshiftoauth_user.go @@ -138,9 +138,11 @@ func (oou *OpenShiftOAuthUser) Create(ctx *deploy.DeployContext) (bool, error) { return false, err } - ctx.CheCluster.Status.OpenShiftOAuthUserCredentialsSecret = OpenShiftOAuthUserCredentialsSecret - if err := deploy.UpdateCheCRStatus(ctx, "openShiftOAuthUserCredentialsSecret", OpenShiftOAuthUserCredentialsSecret); err != nil { - return false, err + if ctx.CheCluster.Status.OpenShiftOAuthUserCredentialsSecret != OpenShiftOAuthUserCredentialsSecret { + ctx.CheCluster.Status.OpenShiftOAuthUserCredentialsSecret = OpenShiftOAuthUserCredentialsSecret + if err := deploy.UpdateCheCRStatus(ctx, "openShiftOAuthUserCredentialsSecret", OpenShiftOAuthUserCredentialsSecret); err != nil { + return false, err + } } if err := deploy.AppendFinalizer(ctx, OpenshiftOauthUserFinalizerName); err != nil { diff --git a/pkg/deploy/role.go b/pkg/deploy/role.go index 76234bc1c..2e6273733 100644 --- a/pkg/deploy/role.go +++ b/pkg/deploy/role.go @@ -30,25 +30,6 @@ var roleDiffOpts = cmp.Options{ cmpopts.IgnoreFields(rbac.PolicyRule{}, "ResourceNames", "NonResourceURLs"), } -func SyncTLSRoleToCluster(deployContext *DeployContext) (bool, error) { - tlsPolicyRule := []rbac.PolicyRule{ - { - APIGroups: []string{ - "", - }, - Resources: []string{ - "secrets", - }, - Verbs: []string{ - "create", - "get", - "patch", - }, - }, - } - return SyncRoleToCluster(deployContext, CheTLSJobRoleName, tlsPolicyRule) -} - func SyncExecRoleToCluster(deployContext *DeployContext) (bool, error) { execPolicyRule := []rbac.PolicyRule{ { diff --git a/pkg/deploy/secret.go b/pkg/deploy/secret.go index 76c6fb9c8..9fc1e52c5 100644 --- a/pkg/deploy/secret.go +++ b/pkg/deploy/secret.go @@ -18,13 +18,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8slabels "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" - "k8s.io/apimachinery/pkg/types" ) var SecretDiffOpts = cmp.Options{ @@ -101,24 +98,3 @@ func GetSecretSpec(deployContext *DeployContext, name string, namespace string, return secret } - -// CreateTLSSecretFromEndpoint creates TLS secret with given name which contains certificates obtained from the given url. -// If the url is empty string, then cluster default certificate will be obtained. -// Does nothing if secret with given name already exists. -func CreateTLSSecretFromEndpoint(deployContext *DeployContext, url string, name string) (err error) { - secret := &corev1.Secret{} - if err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: deployContext.CheCluster.Namespace}, secret); err != nil && errors.IsNotFound(err) { - crtBytes, err := GetEndpointTLSCrtBytes(deployContext, url) - if err != nil { - logrus.Errorf("Failed to extract certificate for secret %s. Failed to create a secret with a self signed crt: %s", name, err) - return err - } - - _, err = SyncSecretToCluster(deployContext, name, deployContext.CheCluster.Namespace, map[string][]byte{"ca.crt": crtBytes}) - if err != nil { - return err - } - } - - return nil -} diff --git a/pkg/deploy/server/server_configmap.go b/pkg/deploy/server/server_configmap.go index efa359570..0e4bcb590 100644 --- a/pkg/deploy/server/server_configmap.go +++ b/pkg/deploy/server/server_configmap.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/eclipse-che/che-operator/pkg/deploy" + deploytls "github.com/eclipse-che/che-operator/pkg/deploy/tls" "github.com/eclipse-che/che-operator/pkg/util" "github.com/sirupsen/logrus" @@ -246,7 +247,7 @@ func (s *Server) getCheConfigMapData() (cheEnv map[string]string, err error) { CheServerSecureExposerJwtProxyImage: deploy.DefaultCheServerSecureExposerJwtProxyImage(s.deployContext.CheCluster), CheJGroupsKubernetesLabels: cheLabels, CheMetricsEnabled: cheMetrics, - CheTrustedCABundlesConfigMap: deploy.CheAllCACertsConfigMapName, + CheTrustedCABundlesConfigMap: deploytls.CheAllCACertsConfigMapName, ServerStrategy: ingressStrategy, WorkspaceExposure: workspaceExposure, SingleHostGatewayConfigMapLabels: singleHostGatewayConfigMapLabels, diff --git a/pkg/deploy/server/server_deployment.go b/pkg/deploy/server/server_deployment.go index 7d6c4a44f..43593897b 100644 --- a/pkg/deploy/server/server_deployment.go +++ b/pkg/deploy/server/server_deployment.go @@ -19,6 +19,7 @@ import ( "github.com/eclipse-che/che-operator/pkg/deploy" identity_provider "github.com/eclipse-che/che-operator/pkg/deploy/identity-provider" "github.com/eclipse-che/che-operator/pkg/deploy/postgres" + "github.com/eclipse-che/che-operator/pkg/deploy/tls" orgv1 "github.com/eclipse-che/che-operator/api/v1" "github.com/eclipse-che/che-operator/pkg/util" @@ -29,13 +30,13 @@ import ( ) func (s Server) getDeploymentSpec() (*appsv1.Deployment, error) { - selfSignedCASecretExists, err := deploy.IsSelfSignedCASecretExists(s.deployContext) + selfSignedCASecretExists, err := tls.IsSelfSignedCASecretExists(s.deployContext) if err != nil { return nil, err } cmResourceVersions := GetCheConfigMapVersion(s.deployContext) - cmResourceVersions += "," + deploy.GetAdditionalCACertsConfigMapVersion(s.deployContext) + cmResourceVersions += "," + tls.GetAdditionalCACertsConfigMapVersion(s.deployContext) terminationGracePeriodSeconds := int64(30) cheFlavor := deploy.DefaultCheFlavor(s.deployContext.CheCluster) @@ -50,7 +51,7 @@ func (s Server) getDeploymentSpec() (*appsv1.Deployment, error) { VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: deploy.CheAllCACertsConfigMapName, + Name: tls.CheAllCACertsConfigMapName, }, }, }, diff --git a/pkg/deploy/tls/certificates.go b/pkg/deploy/tls/certificates.go new file mode 100644 index 000000000..72670c475 --- /dev/null +++ b/pkg/deploy/tls/certificates.go @@ -0,0 +1,188 @@ +// +// Copyright (c) 2012-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 tls + +import ( + "context" + "reflect" + "strings" + + "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" + + // Local constants + // labelEqualSign consyant is used as a replacement for '=' symbol in labels because '=' is not allowed there + labelEqualSign = "-" + // labelCommaSign consyant 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 *deploy.DeployContext) (reconcile.Result, bool, error) { + if ctx.Proxy.TrustedCAMapName != "" { + if ctx.CheCluster.Spec.Server.ServerTrustStoreConfigMapName == "" { + ctx.CheCluster.Spec.Server.ServerTrustStoreConfigMapName = deploy.DefaultServerTrustStoreConfigMapName() + if err := deploy.UpdateCheCRSpec(ctx, "ServerTrustStoreConfigMapName", deploy.DefaultServerTrustStoreConfigMapName()); err != nil { + return reconcile.Result{}, false, err + } + + actual := &corev1.ConfigMap{} + err := ctx.ClusterAPI.NonCachingClient.Get(context.TODO(), types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: deploy.DefaultServerTrustStoreConfigMapName()}, actual) + if err == nil { + if actual.ObjectMeta.Labels[deploy.KubernetesPartOfLabelKey] != deploy.CheEclipseOrg { + if actual.ObjectMeta.Labels == nil { + actual.ObjectMeta.Labels = make(map[string]string) + } + actual.ObjectMeta.Labels[deploy.KubernetesPartOfLabelKey] = deploy.CheEclipseOrg + err := ctx.ClusterAPI.NonCachingClient.Update(context.TODO(), actual) + return reconcile.Result{}, false, err + } + } + } + + result, done, err := c.syncTrustStoreConfigMapToCluster(ctx) + if !done { + return result, done, err + } + } + + return c.syncAdditionalCACertsConfigMapToCluster(ctx) +} + +func (c *CertificatesReconciler) Finalize(ctx *deploy.DeployContext) error { + return nil +} + +func (c *CertificatesReconciler) syncTrustStoreConfigMapToCluster(ctx *deploy.DeployContext) (reconcile.Result, bool, error) { + name := ctx.CheCluster.Spec.Server.ServerTrustStoreConfigMapName + configMapSpec := deploy.GetConfigMapSpec(ctx, name, map[string]string{}, deploy.DefaultCheFlavor(ctx.CheCluster)) + + // OpenShift will automatically injects all certs into the configmap + configMapSpec.ObjectMeta.Labels[injector] = "true" + configMapSpec.ObjectMeta.Labels[deploy.KubernetesPartOfLabelKey] = deploy.CheEclipseOrg + configMapSpec.ObjectMeta.Labels[deploy.KubernetesComponentLabelKey] = CheCACertsConfigMapLabelValue + + actual := &corev1.ConfigMap{} + exists, err := deploy.GetNamespacedObject(ctx, name, actual) + if err != nil { + return reconcile.Result{}, false, err + } + + if !exists { + // We have to create an empty config map with the specific labels + done, err := deploy.Create(ctx, configMapSpec) + return reconcile.Result{}, done, err + } + + if actual.ObjectMeta.Labels[injector] != "true" || + actual.ObjectMeta.Labels[deploy.KubernetesPartOfLabelKey] != deploy.CheEclipseOrg || + actual.ObjectMeta.Labels[deploy.KubernetesComponentLabelKey] != CheCACertsConfigMapLabelValue { + + actual.ObjectMeta.Labels[injector] = "true" + actual.ObjectMeta.Labels[deploy.KubernetesPartOfLabelKey] = deploy.CheEclipseOrg + actual.ObjectMeta.Labels[deploy.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 reconcile.Result{}, false, err + } + } + + return reconcile.Result{}, true, nil +} + +func (c *CertificatesReconciler) syncAdditionalCACertsConfigMapToCluster(ctx *deploy.DeployContext) (reconcile.Result, bool, error) { + // Get all source config maps, if any + caConfigMaps, err := GetCACertsConfigMaps(ctx.ClusterAPI.Client, ctx.CheCluster.GetNamespace()) + if err != nil { + return reconcile.Result{}, 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 reconcile.Result{}, true, nil + } + } else { + if !errors.IsNotFound(err) { + return reconcile.Result{}, 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 + } + + mergedCAConfigMapSpec := deploy.GetConfigMapSpec(ctx, CheAllCACertsConfigMapName, data, deploy.DefaultCheFlavor(ctx.CheCluster)) + mergedCAConfigMapSpec.ObjectMeta.Labels[deploy.KubernetesPartOfLabelKey] = deploy.CheEclipseOrg + mergedCAConfigMapSpec.ObjectMeta.Annotations[CheMergedCAConfigMapRevisionsAnnotationKey] = revisions + done, err := deploy.SyncConfigMapSpecToCluster(ctx, mergedCAConfigMapSpec) + return reconcile.Result{}, done, err +} diff --git a/pkg/deploy/tls/certificates_test.go b/pkg/deploy/tls/certificates_test.go new file mode 100644 index 000000000..a9458999f --- /dev/null +++ b/pkg/deploy/tls/certificates_test.go @@ -0,0 +1,140 @@ +// +// Copyright (c) 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 tls + +import ( + "context" + + "testing" + + orgv1 "github.com/eclipse-che/che-operator/api/v1" + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +func TestSyncTrustStoreConfigMapToCluster(t *testing.T) { + checluster := &orgv1.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: orgv1.CheClusterSpec{ + Server: orgv1.CheClusterSpecServer{ + ServerTrustStoreConfigMapName: "trust", + }, + }, + } + ctx := deploy.GetTestDeployContext(checluster, []runtime.Object{}) + + certificates := NewCertificatesReconciler() + _, done, err := certificates.syncTrustStoreConfigMapToCluster(ctx) + assert.Nil(t, err) + assert.True(t, done) + + trustStoreConfigMap := &corev1.ConfigMap{} + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: "trust", Namespace: "eclipse-che"}, trustStoreConfigMap) + assert.Nil(t, err) + assert.Equal(t, trustStoreConfigMap.ObjectMeta.Labels[injector], "true") +} + +func TestSyncExistedTrustStoreConfigMapToCluster(t *testing.T) { + trustStoreConfigMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "trust", + Namespace: "eclipse-che", + Labels: map[string]string{"a": "b"}, + }, + Data: map[string]string{"d": "c"}, + } + checluster := &orgv1.CheCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "eclipse-che", + Name: "eclipse-che", + }, + Spec: orgv1.CheClusterSpec{ + Server: orgv1.CheClusterSpecServer{ + ServerTrustStoreConfigMapName: "trust", + }, + }, + } + ctx := deploy.GetTestDeployContext(checluster, []runtime.Object{trustStoreConfigMap}) + + certificates := NewCertificatesReconciler() + _, done, err := certificates.syncTrustStoreConfigMapToCluster(ctx) + assert.Nil(t, err) + assert.True(t, done) + + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: "trust", Namespace: "eclipse-che"}, trustStoreConfigMap) + assert.Nil(t, err) + assert.Equal(t, trustStoreConfigMap.ObjectMeta.Labels[injector], "true") + assert.Equal(t, trustStoreConfigMap.ObjectMeta.Labels["a"], "b") + assert.Equal(t, trustStoreConfigMap.Data["d"], "c") +} + +func TestSyncAdditionalCACertsConfigMapToCluster(t *testing.T) { + cert1 := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert1", + Namespace: "eclipse-che", + ResourceVersion: "1", + Labels: map[string]string{ + "app.kubernetes.io/component": "ca-bundle", + "app.kubernetes.io/part-of": "che.eclipse.org"}, + }, + Data: map[string]string{"a1": "b1"}, + } + cert2 := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cert2", + Namespace: "eclipse-che", + // Go client set up resource version 1 itself on object creation. + // ResourceVersion: "1", + Labels: map[string]string{ + "app.kubernetes.io/component": "ca-bundle", + "app.kubernetes.io/part-of": "che.eclipse.org"}, + }, + Data: map[string]string{"a2": "b2"}, + } + + ctx := deploy.GetTestDeployContext(nil, []runtime.Object{cert1}) + + certificates := NewCertificatesReconciler() + _, done, err := certificates.syncAdditionalCACertsConfigMapToCluster(ctx) + assert.Nil(t, err) + assert.True(t, done) + + cacertMerged := &corev1.ConfigMap{} + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: CheAllCACertsConfigMapName, Namespace: "eclipse-che"}, cacertMerged) + assert.Nil(t, err) + assert.Equal(t, cacertMerged.ObjectMeta.Annotations["che.eclipse.org/included-configmaps"], "cert1-1") + + // let's create another configmap + err = ctx.ClusterAPI.Client.Create(context.TODO(), cert2) + assert.Nil(t, err) + + // check ca-cert-merged + _, done, err = certificates.syncAdditionalCACertsConfigMapToCluster(ctx) + assert.Nil(t, err) + assert.False(t, done) + + _, done, err = certificates.syncAdditionalCACertsConfigMapToCluster(ctx) + assert.Nil(t, err) + assert.True(t, done) + + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: CheAllCACertsConfigMapName, Namespace: "eclipse-che"}, cacertMerged) + assert.Nil(t, err) + assert.Equal(t, cacertMerged.ObjectMeta.Annotations["che.eclipse.org/included-configmaps"], "cert1-1.cert2-1") +} diff --git a/pkg/deploy/tls/init_test.go b/pkg/deploy/tls/init_test.go new file mode 100644 index 000000000..2f2b3c30a --- /dev/null +++ b/pkg/deploy/tls/init_test.go @@ -0,0 +1,21 @@ +// +// Copyright (c) 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 tls + +import "github.com/eclipse-che/che-operator/pkg/deploy" + +func init() { + err := deploy.InitTestDefaultsFromDeployment("../../../config/manager/manager.yaml") + if err != nil { + panic(err) + } +} diff --git a/pkg/deploy/tls/tls_secret.go b/pkg/deploy/tls/tls_secret.go new file mode 100644 index 000000000..b00e1f225 --- /dev/null +++ b/pkg/deploy/tls/tls_secret.go @@ -0,0 +1,78 @@ +// +// Copyright (c) 2012-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 tls + +import ( + "github.com/eclipse-che/che-operator/pkg/deploy" + "github.com/eclipse-che/che-operator/pkg/util" + "github.com/sirupsen/logrus" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type TlsSecretReconciler struct { + deploy.Reconcilable +} + +func NewTlsSecretReconciler() *TlsSecretReconciler { + return &TlsSecretReconciler{} +} + +func (t *TlsSecretReconciler) Reconcile(ctx *deploy.DeployContext) (reconcile.Result, bool, error) { + if util.IsOpenShift { + // create a secret with router tls cert when on OpenShift infra and router is configured with a self signed certificate + if ctx.IsSelfSignedCertificate || + // To use Openshift v4 OAuth, the OAuth endpoints are served from a namespace + // and NOT from the Openshift API Master URL (as in v3) + // So we also need the self-signed certificate to access them (same as the Che server) + (util.IsOpenShift4 && ctx.CheCluster.IsOpenShiftOAuthEnabled() && !ctx.CheCluster.Spec.Server.TlsSupport) { + if err := CreateTLSSecretFromEndpoint(ctx, "", deploy.CheTLSSelfSignedCertificateSecretName); err != nil { + return reconcile.Result{}, false, err + } + } + + if util.IsOpenShift && ctx.CheCluster.IsOpenShiftOAuthEnabled() { + // create a secret with OpenShift API crt to be added to keystore that RH SSO will consume + apiUrl, apiInternalUrl, err := util.GetOpenShiftAPIUrls() + if err != nil { + logrus.Errorf("Failed to get OpenShift cluster public hostname. A secret with API crt will not be created and consumed by RH-SSO/Keycloak") + } else { + baseURL := map[bool]string{true: apiInternalUrl, false: apiUrl}[apiInternalUrl != ""] + if err := CreateTLSSecretFromEndpoint(ctx, baseURL, "openshift-api-crt"); err != nil { + return reconcile.Result{}, false, err + } + } + } + } else { + // Handle Che TLS certificates on Kubernetes infrastructure + if ctx.CheCluster.Spec.Server.TlsSupport { + if ctx.CheCluster.Spec.K8s.TlsSecretName != "" { + // Self-signed certificate should be created to secure Che ingresses + result, err := K8sHandleCheTLSSecrets(ctx) + if result.Requeue || result.RequeueAfter > 0 { + return result, false, err + } + } else if ctx.IsSelfSignedCertificate { + // Use default self-signed ingress certificate + if err := CreateTLSSecretFromEndpoint(ctx, "", deploy.CheTLSSelfSignedCertificateSecretName); err != nil { + return reconcile.Result{}, false, err + } + } + } + } + + return reconcile.Result{}, true, nil +} + +func (t *TlsSecretReconciler) Finalize(ctx *deploy.DeployContext) error { + return nil +} diff --git a/pkg/deploy/tls.go b/pkg/deploy/tls/tls_utils.go similarity index 55% rename from pkg/deploy/tls.go rename to pkg/deploy/tls/tls_utils.go index 9493043f6..b6b24e432 100644 --- a/pkg/deploy/tls.go +++ b/pkg/deploy/tls/tls_utils.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2019-2021 Red Hat, Inc. +// Copyright (c) 2012-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/ @@ -9,7 +9,8 @@ // Contributors: // Red Hat, Inc. - initial API and implementation // -package deploy + +package tls import ( "context" @@ -19,53 +20,40 @@ import ( stderrors "errors" "fmt" "net/http" - "reflect" "strings" "time" + "github.com/eclipse-che/che-operator/pkg/deploy" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/eclipse-che/che-operator/pkg/util" routev1 "github.com/openshift/api/route/v1" "github.com/sirupsen/logrus" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1" + rbac "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" - k8sclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) // TLS related constants const ( - CheTLSJobServiceAccountName = "che-tls-job-service-account" - CheTLSJobRoleName = "che-tls-job-role" - CheTLSJobRoleBindingName = "che-tls-job-role-binding" - CheTLSJobName = "che-tls-job" - CheTLSJobComponentName = "che-create-tls-secret-job" - CheTLSSelfSignedCertificateSecretName = "self-signed-certificate" - DefaultCheTLSSecretName = "che-tls" - - // 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" - - // Local constants - // labelEqualSign consyant is used as a replacement for '=' symbol in labels because '=' is not allowed there - labelEqualSign = "-" - // labelCommaSign consyant is used as a replacement for ',' symbol in labels because ',' is not allowed there - labelCommaSign = "." + CheTLSJobServiceAccountName = "che-tls-job-service-account" + CheTLSJobRoleName = "che-tls-job-role" + CheTLSJobRoleBindingName = "che-tls-job-role-binding" + CheTLSJobName = "che-tls-job" + CheTLSJobComponentName = "che-create-tls-secret-job" ) // IsSelfSignedCASecretExists checks if CheTLSSelfSignedCertificateSecretName exists so depending components can mount it -func IsSelfSignedCASecretExists(deployContext *DeployContext) (bool, error) { +func IsSelfSignedCASecretExists(ctx *deploy.DeployContext) (bool, error) { cheTLSSelfSignedCertificateSecret := &corev1.Secret{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: deployContext.CheCluster.Namespace, Name: CheTLSSelfSignedCertificateSecretName}, cheTLSSelfSignedCertificateSecret) + err := ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: deploy.CheTLSSelfSignedCertificateSecretName}, cheTLSSelfSignedCertificateSecret) if err != nil { if errors.IsNotFound(err) { return false, nil @@ -76,12 +64,12 @@ func IsSelfSignedCASecretExists(deployContext *DeployContext) (bool, error) { } // IsSelfSignedCertificateUsed detects whether endpoints are/should be secured by self-signed certificate. -func IsSelfSignedCertificateUsed(deployContext *DeployContext) (bool, error) { +func IsSelfSignedCertificateUsed(ctx *deploy.DeployContext) (bool, error) { if util.IsTestMode() { return true, nil } - cheCASecretExist, err := IsSelfSignedCASecretExists(deployContext) + cheCASecretExist, err := IsSelfSignedCASecretExists(ctx) if err != nil { return false, err } @@ -92,11 +80,11 @@ func IsSelfSignedCertificateUsed(deployContext *DeployContext) (bool, error) { if !util.IsOpenShift { // Handle custom tls secret for Che ingresses - cheTLSSecretName := deployContext.CheCluster.Spec.K8s.TlsSecretName + cheTLSSecretName := ctx.CheCluster.Spec.K8s.TlsSecretName if cheTLSSecretName != "" { // The secret is specified in CR cheTLSSecret := &corev1.Secret{} - err = deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: deployContext.CheCluster.Namespace, Name: cheTLSSecretName}, cheTLSSecret) + err = ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: cheTLSSecretName}, cheTLSSecret) if err != nil { if !errors.IsNotFound(err) { // Failed to get secret, return error to restart reconcile loop. @@ -115,7 +103,7 @@ func IsSelfSignedCertificateUsed(deployContext *DeployContext) (bool, error) { } // Get route/ingress TLS certificates chain - peerCertificates, err := GetEndpointTLSCrtChain(deployContext, "") + peerCertificates, err := GetEndpointTLSCrtChain(ctx, "") if err != nil { return false, err } @@ -133,26 +121,26 @@ func IsSelfSignedCertificateUsed(deployContext *DeployContext) (bool, error) { // GetEndpointTLSCrtChain retrieves TLS certificates chain from given endpoint. // If endpoint is not specified, then a test route/ingress will be created and used to get router certificates. -func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([]*x509.Certificate, error) { +func GetEndpointTLSCrtChain(ctx *deploy.DeployContext, endpointURL string) ([]*x509.Certificate, error) { if util.IsTestMode() { return nil, stderrors.New("Not allowed for tests") } var useTestEndpoint bool = len(endpointURL) < 1 var requestURL string - cheFlavor := DefaultCheFlavor(deployContext.CheCluster) + cheFlavor := deploy.DefaultCheFlavor(ctx.CheCluster) if useTestEndpoint { if util.IsOpenShift { // Create test route to get certificates chain. // Note, it is not possible to use SyncRouteToCluster here as it may cause infinite reconcile loop. - routeSpec, err := GetRouteSpec( - deployContext, + routeSpec, err := deploy.GetRouteSpec( + ctx, "test", "", "", "test", 8080, - deployContext.CheCluster.Spec.Server.CheServerRoute, + ctx.CheCluster.Spec.Server.CheServerRoute, cheFlavor) if err != nil { return nil, err @@ -160,7 +148,7 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ // Remove controller reference to prevent queueing new reconcile loop routeSpec.SetOwnerReferences(nil) // Create route manually - if err := deployContext.ClusterAPI.Client.Create(context.TODO(), routeSpec); err != nil { + if err := ctx.ClusterAPI.Client.Create(context.TODO(), routeSpec); err != nil { if !errors.IsAlreadyExists(err) { logrus.Errorf("Failed to create test route 'test': %s", err) return nil, err @@ -169,7 +157,7 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ // Schedule test route cleanup after the job done. defer func() { - if err := deployContext.ClusterAPI.Client.Delete(context.TODO(), routeSpec); err != nil { + if err := ctx.ClusterAPI.Client.Delete(context.TODO(), routeSpec); err != nil { logrus.Errorf("Failed to delete test route %s: %s", routeSpec.Name, err) } }() @@ -178,7 +166,7 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ route := &routev1.Route{} for { time.Sleep(time.Duration(1) * time.Second) - exists, err := GetNamespacedObject(deployContext, routeSpec.Name, route) + exists, err := deploy.GetNamespacedObject(ctx, routeSpec.Name, route) if err != nil { return nil, err } else if exists { @@ -192,17 +180,17 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ // Create test ingress to get certificates chain. // Note, it is not possible to use SyncIngressToCluster here as it may cause infinite reconcile loop. - _, ingressSpec := GetIngressSpec( - deployContext, + _, ingressSpec := deploy.GetIngressSpec( + ctx, "test", "", "", "test", 8080, - deployContext.CheCluster.Spec.Server.CheServerIngress, + ctx.CheCluster.Spec.Server.CheServerIngress, cheFlavor) // Create ingress manually - if err := deployContext.ClusterAPI.Client.Create(context.TODO(), ingressSpec); err != nil { + if err := ctx.ClusterAPI.Client.Create(context.TODO(), ingressSpec); err != nil { if !errors.IsAlreadyExists(err) { logrus.Errorf("Failed to create test ingress 'test': %s", err) return nil, err @@ -211,7 +199,7 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ // Schedule test ingress cleanup after the job done. defer func() { - if err := deployContext.ClusterAPI.Client.Delete(context.TODO(), ingressSpec); err != nil { + if err := ctx.ClusterAPI.Client.Delete(context.TODO(), ingressSpec); err != nil { logrus.Errorf("Failed to delete test ingress %s: %s", ingressSpec.Name, err) } }() @@ -220,7 +208,7 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ ingress := &networking.Ingress{} for { time.Sleep(time.Duration(1) * time.Second) - exists, err := GetNamespacedObject(deployContext, ingressSpec.Name, ingress) + exists, err := deploy.GetNamespacedObject(ctx, ingressSpec.Name, ingress) if err != nil { return nil, err } else if exists { @@ -234,14 +222,14 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ requestURL = endpointURL } - certificates, err := doRequestForTLSCrtChain(deployContext, requestURL, useTestEndpoint) + certificates, err := doRequestForTLSCrtChain(ctx, requestURL, useTestEndpoint) if err != nil { - if deployContext.Proxy.HttpProxy != "" && useTestEndpoint { + if ctx.Proxy.HttpProxy != "" && useTestEndpoint { // Fetching certificates from the test route without proxy failed. Probably non-proxy connections are blocked. // Retrying with proxy configuration, however it might cause retreiving of wrong certificate in case of TLS interception by proxy. logrus.Warn("Failed to get certificate chain of trust of the OpenShift Ingress bypassing the proxy") - return doRequestForTLSCrtChain(deployContext, requestURL, false) + return doRequestForTLSCrtChain(ctx, requestURL, false) } return nil, err @@ -249,13 +237,13 @@ func GetEndpointTLSCrtChain(deployContext *DeployContext, endpointURL string) ([ return certificates, nil } -func doRequestForTLSCrtChain(deployContext *DeployContext, requestURL string, skipProxy bool) ([]*x509.Certificate, error) { +func doRequestForTLSCrtChain(ctx *deploy.DeployContext, requestURL string, skipProxy bool) ([]*x509.Certificate, error) { transport := &http.Transport{} // Adding the proxy settings to the Transport object. // However, in case of test route we need to reach cluter directly in order to get the right certificate. - if deployContext.Proxy.HttpProxy != "" && !skipProxy { - logrus.Infof("Configuring proxy with %s to extract certificate chain from the following URL: %s", deployContext.Proxy.HttpProxy, requestURL) - ConfigureProxy(deployContext, transport) + if ctx.Proxy.HttpProxy != "" && !skipProxy { + logrus.Infof("Configuring proxy with %s to extract certificate chain from the following URL: %s", ctx.Proxy.HttpProxy, requestURL) + deploy.ConfigureProxy(ctx, transport) } transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} client := &http.Client{ @@ -276,8 +264,8 @@ func doRequestForTLSCrtChain(deployContext *DeployContext, requestURL string, sk // Creates a test TLS route/ingress if endpoint url is empty. // There's an easier way which is to read tls secret in default (3.11) or openshift-ingress (4.0) namespace // which however requires extra privileges for operator service account -func GetEndpointTLSCrtBytes(deployContext *DeployContext, endpointURL string) (certificates []byte, err error) { - peerCertificates, err := GetEndpointTLSCrtChain(deployContext, endpointURL) +func GetEndpointTLSCrtBytes(ctx *deploy.DeployContext, endpointURL string) (certificates []byte, err error) { + peerCertificates, err := GetEndpointTLSCrtChain(ctx, endpointURL) if err != nil { if util.IsTestMode() { fakeCrt := make([]byte, 5) @@ -299,14 +287,14 @@ func GetEndpointTLSCrtBytes(deployContext *DeployContext, endpointURL string) (c } // K8sHandleCheTLSSecrets handles TLS secrets required for Che deployment on Kubernetes infrastructure. -func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, error) { - cheTLSSecretName := deployContext.CheCluster.Spec.K8s.TlsSecretName +func K8sHandleCheTLSSecrets(ctx *deploy.DeployContext) (reconcile.Result, error) { + cheTLSSecretName := ctx.CheCluster.Spec.K8s.TlsSecretName - cheTLSSecretNamespacedName := types.NamespacedName{Namespace: deployContext.CheCluster.Namespace, Name: cheTLSSecretName} - CheTLSSelfSignedCertificateSecretNamespacedName := types.NamespacedName{Namespace: deployContext.CheCluster.Namespace, Name: CheTLSSelfSignedCertificateSecretName} + cheTLSSecretNamespacedName := types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: cheTLSSecretName} + CheTLSSelfSignedCertificateSecretNamespacedName := types.NamespacedName{Namespace: ctx.CheCluster.Namespace, Name: deploy.CheTLSSelfSignedCertificateSecretName} job := &batchv1.Job{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: CheTLSJobName, Namespace: deployContext.CheCluster.Namespace}, job) + err := ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: CheTLSJobName, Namespace: ctx.CheCluster.Namespace}, job) var jobExists bool if err != nil { if !errors.IsNotFound(err) { @@ -320,7 +308,7 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err // ===== Check Che server TLS certificate ===== // cheTLSSecret := &corev1.Secret{} - err = deployContext.ClusterAPI.Client.Get(context.TODO(), cheTLSSecretNamespacedName, cheTLSSecret) + err = ctx.ClusterAPI.Client.Get(context.TODO(), cheTLSSecretNamespacedName, cheTLSSecret) if err != nil { if !errors.IsNotFound(err) { // Error reading secret info @@ -343,58 +331,58 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err // Remove Che CA certificate secret if any cheCASelfSignedCertificateSecret := &corev1.Secret{} - err = deployContext.ClusterAPI.Client.Get(context.TODO(), CheTLSSelfSignedCertificateSecretNamespacedName, cheCASelfSignedCertificateSecret) + err = ctx.ClusterAPI.Client.Get(context.TODO(), CheTLSSelfSignedCertificateSecretNamespacedName, cheCASelfSignedCertificateSecret) if err != nil { if !errors.IsNotFound(err) { // Error reading secret info - logrus.Errorf("Error getting Che self-signed certificate secert \"%s\": %v", CheTLSSelfSignedCertificateSecretName, err) + logrus.Errorf("Error getting Che self-signed certificate secert \"%s\": %v", deploy.CheTLSSelfSignedCertificateSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } // Che CA certificate doesn't exists (that's expected at this point), do nothing } else { // Remove Che CA secret because Che TLS secret is missing (they should be generated together). - if err = deployContext.ClusterAPI.Client.Delete(context.TODO(), cheCASelfSignedCertificateSecret); err != nil { - logrus.Errorf("Error deleting Che self-signed certificate secret \"%s\": %v", CheTLSSelfSignedCertificateSecretName, err) + if err = ctx.ClusterAPI.Client.Delete(context.TODO(), cheCASelfSignedCertificateSecret); err != nil { + logrus.Errorf("Error deleting Che self-signed certificate secret \"%s\": %v", deploy.CheTLSSelfSignedCertificateSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } } // Prepare permissions for the certificate generation job - done, err := SyncServiceAccountToCluster(deployContext, CheTLSJobServiceAccountName) + done, err := deploy.SyncServiceAccountToCluster(ctx, CheTLSJobServiceAccountName) if !done { return reconcile.Result{RequeueAfter: time.Second}, err } - done, err = SyncTLSRoleToCluster(deployContext) + done, err = SyncTLSRoleToCluster(ctx) if !done { return reconcile.Result{}, err } - done, err = SyncRoleBindingToCluster(deployContext, CheTLSJobRoleBindingName, CheTLSJobServiceAccountName, CheTLSJobRoleName, "Role") + done, err = deploy.SyncRoleBindingToCluster(ctx, CheTLSJobRoleBindingName, CheTLSJobServiceAccountName, CheTLSJobRoleName, "Role") if !done { return reconcile.Result{}, err } - domains := deployContext.CheCluster.Spec.K8s.IngressDomain + ",*." + deployContext.CheCluster.Spec.K8s.IngressDomain - if deployContext.CheCluster.Spec.Server.CheHost != "" && !strings.Contains(deployContext.CheCluster.Spec.Server.CheHost, deployContext.CheCluster.Spec.K8s.IngressDomain) && deployContext.CheCluster.Spec.Server.CheHostTLSSecret == "" { - domains += "," + deployContext.CheCluster.Spec.Server.CheHost + domains := ctx.CheCluster.Spec.K8s.IngressDomain + ",*." + ctx.CheCluster.Spec.K8s.IngressDomain + if ctx.CheCluster.Spec.Server.CheHost != "" && !strings.Contains(ctx.CheCluster.Spec.Server.CheHost, ctx.CheCluster.Spec.K8s.IngressDomain) && ctx.CheCluster.Spec.Server.CheHostTLSSecret == "" { + domains += "," + ctx.CheCluster.Spec.Server.CheHost } labels := "" - for labelName, labelValue := range GetLabels(deployContext.CheCluster, cheTLSSecretName) { + for labelName, labelValue := range deploy.GetLabels(ctx.CheCluster, cheTLSSecretName) { labels += fmt.Sprintf("%s=%s ", labelName, labelValue) } - cheTLSSecretsCreationJobImage := DefaultCheTLSSecretsCreationJobImage() + cheTLSSecretsCreationJobImage := deploy.DefaultCheTLSSecretsCreationJobImage() jobEnvVars := map[string]string{ "DOMAIN": domains, - "CHE_NAMESPACE": deployContext.CheCluster.Namespace, + "CHE_NAMESPACE": ctx.CheCluster.Namespace, "CHE_SERVER_TLS_SECRET_NAME": cheTLSSecretName, - "CHE_CA_CERTIFICATE_SECRET_NAME": CheTLSSelfSignedCertificateSecretName, + "CHE_CA_CERTIFICATE_SECRET_NAME": deploy.CheTLSSelfSignedCertificateSecretName, "LABELS": labels, } - _, err = SyncJobToCluster(deployContext, CheTLSJobName, CheTLSJobComponentName, cheTLSSecretsCreationJobImage, CheTLSJobServiceAccountName, jobEnvVars) + _, err = deploy.SyncJobToCluster(ctx, CheTLSJobName, CheTLSJobComponentName, cheTLSSecretsCreationJobImage, CheTLSJobServiceAccountName, jobEnvVars) if err != nil { logrus.Error(err) } @@ -405,11 +393,11 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err if jobExists { // The job object is present if job.Status.Succeeded > 0 { - logrus.Infof("Import public part of Eclipse Che self-signed CA certificate from \"%s\" secret into your browser.", CheTLSSelfSignedCertificateSecretName) - deleteJob(deployContext, job) + logrus.Infof("Import public part of Eclipse Che self-signed CA certificate from \"%s\" secret into your browser.", deploy.CheTLSSelfSignedCertificateSecretName) + deleteJob(ctx, job) } else if job.Status.Failed > 0 { // The job failed, but the certificate is present, shouldn't happen - deleteJob(deployContext, job) + deleteJob(ctx, job) return reconcile.Result{}, nil } // Job hasn't reported finished status yet, wait more @@ -421,7 +409,7 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err // The secret is invalid because required field(s) missing. logrus.Infof("Che TLS secret \"%s\" is invalid. Recreating...", cheTLSSecretName) // Delete old invalid secret - if err = deployContext.ClusterAPI.Client.Delete(context.TODO(), cheTLSSecret); err != nil { + if err = ctx.ClusterAPI.Client.Delete(context.TODO(), cheTLSSecret); err != nil { logrus.Errorf("Error deleting Che TLS secret \"%s\": %v", cheTLSSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } @@ -432,11 +420,11 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err // Check owner reference if cheTLSSecret.ObjectMeta.OwnerReferences == nil { // Set owner Che cluster as Che TLS secret owner - if err := controllerutil.SetControllerReference(deployContext.CheCluster, cheTLSSecret, deployContext.ClusterAPI.Scheme); err != nil { + if err := controllerutil.SetControllerReference(ctx.CheCluster, cheTLSSecret, ctx.ClusterAPI.Scheme); err != nil { logrus.Errorf("Failed to set owner for Che TLS secret \"%s\". Error: %s", cheTLSSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } - if err := deployContext.ClusterAPI.Client.Update(context.TODO(), cheTLSSecret); err != nil { + if err := ctx.ClusterAPI.Client.Update(context.TODO(), cheTLSSecret); err != nil { logrus.Errorf("Failed to update owner for Che TLS secret \"%s\". Error: %s", cheTLSSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } @@ -445,11 +433,11 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err // ===== Check Che CA certificate ===== // cheTLSSelfSignedCertificateSecret := &corev1.Secret{} - err = deployContext.ClusterAPI.Client.Get(context.TODO(), CheTLSSelfSignedCertificateSecretNamespacedName, cheTLSSelfSignedCertificateSecret) + err = ctx.ClusterAPI.Client.Get(context.TODO(), CheTLSSelfSignedCertificateSecretNamespacedName, cheTLSSelfSignedCertificateSecret) if err != nil { if !errors.IsNotFound(err) { // Error reading Che self-signed secret info - logrus.Errorf("Error getting Che self-signed certificate secert \"%s\": %v", CheTLSSelfSignedCertificateSecretName, err) + logrus.Errorf("Error getting Che self-signed certificate secert \"%s\": %v", deploy.CheTLSSelfSignedCertificateSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } // Che CA self-signed cetificate secret doesn't exist. @@ -457,15 +445,15 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err } else { // Che CA self-signed certificate secret exists, check for required data fields if !isCheCASecretValid(cheTLSSelfSignedCertificateSecret) { - logrus.Infof("Che self-signed certificate secret \"%s\" is invalid. Recrating...", CheTLSSelfSignedCertificateSecretName) + logrus.Infof("Che self-signed certificate secret \"%s\" is invalid. Recrating...", deploy.CheTLSSelfSignedCertificateSecretName) // Che CA self-signed certificate secret is invalid, delete it - if err = deployContext.ClusterAPI.Client.Delete(context.TODO(), cheTLSSelfSignedCertificateSecret); err != nil { - logrus.Errorf("Error deleting Che self-signed certificate secret \"%s\": %v", CheTLSSelfSignedCertificateSecretName, err) + if err = ctx.ClusterAPI.Client.Delete(context.TODO(), cheTLSSelfSignedCertificateSecret); err != nil { + logrus.Errorf("Error deleting Che self-signed certificate secret \"%s\": %v", deploy.CheTLSSelfSignedCertificateSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } // Also delete Che TLS as the certificates should be created together // Here it is not mandatory to check Che TLS secret existence as it is handled above - if err = deployContext.ClusterAPI.Client.Delete(context.TODO(), cheTLSSecret); err != nil { + if err = ctx.ClusterAPI.Client.Delete(context.TODO(), cheTLSSecret); err != nil { logrus.Errorf("Error deleting Che TLS secret \"%s\": %v", cheTLSSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } @@ -476,12 +464,12 @@ func K8sHandleCheTLSSecrets(deployContext *DeployContext) (reconcile.Result, err // Check owner reference if cheTLSSelfSignedCertificateSecret.ObjectMeta.OwnerReferences == nil { // Set owner Che cluster as Che TLS secret owner - if err := controllerutil.SetControllerReference(deployContext.CheCluster, cheTLSSelfSignedCertificateSecret, deployContext.ClusterAPI.Scheme); err != nil { - logrus.Errorf("Failed to set owner for Che self-signed certificate secret \"%s\". Error: %s", CheTLSSelfSignedCertificateSecretName, err) + if err := controllerutil.SetControllerReference(ctx.CheCluster, cheTLSSelfSignedCertificateSecret, ctx.ClusterAPI.Scheme); err != nil { + logrus.Errorf("Failed to set owner for Che self-signed certificate secret \"%s\". Error: %s", deploy.CheTLSSelfSignedCertificateSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } - if err := deployContext.ClusterAPI.Client.Update(context.TODO(), cheTLSSelfSignedCertificateSecret); err != nil { - logrus.Errorf("Failed to update owner for Che self-signed certificate secret \"%s\". Error: %s", CheTLSSelfSignedCertificateSecretName, err) + if err := ctx.ClusterAPI.Client.Update(context.TODO(), cheTLSSelfSignedCertificateSecret); err != nil { + logrus.Errorf("Failed to update owner for Che self-signed certificate secret \"%s\". Error: %s", deploy.CheTLSSelfSignedCertificateSecretName, err) return reconcile.Result{RequeueAfter: time.Second}, err } } @@ -508,105 +496,31 @@ func isCheCASecretValid(cheCASelfSignedCertificateSecret *corev1.Secret) bool { return true } -func deleteJob(deployContext *DeployContext, job *batchv1.Job) { - names := util.K8sclient.GetPodsByComponent(CheTLSJobComponentName, deployContext.CheCluster.Namespace) +func deleteJob(ctx *deploy.DeployContext, job *batchv1.Job) { + names := util.K8sclient.GetPodsByComponent(CheTLSJobComponentName, ctx.CheCluster.Namespace) for _, podName := range names { pod := &corev1.Pod{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: podName, Namespace: deployContext.CheCluster.Namespace}, pod) + err := ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: podName, Namespace: ctx.CheCluster.Namespace}, pod) if err == nil { // Delete pod (for some reasons pod isn't removed when job is removed) - if err = deployContext.ClusterAPI.Client.Delete(context.TODO(), pod); err != nil { + if err = ctx.ClusterAPI.Client.Delete(context.TODO(), pod); err != nil { logrus.Errorf("Error deleting pod: '%s', error: %v", podName, err) } } } - if err := deployContext.ClusterAPI.Client.Delete(context.TODO(), job); err != nil { + if err := ctx.ClusterAPI.Client.Delete(context.TODO(), job); err != nil { logrus.Errorf("Error deleting job: '%s', error: %v", CheTLSJobName, err) } } -// SyncAdditionalCACertsConfigMapToCluster makes sure that additional CA certs config map is up to date if any -func SyncAdditionalCACertsConfigMapToCluster(deployContext *DeployContext) (bool, error) { - cr := deployContext.CheCluster - // Get all source config maps, if any - caConfigMaps, err := GetCACertsConfigMaps(deployContext.ClusterAPI.Client, deployContext.CheCluster.GetNamespace()) - if err != nil { - return false, err - } - if len(cr.Spec.Server.ServerTrustStoreConfigMapName) > 0 { - crConfigMap := &corev1.ConfigMap{} - err := deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: deployContext.CheCluster.Namespace, Name: cr.Spec.Server.ServerTrustStoreConfigMapName}, crConfigMap) - if err != nil { - return false, err - } - caConfigMaps = append(caConfigMaps, *crConfigMap) - } - - mergedCAConfigMap := &corev1.ConfigMap{} - err = deployContext.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: deployContext.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 - } - - mergedCAConfigMapSpec := GetConfigMapSpec(deployContext, CheAllCACertsConfigMapName, data, DefaultCheFlavor(cr)) - mergedCAConfigMapSpec.ObjectMeta.Labels[KubernetesPartOfLabelKey] = CheEclipseOrg - mergedCAConfigMapSpec.ObjectMeta.Annotations[CheMergedCAConfigMapRevisionsAnnotationKey] = revisions - return SyncConfigMapSpecToCluster(deployContext, mergedCAConfigMapSpec) -} - // GetCACertsConfigMaps returns list of config maps with additional CA certificates that should be trusted by Che // The selection is based on the specific label func GetCACertsConfigMaps(client k8sclient.Client, namespace string) ([]corev1.ConfigMap, error) { CACertsConfigMapList := &corev1.ConfigMapList{} - caBundleLabelSelectorRequirement, _ := labels.NewRequirement(KubernetesComponentLabelKey, selection.Equals, []string{CheCACertsConfigMapLabelValue}) - cheComponetLabelSelectorRequirement, _ := labels.NewRequirement(KubernetesPartOfLabelKey, selection.Equals, []string{CheEclipseOrg}) + caBundleLabelSelectorRequirement, _ := labels.NewRequirement(deploy.KubernetesComponentLabelKey, selection.Equals, []string{CheCACertsConfigMapLabelValue}) + cheComponetLabelSelectorRequirement, _ := labels.NewRequirement(deploy.KubernetesPartOfLabelKey, selection.Equals, []string{deploy.CheEclipseOrg}) listOptions := &k8sclient.ListOptions{ LabelSelector: labels.NewSelector().Add(*cheComponetLabelSelectorRequirement).Add(*caBundleLabelSelectorRequirement), Namespace: namespace, @@ -619,12 +533,52 @@ func GetCACertsConfigMaps(client k8sclient.Client, namespace string) ([]corev1.C } // GetAdditionalCACertsConfigMapVersion returns revision of merged additional CA certs config map -func GetAdditionalCACertsConfigMapVersion(deployContext *DeployContext) string { +func GetAdditionalCACertsConfigMapVersion(ctx *deploy.DeployContext) string { trustStoreConfigMap := &corev1.ConfigMap{} - exists, _ := GetNamespacedObject(deployContext, CheAllCACertsConfigMapName, trustStoreConfigMap) + exists, _ := deploy.GetNamespacedObject(ctx, CheAllCACertsConfigMapName, trustStoreConfigMap) if exists { return trustStoreConfigMap.ResourceVersion } return "" } + +// CreateTLSSecretFromEndpoint creates TLS secret with given name which contains certificates obtained from the given url. +// If the url is empty string, then cluster default certificate will be obtained. +// Does nothing if secret with given name already exists. +func CreateTLSSecretFromEndpoint(ctx *deploy.DeployContext, url string, name string) (err error) { + secret := &corev1.Secret{} + if err := ctx.ClusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: ctx.CheCluster.Namespace}, secret); err != nil && errors.IsNotFound(err) { + crtBytes, err := GetEndpointTLSCrtBytes(ctx, url) + if err != nil { + logrus.Errorf("Failed to extract certificate for secret %s. Failed to create a secret with a self signed crt: %s", name, err) + return err + } + + _, err = deploy.SyncSecretToCluster(ctx, name, ctx.CheCluster.Namespace, map[string][]byte{"ca.crt": crtBytes}) + if err != nil { + return err + } + } + + return nil +} + +func SyncTLSRoleToCluster(ctx *deploy.DeployContext) (bool, error) { + tlsPolicyRule := []rbac.PolicyRule{ + { + APIGroups: []string{ + "", + }, + Resources: []string{ + "secrets", + }, + Verbs: []string{ + "create", + "get", + "patch", + }, + }, + } + return deploy.SyncRoleToCluster(ctx, CheTLSJobRoleName, tlsPolicyRule) +} diff --git a/pkg/deploy/tls_test.go b/pkg/deploy/tls_test.go deleted file mode 100644 index fce7ab3c7..000000000 --- a/pkg/deploy/tls_test.go +++ /dev/null @@ -1,109 +0,0 @@ -// -// 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 deploy - -import ( - "context" - "testing" - - orgv1 "github.com/eclipse-che/che-operator/api/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -func TestSyncAdditionalCACertsConfigMapToCluster(t *testing.T) { - cert1 := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cert1", - Namespace: "eclipse-che", - ResourceVersion: "1", - Labels: map[string]string{ - "app.kubernetes.io/component": "ca-bundle", - "app.kubernetes.io/part-of": "che.eclipse.org"}, - }, - Data: map[string]string{"a1": "b1"}, - } - cert2 := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cert2", - Namespace: "eclipse-che", - // Go client set up resource version 1 itself on object creation. - // ResourceVersion: "1", - Labels: map[string]string{ - "app.kubernetes.io/component": "ca-bundle", - "app.kubernetes.io/part-of": "che.eclipse.org"}, - }, - Data: map[string]string{"a2": "b2"}, - } - - orgv1.SchemeBuilder.AddToScheme(scheme.Scheme) - corev1.SchemeBuilder.AddToScheme(scheme.Scheme) - cli := fake.NewFakeClientWithScheme(scheme.Scheme, cert1) - deployContext := &DeployContext{ - CheCluster: &orgv1.CheCluster{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "eclipse-che", - Name: "eclipse-che", - }, - }, - ClusterAPI: ClusterAPI{ - Client: cli, - NonCachingClient: cli, - Scheme: scheme.Scheme, - }, - } - - // check ca-cert-merged - done, err := SyncAdditionalCACertsConfigMapToCluster(deployContext) - if !done || err != nil { - t.Fatalf("Failed to sync config map: %v", err) - } - - cacertMerged := &corev1.ConfigMap{} - err = cli.Get(context.TODO(), types.NamespacedName{Name: CheAllCACertsConfigMapName, Namespace: "eclipse-che"}, cacertMerged) - if err != nil { - t.Fatalf("Failed to get config map: %v", err) - } - if cacertMerged.ObjectMeta.Annotations["che.eclipse.org/included-configmaps"] != "cert1-1" { - t.Fatalf("Failed to sync config map") - } - - // let's create another configmap - err = cli.Create(context.TODO(), cert2) - if err != nil { - t.Fatalf("Failed to create config map: %v", err) - } - - // check ca-cert-merged - _, err = SyncAdditionalCACertsConfigMapToCluster(deployContext) - if err != nil { - t.Fatalf("Failed to sync config map: %v", err) - } - - // sync twice to be sure update done correctly - done, err = SyncAdditionalCACertsConfigMapToCluster(deployContext) - if !done || err != nil { - t.Fatalf("Failed to sync config map: %v", err) - } - - err = cli.Get(context.TODO(), types.NamespacedName{Name: CheAllCACertsConfigMapName, Namespace: "eclipse-che"}, cacertMerged) - if err != nil { - t.Fatalf("Failed to get config map: %v", err) - } - if cacertMerged.ObjectMeta.Annotations["che.eclipse.org/included-configmaps"] != "cert1-1.cert2-1" { - t.Fatalf("Failed to sync config map") - } -}