diff --git a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index 917bae442f..38ef4a2943 100644 --- a/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/api/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -151,13 +151,22 @@ spec: OpenstackDataPlaneServiceCert defines the property of a TLS cert issued for a dataplane service properties: + commonName: + description: |- + CommonName overrides how the certificate Common Name is derived. + When set to "system-id", the CN is a UUID5 derived from the node's + ctlplane FQDN, matching the OVN chassis system-id convention. + When empty, CN defaults to the short hostname. + enum: + - system-id + type: string contents: description: |- Contents of the certificate - This is a list of strings for properties that are needed in the cert + This is a list of strings for properties that are needed in the cert. + May be empty for client-only certificates that require no SANs. items: type: string - minItems: 1 type: array edpmRoleServiceName: description: |- @@ -241,8 +250,6 @@ spec: pattern: ^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$ type: string type: array - required: - - contents type: object description: TLSCerts tls certs to be generated type: object diff --git a/api/core/v1beta1/openstackcontrolplane_types.go b/api/core/v1beta1/openstackcontrolplane_types.go index ca3ffade3d..c7eda30121 100644 --- a/api/core/v1beta1/openstackcontrolplane_types.go +++ b/api/core/v1beta1/openstackcontrolplane_types.go @@ -61,7 +61,6 @@ const ( OvnDbCaName = tls.DefaultCAPrefix + "ovn" // LibvirtCaName - LibvirtCaName = tls.DefaultCAPrefix + "libvirt" - // GlanceName - Default Glance name GlanceName = "glance" // CinderName - Default Cinder name diff --git a/api/dataplane/v1beta1/openstackdataplaneservice_types.go b/api/dataplane/v1beta1/openstackdataplaneservice_types.go index d613f4fab6..66ff6d6e91 100644 --- a/api/dataplane/v1beta1/openstackdataplaneservice_types.go +++ b/api/dataplane/v1beta1/openstackdataplaneservice_types.go @@ -28,10 +28,10 @@ import ( // a dataplane service type OpenstackDataPlaneServiceCert struct { // Contents of the certificate - // This is a list of strings for properties that are needed in the cert - // +kubebuilder:validation:Required - // +kubebuilder:validation:MinItems:=1 - Contents []string `json:"contents"` + // This is a list of strings for properties that are needed in the cert. + // May be empty for client-only certificates that require no SANs. + // +kubebuilder:validation:Optional + Contents []string `json:"contents,omitempty"` // Networks to include in SNI for the cert // +kubebuilder:validation:Optional @@ -46,6 +46,14 @@ type OpenstackDataPlaneServiceCert struct { // +kubebuilder:validation:Optional KeyUsages []certmgrv1.KeyUsage `json:"keyUsages,omitempty" yaml:"keyUsages,omitempty"` + // CommonName overrides how the certificate Common Name is derived. + // When set to "system-id", the CN is a UUID5 derived from the node's + // ctlplane FQDN, matching the OVN chassis system-id convention. + // When empty, CN defaults to the short hostname. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=system-id + CommonName string `json:"commonName,omitempty" yaml:"commonName,omitempty"` + // EDPMRoleServiceName is the value of the _service_name variable from // the edpm-ansible role where this certificate is used. For example if the // certificate is for edpm_ovn from edpm-ansible, EDPMRoleServiceName must be diff --git a/bindata/crds/crds.yaml b/bindata/crds/crds.yaml index b532c4a914..713c7b1f74 100644 --- a/bindata/crds/crds.yaml +++ b/bindata/crds/crds.yaml @@ -21434,13 +21434,22 @@ spec: OpenstackDataPlaneServiceCert defines the property of a TLS cert issued for a dataplane service properties: + commonName: + description: |- + CommonName overrides how the certificate Common Name is derived. + When set to "system-id", the CN is a UUID5 derived from the node's + ctlplane FQDN, matching the OVN chassis system-id convention. + When empty, CN defaults to the short hostname. + enum: + - system-id + type: string contents: description: |- Contents of the certificate - This is a list of strings for properties that are needed in the cert + This is a list of strings for properties that are needed in the cert. + May be empty for client-only certificates that require no SANs. items: type: string - minItems: 1 type: array edpmRoleServiceName: description: |- @@ -21524,8 +21533,6 @@ spec: pattern: ^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$ type: string type: array - required: - - contents type: object description: TLSCerts tls certs to be generated type: object diff --git a/bindata/rbac/ovn-operator-rbac.yaml b/bindata/rbac/ovn-operator-rbac.yaml index 2e133574fd..1195004732 100644 --- a/bindata/rbac/ovn-operator-rbac.yaml +++ b/bindata/rbac/ovn-operator-rbac.yaml @@ -127,6 +127,26 @@ rules: - patch - update - watch +- apiGroups: + - cert-manager.io + resources: + - certificates + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - cert-manager.io + resources: + - issuers + verbs: + - get + - list + - watch - apiGroups: - k8s.cni.cncf.io resources: diff --git a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml index 917bae442f..38ef4a2943 100644 --- a/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml +++ b/config/crd/bases/dataplane.openstack.org_openstackdataplaneservices.yaml @@ -151,13 +151,22 @@ spec: OpenstackDataPlaneServiceCert defines the property of a TLS cert issued for a dataplane service properties: + commonName: + description: |- + CommonName overrides how the certificate Common Name is derived. + When set to "system-id", the CN is a UUID5 derived from the node's + ctlplane FQDN, matching the OVN chassis system-id convention. + When empty, CN defaults to the short hostname. + enum: + - system-id + type: string contents: description: |- Contents of the certificate - This is a list of strings for properties that are needed in the cert + This is a list of strings for properties that are needed in the cert. + May be empty for client-only certificates that require no SANs. items: type: string - minItems: 1 type: array edpmRoleServiceName: description: |- @@ -241,8 +250,6 @@ spec: pattern: ^[a-zA-Z0-9][a-zA-Z0-9\-_]*[a-zA-Z0-9]$ type: string type: array - required: - - contents type: object description: TLSCerts tls certs to be generated type: object diff --git a/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_metadata.yaml b/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_metadata.yaml index 8a2aee102c..4bade7a795 100644 --- a/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_metadata.yaml +++ b/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_metadata.yaml @@ -13,16 +13,11 @@ spec: name: neutron-metadata-extra-config optional: true tlsCerts: - default: - contents: - - dnsnames - - ips - networks: - - ctlplane + rbac: + commonName: system-id issuer: osp-rootca-issuer-ovn keyUsages: - digital signature - - key encipherment - client auth caCerts: combined-ca-bundle containerImageFields: diff --git a/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_ovn.yaml b/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_ovn.yaml index 5b570a34bb..5ba33bec22 100644 --- a/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_ovn.yaml +++ b/config/services/dataplane_v1beta1_openstackdataplaneservice_neutron_ovn.yaml @@ -14,16 +14,11 @@ spec: name: neutron-ovn-extra-config optional: true tlsCerts: - default: - contents: - - dnsnames - - ips - networks: - - ctlplane + rbac: + commonName: system-id issuer: osp-rootca-issuer-ovn keyUsages: - digital signature - - key encipherment - client auth caCerts: combined-ca-bundle containerImageFields: diff --git a/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn.yaml b/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn.yaml index 7dbad97a9c..0f7e3f139d 100644 --- a/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn.yaml +++ b/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn.yaml @@ -20,6 +20,12 @@ spec: - key encipherment - server auth - client auth + rbac: + commonName: system-id + issuer: osp-rootca-issuer-ovn + keyUsages: + - digital signature + - client auth caCerts: combined-ca-bundle containerImageFields: - OvnControllerImage diff --git a/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn_bgp_agent.yaml b/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn_bgp_agent.yaml index 30af41db37..1e492fb80a 100644 --- a/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn_bgp_agent.yaml +++ b/config/services/dataplane_v1beta1_openstackdataplaneservice_ovn_bgp_agent.yaml @@ -11,17 +11,11 @@ spec: name: ovn-bgp-agent-extra-config optional: true tlsCerts: - default: - contents: - - dnsnames - - ips - networks: - - ctlplane + rbac: + commonName: system-id issuer: osp-rootca-issuer-ovn keyUsages: - digital signature - - key encipherment - - server auth - client auth caCerts: combined-ca-bundle containerImageFields: diff --git a/internal/dataplane/cert.go b/internal/dataplane/cert.go index 28d9a3ca9f..43b1b44e6d 100644 --- a/internal/dataplane/cert.go +++ b/internal/dataplane/cert.go @@ -35,6 +35,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + "github.com/google/uuid" 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" @@ -43,6 +44,17 @@ import ( dataplanev1 "github.com/openstack-k8s-operators/openstack-operator/api/dataplane/v1beta1" ) +// CommonNameSystemID is the sentinel value for OpenstackDataPlaneServiceCert.CommonName +// that triggers UUID5-based CN derivation matching the OVN chassis system-id convention. +const CommonNameSystemID = "system-id" + +// computeSystemID derives a deterministic UUID5 from a name using the DNS +// namespace, matching ovn-operator's ComputeSystemID() and edpm-ansible's +// {{ name | to_uuid(namespace='6ba7b810-...') }}. +func computeSystemID(name string) string { + return uuid.NewSHA1(uuid.NameSpaceDNS, []byte(name)).String() +} + // Generates an organized data structure that is leveraged to create the secrets. func createSecretsDataStructure(secretMaxSize int, certsData map[string][]byte, @@ -180,7 +192,12 @@ func EnsureTLSCerts(ctx context.Context, helper *helper.Helper, nodeName) } - commonName := strings.Split(baseName, ".")[0] + var commonName string + if service.Spec.TLSCerts[certKey].CommonName == CommonNameSystemID { + commonName = computeSystemID(baseName) + } else { + commonName = strings.Split(baseName, ".")[0] + } certSecret, result, err = GetTLSNodeCert(ctx, helper, instance, certName, issuer, labels, commonName, hosts, ips, service.Spec.TLSCerts[certKey].KeyUsages) diff --git a/internal/dataplane/cert_test.go b/internal/dataplane/cert_test.go new file mode 100644 index 0000000000..523010988c --- /dev/null +++ b/internal/dataplane/cert_test.go @@ -0,0 +1,110 @@ +package deployment + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComputeSystemID(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "short hostname", + input: "edpm-compute-0", + expected: computeSystemID("edpm-compute-0"), + }, + { + name: "FQDN", + input: "edpm-compute-0.ctlplane.example.com", + expected: computeSystemID("edpm-compute-0.ctlplane.example.com"), + }, + { + name: "deterministic: same input always yields same output", + input: "edpm-compute-0", + }, + { + name: "different inputs yield different outputs", + input: "edpm-compute-1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := computeSystemID(tt.input) + + // Must be non-empty + assert.NotEmpty(t, result) + + // Must be deterministic + assert.Equal(t, result, computeSystemID(tt.input), + "computeSystemID must be deterministic") + + if tt.expected != "" { + assert.Equal(t, tt.expected, result) + } + }) + } + + // Different inputs must produce different UUIDs + id0 := computeSystemID("edpm-compute-0") + id1 := computeSystemID("edpm-compute-1") + assert.NotEqual(t, id0, id1, + "different hostnames must produce different system IDs") + + // Verify format is a valid UUID (8-4-4-4-12 hex) + assert.Regexp(t, `^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`, + computeSystemID("test-node"), + "computeSystemID must return a valid UUID string") +} + +func TestCreateSecretsDataStructure(t *testing.T) { + tests := []struct { + name string + secretMaxSize int + certsData map[string][]byte + expectedChunks int + }{ + { + name: "single node fits in one secret", + secretMaxSize: 1048576, + certsData: map[string][]byte{ + "node1-ca.crt": []byte("ca-cert-data"), + "node1-tls.crt": []byte("tls-cert-data"), + "node1-tls.key": []byte("tls-key-data"), + }, + expectedChunks: 1, + }, + { + name: "small max size forces multiple secrets", + secretMaxSize: 1, + certsData: map[string][]byte{ + "node1-ca.crt": []byte("ca-cert-data"), + "node1-tls.crt": []byte("tls-cert-data"), + "node1-tls.key": []byte("tls-key-data"), + "node2-ca.crt": []byte("ca-cert-data"), + "node2-tls.crt": []byte("tls-cert-data"), + "node2-tls.key": []byte("tls-key-data"), + }, + expectedChunks: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := createSecretsDataStructure(tt.secretMaxSize, tt.certsData) + assert.Equal(t, tt.expectedChunks, len(result)) + + // Verify all data is present across chunks + totalKeys := 0 + for _, chunk := range result { + totalKeys += len(chunk) + } + assert.Equal(t, len(tt.certsData), totalKeys, + "all cert data must be present across chunks") + }) + } +} diff --git a/internal/openstack/common.go b/internal/openstack/common.go index 8f5f53d64d..e588465af6 100644 --- a/internal/openstack/common.go +++ b/internal/openstack/common.go @@ -69,6 +69,10 @@ const ( // caCertSelector selector passed to cert-manager to set on the ca cert secret caCertSelector = "ca-cert" + + // rootCAIssuerOvnRbacLabel labels the OVN RBAC CA issuer. + // TODO: upstream this to lib-common certmanager module alongside the other RootCAIssuer*Label constants. + rootCAIssuerOvnRbacLabel = "osp-rootca-issuer-ovn-rbac" ) // GetLogger returns a logger object with a prefix of "controller.name" and additional controller context fields diff --git a/internal/openstack/ovn.go b/internal/openstack/ovn.go index e18f8c6f6c..75c9283f2b 100644 --- a/internal/openstack/ovn.go +++ b/internal/openstack/ovn.go @@ -493,6 +493,12 @@ func ReconcileOVNController(ctx context.Context, instance *corev1beta1.OpenStack ovnControllerSpec.MetricsTLS.CaBundleSecretName = instance.Status.TLS.CaBundleSecretName } + // Pass the OVN CA issuer name so the OVNController can create per-node + // cert-manager Certificate resources for OVN RBAC + if instance.Spec.TLS.PodLevel.Enabled { + ovnControllerSpec.OvnIssuerName = instance.GetOvnIssuer() + } + if ovnControllerSpec.NodeSelector == nil { ovnControllerSpec.NodeSelector = &instance.Spec.NodeSelector } diff --git a/test/functional/ctlplane/base_test.go b/test/functional/ctlplane/base_test.go index 6806c3bfc2..5816f4fe9d 100644 --- a/test/functional/ctlplane/base_test.go +++ b/test/functional/ctlplane/base_test.go @@ -97,7 +97,9 @@ type Names struct { OVNControllerName types.NamespacedName OVNControllerCertName types.NamespacedName OVNDbServerNBName types.NamespacedName + OVNDbServerNBCertName types.NamespacedName OVNDbServerSBName types.NamespacedName + OVNDbServerSBCertName types.NamespacedName OVNMetricsCertName types.NamespacedName NeutronOVNCertName types.NamespacedName OpenStackTopology []types.NamespacedName @@ -275,10 +277,18 @@ func CreateNames(openstackControlplaneName types.NamespacedName) Names { Namespace: openstackControlplaneName.Namespace, Name: "ovndbcluster-nb", }, + OVNDbServerNBCertName: types.NamespacedName{ + Namespace: openstackControlplaneName.Namespace, + Name: "cert-ovndbcluster-nb-ovndbs", + }, OVNDbServerSBName: types.NamespacedName{ Namespace: openstackControlplaneName.Namespace, Name: "ovndbcluster-sb", }, + OVNDbServerSBCertName: types.NamespacedName{ + Namespace: openstackControlplaneName.Namespace, + Name: "cert-ovndbcluster-sb-ovndbs", + }, OVNControllerName: types.NamespacedName{ Namespace: openstackControlplaneName.Namespace, Name: "ovncontroller", diff --git a/test/functional/ctlplane/openstackoperator_controller_test.go b/test/functional/ctlplane/openstackoperator_controller_test.go index eb328a8d6d..5810f02cfc 100644 --- a/test/functional/ctlplane/openstackoperator_controller_test.go +++ b/test/functional/ctlplane/openstackoperator_controller_test.go @@ -1015,7 +1015,7 @@ var _ = Describe("OpenStackOperator controller", func() { //Expect(OSCtlplane.Spec.Placement.APIOverride.Route.Annotations).Should(HaveKeyWithValue("api.placement.openstack.org/timeout", "60s")) }) - It("should create selfsigned issuer and public, internal, libvirt and ovn CA and issuer", func() { + It("should create selfsigned issuer and public, internal, libvirt, ovn and ovn-rbac CA and issuer", func() { OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) Expect(OSCtlplane.Spec.TLS.Ingress.Enabled).Should(BeTrue()) @@ -1834,6 +1834,13 @@ var _ = Describe("OpenStackOperator controller", func() { }, timeout, interval).Should(Succeed()) }) + It("should not set OvnIssuerName when TLS pod-level is disabled", func() { + Eventually(func(g Gomega) { + ovnController := ovn.GetOVNController(names.OVNControllerName) + g.Expect(ovnController.Spec.OvnIssuerName).Should(BeEmpty()) + }, timeout, interval).Should(Succeed()) + }) + It("should remove ovn-controller if nicMappings are removed", func() { // Update spec Eventually(func(g Gomega) { @@ -1906,6 +1913,56 @@ var _ = Describe("OpenStackOperator controller", func() { }) }) + When("A OVN OpenStackControlplane instance with TLS pod-level enabled is created", func() { + BeforeEach(func() { + // create cert secrets for rabbitmq instances + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQCell1CertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.RabbitMQNotificationsCertName)) + // create cert secrets for memcached instance + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.MemcachedCertName)) + // create cert secrets for ovn instance + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNNorthdCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNControllerCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNMetricsCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.NeutronOVNCertName)) + // create cert secrets for ovn db clusters (needed for TLS pod-level) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNDbServerNBCertName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(names.OVNDbServerSBCertName)) + + spec := GetDefaultOpenStackControlPlaneSpec() + spec["ovn"] = map[string]interface{}{ + "enabled": true, + "template": map[string]interface{}{ + "ovnDBCluster": map[string]interface{}{ + "ovndbcluster-nb": map[string]interface{}{ + "dbType": "NB", + }, + "ovndbcluster-sb": map[string]interface{}{ + "dbType": "SB", + }, + }, + "ovnController": map[string]interface{}{ + "nicMappings": map[string]interface{}{ + "datacentre": "ospbr", + }, + }, + }, + } + DeferCleanup( + th.DeleteInstance, + CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec), + ) + }) + + It("should set OvnIssuerName on OVNController", func() { + Eventually(func(g Gomega) { + ovnController := ovn.GetOVNController(names.OVNControllerName) + g.Expect(ovnController.Spec.OvnIssuerName).Should(Equal(corev1.OvnDbCaName)) + }, timeout, interval).Should(Succeed()) + }) + }) + When("A OpenStackControlplane instance is created", func() { BeforeEach(func() { // NOTE(bogdando): DBs certs need to be created here as well, but those are already existing somehow diff --git a/test/functional/dataplane/openstackdataplaneservice_controller_test.go b/test/functional/dataplane/openstackdataplaneservice_controller_test.go index 3056687617..b58234245f 100644 --- a/test/functional/dataplane/openstackdataplaneservice_controller_test.go +++ b/test/functional/dataplane/openstackdataplaneservice_controller_test.go @@ -18,6 +18,7 @@ package functional import ( "os" + certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports . "github.com/onsi/gomega" //revive:disable:dot-imports "k8s.io/apimachinery/pkg/types" @@ -63,4 +64,63 @@ var _ = Describe("OpenstackDataplaneService Test", func() { Expect(service.Spec.DeployOnAllNodeSets).To(BeTrue()) }) }) + + When("A service with TLSCerts including system-id CommonName is created", func() { + BeforeEach(func() { + _ = os.Unsetenv("OPERATOR_SERVICES") + DeferCleanup(th.DeleteInstance, CreateDataPlaneServiceFromSpec( + dataplaneServiceName, + map[string]interface{}{ + "edpmServiceType": "ovn", + "tlsCerts": map[string]interface{}{ + "default": map[string]interface{}{ + "contents": []string{"dnsnames", "ips"}, + "issuer": "osp-rootca-issuer-ovn", + "keyUsages": []string{ + "digital signature", + "key encipherment", + "server auth", + "client auth", + }, + }, + "rbac": map[string]interface{}{ + "commonName": "system-id", + "issuer": "osp-rootca-issuer-ovn", + "keyUsages": []string{ + "digital signature", + "client auth", + }, + }, + }, + })) + DeferCleanup(th.DeleteService, dataplaneServiceName) + }) + + It("should store TLSCerts with CommonName and empty Contents", func() { + service := GetService(dataplaneServiceName) + + Expect(service.Spec.TLSCerts).To(HaveLen(2)) + + defaultCert := service.Spec.TLSCerts["default"] + Expect(defaultCert.Contents).To(ConsistOf("dnsnames", "ips")) + Expect(defaultCert.Issuer).To(Equal("osp-rootca-issuer-ovn")) + Expect(defaultCert.CommonName).To(BeEmpty()) + Expect(defaultCert.KeyUsages).To(ContainElements( + certmgrv1.UsageServerAuth, + certmgrv1.UsageClientAuth, + )) + + rbacCert := service.Spec.TLSCerts["rbac"] + Expect(rbacCert.CommonName).To(Equal("system-id")) + Expect(rbacCert.Contents).To(BeEmpty()) + Expect(rbacCert.Issuer).To(Equal("osp-rootca-issuer-ovn")) + Expect(rbacCert.KeyUsages).To(ContainElements( + certmgrv1.UsageDigitalSignature, + certmgrv1.UsageClientAuth, + )) + Expect(rbacCert.KeyUsages).ToNot(ContainElement( + certmgrv1.UsageServerAuth, + )) + }) + }) })