diff --git a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index e48e77f2c..b4e92bd92 100644 --- a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -46,6 +46,29 @@ spec: items: type: string type: array + datasources: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array deployOnAllNodeSets: type: boolean edpmServiceType: diff --git a/api/v1beta1/common.go b/api/v1beta1/common.go index 13bc98a0a..5b86b32b6 100644 --- a/api/v1beta1/common.go +++ b/api/v1beta1/common.go @@ -24,8 +24,8 @@ import ( corev1 "k8s.io/api/core/v1" ) -// AnsibleVarsFromSource represents the source of a set of ConfigMaps/Secrets -type AnsibleVarsFromSource struct { +// DataSource represents the source of a set of ConfigMaps/Secrets +type DataSource struct { // An optional identifier to prepend to each key in the ConfigMap. Must be a C_IDENTIFIER. // +optional Prefix string `json:"prefix,omitempty" protobuf:"bytes,1,opt,name=prefix"` @@ -55,7 +55,7 @@ type AnsibleOpts struct { // AnsibleVarsFrom is a list of sources to populate ansible variables from. // Values defined by an AnsibleVars with a duplicate key take precedence. // +kubebuilder:validation:Optional - AnsibleVarsFrom []AnsibleVarsFromSource `json:"ansibleVarsFrom,omitempty"` + AnsibleVarsFrom []DataSource `json:"ansibleVarsFrom,omitempty"` // AnsiblePort SSH port for Ansible connection // +kubebuilder:validation:Optional diff --git a/api/v1beta1/openstackdataplaneservice_types.go b/api/v1beta1/openstackdataplaneservice_types.go index 9c8830d87..ca4b16326 100644 --- a/api/v1beta1/openstackdataplaneservice_types.go +++ b/api/v1beta1/openstackdataplaneservice_types.go @@ -66,6 +66,10 @@ type OpenStackDataPlaneServiceSpec struct { // +kubebuilder:validation:Optional Secrets []string `json:"secrets,omitempty"` + // DataSources list of DataSource objects to mount as ExtraMounts for the + // OpenStackAnsibleEE + DataSources []DataSource `json:"datasources,omitempty"` + // TLSCert tls certs to be generated // +kubebuilder:validation:Optional TLSCert *OpenstackDataPlaneServiceCert `json:"tlsCert,omitempty" yaml:"tlsCert,omitempty"` diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 236e3e56e..7afac91ff 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -105,7 +105,7 @@ func (in *AnsibleOpts) DeepCopyInto(out *AnsibleOpts) { } if in.AnsibleVarsFrom != nil { in, out := &in.AnsibleVarsFrom, &out.AnsibleVarsFrom - *out = make([]AnsibleVarsFromSource, len(*in)) + *out = make([]DataSource, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -123,7 +123,7 @@ func (in *AnsibleOpts) DeepCopy() *AnsibleOpts { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AnsibleVarsFromSource) DeepCopyInto(out *AnsibleVarsFromSource) { +func (in *DataSource) DeepCopyInto(out *DataSource) { *out = *in if in.ConfigMapRef != nil { in, out := &in.ConfigMapRef, &out.ConfigMapRef @@ -137,12 +137,12 @@ func (in *AnsibleVarsFromSource) DeepCopyInto(out *AnsibleVarsFromSource) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnsibleVarsFromSource. -func (in *AnsibleVarsFromSource) DeepCopy() *AnsibleVarsFromSource { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSource. +func (in *DataSource) DeepCopy() *DataSource { if in == nil { return nil } - out := new(AnsibleVarsFromSource) + out := new(DataSource) in.DeepCopyInto(out) return out } @@ -674,6 +674,13 @@ func (in *OpenStackDataPlaneServiceSpec) DeepCopyInto(out *OpenStackDataPlaneSer *out = make([]string, len(*in)) copy(*out, *in) } + if in.DataSources != nil { + in, out := &in.DataSources, &out.DataSources + *out = make([]DataSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.TLSCert != nil { in, out := &in.TLSCert, &out.TLSCert *out = new(OpenstackDataPlaneServiceCert) diff --git a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index e48e77f2c..b4e92bd92 100644 --- a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -46,6 +46,29 @@ spec: items: type: string type: array + datasources: + items: + properties: + configMapRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + type: string + secretRef: + properties: + name: + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array deployOnAllNodeSets: type: boolean edpmServiceType: diff --git a/controllers/openstackdataplanenodeset_controller.go b/controllers/openstackdataplanenodeset_controller.go index c67dafefe..b3d47fb2b 100644 --- a/controllers/openstackdataplanenodeset_controller.go +++ b/controllers/openstackdataplanenodeset_controller.go @@ -539,7 +539,7 @@ func (r *OpenStackDataPlaneNodeSetReconciler) SetupWithManager(mgr ctrl.Manager) nodeSet := rawObj.(*dataplanev1.OpenStackDataPlaneNodeSet) configMaps := make([]string, 0) - appendConfigMaps := func(varsFrom []dataplanev1.AnsibleVarsFromSource) { + appendConfigMaps := func(varsFrom []dataplanev1.DataSource) { for _, ref := range varsFrom { if ref.ConfigMapRef != nil { configMaps = append(configMaps, ref.ConfigMapRef.Name) @@ -566,7 +566,7 @@ func (r *OpenStackDataPlaneNodeSetReconciler) SetupWithManager(mgr ctrl.Manager) secrets = append(secrets, nodeSet.Spec.NodeTemplate.AnsibleSSHPrivateKeySecret) } - appendSecrets := func(varsFrom []dataplanev1.AnsibleVarsFromSource) { + appendSecrets := func(varsFrom []dataplanev1.DataSource) { for _, ref := range varsFrom { if ref.SecretRef != nil { secrets = append(secrets, ref.SecretRef.Name) diff --git a/docs/assemblies/custom_resources.adoc b/docs/assemblies/custom_resources.adoc index 8d38f344c..644440edc 100644 --- a/docs/assemblies/custom_resources.adoc +++ b/docs/assemblies/custom_resources.adoc @@ -10,7 +10,7 @@ * <> * <> -* <> +* <> * <> * <> * <> @@ -110,7 +110,7 @@ AnsibleOpts defines a logical grouping of Ansible related configuration options. | ansibleVarsFrom | AnsibleVarsFrom is a list of sources to populate ansible variables from. Values defined by an AnsibleVars with a duplicate key take precedence. -| []<> +| []<> | false | ansiblePort @@ -121,10 +121,10 @@ AnsibleOpts defines a logical grouping of Ansible related configuration options. <> -[#ansiblevarsfromsource] -==== AnsibleVarsFromSource +[#datasource] +==== DataSource -AnsibleVarsFromSource represents the source of a set of ConfigMaps/Secrets +DataSource represents the source of a set of ConfigMaps/Secrets |=== | Field | Description | Scheme | Required @@ -309,6 +309,11 @@ OpenStackDataPlaneServiceSpec defines the desired state of OpenStackDataPlaneSer | []string | false +| datasources +| DataSources list of DataSource objects to mount as ExtraMounts for the OpenStackAnsibleEE +| []<> +| false + | tlsCert | TLSCert tls certs to be generated | *<> diff --git a/docs/assemblies/proc_creating-a-custom-service.adoc b/docs/assemblies/proc_creating-a-custom-service.adoc index ba0159cb1..f00f1bb50 100644 --- a/docs/assemblies/proc_creating-a-custom-service.adoc +++ b/docs/assemblies/proc_creating-a-custom-service.adoc @@ -68,7 +68,7 @@ spec: . Optional: Designate and configure a node set for a Compute feature or workload. For more information, see xref:proc_configuring-a-node-set-for-a-Compute-feature-or-workload_dataplane[Configuring a node set for a Compute feature or workload]. -. Optional: Specify the names of `Secret` resources to use to pass secrets into the `OpenStackAnsibleEE` job: +. Optional: Specify `Secret` resources to use to pass secrets into the `OpenStackAnsibleEE` job. Secrets are specified with a `name` and `required` field. When `required` is false, the service deployment will not fail if the secret doesn't exist. + ---- apiVersion: dataplane.openstack.org/v1beta1 @@ -80,8 +80,10 @@ spec: play: | ... secrets: - - hello-world-secret-0 - - hello-world-secret-1 + - name: hello-world-secret-0 + required: true + - name: hello-world-secret-1 + required: false ---- + A mount is created for each `secret` in the `OpenStackAnsibleEE` pod with a filename that matches the `secret` value. The mounts are created under `/var/lib/openstack/configs/`. diff --git a/pkg/deployment/deployment.go b/pkg/deployment/deployment.go index 6ab8e88c4..00ce97a1f 100644 --- a/pkg/deployment/deployment.go +++ b/pkg/deployment/deployment.go @@ -39,6 +39,7 @@ import ( ansibleeev1 "github.com/openstack-k8s-operators/openstack-ansibleee-operator/api/v1beta1" openstackv1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" ) // Deployer defines a data structure with all of the relevant objects required for a full deployment. @@ -361,14 +362,44 @@ func (d *Deployer) addServiceExtraMounts( client := d.Helper.GetClient() baseMountPath := path.Join(ConfigPaths, service.Name) - for _, cmName := range service.Spec.ConfigMaps { + var configMaps []*v1.ConfigMap + var secrets []*v1.Secret - volMounts := storage.VolMounts{} + for _, dataSource := range service.Spec.DataSources { + _cm, _secret, err := dataplaneutil.GetDataSourceCmSecret(d.Ctx, d.Helper, service.Namespace, dataSource) + if err != nil { + return nil, err + } + + if _cm != nil { + configMaps = append(configMaps, _cm) + } + if _secret != nil { + secrets = append(secrets, _secret) + } + } + + for _, cmName := range service.Spec.ConfigMaps { cm := &corev1.ConfigMap{} err := client.Get(d.Ctx, types.NamespacedName{Name: cmName, Namespace: service.Namespace}, cm) if err != nil { return d.AeeSpec, err } + configMaps = append(configMaps, cm) + } + + for _, secretName := range service.Spec.Secrets { + sec := &corev1.Secret{} + err := client.Get(d.Ctx, types.NamespacedName{Name: secretName, Namespace: service.Namespace}, sec) + if err != nil { + return d.AeeSpec, err + } + secrets = append(secrets, sec) + } + + for _, cm := range configMaps { + + volMounts := storage.VolMounts{} keys := []string{} for key := range cm.Data { @@ -380,13 +411,13 @@ func (d *Deployer) addServiceExtraMounts( sort.Strings(keys) for idx, key := range keys { - name := fmt.Sprintf("%s-%s", cmName, strconv.Itoa(idx)) + name := fmt.Sprintf("%s-%s", cm.Name, strconv.Itoa(idx)) volume := corev1.Volume{ Name: name, VolumeSource: corev1.VolumeSource{ ConfigMap: &corev1.ConfigMapVolumeSource{ LocalObjectReference: corev1.LocalObjectReference{ - Name: cmName, + Name: cm.Name, }, Items: []corev1.KeyToPath{ { @@ -412,15 +443,9 @@ func (d *Deployer) addServiceExtraMounts( d.AeeSpec.ExtraMounts = append(d.AeeSpec.ExtraMounts, volMounts) } - for _, secretName := range service.Spec.Secrets { + for _, sec := range secrets { volMounts := storage.VolMounts{} - sec := &corev1.Secret{} - err := client.Get(d.Ctx, types.NamespacedName{Name: secretName, Namespace: service.Namespace}, sec) - if err != nil { - return d.AeeSpec, err - } - keys := []string{} for key := range sec.Data { keys = append(keys, key) @@ -428,12 +453,12 @@ func (d *Deployer) addServiceExtraMounts( sort.Strings(keys) for idx, key := range keys { - name := fmt.Sprintf("%s-%s", secretName, strconv.Itoa(idx)) + name := fmt.Sprintf("%s-%s", sec.Name, strconv.Itoa(idx)) volume := corev1.Volume{ Name: name, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, + SecretName: sec.Name, Items: []corev1.KeyToPath{ { Key: key, @@ -457,5 +482,6 @@ func (d *Deployer) addServiceExtraMounts( d.AeeSpec.ExtraMounts = append(d.AeeSpec.ExtraMounts, volMounts) } + return d.AeeSpec, nil } diff --git a/pkg/deployment/hashes.go b/pkg/deployment/hashes.go index 35f717853..ac514b1ef 100644 --- a/pkg/deployment/hashes.go +++ b/pkg/deployment/hashes.go @@ -20,6 +20,7 @@ import ( "context" dataplanev1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1" + dataplaneutil "github.com/openstack-k8s-operators/dataplane-operator/pkg/util" "github.com/openstack-k8s-operators/lib-common/modules/common/configmap" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/secret" @@ -48,6 +49,27 @@ func GetDeploymentHashesForService( helper.GetLogger().Error(err, "Unable to retrieve OpenStackDataPlaneService %v") return err } + + for _, dataSource := range service.Spec.DataSources { + cm, sec, err := dataplaneutil.GetDataSourceCmSecret(ctx, helper, namespace, dataSource) + if err != nil { + return err + } + + if cm != nil { + configMapHashes[cm.Name], err = configmap.Hash(cm) + if err != nil { + helper.GetLogger().Error(err, "Unable to hash ConfigMap %v") + } + } + if sec != nil { + secretHashes[sec.Name], err = secret.Hash(sec) + if err != nil { + helper.GetLogger().Error(err, "Unable to hash Secret %v") + } + } + } + for _, cmName := range service.Spec.ConfigMaps { namespacedName := types.NamespacedName{ Name: cmName, diff --git a/pkg/deployment/inventory.go b/pkg/deployment/inventory.go index 0633060ac..b9870968a 100644 --- a/pkg/deployment/inventory.go +++ b/pkg/deployment/inventory.go @@ -25,11 +25,9 @@ import ( "strings" yaml "gopkg.in/yaml.v3" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" dataplanev1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1" + "github.com/openstack-k8s-operators/dataplane-operator/pkg/util" infranetworkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/ansible" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" @@ -43,57 +41,34 @@ func getAnsibleVarsFrom(ctx context.Context, helper *helper.Helper, namespace st var result = make(map[string]string) - client := helper.GetClient() - - // AnsibleVars will override AnsibleVarsFrom variables. - // Process AnsibleVarsFrom first then allow AnsibleVars to replace existing values. - for _, varFrom := range ansible.AnsibleVarsFrom { - switch { - case varFrom.ConfigMapRef != nil: - cm := varFrom.ConfigMapRef - optional := cm.Optional != nil && *cm.Optional - configMap := &v1.ConfigMap{} - err := client.Get(ctx, types.NamespacedName{Name: cm.Name, Namespace: namespace}, configMap) - if err != nil { - if errors.IsNotFound(err) && optional { - // ignore error when marked optional - utils.LogErrorForObject(helper, err, "could not get ansible vars, the configMap: "+cm.Name+"is missing", configMap) - continue - } - return result, err - } + for _, dataSource := range ansible.AnsibleVarsFrom { + configMap, secret, err := util.GetDataSourceCmSecret(ctx, helper, namespace, dataSource) + if err != nil { + return result, err + } + // AnsibleVars will override AnsibleVarsFrom variables. + // Process AnsibleVarsFrom first then allow AnsibleVars to replace existing values. + if configMap != nil { for k, v := range configMap.Data { - if len(varFrom.Prefix) > 0 { - k = varFrom.Prefix + k + if len(dataSource.Prefix) > 0 { + k = dataSource.Prefix + k } result[k] = v } + } - case varFrom.SecretRef != nil: - s := varFrom.SecretRef - optional := s.Optional != nil && *s.Optional - secret := &v1.Secret{} - err := client.Get(ctx, types.NamespacedName{Name: s.Name, Namespace: namespace}, secret) - if err != nil { - if errors.IsNotFound(err) && optional { - // ignore error when marked optional - utils.LogErrorForObject(helper, err, "could not get ansible vars, the secret: "+s.Name+"is missing", secret) - continue - } - return result, err - } - + if secret != nil { for k, v := range secret.Data { - if len(varFrom.Prefix) > 0 { - k = varFrom.Prefix + k + if len(dataSource.Prefix) > 0 { + k = dataSource.Prefix + k } result[k] = string(v) } } - } + } return result, nil } diff --git a/pkg/util/datasource.go b/pkg/util/datasource.go new file mode 100644 index 000000000..4c4768a45 --- /dev/null +++ b/pkg/util/datasource.go @@ -0,0 +1,72 @@ +/* +Copyright 2024. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + + dataplanev1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + utils "github.com/openstack-k8s-operators/lib-common/modules/common/util" + v1 "k8s.io/api/core/v1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" +) + +// GetDataSourceCmSecrets gets the ConfigMaps and Secrets from a DataSource +func GetDataSourceCmSecret(ctx context.Context, helper *helper.Helper, namespace string, dataSource dataplanev1.DataSource) (*v1.ConfigMap, *v1.Secret, error) { + + var configMap *v1.ConfigMap + var secret *v1.Secret + + client := helper.GetClient() + + switch { + case dataSource.ConfigMapRef != nil: + cm := dataSource.ConfigMapRef + optional := cm.Optional != nil && *cm.Optional + configMap = &v1.ConfigMap{} + err := client.Get(ctx, types.NamespacedName{Name: cm.Name, Namespace: namespace}, configMap) + if err != nil { + if k8s_errors.IsNotFound(err) && optional { + // ignore error when marked optional + utils.LogForObject(helper, "Optional ConfigMap not found", configMap) + } else { + utils.LogErrorForObject(helper, err, "Required ConfigMap not found", configMap) + return configMap, secret, err + } + } + + case dataSource.SecretRef != nil: + s := dataSource.SecretRef + optional := s.Optional != nil && *s.Optional + secret = &v1.Secret{} + err := client.Get(ctx, types.NamespacedName{Name: s.Name, Namespace: namespace}, secret) + if err != nil { + if k8s_errors.IsNotFound(err) && optional { + // ignore error when marked optional + utils.LogForObject(helper, "Optional Secret not found", secret) + } else { + utils.LogErrorForObject(helper, err, "Required Secret not found", secret) + return configMap, secret, err + } + } + + } + + return configMap, secret, nil +}