From 76289136a3c83db8c13281bcf21ffd5f7f7c17b6 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Wed, 24 Apr 2024 16:42:16 +0200 Subject: [PATCH] [wip] nova novncproxy vencrypt support --- api/bases/nova.openstack.org_nova.yaml | 27 +- api/bases/nova.openstack.org_novacells.yaml | 25 +- .../nova.openstack.org_novanovncproxies.yaml | 23 +- api/v1beta1/novanovncproxy_types.go | 25 +- api/v1beta1/zz_generated.deepcopy.go | 18 + config/crd/bases/nova.openstack.org_nova.yaml | 27 +- .../bases/nova.openstack.org_novacells.yaml | 25 +- .../nova.openstack.org_novanovncproxies.yaml | 23 +- controllers/common.go | 13 +- controllers/novanovncproxy_controller.go | 54 ++- pkg/novncproxy/const.go | 5 +- pkg/novncproxy/deployment.go | 17 +- templates/nova.conf | 6 + test/functional/base_test.go | 37 +- test/functional/nova_novncproxy_test.go | 359 +++++++++++++++++- 15 files changed, 622 insertions(+), 62 deletions(-) diff --git a/api/bases/nova.openstack.org_nova.yaml b/api/bases/nova.openstack.org_nova.yaml index 1073e4919..3f41d982c 100644 --- a/api/bases/nova.openstack.org_nova.yaml +++ b/api/bases/nova.openstack.org_nova.yaml @@ -1085,10 +1085,29 @@ spec: description: CaBundleSecretName - holding the CA certs in a pre-created bundle file type: string - secretName: - description: SecretName - holding the cert, key for - the service - type: string + service: + description: Service - Cert secret used for the nova + novnc service endpoint + properties: + secretName: + description: SecretName - holding the cert, key + for the service + type: string + type: object + vencrypt: + description: Vencrypt - cert secret containing the x509 + certificate to be presented to the VNC server. The + CommonName field should match the primary hostname + of the controller node. If using a HA deployment, + the Organization field can also be configured to a + value that is common across all console proxy instances + in the deployment. https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + properties: + secretName: + description: SecretName - holding the cert, key + for the service + type: string + type: object type: object type: object nodeSelector: diff --git a/api/bases/nova.openstack.org_novacells.yaml b/api/bases/nova.openstack.org_novacells.yaml index 8125e0dee..b485c93fb 100644 --- a/api/bases/nova.openstack.org_novacells.yaml +++ b/api/bases/nova.openstack.org_novacells.yaml @@ -731,9 +731,28 @@ spec: description: CaBundleSecretName - holding the CA certs in a pre-created bundle file type: string - secretName: - description: SecretName - holding the cert, key for the service - type: string + service: + description: Service - Cert secret used for the nova novnc + service endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object + vencrypt: + description: Vencrypt - cert secret containing the x509 certificate + to be presented to the VNC server. The CommonName field + should match the primary hostname of the controller node. + If using a HA deployment, the Organization field can also + be configured to a value that is common across all console + proxy instances in the deployment. https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object type: object type: object nodeSelector: diff --git a/api/bases/nova.openstack.org_novanovncproxies.yaml b/api/bases/nova.openstack.org_novanovncproxies.yaml index f27184902..84ec53b46 100644 --- a/api/bases/nova.openstack.org_novanovncproxies.yaml +++ b/api/bases/nova.openstack.org_novanovncproxies.yaml @@ -336,9 +336,26 @@ spec: description: CaBundleSecretName - holding the CA certs in a pre-created bundle file type: string - secretName: - description: SecretName - holding the cert, key for the service - type: string + service: + description: Service - Cert secret used for the nova novnc service + endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the service + type: string + type: object + vencrypt: + description: Vencrypt - cert secret containing the x509 certificate + to be presented to the VNC server. The CommonName field should + match the primary hostname of the controller node. If using + a HA deployment, the Organization field can also be configured + to a value that is common across all console proxy instances + in the deployment. https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + properties: + secretName: + description: SecretName - holding the cert, key for the service + type: string + type: object type: object required: - cellDatabaseHostname diff --git a/api/v1beta1/novanovncproxy_types.go b/api/v1beta1/novanovncproxy_types.go index 05e53b710..822a56dae 100644 --- a/api/v1beta1/novanovncproxy_types.go +++ b/api/v1beta1/novanovncproxy_types.go @@ -75,7 +75,28 @@ type NovaNoVNCProxyTemplate struct { // +kubebuilder:validation:Optional // +operator-sdk:csv:customresourcedefinitions:type=spec // TLS - Parameters related to the TLS - TLS tls.SimpleService `json:"tls,omitempty"` + TLS TLSSection `json:"tls"` +} + +// TLSSection defines the desired state of TLS configuration +type TLSSection struct { + // +kubebuilder:validation:optional + //+operator-sdk:csv:customresourcedefinitions:type=spec + // Service - Cert secret used for the nova novnc service endpoint + Service tls.GenericService `json:"service,omitempty"` + + // +kubebuilder:validation:optional + //+operator-sdk:csv:customresourcedefinitions:type=spec + // Vencrypt - cert secret containing the x509 certificate to be presented to the VNC server. + // The CommonName field should match the primary hostname of the controller node. If using a HA deployment, + // the Organization field can also be configured to a value that is common across all console proxy instances in the deployment. + // https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + Vencrypt tls.GenericService `json:"vencrypt,omitempty"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing CA bundle + tls.Ca `json:",inline"` } // VNCProxyOverrideSpec to override the generated manifest of several child resources. @@ -135,7 +156,7 @@ type NovaNoVNCProxySpec struct { // +kubebuilder:validation:Optional // +operator-sdk:csv:customresourcedefinitions:type=spec // TLS - Parameters related to the TLS - TLS tls.SimpleService `json:"tls,omitempty"` + TLS TLSSection `json:"tls"` // +kubebuilder:validation:Required // MemcachedInstance is the name of the Memcached CR that all nova service will use. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 050cdbc05..8d7c0bc49 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1623,6 +1623,24 @@ func (in *PasswordSelector) DeepCopy() *PasswordSelector { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSSection) DeepCopyInto(out *TLSSection) { + *out = *in + in.Service.DeepCopyInto(&out.Service) + in.Vencrypt.DeepCopyInto(&out.Vencrypt) + out.Ca = in.Ca +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSSection. +func (in *TLSSection) DeepCopy() *TLSSection { + if in == nil { + return nil + } + out := new(TLSSection) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VNCProxyOverrideSpec) DeepCopyInto(out *VNCProxyOverrideSpec) { *out = *in diff --git a/config/crd/bases/nova.openstack.org_nova.yaml b/config/crd/bases/nova.openstack.org_nova.yaml index 1073e4919..3f41d982c 100644 --- a/config/crd/bases/nova.openstack.org_nova.yaml +++ b/config/crd/bases/nova.openstack.org_nova.yaml @@ -1085,10 +1085,29 @@ spec: description: CaBundleSecretName - holding the CA certs in a pre-created bundle file type: string - secretName: - description: SecretName - holding the cert, key for - the service - type: string + service: + description: Service - Cert secret used for the nova + novnc service endpoint + properties: + secretName: + description: SecretName - holding the cert, key + for the service + type: string + type: object + vencrypt: + description: Vencrypt - cert secret containing the x509 + certificate to be presented to the VNC server. The + CommonName field should match the primary hostname + of the controller node. If using a HA deployment, + the Organization field can also be configured to a + value that is common across all console proxy instances + in the deployment. https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + properties: + secretName: + description: SecretName - holding the cert, key + for the service + type: string + type: object type: object type: object nodeSelector: diff --git a/config/crd/bases/nova.openstack.org_novacells.yaml b/config/crd/bases/nova.openstack.org_novacells.yaml index 8125e0dee..b485c93fb 100644 --- a/config/crd/bases/nova.openstack.org_novacells.yaml +++ b/config/crd/bases/nova.openstack.org_novacells.yaml @@ -731,9 +731,28 @@ spec: description: CaBundleSecretName - holding the CA certs in a pre-created bundle file type: string - secretName: - description: SecretName - holding the cert, key for the service - type: string + service: + description: Service - Cert secret used for the nova novnc + service endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object + vencrypt: + description: Vencrypt - cert secret containing the x509 certificate + to be presented to the VNC server. The CommonName field + should match the primary hostname of the controller node. + If using a HA deployment, the Organization field can also + be configured to a value that is common across all console + proxy instances in the deployment. https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object type: object type: object nodeSelector: diff --git a/config/crd/bases/nova.openstack.org_novanovncproxies.yaml b/config/crd/bases/nova.openstack.org_novanovncproxies.yaml index f27184902..84ec53b46 100644 --- a/config/crd/bases/nova.openstack.org_novanovncproxies.yaml +++ b/config/crd/bases/nova.openstack.org_novanovncproxies.yaml @@ -336,9 +336,26 @@ spec: description: CaBundleSecretName - holding the CA certs in a pre-created bundle file type: string - secretName: - description: SecretName - holding the cert, key for the service - type: string + service: + description: Service - Cert secret used for the nova novnc service + endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the service + type: string + type: object + vencrypt: + description: Vencrypt - cert secret containing the x509 certificate + to be presented to the VNC server. The CommonName field should + match the primary hostname of the controller node. If using + a HA deployment, the Organization field can also be configured + to a value that is common across all console proxy instances + in the deployment. https://docs.openstack.org/nova/latest/admin/remote-console-access.html#novnc-proxy-server-configuration + properties: + secretName: + description: SecretName - holding the cert, key for the service + type: string + type: object type: object required: - cellDatabaseHostname diff --git a/controllers/common.go b/controllers/common.go index 71a6fbd2d..9a5c2bbf7 100644 --- a/controllers/common.go +++ b/controllers/common.go @@ -89,12 +89,13 @@ const ( TransportURLSelector = "transport_url" // fields to index to reconcile when change - passwordSecretField = ".spec.secret" - caBundleSecretNameField = ".spec.tls.caBundleSecretName" - tlsAPIInternalField = ".spec.tls.api.internal.secretName" - tlsAPIPublicField = ".spec.tls.api.public.secretName" - tlsMetadataField = ".spec.tls.secretName" - tlsNoVNCProxyField = ".spec.tls.secretName" + passwordSecretField = ".spec.secret" + caBundleSecretNameField = ".spec.tls.caBundleSecretName" + tlsAPIInternalField = ".spec.tls.api.internal.secretName" + tlsAPIPublicField = ".spec.tls.api.public.secretName" + tlsMetadataField = ".spec.tls.secretName" + tlsNoVNCProxyServiceField = ".spec.tls.service.secretName" + tlsNoVNCProxyVencryptField = ".spec.tls.vencrypt.secretName" // NovaAPIDatabaseName is the name of the DB schema created for the // top level nova DB diff --git a/controllers/novanovncproxy_controller.go b/controllers/novanovncproxy_controller.go index 49ab45c50..02083b696 100644 --- a/controllers/novanovncproxy_controller.go +++ b/controllers/novanovncproxy_controller.go @@ -218,9 +218,9 @@ func (r *NovaNoVNCProxyReconciler) Reconcile(ctx context.Context, req ctrl.Reque } } - // Validate metadata service cert secret - if instance.Spec.TLS.Enabled() { - hash, ctrlResult, err := instance.Spec.TLS.ValidateCertSecret(ctx, h, instance.Namespace) + // Validate the service cert secret + if instance.Spec.TLS.Service.Enabled() { + hash, ctrlResult, err := instance.Spec.TLS.Service.ValidateCertSecret(ctx, h, instance.Namespace) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( condition.TLSInputReadyCondition, @@ -235,6 +235,23 @@ func (r *NovaNoVNCProxyReconciler) Reconcile(ctx context.Context, req ctrl.Reque hashes[tls.TLSHashName] = env.SetValue(hash) } + // Validate the Vencrypt cert secret + if instance.Spec.TLS.Vencrypt.Enabled() { + hash, ctrlResult, err := instance.Spec.TLS.Vencrypt.ValidateCertSecret(ctx, h, instance.Namespace) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.TLSInputReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.TLSInputErrorMessage, + err.Error())) + return ctrl.Result{}, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + hashes[novncproxy.VencryptName] = env.SetValue(hash) + } + // all cert input checks out so report InputReady instance.Status.Conditions.MarkTrue(condition.TLSInputReadyCondition, condition.InputReadyMessage) @@ -414,11 +431,17 @@ func (r *NovaNoVNCProxyReconciler) generateConfigs( "MemcachedServersWithInet": memcachedInstance.GetMemcachedServerListWithInetString(), "MemcachedTLS": memcachedInstance.GetMemcachedTLSSupport(), } - if instance.Spec.TLS.GenericService.Enabled() { + if instance.Spec.TLS.Service.Enabled() { templateParameters["SSLCertificateFile"] = fmt.Sprintf("/etc/pki/tls/certs/%s.crt", novncproxy.ServiceName) templateParameters["SSLCertificateKeyFile"] = fmt.Sprintf("/etc/pki/tls/private/%s.key", novncproxy.ServiceName) } + if instance.Spec.TLS.Vencrypt.Enabled() { + templateParameters["VencryptClientKey"] = "/etc/pki/nova-novncproxy/client-key.pem" + templateParameters["VencryptClientCert"] = "/etc/pki/nova-novncproxy/client-cert.pem" + templateParameters["VencryptCACerts"] = "/etc/pki/nova-novncproxy/ca-cert.pem" + } + var tlsCfg *tls.Service if instance.Spec.TLS.CaBundleSecretName != "" { tlsCfg = &tls.Service{} @@ -697,7 +720,8 @@ var ( noVNCProxyWatchFields = []string{ passwordSecretField, caBundleSecretNameField, - tlsNoVNCProxyField, + tlsNoVNCProxyServiceField, + tlsNoVNCProxyVencryptField, } ) @@ -755,14 +779,26 @@ func (r *NovaNoVNCProxyReconciler) SetupWithManager(mgr ctrl.Manager) error { return err } - // index tlsNoVNCProxyField - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &novav1.NovaNoVNCProxy{}, tlsNoVNCProxyField, func(rawObj client.Object) []string { + // index service cert secret tlsNoVNCProxyField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &novav1.NovaNoVNCProxy{}, tlsNoVNCProxyServiceField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*novav1.NovaNoVNCProxy) + if cr.Spec.TLS.Service.SecretName == nil { + return nil + } + return []string{*cr.Spec.TLS.Service.SecretName} + }); err != nil { + return err + } + + // index vencrypt cert secret tlsNoVNCProxyField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &novav1.NovaNoVNCProxy{}, tlsNoVNCProxyVencryptField, func(rawObj client.Object) []string { // Extract the secret name from the spec, if one is provided cr := rawObj.(*novav1.NovaNoVNCProxy) - if cr.Spec.TLS.SecretName == nil { + if cr.Spec.TLS.Vencrypt.SecretName == nil { return nil } - return []string{*cr.Spec.TLS.SecretName} + return []string{*cr.Spec.TLS.Vencrypt.SecretName} }); err != nil { return err } diff --git a/pkg/novncproxy/const.go b/pkg/novncproxy/const.go index 51c89d7a5..70c0a9b02 100644 --- a/pkg/novncproxy/const.go +++ b/pkg/novncproxy/const.go @@ -20,6 +20,7 @@ const ( //NoVNCProxyPort - The port the nova-noVNCProxyPort service is exposed on NoVNCProxyPort = 6080 // ServiceName - The name of the service exposed to k8s - ServiceName = "nova-novncproxy" - Host = "::0" + ServiceName = "nova-novncproxy" + Host = "::0" + VencryptName = "vencrypt" ) diff --git a/pkg/novncproxy/deployment.go b/pkg/novncproxy/deployment.go index 667b554f5..94853cb7a 100644 --- a/pkg/novncproxy/deployment.go +++ b/pkg/novncproxy/deployment.go @@ -69,7 +69,7 @@ func StatefulSet( Path: "/vnc_lite.html", } - if instance.Spec.TLS.GenericService.Enabled() { + if instance.Spec.TLS.Service.Enabled() { livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS startupProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS @@ -104,8 +104,9 @@ func StatefulSet( volumeMounts = append(volumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...) } - if instance.Spec.TLS.GenericService.Enabled() { - svc, err := instance.Spec.TLS.GenericService.ToService() + // add service certs if defined + if instance.Spec.TLS.Service.Enabled() { + svc, err := instance.Spec.TLS.Service.ToService() if err != nil { return nil, err } @@ -113,6 +114,16 @@ func StatefulSet( volumeMounts = append(volumeMounts, svc.CreateVolumeMounts(ServiceName)...) } + // add Vencrypt certs if defined + if instance.Spec.TLS.Vencrypt.Enabled() { + svc, err := instance.Spec.TLS.Vencrypt.ToService() + if err != nil { + return nil, err + } + volumes = append(volumes, svc.CreateVolume(VencryptName)) + volumeMounts = append(volumeMounts, svc.CreateVolumeMounts(VencryptName)...) + } + statefulset := &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: instance.Name, diff --git a/templates/nova.conf b/templates/nova.conf index 5c33e3dc8..aa9c571f3 100644 --- a/templates/nova.conf +++ b/templates/nova.conf @@ -139,6 +139,12 @@ notify_on_state_change = vm_and_task_state enabled = True novncproxy_host = "::0" novncproxy_port = 6080 +{{if (index . "VencryptClientKey") }} +auth_schemes=vencrypt,none +vencrypt_client_key=/etc/pki/private/vencrypt-novncproxy.key +vencrypt_client_cert=/etc/pki/certs/vencrypt-novncproxy.crt +vencrypt_ca_certs=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem +{{end}} {{ else if and (eq .service_name "nova-compute") .vnc_enabled }} [vnc] enabled = True diff --git a/test/functional/base_test.go b/test/functional/base_test.go index 754d34817..345a6c146 100644 --- a/test/functional/base_test.go +++ b/test/functional/base_test.go @@ -677,22 +677,23 @@ func GetCellNames(novaName types.NamespacedName, cell string) CellNames { } type NovaNames struct { - Namespace string - NovaName types.NamespacedName - InternalNovaServiceName types.NamespacedName - PublicNovaServiceName types.NamespacedName - AdminNovaServiceName types.NamespacedName - KeystoneServiceName types.NamespacedName - APIName types.NamespacedName - APIMariaDBDatabaseName types.NamespacedName - APIMariaDBDatabaseAccount types.NamespacedName - APIDeploymentName types.NamespacedName - APIKeystoneEndpointName types.NamespacedName - APIStatefulSetName types.NamespacedName - APIConfigDataName types.NamespacedName - InternalCertSecretName types.NamespacedName - PublicCertSecretName types.NamespacedName - CaBundleSecretName types.NamespacedName + Namespace string + NovaName types.NamespacedName + InternalNovaServiceName types.NamespacedName + PublicNovaServiceName types.NamespacedName + AdminNovaServiceName types.NamespacedName + KeystoneServiceName types.NamespacedName + APIName types.NamespacedName + APIMariaDBDatabaseName types.NamespacedName + APIMariaDBDatabaseAccount types.NamespacedName + APIDeploymentName types.NamespacedName + APIKeystoneEndpointName types.NamespacedName + APIStatefulSetName types.NamespacedName + APIConfigDataName types.NamespacedName + InternalCertSecretName types.NamespacedName + PublicCertSecretName types.NamespacedName + CaBundleSecretName types.NamespacedName + VNCProxyVencryptCertSecretName types.NamespacedName // refers internal API network for all Nova services (not just nova API) InternalAPINetworkNADName types.NamespacedName SchedulerName types.NamespacedName @@ -777,7 +778,9 @@ func GetNovaNames(novaName types.NamespacedName, cellNames []string) NovaNames { CaBundleSecretName: types.NamespacedName{ Namespace: novaName.Namespace, Name: "combined-ca-bundle"}, - + VNCProxyVencryptCertSecretName: types.NamespacedName{ + Namespace: novaAPI.Namespace, + Name: "vencrypt-tls-certs"}, InternalAPINetworkNADName: types.NamespacedName{ Namespace: novaName.Namespace, Name: "internalapi", diff --git a/test/functional/nova_novncproxy_test.go b/test/functional/nova_novncproxy_test.go index 0cf35bee8..55eca64df 100644 --- a/test/functional/nova_novncproxy_test.go +++ b/test/functional/nova_novncproxy_test.go @@ -761,11 +761,13 @@ var _ = Describe("NovaNoVNCProxy controller", func() { }) - When("NovaNoVNCProxy is created with TLS CA cert secret", func() { + When("NovaNoVNCProxy is created with service and CA bundle cert secret", func() { BeforeEach(func() { spec := GetDefaultNovaNoVNCProxySpec(cell1) spec["tls"] = map[string]interface{}{ - "secretName": ptr.To(novaNames.InternalCertSecretName.Name), + "service": map[string]interface{}{ + "secretName": ptr.To(novaNames.InternalCertSecretName.Name), + }, "caBundleSecretName": novaNames.CaBundleSecretName.Name, } memcachedSpec := memcachedv1.MemcachedSpec{ @@ -817,7 +819,7 @@ var _ = Describe("NovaNoVNCProxy controller", func() { ) }) - It("creates a StatefulSet for nova-metadata service with TLS CA cert attached", func() { + It("creates a StatefulSet for nova-novncproxy service with TLS CA cert attached", func() { DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.InternalCertSecretName)) th.SimulateStatefulSetReplicaReady(cell1.NoVNCProxyStatefulSetName) @@ -917,4 +919,355 @@ var _ = Describe("NovaNoVNCProxy controller", func() { }, timeout, interval).Should(Succeed()) }) }) + + When("NovaNoVNCProxy is created with vencrypt and CA bundle cert secret", func() { + BeforeEach(func() { + spec := GetDefaultNovaNoVNCProxySpec(cell1) + spec["tls"] = map[string]interface{}{ + "vencrypt": map[string]interface{}{ + "secretName": ptr.To(novaNames.VNCProxyVencryptCertSecretName.Name), + }, + "caBundleSecretName": novaNames.CaBundleSecretName.Name, + } + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(3)), + }, + } + + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(novaNames.NovaName.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateTLSMemcachedReady(novaNames.MemcachedNamespace) + DeferCleanup(th.DeleteInstance, CreateNovaNoVNCProxy(cell1.NoVNCProxyName, spec)) + DeferCleanup( + k8sClient.Delete, ctx, CreateDefaultCellInternalSecret(cell1)) + }) + + It("reports that the CA secret is missing", func() { + th.ExpectConditionWithDetails( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionFalse, + condition.ErrorReason, + fmt.Sprintf("TLSInput error occured in TLS sources Secret %s/combined-ca-bundle not found", novaNames.Namespace), + ) + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + }) + + It("reports that the vencrypt cert secret is missing", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + + th.ExpectConditionWithDetails( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionFalse, + condition.ErrorReason, + fmt.Sprintf("TLSInput error occured in TLS sources Secret %s/vencrypt-tls-certs not found", novaNames.Namespace), + ) + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + }) + + It("creates a StatefulSet for nova-novncproxy service with TLS CA cert attached", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.VNCProxyVencryptCertSecretName)) + th.SimulateStatefulSetReplicaReady(cell1.NoVNCProxyStatefulSetName) + + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionTrue, + ) + + ss := th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName) + + // Check the resulting deployment fields + Expect(int(*ss.Spec.Replicas)).To(Equal(1)) + Expect(ss.Spec.Template.Spec.Volumes).To(HaveLen(3)) + Expect(ss.Spec.Template.Spec.Containers).To(HaveLen(1)) + + // cert deployment volumes + th.AssertVolumeExists(novaNames.CaBundleSecretName.Name, ss.Spec.Template.Spec.Volumes) + th.AssertVolumeExists("vencrypt-tls-certs", ss.Spec.Template.Spec.Volumes) + + // CA container certs + apiContainer := ss.Spec.Template.Spec.Containers[0] + th.AssertVolumeMountExists(novaNames.CaBundleSecretName.Name, "tls-ca-bundle.pem", apiContainer.VolumeMounts) + th.AssertVolumeMountExists("vencrypt-tls-certs", "tls.key", apiContainer.VolumeMounts) + th.AssertVolumeMountExists("vencrypt-tls-certs", "tls.crt", apiContainer.VolumeMounts) + + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionTrue, + ) + + configDataMap := th.GetSecret( + types.NamespacedName{ + Namespace: cell1.NoVNCProxyName.Namespace, + Name: fmt.Sprintf("%s-config-data", cell1.NoVNCProxyName.Name), + }, + ) + Expect(configDataMap).ShouldNot(BeNil()) + Expect(configDataMap.Data).Should(HaveKey("01-nova.conf")) + configData := string(configDataMap.Data["01-nova.conf"]) + Expect(configData).Should(ContainSubstring("auth_schemes=vencrypt,none")) + Expect(configData).Should(ContainSubstring("vencrypt_client_key=/etc/pki/private/vencrypt-novncproxy.key")) + Expect(configData).Should(ContainSubstring("vencrypt_client_cert=/etc/pki/certs/vencrypt-novncproxy.crt")) + Expect(configData).Should(ContainSubstring("vencrypt_ca_certs=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem")) + + configData = string(configDataMap.Data["01-nova.conf"]) + Expect(configData).Should( + ContainSubstring(fmt.Sprintf("memcache_servers=memcached-0.memcached.%s.svc:11211,memcached-1.memcached.%s.svc:11211,memcached-2.memcached.%s.svc:11211", + novaNames.Namespace, novaNames.Namespace, novaNames.Namespace))) + Expect(configData).Should( + ContainSubstring(fmt.Sprintf("memcached_servers=inet:[memcached-0.memcached.%s.svc]:11211,inet:[memcached-1.memcached.%s.svc]:11211,inet:[memcached-2.memcached.%s.svc]:11211", + novaNames.Namespace, novaNames.Namespace, novaNames.Namespace))) + Expect(configData).Should( + ContainSubstring("tls_enabled=true")) + + myCnf := configDataMap.Data["my.cnf"] + Expect(myCnf).To( + ContainSubstring("[client]\nssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem\nssl=1")) + }) + + It("reconfigures the NovaNoVNCProxy pod when CA changes", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.VNCProxyVencryptCertSecretName)) + th.SimulateStatefulSetReplicaReady(cell1.NoVNCProxyStatefulSetName) + + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionTrue, + ) + + ss := th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName) + + // Check the resulting deployment fields + Expect(int(*ss.Spec.Replicas)).To(Equal(1)) + Expect(ss.Spec.Template.Spec.Volumes).To(HaveLen(3)) + Expect(ss.Spec.Template.Spec.Containers).To(HaveLen(1)) + + // Grab the current config hash + originalHash := GetEnvVarValue( + th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName).Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "") + Expect(originalHash).NotTo(BeEmpty()) + + // Change the content of the CA secret + th.UpdateSecret(novaNames.CaBundleSecretName, "tls-ca-bundle.pem", []byte("DifferentCAData")) + + // Assert that the deployment is updated + Eventually(func(g Gomega) { + newHash := GetEnvVarValue( + th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName).Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "") + g.Expect(newHash).NotTo(BeEmpty()) + g.Expect(newHash).NotTo(Equal(originalHash)) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("NovaNoVNCProxy is created with both service, vencrypt and CA bundle cert secret", func() { + BeforeEach(func() { + spec := GetDefaultNovaNoVNCProxySpec(cell1) + spec["tls"] = map[string]interface{}{ + "service": map[string]interface{}{ + "secretName": ptr.To(novaNames.InternalCertSecretName.Name), + }, + "vencrypt": map[string]interface{}{ + "secretName": ptr.To(novaNames.VNCProxyVencryptCertSecretName.Name), + }, + "caBundleSecretName": novaNames.CaBundleSecretName.Name, + } + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(3)), + }, + } + + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(novaNames.NovaName.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateTLSMemcachedReady(novaNames.MemcachedNamespace) + DeferCleanup(th.DeleteInstance, CreateNovaNoVNCProxy(cell1.NoVNCProxyName, spec)) + DeferCleanup( + k8sClient.Delete, ctx, CreateDefaultCellInternalSecret(cell1)) + }) + + It("reports that the CA secret is missing", func() { + th.ExpectConditionWithDetails( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionFalse, + condition.ErrorReason, + fmt.Sprintf("TLSInput error occured in TLS sources Secret %s/combined-ca-bundle not found", novaNames.Namespace), + ) + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + }) + + It("reports that the service cert secret is missing", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + + th.ExpectConditionWithDetails( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionFalse, + condition.ErrorReason, + fmt.Sprintf("TLSInput error occured in TLS sources Secret %s/internal-tls-certs not found", novaNames.Namespace), + ) + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + }) + + It("reports that the vencrypt cert secret is missing", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.InternalCertSecretName)) + + th.ExpectConditionWithDetails( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionFalse, + condition.ErrorReason, + fmt.Sprintf("TLSInput error occured in TLS sources Secret %s/vencrypt-tls-certs not found", novaNames.Namespace), + ) + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + }) + + It("creates a StatefulSet for nova-novncproxy service with TLS CA cert attached", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.InternalCertSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.VNCProxyVencryptCertSecretName)) + th.SimulateStatefulSetReplicaReady(cell1.NoVNCProxyStatefulSetName) + + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionTrue, + ) + + ss := th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName) + + // Check the resulting deployment fields + Expect(int(*ss.Spec.Replicas)).To(Equal(1)) + Expect(ss.Spec.Template.Spec.Volumes).To(HaveLen(4)) + Expect(ss.Spec.Template.Spec.Containers).To(HaveLen(1)) + + // cert deployment volumes + th.AssertVolumeExists(novaNames.CaBundleSecretName.Name, ss.Spec.Template.Spec.Volumes) + th.AssertVolumeExists("nova-novncproxy-tls-certs", ss.Spec.Template.Spec.Volumes) + th.AssertVolumeExists("vencrypt-tls-certs", ss.Spec.Template.Spec.Volumes) + + // CA container certs + apiContainer := ss.Spec.Template.Spec.Containers[0] + th.AssertVolumeMountExists(novaNames.CaBundleSecretName.Name, "tls-ca-bundle.pem", apiContainer.VolumeMounts) + th.AssertVolumeMountExists("nova-novncproxy-tls-certs", "tls.key", apiContainer.VolumeMounts) + th.AssertVolumeMountExists("nova-novncproxy-tls-certs", "tls.crt", apiContainer.VolumeMounts) + th.AssertVolumeMountExists("vencrypt-tls-certs", "tls.key", apiContainer.VolumeMounts) + th.AssertVolumeMountExists("vencrypt-tls-certs", "tls.crt", apiContainer.VolumeMounts) + + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.ReadyCondition, + corev1.ConditionTrue, + ) + + configDataMap := th.GetSecret( + types.NamespacedName{ + Namespace: cell1.NoVNCProxyName.Namespace, + Name: fmt.Sprintf("%s-config-data", cell1.NoVNCProxyName.Name), + }, + ) + Expect(configDataMap).ShouldNot(BeNil()) + Expect(configDataMap.Data).Should(HaveKey("01-nova.conf")) + configData := string(configDataMap.Data["01-nova.conf"]) + Expect(configData).Should(ContainSubstring("ssl_only=true")) + Expect(configData).Should(ContainSubstring("cert=/etc/pki/tls/certs/nova-novncproxy.crt")) + Expect(configData).Should(ContainSubstring("key=/etc/pki/tls/private/nova-novncproxy.key")) + Expect(configData).Should(ContainSubstring("auth_schemes=vencrypt,none")) + Expect(configData).Should(ContainSubstring("vencrypt_client_key=/etc/pki/private/vencrypt-novncproxy.key")) + Expect(configData).Should(ContainSubstring("vencrypt_client_cert=/etc/pki/certs/vencrypt-novncproxy.crt")) + Expect(configData).Should(ContainSubstring("vencrypt_ca_certs=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem")) + + configData = string(configDataMap.Data["01-nova.conf"]) + Expect(configData).Should( + ContainSubstring(fmt.Sprintf("memcache_servers=memcached-0.memcached.%s.svc:11211,memcached-1.memcached.%s.svc:11211,memcached-2.memcached.%s.svc:11211", + novaNames.Namespace, novaNames.Namespace, novaNames.Namespace))) + Expect(configData).Should( + ContainSubstring(fmt.Sprintf("memcached_servers=inet:[memcached-0.memcached.%s.svc]:11211,inet:[memcached-1.memcached.%s.svc]:11211,inet:[memcached-2.memcached.%s.svc]:11211", + novaNames.Namespace, novaNames.Namespace, novaNames.Namespace))) + Expect(configData).Should( + ContainSubstring("tls_enabled=true")) + + myCnf := configDataMap.Data["my.cnf"] + Expect(myCnf).To( + ContainSubstring("[client]\nssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem\nssl=1")) + }) + + It("reconfigures the NovaNoVNCProxy pod when CA changes", func() { + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(novaNames.CaBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.InternalCertSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(novaNames.VNCProxyVencryptCertSecretName)) + th.SimulateStatefulSetReplicaReady(cell1.NoVNCProxyStatefulSetName) + + th.ExpectCondition( + cell1.NoVNCProxyName, + ConditionGetterFunc(NoVNCProxyConditionGetter), + condition.TLSInputReadyCondition, + corev1.ConditionTrue, + ) + + ss := th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName) + + // Check the resulting deployment fields + Expect(int(*ss.Spec.Replicas)).To(Equal(1)) + Expect(ss.Spec.Template.Spec.Volumes).To(HaveLen(4)) + Expect(ss.Spec.Template.Spec.Containers).To(HaveLen(1)) + + // Grab the current config hash + originalHash := GetEnvVarValue( + th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName).Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "") + Expect(originalHash).NotTo(BeEmpty()) + + // Change the content of the CA secret + th.UpdateSecret(novaNames.CaBundleSecretName, "tls-ca-bundle.pem", []byte("DifferentCAData")) + + // Assert that the deployment is updated + Eventually(func(g Gomega) { + newHash := GetEnvVarValue( + th.GetStatefulSet(cell1.NoVNCProxyStatefulSetName).Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "") + g.Expect(newHash).NotTo(BeEmpty()) + g.Expect(newHash).NotTo(Equal(originalHash)) + }, timeout, interval).Should(Succeed()) + }) + }) })