diff --git a/api/bases/neutron.openstack.org_neutronapis.yaml b/api/bases/neutron.openstack.org_neutronapis.yaml index 3d2f564e..dafc7547 100644 --- a/api/bases/neutron.openstack.org_neutronapis.yaml +++ b/api/bases/neutron.openstack.org_neutronapis.yaml @@ -1152,6 +1152,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 ce526fd0..cd2684ea 100644 --- a/api/go.mod +++ b/api/go.mod @@ -3,7 +3,7 @@ module github.com/openstack-k8s-operators/neutron-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 bff38bcb..edeb9913 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/neutronapi_types.go b/api/v1beta1/neutronapi_types.go index 80badf27..de129f8f 100644 --- a/api/v1beta1/neutronapi_types.go +++ b/api/v1beta1/neutronapi_types.go @@ -149,6 +149,10 @@ type NeutronAPISpecCore struct { // +operator-sdk:csv:customresourcedefinitions:type=spec // TLS - Parameters related to the TLS TLS NeutronApiTLS `json:"tls,omitempty"` + + // +kubebuilder:validation:Optional + // HttpdCustomization - customize the httpd service + HttpdCustomization HttpdCustomization `json:"httpdCustomization,omitempty"` } type NeutronApiTLS struct { @@ -181,6 +185,19 @@ type PasswordSelector struct { Service string `json:"service"` } +// 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"` +} + // NeutronAPIStatus defines the observed state of NeutronAPI type NeutronAPIStatus struct { // ReadyCount of neutron API instances diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index a49cd880..386aaf4a 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -49,6 +49,26 @@ func (in *APIOverrideSpec) DeepCopy() *APIOverrideSpec { 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 +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NeutronAPI) DeepCopyInto(out *NeutronAPI) { *out = *in @@ -186,6 +206,7 @@ func (in *NeutronAPISpecCore) DeepCopyInto(out *NeutronAPISpecCore) { } in.Override.DeepCopyInto(&out.Override) in.TLS.DeepCopyInto(&out.TLS) + in.HttpdCustomization.DeepCopyInto(&out.HttpdCustomization) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NeutronAPISpecCore. diff --git a/config/crd/bases/neutron.openstack.org_neutronapis.yaml b/config/crd/bases/neutron.openstack.org_neutronapis.yaml index 3d2f564e..dafc7547 100644 --- a/config/crd/bases/neutron.openstack.org_neutronapis.yaml +++ b/config/crd/bases/neutron.openstack.org_neutronapis.yaml @@ -1152,6 +1152,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/neutronapi_controller.go b/controllers/neutronapi_controller.go index 9e72d17e..8d8b0498 100644 --- a/controllers/neutronapi_controller.go +++ b/controllers/neutronapi_controller.go @@ -23,6 +23,7 @@ import ( "time" "github.com/go-logr/logr" + "gopkg.in/yaml.v2" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -210,11 +211,12 @@ func (r *NeutronAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) // fields to index to reconcile when change const ( - passwordSecretField = ".spec.secret" - caBundleSecretNameField = ".spec.tls.caBundleSecretName" - tlsAPIInternalField = ".spec.tls.api.internal.secretName" - tlsAPIPublicField = ".spec.tls.api.public.secretName" - tlsOVNField = ".spec.tls.ovn.secretName" + passwordSecretField = ".spec.secret" + caBundleSecretNameField = ".spec.tls.caBundleSecretName" + tlsAPIInternalField = ".spec.tls.api.internal.secretName" + tlsAPIPublicField = ".spec.tls.api.public.secretName" + tlsOVNField = ".spec.tls.ovn.secretName" + httpdCustomServiceConfigSecretField = ".spec.httpdCustomization.customServiceConfigSecret" ) var allWatchFields = []string{ @@ -223,6 +225,7 @@ var allWatchFields = []string{ tlsAPIInternalField, tlsAPIPublicField, tlsOVNField, + httpdCustomServiceConfigSecretField, } // SetupWithManager - @@ -287,6 +290,18 @@ func (r *NeutronAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma return err } + // index httpdOverrideSecretField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &neutronv1beta1.NeutronAPI{}, httpdCustomServiceConfigSecretField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*neutronv1beta1.NeutronAPI) + if cr.Spec.HttpdCustomization.CustomConfigSecret == nil { + return nil + } + return []string{*cr.Spec.HttpdCustomization.CustomConfigSecret} + }); err != nil { + return err + } + crs := &neutronv1beta1.NeutronAPIList{} return ctrl.NewControllerManagedBy(mgr). For(&neutronv1beta1.NeutronAPI{}). @@ -1545,6 +1560,14 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( return err } + httpdOverrideSecret := &corev1.Secret{} + if instance.Spec.HttpdCustomization.CustomConfigSecret != nil && *instance.Spec.HttpdCustomization.CustomConfigSecret != "" { + httpdOverrideSecret, _, err = secret.GetSecret(ctx, h, *instance.Spec.HttpdCustomization.CustomConfigSecret, instance.Namespace) + if err != nil { + return err + } + } + templateParameters := make(map[string]interface{}) templateParameters["ServiceUser"] = instance.Spec.ServiceUser templateParameters["KeystoneInternalURL"] = keystoneInternalURL @@ -1598,6 +1621,7 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( } // create httpd vhost template parameters + customTemplates := map[string]string{} httpdVhostConfig := map[string]interface{}{} for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { endptConfig := map[string]interface{}{} @@ -1608,11 +1632,28 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( endptConfig["SSLCertificateFile"] = fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String()) endptConfig["SSLCertificateKeyFile"] = fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String()) } + + endptConfig["Override"] = false + if len(httpdOverrideSecret.Data) > 0 { + endptConfig["Override"] = true + for key, data := range httpdOverrideSecret.Data { + if len(data) > 0 { + customTemplates["httpd_custom_"+endpt.String()+"_"+key] = string(data) + } + } + } httpdVhostConfig[endpt.String()] = endptConfig } templateParameters["VHosts"] = httpdVhostConfig + // 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) + secrets := []util.Template{ { Name: fmt.Sprintf("%s-config", instance.Name), @@ -1634,7 +1675,8 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( "10-neutron-httpd.conf": "/neutronapi/httpd/10-neutron-httpd.conf", "ssl.conf": "/neutronapi/httpd/ssl.conf", }, - ConfigOptions: templateParameters, + ConfigOptions: templateParameters, + StringTemplate: customTemplates, }, } return secret.EnsureSecrets(ctx, h, instance, secrets, envVars) diff --git a/go.mod b/go.mod index e8e78f36..b318eb09 100644 --- a/go.mod +++ b/go.mod @@ -10,13 +10,14 @@ require ( github.com/onsi/gomega v1.34.1 github.com/openstack-k8s-operators/infra-operator/apis v0.5.1-0.20250108092548-58707fa645ce github.com/openstack-k8s-operators/keystone-operator/api v0.5.1-0.20250107165241-16c3ed8e549f - 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 github.com/openstack-k8s-operators/mariadb-operator/api v0.5.1-0.20250108071621-aa59f25c5b1a github.com/openstack-k8s-operators/neutron-operator/api v0.0.0-00010101000000-000000000000 github.com/openstack-k8s-operators/ovn-operator/api v0.5.1-0.20250107161655-560140ff3cb0 go.uber.org/zap v1.27.0 + 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 05ddc82f..f59dda7a 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,8 @@ github.com/openstack-k8s-operators/infra-operator/apis v0.5.1-0.20250108092548-5 github.com/openstack-k8s-operators/infra-operator/apis v0.5.1-0.20250108092548-58707fa645ce/go.mod h1:TDaE7BVQvJwJGFm33R6xcPTeF8LKAnMh+a1ho+YqJHs= github.com/openstack-k8s-operators/keystone-operator/api v0.5.1-0.20250107165241-16c3ed8e549f h1:jlUo93FAwlDll1bJRxJO5B1Vi3t3wCoHQuy5HEO96ME= github.com/openstack-k8s-operators/keystone-operator/api v0.5.1-0.20250107165241-16c3ed8e549f/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/neutronapi/config/neutron-httpd-config.json b/templates/neutronapi/config/neutron-httpd-config.json index 39b9d437..66f3ba69 100644 --- a/templates/neutronapi/config/neutron-httpd-config.json +++ b/templates/neutronapi/config/neutron-httpd-config.json @@ -37,6 +37,13 @@ "owner": "neutron:apache", "optional": true, "perm": "0644" + }, + { + "source": "/var/lib/config-data/default/httpd_custom_*", + "dest": "/etc/httpd/conf/", + "owner": "apache", + "perm": "0444", + "optional": true } ], "permissions": [ diff --git a/templates/neutronapi/httpd/10-neutron-httpd.conf b/templates/neutronapi/httpd/10-neutron-httpd.conf index fd73382c..79d56e2f 100644 --- a/templates/neutronapi/httpd/10-neutron-httpd.conf +++ b/templates/neutronapi/httpd/10-neutron-httpd.conf @@ -32,5 +32,10 @@ SSLCertificateFile "{{ $vhost.SSLCertificateFile }}" SSLCertificateKeyFile "{{ $vhost.SSLCertificateKeyFile }}" {{- end }} + +{{- if $vhost.Override }} + Include conf/httpd_custom_{{ $endpt }}_* +{{- end }} + {{ end }} diff --git a/test/functional/neutronapi_controller_test.go b/test/functional/neutronapi_controller_test.go index 26ccb1c4..1a3d97cc 100644 --- a/test/functional/neutronapi_controller_test.go +++ b/test/functional/neutronapi_controller_test.go @@ -27,6 +27,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" //revive:disable-next-line:dot-imports + "github.com/openstack-k8s-operators/lib-common/modules/common" "github.com/openstack-k8s-operators/lib-common/modules/common/service" . "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers" @@ -1458,6 +1459,73 @@ func getNeutronAPIControllerSuite(ml2MechanismDrivers []string) func() { }) + When("A NeutronAPI is created with HttpdCustomization.CustomConfigSecret", func() { + BeforeEach(func() { + customServiceConfigSecretName := types.NamespacedName{Name: "foo", Namespace: namespace} + customConfig := []byte(`CustomParam "foo" + CustomKeystonePublicURL "{{ .KeystonePublicURL }}"`) + th.CreateSecret( + customServiceConfigSecretName, + map[string][]byte{ + "bar.conf": customConfig, + }, + ) + + spec["httpdCustomization"] = map[string]interface{}{ + "customConfigSecret": customServiceConfigSecretName.Name, + } + + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateNeutronAPISecret(namespace, SecretName)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetNeutronAPI(neutronAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + SimulateTransportURLReady(apiTransportURLName) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + infra.SimulateMemcachedReady(memcachedName) + DeferCleanup(DeleteOVNDBClusters, CreateOVNDBClusters(namespace)) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(namespace)) + mariadb.SimulateMariaDBAccountCompleted(types.NamespacedName{Namespace: namespace, Name: GetNeutronAPI(neutronAPIName).Spec.DatabaseAccount}) + mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: neutronapi.DatabaseCRName}) + th.SimulateJobSuccess(neutronDBSyncJobName) + keystone.SimulateKeystoneServiceReady(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + keystone.SimulateKeystoneEndpointReady(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + }) + + It("it renders the custom template and adds it to the placement-config-data secret", func() { + secret := types.NamespacedName{ + Namespace: neutronAPIName.Namespace, + Name: fmt.Sprintf("%s-%s", neutronAPIName.Name, "config"), + } + scrt := th.GetSecret(secret) + Expect(scrt).ShouldNot(BeNil()) + Expect(scrt.Data).Should(HaveKey(common.TemplateParameters)) + configData := string(scrt.Data[common.TemplateParameters]) + keystonePublicURL := "http://keystone-public-openstack.testing" + Expect(configData).Should(ContainSubstring(fmt.Sprintf("KeystonePublicURL: %s", keystonePublicURL))) + + secret = types.NamespacedName{ + Namespace: neutronAPIName.Namespace, + Name: fmt.Sprintf("%s-%s", neutronAPIName.Name, "httpd-config"), + } + scrt = th.GetSecret(secret) + Expect(scrt).ShouldNot(BeNil()) + for _, cfg := range []string{"httpd_custom_internal_bar.conf", "httpd_custom_public_bar.conf"} { + Expect(scrt.Data).Should(HaveKey(cfg)) + configData := string(scrt.Data[cfg]) + Expect(configData).Should(ContainSubstring("CustomParam \"foo\"")) + Expect(configData).Should(ContainSubstring(fmt.Sprintf("CustomKeystonePublicURL \"%s\"", keystonePublicURL))) + } + }) + }) + // Run MariaDBAccount suite tests. these are pre-packaged ginkgo tests // that exercise standard account create / update patterns that should be // common to all controllers that ensure MariaDBAccount CRs.