From 21c976b15f548b3ce2ea6b97ec0d82594c6c1dff Mon Sep 17 00:00:00 2001 From: Veronika Fisarova Date: Mon, 6 Nov 2023 11:20:59 +0100 Subject: [PATCH] Add TLS support for multiple endpoints and support for creating custom Issuer Some services create multiple endpoints, therefore each needs to support TLS. Also add TLS support for DB and Messaging services. Signed-off-by: Veronika Fisarova Depends-On: https://github.com/openstack-k8s-operators/lib-common/pull/399 Co-authored-by: Martin Schuppert --- modules/common/test/functional/tls_test.go | 136 ++++++ modules/common/tls/tls.go | 432 ++++++++++++++++---- modules/common/tls/tls_test.go | 431 +++++++++++++++---- modules/common/tls/zz_generated.deepcopy.go | 72 +++- 4 files changed, 894 insertions(+), 177 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 422d5e82..fd55e7b2 100644 --- a/modules/common/tls/tls.go +++ b/modules/common/tls/tls.go @@ -20,13 +20,22 @@ 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" + "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const ( @@ -45,133 +54,418 @@ const ( DownstreamTLSCABundlePath = "/etc/pki/ca-trust/extracted/pem/" + CABundleKey // UpstreamTLSCABundlePath - UpstreamTLSCABundlePath = "/etc/ssl/certs/ca-certificates.crt" + // CABundleKey - key of the secret entry holding the ca bundle + + // 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 { +// SimpleService defines the observed state of TLS for a single service +type SimpleService struct { + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing certificates for the service + GenericService `json:",inline"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Secret containing CA bundle + Ca `json:",inline"` +} + +// API defines the observed state of TLS with API only +type API struct { + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // API tls type which encapsulates for API services + API APIService `json:"api,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 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 + // Public GenericService - holds the secret for the public endpoint + Public GenericService `json:"public,omitempty"` + + // +kubebuilder:validation:optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // Internal GenericService - holds the secret for the internal endpoint + Internal GenericService `json:"internal,omitempty"` +} + +// GenericService contains server-specific TLS secret or issuer +type GenericService struct { // +kubebuilder:validation:Optional - DisableNonTLSListeners bool `json:"disableNonTLSListeners,omitempty"` + // SecretName - holding the cert, key for the service + SecretName *string `json:"secretName,omitempty"` } // 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 - CaSecretName string `json:"caSecretName,omitempty"` + // CaBundleSecretName - holding the CA certs in a pre-created bundle file + CaBundleSecretName string `json:"caBundleSecretName,omitempty"` } -// TLS - a generic type, which encapsulates both the service and CA configurations -type TLS struct { - Service *Service `json:"service"` - Ca *Ca `json:"ca"` +// 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"` } -// NewTLS - initialize and return a TLS struct -func NewTLS(ctx context.Context, h *helper.Helper, namespace string, service *Service, ca *Ca) (*TLS, error) { +// Enabled - returns true if TLS is configured for the service +func (s *GenericService) Enabled() bool { + return s.SecretName != nil && *s.SecretName != "" +} + +// Enabled - returns true if TLS is configured for the public and internal +func (a *APIService) Enabled(endpt service.Endpoint) bool { + switch endpt { + case service.EndpointPublic: + return (a.Disabled == nil || (a.Disabled != nil && !*a.Disabled)) && a.Public.Enabled() + case service.EndpointInternal: + return (a.Disabled == nil || (a.Disabled != nil && !*a.Disabled)) && a.Internal.Enabled() + } + + return false +} + +// ValidateCertSecrets - validates the content of the cert secrets to make sure "tls-ca-bundle.pem" key exists +func (a *APIService) ValidateCertSecrets( + ctx context.Context, + h *helper.Helper, + namespace string, +) (string, ctrl.Result, error) { + var svc GenericService + certHashes := map[string]env.Setter{} + for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { + switch endpt { + case service.EndpointPublic: + if !a.Enabled(service.EndpointPublic) { + continue + } + + svc = a.Public - // Ensure service SecretName exists or return an error - if service != nil && service.SecretName != "" { - secretData, _, err := secret.GetSecret(ctx, h, service.SecretName, namespace) + case service.EndpointInternal: + if !a.Enabled(service.EndpointInternal) { + continue + } + + svc = a.Internal + } + + hash, ctrlResult, err := svc.ValidateCertSecret(ctx, h, namespace) if err != nil { - return nil, fmt.Errorf("error ensuring secret %s exists: %w", service.SecretName, err) + return "", ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return "", ctrlResult, nil } + certHashes["cert-"+endpt.String()] = env.SetValue(hash) + } + + certsHash, err := util.HashOfInputHashes(certHashes) + if err != nil { + return "", ctrl.Result{}, err + } + return certsHash, ctrl.Result{}, nil +} + +// ToService - convert tls.APIService to tls.Service +func (s *GenericService) ToService() (*Service, error) { + toS := &Service{} + + sBytes, err := json.Marshal(s) + if err != nil { + return nil, fmt.Errorf("error marshalling api service: %w", err) + } + + err = json.Unmarshal(sBytes, toS) + if err != nil { + return nil, fmt.Errorf("error unmarshalling tls service: %w", err) + } + + return toS, nil +} + +// ValidateCertSecret - validates the content of the cert secrets to make sure "tls-ca-bundle.pem" key exists +func (s *GenericService) ValidateCertSecret( + ctx context.Context, + h *helper.Helper, + namespace string, +) (string, ctrl.Result, error) { + hash := "" + + endptTLSCfg, err := s.ToService() + if err != nil { + return "", ctrl.Result{}, err + } - _, keyOk := secretData.Data["tls.key"] - _, certOk := secretData.Data["tls.crt"] - if !keyOk || !certOk { - return nil, fmt.Errorf("secret %s does not contain both tls.key and tls.crt", service.SecretName) + if endptTLSCfg.SecretName != "" { + // validate the cert secret has the expected keys + var ctrlResult reconcile.Result + hash, ctrlResult, err = endptTLSCfg.ValidateCertSecret(ctx, h, namespace) + if err != nil { + return "", ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return "", ctrlResult, nil } } - return &TLS{ - Service: service, - Ca: ca, - }, nil + return hash, ctrl.Result{}, nil } -// CreateVolumeMounts - add volume mount for TLS certificate and CA certificates, this counts on openstack-operator providing CA certs with unique names -func (t *TLS) CreateVolumeMounts() []corev1.VolumeMount { - var volumeMounts []corev1.VolumeMount +// ValidateCACertSecret - validates the content of the cert secret to make sure "tls-ca-bundle.pem" key exists +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 + } - if t.Service != nil && t.Service.SecretName != "" { - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "tls-crt", - MountPath: "/etc/pki/tls/certs/tls.crt", - SubPath: "tls.crt", - ReadOnly: true, - }) - volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "tls-key", - MountPath: "/etc/pki/tls/certs/tls.key", - SubPath: "tls.key", - ReadOnly: true, - }) + 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) + } + } + + certsHash, err := util.HashOfInputHashes(certHashes) + if err != nil { + return "", ctrl.Result{}, err + } + return certsHash, ctrl.Result{}, nil +} + +// getCertMountPath - return certificate mount path +func (s *Service) getCertMountPath(serviceID string) string { + if serviceID == "" { + serviceID = "default" } - if t.Ca != nil && t.Ca.CaSecretName != "" { + certMountPath := fmt.Sprintf("/etc/pki/tls/certs/%s.crt", serviceID) + if s.CertMount != nil { + certMountPath = *s.CertMount + } + + return certMountPath +} + +// getKeyMountPath - return key mount path +func (s *Service) getKeyMountPath(serviceID string) string { + if serviceID == "" { + serviceID = "default" + } + + keyMountPath := fmt.Sprintf("/etc/pki/tls/private/%s.key", serviceID) + if s.KeyMount != nil { + keyMountPath = *s.KeyMount + } + + return keyMountPath +} + +// CreateVolumeMounts - add volume mount for TLS certificates and CA certificate for the service +func (s *Service) CreateVolumeMounts(serviceID string) []corev1.VolumeMount { + volumeMounts := []corev1.VolumeMount{} + if serviceID == "" { + serviceID = "default" + } + if s.SecretName != "" { volumeMounts = append(volumeMounts, corev1.VolumeMount{ - Name: "ca-certs", - MountPath: "/etc/pki/ca-trust/extracted/pem", + Name: serviceID + "-tls-certs", + MountPath: s.getCertMountPath(serviceID), + SubPath: CertKey, + ReadOnly: true, + }, corev1.VolumeMount{ + Name: serviceID + "-tls-certs", + MountPath: s.getKeyMountPath(serviceID), + SubPath: PrivateKey, 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 certificate and CA certificates -func (t *TLS) CreateVolumes() []corev1.Volume { - var volumes []corev1.Volume - - if t.Service != nil && t.Service.SecretName != "" { - volumes = append(volumes, corev1.Volume{ - Name: "tls-certs", +// 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: serviceID + "-tls-certs", VolumeSource: corev1.VolumeSource{ Secret: &corev1.SecretVolumeSource{ - SecretName: t.Service.SecretName, + SecretName: s.SecretName, DefaultMode: ptr.To[int32](0440), }, }, - }) + } + } + + 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, + }, + } } - if t.Ca != nil && t.Ca.CaSecretName != "" { - volumes = append(volumes, corev1.Volume{ - Name: "ca-certs", + 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: t.Ca.CaSecretName, + SecretName: c.CaBundleSecretName, DefaultMode: ptr.To[int32](0444), }, }, - }) + } } - return volumes + return volume } // CreateDatabaseClientConfig - connection flags for the MySQL client // Configures TLS connections for clients that use TLS certificates // returns a string of mysql config statements -func (t *TLS) CreateDatabaseClientConfig() string { +// With the serviceID it is possible to control which certificate +// to be use if there are multiple mounted to the deployment. +func (s *Service) CreateDatabaseClientConfig(serviceID string) string { conn := []string{} - // This assumes certificates are always injected in - // a common directory for all services - if t.Service.SecretName != "" { + + if serviceID != "" || (s.CertMount != nil && s.KeyMount != nil) { + certPath := s.getCertMountPath(serviceID) + keyPath := s.getKeyMountPath(serviceID) + conn = append(conn, - "ssl-cert=/etc/pki/tls/certs/tls.crt", - "ssl-key=/etc/pki/tls/private/tls.key") + 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.CaSecretName != "" { - conn = append(conn, - "ssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem") + + // Client uses a CA certificate + caPath := "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem" + if s.CaMount != nil { + caPath = *s.CaMount } + conn = append(conn, fmt.Sprintf("ssl-ca=%s", caPath)) + if len(conn) > 0 { conn = append([]string{"ssl=1"}, conn...) } + return strings.Join(conn, "\n") } diff --git a/modules/common/tls/tls_test.go b/modules/common/tls/tls_test.go index 8b494a12..138b2265 100644 --- a/modules/common/tls/tls_test.go +++ b/modules/common/tls/tls_test.go @@ -17,156 +17,407 @@ 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) { +func TestAPIEnabled(t *testing.T) { tests := []struct { - name string - service *Service - ca *Ca - wantMountsLen int + name string + endpt service.Endpoint + api *APIService + want bool }{ { - name: "No Secrets", - service: &Service{}, - ca: &Ca{}, - wantMountsLen: 0, + name: "empty API", + endpt: service.EndpointInternal, + api: &APIService{}, + want: false, + }, + { + name: "Internal SecretName nil", + endpt: service.EndpointInternal, + api: &APIService{ + Internal: GenericService{SecretName: nil}, + Public: GenericService{SecretName: nil}, + }, + want: false, }, { - name: "Only TLS Secret", - service: &Service{SecretName: "test-tls-secret"}, - ca: &Ca{}, - wantMountsLen: 2, + name: "Internal SecretName defined", + endpt: service.EndpointInternal, + api: &APIService{ + Internal: GenericService{SecretName: ptr.To("foo")}, + Public: GenericService{SecretName: nil}, + }, + want: true, }, { - name: "Only CA Secret", - service: &Service{}, - ca: &Ca{CaSecretName: "test-ca1"}, - wantMountsLen: 1, + name: "Public SecretName nil", + endpt: service.EndpointPublic, + api: &APIService{ + Internal: GenericService{SecretName: nil}, + Public: GenericService{SecretName: nil}, + }, + want: false, }, { - name: "TLS and CA Secrets", - service: &Service{SecretName: "test-tls-secret"}, - ca: &Ca{CaSecretName: "test-ca1"}, - wantMountsLen: 3, + name: "Public SecretName defined", + endpt: service.EndpointPublic, + api: &APIService{ + Internal: GenericService{SecretName: nil}, + Public: GenericService{SecretName: ptr.To("foo")}, + }, + want: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tlsInstance := &TLS{Service: tt.service, Ca: tt.ca} - mounts := tlsInstance.CreateVolumeMounts() - if len(mounts) != tt.wantMountsLen { - t.Errorf("CreateVolumeMounts() got = %v mounts, want %v mounts", len(mounts), tt.wantMountsLen) - } + g := NewWithT(t) + + g.Expect(tt.api.Enabled(tt.endpt)).To(BeEquivalentTo(tt.want)) }) } } -func TestCreateVolumes(t *testing.T) { +func TestGenericServiceToService(t *testing.T) { tests := []struct { - name string - service *Service - ca *Ca - wantVolLen int + name string + service *GenericService + want Service }{ { - name: "No Secrets", - service: &Service{}, - ca: &Ca{}, - wantVolLen: 0, + 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: "Only TLS Secret", - service: &Service{SecretName: "test-tls-secret"}, - ca: &Ca{}, - wantVolLen: 1, + name: "No TLS Secret", + service: &Service{}, + id: "foo", + want: []corev1.VolumeMount{}, }, { - name: "Only CA Secret", - service: &Service{}, - ca: &Ca{CaSecretName: "test-ca1"}, - wantVolLen: 1, + 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: "TLS and CA Secrets", - service: &Service{SecretName: "test-tls-secret"}, - ca: &Ca{CaSecretName: "test-ca1"}, - wantVolLen: 2, + 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", + }, + }, + }, + { + name: "TLS and CA Secrets", + service: &Service{ + 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", + }, + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tlsInstance := &TLS{Service: tt.service, Ca: tt.ca} - volumes := tlsInstance.CreateVolumes() - if len(volumes) != tt.wantVolLen { - t.Errorf("CreateVolumes() got = %v volumes, want %v volumes", len(volumes), tt.wantVolLen) - } + 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 TestServiceCreateVolume(t *testing.T) { + tests := []struct { + name string + service *Service + id string + want corev1.Volume + }{ + { + 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) { + 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 TestGenerateTLSConnectionConfig(t *testing.T) { +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)) + }) + } +} + +func TestCreateDatabaseClientConfig(t *testing.T) { tests := []struct { name string - service *Service - ca *Ca + service Service + serviceID string wantStmts []string excludeStmts []string }{ { - name: "No Secrets", - service: &Service{}, - ca: &Ca{}, - wantStmts: []string{}, - excludeStmts: []string{"ssl=1", "ssl-cert=", "ssl-key=", "ssl-ca="}, + name: "Only CA Secret", + service: Service{}, + serviceID: "", + wantStmts: []string{"ssl=1", "ssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"}, + excludeStmts: []string{"ssl-cert=", "ssl-key="}, + }, + { + name: "TLS Secret specified", + service: Service{SecretName: "test-tls-secret"}, + serviceID: "foo", + wantStmts: []string{"ssl=1", "ssl-cert=/etc/pki/tls/certs/foo.crt", "ssl-key=/etc/pki/tls/private/foo.key", "ssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"}, + excludeStmts: []string{}, }, { - name: "Only TLS Secret", - service: &Service{SecretName: "test-tls-secret"}, - ca: &Ca{}, - wantStmts: []string{"ssl=1", "ssl-cert=", "ssl-key="}, - excludeStmts: []string{"ssl-ca="}, + name: "TLS and CA custom mount", + service: Service{SecretName: "test-tls-secret", CaMount: ptr.To("/some/path/ca.crt")}, + serviceID: "foo", + wantStmts: []string{"ssl=1", "ssl-cert=/etc/pki/tls/certs/foo.crt", "ssl-key=/etc/pki/tls/private/foo.key", "ssl-ca=/some/path/ca.crt"}, + excludeStmts: []string{}, }, { - name: "Only CA Secret", - service: &Service{}, - ca: &Ca{CaSecretName: "test-ca1"}, - wantStmts: []string{"ssl=1", "ssl-ca="}, - excludeStmts: []string{"ssl-cert=", "ssl-key="}, + name: "TLS custom mount", + service: Service{SecretName: "test-tls-secret", CertMount: ptr.To("/some/path/cert.crt"), KeyMount: ptr.To("/some/path/cert.key")}, + serviceID: "", + wantStmts: []string{"ssl=1", "ssl-cert=/some/path/cert.crt", "ssl-key=/some/path/cert.key", "ssl-ca=/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"}, + excludeStmts: []string{}, }, { - name: "TLS and CA Secrets", - service: &Service{SecretName: "test-tls-secret"}, - ca: &Ca{CaSecretName: "test-ca1"}, - wantStmts: []string{"ssl=1", "ssl-cert=", "ssl-key=", "ssl-ca="}, + name: "TLS custom mount AND CA custom mount", + service: Service{SecretName: "test-tls-secret", CertMount: ptr.To("/some/path/cert.crt"), KeyMount: ptr.To("/some/path/cert.key"), CaMount: ptr.To("/some/path/ca.crt")}, + serviceID: "", + wantStmts: []string{"ssl=1", "ssl-cert=/some/path/cert.crt", "ssl-key=/some/path/cert.key", "ssl-ca=/some/path/ca.crt"}, excludeStmts: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - tlsInstance := &TLS{Service: tt.service, Ca: tt.ca} - configStr := tlsInstance.CreateDatabaseClientConfig() - var missingStmts []string + g := NewWithT(t) + + configStr := tt.service.CreateDatabaseClientConfig(tt.serviceID) + 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 494f27a9..699c84f0 100644 --- a/modules/common/tls/zz_generated.deepcopy.go +++ b/modules/common/tls/zz_generated.deepcopy.go @@ -23,6 +23,45 @@ package tls import () +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *API) DeepCopyInto(out *API) { + *out = *in + in.API.DeepCopyInto(&out.API) + out.Ca = in.Ca +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new API. +func (in *API) DeepCopy() *API { + if in == nil { + return nil + } + out := new(API) + 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 + } + in.Public.DeepCopyInto(&out.Public) + in.Internal.DeepCopyInto(&out.Internal) +} + +// 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 @@ -39,41 +78,38 @@ 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 *GenericService) DeepCopyInto(out *GenericService) { *out = *in + if in.SecretName != nil { + in, out := &in.SecretName, &out.SecretName + *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 *SimpleService) DeepCopyInto(out *SimpleService) { *out = *in - if in.Service != nil { - in, out := &in.Service, &out.Service - *out = new(Service) - **out = **in - } - if in.Ca != nil { - in, out := &in.Ca, &out.Ca - *out = new(Ca) - **out = **in - } + in.GenericService.DeepCopyInto(&out.GenericService) + out.Ca = in.Ca } -// 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 SimpleService. +func (in *SimpleService) DeepCopy() *SimpleService { if in == nil { return nil } - out := new(TLS) + out := new(SimpleService) in.DeepCopyInto(out) return out }