diff --git a/deploy/crds/org_v1_che_cr.yaml b/deploy/crds/org_v1_che_cr.yaml index 629d9a5b5..d4348114b 100644 --- a/deploy/crds/org_v1_che_cr.yaml +++ b/deploy/crds/org_v1_che_cr.yaml @@ -65,7 +65,12 @@ spec: # instruct Che server to launch a special pod to precreate a subpath in a PV preCreateSubPaths: true # image:tag for preCreateSubPaths jobs - pvcJobsImage: + pvcJobsImage: '' + # keep blank unless you need to use a non default storage class for Postgres PVC + postgresPVCStorageClassName: '' + # keep blank unless you need to use a non default storage class for workspace PVC(s) + workspacePVCStorageClassName: '' + auth: # instructs operator on whether or not to deploy Keycloak/RH SSO instance. When set to true provision connection details externalKeycloak: diff --git a/pkg/apis/org/v1/che_types.go b/pkg/apis/org/v1/che_types.go index 47071c127..7c00127da 100644 --- a/pkg/apis/org/v1/che_types.go +++ b/pkg/apis/org/v1/che_types.go @@ -120,6 +120,10 @@ type CheClusterSpecStorage struct { PreCreateSubPaths bool `json:"preCreateSubPaths"` // PvcJobsImage is image:tag for preCreateSubPaths jobs PvcJobsImage string `json:"pvcJobsImage"` + // PostgresPVCStorageClassName is storage class for a postgres pvc. Empty string by default, which means default storage class is used + PostgresPVCStorageClassName string `json:"postgresPVCStorageClassName"` + // WorkspacePVCStorageClassName is storage class for a workspaces pvc. Empty string by default, which means default storage class is used + WorkspacePVCStorageClassName string `json:"workspacePVCStorageClassName"` } type CheClusterSpecK8SOnly struct { diff --git a/pkg/apis/org/v1/zz_generated.deepcopy.go b/pkg/apis/org/v1/zz_generated.deepcopy.go index ab783afcf..e1922b1ab 100644 --- a/pkg/apis/org/v1/zz_generated.deepcopy.go +++ b/pkg/apis/org/v1/zz_generated.deepcopy.go @@ -1,14 +1,3 @@ -// -// 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 -// // +build !ignore_autogenerated /* diff --git a/pkg/controller/che/che_controller.go b/pkg/controller/che/che_controller.go index 6414c94e5..19374e03c 100644 --- a/pkg/controller/che/che_controller.go +++ b/pkg/controller/che/che_controller.go @@ -173,6 +173,7 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { } var _ reconcile.Reconciler = &ReconcileChe{} +var oAuthFinalizerName = "oauthclients.finalizers.che.eclipse.org" // ReconcileChe reconciles a CheCluster object type ReconcileChe struct { @@ -207,6 +208,15 @@ func (r *ReconcileChe) Reconcile(request reconcile.Request) (reconcile.Result, e if err != nil { logrus.Errorf("An error occurred when detecting current infra: %s", err) } + + // delete oAuthClient before CR is deleted + doInstallOpenShiftoAuthProvider := instance.Spec.Auth.OpenShiftOauth + if doInstallOpenShiftoAuthProvider { + if err := r.ReconcileFinalizer(instance); err != nil { + return reconcile.Result{}, err + } + } + // create a secret with router tls cert if isOpenShift { secret := &corev1.Secret{} diff --git a/pkg/controller/che/che_controller_test.go b/pkg/controller/che/che_controller_test.go index ab7e98c13..4a61be107 100644 --- a/pkg/controller/che/che_controller_test.go +++ b/pkg/controller/che/che_controller_test.go @@ -16,6 +16,7 @@ import ( orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" oauth "github.com/openshift/api/oauth/v1" routev1 "github.com/openshift/api/route/v1" + "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,6 +26,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" + "time" "testing" ) @@ -180,4 +182,49 @@ func TestCheController(t *testing.T) { if err == nil { t.Fatalf("Deployment postgres shoud not exist") } + + // check of storageClassName ends up in pvc spec + fakeStorageClassName := "fake-storage-class-name" + cheCR.Spec.Storage.PostgresPVCStorageClassName = fakeStorageClassName + cheCR.Spec.Database.ExternalDB = false + if err := r.client.Update(context.TODO(), cheCR); err != nil { + t.Fatalf("Failed to update %s CR: %s", cheCR.Name, err) + } + pvc := &corev1.PersistentVolumeClaim{} + if err = r.client.Get(context.TODO(), types.NamespacedName{Name: "postgres-data", Namespace: cheCR.Namespace}, pvc); err != nil { + t.Fatalf("Failed to get PVC: %s", err) + } + if err = r.client.Delete(context.TODO(), pvc); err != nil { + t.Fatalf("Failed to delete PVC %s: %s", pvc.Name, err) + } + res, err = r.Reconcile(req) + if err != nil { + t.Fatalf("reconcile: (%v)", err) + } + pvc = &corev1.PersistentVolumeClaim{} + if err = r.client.Get(context.TODO(), types.NamespacedName{Name: "postgres-data", Namespace: cheCR.Namespace}, pvc); err != nil { + t.Fatalf("Failed to get PVC: %s", err) + } + actualStorageClassName := pvc.Spec.StorageClassName + if len(*actualStorageClassName) != len(fakeStorageClassName) { + t.Fatalf("Expecting %s storageClassName, got %s", fakeStorageClassName, *actualStorageClassName ) + } + + // check if oAuthClient is deleted after CR is deleted (finalizer logic) + // since fake api does not set deletion timestamp, CR is updated in tests rather than deleted + logrus.Info("Updating CR with deletion timestamp") + deletionTimestamp := &metav1.Time{Time: time.Now()} + cheCR.DeletionTimestamp = deletionTimestamp + if err := r.client.Update(context.TODO(), cheCR); err != nil { + t.Fatalf("Failed to update CR: %s", err) + } + if err := r.ReconcileFinalizer(cheCR); err != nil { + t.Fatal("Failed to reconcile oAuthClient") + } + oauthClientName := cheCR.Spec.Auth.OauthClientName + _, err = r.GetOAuthClient(oauthClientName) + if err == nil { + t.Fatalf("OauthClient %s has not been deleted", oauthClientName) + } + logrus.Infof("Disregard the error above. OauthClient %s has been deleted", oauthClientName) } diff --git a/pkg/controller/che/finalizer.go b/pkg/controller/che/finalizer.go new file mode 100644 index 000000000..368eabd6e --- /dev/null +++ b/pkg/controller/che/finalizer.go @@ -0,0 +1,42 @@ +package che + +import ( + "context" + orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + "github.com/eclipse/che-operator/pkg/util" + "github.com/sirupsen/logrus" +) + +func (r *ReconcileChe) ReconcileFinalizer(instance *orgv1.CheCluster) (err error) { + if instance.ObjectMeta.DeletionTimestamp.IsZero() { + if !util.ContainsString(instance.ObjectMeta.Finalizers, oAuthFinalizerName) { + instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, oAuthFinalizerName) + if err := r.client.Update(context.Background(), instance); err != nil { + return err + } + } + } else { + if util.ContainsString(instance.ObjectMeta.Finalizers, oAuthFinalizerName) { + oAuthClientName := instance.Spec.Auth.OauthClientName + logrus.Infof("Custom resource %s is being deleted. Deleting oAuthClient %s first", instance.Name, oAuthClientName) + oAuthClient, err := r.GetOAuthClient(oAuthClientName) + if err != nil { + logrus.Errorf("Failed to get %s oAuthClient: %s", oAuthClientName, err) + return err + } + if err := r.client.Delete(context.TODO(), oAuthClient); err != nil { + logrus.Errorf("Failed to delete %s oAuthClient: %s", oAuthClientName, err) + return err + } + instance.ObjectMeta.Finalizers = util.DoRemoveString(instance.ObjectMeta.Finalizers, oAuthFinalizerName) + logrus.Infof("Updating %s CR", instance.Name) + + if err := r.client.Update(context.Background(), instance); err != nil { + logrus.Errorf("Failed to update %s CR: %s", instance.Name, err) + return err + } + } + return nil + } + return nil +} diff --git a/pkg/controller/che/get.go b/pkg/controller/che/get.go index 43651d23e..31aaedc54 100644 --- a/pkg/controller/che/get.go +++ b/pkg/controller/che/get.go @@ -14,16 +14,17 @@ package che import ( "context" orgv1 "github.com/eclipse/che-operator/pkg/apis/org/v1" + oauth "github.com/openshift/api/oauth/v1" + routev1 "github.com/openshift/api/route/v1" "github.com/sirupsen/logrus" appsv1 "k8s.io/api/apps/v1" - routev1 "github.com/openshift/api/route/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/api/extensions/v1beta1" "k8s.io/apimachinery/pkg/types" - corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) -func(r *ReconcileChe) GetEffectiveDeployment(instance *orgv1.CheCluster, name string) (deployment *appsv1.Deployment, err error) { +func (r *ReconcileChe) GetEffectiveDeployment(instance *orgv1.CheCluster, name string) (deployment *appsv1.Deployment, err error) { deployment = &appsv1.Deployment{} err = r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, deployment) if err != nil { @@ -33,8 +34,7 @@ func(r *ReconcileChe) GetEffectiveDeployment(instance *orgv1.CheCluster, name st return deployment, nil } - -func(r *ReconcileChe) GetEffectiveIngress(instance *orgv1.CheCluster, name string) (ingress *v1beta1.Ingress) { +func (r *ReconcileChe) GetEffectiveIngress(instance *orgv1.CheCluster, name string) (ingress *v1beta1.Ingress) { ingress = &v1beta1.Ingress{} err := r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, ingress) if err != nil { @@ -44,9 +44,7 @@ func(r *ReconcileChe) GetEffectiveIngress(instance *orgv1.CheCluster, name strin return ingress } - - -func(r *ReconcileChe) GetEffectiveRoute(instance *orgv1.CheCluster, name string) (route *routev1.Route) { +func (r *ReconcileChe) GetEffectiveRoute(instance *orgv1.CheCluster, name string) (route *routev1.Route) { route = &routev1.Route{} err := r.client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: instance.Namespace}, route) if err != nil { @@ -75,4 +73,13 @@ func (r *ReconcileChe) GetCR(request reconcile.Request) (instance *orgv1.CheClus return nil, err } return instance, nil -} \ No newline at end of file +} + +func (r *ReconcileChe) GetOAuthClient(oAuthClientName string) (oAuthClient *oauth.OAuthClient, err error) { + oAuthClient = &oauth.OAuthClient{} + if err := r.client.Get(context.TODO(), types.NamespacedName{Name: oAuthClientName, Namespace: ""}, oAuthClient); err != nil { + logrus.Errorf("Failed to Get oAuthClient %s: %s", oAuthClientName, err) + return nil, err + } + return oAuthClient, nil +} diff --git a/pkg/deploy/che_configmap.go b/pkg/deploy/che_configmap.go index 19386c7ea..9abcfec45 100644 --- a/pkg/deploy/che_configmap.go +++ b/pkg/deploy/che_configmap.go @@ -40,6 +40,7 @@ type CheConfigMap struct { PvcStrategy string `json:"CHE_INFRA_KUBERNETES_PVC_STRATEGY"` PvcClaimSize string `json:"CHE_INFRA_KUBERNETES_PVC_QUANTITY"` PvcJobsImage string `json:"CHE_INFRA_KUBERNETES_PVC_JOBS_IMAGE"` + WorkspacePvcStorageClassName string `json:"CHE_INFRA_KUBERNETES_PVC_STORAGE__CLASS__NAME"` PreCreateSubPaths string `json:"CHE_INFRA_KUBERNETES_PVC_PRECREATE__SUBPATHS"` TlsSupport string `json:"CHE_INFRA_OPENSHIFT_TLS__ENABLED"` K8STrustCerts string `json:"CHE_INFRA_KUBERNETES_TRUST__CERTS"` @@ -62,7 +63,7 @@ type CheConfigMap struct { WebSocketEndpointMinor string `json:"CHE_WEBSOCKET_ENDPOINT__MINOR"` } -func GetCustomConfigMapData()(cheEnv map[string]string) { +func GetCustomConfigMapData() (cheEnv map[string]string) { cheEnv = map[string]string{ "CHE_PREDEFINED_STACKS_RELOAD__ON__START": "true", @@ -122,6 +123,7 @@ func GetConfigMapData(cr *orgv1.CheCluster) (cheEnv map[string]string) { tlsSecretName := cr.Spec.K8SOnly.TlsSecretName pvcStrategy := util.GetValue(cr.Spec.Storage.PvcStrategy, DefaultPvcStrategy) pvcClaimSize := util.GetValue(cr.Spec.Storage.PvcClaimSize, DefaultPvcClaimSize) + workspacePvcStorageClassName := cr.Spec.Storage.WorkspacePVCStorageClassName pvcJobsImage := util.GetValue(cr.Spec.Storage.PvcJobsImage, DefaultPvcJobsImage) preCreateSubPaths := "true" if !cr.Spec.Storage.PreCreateSubPaths { @@ -152,6 +154,7 @@ func GetConfigMapData(cr *orgv1.CheCluster) (cheEnv map[string]string) { WorkspacesNamespace: workspacesNamespace, PvcStrategy: pvcStrategy, PvcClaimSize: pvcClaimSize, + WorkspacePvcStorageClassName: workspacePvcStorageClassName, PvcJobsImage: pvcJobsImage, PreCreateSubPaths: preCreateSubPaths, TlsSupport: tls, diff --git a/pkg/deploy/pvc.go b/pkg/deploy/pvc.go index d977acd50..fb77b0e59 100644 --- a/pkg/deploy/pvc.go +++ b/pkg/deploy/pvc.go @@ -19,7 +19,26 @@ import ( ) func NewPvc(cr *orgv1.CheCluster, name string, pvcClaimSize string, labels map[string]string) *corev1.PersistentVolumeClaim { - //value := true + + accessModes := []corev1.PersistentVolumeAccessMode{ + // todo Make configurable + corev1.ReadWriteOnce, + } + resources := corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceName(corev1.ResourceStorage): resource.MustParse(pvcClaimSize), + }} + pvcSpec := corev1.PersistentVolumeClaimSpec{ + AccessModes: accessModes, + Resources: resources, + } + if len(cr.Spec.Storage.PostgresPVCStorageClassName) > 1 { + pvcSpec = corev1.PersistentVolumeClaimSpec{ + AccessModes: accessModes, + StorageClassName: &cr.Spec.Storage.PostgresPVCStorageClassName, + Resources: resources, + } + } return &corev1.PersistentVolumeClaim{ TypeMeta: metav1.TypeMeta{ Kind: "PersistentVolumeClaim", @@ -30,18 +49,7 @@ func NewPvc(cr *orgv1.CheCluster, name string, pvcClaimSize string, labels map[s Namespace: cr.Namespace, Labels: labels, }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - // todo Make configurable - corev1.ReadWriteOnce, - }, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceName(corev1.ResourceStorage): resource.MustParse(pvcClaimSize), - }, - }, - }, + Spec: pvcSpec, } } - diff --git a/pkg/util/util.go b/pkg/util/util.go index 47638b27e..6c3ee953f 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -25,6 +25,26 @@ import ( "time" ) + +func ContainsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +func DoRemoveString(slice []string, s string) (result []string) { + for _, item := range slice { + if item == s { + continue + } + result = append(result, item) + } + return +} + func GeneratePasswd(stringLength int) (passwd string) { rand.Seed(time.Now().UnixNano()) chars := []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ" +