diff --git a/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml b/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml index 4e48feaf2..d8dc44d65 100644 --- a/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml +++ b/api/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml @@ -1876,6 +1876,8 @@ spec: items: type: string type: array + tlsEnabled: + type: boolean required: - nodeTemplate - nodes diff --git a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index 3adcfd10d..5d72ad2fc 100644 --- a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -30,10 +30,18 @@ spec: type: object spec: properties: + caCerts: + type: string configMaps: items: type: string type: array + hasTLSCerts: + type: boolean + issuers: + additionalProperties: + type: string + type: object label: maxLength: 53 pattern: '[a-z]([-a-z0-9]*[a-z0-9])' diff --git a/api/v1beta1/openstackdataplanenodeset_types.go b/api/v1beta1/openstackdataplanenodeset_types.go index 42f4fe46c..693886f20 100644 --- a/api/v1beta1/openstackdataplanenodeset_types.go +++ b/api/v1beta1/openstackdataplanenodeset_types.go @@ -59,6 +59,11 @@ type OpenStackDataPlaneNodeSetSpec struct { // +kubebuilder:default={download-cache,bootstrap,configure-network,validate-network,install-os,configure-os,run-os,ovn,neutron-metadata,libvirt,nova,telemetry} // Services list Services []string `json:"services"` + + // TLSEnabled - Whether the node set has TLS enabled. + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + TLSEnabled *bool `json:"tlsEnabled,omitempty" yaml:"tlsEnabled,omitempty"` } //+kubebuilder:object:root=true diff --git a/api/v1beta1/openstackdataplaneservice_types.go b/api/v1beta1/openstackdataplaneservice_types.go index c8d82238b..5867c6a26 100644 --- a/api/v1beta1/openstackdataplaneservice_types.go +++ b/api/v1beta1/openstackdataplaneservice_types.go @@ -76,6 +76,19 @@ type OpenStackDataPlaneServiceSpec struct { // OpenStackAnsibleEERunnerImage image to use as the ansibleEE runner image // +kubebuilder:validation:Optional OpenStackAnsibleEERunnerImage string `json:"openStackAnsibleEERunnerImage,omitempty" yaml:"openStackAnsibleEERunnerImage,omitempty"` + + // HasTLSCerts - Whether the nodes have TLS certs + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec,xDescriptors={"urn:alm:descriptor:com.tectonic.ui:booleanSwitch"} + HasTLSCerts *bool `json:"hasTLSCerts,omitempty" yaml:"hasTLSCerts,omitempty"` + + // Issuers - Issuers to issue TLS Certificates + // +kubebuilder:validation:Optional + Issuers map[string]string `json:"issuers,omitempty"` + + // CACerts - Secret containing the CA certificate chain + // +kubebuilder:validation:Optional + CACerts string `json:"caCerts,omitempty" yaml:"caCerts,omitempty"` } // OpenStackDataPlaneServiceStatus defines the observed state of OpenStackDataPlaneService diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index a87c87918..812885656 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -403,6 +403,11 @@ func (in *OpenStackDataPlaneNodeSetSpec) DeepCopyInto(out *OpenStackDataPlaneNod *out = make([]string, len(*in)) copy(*out, *in) } + if in.TLSEnabled != nil { + in, out := &in.TLSEnabled, &out.TLSEnabled + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDataPlaneNodeSetSpec. @@ -519,6 +524,18 @@ func (in *OpenStackDataPlaneServiceSpec) DeepCopyInto(out *OpenStackDataPlaneSer *out = make([]string, len(*in)) copy(*out, *in) } + if in.HasTLSCerts != nil { + in, out := &in.HasTLSCerts, &out.HasTLSCerts + *out = new(bool) + **out = **in + } + if in.Issuers != nil { + in, out := &in.Issuers, &out.Issuers + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenStackDataPlaneServiceSpec. diff --git a/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml b/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml index 4e48feaf2..d8dc44d65 100644 --- a/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml +++ b/config/crd/bases/dataplane.openstack.org_openstackdataplanenodesets.yaml @@ -1876,6 +1876,8 @@ spec: items: type: string type: array + tlsEnabled: + type: boolean required: - nodeTemplate - nodes diff --git a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index 3adcfd10d..5d72ad2fc 100644 --- a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -30,10 +30,18 @@ spec: type: object spec: properties: + caCerts: + type: string configMaps: items: type: string type: array + hasTLSCerts: + type: boolean + issuers: + additionalProperties: + type: string + type: object label: maxLength: 53 pattern: '[a-z]([-a-z0-9]*[a-z0-9])' diff --git a/config/manifests/bases/dataplane-operator.clusterserviceversion.yaml b/config/manifests/bases/dataplane-operator.clusterserviceversion.yaml index b0345abdc..694c25029 100644 --- a/config/manifests/bases/dataplane-operator.clusterserviceversion.yaml +++ b/config/manifests/bases/dataplane-operator.clusterserviceversion.yaml @@ -77,6 +77,12 @@ spec: displayName: OpenStack Data Plane Service kind: OpenStackDataPlaneService name: openstackdataplaneservices.dataplane.openstack.org + specDescriptors: + - description: HasTLSCerts - Whether the nodes have TLS certs + displayName: Has TLSCerts + path: hasTLSCerts + x-descriptors: + - urn:alm:descriptor:com.tectonic.ui:booleanSwitch statusDescriptors: - description: Conditions displayName: Conditions diff --git a/config/services/dataplane_v1beta1_openstackdataplaneservice_libvirt.yaml b/config/services/dataplane_v1beta1_openstackdataplaneservice_libvirt.yaml index 8c877210e..4ce2f400f 100644 --- a/config/services/dataplane_v1beta1_openstackdataplaneservice_libvirt.yaml +++ b/config/services/dataplane_v1beta1_openstackdataplaneservice_libvirt.yaml @@ -5,3 +5,6 @@ metadata: spec: label: libvirt playbook: osp.edpm.libvirt + hasTLSCerts: True + issuers: + default: osp-rootca-issuer-internal diff --git a/config/services/dataplane_v1beta1_openstackdataplaneservice_nova.yaml b/config/services/dataplane_v1beta1_openstackdataplaneservice_nova.yaml index e13a2ef0c..70c3e9fbd 100644 --- a/config/services/dataplane_v1beta1_openstackdataplaneservice_nova.yaml +++ b/config/services/dataplane_v1beta1_openstackdataplaneservice_nova.yaml @@ -11,3 +11,6 @@ spec: # and ssh-publickey - nova-migration-ssh-key playbook: osp.edpm.nova + hasTLSCerts: True + issuers: + default: osp-rootca-issuer-internal diff --git a/controllers/openstackdataplanenodeset_controller.go b/controllers/openstackdataplanenodeset_controller.go index 97c54183e..3a79256db 100644 --- a/controllers/openstackdataplanenodeset_controller.go +++ b/controllers/openstackdataplanenodeset_controller.go @@ -200,7 +200,7 @@ func (r *OpenStackDataPlaneNodeSetReconciler) Reconcile(ctx context.Context, req } // Ensure DNSData Required for Nodes - dnsAddresses, dnsClusterAddresses, ctlplaneSearchDomain, isReady, err := deployment.EnsureDNSData( + dnsAddresses, dnsClusterAddresses, ctlplaneSearchDomain, isReady, allHostnames, allIPs, err := deployment.EnsureDNSData( ctx, helper, instance, allIPSets) if err != nil || !isReady { @@ -209,6 +209,24 @@ func (r *OpenStackDataPlaneNodeSetReconciler) Reconcile(ctx context.Context, req instance.Status.DNSClusterAddresses = dnsClusterAddresses instance.Status.CtlplaneSearchDomain = ctlplaneSearchDomain + // Issue certs for TLS for services that need them + if instance.Spec.TLSEnabled != nil && *instance.Spec.TLSEnabled { + for _, serviceName := range instance.Spec.Services { + service, err := deployment.GetService(ctx, helper, serviceName) + if err != nil { + return ctrl.Result{}, err + } + if service.Spec.HasTLSCerts != nil && *service.Spec.HasTLSCerts { + result, err = deployment.EnsureTLSCerts(ctx, helper, instance, allHostnames, allIPs, service) + if err != nil { + return ctrl.Result{}, err + } else if (result != ctrl.Result{}) { + return result, nil + } + } + } + } + ansibleSSHPrivateKeySecret := instance.Spec.NodeTemplate.AnsibleSSHPrivateKeySecret secretKeys := []string{} diff --git a/docs/openstack_dataplanenodeset.md b/docs/openstack_dataplanenodeset.md index e8c3e0a5f..d5bcef245 100644 --- a/docs/openstack_dataplanenodeset.md +++ b/docs/openstack_dataplanenodeset.md @@ -129,6 +129,7 @@ OpenStackDataPlaneNodeSetSpec defines the desired state of OpenStackDataPlaneNod | env | Env is a list containing the environment variables to pass to the pod | []corev1.EnvVar | false | | networkAttachments | NetworkAttachments is a list of NetworkAttachment resource names to pass to the ansibleee resource which allows to connect the ansibleee runner to the given network | []string | false | | services | Services list | []string | true | +| tlsEnabled | TLSEnabled - Whether the node set has TLS enabled. | *bool | false | [Back to Custom Resources](#custom-resources) diff --git a/docs/openstack_dataplaneservice.md b/docs/openstack_dataplaneservice.md index d59b7fbee..0666b6d1f 100644 --- a/docs/openstack_dataplaneservice.md +++ b/docs/openstack_dataplaneservice.md @@ -125,6 +125,9 @@ OpenStackDataPlaneServiceSpec defines the desired state of OpenStackDataPlaneSer | configMaps | ConfigMaps list of ConfigMap names to mount as ExtraMounts for the OpenStackAnsibleEE | []string | false | | secrets | Secrets list of Secret names to mount as ExtraMounts for the OpenStackAnsibleEE | []string | false | | openStackAnsibleEERunnerImage | OpenStackAnsibleEERunnerImage image to use as the ansibleEE runner image | string | false | +| hasTLSCerts | HasTLSCerts - Whether the nodes have TLS certs | *bool | false | +| issuers | Issuers - Issuers to issue TLS Certificates | map[string]string | false | +| caCerts | CACerts - Secret containing the CA certificate chain | string | false | [Back to Custom Resources](#custom-resources) diff --git a/go.mod b/go.mod index 6278c48b9..4141671f6 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ replace golang.org/x/net => golang.org/x/net v0.18.0 //allow-merging replace github.com/openstack-k8s-operators/dataplane-operator/api => ./api require ( + github.com/cert-manager/cert-manager v1.11.5 github.com/go-logr/logr v1.3.0 github.com/google/uuid v1.4.0 github.com/k8snetworkplumbingwg/network-attachment-definition-client v1.4.0 @@ -16,6 +17,7 @@ require ( github.com/openstack-k8s-operators/dataplane-operator/api v0.0.0-20230724101130-2d6fe1f4706b github.com/openstack-k8s-operators/infra-operator/apis v0.3.1-0.20231122104142-3b449040167e github.com/openstack-k8s-operators/lib-common/modules/ansible v0.3.1-0.20231128145648-956f4d361a63 + github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.0.0-20231109064837-a0ac89bc5a39 github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231128145648-956f4d361a63 github.com/openstack-k8s-operators/lib-common/modules/storage v0.3.1-0.20231128145648-956f4d361a63 github.com/openstack-k8s-operators/lib-common/modules/test v0.3.1-0.20231128145648-956f4d361a63 @@ -26,6 +28,7 @@ require ( k8s.io/api v0.26.11 k8s.io/apimachinery v0.26.11 k8s.io/client-go v0.26.11 + k8s.io/utils v0.0.0-20231127182322-b307cd553661 sigs.k8s.io/controller-runtime v0.14.7 ) @@ -84,7 +87,7 @@ require ( k8s.io/component-base v0.26.11 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/kube-openapi v0.0.0-20230515203736-54b630e78af5 // indirect - k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect + sigs.k8s.io/gateway-api v0.6.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect diff --git a/go.sum b/go.sum index 08a08d56a..7973460f0 100644 --- a/go.sum +++ b/go.sum @@ -47,6 +47,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cert-manager/cert-manager v1.11.5 h1:K2LurvwIE4hIhODQZnkOW6ljYe3lVMAliS/to+gI05o= +github.com/cert-manager/cert-manager v1.11.5/go.mod h1:zNOyoTEwdn9Rtj5Or2pjBY1Bqwtw4vBElP2fKSP8/g8= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -239,6 +241,8 @@ github.com/openstack-k8s-operators/infra-operator/apis v0.3.1-0.20231122104142-3 github.com/openstack-k8s-operators/infra-operator/apis v0.3.1-0.20231122104142-3b449040167e/go.mod h1:FnKU6sravC43Uj0iq2bhZaPMjoPCBhkNlVdiVoGi5/E= github.com/openstack-k8s-operators/lib-common/modules/ansible v0.3.1-0.20231128145648-956f4d361a63 h1:Dm2f2i/GDDIpxk1gjhFD1fhHT3naXbXCUmLhyv/jBqk= github.com/openstack-k8s-operators/lib-common/modules/ansible v0.3.1-0.20231128145648-956f4d361a63/go.mod h1:A9sWNibvjr1a9B/mpy4k6J9xkH11fnn0Dx/X1EZ3On8= +github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.0.0-20231109064837-a0ac89bc5a39 h1:f6F22jZ6HNBdlrTBDziaoWM1HqW2LOME3nq+07SuC+s= +github.com/openstack-k8s-operators/lib-common/modules/certmanager v0.0.0-20231109064837-a0ac89bc5a39/go.mod h1:Gr8E0kTkczsoUJ1AIzj9Z5vhl6V21ZrNJXICMB527qI= github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231128145648-956f4d361a63 h1:iA/8vt+o2bMxYvvenNB7VArBvM8UyDLw3G7S/teMLc0= github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231128145648-956f4d361a63/go.mod h1:OYad2L+OD4j5CR49di7gu3Q1UkLBmpYwvtdoGlnasL4= github.com/openstack-k8s-operators/lib-common/modules/storage v0.3.1-0.20231128145648-956f4d361a63 h1:ok420+r0QGypb4ORk2Zi4k9i0pgXjMZHQ1w/6zgxyrE= @@ -643,6 +647,8 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/controller-runtime v0.14.7 h1:Vrnm2vk9ZFlRkXATHz0W0wXcqNl7kPat8q2JyxVy0Q8= sigs.k8s.io/controller-runtime v0.14.7/go.mod h1:ErTs3SJCOujNUnTz4AS+uh8hp6DHMo1gj6fFndJT1X8= +sigs.k8s.io/gateway-api v0.6.0 h1:v2FqrN2ROWZLrSnI2o91taHR8Sj3s+Eh3QU7gLNWIqA= +sigs.k8s.io/gateway-api v0.6.0/go.mod h1:EYJT+jlPWTeNskjV0JTki/03WX1cyAnBhwBJfYHpV/0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/main.go b/main.go index 83f972794..0d104d3ba 100644 --- a/main.go +++ b/main.go @@ -41,6 +41,8 @@ import ( baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client/config" + certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certmgrmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" dataplanev1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1" dataplanev1beta1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1" "github.com/openstack-k8s-operators/dataplane-operator/controllers" @@ -61,6 +63,8 @@ func init() { utilruntime.Must(baremetalv1.AddToScheme(scheme)) utilruntime.Must(infranetworkv1.AddToScheme(scheme)) utilruntime.Must(dataplanev1beta1.AddToScheme(scheme)) + utilruntime.Must(certmgrv1.AddToScheme(scheme)) + utilruntime.Must(certmgrmetav1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } diff --git a/pkg/deployment/cert.go b/pkg/deployment/cert.go new file mode 100644 index 000000000..05899230d --- /dev/null +++ b/pkg/deployment/cert.go @@ -0,0 +1,166 @@ +/* +Copyright 2023. + +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 deployment + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + dataplanev1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1" + infranetworkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" + "github.com/openstack-k8s-operators/lib-common/modules/certmanager" + "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/secret" +) + +// EnsureTLSCerts generates a secret containing all the certificates for the relevant service +// This secret will be mounted by the ansibleEE pod as an extra mount when the service is deployed. +func EnsureTLSCerts(ctx context.Context, helper *helper.Helper, + instance *dataplanev1.OpenStackDataPlaneNodeSet, + allHostnames map[string]map[infranetworkv1.NetNameStr]string, + allIPs map[string]map[infranetworkv1.NetNameStr]string, + service dataplanev1.OpenStackDataPlaneService) (ctrl.Result, error) { + + certsData := map[string][]byte{} + + // for each node in the nodeset, issue all the TLS certs needed based on the + // ips or DNS Names + for nodeName := range instance.Spec.Nodes { + var dnsNames map[infranetworkv1.NetNameStr]string + var ips map[infranetworkv1.NetNameStr]string + var secretName string + var certSecret *corev1.Secret = nil + var err error + var result ctrl.Result + + // TODO(alee) decide if we want to use other labels + // For now we just add the hostname so we can select all the certs on one node + labels := map[string]string{ + "hostname": nodeName, + "service": service.Name, + } + secretName = "cert-" + service.Name + "-" + nodeName + + dnsNames = allHostnames[nodeName] + ips = allIPs[nodeName] + + switch service.Name { + case "nova", "libvirt": + // nova and libvirt want a cert with ctlplane ip and dns name + hosts := []string{dnsNames[CtlPlaneNetwork]} + ctlIPs := []string{ips[CtlPlaneNetwork]} + certSecret, result, err = GetTLSNodeCert(ctx, helper, instance, secretName, + service.Spec.Issuers["default"], labels, + nodeName, hosts, ctlIPs, nil) + default: + // The default case provides a cert with all the dns names for the host. + // This will probably be sufficient for most services. If a service needs + // a different kind of cert (for example, containing ips, or using a different + // issuer) then add a case for the service in this switch statement + hosts := make([]string, 0, len(dnsNames)) + for _, host := range dnsNames { + hosts = append(hosts, host) + } + secretName = "cert-default-" + nodeName + certSecret, result, err = GetTLSNodeCert(ctx, helper, instance, secretName, + certmanager.RootCAIssuerInternalLabel, labels, + nodeName, hosts, nil, nil) + } + + // handle cert request errors + if (err != nil) || (result != ctrl.Result{}) { + return result, err + } + // TODO(alee) Add an owner reference to the secret so it can be monitored + // We'll do this once stuggi adds a function to do this in libcommon + + // To use this cert, add it to the relevant service data + certsData[nodeName+"-tls.key"] = certSecret.Data["tls.key"] + certsData[nodeName+"-tls.crt"] = certSecret.Data["tls.crt"] + } + + // create a secret to hold the certs for the service + serviceCertsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetServiceCertsSecretName(instance, service.Name), + Namespace: instance.Namespace, + }, + Data: certsData, + } + _, result, err := secret.CreateOrPatchSecret(ctx, helper, instance, serviceCertsSecret) + if err != nil { + err = fmt.Errorf("Error creating certs secret for %s - %w", service.Name, err) + return ctrl.Result{}, err + } else if result != controllerutil.OperationResultNone { + return ctrl.Result{RequeueAfter: time.Second * 5}, nil + } + + return ctrl.Result{}, nil +} + +// GetTLSNodeCert creates or retrieves the cert for a node for a given service +func GetTLSNodeCert(ctx context.Context, helper *helper.Helper, + instance *dataplanev1.OpenStackDataPlaneNodeSet, + secretName string, issuer string, + labels map[string]string, nodeName string, + hostnames []string, ips []string, usages []certmgrv1.KeyUsage) (*corev1.Secret, ctrl.Result, error) { + certSecret, _, err := secret.GetSecret(ctx, helper, secretName, instance.Namespace) + var result ctrl.Result + if err != nil { + if !k8serrors.IsNotFound(err) { + err = fmt.Errorf("Error retrieving secret %s - %w", secretName, err) + return nil, ctrl.Result{}, err + } + + duration := ptr.To(time.Hour * 24 * 365) + request := certmanager.CertificateRequest{ + IssuerName: issuer, + CertName: secretName, + Duration: duration, + Hostnames: hostnames, + Ips: ips, + Annotations: nil, + Labels: labels, + Usages: usages, + } + + certSecret, result, err = certmanager.EnsureCert(ctx, helper, request) + if err != nil { + return nil, ctrl.Result{}, err + } else if (result != ctrl.Result{}) { + return nil, result, nil + } + } + return certSecret, ctrl.Result{}, nil +} + +// GetServiceCertsSecretName - return name of secret to be mounted in ansibleEE which contains +// all the TLS certs for the relevant service +// The convention we use here is "--certs", so for example, +// openstack-epdm-nova-certs. +func GetServiceCertsSecretName(instance *dataplanev1.OpenStackDataPlaneNodeSet, serviceName string) string { + return fmt.Sprintf("%s-%s-certs", instance.Name, serviceName) +} diff --git a/pkg/deployment/const.go b/pkg/deployment/const.go index 2e19cc96a..04bc1342c 100644 --- a/pkg/deployment/const.go +++ b/pkg/deployment/const.go @@ -47,4 +47,10 @@ const ( // ConfigPaths base path for volume mounts in OpenStackAnsibleEE pod ConfigPaths = "/var/lib/openstack/configs" + + // CertPaths base path for cert volume mount in OpenStackAnsibleEE pod + CertPaths = "/var/lib/openstack/certs" + + // CACertPaths base path for CA cert volume mount in OpenStackAnsibleEE pod + CACertPaths = "/var/lib/openstack/cacerts" ) diff --git a/pkg/deployment/deployment.go b/pkg/deployment/deployment.go index 1252edd6b..9e07cbb4e 100644 --- a/pkg/deployment/deployment.go +++ b/pkg/deployment/deployment.go @@ -31,6 +31,7 @@ import ( dataplaneutil "github.com/openstack-k8s-operators/dataplane-operator/pkg/util" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" + "github.com/openstack-k8s-operators/lib-common/modules/common/tls" "github.com/openstack-k8s-operators/lib-common/modules/common/util" "github.com/openstack-k8s-operators/lib-common/modules/storage" ansibleeev1alpha1 "github.com/openstack-k8s-operators/openstack-ansibleee-operator/api/v1alpha1" @@ -81,11 +82,11 @@ func Deploy( // specific mounts. aeeSpec.ExtraMounts = make([]storage.VolMounts, len(aeeSpecMounts)) copy(aeeSpec.ExtraMounts, aeeSpecMounts) - aeeSpec, err = addServiceExtraMounts(ctx, helper, aeeSpec, foundService) - + aeeSpec, err = addServiceExtraMounts(ctx, helper, aeeSpec, foundService, nodeSet) if err != nil { return &ctrl.Result{}, err } + err = ConditionalDeploy( ctx, helper, @@ -225,6 +226,7 @@ func addServiceExtraMounts( helper *helper.Helper, aeeSpec dataplanev1.AnsibleEESpec, service dataplanev1.OpenStackDataPlaneService, + nodeSet *dataplanev1.OpenStackDataPlaneNodeSet, ) (dataplanev1.AnsibleEESpec, error) { client := helper.GetClient() baseMountPath := path.Join(ConfigPaths, service.Name) @@ -322,5 +324,64 @@ func addServiceExtraMounts( aeeSpec.ExtraMounts = append(aeeSpec.ExtraMounts, volMounts) } + + // Add mounts for TLS certs + if nodeSet.Spec.TLSEnabled != nil && *nodeSet.Spec.TLSEnabled { + if service.Spec.HasTLSCerts != nil && *service.Spec.HasTLSCerts { + volMounts := storage.VolMounts{} + secretName := GetServiceCertsSecretName(nodeSet, service.Name) + sec := &corev1.Secret{} + err := client.Get(ctx, types.NamespacedName{Name: secretName, Namespace: service.Namespace}, sec) + if err != nil { + return aeeSpec, err + } + volume := corev1.Volume{ + Name: secretName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, + }, + } + + volumeMount := corev1.VolumeMount{ + Name: secretName, + MountPath: CertPaths, + } + + volMounts.Volumes = append(volMounts.Volumes, volume) + volMounts.Mounts = append(volMounts.Mounts, volumeMount) + + // add mount for cacerts + var caCertSecretName string + if len(service.Spec.CACerts) > 0 { + caCertSecretName = service.Spec.CACerts + } else { + caCertSecretName = tls.CABundleLabel + } + + err = client.Get(ctx, types.NamespacedName{Name: caCertSecretName, Namespace: service.Namespace}, sec) + if err != nil { + return aeeSpec, err + } + volume = corev1.Volume{ + Name: caCertSecretName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: caCertSecretName, + }, + }, + } + + volumeMount = corev1.VolumeMount{ + Name: caCertSecretName, + MountPath: CACertPaths, + } + + volMounts.Volumes = append(volMounts.Volumes, volume) + volMounts.Mounts = append(volMounts.Mounts, volumeMount) + aeeSpec.ExtraMounts = append(aeeSpec.ExtraMounts, volMounts) + } + } return aeeSpec, nil } diff --git a/pkg/deployment/inventory.go b/pkg/deployment/inventory.go index 00c1469c2..9d6eea027 100644 --- a/pkg/deployment/inventory.go +++ b/pkg/deployment/inventory.go @@ -46,6 +46,12 @@ func GenerateNodeSetInventory(ctx context.Context, helper *helper.Helper, utils.LogErrorForObject(helper, err, "Could not resolve ansible group vars", instance) return "", err } + + // add TLS ansible variable + if instance.Spec.TLSEnabled != nil && *instance.Spec.TLSEnabled { + nodeSetGroup.Vars["tls_certs_enabled"] = "true" + } + for nodeName, node := range instance.Spec.Nodes { host := nodeSetGroup.AddHost(nodeName) // Use ansible_host if provided else use hostname. Fall back to diff --git a/pkg/deployment/ipam.go b/pkg/deployment/ipam.go index 1b34f4472..f12e1736e 100644 --- a/pkg/deployment/ipam.go +++ b/pkg/deployment/ipam.go @@ -67,16 +67,24 @@ func EnsureIPSets(ctx context.Context, helper *helper.Helper, // createOrPatchDNSData builds the DNSData func createOrPatchDNSData(ctx context.Context, helper *helper.Helper, instance *dataplanev1.OpenStackDataPlaneNodeSet, - allIPSets map[string]infranetworkv1.IPSet) (string, error) { + allIPSets map[string]infranetworkv1.IPSet) ( + string, map[string]map[infranetworkv1.NetNameStr]string, + map[string]map[infranetworkv1.NetNameStr]string, error) { var allDNSRecords []infranetworkv1.DNSHost var ctlplaneSearchDomain string + allHostnames := map[string]map[infranetworkv1.NetNameStr]string{} + allIPs := map[string]map[infranetworkv1.NetNameStr]string{} + // Build DNSData CR for nodeName, node := range instance.Spec.Nodes { var shortName string nets := node.Networks hostName := node.HostName + allHostnames[nodeName] = map[infranetworkv1.NetNameStr]string{} + allIPs[nodeName] = map[infranetworkv1.NetNameStr]string{} + if isFQDN(hostName) { shortName = strings.Split(hostName, ".")[0] } else { @@ -97,10 +105,13 @@ func createOrPatchDNSData(ctx context.Context, helper *helper.Helper, fqdnName := strings.Join([]string{shortName, res.DNSDomain}, ".") if fqdnName != hostName { fqdnNames = append(fqdnNames, fqdnName) + allHostnames[nodeName][res.Network] = fqdnName } if isFQDN(hostName) && res.Network == CtlPlaneNetwork { fqdnNames = append(fqdnNames, hostName) + allHostnames[nodeName][res.Network] = hostName } + allIPs[nodeName][res.Network] = res.Address dnsRecord.Hostnames = fqdnNames allDNSRecords = append(allDNSRecords, dnsRecord) // Adding only ctlplane domain for ansibleee. @@ -130,16 +141,18 @@ func createOrPatchDNSData(ctx context.Context, helper *helper.Helper, return err }) if err != nil { - return "", err + return "", allHostnames, allIPs, err } - return ctlplaneSearchDomain, nil + return ctlplaneSearchDomain, allHostnames, allIPs, nil } // EnsureDNSData Ensures DNSData is created func EnsureDNSData(ctx context.Context, helper *helper.Helper, instance *dataplanev1.OpenStackDataPlaneNodeSet, - allIPSets map[string]infranetworkv1.IPSet) ([]string, []string, string, bool, error) { + allIPSets map[string]infranetworkv1.IPSet) ( + []string, []string, string, bool, map[string]map[infranetworkv1.NetNameStr]string, + map[string]map[infranetworkv1.NetNameStr]string, error) { // Verify dnsmasq CR exists dnsAddresses, dnsClusterAddresses, isReady, err := CheckDNSService( @@ -161,17 +174,17 @@ func EnsureDNSData(ctx context.Context, helper *helper.Helper, if dnsAddresses == nil { instance.Status.Conditions.Remove(dataplanev1.NodeSetDNSDataReadyCondition) } - return nil, nil, "", isReady, err + return nil, nil, "", isReady, nil, nil, err } // Create or Patch DNSData - ctlplaneSearchDomain, err := createOrPatchDNSData( + ctlplaneSearchDomain, allHostnames, allIPs, err := createOrPatchDNSData( ctx, helper, instance, allIPSets) if err != nil { instance.Status.Conditions.MarkFalse( dataplanev1.NodeSetDNSDataReadyCondition, condition.ErrorReason, condition.SeverityError, dataplanev1.NodeSetDNSDataReadyErrorMessage) - return nil, nil, "", false, err + return nil, nil, "", false, nil, nil, err } dnsData := &infranetworkv1.DNSData{ @@ -187,7 +200,7 @@ func EnsureDNSData(ctx context.Context, helper *helper.Helper, dataplanev1.NodeSetDNSDataReadyCondition, condition.ErrorReason, condition.SeverityError, dataplanev1.NodeSetDNSDataReadyErrorMessage) - return nil, nil, "", false, err + return nil, nil, "", false, nil, nil, err } if !dnsData.IsReady() { @@ -196,12 +209,12 @@ func EnsureDNSData(ctx context.Context, helper *helper.Helper, dataplanev1.NodeSetDNSDataReadyCondition, condition.RequestedReason, condition.SeverityInfo, dataplanev1.NodeSetDNSDataReadyWaitingMessage) - return nil, nil, "", false, nil + return nil, nil, "", false, nil, nil, nil } instance.Status.Conditions.MarkTrue( dataplanev1.NodeSetDNSDataReadyCondition, dataplanev1.NodeSetDNSDataReadyMessage) - return dnsAddresses, dnsClusterAddresses, ctlplaneSearchDomain, true, nil + return dnsAddresses, dnsClusterAddresses, ctlplaneSearchDomain, true, allHostnames, allIPs, nil } // reserveIPs Reserves IPs by creating IPSets