diff --git a/api/v1alpha1/drupalprojectconfig_types.go b/api/v1alpha1/drupalprojectconfig_types.go index db4cc702964b7dc37d6c20f9761840315186497b..17042fadc15e1cbc920233f7668c4f199a5be21c 100644 --- a/api/v1alpha1/drupalprojectconfig_types.go +++ b/api/v1alpha1/drupalprojectconfig_types.go @@ -25,6 +25,9 @@ type DrupalProjectConfigSpec struct { // PrimarySiteName defines the primary DrupalSite instance of a project // +optional PrimarySiteName string `json:"primarySiteName,omitempty"` + // PrimarySiteUrl defines the primary urls of a project + // +optional + PrimarySiteUrl []Url `json:"primarySiteUrl,omitempty"` } // DrupalProjectConfigStatus defines the observed state of DrupalProjectConfig diff --git a/api/v1alpha1/drupalsite_types.go b/api/v1alpha1/drupalsite_types.go index 9eb69e8be9493c432cd1555364a9983ed63058a0..66f79af511dc8481d4a5a0b0b3b4050f68faa4ea 100644 --- a/api/v1alpha1/drupalsite_types.go +++ b/api/v1alpha1/drupalsite_types.go @@ -159,6 +159,10 @@ type DrupalSiteStatus struct { // IsPrimary states if the Drupalsite is the main instance of the project // +kubebuilder:default=false IsPrimary bool `json:"isPrimary,omitempty"` + + // SiteUrl defines the urls of a site + // +optional + SiteUrl []Url `json:"siteUrl,omitempty"` } // ReleaseID reports the actual release of CERN Drupal Distribution that is being used in the deployment. diff --git a/config/crd/bases/drupal.webservices.cern.ch_drupalprojectconfigs.yaml b/config/crd/bases/drupal.webservices.cern.ch_drupalprojectconfigs.yaml index 069919d86631d3dd423c2cec9a07954a05ce4c21..a2620ea6066a8e19ed2ab1cb2757f070f161797b 100644 --- a/config/crd/bases/drupal.webservices.cern.ch_drupalprojectconfigs.yaml +++ b/config/crd/bases/drupal.webservices.cern.ch_drupalprojectconfigs.yaml @@ -41,6 +41,13 @@ spec: description: PrimarySiteName defines the primary DrupalSite instance of a project type: string + primarySiteUrl: + description: PrimarySiteUrl defines the primary urls of a project + items: + description: Url refers to where the site should be made available. + pattern: '[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)' + type: string + type: array type: object status: description: DrupalProjectConfigStatus defines the observed state of DrupalProjectConfig diff --git a/config/crd/bases/drupal.webservices.cern.ch_drupalsiteconfigoverrides.yaml b/config/crd/bases/drupal.webservices.cern.ch_drupalsiteconfigoverrides.yaml index 977bde900048b41294eea05c07f882abb61ca869..e31c649be91c09c80e25a87dba5ac555eadbce45 100644 --- a/config/crd/bases/drupal.webservices.cern.ch_drupalsiteconfigoverrides.yaml +++ b/config/crd/bases/drupal.webservices.cern.ch_drupalsiteconfigoverrides.yaml @@ -54,7 +54,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object requests: additionalProperties: @@ -66,7 +66,7 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object type: object @@ -86,7 +86,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object requests: additionalProperties: @@ -98,7 +98,7 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object type: object @@ -118,7 +118,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object requests: additionalProperties: @@ -130,7 +130,7 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object type: object @@ -150,7 +150,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object requests: additionalProperties: @@ -162,7 +162,7 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object type: object @@ -182,7 +182,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object requests: additionalProperties: @@ -194,7 +194,7 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object type: object @@ -214,7 +214,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object requests: additionalProperties: @@ -226,7 +226,7 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object type: object diff --git a/config/crd/bases/drupal.webservices.cern.ch_drupalsites.yaml b/config/crd/bases/drupal.webservices.cern.ch_drupalsites.yaml index 3d2aed9bc383bbb7f481e2acb2c7869e302d19fa..9c6741ebe9fa32f6b05f7cd6f9acad48d8c1c8f8 100644 --- a/config/crd/bases/drupal.webservices.cern.ch_drupalsites.yaml +++ b/config/crd/bases/drupal.webservices.cern.ch_drupalsites.yaml @@ -238,6 +238,13 @@ spec: description: ServingPodImage reports the complete image name of the PHP-FPM container that is being used in the deployment. type: string + siteUrl: + description: SiteUrl defines the urls of a site + items: + description: Url refers to where the site should be made available. + pattern: '[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)' + type: string + type: array type: object required: - spec diff --git a/controllers/drupalsite_controller.go b/controllers/drupalsite_controller.go index fd4e60e1766373c5acb996a67871cfa9381c928b..b3bdd50ac5bbf4fb1cb300a1727c51d81fbbebd9 100644 --- a/controllers/drupalsite_controller.go +++ b/controllers/drupalsite_controller.go @@ -437,7 +437,7 @@ func (r *DrupalSiteReconciler) Reconcile(ctx context.Context, req ctrl.Request) } // Ensure all resources (server deployment is excluded here during updates) - if transientErrs := r.ensureResources(drupalSite, deploymentConfig, log); transientErrs != nil { + if transientErrs := r.ensureResources(drupalSite, drupalProjectConfig, deploymentConfig, log); transientErrs != nil { transientErr := concat(transientErrs) return handleTransientErr(transientErr, "%v while ensuring the resources", "Ready") } @@ -453,6 +453,15 @@ func (r *DrupalSiteReconciler) Reconcile(ctx context.Context, req ctrl.Request) log.V(3).Info("Ensured all resources are present.") + // Update status of drupalsite with primary urls + if drupalSite.Status.IsPrimary && !routesEqual(drupalSite.Status.SiteUrl, uniqueRoutes(drupalSite.Spec.SiteURL, drupalProjectConfig.Spec.PrimarySiteUrl)) { + drupalSite.Status.SiteUrl = uniqueRoutes(drupalSite.Spec.SiteURL, drupalProjectConfig.Spec.PrimarySiteUrl) + return r.updateCRStatusOrFailReconcile(ctx, log, drupalSite) + } else if !drupalSite.Status.IsPrimary { + drupalSite.Status.SiteUrl = drupalSite.Spec.SiteURL + return r.updateCRStatusOrFailReconcile(ctx, log, drupalSite) + } + // 4. Check DBOD has been provisioned and reconcile if needed if dbodReady := r.isDBODProvisioned(ctx, drupalSite); !dbodReady { diff --git a/controllers/drupalsite_controller_test.go b/controllers/drupalsite_controller_test.go index c204857f2bbe3422107e578d25cff797f0890641..7f05895dddd9cea1289974b58b40331616d1abd3 100644 --- a/controllers/drupalsite_controller_test.go +++ b/controllers/drupalsite_controller_test.go @@ -20,6 +20,7 @@ import ( "context" "crypto/md5" "encoding/hex" + "reflect" "time" . "github.com/onsi/ginkgo" @@ -60,7 +61,9 @@ var _ = Describe("DrupalSite controller", func() { interval = time.Millisecond * 250 ) var ( - drupalSiteObject = &drupalwebservicesv1alpha1.DrupalSite{} + drupalSiteObject = &drupalwebservicesv1alpha1.DrupalSite{} + drupalSiteObjectDev = &drupalwebservicesv1alpha1.DrupalSite{} + drupalProjectConfig = &drupalwebservicesv1alpha1.DrupalProjectConfig{} ) ctx := context.Background() @@ -96,6 +99,22 @@ var _ = Describe("DrupalSite controller", func() { }, }, } + drupalProjectConfig = &drupalwebservicesv1alpha1.DrupalProjectConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalProjectConfigSpec{ + PrimarySiteName: key.Name, + PrimarySiteUrl: []drupalwebservicesv1alpha1.Url{ + "test-primary.webtest.cern.ch", + }, + }, + } }) Describe("Creating drupalSite object", func() { @@ -119,6 +138,11 @@ var _ = Describe("DrupalSite controller", func() { return k8sClient.Create(ctx, drupalSiteObject) }, timeout, interval).Should(Succeed()) + By("By creating a drupalProjectConfig") + Eventually(func() error { + return k8sClient.Create(ctx, drupalProjectConfig) + }, timeout, interval).Should(Succeed()) + By("Expecting drupalSite object created") cr := drupalwebservicesv1alpha1.DrupalSite{} Eventually(func() error { @@ -766,6 +790,22 @@ var _ = Describe("DrupalSite controller", func() { SiteURL: []drupalwebservicesv1alpha1.Url{dummySiteUrl}, }, } + drupalProjectConfig = &drupalwebservicesv1alpha1.DrupalProjectConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalProjectConfigSpec{ + PrimarySiteName: key.Name, + PrimarySiteUrl: []drupalwebservicesv1alpha1.Url{ + "test-primary.webtest.cern.ch", + }, + }, + } By("By creating the testing namespace") Eventually(func() error { @@ -779,6 +819,11 @@ var _ = Describe("DrupalSite controller", func() { return k8sClient.Create(ctx, drupalSiteObject) }, timeout, interval).Should(Succeed()) + By("By creating a drupalProjectConfig") + Eventually(func() error { + return k8sClient.Create(ctx, drupalProjectConfig) + }, timeout, interval).Should(Succeed()) + // Create drupalSite object By("Expecting drupalSite object created") cr := drupalwebservicesv1alpha1.DrupalSite{} @@ -1496,6 +1541,22 @@ var _ = Describe("DrupalSite controller", func() { SiteURL: []drupalwebservicesv1alpha1.Url{dummySiteUrl}, }, } + drupalProjectConfig = &drupalwebservicesv1alpha1.DrupalProjectConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalProjectConfigSpec{ + PrimarySiteName: key.Name, + PrimarySiteUrl: []drupalwebservicesv1alpha1.Url{ + "test-primary.webtest.cern.ch", + }, + }, + } By("By creating the testing namespace") Eventually(func() error { @@ -1504,6 +1565,11 @@ var _ = Describe("DrupalSite controller", func() { }) }, timeout, interval).Should(Succeed()) + By("By creating a drupalProjectConfig") + Eventually(func() error { + return k8sClient.Create(ctx, drupalProjectConfig) + }, timeout, interval).Should(Succeed()) + By("By creating a new drupalSite") Eventually(func() error { return k8sClient.Create(ctx, drupalSiteObject) @@ -1981,6 +2047,136 @@ var _ = Describe("DrupalSite controller", func() { }) }) }) + Describe("Using DrupalProjectConfig", func() { + Context("Promoting a drupalsite to primary", func() { + It("Should update the status.siteUrl of each site", func() { + key = types.NamespacedName{ + Name: Name + "-primary", + Namespace: "drupalprojectconfig", + } + drupalSiteObject = &drupalwebservicesv1alpha1.DrupalSite{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalSiteSpec{ + Version: drupalwebservicesv1alpha1.Version{ + Name: "v8.9-1", + ReleaseSpec: "stable", + }, + Configuration: drupalwebservicesv1alpha1.Configuration{ + DiskSize: "10Gi", + QoSClass: drupalwebservicesv1alpha1.QoSStandard, + DatabaseClass: drupalwebservicesv1alpha1.DBODStandard, + }, + SiteURL: []drupalwebservicesv1alpha1.Url{dummySiteUrl}, + }, + } + drupalSiteObjectDev = &drupalwebservicesv1alpha1.DrupalSite{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name + "dev", + Namespace: key.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalSiteSpec{ + Version: drupalwebservicesv1alpha1.Version{ + Name: "v8.9-1", + ReleaseSpec: "stable", + }, + Configuration: drupalwebservicesv1alpha1.Configuration{ + DiskSize: "10Gi", + QoSClass: drupalwebservicesv1alpha1.QoSStandard, + DatabaseClass: drupalwebservicesv1alpha1.DBODStandard, + }, + SiteURL: []drupalwebservicesv1alpha1.Url{"test-dev.webtest.cern.ch"}, + }, + } + drupalProjectConfig = &drupalwebservicesv1alpha1.DrupalProjectConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "drupal.webservices.cern.ch/v1alpha1", + Kind: "DrupalSite", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: drupalwebservicesv1alpha1.DrupalProjectConfigSpec{ + PrimarySiteName: key.Name, + PrimarySiteUrl: []drupalwebservicesv1alpha1.Url{ + "test-primary.webtest.cern.ch", + }, + }, + } + + By("By creating the testing namespace") + Eventually(func() error { + return k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: key.Namespace}, + }) + }, timeout, interval).Should(Succeed()) + + By("By creating a drupalProjectConfig") + Eventually(func() error { + return k8sClient.Create(ctx, drupalProjectConfig) + }, timeout, interval).Should(Succeed()) + + By("By creating a new drupalSite") + Eventually(func() error { + return k8sClient.Create(ctx, drupalSiteObject) + }, timeout, interval).Should(Succeed()) + + By("Expecting the isPrimary status field in drupalSite") + Eventually(func() bool { + cr := drupalwebservicesv1alpha1.DrupalSite{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: key.Namespace}, &cr) + return cr.Status.IsPrimary == true + }, timeout, interval).Should(BeTrue()) + + By("Expecting the siteUrl status field in drupalSite") + Eventually(func() bool { + cr := drupalwebservicesv1alpha1.DrupalSite{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: key.Namespace}, &cr) + return reflect.DeepEqual(cr.Status.SiteUrl, []drupalwebservicesv1alpha1.Url{dummySiteUrl, "test-primary.webtest.cern.ch"}) + }, timeout, interval).Should(BeTrue()) + + By("Expecting the primarySiteName spec field in drupalSiteProjectConfig") + Eventually(func() bool { + dpc := drupalwebservicesv1alpha1.DrupalProjectConfig{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: key.Namespace}, &dpc) + return dpc.Spec.PrimarySiteName == key.Name + }, timeout, interval).Should(BeTrue()) + + By("By creating a second drupalSite") + Eventually(func() error { + return k8sClient.Create(ctx, drupalSiteObjectDev) + }, timeout, interval).Should(Succeed()) + + By("Changing the primary site") + Eventually(func() error { + dpc := drupalwebservicesv1alpha1.DrupalProjectConfig{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: key.Namespace}, &dpc) + dpc.Spec.PrimarySiteName = key.Name + "dev" + return k8sClient.Update(ctx, &dpc) + }, timeout, interval).Should(Succeed()) + + By("Expecting the siteUrl status field to be uddated in both drupalSites") + Eventually(func() bool { + cr_old_primary := drupalwebservicesv1alpha1.DrupalSite{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name, Namespace: key.Namespace}, &cr_old_primary) + cr_new_primary := drupalwebservicesv1alpha1.DrupalSite{} + k8sClient.Get(ctx, types.NamespacedName{Name: key.Name + "dev", Namespace: key.Namespace}, &cr_new_primary) + return reflect.DeepEqual(cr_old_primary.Status.SiteUrl, []drupalwebservicesv1alpha1.Url{dummySiteUrl}) && reflect.DeepEqual(cr_new_primary.Status.SiteUrl, []drupalwebservicesv1alpha1.Url{"test-dev.webtest.cern.ch", "test-primary.webtest.cern.ch"}) + }, timeout, interval).Should(BeTrue()) + }) + }) + }) Describe("Deleting the drupalsite object", func() { Context("With critical QoS", func() { It("Should be deleted successfully", func() { diff --git a/controllers/drupalsite_resources.go b/controllers/drupalsite_resources.go index 9c9c5fa8286d553646ca7817a82df37ed0bcf590..36db838d2896e0c499330d719b91156b5d426eba 100644 --- a/controllers/drupalsite_resources.go +++ b/controllers/drupalsite_resources.go @@ -196,46 +196,46 @@ func (r *DrupalSiteReconciler) ensureDeploymentConfigmapHash(ctx context.Context ensureResources ensures the presence of all the resources that the DrupalSite needs to serve content. This includes BuildConfigs/ImageStreams, DB, PVC, PHP/Nginx deployment + service, site install job, Routes. */ -func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, deploymentConfig DeploymentConfig, log logr.Logger) (transientErrs []reconcileError) { +func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, dpc *webservicesv1a1.DrupalProjectConfig, deploymentConfig DeploymentConfig, log logr.Logger) (transientErrs []reconcileError) { ctx := context.TODO() // 1. BuildConfigs and ImageStreams if len(drp.Spec.Configuration.ExtraConfigurationRepo) > 0 { - if transientErr := r.ensureResourceX(ctx, drp, "is_s2i", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "is_s2i", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for S2I SiteBuilder ImageStream")) } - if transientErr := r.ensureResourceX(ctx, drp, "bc_s2i", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "bc_s2i", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for S2I SiteBuilder BuildConfig")) } - if transientErr := r.ensureResourceX(ctx, drp, "gitlab_trigger_secret", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "gitlab_trigger_secret", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for S2I SiteBuilder Secret")) } } // 2. Data layer - if transientErr := r.ensureResourceX(ctx, drp, "pvc_drupal", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "pvc_drupal", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for Drupal PVC")) } - if transientErr := r.ensureResourceX(ctx, drp, "dbod_cr", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "dbod_cr", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for DBOD resource")) } - if transientErr := r.ensureResourceX(ctx, drp, "webdav_secret", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "webdav_secret", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for WebDAV Secret")) } // 3. Serving layer - if transientErr := r.ensureResourceX(ctx, drp, "cm_php", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "cm_php", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for PHP-FPM CM")) } - if transientErr := r.ensureResourceX(ctx, drp, "cm_nginx_global", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "cm_nginx_global", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for Nginx CM")) } - if transientErr := r.ensureResourceX(ctx, drp, "cm_settings", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "cm_settings", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for settings.php CM")) } - if transientErr := r.ensureResourceX(ctx, drp, "cm_php_cli", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "cm_php_cli", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for PHP Job CM")) } if r.isDBODProvisioned(ctx, drp) { @@ -243,7 +243,7 @@ func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, transientErrs = append(transientErrs, transientErr.Wrap("%v: for Drupal deployment")) } } - if transientErr := r.ensureResourceX(ctx, drp, "svc_nginx", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "svc_nginx", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for Nginx SVC")) } /* A new drupalsite can be initialized with 3 different ways depending its Spec: @@ -256,15 +256,15 @@ func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, if r.isDBODProvisioned(ctx, drp) && !(drp.ConditionTrue("Initialized")) { switch { case drp.Spec.Configuration.CloneFrom != "": - if transientErr := r.ensureResourceX(ctx, drp, "clone_job", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "clone_job", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for clone Job")) } case drp.Spec.Configuration.Easystart == "enable": - if transientErr := r.ensureResourceX(ctx, drp, "easystart_taskrun", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "easystart_taskrun", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for easystart TaskRun")) } default: - if transientErr := r.ensureResourceX(ctx, drp, "site_install_job", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "site_install_job", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for site install Job")) } } @@ -274,15 +274,15 @@ func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, if drp.ConditionTrue("Initialized") { // each function below ensures 1 route per entry in `spec.siteUrl[]`. This is understandably part of the job of "ensuring resource X". - if transientErr := r.ensureResourceX(ctx, drp, "route", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "route", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for Route")) } - if transientErr := r.ensureResourceX(ctx, drp, "oidc_return_uri", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "oidc_return_uri", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for OidcReturnURI")) } // each function below removes any unwanted routes - if transientErr := r.ensureNoExtraRouteResource(ctx, drp, "drupal", log); transientErr != nil { + if transientErr := r.ensureNoExtraRouteResource(ctx, drp, dpc, "drupal", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: while ensuring no extra routes")) } if transientErr := r.ensureNoExtraOidcReturnUriResource(ctx, drp, "drupal", log); transientErr != nil { @@ -302,7 +302,7 @@ func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, // 5. Cluster-scoped: Backup schedule, Tekton RBAC // Create Velero schedule only after site is initialized in order for the first backup to not report 'Failed' or 'PartiallyFailed' status if drp.ConditionTrue("Initialized") && (drp.Status.IsPrimary || drp.Spec.Configuration.ScheduledBackups == "enabled") { - if transientErr := r.ensureResourceX(ctx, drp, "backup_schedule", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "backup_schedule", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for Velero Schedule")) } } else { @@ -310,7 +310,7 @@ func (r *DrupalSiteReconciler) ensureResources(drp *webservicesv1a1.DrupalSite, transientErrs = append(transientErrs, transientErr.Wrap("%v: while deleting the Velero schedule")) } } - if transientErr := r.ensureResourceX(ctx, drp, "tekton_extra_perm_rbac", log); transientErr != nil { + if transientErr := r.ensureResourceX(ctx, drp, dpc, "tekton_extra_perm_rbac", log); transientErr != nil { transientErrs = append(transientErrs, transientErr.Wrap("%v: for Tekton Extra Permissions ClusterRoleBinding")) } return transientErrs @@ -339,7 +339,7 @@ ensureResourceX ensure the requested resource is created, with the following val - tekton_extra_perm_rbac: ClusterRoleBinding for tekton tasks - gitlab_trigger_secret: Secret for Gitlab trigger config in buildconfig */ -func (r *DrupalSiteReconciler) ensureResourceX(ctx context.Context, d *webservicesv1a1.DrupalSite, resType string, log logr.Logger) (transientErr reconcileError) { +func (r *DrupalSiteReconciler) ensureResourceX(ctx context.Context, d *webservicesv1a1.DrupalSite, dpc *webservicesv1a1.DrupalProjectConfig, resType string, log logr.Logger) (transientErr reconcileError) { switch resType { case "is_s2i": is := &imagev1.ImageStream{ObjectMeta: metav1.ObjectMeta{Name: "sitebuilder-s2i-" + d.Name, Namespace: d.Namespace}} @@ -398,6 +398,9 @@ func (r *DrupalSiteReconciler) ensureResourceX(ctx context.Context, d *webservic return nil case "route": routeRequestList := d.Spec.SiteURL + if d.Status.IsPrimary { + routeRequestList = uniqueRoutes(routeRequestList, dpc.Spec.PrimarySiteUrl) + } for _, req := range routeRequestList { hash := md5.Sum([]byte(req)) route := &routev1.Route{ObjectMeta: metav1.ObjectMeta{Name: d.Name + "-" + hex.EncodeToString(hash[0:4]), Namespace: d.Namespace}} @@ -596,7 +599,7 @@ func (r *DrupalSiteReconciler) ensureDrupalDeployment(ctx context.Context, d *we } // ensureNoExtraRouteResource uses the current SiteURL resource as reference and deletes any extra route -func (r *DrupalSiteReconciler) ensureNoExtraRouteResource(ctx context.Context, d *webservicesv1a1.DrupalSite, label string, log logr.Logger) (transientErr reconcileError) { +func (r *DrupalSiteReconciler) ensureNoExtraRouteResource(ctx context.Context, d *webservicesv1a1.DrupalSite, dpc *webservicesv1a1.DrupalProjectConfig, label string, log logr.Logger) (transientErr reconcileError) { ls := labelsForDrupalSite(d.Name) ls["app"] = "drupal" ls["route"] = label @@ -618,6 +621,10 @@ func (r *DrupalSiteReconciler) ensureNoExtraRouteResource(ctx context.Context, d } routeRequestList := d.Spec.SiteURL routesToRemove := []webservicesv1a1.Url{} + if d.Status.IsPrimary { + routeRequestList = uniqueRoutes(routeRequestList, dpc.Spec.PrimarySiteUrl) + } + for _, route := range existingRoutes.Items { flag := false for _, req := range routeRequestList { @@ -2262,3 +2269,43 @@ func containerExists(name string, currentobject *appsv1.Deployment) { currentobject.Spec.Template.Spec.Containers = append(currentobject.Spec.Template.Spec.Containers, corev1.Container{Name: name}) } } + +// uniqueRoutes returns the total routes of a drupalsite +func uniqueRoutes(routeRequestList []webservicesv1a1.Url, primarySiteUrl []webservicesv1a1.Url) []webservicesv1a1.Url { + routeRequestList = append(routeRequestList, primarySiteUrl...) + keys := make(map[string]bool) + uniqueRouteRequestList := []webservicesv1a1.Url{} + for _, route := range routeRequestList { + if _, value := keys[string(route)]; !value { + keys[string(route)] = true + uniqueRouteRequestList = append(uniqueRouteRequestList, route) + } + } + return uniqueRouteRequestList +} + +// routesEqual compares the urls of drupalsite.status.siteUrl with the total of +// drupalsite.spec.siteUrl and drupalProjectConfig.spec.PrimaryUrl +func routesEqual(currentRoutes []webservicesv1a1.Url, expectedRoutes []webservicesv1a1.Url) bool { + if len(currentRoutes) != len(expectedRoutes) { + return false + } else { + for _, currentRoute := range currentRoutes { + if !findUrl(expectedRoutes, string(currentRoute)) { + return false + } + } + return true + } + +} + +// Checks if a url exists in a Url slice +func findUrl(urls []webservicesv1a1.Url, url string) bool { + for _, item := range urls { + if string(item) == url { + return true + } + } + return false +}