From d4cc24e398ec9a69d011a402be40973b93ef9656 Mon Sep 17 00:00:00 2001 From: Martin Schuppert Date: Fri, 24 Nov 2023 11:38:46 +0100 Subject: [PATCH] [tlse] tls for NeutronAPI pod configuration Public/Internal service cert secrets and the CA bundle secret can be passed to configure httpd virtual hosts for tls termination. The certs get direct mounted to the appropriate place in etc/pki/tls/certs/%s.crt|key and a CA bundle to /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem . Job deployments for bootstrap/cron get the CA bundle added if configured. Also indexes the named input resources for password, CA bundle, and endpoint secrets to be able to watch them for any changes and reconcile if needed. Depends-On: https://github.com/openstack-k8s-operators/lib-common/pull/384 Jira: OSPRH-2197 --- .../neutron.openstack.org_neutronapis.yaml | 30 +++ api/go.mod | 2 + api/go.sum | 4 +- api/v1beta1/neutronapi_types.go | 7 +- api/v1beta1/zz_generated.deepcopy.go | 1 + .../neutron.openstack.org_neutronapis.yaml | 30 +++ .../samples/neutron_v1beta1_neutronapi.yaml | 8 + controllers/neutronapi_controller.go | 178 ++++++++++++- go.mod | 2 + go.sum | 4 +- pkg/neutronapi/dbsync.go | 9 +- pkg/neutronapi/deployment.go | 60 ++++- pkg/neutronapi/volumes.go | 6 + .../neutronapi/httpd/10-neutron-httpd.conf | 15 +- templates/neutronapi/httpd/httpd.conf | 1 + templates/neutronapi/httpd/ssl.conf | 21 ++ test/functional/base_test.go | 28 +- test/functional/neutronapi_controller_test.go | 242 ++++++++++++++++-- 18 files changed, 583 insertions(+), 65 deletions(-) create mode 100644 templates/neutronapi/httpd/ssl.conf diff --git a/api/bases/neutron.openstack.org_neutronapis.yaml b/api/bases/neutron.openstack.org_neutronapis.yaml index 8d5e1efe..7167b5d8 100644 --- a/api/bases/neutron.openstack.org_neutronapis.yaml +++ b/api/bases/neutron.openstack.org_neutronapis.yaml @@ -2237,6 +2237,36 @@ spec: description: ServiceUser - optional username used for this service to register in neutron type: string + tls: + description: TLS - Parameters related to the TLS + properties: + api: + description: API tls type which encapsulates for API services + properties: + internal: + description: Internal GenericService - holds the secret for + the internal endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object + public: + description: Public GenericService - holds the secret for + the public endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object + type: object + caBundleSecretName: + description: CaBundleSecretName - holding the CA certs in a pre-created + bundle file + type: string + type: object required: - containerImage - databaseInstance diff --git a/api/go.mod b/api/go.mod index ea025243..fe0ad0ab 100644 --- a/api/go.mod +++ b/api/go.mod @@ -69,3 +69,5 @@ require ( // mschuppert: map to latest commit from release-4.13 tag // must consistent within modules and service operators replace github.com/openshift/api => github.com/openshift/api v0.0.0-20230414143018-3367bc7e6ac7 //allow-merging + +replace github.com/openstack-k8s-operators/lib-common/modules/common => github.com/deydra71/lib-common/modules/common v0.0.0-20240108150456-e7962ed7031f diff --git a/api/go.sum b/api/go.sum index f2bf052e..c3c4a3fc 100644 --- a/api/go.sum +++ b/api/go.sum @@ -62,6 +62,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deydra71/lib-common/modules/common v0.0.0-20240108150456-e7962ed7031f h1:1DOq6SRvQLbPRrwtoZuA3UyQPMLNYqM2VyNX6JYKgmo= +github.com/deydra71/lib-common/modules/common v0.0.0-20240108150456-e7962ed7031f/go.mod h1:ov4lAbniNUsLqZCBp1RTixpqXc8JlzA5B+yTcCkJXQg= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -220,8 +222,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240106101723-5f7aa263457f h1:ifW2n0TkrS1Wa58DK/N+zAKdGH5XQh4Llk/hefsXzN8= -github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240106101723-5f7aa263457f/go.mod h1:ov4lAbniNUsLqZCBp1RTixpqXc8JlzA5B+yTcCkJXQg= github.com/openstack-k8s-operators/lib-common/modules/storage v0.3.1-0.20240106101723-5f7aa263457f h1:b9fpRkubG+tk6uKGCNz/kuTWYtpUFsm3d/jECF1AmAs= github.com/openstack-k8s-operators/lib-common/modules/storage v0.3.1-0.20240106101723-5f7aa263457f/go.mod h1:MwShIB0G7riRDWXS2JQfcdETm+yutb3qpdnxu/yg+Xk= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/api/v1beta1/neutronapi_types.go b/api/v1beta1/neutronapi_types.go index e24cc96a..b1258c74 100644 --- a/api/v1beta1/neutronapi_types.go +++ b/api/v1beta1/neutronapi_types.go @@ -19,6 +19,7 @@ package v1beta1 import ( "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/service" + "github.com/openstack-k8s-operators/lib-common/modules/common/tls" "github.com/openstack-k8s-operators/lib-common/modules/common/util" "github.com/openstack-k8s-operators/lib-common/modules/storage" @@ -64,7 +65,6 @@ type NeutronAPISpec struct { // Needed to request a transportURL that is created and used in Neutron RabbitMqClusterName string `json:"rabbitMqClusterName"` - // +kubebuilder:validation:Required // +kubebuilder:default=memcached // Memcached instance name. @@ -132,6 +132,11 @@ type NeutronAPISpec struct { // +kubebuilder:validation:Optional // Override, provides the ability to override the generated manifest of several child resources. Override APIOverrideSpec `json:"override,omitempty"` + + // +kubebuilder:validation:Optional + // +operator-sdk:csv:customresourcedefinitions:type=spec + // TLS - Parameters related to the TLS + TLS tls.API `json:"tls,omitempty"` } // APIOverrideSpec to override the generated manifest of several child resources. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 50c0a75c..33d24925 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -177,6 +177,7 @@ func (in *NeutronAPISpec) DeepCopyInto(out *NeutronAPISpec) { } } in.Override.DeepCopyInto(&out.Override) + in.TLS.DeepCopyInto(&out.TLS) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NeutronAPISpec. diff --git a/config/crd/bases/neutron.openstack.org_neutronapis.yaml b/config/crd/bases/neutron.openstack.org_neutronapis.yaml index 8d5e1efe..7167b5d8 100644 --- a/config/crd/bases/neutron.openstack.org_neutronapis.yaml +++ b/config/crd/bases/neutron.openstack.org_neutronapis.yaml @@ -2237,6 +2237,36 @@ spec: description: ServiceUser - optional username used for this service to register in neutron type: string + tls: + description: TLS - Parameters related to the TLS + properties: + api: + description: API tls type which encapsulates for API services + properties: + internal: + description: Internal GenericService - holds the secret for + the internal endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object + public: + description: Public GenericService - holds the secret for + the public endpoint + properties: + secretName: + description: SecretName - holding the cert, key for the + service + type: string + type: object + type: object + caBundleSecretName: + description: CaBundleSecretName - holding the CA certs in a pre-created + bundle file + type: string + type: object required: - containerImage - databaseInstance diff --git a/config/samples/neutron_v1beta1_neutronapi.yaml b/config/samples/neutron_v1beta1_neutronapi.yaml index 682f2edd..a4114f1e 100644 --- a/config/samples/neutron_v1beta1_neutronapi.yaml +++ b/config/samples/neutron_v1beta1_neutronapi.yaml @@ -17,3 +17,11 @@ spec: service: false preserveJobs: false secret: neutron-secret + #tls: + # api: + # disabled: false + # internal: + # secretName: cert-internal-svc + # public: + # secretName: cert-public-svc + # caBundleSecretName: combined-ca-bundle diff --git a/controllers/neutronapi_controller.go b/controllers/neutronapi_controller.go index d579b9e6..4c293aa0 100644 --- a/controllers/neutronapi_controller.go +++ b/controllers/neutronapi_controller.go @@ -23,14 +23,18 @@ import ( "time" "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" + "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" @@ -49,6 +53,7 @@ import ( common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac" "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/tls" "github.com/openstack-k8s-operators/lib-common/modules/common/util" mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" neutronv1beta1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" @@ -200,8 +205,74 @@ func (r *NeutronAPIReconciler) Reconcile(ctx context.Context, req ctrl.Request) return r.reconcileNormal(ctx, instance, helper) } +// fields to index to reconcile when change +const ( + passwordSecretField = ".spec.secret" + caBundleSecretNameField = ".spec.tls.caBundleSecretName" + tlsAPIInternalField = ".spec.tls.api.internal.secretName" + tlsAPIPublicField = ".spec.tls.api.public.secretName" +) + +var ( + allWatchFields = []string{ + passwordSecretField, + caBundleSecretNameField, + tlsAPIInternalField, + tlsAPIPublicField, + } +) + // SetupWithManager - func (r *NeutronAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { + + // index passwordSecretField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &neutronv1beta1.NeutronAPI{}, passwordSecretField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*neutronv1beta1.NeutronAPI) + if cr.Spec.Secret == "" { + return nil + } + return []string{cr.Spec.Secret} + }); err != nil { + return err + } + + // index caBundleSecretNameField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &neutronv1beta1.NeutronAPI{}, caBundleSecretNameField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*neutronv1beta1.NeutronAPI) + if cr.Spec.TLS.CaBundleSecretName == "" { + return nil + } + return []string{cr.Spec.TLS.CaBundleSecretName} + }); err != nil { + return err + } + + // index tlsAPIInternalField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &neutronv1beta1.NeutronAPI{}, tlsAPIInternalField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*neutronv1beta1.NeutronAPI) + if cr.Spec.TLS.API.Internal.SecretName == nil { + return nil + } + return []string{*cr.Spec.TLS.API.Internal.SecretName} + }); err != nil { + return err + } + + // index tlsAPIPublicField + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &neutronv1beta1.NeutronAPI{}, tlsAPIPublicField, func(rawObj client.Object) []string { + // Extract the secret name from the spec, if one is provided + cr := rawObj.(*neutronv1beta1.NeutronAPI) + if cr.Spec.TLS.API.Public.SecretName == nil { + return nil + } + return []string{*cr.Spec.TLS.API.Public.SecretName} + }); err != nil { + return err + } + crs := &neutronv1beta1.NeutronAPIList{} return ctrl.NewControllerManagedBy(mgr). For(&neutronv1beta1.NeutronAPI{}). @@ -218,9 +289,47 @@ func (r *NeutronAPIReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Ma Owns(&rbacv1.RoleBinding{}). Watches(&source.Kind{Type: &ovnclient.OVNDBCluster{}}, handler.EnqueueRequestsFromMapFunc(ovnclient.OVNDBClusterNamespaceMapFunc(crs, mgr.GetClient(), r.GetLogger(ctx)))). Watches(&source.Kind{Type: &memcachedv1.Memcached{}}, handler.EnqueueRequestsFromMapFunc(r.memcachedNamespaceMapFunc(ctx, crs))). + Watches( + &source.Kind{Type: &corev1.Secret{}}, + handler.EnqueueRequestsFromMapFunc(r.findObjectsForSrc), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). Complete(r) } +func (r *NeutronAPIReconciler) findObjectsForSrc(src client.Object) []reconcile.Request { + requests := []reconcile.Request{} + + l := log.FromContext(context.Background()).WithName("Controllers").WithName("NeutronAPI") + + for _, field := range allWatchFields { + crList := &neutronv1beta1.NeutronAPIList{} + listOps := &client.ListOptions{ + FieldSelector: fields.OneTermEqualSelector(field, src.GetName()), + Namespace: src.GetNamespace(), + } + err := r.List(context.TODO(), crList, listOps) + if err != nil { + return []reconcile.Request{} + } + + for _, item := range crList.Items { + l.Info(fmt.Sprintf("input source %s changed, reconcile: %s - %s", src.GetName(), item.GetName(), item.GetNamespace())) + + requests = append(requests, + reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: item.GetName(), + Namespace: item.GetNamespace(), + }, + }, + ) + } + } + + return requests +} + func (r *NeutronAPIReconciler) reconcileDelete(ctx context.Context, instance *neutronv1beta1.NeutronAPI, helper *helper.Helper) (ctrl.Result, error) { Log := r.GetLogger(ctx) Log.Info("Reconciling Service delete") @@ -364,6 +473,39 @@ func (r *NeutronAPIReconciler) reconcileInit( return ctrl.Result{}, err } + // + // TLS input validation + // + // Validate the CA cert secret if provided + if instance.Spec.TLS.CaBundleSecretName != "" { + hash, ctrlResult, err := tls.ValidateCACertSecret( + ctx, + helper.GetClient(), + types.NamespacedName{ + Name: instance.Spec.TLS.CaBundleSecretName, + Namespace: instance.Namespace, + }, + ) + if err != nil { + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + + if hash != "" { + secretVars[tls.CABundleKey] = env.SetValue(hash) + } + } + + // Validate API service certs secrets + certsHash, ctrlResult, err := instance.Spec.TLS.API.ValidateCertSecrets(ctx, helper, instance.Namespace) + if err != nil { + return ctrlResult, err + } else if (ctrlResult != ctrl.Result{}) { + return ctrlResult, nil + } + secretVars[tls.TLSHashName] = env.SetValue(certsHash) + // // create hash over all the different input resources to identify if any those changed // and a restart/recreate is required. @@ -525,7 +667,12 @@ func (r *NeutronAPIReconciler) reconcileInit( } // create service - end - // TODO: TLS, pass in https as protocol, create TLS cert + // if TLS is enabled + if instance.Spec.TLS.API.Enabled(endpointType) { + // set endpoint protocol to https + data.Protocol = ptr.To(service.ProtocolHTTPS) + } + apiEndpoints[string(endpointType)], err = svc.GetAPIEndpoint( svcOverride.EndpointURL, data.Protocol, data.Path) if err != nil { @@ -840,7 +987,16 @@ func (r *NeutronAPIReconciler) reconcileNormal(ctx context.Context, instance *ne return ctrlResult, fmt.Errorf("Failed to fetch input hash for Neutron deployment") } - deplDef := neutronapi.Deployment(instance, inputHash, serviceLabels, serviceAnnotations) + deplDef, err := neutronapi.Deployment(instance, inputHash, serviceLabels, serviceAnnotations) + if err != nil { + instance.Status.Conditions.Set(condition.FalseCondition( + condition.DeploymentReadyCondition, + condition.ErrorReason, + condition.SeverityWarning, + condition.DeploymentReadyErrorMessage, + err.Error())) + return ctrlResult, err + } depl := deployment.NewDeployment( deplDef, time.Duration(5)*time.Second, @@ -1327,6 +1483,22 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( templateParameters["NBConnection"] = nbEndpoint templateParameters["SBConnection"] = sbEndpoint + // create httpd vhost template parameters + httpdVhostConfig := map[string]interface{}{} + for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { + endptConfig := map[string]interface{}{} + endptConfig["ServerName"] = fmt.Sprintf("neutron-%s.%s.svc", endpt.String(), instance.Namespace) + endptConfig["TLS"] = false // default TLS to false, and set it bellow to true if enabled + if instance.Spec.TLS.API.Enabled(endpt) { + endptConfig["TLS"] = true + endptConfig["SSLCertificateFile"] = fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String()) + endptConfig["SSLCertificateKeyFile"] = fmt.Sprintf("/etc/pki/tls/private/%s.key", endpt.String()) + } + httpdVhostConfig[endpt.String()] = endptConfig + } + + templateParameters["VHosts"] = httpdVhostConfig + secrets := []util.Template{ { Name: fmt.Sprintf("%s-config", instance.Name), @@ -1346,7 +1518,9 @@ func (r *NeutronAPIReconciler) generateServiceSecrets( AdditionalTemplate: map[string]string{ "httpd.conf": "/neutronapi/httpd/httpd.conf", "10-neutron-httpd.conf": "/neutronapi/httpd/10-neutron-httpd.conf", + "ssl.conf": "/neutronapi/httpd/ssl.conf", }, + ConfigOptions: templateParameters, }, } return secret.EnsureSecrets(ctx, h, instance, secrets, envVars) diff --git a/go.mod b/go.mod index 5913beb5..5688833e 100644 --- a/go.mod +++ b/go.mod @@ -90,3 +90,5 @@ replace github.com/openstack-k8s-operators/neutron-operator/api => ./api // mschuppert: map to latest commit from release-4.13 tag // must consistent within modules and service operators replace github.com/openshift/api => github.com/openshift/api v0.0.0-20230414143018-3367bc7e6ac7 //allow-merging + +replace github.com/openstack-k8s-operators/lib-common/modules/common => github.com/deydra71/lib-common/modules/common v0.0.0-20240108150456-e7962ed7031f diff --git a/go.sum b/go.sum index d031eca2..93d8c958 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deydra71/lib-common/modules/common v0.0.0-20240108150456-e7962ed7031f h1:1DOq6SRvQLbPRrwtoZuA3UyQPMLNYqM2VyNX6JYKgmo= +github.com/deydra71/lib-common/modules/common v0.0.0-20240108150456-e7962ed7031f/go.mod h1:ov4lAbniNUsLqZCBp1RTixpqXc8JlzA5B+yTcCkJXQg= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -238,8 +240,6 @@ github.com/openstack-k8s-operators/infra-operator/apis v0.3.1-0.20240104150635-c github.com/openstack-k8s-operators/infra-operator/apis v0.3.1-0.20240104150635-c4ffc51e0752/go.mod h1:y4qeIT1ubUm9SKrvhVTuEYWSm0so38P5Hu3ZpdMJMek= github.com/openstack-k8s-operators/keystone-operator/api v0.3.1-0.20240104144437-5355d932c316 h1:IwTuIoC78bbp3awd8P0tWeknCe2jNLB1FCJDIwI/2Pg= github.com/openstack-k8s-operators/keystone-operator/api v0.3.1-0.20240104144437-5355d932c316/go.mod h1:qx+z+k0RMK8Vcl5Nug6bOScEg7ROSxEV4FFy0gjcQDQ= -github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240106101723-5f7aa263457f h1:ifW2n0TkrS1Wa58DK/N+zAKdGH5XQh4Llk/hefsXzN8= -github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20240106101723-5f7aa263457f/go.mod h1:ov4lAbniNUsLqZCBp1RTixpqXc8JlzA5B+yTcCkJXQg= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.3.1-0.20240106101723-5f7aa263457f h1:bXaUzrP+JiBHSCQwmndGjD5nafFAK+t8X2oXVP8hj3c= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.3.1-0.20240106101723-5f7aa263457f/go.mod h1:Lb1xXXJpbWxU619nemBD4XQy+rqRhzyNcGji9zZbG20= github.com/openstack-k8s-operators/lib-common/modules/storage v0.3.1-0.20240106101723-5f7aa263457f h1:b9fpRkubG+tk6uKGCNz/kuTWYtpUFsm3d/jECF1AmAs= diff --git a/pkg/neutronapi/dbsync.go b/pkg/neutronapi/dbsync.go index ff698b65..78e35869 100644 --- a/pkg/neutronapi/dbsync.go +++ b/pkg/neutronapi/dbsync.go @@ -15,8 +15,15 @@ func DbSyncJob( annotations map[string]string, ) *batchv1.Job { dbSyncExtraMounts := []neutronv1beta1.NeutronExtraVolMounts{} - volumeMounts := GetVolumeMounts("db-sync", dbSyncExtraMounts, DbsyncPropagation) + volumes := GetVolumes(cr.Name, dbSyncExtraMounts, DbsyncPropagation) + volumeMounts := GetVolumeMounts("db-sync", dbSyncExtraMounts, DbsyncPropagation) + + // add CA cert if defined + if cr.Spec.TLS.CaBundleSecretName != "" { + volumes = append(volumes, cr.Spec.TLS.CreateVolume()) + volumeMounts = append(volumeMounts, cr.Spec.TLS.CreateVolumeMounts(nil)...) + } envVars := map[string]env.Setter{} envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") diff --git a/pkg/neutronapi/deployment.go b/pkg/neutronapi/deployment.go index f9804615..44145ccf 100644 --- a/pkg/neutronapi/deployment.go +++ b/pkg/neutronapi/deployment.go @@ -13,15 +13,20 @@ limitations under the License. package neutronapi import ( + "fmt" + "github.com/openstack-k8s-operators/lib-common/modules/common" "github.com/openstack-k8s-operators/lib-common/modules/common/affinity" "github.com/openstack-k8s-operators/lib-common/modules/common/env" + "github.com/openstack-k8s-operators/lib-common/modules/common/service" + "github.com/openstack-k8s-operators/lib-common/modules/common/tls" neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" ) const ( @@ -35,7 +40,7 @@ func Deployment( configHash string, labels map[string]string, annotations map[string]string, -) *appsv1.Deployment { +) (*appsv1.Deployment, error) { livenessProbe := &corev1.Probe{ // TODO might need tuning TimeoutSeconds: 5, @@ -49,7 +54,7 @@ func Deployment( InitialDelaySeconds: 20, } args := []string{"-c"} - httpd_args := []string{"-DFOREGROUND"} + httpdArgs := []string{"-DFOREGROUND"} if instance.Spec.Debug.Service { args = append(args, common.DebugCommand) @@ -77,12 +82,52 @@ func Deployment( Path: "/", Port: intstr.IntOrString{Type: intstr.Int, IntVal: int32(NeutronPublicPort)}, } + + if instance.Spec.TLS.API.Enabled(service.EndpointPublic) { + livenessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + readinessProbe.HTTPGet.Scheme = corev1.URISchemeHTTPS + } } envVars := map[string]env.Setter{} envVars["KOLLA_CONFIG_STRATEGY"] = env.SetValue("COPY_ALWAYS") envVars["CONFIG_HASH"] = env.SetValue(configHash) + // create Volume and VolumeMounts + volumes := GetVolumes(instance.Name, instance.Spec.ExtraMounts, NeutronAPIPropagation) + apiVolumeMounts := GetVolumeMounts("neutron-api", instance.Spec.ExtraMounts, NeutronAPIPropagation) + httpdVolumeMounts := GetHttpdVolumeMount() + + // add CA cert if defined + if instance.Spec.TLS.CaBundleSecretName != "" { + volumes = append(volumes, instance.Spec.TLS.CreateVolume()) + apiVolumeMounts = append(apiVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...) + httpdVolumeMounts = append(httpdVolumeMounts, instance.Spec.TLS.CreateVolumeMounts(nil)...) + } + + for _, endpt := range []service.Endpoint{service.EndpointInternal, service.EndpointPublic} { + if instance.Spec.TLS.API.Enabled(endpt) { + var tlsEndptCfg tls.GenericService + switch endpt { + case service.EndpointPublic: + tlsEndptCfg = instance.Spec.TLS.API.Public + case service.EndpointInternal: + tlsEndptCfg = instance.Spec.TLS.API.Internal + } + + svc, err := tlsEndptCfg.ToService() + if err != nil { + return nil, err + } + // httpd container is not using kolla, mount the certs to its dst + svc.CertMount = ptr.To(fmt.Sprintf("/etc/pki/tls/certs/%s.crt", endpt.String())) + svc.KeyMount = ptr.To(fmt.Sprintf("/etc/pki/tls/private/%s.crt", endpt.String())) + + volumes = append(volumes, svc.CreateVolume(endpt.String())) + httpdVolumeMounts = append(httpdVolumeMounts, svc.CreateVolumeMounts(endpt.String())...) + } + } + deployment := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: ServiceName, @@ -108,7 +153,7 @@ func Deployment( Image: instance.Spec.ContainerImage, SecurityContext: getNeutronSecurityContext(), Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), - VolumeMounts: GetVolumeMounts("neutron-api", instance.Spec.ExtraMounts, NeutronAPIPropagation), + VolumeMounts: apiVolumeMounts, Resources: instance.Spec.Resources, ReadinessProbe: readinessProbe, LivenessProbe: livenessProbe, @@ -117,22 +162,23 @@ func Deployment( { Name: ServiceName + "-httpd", Command: []string{NeutronAPIHttpdCommand}, - Args: httpd_args, + Args: httpdArgs, Image: instance.Spec.ContainerImage, SecurityContext: getNeutronHttpdSecurityContext(), Env: env.MergeEnvs([]corev1.EnvVar{}, envVars), - VolumeMounts: GetHttpdVolumeMount(), + VolumeMounts: httpdVolumeMounts, Resources: instance.Spec.Resources, ReadinessProbe: readinessProbe, LivenessProbe: livenessProbe, TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, }, }, + Volumes: volumes, }, }, }, } - deployment.Spec.Template.Spec.Volumes = GetVolumes(instance.Name, instance.Spec.ExtraMounts, NeutronAPIPropagation) + // If possible two pods of the same service should not // run on the same worker node. If this is not possible // the get still created on the same worker node. @@ -147,5 +193,5 @@ func Deployment( deployment.Spec.Template.Spec.NodeSelector = instance.Spec.NodeSelector } - return deployment + return deployment, nil } diff --git a/pkg/neutronapi/volumes.go b/pkg/neutronapi/volumes.go index 5cabb71f..238216b4 100644 --- a/pkg/neutronapi/volumes.go +++ b/pkg/neutronapi/volumes.go @@ -75,5 +75,11 @@ func GetHttpdVolumeMount() []corev1.VolumeMount { SubPath: "10-neutron-httpd.conf", ReadOnly: true, }, + { + Name: "httpd-config", + MountPath: "/etc/httpd/conf.d/ssl.conf", + SubPath: "ssl.conf", + ReadOnly: true, + }, } } diff --git a/templates/neutronapi/httpd/10-neutron-httpd.conf b/templates/neutronapi/httpd/10-neutron-httpd.conf index f381424c..ea73f7e6 100644 --- a/templates/neutronapi/httpd/10-neutron-httpd.conf +++ b/templates/neutronapi/httpd/10-neutron-httpd.conf @@ -1,4 +1,8 @@ +{{ range $endpt, $vhost := .VHosts }} +# {{ $endpt }} vhost {{ $vhost.ServerName }} configuration + ServerName {{ $vhost.ServerName }} + ## Logging ErrorLog /dev/stdout ServerSignature Off @@ -14,8 +18,13 @@ ProxyPass / http://localhost:9697/ retry=10 ProxyPassReverse / http://localhost:9697/ +{{- if $vhost.TLS }} + SetEnvIf X-Forwarded-Proto https HTTPS=1 + ## SSL directives - #SSLEngine on - #SSLCertificateFile "/etc/pki/tls/certs/httpd/httpd-internal_api.crt" - #SSLCertificateKeyFile "/etc/pki/tls/private/httpd/httpd-internal_api.key" + SSLEngine on + SSLCertificateFile "{{ $vhost.SSLCertificateFile }}" + SSLCertificateKeyFile "{{ $vhost.SSLCertificateKeyFile }}" +{{- end }} +{{ end }} diff --git a/templates/neutronapi/httpd/httpd.conf b/templates/neutronapi/httpd/httpd.conf index 43c0d9b7..7aa6c360 100644 --- a/templates/neutronapi/httpd/httpd.conf +++ b/templates/neutronapi/httpd/httpd.conf @@ -12,6 +12,7 @@ Listen 9696 TypesConfig /etc/mime.types Include conf.modules.d/*.conf +Include conf.d/*.conf LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy diff --git a/templates/neutronapi/httpd/ssl.conf b/templates/neutronapi/httpd/ssl.conf new file mode 100644 index 00000000..e3da4ecb --- /dev/null +++ b/templates/neutronapi/httpd/ssl.conf @@ -0,0 +1,21 @@ + + SSLRandomSeed startup builtin + SSLRandomSeed startup file:/dev/urandom 512 + SSLRandomSeed connect builtin + SSLRandomSeed connect file:/dev/urandom 512 + + AddType application/x-x509-ca-cert .crt + AddType application/x-pkcs7-crl .crl + + SSLPassPhraseDialog builtin + SSLSessionCache "shmcb:/var/cache/mod_ssl/scache(512000)" + SSLSessionCacheTimeout 300 + Mutex default + SSLCryptoDevice builtin + SSLHonorCipherOrder On + SSLUseStapling Off + SSLStaplingCache "shmcb:/run/httpd/ssl_stapling(32768)" + SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!RC4:!3DES + SSLProtocol all -SSLv2 -SSLv3 -TLSv1 + SSLOptions StdEnvVars + diff --git a/test/functional/base_test.go b/test/functional/base_test.go index 29db23d7..1684087f 100644 --- a/test/functional/base_test.go +++ b/test/functional/base_test.go @@ -24,6 +24,7 @@ import ( k8s_errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" @@ -33,6 +34,10 @@ import ( const ( SecretName = "test-secret" + PublicCertSecretName = "public-tls-certs" + InternalCertSecretName = "internal-tls-certs" + CABundleSecretName = "combined-ca-bundle" + timeout = time.Second * 10 interval = timeout / 100 ) @@ -64,7 +69,7 @@ func GetDefaultNeutronAPISpec() map[string]interface{} { } } -func CreateNeutronAPI(namespace string, NeutronAPIName string, spec map[string]interface{}) types.NamespacedName { +func CreateNeutronAPI(namespace string, NeutronAPIName string, spec map[string]interface{}) client.Object { raw := map[string]interface{}{ "apiVersion": "neutron.openstack.org/v1beta1", @@ -75,27 +80,8 @@ func CreateNeutronAPI(namespace string, NeutronAPIName string, spec map[string]i }, "spec": spec, } - th.CreateUnstructured(raw) - - return types.NamespacedName{Name: NeutronAPIName, Namespace: namespace} -} - -func DeleteNeutronAPI(name types.NamespacedName) { - // We have to wait for the controller to fully delete the instance - Eventually(func(g Gomega) { - NeutronAPI := &neutronv1.NeutronAPI{} - err := k8sClient.Get(ctx, name, NeutronAPI) - // if it is already gone that is OK - if k8s_errors.IsNotFound(err) { - return - } - g.Expect(err).Should(BeNil()) - g.Expect(k8sClient.Delete(ctx, NeutronAPI)).Should(Succeed()) - - err = k8sClient.Get(ctx, name, NeutronAPI) - g.Expect(k8s_errors.IsNotFound(err)).To(BeTrue()) - }, timeout, interval).Should(Succeed()) + return th.CreateUnstructured(raw) } func GetNeutronAPI(name types.NamespacedName) *neutronv1.NeutronAPI { diff --git a/test/functional/neutronapi_controller_test.go b/test/functional/neutronapi_controller_test.go index ecd1263a..638b15f9 100644 --- a/test/functional/neutronapi_controller_test.go +++ b/test/functional/neutronapi_controller_test.go @@ -48,6 +48,9 @@ var _ = Describe("NeutronAPI controller", func() { var memcachedName types.NamespacedName var name string var spec map[string]interface{} + var caBundleSecretName types.NamespacedName + var internalCertSecretName types.NamespacedName + var publicCertSecretName types.NamespacedName BeforeEach(func() { name = fmt.Sprintf("neutron-%s", uuid.New().String()) @@ -58,6 +61,10 @@ var _ = Describe("NeutronAPI controller", func() { spec = GetDefaultNeutronAPISpec() + neutronAPIName = types.NamespacedName{ + Namespace: namespace, + Name: name, + } memcachedSpec = memcachedv1.MemcachedSpec{ Replicas: pointer.Int32(3), } @@ -65,14 +72,25 @@ var _ = Describe("NeutronAPI controller", func() { Name: "memcached", Namespace: namespace, } + caBundleSecretName = types.NamespacedName{ + Name: CABundleSecretName, + Namespace: namespace, + } + internalCertSecretName = types.NamespacedName{ + Name: InternalCertSecretName, + Namespace: namespace, + } + publicCertSecretName = types.NamespacedName{ + Name: PublicCertSecretName, + Namespace: namespace, + } }) When("A NeutronAPI instance is created", func() { - BeforeEach(func() { - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) }) + It("should have the Spec fields initialized", func() { NeutronAPI := GetNeutronAPI(neutronAPIName) Expect(NeutronAPI.Spec.DatabaseInstance).Should(Equal("test-neutron-db-instance")) @@ -124,9 +142,9 @@ var _ = Describe("NeutronAPI controller", func() { When("an unrelated secret is provided", func() { BeforeEach(func() { - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) }) + It("should remain in a state of waiting for the proper secret", func() { SimulateTransportURLReady(apiTransportURLName) secret = &corev1.Secret{ @@ -163,6 +181,7 @@ var _ = Describe("NeutronAPI controller", func() { When("the proper secret is provided, TransportURL and Memcached are Created", func() { BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) DeferCleanup(DeleteOVNDBClusters, CreateOVNDBClusters(namespace)) secret = &corev1.Secret{ @@ -175,8 +194,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) SimulateTransportURLReady(apiTransportURLName) DeferCleanup(k8sClient.Delete, ctx, secret) DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) @@ -262,6 +279,8 @@ var _ = Describe("NeutronAPI controller", func() { When("Memcached is available", func() { BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -272,8 +291,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup(k8sClient.Delete, ctx, secret) DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) infra.SimulateMemcachedReady(memcachedName) @@ -292,6 +309,8 @@ var _ = Describe("NeutronAPI controller", func() { When("OVNDBCluster instance is not available", func() { BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -303,8 +322,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup(k8sClient.Delete, ctx, secret) SimulateTransportURLReady(apiTransportURLName) }) @@ -319,6 +336,8 @@ var _ = Describe("NeutronAPI controller", func() { When("keystoneAPI instance is not available", func() { BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -330,8 +349,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup(k8sClient.Delete, ctx, secret) SimulateTransportURLReady(apiTransportURLName) DeferCleanup(DeleteOVNDBClusters, CreateOVNDBClusters(namespace)) @@ -347,6 +364,9 @@ var _ = Describe("NeutronAPI controller", func() { When("keystoneAPI, OVNDBCluster and Memcached instances are available", func() { BeforeEach(func() { + spec["customServiceConfig"] = "[DEFAULT]\ndebug=True" + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -358,9 +378,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - spec["customServiceConfig"] = "[DEFAULT]\ndebug=True" - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup(k8sClient.Delete, ctx, secret) DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) infra.SimulateMemcachedReady(memcachedName) @@ -734,6 +751,8 @@ var _ = Describe("NeutronAPI controller", func() { When("DB is created", func() { BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -745,8 +764,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup(k8sClient.Delete, ctx, secret) DeferCleanup( mariadb.DeleteDBService, @@ -786,6 +803,8 @@ var _ = Describe("NeutronAPI controller", func() { When("Keystone Resources are created", func() { BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -798,8 +817,6 @@ var _ = Describe("NeutronAPI controller", func() { } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) DeferCleanup(k8sClient.Delete, ctx, secret) - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup( mariadb.DeleteDBService, mariadb.CreateDBService( @@ -870,13 +887,16 @@ var _ = Describe("NeutronAPI controller", func() { nHttpdProxyContainer := deployment.Spec.Template.Spec.Containers[1] Expect(nHttpdProxyContainer.LivenessProbe.HTTPGet.Port.IntVal).To(Equal(int32(9696))) Expect(nHttpdProxyContainer.ReadinessProbe.HTTPGet.Port.IntVal).To(Equal(int32(9696))) - Expect(nHttpdProxyContainer.VolumeMounts).To(HaveLen(2)) + Expect(nHttpdProxyContainer.VolumeMounts).To(HaveLen(3)) Expect(nHttpdProxyContainer.Image).To(Equal(util.GetEnvVar("RELATED_IMAGE_NEUTRON_API_IMAGE_URL_DEFAULT", neutronv1.NeutronAPIContainerImage))) }) }) When("NeutronAPI is created with networkAttachments", func() { BeforeEach(func() { + spec["networkAttachments"] = []string{"internalapi"} + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: SecretName, @@ -888,9 +908,6 @@ var _ = Describe("NeutronAPI controller", func() { }, } Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) - spec["networkAttachments"] = []string{"internalapi"} - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) DeferCleanup(k8sClient.Delete, ctx, secret) DeferCleanup( mariadb.DeleteDBService, @@ -1056,8 +1073,7 @@ var _ = Describe("NeutronAPI controller", func() { spec["debug"] = map[string]interface{}{ "service": true, } - neutronAPIName = CreateNeutronAPI(namespace, name, spec) - DeferCleanup(DeleteNeutronAPI, neutronAPIName) + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) DeferCleanup(k8sClient.Delete, ctx, secret) DeferCleanup( mariadb.DeleteDBService, @@ -1096,4 +1112,178 @@ var _ = Describe("NeutronAPI controller", func() { Expect(depl.Spec.Template.Spec.Containers[0].Args[1]).Should(ContainSubstring("infinity")) }) }) + + When("A NeutronAPI is created with TLS", func() { + BeforeEach(func() { + spec["tls"] = map[string]interface{}{ + "api": map[string]interface{}{ + "internal": map[string]interface{}{ + "secretName": InternalCertSecretName, + }, + "public": map[string]interface{}{ + "secretName": PublicCertSecretName, + }, + }, + "caBundleSecretName": CABundleSecretName, + } + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(caBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(internalCertSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(publicCertSecretName)) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: SecretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "NeutronPassword": []byte("12345678"), + "transport_url": []byte("rabbit://user@svc:1234"), + }, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, secret) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetNeutronAPI(neutronAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + SimulateTransportURLReady(apiTransportURLName) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + infra.SimulateMemcachedReady(memcachedName) + DeferCleanup(DeleteOVNDBClusters, CreateOVNDBClusters(namespace)) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(namespace)) + mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: neutronAPIName.Name}) + th.SimulateJobSuccess(types.NamespacedName{Namespace: namespace, Name: neutronAPIName.Name + "-db-sync"}) + keystone.SimulateKeystoneServiceReady(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + keystone.SimulateKeystoneEndpointReady(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + }) + + It("it creates deployment with CA and service certs mounted", func() { + + deployment := th.GetDeployment( + types.NamespacedName{ + Namespace: neutronAPIName.Namespace, + Name: "neutron", + }, + ) + + // cert deployment volumes + th.AssertVolumeExists(caBundleSecretName.Name, deployment.Spec.Template.Spec.Volumes) + th.AssertVolumeExists(internalCertSecretName.Name, deployment.Spec.Template.Spec.Volumes) + th.AssertVolumeExists(publicCertSecretName.Name, deployment.Spec.Template.Spec.Volumes) + + // svc container ca cert + nSvcContainer := deployment.Spec.Template.Spec.Containers[0] + th.AssertVolumeMountExists(caBundleSecretName.Name, "tls-ca-bundle.pem", nSvcContainer.VolumeMounts) + + // httpd container certs + nHttpdProxyContainer := deployment.Spec.Template.Spec.Containers[1] + th.AssertVolumeMountExists(publicCertSecretName.Name, "tls.key", nHttpdProxyContainer.VolumeMounts) + th.AssertVolumeMountExists(publicCertSecretName.Name, "tls.crt", nHttpdProxyContainer.VolumeMounts) + th.AssertVolumeMountExists(internalCertSecretName.Name, "tls.key", nHttpdProxyContainer.VolumeMounts) + th.AssertVolumeMountExists(internalCertSecretName.Name, "tls.crt", nHttpdProxyContainer.VolumeMounts) + th.AssertVolumeMountExists(caBundleSecretName.Name, "tls-ca-bundle.pem", nHttpdProxyContainer.VolumeMounts) + + Expect(nHttpdProxyContainer.ReadinessProbe.HTTPGet.Scheme).To(Equal(corev1.URISchemeHTTPS)) + Expect(nHttpdProxyContainer.LivenessProbe.HTTPGet.Scheme).To(Equal(corev1.URISchemeHTTPS)) + }) + + It("TLS Endpoints are created", func() { + + th.ExpectCondition( + neutronAPIName, + ConditionGetterFunc(NeutronAPIConditionGetter), + condition.KeystoneEndpointReadyCondition, + corev1.ConditionTrue, + ) + keystoneEndpoint := keystone.GetKeystoneEndpoint(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + endpoints := keystoneEndpoint.Spec.Endpoints + Expect(endpoints).To(HaveKeyWithValue("public", "https://neutron-public."+neutronAPIName.Namespace+".svc:9696")) + Expect(endpoints).To(HaveKeyWithValue("internal", "https://neutron-internal."+neutronAPIName.Namespace+".svc:9696")) + }) + }) + + When("A NeutronAPI is created with TLS and service override endpointURL set", func() { + BeforeEach(func() { + spec["tls"] = map[string]interface{}{ + "api": map[string]interface{}{ + "internal": map[string]interface{}{ + "secretName": InternalCertSecretName, + }, + "public": map[string]interface{}{ + "secretName": PublicCertSecretName, + }, + }, + "caBundleSecretName": CABundleSecretName, + } + + serviceOverride := map[string]interface{}{} + serviceOverride["public"] = map[string]interface{}{ + "endpointURL": "https://neutron-openstack.apps-crc.testing", + } + + spec["override"] = map[string]interface{}{ + "service": serviceOverride, + } + + DeferCleanup(th.DeleteInstance, CreateNeutronAPI(neutronAPIName.Namespace, neutronAPIName.Name, spec)) + + DeferCleanup(k8sClient.Delete, ctx, th.CreateCABundleSecret(caBundleSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(internalCertSecretName)) + DeferCleanup(k8sClient.Delete, ctx, th.CreateCertSecret(publicCertSecretName)) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: SecretName, + Namespace: namespace, + }, + Data: map[string][]byte{ + "NeutronPassword": []byte("12345678"), + "transport_url": []byte("rabbit://user@svc:1234"), + }, + } + Expect(k8sClient.Create(ctx, secret)).Should(Succeed()) + DeferCleanup(k8sClient.Delete, ctx, secret) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + namespace, + GetNeutronAPI(neutronAPIName).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + SimulateTransportURLReady(apiTransportURLName) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, "memcached", memcachedSpec)) + infra.SimulateMemcachedReady(memcachedName) + DeferCleanup(DeleteOVNDBClusters, CreateOVNDBClusters(namespace)) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(namespace)) + mariadb.SimulateMariaDBDatabaseCompleted(types.NamespacedName{Namespace: namespace, Name: neutronAPIName.Name}) + th.SimulateJobSuccess(types.NamespacedName{Namespace: namespace, Name: neutronAPIName.Name + "-db-sync"}) + keystone.SimulateKeystoneServiceReady(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + keystone.SimulateKeystoneEndpointReady(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + }) + + It("registers endpointURL as public neutron endpoint", func() { + + th.ExpectCondition( + neutronAPIName, + ConditionGetterFunc(NeutronAPIConditionGetter), + condition.KeystoneEndpointReadyCondition, + corev1.ConditionTrue, + ) + keystoneEndpoint := keystone.GetKeystoneEndpoint(types.NamespacedName{Namespace: namespace, Name: "neutron"}) + endpoints := keystoneEndpoint.Spec.Endpoints + Expect(endpoints).To(HaveKeyWithValue("public", "https://neutron-openstack.apps-crc.testing")) + Expect(endpoints).To(HaveKeyWithValue("internal", "https://neutron-internal."+neutronAPIName.Namespace+".svc:9696")) + }) + }) })