Automate TLS secrets generation for Kubernetes family infrastructures (#220)

* Automate TLS secrets generation for Kubernetes family infrastructures

Signed-off-by: Mykola Morhun <mmorhun@redhat.com>
pull/240/head
Mykola Morhun 2020-04-28 16:48:58 +03:00 committed by GitHub
parent e655435d5d
commit bc47b7b1af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 798 additions and 130 deletions

View File

@ -51,6 +51,8 @@ spec:
value: quay.io/eclipse/che-plugin-registry:7.12.0
- name: IMAGE_default_devfile_registry
value: quay.io/eclipse/che-devfile-registry:7.12.0
- name: IMAGE_default_che_tls_secrets_creation_job
value: quay.io/eclipse/che-tls-secret-creator:alpine-3029769
- name: IMAGE_default_pvc_jobs
value: registry.access.redhat.com/ubi8-minimal:8.1-409
- name: IMAGE_default_postgres

View File

@ -50,6 +50,8 @@ spec:
value: quay.io/eclipse/che-plugin-registry:7.12.0
- name: IMAGE_default_devfile_registry
value: quay.io/eclipse/che-devfile-registry:7.12.0
- name: IMAGE_default_che_tls_secrets_creation_job
value: quay.io/eclipse/che-tls-secret-creator:alpine-3029769
- name: IMAGE_default_pvc_jobs
value: registry.access.redhat.com/ubi8-minimal:8.1-409
- name: IMAGE_default_postgres

View File

@ -20,6 +20,12 @@ rules:
- ingresses
verbs:
- '*'
- apiGroups:
- batch
resources:
- jobs
verbs:
- '*'
- apiGroups:
- route.openshift.io
resources:

View File

@ -273,6 +273,26 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
}
// Handle Che TLS certificates on Kubernetes like infrastructures
if instance.Spec.Server.TlsSupport && instance.Spec.Server.SelfSignedCert && !isOpenShift {
// Ensure TLS configuration is correct
if err := CheckAndUpdateTLSConfiguration(instance, clusterAPI); err != nil {
instance, _ = r.GetCR(request)
return reconcile.Result{Requeue: true, RequeueAfter: time.Second * 1}, err
}
// Create TLS secrets if needed
result, err := HandleCheTLSSecrets(instance, clusterAPI)
if result.Requeue || result.RequeueAfter > 0 {
if err != nil {
logrus.Error(err)
}
if !tests {
return result, err
}
}
}
// Get custom ConfigMap
// if it exists, add the data into CustomCheProperties
customConfigMap := &corev1.ConfigMap{}
@ -401,44 +421,97 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
// create service accounts:
// che is the one which token is used to create workspace objects
// che-workspace is SA used by plugins like exec and terminal with limited privileges
cheServiceAccount := deploy.NewServiceAccount(instance, "che")
if err := r.CreateServiceAccount(instance, cheServiceAccount); err != nil {
return reconcile.Result{}, err
cheSA, err := deploy.SyncServiceAccountToCluster(instance, "che", clusterAPI)
if cheSA == nil {
logrus.Info("Waiting on service account 'che' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
workspaceServiceAccount := deploy.NewServiceAccount(instance, "che-workspace")
if err := r.CreateServiceAccount(instance, workspaceServiceAccount); err != nil {
return reconcile.Result{}, err
cheWorkspaceSA, err := deploy.SyncServiceAccountToCluster(instance, "che-workspace", clusterAPI)
if cheWorkspaceSA == nil {
logrus.Info("Waiting on service account 'che-workspace' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
// create exec and view roles for CheCluster server and workspaces
execRole := deploy.NewRole(instance, "exec", []string{"pods/exec"}, []string{"*"})
if err := r.CreateNewRole(instance, execRole); err != nil {
return reconcile.Result{}, err
role, err := deploy.SyncRoleToCluster(instance, "exec", []string{"pods/exec"}, []string{"*"}, clusterAPI)
if role == nil {
logrus.Info("Waiting on role 'exec' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
viewRole := deploy.NewRole(instance, "view", []string{"pods"}, []string{"list"})
if err := r.CreateNewRole(instance, viewRole); err != nil {
return reconcile.Result{}, err
viewRole, err := deploy.SyncRoleToCluster(instance, "view", []string{"pods"}, []string{"list"}, clusterAPI)
if viewRole == nil {
logrus.Info("Waiting on role 'view' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
// create RoleBindings for created (and existing ClusterRole) roles and service accounts
cheRoleBinding := deploy.NewRoleBinding(instance, "che", cheServiceAccount.Name, "edit", "ClusterRole")
if err := r.CreateNewRoleBinding(instance, cheRoleBinding); err != nil {
return reconcile.Result{}, err
cheRoleBinding, err := deploy.SyncRoleBindingToCluster(instance, "che", "che", "edit", "ClusterRole", clusterAPI)
if cheRoleBinding == nil {
logrus.Info("Waiting on role binding 'che' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
execRoleBinding := deploy.NewRoleBinding(instance, "che-workspace-exec", workspaceServiceAccount.Name, execRole.Name, "Role")
if err = r.CreateNewRoleBinding(instance, execRoleBinding); err != nil {
return reconcile.Result{}, err
cheWSExecRoleBinding, err := deploy.SyncRoleBindingToCluster(instance, "che-workspace-exec", "che-workspace", "exec", "Role", clusterAPI)
if cheWSExecRoleBinding == nil {
logrus.Info("Waiting on role binding 'che-workspace-exec' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
viewRoleBinding := deploy.NewRoleBinding(instance, "che-workspace-view", workspaceServiceAccount.Name, viewRole.Name, "Role")
if err := r.CreateNewRoleBinding(instance, viewRoleBinding); err != nil {
return reconcile.Result{}, err
cheWSViewRoleBinding, err := deploy.SyncRoleBindingToCluster(instance, "che-workspace-view", "che-workspace", "view", "Role", clusterAPI)
if cheWSViewRoleBinding == nil {
logrus.Info("Waiting on role binding 'che-workspace-view' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
// If the user specified an additional cluster role to use for the Che workspace, create a role binding for it
// Use a role binding instead of a cluster role binding to keep the additional access scoped to the workspace's namespace
workspaceClusterRole := instance.Spec.Server.CheWorkspaceClusterRole
if workspaceClusterRole != "" {
customRoleBinding := deploy.NewRoleBinding(instance, "che-workspace-custom", workspaceServiceAccount.Name, workspaceClusterRole, "ClusterRole")
if err = r.CreateNewRoleBinding(instance, customRoleBinding); err != nil {
return reconcile.Result{}, err
cheWSCustomRoleBinding, err := deploy.SyncRoleBindingToCluster(instance, "che-workspace-custom", "view", workspaceClusterRole, "ClusterRole", clusterAPI)
if cheWSCustomRoleBinding == nil {
logrus.Info("Waiting on role binding 'che-workspace-custom' to be created")
if err != nil {
logrus.Error(err)
}
if !tests {
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
}
@ -782,10 +855,6 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
}
guessedDevfileRegistryURL := protocol + "://" + host
if err != nil {
return reconcile.Result{}, err
}
if devfileRegistryURL == "" {
devfileRegistryURL = guessedDevfileRegistryURL
}
@ -942,9 +1011,6 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e
}
guessedPluginRegistryURL := protocol + "://" + host
if err != nil {
return reconcile.Result{}, err
}
guessedPluginRegistryURL += "/v3"
if pluginRegistryURL == "" {
pluginRegistryURL = guessedPluginRegistryURL

View File

@ -21,54 +21,12 @@ import (
oauth "github.com/openshift/api/oauth/v1"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
rbac "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
func (r *ReconcileChe) CreateServiceAccount(cr *orgv1.CheCluster, serviceAccount *corev1.ServiceAccount) error {
if err := controllerutil.SetControllerReference(cr, serviceAccount, r.scheme); err != nil {
return err
}
serviceAccountFound := &corev1.ServiceAccount{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: serviceAccount.Name, Namespace: serviceAccount.Namespace}, serviceAccountFound)
if err != nil && errors.IsNotFound(err) {
logrus.Infof("Creating a new object: %s, name: %s", serviceAccount.Kind, serviceAccount.Name)
err = r.client.Create(context.TODO(), serviceAccount)
if err != nil {
logrus.Errorf("Failed to create %s %s: %s", serviceAccount.Name, serviceAccount.Kind, err)
return err
}
return nil
} else if err != nil {
return err
}
return nil
}
func (r *ReconcileChe) CreateNewRole(instance *orgv1.CheCluster, role *rbac.Role) error {
if err := controllerutil.SetControllerReference(instance, role, r.scheme); err != nil {
return err
}
roleFound := &rbac.Role{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, roleFound)
if err != nil && errors.IsNotFound(err) {
logrus.Infof("Creating a new object: %s, name: %s", role.Kind, role.Name)
err = r.client.Create(context.TODO(), role)
if err != nil {
logrus.Errorf("Failed to create %s %s: %s", role.Name, role.Kind, err)
return err
}
return nil
} else if err != nil {
logrus.Errorf("An error occurred: %s", err)
return err
}
return nil
}
func (r *ReconcileChe) CreateNewSecret(instance *orgv1.CheCluster, secret *corev1.Secret) error {
if err := controllerutil.SetControllerReference(instance, secret, r.scheme); err != nil {
logrus.Errorf("An error occurred: %s", err)
@ -111,27 +69,6 @@ func (r *ReconcileChe) CreateNewOauthClient(instance *orgv1.CheCluster, oAuthCli
return nil
}
func (r *ReconcileChe) CreateNewRoleBinding(instance *orgv1.CheCluster, roleBinding *rbac.RoleBinding) error {
if err := controllerutil.SetControllerReference(instance, roleBinding, r.scheme); err != nil {
return err
}
roleBindingFound := &rbac.RoleBinding{}
err := r.client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: roleBinding.Namespace}, roleBindingFound)
if err != nil && errors.IsNotFound(err) {
logrus.Infof("Creating a new object: %s, name: %s", roleBinding.Kind, roleBinding.Name)
err = r.client.Create(context.TODO(), roleBinding)
if err != nil {
logrus.Errorf("Failed to create %s %s: %s", roleBinding.Name, roleBinding.Kind, err)
return err
}
return nil
} else if err != nil {
logrus.Errorf("An error occurred: %s", err)
return err
}
return nil
}
func (r *ReconcileChe) CreateIdentityProviderItems(instance *orgv1.CheCluster, request reconcile.Request, cheFlavor string, keycloakDeploymentName string, isOpenShift4 bool) (err error) {
tests := r.tests
oAuthClientName := instance.Spec.Auth.OAuthClientName

View File

@ -0,0 +1,262 @@
//
// Copyright (c) 2020 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//
// Contributors:
// Red Hat, Inc. - initial API and implementation
//
package che
import (
"context"
"time"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/eclipse/che-operator/pkg/deploy"
"github.com/sirupsen/logrus"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
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"
)
// HandleCheTLSSecrets handles TLS secrets required for Che deployment
func HandleCheTLSSecrets(checluster *orgv1.CheCluster, clusterAPI deploy.ClusterAPI) (reconcile.Result, error) {
cheTLSSecretName := checluster.Spec.K8s.TlsSecretName
// ===== Check Che TLS certificate ===== //
cheTLSSecret := &corev1.Secret{}
err := clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: checluster.Namespace, Name: cheTLSSecretName}, cheTLSSecret)
if err != nil {
if !errors.IsNotFound(err) {
// Error reading secret info
logrus.Errorf("Error getting Che TLS secert \"%s\": %v", cheTLSSecretName, err)
return reconcile.Result{RequeueAfter: time.Second}, err
}
// Che TLS secret doesn't exist, generate a new one
// Remove Che CA certificate secret if any
cheCASelfSignedCertificateSecret := &corev1.Secret{}
err = clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: checluster.Namespace, Name: CheTLSSelfSignedCertificateSecretName}, 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)
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 = clusterAPI.Client.Delete(context.TODO(), cheCASelfSignedCertificateSecret); err != nil {
logrus.Errorf("Error deleting Che self-signed certificate secret \"%s\": %v", CheTLSSelfSignedCertificateSecretName, err)
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
// Prepare permissions for the certificate generation job
sa, err := deploy.SyncServiceAccountToCluster(checluster, CheTLSJobServiceAccountName, clusterAPI)
if sa == nil {
return reconcile.Result{RequeueAfter: time.Second}, err
}
role, err := deploy.SyncRoleToCluster(checluster, CheTLSJobRoleName, []string{"secrets"}, []string{"create"}, clusterAPI)
if role == nil {
return reconcile.Result{RequeueAfter: time.Second}, err
}
roleBiding, err := deploy.SyncRoleBindingToCluster(checluster, CheTLSJobRoleBindingName, CheTLSJobServiceAccountName, CheTLSJobRoleName, "Role", clusterAPI)
if roleBiding == nil {
return reconcile.Result{RequeueAfter: time.Second}, err
}
cheTLSSecretsCreationJobImage := deploy.DefaultCheTLSSecretsCreationJobImage()
jobEnvVars := map[string]string{
"DOMAIN": checluster.Spec.K8s.IngressDomain,
"CHE_NAMESPACE": checluster.Namespace,
"CHE_SERVER_TLS_SECRET_NAME": cheTLSSecretName,
"CHE_CA_CERTIFICATE_SECRET_NAME": CheTLSSelfSignedCertificateSecretName,
}
job, err := deploy.SyncJobToCluster(checluster, CheTLSJobName, CheTlsJobComponentName, cheTLSSecretsCreationJobImage, CheTLSJobServiceAccountName, jobEnvVars, clusterAPI)
if err != nil {
logrus.Error(err)
return reconcile.Result{RequeueAfter: time.Second}, err
}
if job == nil || job.Status.Succeeded == 0 {
logrus.Infof("Waiting on job '%s' to be finished", CheTLSJobName)
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
// cleanup job
job := &batchv1.Job{}
err = clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: CheTLSJobName, Namespace: checluster.Namespace}, job)
if err != nil && !errors.IsNotFound(err) {
// Failed to get the job
return reconcile.Result{RequeueAfter: time.Second}, err
}
if err == nil {
// The job object is present
if job.Status.Succeeded > 0 {
logrus.Infof("Import public part of Eclipse Che self-signed CA certificvate from \"%s\" secret into your browser.", CheTLSSelfSignedCertificateSecretName)
deleteJob(job, checluster, clusterAPI)
} else if job.Status.Failed > 0 {
// The job failed, but the certificate is present, shouldn't happen
deleteJob(job, checluster, clusterAPI)
return reconcile.Result{}, nil
}
// Job hasn't reported finished status yet, wait more
return reconcile.Result{RequeueAfter: time.Second}, nil
}
// Che TLS certificate exists, check for required data fields
if !isCheTLSSecretValid(cheTLSSecret) {
// 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 = 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
}
// Recreate the secret
return reconcile.Result{RequeueAfter: time.Second}, err
}
// Check owner reference
if cheTLSSecret.ObjectMeta.OwnerReferences == nil {
// Set owner Che cluster as Che TLS secret owner
if err := controllerutil.SetControllerReference(checluster, cheTLSSecret, 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 := 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
}
}
// ===== Check Che CA certificate ===== //
cheTLSSelfSignedCertificateSecret := &corev1.Secret{}
err = clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: checluster.Namespace, Name: CheTLSSelfSignedCertificateSecretName}, 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)
return reconcile.Result{RequeueAfter: time.Second}, err
}
// Che CA self-signed cetificate secret doesn't exist.
// Such situation could happen between reconcile loops, when CA cert is deleted.
// However the certificates should be created together,
// so it is mandatory to remove Che TLS secret now and recreate the pair.
cheTLSSecret = &corev1.Secret{}
err = clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Namespace: checluster.Namespace, Name: cheTLSSecretName}, cheTLSSecret)
if err != nil { // No need to check for not found error as the secret already exists at this point
logrus.Errorf("Error getting Che TLS secert \"%s\": %v", cheTLSSecretName, err)
return reconcile.Result{RequeueAfter: time.Second}, err
}
if err = 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
}
// Invalid secrets cleaned up, recreate them now
return reconcile.Result{RequeueAfter: time.Second}, err
}
// 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)
// Che CA self-signed certificate secret is invalid, delete it
if err = clusterAPI.Client.Delete(context.TODO(), cheTLSSelfSignedCertificateSecret); err != nil {
logrus.Errorf("Error deleting Che self-signed certificate secret \"%s\": %v", 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 = 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
}
// Regenerate Che TLS certicates and recreate secrets
return reconcile.Result{RequeueAfter: time.Second}, nil
}
// Check owner reference
if cheTLSSelfSignedCertificateSecret.ObjectMeta.OwnerReferences == nil {
// Set owner Che cluster as Che TLS secret owner
if err := controllerutil.SetControllerReference(checluster, cheTLSSelfSignedCertificateSecret, clusterAPI.Scheme); err != nil {
logrus.Errorf("Failed to set owner for Che self-signed certificate secret \"%s\". Error: %s", CheTLSSelfSignedCertificateSecretName, err)
return reconcile.Result{RequeueAfter: time.Second}, err
}
if err := 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)
return reconcile.Result{RequeueAfter: time.Second}, err
}
}
// Both secrets are ok, go further in reconcile loop
return reconcile.Result{}, nil
}
func isCheTLSSecretValid(cheTLSSecret *corev1.Secret) bool {
if data, exists := cheTLSSecret.Data["tls.key"]; !exists || len(data) == 0 {
return false
}
if data, exists := cheTLSSecret.Data["tls.crt"]; !exists || len(data) == 0 {
return false
}
return true
}
func isCheCASecretValid(cheCASelfSignedCertificateSecret *corev1.Secret) bool {
if data, exists := cheCASelfSignedCertificateSecret.Data["ca.crt"]; !exists || len(data) == 0 {
return false
}
return true
}
// CheckAndUpdateTLSConfiguration validates TLS configuration and precreated objects if any
// If configuration is wrong it will guess most common use cases and will make changes in Che CR accordingly to the assumption.
func CheckAndUpdateTLSConfiguration(checluster *orgv1.CheCluster, clusterAPI deploy.ClusterAPI) error {
if checluster.Spec.K8s.TlsSecretName == "" {
checluster.Spec.K8s.TlsSecretName = "che-tls"
if err := deploy.UpdateCheCRSpec(checluster, "TlsSecretName", checluster.Spec.K8s.TlsSecretName, clusterAPI); err != nil {
return err
}
}
return nil
}
func deleteJob(job *batchv1.Job, checluster *orgv1.CheCluster, clusterAPI deploy.ClusterAPI) {
names := k8sclient.GetPodsByComponent(CheTlsJobComponentName, checluster.Namespace)
for _, podName := range names {
pod := &corev1.Pod{}
err := clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: podName, Namespace: checluster.Namespace}, pod)
if err == nil {
// Delete pod (for some reasons pod isn't removed when job is removed)
if err = clusterAPI.Client.Delete(context.TODO(), pod); err != nil {
logrus.Errorf("Error deleting pod: '%s', error: %v", podName, err)
}
}
}
if err := clusterAPI.Client.Delete(context.TODO(), job); err != nil {
logrus.Errorf("Error deleting job: '%s', error: %v", CheTLSJobName, err)
}
}

View File

@ -27,13 +27,14 @@ import (
)
var (
defaultCheServerImage string
defaultCheVersion string
defaultPluginRegistryImage string
defaultDevfileRegistryImage string
defaultPvcJobsImage string
defaultPostgresImage string
defaultKeycloakImage string
defaultCheServerImage string
defaultCheVersion string
defaultPluginRegistryImage string
defaultDevfileRegistryImage string
defaultCheTLSSecretsCreationJobImage string
defaultPvcJobsImage string
defaultPostgresImage string
defaultKeycloakImage string
defaultCheWorkspacePluginBrokerMetadataImage string
defaultCheWorkspacePluginBrokerArtifactsImage string
@ -114,6 +115,7 @@ func InitDefaultsFromEnv() {
defaultCheServerImage = getDefaultFromEnv("IMAGE_default_che_server")
defaultPluginRegistryImage = getDefaultFromEnv("IMAGE_default_plugin_registry")
defaultDevfileRegistryImage = getDefaultFromEnv("IMAGE_default_devfile_registry")
defaultCheTLSSecretsCreationJobImage = getDefaultFromEnv("IMAGE_default_che_tls_secrets_creation_job")
defaultPvcJobsImage = getDefaultFromEnv("IMAGE_default_pvc_jobs")
defaultPostgresImage = getDefaultFromEnv("IMAGE_default_postgres")
defaultKeycloakImage = getDefaultFromEnv("IMAGE_default_keycloak")
@ -133,6 +135,7 @@ func InitDefaultsFromFile(defaultsPath string) {
defaultCheServerImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_che_server")
defaultPluginRegistryImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_plugin_registry")
defaultDevfileRegistryImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_devfile_registry")
defaultCheTLSSecretsCreationJobImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_che_tls_secrets_creation_job")
defaultPvcJobsImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_pvc_jobs")
defaultPostgresImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_postgres")
defaultKeycloakImage = util.GetDeploymentEnv(operatorDeployment, "IMAGE_default_keycloak")
@ -195,6 +198,10 @@ func DefaultCheServerImage(cr *orgv1.CheCluster) string {
return patchDefaultImageName(cr, defaultCheServerImage)
}
func DefaultCheTLSSecretsCreationJobImage() string {
return defaultCheTLSSecretsCreationJobImage
}
func DefaultPvcJobsImage(cr *orgv1.CheCluster) string {
return patchDefaultImageName(cr, defaultPvcJobsImage)
}

View File

@ -20,16 +20,17 @@ import (
)
const (
cheVersionTest = "7.12.0"
cheServerImageTest = "quay.io/eclipse/che-server:7.12.0"
pluginRegistryImageTest = "quay.io/eclipse/che-plugin-registry:7.12.0"
devfileRegistryImageTest = "quay.io/eclipse/che-devfile-registry:7.12.0"
pvcJobsImageTest = "registry.access.redhat.com/ubi8-minimal:8.1-409"
postgresImageTest = "centos/postgresql-96-centos7:9.6"
keycloakImageTest = "quay.io/eclipse/che-keycloak:7.12.0"
brokerMetadataTest = "quay.io/eclipse/che-plugin-metadata-broker:v3.1.2"
brokerArtifactsTest = "quay.io/eclipse/che-plugin-artifacts-broker:v3.1.2"
jwtProxyTest = "quay.io/eclipse/che-jwtproxy:fd94e60"
cheVersionTest = "7.12.0"
cheServerImageTest = "quay.io/eclipse/che-server:7.12.0"
pluginRegistryImageTest = "quay.io/eclipse/che-plugin-registry:7.12.0"
devfileRegistryImageTest = "quay.io/eclipse/che-devfile-registry:7.12.0"
cheTLSSecretsCeationJobImageTest = "quay.io/eclipse/che-tls-secrets-creation-job:7.12.0"
pvcJobsImageTest = "registry.access.redhat.com/ubi8-minimal:8.1-409"
postgresImageTest = "centos/postgresql-96-centos7:9.6"
keycloakImageTest = "quay.io/eclipse/che-keycloak:7.12.0"
brokerMetadataTest = "quay.io/eclipse/che-plugin-metadata-broker:v3.1.2"
brokerArtifactsTest = "quay.io/eclipse/che-plugin-artifacts-broker:v3.1.2"
jwtProxyTest = "quay.io/eclipse/che-jwtproxy:fd94e60"
)
func init() {
@ -37,6 +38,7 @@ func init() {
os.Setenv("IMAGE_default_che_server", cheServerImageTest)
os.Setenv("IMAGE_default_plugin_registry", pluginRegistryImageTest)
os.Setenv("IMAGE_default_devfile_registry", devfileRegistryImageTest)
os.Setenv("IMAGE_default_che_tls_secrets_creation_job", cheTLSSecretsCeationJobImageTest)
os.Setenv("IMAGE_default_pvc_jobs", pvcJobsImageTest)
os.Setenv("IMAGE_default_postgres", postgresImageTest)
os.Setenv("IMAGE_default_keycloak", keycloakImageTest)

174
pkg/deploy/job.go Normal file
View File

@ -0,0 +1,174 @@
//
// Copyright (c) 2020 Red Hat, Inc.
// This program and the accompanying materials are made
// available under the terms of the Eclipse Public License 2.0
// which is available at https://www.eclipse.org/legal/epl-2.0/
//
// SPDX-License-Identifier: EPL-2.0
//
// Contributors:
// Red Hat, Inc. - initial API and implementation
//
package deploy
import (
"context"
"fmt"
"reflect"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/eclipse/che-operator/pkg/util"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/sirupsen/logrus"
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
var (
jobDiffOpts = cmp.Options{
cmpopts.IgnoreFields(batchv1.Job{}, "TypeMeta", "ObjectMeta", "Status"),
cmpopts.IgnoreFields(batchv1.JobSpec{}, "Selector", "TTLSecondsAfterFinished"),
cmpopts.IgnoreFields(v1.PodTemplateSpec{}, "ObjectMeta"),
cmpopts.IgnoreFields(corev1.Container{}, "TerminationMessagePath", "TerminationMessagePolicy"),
cmpopts.IgnoreFields(corev1.PodSpec{}, "DNSPolicy", "SchedulerName", "SecurityContext"),
cmp.Comparer(func(x, y []corev1.EnvVar) bool {
xMap := make(map[string]string)
yMap := make(map[string]string)
for _, env := range x {
xMap[env.Name] = env.Value
}
for _, env := range y {
yMap[env.Name] = env.Value
}
return reflect.DeepEqual(xMap, yMap)
}),
}
)
func SyncJobToCluster(
checluster *orgv1.CheCluster,
name string,
component string,
image string,
serviceAccountName string,
env map[string]string,
clusterAPI ClusterAPI) (*batchv1.Job, error) {
specJob, err := getSpecJob(checluster, name, component, image, serviceAccountName, env, clusterAPI)
if err != nil {
return nil, err
}
clusterJob, err := getClusterJob(specJob.Name, specJob.Namespace, clusterAPI)
if err != nil {
return nil, err
}
if clusterJob == nil {
logrus.Infof("Creating a new object: %s, name %s", specJob.Kind, specJob.Name)
err := clusterAPI.Client.Create(context.TODO(), specJob)
return nil, err
}
diff := cmp.Diff(clusterJob, specJob, jobDiffOpts)
if len(diff) > 0 {
logrus.Infof("Updating existed object: %s, name: %s", clusterJob.Kind, clusterJob.Name)
fmt.Printf("Difference:\n%s", diff)
if err := clusterAPI.Client.Delete(context.TODO(), clusterJob); err != nil {
return nil, err
}
err := clusterAPI.Client.Create(context.TODO(), specJob)
return nil, err
}
return clusterJob, nil
}
// GetSpecJob creates new job configuration by given parameters.
func getSpecJob(
checluster *orgv1.CheCluster,
name string,
component string,
image string,
serviceAccountName string,
env map[string]string,
clusterAPI ClusterAPI) (*batchv1.Job, error) {
labels := GetLabels(checluster, util.GetValue(checluster.Spec.Server.CheFlavor, DefaultCheFlavor))
labels["component"] = component
backoffLimit := int32(3)
parallelism := int32(1)
comletions := int32(1)
terminationGracePeriodSeconds := int64(30)
ttlSecondsAfterFinished := int32(30)
pullPolicy := corev1.PullPolicy(util.GetValue(string(checluster.Spec.Server.CheImagePullPolicy), "IfNotPresent"))
var jobEnvVars []corev1.EnvVar
for envVarName, envVarValue := range env {
jobEnvVars = append(jobEnvVars, corev1.EnvVar{Name: envVarName, Value: envVarValue})
}
job := &batchv1.Job{
TypeMeta: metav1.TypeMeta{
Kind: "Job",
APIVersion: batchv1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: checluster.Namespace,
Labels: labels,
},
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
ServiceAccountName: serviceAccountName,
DeprecatedServiceAccount: serviceAccountName,
RestartPolicy: "Never",
TerminationGracePeriodSeconds: &terminationGracePeriodSeconds,
Containers: []corev1.Container{
{
Name: name + "-job-container",
Image: image,
ImagePullPolicy: pullPolicy,
Env: jobEnvVars,
},
},
},
},
TTLSecondsAfterFinished: &ttlSecondsAfterFinished,
Parallelism: &parallelism,
BackoffLimit: &backoffLimit,
Completions: &comletions,
},
}
if err := controllerutil.SetControllerReference(checluster, job, clusterAPI.Scheme); err != nil {
return nil, err
}
return job, nil
}
// GetClusterJob gets and returns specified job
func getClusterJob(name string, namespace string, clusterAPI ClusterAPI) (*batchv1.Job, error) {
job := &batchv1.Job{}
err := clusterAPI.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: namespace}, job)
if err != nil {
if errors.IsNotFound(err) {
return nil, nil
}
return nil, err
}
return job, nil
}

View File

@ -12,22 +12,71 @@
package deploy
import (
"context"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/eclipse/che-operator/pkg/util"
"github.com/sirupsen/logrus"
rbac "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
runtimeClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
func NewRole(cr *orgv1.CheCluster, name string, resources []string, verbs []string) *rbac.Role {
labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor))
return &rbac.Role{
func SyncRoleToCluster(
checluster *orgv1.CheCluster,
name string,
resources []string,
verbs []string,
clusterAPI ClusterAPI) (*rbac.Role, error) {
specRole, err := getSpecRole(checluster, name, resources, verbs, clusterAPI)
if err != nil {
return nil, err
}
clusterRole, err := getClusterRole(specRole.Name, specRole.Namespace, clusterAPI.Client)
if err != nil {
return nil, err
}
if clusterRole == nil {
logrus.Infof("Creating a new object: %s, name %s", specRole.Kind, specRole.Name)
err := clusterAPI.Client.Create(context.TODO(), specRole)
return nil, err
}
return clusterRole, nil
}
func getClusterRole(name string, namespace string, client runtimeClient.Client) (*rbac.Role, error) {
role := &rbac.Role{}
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err := client.Get(context.TODO(), namespacedName, role)
if err != nil {
if errors.IsNotFound(err) {
return nil, nil
}
return nil, err
}
return role, nil
}
func getSpecRole(checluster *orgv1.CheCluster, name string, resources []string, verbs []string, clusterAPI ClusterAPI) (*rbac.Role, error) {
labels := GetLabels(checluster, util.GetValue(checluster.Spec.Server.CheFlavor, DefaultCheFlavor))
role := &rbac.Role{
TypeMeta: metav1.TypeMeta{
Kind: "Role",
APIVersion: rbac.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: cr.Namespace,
Namespace: checluster.Namespace,
Labels: labels,
},
Rules: []rbac.PolicyRule{
@ -40,7 +89,11 @@ func NewRole(cr *orgv1.CheCluster, name string, resources []string, verbs []stri
},
},
}
err := controllerutil.SetControllerReference(checluster, role, clusterAPI.Scheme)
if err != nil {
return nil, err
}
return role, nil
}

View File

@ -12,29 +12,86 @@
package deploy
import (
"context"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/eclipse/che-operator/pkg/util"
"github.com/sirupsen/logrus"
rbac "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
runtimeClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
func NewRoleBinding(cr *orgv1.CheCluster, name string, serviceAccountName string, roleName string, roleKind string) *rbac.RoleBinding {
labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor))
return &rbac.RoleBinding{
func SyncRoleBindingToCluster(
checluster *orgv1.CheCluster,
name string,
serviceAccountName string,
roleName string,
roleKind string,
clusterAPI ClusterAPI) (*rbac.RoleBinding, error) {
specRB, err := getSpecRoleBinding(checluster, name, serviceAccountName, roleName, roleKind, clusterAPI)
if err != nil {
return nil, err
}
clusterRB, err := getClusterRoleBiding(specRB.Name, specRB.Namespace, clusterAPI.Client)
if err != nil {
return nil, err
}
if clusterRB == nil {
logrus.Infof("Creating a new object: %s, name %s", specRB.Kind, specRB.Name)
err := clusterAPI.Client.Create(context.TODO(), specRB)
return nil, err
}
return clusterRB, nil
}
func getClusterRoleBiding(name string, namespace string, client runtimeClient.Client) (*rbac.RoleBinding, error) {
roleBinding := &rbac.RoleBinding{}
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err := client.Get(context.TODO(), namespacedName, roleBinding)
if err != nil {
if errors.IsNotFound(err) {
return nil, nil
}
return nil, err
}
return roleBinding, nil
}
func getSpecRoleBinding(
checluster *orgv1.CheCluster,
name string,
serviceAccountName string,
roleName string,
roleKind string,
clusterAPI ClusterAPI) (*rbac.RoleBinding, error) {
labels := GetLabels(checluster, util.GetValue(checluster.Spec.Server.CheFlavor, DefaultCheFlavor))
roleBinding := &rbac.RoleBinding{
TypeMeta: metav1.TypeMeta{
Kind: "RoleBinding",
APIVersion: rbac.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: cr.Namespace,
Namespace: checluster.Namespace,
Labels: labels,
},
Subjects: []rbac.Subject{
{
Kind: rbac.ServiceAccountKind,
Name: serviceAccountName,
Namespace: cr.Namespace,
Namespace: checluster.Namespace,
},
},
RoleRef: rbac.RoleRef{
@ -43,5 +100,11 @@ func NewRoleBinding(cr *orgv1.CheCluster, name string, serviceAccountName string
APIGroup: "rbac.authorization.k8s.io",
},
}
}
err := controllerutil.SetControllerReference(checluster, roleBinding, clusterAPI.Scheme)
if err != nil {
return nil, err
}
return roleBinding, nil
}

View File

@ -12,23 +12,73 @@
package deploy
import (
"context"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/eclipse/che-operator/pkg/util"
"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/types"
runtimeClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)
func NewServiceAccount(cr *orgv1.CheCluster, name string) *corev1.ServiceAccount {
labels := GetLabels(cr, util.GetValue(cr.Spec.Server.CheFlavor, DefaultCheFlavor))
return &corev1.ServiceAccount{
func SyncServiceAccountToCluster(checluster *orgv1.CheCluster, name string, clusterAPI ClusterAPI) (*corev1.ServiceAccount, error) {
specSA, err := getSpecServiceAccount(checluster, name, clusterAPI)
if err != nil {
return nil, err
}
clusterSA, err := getClusterServiceAccount(specSA.Name, specSA.Namespace, clusterAPI.Client)
if err != nil {
return nil, err
}
if clusterSA == nil {
logrus.Infof("Creating a new object: %s, name %s", specSA.Kind, specSA.Name)
err := clusterAPI.Client.Create(context.TODO(), specSA)
return nil, err
}
return clusterSA, nil
}
func getClusterServiceAccount(name string, namespace string, client runtimeClient.Client) (*corev1.ServiceAccount, error) {
serviceAccount := &corev1.ServiceAccount{}
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: name,
}
err := client.Get(context.TODO(), namespacedName, serviceAccount)
if err != nil {
if errors.IsNotFound(err) {
return nil, nil
}
return nil, err
}
return serviceAccount, nil
}
func getSpecServiceAccount(checluster *orgv1.CheCluster, name string, clusterAPI ClusterAPI) (*corev1.ServiceAccount, error) {
labels := GetLabels(checluster, util.GetValue(checluster.Spec.Server.CheFlavor, DefaultCheFlavor))
serviceAccount := &corev1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceAccount",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: cr.Namespace,
Namespace: checluster.Namespace,
Labels: labels,
},
}
err := controllerutil.SetControllerReference(checluster, serviceAccount, clusterAPI.Scheme)
if err != nil {
return nil, err
}
return serviceAccount, nil
}

30
pkg/deploy/update.go Normal file
View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2012-2019 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"
orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1"
"github.com/sirupsen/logrus"
)
func UpdateCheCRSpec(instance *orgv1.CheCluster, updatedField string, value string, clusterAPI ClusterAPI) (err error) {
logrus.Infof("Updating %s CR with %s: %s", instance.Name, updatedField, value)
err = clusterAPI.Client.Update(context.TODO(), instance)
if err != nil {
logrus.Errorf("Failed to update %s CR: %s", instance.Name, err)
return err
}
logrus.Infof("Custom resource %s updated", instance.Name)
return nil
}

View File

@ -109,7 +109,7 @@ func (cl *k8s) GetDeploymentPod(name string, ns string) (podName string, err err
podList, _ := api.Pods(ns).List(listOptions)
podListItems := podList.Items
if len(podListItems) == 0 {
logrus.Errorf("Failed to find pod to exec into. List of pods: %v", podListItems)
logrus.Errorf("Failed to find pod for component %s. List of pods: %v", name, podListItems)
return "", err
}
// expecting only one pod to be there so, taking the first one
@ -118,6 +118,20 @@ func (cl *k8s) GetDeploymentPod(name string, ns string) (podName string, err err
return podName, nil
}
func (cl *k8s) GetPodsByComponent(name string, ns string) []string {
names := []string{}
api := cl.clientset.CoreV1()
listOptions := metav1.ListOptions{
LabelSelector: "component=" + name,
}
podList, _ := api.Pods(ns).List(listOptions)
for _, pod := range podList.Items {
names = append(names, pod.Name)
}
return names
}
// Reads 'user' and 'password' from the given secret
func (cl *k8s) ReadSecret(name string, ns string) (user string, password string, err error) {
secret, err := cl.clientset.CoreV1().Secrets(ns).Get(name, metav1.GetOptions{})