diff --git a/api/bases/horizon.openstack.org_horizons.yaml b/api/bases/horizon.openstack.org_horizons.yaml index d7e90a7..c9ba664 100644 --- a/api/bases/horizon.openstack.org_horizons.yaml +++ b/api/bases/horizon.openstack.org_horizons.yaml @@ -1134,6 +1134,20 @@ spec: - extraVol type: object type: array + httpdCustomization: + description: HttpdCustomization - customize the httpd service + properties: + customConfigSecret: + description: |- + CustomConfigSecret - customize the httpd vhost config using this parameter to specify + a secret that contains service config data. The content of each provided snippet gets + rendered as a go template and placed into /etc/httpd/conf/httpd_custom_ . + In the default httpd template at the end of the vhost those custom configs get + included using `Include conf/httpd_custom__*`. + For information on how sections in httpd configuration get merged, check section + "How the sections are merged" in https://httpd.apache.org/docs/current/sections.html#merging + type: string + type: object memcachedInstance: default: memcached description: Memcached instance name. diff --git a/api/go.mod b/api/go.mod index 0c4795d..0bd865c 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,7 +3,7 @@ module github.com/openstack-k8s-operators/horizon-operator/api go 1.21 require ( - github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241216113837-d172b3ac0f4e + github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20250116145727-01a8948d5dd7 github.com/openstack-k8s-operators/lib-common/modules/storage v0.5.1-0.20241216113837-d172b3ac0f4e k8s.io/api v0.29.12 k8s.io/apimachinery v0.29.12 diff --git a/api/go.sum b/api/go.sum index bff38bc..edeb991 100644 --- a/api/go.sum +++ b/api/go.sum @@ -73,8 +73,8 @@ github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241216113837-d172b3ac0f4e h1:hf4kVQBkyG79WcHBxdQ25QrDBbGFdarebS1Tc0Xclq4= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241216113837-d172b3ac0f4e/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20250116145727-01a8948d5dd7 h1:vXHpH93PjbAgg5ZN6n5WmxkybVQOs0nhXvVw62o7aZs= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20250116145727-01a8948d5dd7/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= github.com/openstack-k8s-operators/lib-common/modules/storage v0.5.1-0.20241216113837-d172b3ac0f4e h1:Qz0JFEoRDUyjEWorNY3LggwxTsmpMtQkcpmZDQulGHQ= github.com/openstack-k8s-operators/lib-common/modules/storage v0.5.1-0.20241216113837-d172b3ac0f4e/go.mod h1:tfgBeLRqmlH/NQkLPe7396rj+t0whv2wPuMb8Ttvh8w= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= diff --git a/api/v1beta1/horizon_types.go b/api/v1beta1/horizon_types.go index 182afe3..a58f4ca 100644 --- a/api/v1beta1/horizon_types.go +++ b/api/v1beta1/horizon_types.go @@ -104,6 +104,10 @@ type HorizonSpecCore struct { // +kubebuilder:validation:Optional // NetworkAttachments is a list of NetworkAttachment resource names to expose the services to the given network NetworkAttachments []string `json:"networkAttachments,omitempty"` + + // +kubebuilder:validation:Optional + // HttpdCustomization - customize the httpd service + HttpdCustomization HttpdCustomization `json:"httpdCustomization,omitempty"` } // HorizionOverrideSpec to override the generated manifest of several child resources. @@ -112,6 +116,19 @@ type HorizionOverrideSpec struct { Service *service.RoutedOverrideSpec `json:"service,omitempty"` } +// HttpdCustomization - customize the httpd service +type HttpdCustomization struct { + // +kubebuilder:validation:Optional + // CustomConfigSecret - customize the httpd vhost config using this parameter to specify + // a secret that contains service config data. The content of each provided snippet gets + // rendered as a go template and placed into /etc/httpd/conf/httpd_custom_ . + // In the default httpd template at the end of the vhost those custom configs get + // included using `Include conf/httpd_custom__*`. + // For information on how sections in httpd configuration get merged, check section + // "How the sections are merged" in https://httpd.apache.org/docs/current/sections.html#merging + CustomConfigSecret *string `json:"customConfigSecret,omitempty"` +} + // HorizonStatus defines the observed state of Horizon type HorizonStatus struct { // Map of hashes to track e.g. job status diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index e48f8a3..b40147e 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -200,6 +200,7 @@ func (in *HorizonSpecCore) DeepCopyInto(out *HorizonSpecCore) { *out = make([]string, len(*in)) copy(*out, *in) } + in.HttpdCustomization.DeepCopyInto(&out.HttpdCustomization) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizonSpecCore. @@ -256,3 +257,23 @@ func (in *HorizonStatus) DeepCopy() *HorizonStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HttpdCustomization) DeepCopyInto(out *HttpdCustomization) { + *out = *in + if in.CustomConfigSecret != nil { + in, out := &in.CustomConfigSecret, &out.CustomConfigSecret + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HttpdCustomization. +func (in *HttpdCustomization) DeepCopy() *HttpdCustomization { + if in == nil { + return nil + } + out := new(HttpdCustomization) + in.DeepCopyInto(out) + return out +} diff --git a/config/crd/bases/horizon.openstack.org_horizons.yaml b/config/crd/bases/horizon.openstack.org_horizons.yaml index d7e90a7..c9ba664 100644 --- a/config/crd/bases/horizon.openstack.org_horizons.yaml +++ b/config/crd/bases/horizon.openstack.org_horizons.yaml @@ -1134,6 +1134,20 @@ spec: - extraVol type: object type: array + httpdCustomization: + description: HttpdCustomization - customize the httpd service + properties: + customConfigSecret: + description: |- + CustomConfigSecret - customize the httpd vhost config using this parameter to specify + a secret that contains service config data. The content of each provided snippet gets + rendered as a go template and placed into /etc/httpd/conf/httpd_custom_ . + In the default httpd template at the end of the vhost those custom configs get + included using `Include conf/httpd_custom__*`. + For information on how sections in httpd configuration get merged, check section + "How the sections are merged" in https://httpd.apache.org/docs/current/sections.html#merging + type: string + type: object memcachedInstance: default: memcached description: Memcached instance name. diff --git a/controllers/horizon_controller.go b/controllers/horizon_controller.go index 4a7db03..93f927e 100644 --- a/controllers/horizon_controller.go +++ b/controllers/horizon_controller.go @@ -42,6 +42,7 @@ import ( "github.com/openstack-k8s-operators/lib-common/modules/common/service" "github.com/openstack-k8s-operators/lib-common/modules/common/tls" util "github.com/openstack-k8s-operators/lib-common/modules/common/util" + "gopkg.in/yaml.v2" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -203,15 +204,17 @@ func (r *HorizonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re // fields to index to reconcile when change const ( - passwordSecretField = ".spec.secret" - tlsField = ".spec.tls.secretName" - caBundleSecretNameField = ".spec.tls.caBundleSecretName" + passwordSecretField = ".spec.secret" + tlsField = ".spec.tls.secretName" + caBundleSecretNameField = ".spec.tls.caBundleSecretName" + httpdCustomServiceConfigSecretField = ".spec.httpdCustomization.customServiceConfigSecret" ) var allWatchFields = []string{ passwordSecretField, caBundleSecretNameField, tlsField, + httpdCustomServiceConfigSecretField, } // SetupWithManager - @@ -254,6 +257,18 @@ func (r *HorizonReconciler) SetupWithManager(mgr ctrl.Manager) error { return err } + // index httpdOverrideSecretField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &horizonv1beta1.Horizon{}, httpdCustomServiceConfigSecretField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*horizonv1beta1.Horizon) + if cr.Spec.HttpdCustomization.CustomConfigSecret == nil { + return nil + } + return []string{*cr.Spec.HttpdCustomization.CustomConfigSecret} + }); err != nil { + return err + } + memcachedFn := func(_ context.Context, o client.Object) []reconcile.Request { result := []reconcile.Request{} @@ -887,16 +902,24 @@ func (r *HorizonReconciler) generateServiceConfigMaps( return err } + httpdOverrideSecret := &corev1.Secret{} + if instance.Spec.HttpdCustomization.CustomConfigSecret != nil && *instance.Spec.HttpdCustomization.CustomConfigSecret != "" { + httpdOverrideSecret, _, err = oko_secret.GetSecret(ctx, h, *instance.Spec.HttpdCustomization.CustomConfigSecret, instance.Namespace) + if err != nil { + return err + } + } + templateParameters := map[string]interface{}{ - "keystoneURL": authURL, - "horizonEndpoint": instance.Status.Endpoint, - "horizonEndpointHost": url.Host, - "memcachedServers": mc.GetMemcachedServerListQuotedString(), - "memcachedTLS": mc.GetMemcachedTLSSupport(), - "ServerName": fmt.Sprintf("%s.%s.svc", horizon.ServiceName, instance.Namespace), - "Port": horizon.HorizonPort, - "TLS": false, - "isPublicHTTPS": url.Scheme == "https", + "KeystoneEndpointInternal": authURL, + "HorizonEndpoint": instance.Status.Endpoint, + "HorizonEndpointHost": url.Host, + "MemcachedServers": mc.GetMemcachedServerListQuotedString(), + "MemcachedTLS": mc.GetMemcachedTLSSupport(), + "ServerName": fmt.Sprintf("%s.%s.svc", horizon.ServiceName, instance.Namespace), + "Port": horizon.HorizonPort, + "TLS": false, + "IsPublicHTTPS": url.Scheme == "https", } // create httpd tls template parameters @@ -907,16 +930,36 @@ func (r *HorizonReconciler) generateServiceConfigMaps( templateParameters["SSLCertificateKeyFile"] = fmt.Sprintf("/etc/pki/tls/private/%s.key", horizon.ServiceName) } + // httpd overrides + customTemplates := map[string]string{} + templateParameters["Override"] = false + if len(httpdOverrideSecret.Data) > 0 { + templateParameters["Override"] = true + for key, data := range httpdOverrideSecret.Data { + if len(data) > 0 { + customTemplates["httpd_custom_"+key] = string(data) + } + } + } + + // Marshal the templateParameters map to YAML + yamlData, err := yaml.Marshal(templateParameters) + if err != nil { + return fmt.Errorf("Error marshalling to YAML: %w", err) + } + customData[common.TemplateParameters] = string(yamlData) + cms := []util.Template{ // ConfigMap { - Name: fmt.Sprintf("%s-config-data", instance.Name), - Namespace: instance.Namespace, - Type: util.TemplateTypeConfig, - InstanceType: instance.Kind, - CustomData: customData, - ConfigOptions: templateParameters, - Labels: cmLabels, + Name: fmt.Sprintf("%s-config-data", instance.Name), + Namespace: instance.Namespace, + Type: util.TemplateTypeConfig, + InstanceType: instance.Kind, + CustomData: customData, + ConfigOptions: templateParameters, + StringTemplate: customTemplates, + Labels: cmLabels, }, } return configmap.EnsureConfigMaps(ctx, h, instance, cms, envVars) diff --git a/go.mod b/go.mod index f1fbaac..9342fe6 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,10 @@ require ( github.com/openstack-k8s-operators/horizon-operator/api v0.3.1-0.20240214134444-c675e5f69043 github.com/openstack-k8s-operators/infra-operator/apis v0.5.1-0.20241217184302-c302f3d72ada github.com/openstack-k8s-operators/keystone-operator/api v0.5.1-0.20241217165019-8e243bd36596 - github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241216113837-d172b3ac0f4e + github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20250116145727-01a8948d5dd7 github.com/openstack-k8s-operators/lib-common/modules/storage v0.5.1-0.20241216113837-d172b3ac0f4e github.com/openstack-k8s-operators/lib-common/modules/test v0.5.1-0.20241216113837-d172b3ac0f4e + gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.29.12 k8s.io/apimachinery v0.29.12 k8s.io/client-go v0.29.12 @@ -73,7 +74,6 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.29.12 // indirect k8s.io/component-base v0.29.12 // indirect diff --git a/go.sum b/go.sum index c9702bf..387d62c 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/openstack-k8s-operators/infra-operator/apis v0.5.1-0.20241217184302-c github.com/openstack-k8s-operators/infra-operator/apis v0.5.1-0.20241217184302-c302f3d72ada/go.mod h1:gznNWtIOdZLwyv3/LmWbDqtwRgtyzCw616Rwrn51DT0= github.com/openstack-k8s-operators/keystone-operator/api v0.5.1-0.20241217165019-8e243bd36596 h1:JKeShCY9BQj6cYDk44bgEIm8jPcvggodGxrW4ECzsv4= github.com/openstack-k8s-operators/keystone-operator/api v0.5.1-0.20241217165019-8e243bd36596/go.mod h1:CyuEOM1TpXKNUR1n8cudNtRzTEwkzv90JFkpDPPId8E= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241216113837-d172b3ac0f4e h1:hf4kVQBkyG79WcHBxdQ25QrDBbGFdarebS1Tc0Xclq4= -github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20241216113837-d172b3ac0f4e/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20250116145727-01a8948d5dd7 h1:vXHpH93PjbAgg5ZN6n5WmxkybVQOs0nhXvVw62o7aZs= +github.com/openstack-k8s-operators/lib-common/modules/common v0.5.1-0.20250116145727-01a8948d5dd7/go.mod h1:YpNTuJhDWhbXM50O3qBkhO7M+OOyRmWkNVmJ4y3cyFs= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.5.1-0.20241216113837-d172b3ac0f4e h1:HFo4OqPY0x4ZQeaWI2YGonTXAGTQFt+rOEJlfZVhS7s= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.5.1-0.20241216113837-d172b3ac0f4e/go.mod h1:IASoGvp5QM/tBJUd/8i8uIjj4DBnI+64Ydh4r7pmnvA= github.com/openstack-k8s-operators/lib-common/modules/storage v0.5.1-0.20241216113837-d172b3ac0f4e h1:Qz0JFEoRDUyjEWorNY3LggwxTsmpMtQkcpmZDQulGHQ= diff --git a/templates/horizon/config/horizon.json b/templates/horizon/config/horizon.json index b36e21a..bdcf3e2 100644 --- a/templates/horizon/config/horizon.json +++ b/templates/horizon/config/horizon.json @@ -49,6 +49,13 @@ "perm": "0600", "optional": true, "merge": true + }, + { + "source": "/var/lib/config-data/default/httpd_custom_*", + "dest": "/etc/httpd/conf/", + "owner": "apache", + "perm": "0444", + "optional": true } ], "permissions": [ diff --git a/templates/horizon/config/httpd.conf b/templates/horizon/config/httpd.conf index 31e49a6..f50de43 100644 --- a/templates/horizon/config/httpd.conf +++ b/templates/horizon/config/httpd.conf @@ -57,11 +57,15 @@ LogLevel debug CustomLog /dev/stdout "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" env=forwarded ## RedirectMatch rules - RedirectMatch permanent ^/$ "{{ .horizonEndpoint }}/dashboard" + RedirectMatch permanent ^/$ "{{ .HorizonEndpoint }}/dashboard" ## WSGI configuration WSGIApplicationGroup %{GLOBAL} WSGIDaemonProcess apache display-name=horizon group=apache processes=4 threads=1 user=apache WSGIProcessGroup apache WSGIScriptAlias /dashboard "/usr/share/openstack-dashboard/openstack_dashboard/wsgi.py" + +{{- if .Override }} + Include conf/httpd_custom_* +{{- end }} diff --git a/templates/horizon/config/local_settings.py b/templates/horizon/config/local_settings.py index 6ad41bf..7015971 100644 --- a/templates/horizon/config/local_settings.py +++ b/templates/horizon/config/local_settings.py @@ -65,8 +65,8 @@ def get_pod_ip(): import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) hostport = ( - "{{ .horizonEndpointHost }}", - {{- if .isPublicHTTPS }} + "{{ .HorizonEndpointHost }}", + {{- if .IsPublicHTTPS }} 443 {{- else }} 80 @@ -83,11 +83,11 @@ def get_pod_ip(): finally: s.close() -ALLOWED_HOSTS = [get_pod_ip(), "{{ .horizonEndpointHost }}"] +ALLOWED_HOSTS = [get_pod_ip(), "{{ .HorizonEndpointHost }}"] USE_X_FORWARDED_HOST = True -{{- if .isPublicHTTPS }} +{{- if .IsPublicHTTPS }} SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') SECURE_SSL_REDIRECT = True CSRF_COOKIE_SECURE = True @@ -122,11 +122,11 @@ def get_pod_ip(): CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache', - 'LOCATION': [ {{.memcachedServers}} ], + 'LOCATION': [ {{.MemcachedServers}} ], # To drop the cached sessions when config changes 'KEY_PREFIX': os.environ['CONFIG_HASH'], 'OPTIONS': { -{{- if .memcachedTLS }} +{{- if .MemcachedTLS }} 'tls_context': ssl.create_default_context() {{- end }} } @@ -154,7 +154,7 @@ def get_pod_ip(): OPENSTACK_HOST = "127.0.0.1" #OPENSTACK_KEYSTONE_URL = "http://%s/identity/v3" % OPENSTACK_HOST -OPENSTACK_KEYSTONE_URL = "{{ .keystoneURL }}/v3" +OPENSTACK_KEYSTONE_URL = "{{ .KeystoneEndpointInternal }}/v3" # The timezone of the server. This should correspond with the timezone # of your entire OpenStack installation, and hopefully be in UTC. diff --git a/tests/functional/horizon_controller_test.go b/tests/functional/horizon_controller_test.go index 60f4b30..a1c26fb 100644 --- a/tests/functional/horizon_controller_test.go +++ b/tests/functional/horizon_controller_test.go @@ -16,6 +16,7 @@ import ( "github.com/openstack-k8s-operators/horizon-operator/pkg/horizon" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" + "github.com/openstack-k8s-operators/lib-common/modules/common" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" ) @@ -529,4 +530,59 @@ var _ = Describe("Horizon controller", func() { Expect(nad).To(Equal(horizon.Status.NetworkAttachments)) }) }) + + When("A Horizon is created with HttpdCustomization.CustomConfigSecret", func() { + BeforeEach(func() { + customServiceConfigSecretName := types.NamespacedName{Name: "foo", Namespace: namespace} + customConfig := []byte(`CustomParam "foo" +CustomKeystoneEndpointInternal "{{ .KeystoneEndpointInternal }}"`) + th.CreateSecret( + customServiceConfigSecretName, + map[string][]byte{ + "bar.conf": customConfig, + }, + ) + + rawSpec := map[string]interface{}{ + "secret": SecretName, + "memcachedInstance": "memcached", + "httpdCustomization": map[string]interface{}{ + "customConfigSecret": customServiceConfigSecretName.Name, + }, + } + DeferCleanup(th.DeleteInstance, CreateHorizon(horizonName, rawSpec)) + DeferCleanup( + k8sClient.Delete, ctx, CreateHorizonSecret(namespace, SecretName)) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + infra.SimulateMemcachedReady(types.NamespacedName{ + Name: "memcached", + Namespace: namespace, + }) + keystoneAPI := keystone.CreateKeystoneAPI(namespace) + DeferCleanup(keystone.DeleteKeystoneAPI, keystoneAPI) + th.SimulateDeploymentReadyWithPods( + horizonName, + map[string][]string{}, + ) + + }) + It("it renders the custom template and adds it to the config-data secret", func() { + cm := th.GetConfigMap(types.NamespacedName{ + Namespace: horizonName.Namespace, + Name: horizonName.Name + "-config-data", + }) + + Expect(cm).ShouldNot(BeNil()) + Expect(cm.Data).Should(HaveKey(common.TemplateParameters)) + configData := string(cm.Data[common.TemplateParameters]) + keystoneInternalURL := "http://keystone-internal.openstack.svc:5000" + Expect(configData).Should(ContainSubstring(fmt.Sprintf("KeystoneEndpointInternal: %s", keystoneInternalURL))) + + Expect(cm.Data).Should(HaveKey("httpd_custom_bar.conf")) + configData = string(cm.Data["httpd_custom_bar.conf"]) + Expect(configData).Should(ContainSubstring("CustomParam \"foo\"")) + Expect(configData).Should(ContainSubstring(fmt.Sprintf("CustomKeystoneEndpointInternal \"%s\"", keystoneInternalURL))) + + }) + }) })