diff --git a/controllers/drupalsite_controller_test.go b/controllers/drupalsite_controller_test.go index 8028d3818a52ca15ee49347f060cf7ee4c660937..1844dee23704f90f8198ebff7e419183cc34d7d1 100644 --- a/controllers/drupalsite_controller_test.go +++ b/controllers/drupalsite_controller_test.go @@ -240,18 +240,204 @@ var _ = Describe("DrupalSite controller", func() { // Check if the Schedule resource is created By("Expecting Schedule to be created") + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: generateScheduleName(key.Namespace, key.Name), Namespace: veleroNamespace}, &schedule) + }, timeout, interval).Should(Succeed()) + + // Check Routes + By("Expecting Drupal Route(s) to be created") + for _, url := range cr.Spec.SiteURL { + route = routev1.Route{} + hash := md5.Sum([]byte(url)) + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: key.Namespace}, &route) + return route.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + } + + // Check OidcReturnUris + By("Expecting OidcReturnURIs created") + for _, url := range cr.Spec.SiteURL { + oidcReturnUri = authz.OidcReturnURI{} + hash := md5.Sum([]byte(url)) + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: key.Namespace}, &oidcReturnUri) + return oidcReturnUri.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + } + }) + }) + }) + + Describe("Cloning a drupalSite object", func() { + Context("With cloneFrom", func() { + It("All dependent resources should be created", func() { + keyClone := types.NamespacedName{ + Name: Name + "-clone", + Namespace: Namespace, + } + drupalSiteObjectClone := &drupalwebservicesv1alpha1.DrupalSite{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: keyClone.Name, + Namespace: keyClone.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalSiteSpec{ + Version: drupalwebservicesv1alpha1.Version{ + Name: "v9.3-2", + ReleaseSpec: "stable", + }, + Configuration: drupalwebservicesv1alpha1.Configuration{ + DiskSize: "6Gi", + QoSClass: drupalwebservicesv1alpha1.QoSStandard, + DatabaseClass: drupalwebservicesv1alpha1.DBODStandard, + CloneFrom: drupalwebservicesv1alpha1.CloneFrom(key.Name), + ScheduledBackups: "enabled", + }, + SiteURL: []drupalwebservicesv1alpha1.Url{dummySiteUrl}, + }, + } + + By("By creating a new drupalSite") + Eventually(func() error { + return k8sClient.Create(ctx, drupalSiteObjectClone) + }, timeout, interval).Should(Succeed()) + + // Create drupalSite object + By("Expecting drupalSite object created") + cr := drupalwebservicesv1alpha1.DrupalSite{} + Eventually(func() error { + return k8sClient.Get(ctx, keyClone, &cr) + }, timeout, interval).Should(Succeed()) + + trueVar := true + expectedOwnerReference := metav1.OwnerReference{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + Name: keyClone.Name, + UID: cr.UID, + Controller: &trueVar, + } + configmap := corev1.ConfigMap{} + svc := corev1.Service{} + pvc := corev1.PersistentVolumeClaim{} + job := batchv1.Job{} + deploy := appsv1.Deployment{} + dbod := dbodv1a1.Database{} + route := routev1.Route{} + oidcReturnUri := authz.OidcReturnURI{} + schedule := velerov1.Schedule{} + + // Check DBOD resource creation + By("Expecting Database resource created") Eventually(func() []metav1.OwnerReference { - k8sClient.Get(ctx, types.NamespacedName{Name: key.Namespace + "-" + key.Name, Namespace: veleroNamespace}, &schedule) + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &dbod) + return dbod.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // Update DBOD resource status field + By("Updating the DBOD instance in Database resource status") + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &dbod) + dbod.Status.DbodInstance = "test" + return k8sClient.Status().Update(ctx, &dbod) + }, timeout, interval).Should(Succeed()) + + By("Expecting the drupal deployment to have at least 2 containers") + Eventually(func() bool { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &deploy) + return len(deploy.Spec.Template.Spec.Containers) >= 2 + }, timeout, interval).Should(BeTrue()) + + // Check PHP-FPM configMap creation + By("Expecting PHP_FPM configmaps created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: "php-fpm-" + keyClone.Name, Namespace: keyClone.Namespace}, &configmap) + return configmap.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // Check Nginx configMap creation + By("Expecting Nginx configmaps created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: "nginx-" + keyClone.Name, Namespace: keyClone.Namespace}, &configmap) + return configmap.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // Check Site settings configMap creation + By("Expecting Site settings configmaps created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: "site-settings-" + keyClone.Name, Namespace: keyClone.Namespace}, &configmap) + return configmap.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // Check PHP Cli configMap creation + By("Expecting PHP Cli configmaps created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: "php-cli-config-" + keyClone.Name, Namespace: keyClone.Namespace}, &configmap) + return configmap.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + // Check Drupal service + By("Expecting Drupal service created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &svc) + return svc.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // Check drupal persistentVolumeClaim + By("Expecting drupal persistentVolumeClaim created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: "pv-claim-" + keyClone.Name, Namespace: keyClone.Namespace}, &pvc) + return pvc.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // Check Drupal deployments + By("Expecting Drupal deployments created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &deploy) + return deploy.ObjectMeta.OwnerReferences + }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + + // // Check Drush job + By("Expecting Drush job created") + Eventually(func() []metav1.OwnerReference { + k8sClient.Get(ctx, types.NamespacedName{Name: "clone-" + keyClone.Name, Namespace: keyClone.Namespace}, &job) return job.ObjectMeta.OwnerReferences }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + // Update drupalSite custom resource status fields to allow route conditions + By("Updating 'initialized' status field in drupalSite resource") + Eventually(func() error { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &cr) + cr.Status.Conditions.SetCondition(status.Condition{Type: "Initialized", Status: "True"}) + return k8sClient.Status().Update(ctx, &cr) + }, timeout, interval).Should(Succeed()) + + // Update deployment status fields to allow 'ready' status field to be set on the drupalSite resource + By("Updating 'ReadyReplicas' and 'AvailableReplicas' status fields in deployment resource") + Eventually(func() error { + k8sClient.Get(ctx, keyClone, &deploy) + deploy.Status.Replicas = 1 + deploy.Status.AvailableReplicas = 1 + deploy.Status.ReadyReplicas = 1 + return k8sClient.Status().Update(ctx, &deploy) + }, timeout, interval).Should(Succeed()) + + // Check if the Schedule resource is created + By("Expecting Schedule to be created") + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: generateScheduleName(keyClone.Namespace, keyClone.Name), Namespace: veleroNamespace}, &schedule) + }, timeout, interval).Should(Succeed()) + // Check Routes By("Expecting Drupal Route(s) to be created") for _, url := range cr.Spec.SiteURL { route = routev1.Route{} hash := md5.Sum([]byte(url)) Eventually(func() []metav1.OwnerReference { - k8sClient.Get(ctx, types.NamespacedName{Name: key.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: key.Namespace}, &route) + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: keyClone.Namespace}, &route) return route.ObjectMeta.OwnerReferences }, timeout, interval).Should(ContainElement(expectedOwnerReference)) } @@ -262,10 +448,33 @@ var _ = Describe("DrupalSite controller", func() { oidcReturnUri = authz.OidcReturnURI{} hash := md5.Sum([]byte(url)) Eventually(func() []metav1.OwnerReference { - k8sClient.Get(ctx, types.NamespacedName{Name: key.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: key.Namespace}, &oidcReturnUri) + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: keyClone.Namespace}, &oidcReturnUri) return oidcReturnUri.ObjectMeta.OwnerReferences }, timeout, interval).Should(ContainElement(expectedOwnerReference)) } + + // Check if the clone labels are set on the drupalSite + By("Expecting the clone labels to be set on the cloned site") + Eventually(func() bool { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &cr) + return (cr.Labels["clonedFrom"] == key.Name && cr.Labels["clonedOn"] != "") + }, timeout, interval).Should(BeTrue()) + + // Check if the cloneFrom field is reset on the drupalSite + By("Expecting the cloneFrom field is reset on the cloned site") + Eventually(func() bool { + k8sClient.Get(ctx, types.NamespacedName{Name: keyClone.Name, Namespace: keyClone.Namespace}, &cr) + return (cr.Spec.Configuration.CloneFrom == "") + }, timeout, interval).Should(BeTrue()) + + // Check if the diskSize is set correctly + By("Expecting the clone site diskSize is set to the same as source site") + Eventually(func() bool { + cr_source := drupalwebservicesv1alpha1.DrupalSite{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: key.Namespace}, &cr_source) + return (cr.Spec.Configuration.DiskSize == cr_source.Spec.Configuration.DiskSize) + }, timeout, interval).Should(BeTrue()) + }) }) }) @@ -909,10 +1118,9 @@ var _ = Describe("DrupalSite controller", func() { // Check if the Schedule resource is created By("Expecting Schedule to be created") - Eventually(func() []metav1.OwnerReference { - k8sClient.Get(ctx, types.NamespacedName{Name: key.Namespace + "-" + key.Name, Namespace: veleroNamespace}, &schedule) - return job.ObjectMeta.OwnerReferences - }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: generateScheduleName(key.Namespace, key.Name), Namespace: veleroNamespace}, &schedule) + }, timeout, interval).Should(Succeed()) // Check Routes By("Expecting Drupal Route(s) to be created") @@ -1633,10 +1841,9 @@ var _ = Describe("DrupalSite controller", func() { // Check if the Schedule resource is created By("Expecting Schedule to be created") - Eventually(func() []metav1.OwnerReference { - k8sClient.Get(ctx, types.NamespacedName{Name: key.Namespace + "-" + key.Name, Namespace: veleroNamespace}, &schedule) - return job.ObjectMeta.OwnerReferences - }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: generateScheduleName(key.Namespace, key.Name), Namespace: veleroNamespace}, &schedule) + }, timeout, interval).Should(Succeed()) // Check Routes By("Expecting Drupal Route(s) to be created") @@ -1786,10 +1993,9 @@ var _ = Describe("DrupalSite controller", func() { // Check if the Schedule resource is created By("Expecting Schedule to be created") - Eventually(func() []metav1.OwnerReference { - k8sClient.Get(ctx, types.NamespacedName{Name: key.Namespace + "-" + key.Name, Namespace: veleroNamespace}, &schedule) - return job.ObjectMeta.OwnerReferences - }, timeout, interval).Should(ContainElement(expectedOwnerReference)) + Eventually(func() error { + return k8sClient.Get(ctx, types.NamespacedName{Name: generateScheduleName(key.Namespace, key.Name), Namespace: veleroNamespace}, &schedule) + }, timeout, interval).Should(Succeed()) // Check Routes By("Expecting Drupal Route(s) to be created") diff --git a/controllers/drupalsite_controller_utils.go b/controllers/drupalsite_controller_utils.go index cda16e20232b9db0d0d633fbc1378e1d057d0d65..4ad09501a5f6ce20e4eb861dae4b880adb2a50b3 100644 --- a/controllers/drupalsite_controller_utils.go +++ b/controllers/drupalsite_controller_utils.go @@ -20,6 +20,7 @@ import ( "crypto/md5" "encoding/hex" "fmt" + "time" "github.com/go-logr/logr" buildv1 "github.com/openshift/api/build/v1" @@ -170,29 +171,44 @@ func (r *DrupalSiteReconciler) ensureSpecFinalizer(ctx context.Context, drp *web // Validate that CloneFrom is an existing DrupalSite if drp.Spec.Configuration.CloneFrom != "" { - sourceSite := webservicesv1a1.DrupalSite{} - err := r.Get(ctx, types.NamespacedName{Name: string(drp.Spec.Configuration.CloneFrom), Namespace: drp.Namespace}, &sourceSite) - switch { - case k8sapierrors.IsNotFound(err): - return false, newApplicationError(fmt.Errorf("CloneFrom DrupalSite doesn't exist"), ErrInvalidSpec) - case err != nil: - return false, newApplicationError(err, ErrClientK8s) - } - // The destination disk size must be at least as large as the source - if drp.Spec.Configuration.DiskSize < sourceSite.Spec.Configuration.DiskSize { - drp.Spec.Configuration.DiskSize = sourceSite.Spec.Configuration.DiskSize - update = true - } - // The extraConfigurationRepo should be set in the clone site if defined in the source - // TODO: Remove logic for ExtraConfigurationRepo once we deprecate the field - if sourceSite.Spec.Configuration.ExtraConfigurationRepo != "" && drp.Spec.Configuration.ExtraConfigurationRepo == "" { - drp.Spec.Configuration.ExtraConfigurationRepo = sourceSite.Spec.Configuration.ExtraConfigurationRepo - update = true - } - // The extraConfigurationRepository should be set in the clone site if defined in the source - if sourceSite.Spec.Configuration.ExtraConfigurationRepository.Branch != "" && sourceSite.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl != "" && drp.Spec.Configuration.ExtraConfigurationRepository.Branch == "" && drp.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl != "" { - drp.Spec.Configuration.ExtraConfigurationRepository.Branch = sourceSite.Spec.Configuration.ExtraConfigurationRepository.Branch - drp.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl = sourceSite.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl + if !drp.ConditionTrue("Initialized") { + sourceSite := webservicesv1a1.DrupalSite{} + err := r.Get(ctx, types.NamespacedName{Name: string(drp.Spec.Configuration.CloneFrom), Namespace: drp.Namespace}, &sourceSite) + switch { + case k8sapierrors.IsNotFound(err): + return false, newApplicationError(fmt.Errorf("CloneFrom DrupalSite doesn't exist"), ErrInvalidSpec) + case err != nil: + return false, newApplicationError(err, ErrClientK8s) + } + + // The destination disk size must be at least as large as the source + if drp.Spec.Configuration.DiskSize != sourceSite.Spec.Configuration.DiskSize { + drp.Spec.Configuration.DiskSize = sourceSite.Spec.Configuration.DiskSize + update = true + } + // The extraConfigurationRepo should be set in the clone site if defined in the source + // TODO: Remove logic for ExtraConfigurationRepo once we deprecate the field + if sourceSite.Spec.Configuration.ExtraConfigurationRepo != "" && drp.Spec.Configuration.ExtraConfigurationRepo == "" { + drp.Spec.Configuration.ExtraConfigurationRepo = sourceSite.Spec.Configuration.ExtraConfigurationRepo + update = true + } + // The extraConfigurationRepository should be set in the clone site if defined in the source + if sourceSite.Spec.Configuration.ExtraConfigurationRepository.Branch != "" && sourceSite.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl != "" && drp.Spec.Configuration.ExtraConfigurationRepository.Branch == "" && drp.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl != "" { + drp.Spec.Configuration.ExtraConfigurationRepository.Branch = sourceSite.Spec.Configuration.ExtraConfigurationRepository.Branch + drp.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl = sourceSite.Spec.Configuration.ExtraConfigurationRepository.RepositoryUrl + update = true + } + } else { + if drp.Labels == nil { + drp.Labels = map[string]string{} + } + // Source site name from which the clone was requested + drp.Labels["clonedFrom"] = string(drp.Spec.Configuration.CloneFrom) + // Set the time when the clone was requested + loc, _ := time.LoadLocation("") + drp.Labels["clonedOn"] = time.Now().In(loc).Format("Jan_2_2006_3-04pm_UTC") + // Reset the `cloneFrom` field + drp.Spec.Configuration.CloneFrom = "" update = true } }