From c5db1ed05f12603cf3a8eb222e2b8caf04e779c9 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Wed, 8 Nov 2023 10:46:03 +0100 Subject: [PATCH] [tls] wip Depends-On: https://github.com/openstack-k8s-operators/lib-common/pull/399 --- modules/common/test/functional/tls_test.go | 136 +++++++ modules/common/tls/tls.go | 408 +++++++++++++------ modules/common/tls/tls_test.go | 425 ++++++++++++++++---- modules/common/tls/zz_generated.deepcopy.go | 111 +++-- 4 files changed, 853 insertions(+), 227 deletions(-) create mode 100644 modules/common/test/functional/tls_test.go diff --git a/modules/common/test/functional/tls_test.go b/modules/common/test/functional/tls_test.go new file mode 100644 index 00000000..ff849c39 --- /dev/null +++ b/modules/common/test/functional/tls_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2023 Red Hat + +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 functional + +import ( + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" + "github.com/openstack-k8s-operators/lib-common/modules/common/tls" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" +) + +var _ = Describe("tls package", func() { + var namespace string + + BeforeEach(func() { + // NOTE(gibi): We need to create a unique namespace for each test run + // as namespaces cannot be deleted in a locally running envtest. See + // https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation + namespace = uuid.New().String() + th.CreateNamespace(namespace) + // We still request the delete of the Namespace to properly cleanup if + // we run the test in an existing cluster. + DeferCleanup(th.DeleteNamespace, namespace) + + }) + + It("validates CA cert secret", func() { + sname := types.NamespacedName{ + Name: "ca", + Namespace: namespace, + } + th.CreateEmptySecret(sname) + + // validate bad ca cert secret + _, ctrlResult, err := tls.ValidateCACertSecret(th.Ctx, cClient, sname) + Expect(err).To(HaveOccurred()) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + + // update ca cert secret with good data + th.UpdateSecret(sname, tls.CABundleKey, []byte("foo")) + hash, ctrlResult, err := tls.ValidateCACertSecret(th.Ctx, cClient, sname) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + Expect(hash).To(BeIdenticalTo("n56fh645hfbh687hc9h678h87h64bh598h577hch5d6h5c9h5d4h74h84h5f4hfch6dh678h547h9bhbchb6h89h5c4h68dhc9h664h557h595h5c5q")) + }) + + It("validates service cert secret", func() { + sname := types.NamespacedName{ + Name: "cert", + Namespace: namespace, + } + + // create bad cert secret + th.CreateEmptySecret(sname) + + // validate bad cert secret + s := &tls.Service{ + SecretName: sname.Name, + } + _, ctrlResult, err := s.ValidateCertSecret(th.Ctx, h, namespace) + Expect(err).To(HaveOccurred()) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + + // update cert secret with cert, still key missing + th.UpdateSecret(sname, tls.CertKey, []byte("cert")) + _, ctrlResult, err = s.ValidateCertSecret(th.Ctx, h, namespace) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("field tls.key not found in Secret")) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + + // update cert secret with key to be a good cert secret + th.UpdateSecret(sname, tls.PrivateKey, []byte("key")) + + // validate good cert secret + hash, ctrlResult, err := s.ValidateCertSecret(th.Ctx, h, namespace) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + Expect(hash).To(BeIdenticalTo("n547h97h5cfh587h56ch594h79hd4h96h5cfh565h587h569h688h666h685h67ch7fhfbh664h5f9h694h564h9ch645h675h665h78h7h87h566hb6q")) + }) + + It("validates endpoint certs secrets", func() { + sname := types.NamespacedName{ + Name: "cert", + Namespace: namespace, + } + // create bad cert secret + th.CreateSecret(sname, map[string][]byte{ + tls.PrivateKey: []byte("key"), + }) + + endpointCfgs := map[service.Endpoint]tls.Service{} + + // validate empty service map + _, ctrlResult, err := tls.ValidateEndpointCerts(th.Ctx, h, namespace, endpointCfgs) + Expect(err).ToNot(HaveOccurred()) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + + endpointCfgs[service.EndpointInternal] = tls.Service{ + SecretName: sname.Name, + } + endpointCfgs[service.EndpointPublic] = tls.Service{ + SecretName: sname.Name, + } + + // validate service map with bad cert secret + _, ctrlResult, err = tls.ValidateEndpointCerts(th.Ctx, h, namespace, endpointCfgs) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("field tls.crt not found in Secret")) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + + // update cert secret to have missing private key + th.UpdateSecret(sname, tls.CertKey, []byte("cert")) + + // validate service map with good cert secret + hash, ctrlResult, err := tls.ValidateEndpointCerts(th.Ctx, h, namespace, endpointCfgs) + Expect(err).ShouldNot(HaveOccurred()) + Expect(ctrlResult).To(BeIdenticalTo(ctrl.Result{})) + Expect(hash).To(BeIdenticalTo("n5d7h65dh5d5h569hffh66ch568h95h686h58fhcfh586h5b8hc6hd7h65bh56bh55bh656hfh5f7h84h54bh65dh5c9h8ch64bh64bhdfh8ch589h54bq")) + }) +}) diff --git a/modules/common/tls/tls.go b/modules/common/tls/tls.go index 0ade2a44..b60d09cc 100644 --- a/modules/common/tls/tls.go +++ b/modules/common/tls/tls.go @@ -20,147 +20,301 @@ package tls import ( "context" + "encoding/json" "fmt" "strings" + "time" + "github.com/openstack-k8s-operators/lib-common/modules/common/env" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" "github.com/openstack-k8s-operators/lib-common/modules/common/secret" "github.com/openstack-k8s-operators/lib-common/modules/common/service" + "github.com/openstack-k8s-operators/lib-common/modules/common/util" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) const ( // CABundleLabel added to the CA bundle secret for the namespace CABundleLabel = "combined-ca-bundle" + // CABundleKey - key of the secret entry holding the ca bundle + CABundleKey = "tls-ca-bundle.pem" + + // CertKey - key of the secret entry holding the cert + CertKey = "tls.crt" + // PrivateKey - key of the secret entry holding the cert private key + PrivateKey = "tls.key" + // CAKey - key of the secret entry holding the ca + CAKey = "ca.crt" + + // TLSHashName - Name of the hash of hashes of all cert resources used to indentify a change + TLSHashName = "certs" ) -// Service contains server-specific TLS secret -type Service struct { +// APIDBMessaging defines the observed state of TLS with API, DB and Messaging +type APIDBMessaging struct { + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing CA bundle + API APIService `json:"api,omitempty"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing CA bundle + DB DB `json:"db,omitempty"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing CA bundle + Messaging DB `json:"messaging,omitempty"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing CA bundle + Ca `json:",inline"` +} + +// APIService - API tls type which encapsulates for API services +type APIService struct { // +kubebuilder:validation:Optional - // SecretName - holding the cert, key for the service - SecretName string `json:"secretName,omitempty"` + // Disabled TLS for the deployment of the service + Disabled *bool `json:"disabled,omitempty"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // The key must be the endpoint type (public, internal) + Endpoint map[service.Endpoint]GenericService `json:"endpoint,omitempty"` +} + +// DB - tls type reflect TLS settings for DB +type DB struct { // +kubebuilder:validation:Optional - // CertMount - dst location to mount the service tls.crt cert. Can be used to override the default location which is /etc/tls//tls.crt - CertMount *string `json:"certMount,omitempty"` + // Disabled TLS for db connection + Disabled *bool `json:"disabled,omitempty"` +} + +// Messaging - tls type reflect TLS settings for Messaging +type Messaging struct { // +kubebuilder:validation:Optional - // KeyMount - dst location to mount the service tls.key key. Can be used to override the default location which is /etc/tls//tls.key - KeyMount *string `json:"keyMount,omitempty"` + // Disabled TLS for messaging + Disabled *bool `json:"disabled,omitempty"` +} + +// GenericService contains server-specific TLS secret or issuer +type GenericService struct { // +kubebuilder:validation:Optional - // CaMount - dst location to mount the CA cert ca.crt to. Can be used if the service CA cert should be mounted specifically, e.g. to be set in a service config for validation, instead of the env wide bundle. - CaMount *string `json:"caMount,omitempty"` + // SecretName - holding the cert, key for the service + SecretName *string `json:"secretName,omitempty"` + // +kubebuilder:validation:Optional - // DisableNonTLSListeners - disable non TLS listeners of the service (if supported) - DisableNonTLSListeners bool `json:"disableNonTLSListeners,omitempty"` + // IssuerName - name of the issuer to be used to issue certificate for the service + IssuerName *string `json:"issuerName"` } // Ca contains CA-specific settings, which could be used both by services (to define their own CA certificates) // and by clients (to verify the server's certificate) type Ca struct { - // +kubebuilder:validation:Optional // CaBundleSecretName - holding the CA certs in a pre-created bundle file - CaBundleSecretName string `json:"caBundleSecretName"` + CaBundleSecretName string `json:"caBundleSecretName,omitempty"` +} - // +kubebuilder:validation:Optional - // +kubebuilder:default="/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" - // CaBundleMount - dst location to mount the CA cert bundle - CaBundleMount string `json:"caBundleMount"` +// Service contains server-specific TLS secret +// +kubebuilder:object:generate:=false +type Service struct { + // SecretName - holding the cert, key for the service + SecretName string `json:"secretName"` + + // CertMount - dst location to mount the service tls.crt cert. Can be used to override the default location which is /etc/tls/certs/.crt + CertMount *string `json:"certMount,omitempty"` + + // KeyMount - dst location to mount the service tls.key key. Can be used to override the default location which is /etc/tls/private/.key + KeyMount *string `json:"keyMount,omitempty"` + + // CaMount - dst location to mount the CA cert ca.crt to. Can be used if the service CA cert should be mounted specifically, e.g. to be set in a service config for validation, instead of the env wide bundle. + CaMount *string `json:"caMount,omitempty"` } // TLS - a generic type, which encapsulates both the service and CA configurations // Service is for the services with a single endpoint // TypedSecretName handles multiple service endpoints with respective secrets +// +kubebuilder:object:generate:=false type TLS struct { // certificate configuration for API service certs APIService map[service.Endpoint]Service `json:"APIService"` // certificate configuration for additional arbitrary certs Service map[string]Service `json:"service"` // CA bundle configuration - Ca *Ca `json:"ca"` + *Ca `json:",inline"` } -// NewTLS - initialize and return a TLS struct -func NewTLS(ctx context.Context, h *helper.Helper, namespace string, serviceMap map[string]Service, endpointMap map[string]service.Endpoint, ca *Ca) (*TLS, error) { +// Enabled - returns true if the tls is not disabled for the service and +// TLS endpoint configuration is available +func (a *APIService) Enabled() bool { + return (a.Disabled == nil || (a.Disabled != nil && !*a.Disabled)) && + a.Endpoint != nil +} - apiService := make(map[service.Endpoint]Service) +// ToService - convert tls.APIService to tls.Service +func (s *GenericService) ToService() (*Service, error) { + toS := &Service{} - // Ensure service SecretName exists for each service in the map or return an error - for serviceName, service := range serviceMap { - if service.SecretName != "" { - secretData, _, err := secret.GetSecret(ctx, h, service.SecretName, namespace) - if err != nil { - return nil, fmt.Errorf("error ensuring secret %s exists for service '%s': %w", service.SecretName, serviceName, err) - } + sBytes, err := json.Marshal(s) + if err != nil { + return nil, fmt.Errorf("error marshalling api service: %w", err) + } - _, keyOk := secretData.Data["tls.key"] - _, certOk := secretData.Data["tls.crt"] - if !keyOk || !certOk { - return nil, fmt.Errorf("secret %s for service '%s' does not contain both tls.key and tls.crt", service.SecretName, serviceName) - } + err = json.Unmarshal(sBytes, toS) + if err != nil { + return nil, fmt.Errorf("error unmarshalling tls service: %w", err) + } + + return toS, nil +} + +// EndpointToServiceMap - converts API.Endpoint into map[service.Endpoint]Service +func (a *APIService) EndpointToServiceMap() (map[service.Endpoint]Service, error) { + sMap := map[service.Endpoint]Service{} + for endpt, cfg := range a.Endpoint { + a, err := cfg.ToService() + if err != nil { + return nil, err } + sMap[endpt] = *a + } - // Use the endpointMap to get the correct Endpoint type for the apiService key - endpoint, ok := endpointMap[serviceName] - if !ok { - return nil, fmt.Errorf("no endpoint defined for service '%s'", serviceName) + return sMap, nil +} + +// ValidateCACertSecret - validates the content of the cert secret to make sure "tls-ca-bundle.pem" key exist +func ValidateCACertSecret( + ctx context.Context, + c client.Client, + caSecret types.NamespacedName, +) (string, ctrl.Result, error) { + hash, ctrlResult, err := secret.VerifySecret( + ctx, + caSecret, + []string{CABundleKey}, + c, + 5*time.Second) + if err != nil { + return "", ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return "", ctrlResult, nil + } + + return hash, ctrl.Result{}, nil +} + +// ValidateCertSecret - validates the content of the cert secret to make sure "tls.key", "tls.crt" and optional "ca.crt" keys exist +func (s *Service) ValidateCertSecret(ctx context.Context, h *helper.Helper, namespace string) (string, ctrl.Result, error) { + // define keys to expect in cert secret + keys := []string{PrivateKey, CertKey} + if s.CaMount != nil { + keys = append(keys, CAKey) + } + + hash, ctrlResult, err := secret.VerifySecret( + ctx, + types.NamespacedName{Name: s.SecretName, Namespace: namespace}, + keys, + h.GetClient(), + 5*time.Second) + if err != nil { + return "", ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return "", ctrlResult, nil + } + + return hash, ctrl.Result{}, nil +} + +// ValidateEndpointCerts - validates all services from an endpointCfgs and +// returns the hash of hashes for all the certificates +func ValidateEndpointCerts( + ctx context.Context, + h *helper.Helper, + namespace string, + endpointCfgs map[service.Endpoint]Service, +) (string, ctrl.Result, error) { + certHashes := map[string]env.Setter{} + for endpt, endpointTLSCfg := range endpointCfgs { + if endpointTLSCfg.SecretName != "" { + // validate the cert secret has the expected keys + hash, ctrlResult, err := endpointTLSCfg.ValidateCertSecret(ctx, h, namespace) + if err != nil { + return "", ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return "", ctrlResult, nil + } + + certHashes["cert-"+endpt.String()] = env.SetValue(hash) } - apiService[endpoint] = service } - return &TLS{ - APIService: apiService, - Service: serviceMap, - Ca: ca, - }, nil + certsHash, err := util.HashOfInputHashes(certHashes) + if err != nil { + return "", ctrl.Result{}, err + } + return certsHash, ctrl.Result{}, nil } // CreateVolumeMounts - add volume mount for TLS certificates and CA certificate for the service -func (s *Service) CreateVolumeMounts() []corev1.VolumeMount { - var volumeMounts []corev1.VolumeMount +func (s *Service) CreateVolumeMounts(serviceID string) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{} + + if serviceID == "" { + serviceID = "default" + } if s.SecretName != "" { - certMountPath := "/etc/pki/tls/certs/tls.crt" + certMountPath := fmt.Sprintf("/etc/pki/tls/certs/%s.crt", serviceID) if s.CertMount != nil { certMountPath = *s.CertMount } - keyMountPath := "/etc/pki/tls/private/tls.key" + keyMountPath := fmt.Sprintf("/etc/pki/tls/private/%s.key", serviceID) if s.KeyMount != nil { keyMountPath = *s.KeyMount } volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "tls-crt", + Name: serviceID + "-tls-certs", MountPath: certMountPath, - SubPath: "tls.crt", + SubPath: CertKey, ReadOnly: true, }, corev1.VolumeMount{ - Name: "tls-key", + Name: serviceID + "-tls-certs", MountPath: keyMountPath, - SubPath: "tls.key", + SubPath: PrivateKey, ReadOnly: true, }) - } - if s.CaMount != nil { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "ca-certs", - MountPath: *s.CaMount, - SubPath: "ca.crt", - ReadOnly: true, - }) + if s.CaMount != nil { + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: serviceID + "-tls-certs", + MountPath: *s.CaMount, + SubPath: CAKey, + ReadOnly: true, + }) + } } return volumeMounts } -// CreateVolumes - add volume for TLS certificates and CA certificate for the service -func (s *Service) CreateVolumes() []corev1.Volume { - var volumes []corev1.Volume - +// CreateVolume - add volume for TLS certificates and CA certificate for the service +func (s *Service) CreateVolume(serviceID string) corev1.Volume { + volume := corev1.Volume{} + if serviceID == "" { + serviceID = "default" + } if s.SecretName != "" { - volume := corev1.Volume{ - Name: "tls-certs", + volume = corev1.Volume{ + Name: serviceID + "-tls-certs", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ SecretName: s.SecretName, @@ -168,93 +322,115 @@ func (s *Service) CreateVolumes() []corev1.Volume { }, }, } - volumes = append(volumes, volume) } - if s.CaMount != nil { - caVolume := corev1.Volume{ - Name: "ca-certs", + return volume +} + +// CreateVolumeMounts creates volume mounts for CA bundle file +func (c *Ca) CreateVolumeMounts(caBundleMount *string) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{} + + if caBundleMount == nil { + caBundleMount = ptr.To("/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem") + } + + if c.CaBundleSecretName != "" { + volumeMounts = []corev1.VolumeMount{ + { + Name: CABundleLabel, + MountPath: *caBundleMount, + SubPath: CABundleKey, + ReadOnly: true, + }, + } + } + + return volumeMounts +} + +// CreateVolume creates volumes for CA bundle file +func (c *Ca) CreateVolume() corev1.Volume { + volume := corev1.Volume{} + + if c.CaBundleSecretName != "" { + volume = corev1.Volume{ + Name: CABundleLabel, VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: *s.CaMount, + SecretName: c.CaBundleSecretName, DefaultMode: ptr.To[int32](0444), }, }, } - volumes = append(volumes, caVolume) } - return volumes + return volume } -// CreateVolumeMounts creates volume mounts for CA bundle file -func (c *Ca) CreateVolumeMounts() []corev1.VolumeMount { - var volumeMounts []corev1.VolumeMount - - if c.CaBundleMount != "" { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: CABundleLabel, - MountPath: c.CaBundleMount, - ReadOnly: true, - }) - } +// NewTLS - initialize and return a TLS struct +func NewTLS(ctx context.Context, h *helper.Helper, namespace string, serviceMap map[string]Service, endpointMap map[string]service.Endpoint, ca *Ca) (*TLS, ctrl.Result, error) { - return volumeMounts -} + apiService := make(map[service.Endpoint]Service) -// CreateVolumes creates volumes for CA bundle file -func (c *Ca) CreateVolumes() []corev1.Volume { - var volumes []corev1.Volume + // Ensure service SecretName exists for each service in the map or return an error + for serviceName, service := range serviceMap { + _, ctrlResult, err := service.ValidateCertSecret(ctx, h, namespace) + if err != nil { + return nil, ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return nil, ctrlResult, nil + } - volume := corev1.Volume{ - Name: CABundleLabel, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: c.CaBundleSecretName, - DefaultMode: ptr.To[int32](0444), - }, - }, + // Use the endpointMap to get the correct Endpoint type for the apiService key + endpoint, ok := endpointMap[serviceName] + if !ok { + return nil, ctrl.Result{}, fmt.Errorf("no endpoint defined for service '%s'", serviceName) + } + apiService[endpoint] = service } - volumes = append(volumes, volume) - return volumes + return &TLS{ + APIService: apiService, + Service: serviceMap, + Ca: ca, + }, ctrl.Result{}, nil } // CreateDatabaseClientConfig - connection flags for the MySQL client // Configures TLS connections for clients that use TLS certificates // returns a string of mysql config statements // (vfisarov): Note dciabrin to recheck this after updates -func (t *TLS) CreateDatabaseClientConfig() string { +func (t *TLS) CreateDatabaseClientConfig(caBundleMount *string) string { conn := []string{} // This assumes certificates are always injected in // a common directory for all services for _, service := range t.Service { - if service.SecretName != "" { - certPath := "/etc/pki/tls/certs/tls.crt" - keyPath := "/etc/pki/tls/private/tls.key" - // Override paths if custom mounts are defined - if service.CertMount != nil { - certPath = *service.CertMount - } - if service.KeyMount != nil { - keyPath = *service.KeyMount - } + certPath := "/etc/pki/tls/certs/tls.crt" + keyPath := "/etc/pki/tls/private/tls.key" - conn = append(conn, - fmt.Sprintf("ssl-cert=%s", certPath), - fmt.Sprintf("ssl-key=%s", keyPath), - ) + // Override paths if custom mounts are defined + if service.CertMount != nil { + certPath = *service.CertMount + } + if service.KeyMount != nil { + keyPath = *service.KeyMount } + + conn = append(conn, + fmt.Sprintf("ssl-cert=%s", certPath), + fmt.Sprintf("ssl-key=%s", keyPath), + ) } // Client uses a CA certificate that gets merged // into the pod's CA bundle by kolla_start if t.Ca != nil && t.Ca.CaBundleSecretName != "" { caPath := "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" - if t.Ca.CaBundleMount != "" { - caPath = t.Ca.CaBundleMount + if caBundleMount != nil { + caPath = *caBundleMount } conn = append(conn, fmt.Sprintf("ssl-ca=%s", caPath)) } diff --git a/modules/common/tls/tls_test.go b/modules/common/tls/tls_test.go index b2aaf77c..d06e85c4 100644 --- a/modules/common/tls/tls_test.go +++ b/modules/common/tls/tls_test.go @@ -17,105 +17,380 @@ limitations under the License. package tls import ( - "strings" "testing" corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" + + . "github.com/onsi/gomega" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" ) -func TestCreateVolumeMounts(t *testing.T) { - caCert := "ca-cert" +func TestAPIEnabled(t *testing.T) { tests := []struct { - name string - service *Service - wantMountsLen int + name string + api *APIService + want bool }{ { - name: "No Secrets", - service: &Service{}, - wantMountsLen: 0, + name: "empty API", + api: &APIService{}, + want: false, }, { - name: "Only TLS Secret", - service: &Service{SecretName: "test-tls-secret"}, - wantMountsLen: 2, + name: "defined API Endpoint map", + api: &APIService{ + Disabled: nil, + Endpoint: map[service.Endpoint]GenericService{}, + }, + want: true, }, { - name: "Only CA Secret", - service: &Service{ - CaMount: &caCert, + name: "empty API Endpoint map", + api: &APIService{ + Disabled: ptr.To(true), + Endpoint: map[service.Endpoint]GenericService{}, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + g.Expect(tt.api.Enabled()).To(BeEquivalentTo(tt.want)) + }) + } +} + +func TestAPIEndpointToService(t *testing.T) { + tests := []struct { + name string + api *APIService + want map[service.Endpoint]Service + }{ + { + name: "empty API", + api: &APIService{}, + want: map[service.Endpoint]Service{}, + }, + { + name: "empty API.Endpoint", + api: &APIService{ + Endpoint: map[service.Endpoint]GenericService{}, + }, + want: map[service.Endpoint]Service{}, + }, + { + name: "empty API.Endpoint entry", + api: &APIService{ + Endpoint: map[service.Endpoint]GenericService{ + service.EndpointInternal: {}, + }, + }, + want: map[service.Endpoint]Service{}, + }, + { + name: "empty API.Endpoint entry", + api: &APIService{ + Endpoint: map[service.Endpoint]GenericService{ + service.EndpointInternal: { + SecretName: ptr.To("foo"), + }, + }, + }, + want: map[service.Endpoint]Service{ + service.EndpointInternal: { + SecretName: "foo", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + s, err := tt.api.EndpointToServiceMap() + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(s).NotTo(BeNil()) + }) + } +} + +func TestGenericServiceToService(t *testing.T) { + tests := []struct { + name string + service *GenericService + want Service + }{ + { + name: "empty APIService", + service: &GenericService{}, + want: Service{}, + }, + { + name: "APIService SecretName specified", + service: &GenericService{ + SecretName: ptr.To("foo"), + }, + want: Service{ + SecretName: "foo", + }, + }, + { + name: "APIService SecretName nil", + service: &GenericService{ + SecretName: nil, + }, + want: Service{ + SecretName: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + s, err := tt.service.ToService() + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(s).NotTo(BeNil()) + }) + } +} + +func TestServiceCreateVolumeMounts(t *testing.T) { + tests := []struct { + name string + service *Service + id string + want []corev1.VolumeMount + }{ + { + name: "No TLS Secret", + service: &Service{}, + id: "foo", + want: []corev1.VolumeMount{}, + }, + { + name: "Only TLS Secret", + service: &Service{SecretName: "cert-secret"}, + id: "foo", + want: []corev1.VolumeMount{ + { + MountPath: "/etc/pki/tls/certs/foo.crt", + Name: "foo-tls-certs", + ReadOnly: true, + SubPath: "tls.crt", + }, + { + MountPath: "/etc/pki/tls/private/foo.key", + Name: "foo-tls-certs", + ReadOnly: true, + SubPath: "tls.key", + }, + }, + }, + { + name: "Only TLS Secret no serviceID", + service: &Service{SecretName: "cert-secret"}, + want: []corev1.VolumeMount{ + { + MountPath: "/etc/pki/tls/certs/default.crt", + Name: "default-tls-certs", + ReadOnly: true, + SubPath: "tls.crt", + }, + { + MountPath: "/etc/pki/tls/private/default.key", + Name: "default-tls-certs", + ReadOnly: true, + SubPath: "tls.key", + }, }, - wantMountsLen: 1, }, { name: "TLS and CA Secrets", service: &Service{ - SecretName: "test-tls-secret", - CaMount: &caCert, + SecretName: "cert-secret", + CaMount: ptr.To("/mount/my/ca.crt"), + }, + id: "foo", + want: []corev1.VolumeMount{ + { + MountPath: "/etc/pki/tls/certs/foo.crt", + Name: "foo-tls-certs", + ReadOnly: true, + SubPath: "tls.crt", + }, + { + MountPath: "/etc/pki/tls/private/foo.key", + Name: "foo-tls-certs", + ReadOnly: true, + SubPath: "tls.key", + }, + { + MountPath: "/mount/my/ca.crt", + Name: "foo-tls-certs", + ReadOnly: true, + SubPath: "ca.crt", + }, }, - wantMountsLen: 3, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - mounts := tt.service.CreateVolumeMounts() - if len(mounts) != tt.wantMountsLen { - t.Errorf("CreateVolumeMounts() got = %v mounts, want %v mounts", len(mounts), tt.wantMountsLen) - } + g := NewWithT(t) + + mounts := tt.service.CreateVolumeMounts(tt.id) + g.Expect(mounts).To(HaveLen(len(tt.want))) + g.Expect(mounts).To(Equal(tt.want)) }) } } -func TestCreateVolumes(t *testing.T) { +func TestServiceCreateVolume(t *testing.T) { tests := []struct { - name string - serviceMap map[string]Service - ca *Ca - wantVolLen int + name string + service *Service + id string + want corev1.Volume }{ { - name: "No Secrets", - serviceMap: map[string]Service{}, - ca: &Ca{}, - wantVolLen: 0, - }, - { - name: "Only TLS Secret", - serviceMap: map[string]Service{"test-service": {SecretName: "test-tls-secret"}}, - ca: &Ca{}, - wantVolLen: 1, - }, - // { - // name: "Only CA Secret", - // serviceMap: map[string]Service{}, - // ca: &Ca{CaBundleSecretName: "test-ca1"}, - // wantVolLen: 1, - // }, - // { - // name: "TLS and CA Secrets", - // serviceMap: map[string]Service{"test-service": {SecretName: "test-tls-secret"}}, - // ca: &Ca{CaBundleSecretName: "test-ca1"}, - // wantVolLen: 2, - // }, - // { - // name: "TLS with Custom CA Mount", - // serviceMap: map[string]Service{"test-service": {SecretName: "test-tls-secret", CaMount: ptr.String("custom-ca-mount")}}, - // ca: &Ca{CaBundleSecretName: "test-ca1"}, - // wantVolLen: 3, - // }, + name: "No Secrets", + service: &Service{}, + want: corev1.Volume{}, + }, + { + name: "Only TLS Secret", + service: &Service{SecretName: "cert-secret"}, + id: "foo", + want: corev1.Volume{ + Name: "foo-tls-certs", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "cert-secret", + DefaultMode: ptr.To[int32](0440), + }, + }, + }, + }, + { + name: "Only TLS Secret no serviceID", + service: &Service{SecretName: "cert-secret"}, + want: corev1.Volume{ + Name: "default-tls-certs", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "cert-secret", + DefaultMode: ptr.To[int32](0440), + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tlsInstance := &TLS{Service: tt.serviceMap, Ca: tt.ca} - volumes := make([]corev1.Volume, 0) - for _, svc := range tlsInstance.Service { - volumes = append(volumes, svc.CreateVolumes()...) - } - if len(volumes) != tt.wantVolLen { - t.Errorf("CreateVolumes() got = %v volumes, want %v volumes", len(volumes), tt.wantVolLen) - } + g := NewWithT(t) + + volume := tt.service.CreateVolume(tt.id) + g.Expect(volume).To(Equal(tt.want)) + }) + } +} + +func TestCACreateVolumeMounts(t *testing.T) { + tests := []struct { + name string + ca *Ca + caBundleMount *string + want []corev1.VolumeMount + }{ + { + name: "Empty Ca", + ca: &Ca{}, + want: []corev1.VolumeMount{}, + }, + { + name: "Only CaBundleSecretName no caBundleMount", + ca: &Ca{ + CaBundleSecretName: "ca-secret", + }, + want: []corev1.VolumeMount{ + { + MountPath: "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + Name: "combined-ca-bundle", + ReadOnly: true, + SubPath: "tls-ca-bundle.pem", + }, + }, + }, + { + name: "CaBundleSecretName and caBundleMount", + ca: &Ca{ + CaBundleSecretName: "ca-secret", + }, + caBundleMount: ptr.To("/mount/my/ca.crt"), + want: []corev1.VolumeMount{ + { + MountPath: "/mount/my/ca.crt", + Name: "combined-ca-bundle", + ReadOnly: true, + SubPath: "tls-ca-bundle.pem", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + mounts := tt.ca.CreateVolumeMounts(tt.caBundleMount) + g.Expect(mounts).To(HaveLen(len(tt.want))) + g.Expect(mounts).To(Equal(tt.want)) + }) + } +} + +func TestCaCreateVolume(t *testing.T) { + tests := []struct { + name string + ca *Ca + want corev1.Volume + }{ + { + name: "Empty Ca", + ca: &Ca{}, + want: corev1.Volume{}, + }, + { + name: "Set CaBundleSecretName", + ca: &Ca{ + CaBundleSecretName: "ca-secret", + }, + want: corev1.Volume{ + Name: "combined-ca-bundle", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "ca-secret", + DefaultMode: ptr.To[int32](0444), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + volume := tt.ca.CreateVolume() + g.Expect(volume).To(Equal(tt.want)) }) } } @@ -160,24 +435,16 @@ func TestGenerateTLSConnectionConfig(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + tlsInstance := &TLS{Service: tt.services, Ca: tt.ca} - configStr := tlsInstance.CreateDatabaseClientConfig() - var missingStmts []string + configStr := tlsInstance.CreateDatabaseClientConfig(nil) + for _, stmt := range tt.wantStmts { - if !strings.Contains(configStr, stmt) { - missingStmts = append(missingStmts, stmt) - } + g.Expect(configStr).To(ContainSubstring(stmt)) } - var unexpectedStmts []string for _, stmt := range tt.excludeStmts { - if strings.Contains(configStr, stmt) { - unexpectedStmts = append(unexpectedStmts, stmt) - } - } - if len(missingStmts) != 0 || len(unexpectedStmts) != 0 { - t.Errorf("CreateDatabaseClientConfig() "+ - "missing statements: %v, unexpected statements: %v", - missingStmts, unexpectedStmts) + g.Expect(configStr).ToNot(ContainSubstring(stmt)) } }) } diff --git a/modules/common/tls/zz_generated.deepcopy.go b/modules/common/tls/zz_generated.deepcopy.go index e3acd747..68bacaa9 100644 --- a/modules/common/tls/zz_generated.deepcopy.go +++ b/modules/common/tls/zz_generated.deepcopy.go @@ -25,6 +25,52 @@ import ( "github.com/openstack-k8s-operators/lib-common/modules/common/service" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIDBMessaging) DeepCopyInto(out *APIDBMessaging) { + *out = *in + in.API.DeepCopyInto(&out.API) + in.DB.DeepCopyInto(&out.DB) + in.Messaging.DeepCopyInto(&out.Messaging) + out.Ca = in.Ca +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIDBMessaging. +func (in *APIDBMessaging) DeepCopy() *APIDBMessaging { + if in == nil { + return nil + } + out := new(APIDBMessaging) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIService) DeepCopyInto(out *APIService) { + *out = *in + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) + **out = **in + } + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint + *out = make(map[service.Endpoint]GenericService, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIService. +func (in *APIService) DeepCopy() *APIService { + if in == nil { + return nil + } + out := new(APIService) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Ca) DeepCopyInto(out *Ca) { *out = *in @@ -41,65 +87,66 @@ func (in *Ca) DeepCopy() *Ca { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Service) DeepCopyInto(out *Service) { +func (in *DB) DeepCopyInto(out *DB) { *out = *in - if in.CertMount != nil { - in, out := &in.CertMount, &out.CertMount - *out = new(string) + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) **out = **in } - if in.KeyMount != nil { - in, out := &in.KeyMount, &out.KeyMount +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DB. +func (in *DB) DeepCopy() *DB { + if in == nil { + return nil + } + out := new(DB) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericService) DeepCopyInto(out *GenericService) { + *out = *in + if in.SecretName != nil { + in, out := &in.SecretName, &out.SecretName *out = new(string) **out = **in } - if in.CaMount != nil { - in, out := &in.CaMount, &out.CaMount + if in.IssuerName != nil { + in, out := &in.IssuerName, &out.IssuerName *out = new(string) **out = **in } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service. -func (in *Service) DeepCopy() *Service { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericService. +func (in *GenericService) DeepCopy() *GenericService { if in == nil { return nil } - out := new(Service) + out := new(GenericService) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TLS) DeepCopyInto(out *TLS) { +func (in *Messaging) DeepCopyInto(out *Messaging) { *out = *in - if in.APIService != nil { - in, out := &in.APIService, &out.APIService - *out = make(map[service.Endpoint]Service, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } - if in.Service != nil { - in, out := &in.Service, &out.Service - *out = make(map[string]Service, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } - if in.Ca != nil { - in, out := &in.Ca, &out.Ca - *out = new(Ca) + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) **out = **in } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLS. -func (in *TLS) DeepCopy() *TLS { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Messaging. +func (in *Messaging) DeepCopy() *Messaging { if in == nil { return nil } - out := new(TLS) + out := new(Messaging) in.DeepCopyInto(out) return out }