diff --git a/apis/dataplane/v1beta1/openstackdataplanenodeset_types.go b/apis/dataplane/v1beta1/openstackdataplanenodeset_types.go index 83e3a9bb5..1c14ff9e0 100644 --- a/apis/dataplane/v1beta1/openstackdataplanenodeset_types.go +++ b/apis/dataplane/v1beta1/openstackdataplanenodeset_types.go @@ -17,9 +17,11 @@ limitations under the License. package v1beta1 import ( + "context" "fmt" "golang.org/x/exp/slices" + "sigs.k8s.io/controller-runtime/pkg/client" infranetworkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" @@ -290,3 +292,47 @@ func (r *OpenStackDataPlaneNodeSetSpec) duplicateNodeCheck(nodeSetList *OpenStac return } + +// Compare TLS settings of control plane and data plane +// if control plane name is specified attempt to retrieve it +// otherwise get any control plane in the namespace +func (r *OpenStackDataPlaneNodeSetSpec) ValidateTLS(namespace string, reconcilerClient client.Client, ctx context.Context) error { + var err error + controlPlanes := openstackv1.OpenStackControlPlaneList{} + opts := client.ListOptions{ + Namespace: namespace, + } + + // Attempt to get list of all ControlPlanes fail if that isn't possible + if err = reconcilerClient.List(ctx, &controlPlanes, &opts); err != nil { + return err + } + // Verify TLS status of control plane only if there is a single one + // otherwise proceed without verification + if len(controlPlanes.Items) == 1 { + controlPlane := controlPlanes.Items[0] + fieldErr := r.TLSMatch(controlPlane) + if fieldErr != nil { + err = fmt.Errorf("%s", fieldErr.Error()) + } + } + + return err +} + +// Do TLS flags match in control plane ingress, pods and data plane +func (r *OpenStackDataPlaneNodeSetSpec) TLSMatch(controlPlane openstackv1.OpenStackControlPlane) *field.Error { + + if controlPlane.Spec.TLS.Ingress.Enabled != r.TLSEnabled || controlPlane.Spec.TLS.PodLevel.Enabled != r.TLSEnabled { + + return field.Forbidden( + field.NewPath("spec.tlsEnabled"), + fmt.Sprintf( + "TLS settings on Data Plane node set and Control Plane %s do not match, Node set: %t Control Plane Ingress: %t Control Plane PodLevel: %t", + controlPlane.Name, + r.TLSEnabled, + controlPlane.Spec.TLS.Ingress.Enabled, + controlPlane.Spec.TLS.PodLevel.Enabled)) + } + return nil +} diff --git a/controllers/dataplane/openstackdataplanedeployment_controller.go b/controllers/dataplane/openstackdataplanedeployment_controller.go index 170e25ec7..54d66508b 100644 --- a/controllers/dataplane/openstackdataplanedeployment_controller.go +++ b/controllers/dataplane/openstackdataplanedeployment_controller.go @@ -62,6 +62,7 @@ func (r *OpenStackDataPlaneDeploymentReconciler) GetLogger(ctx context.Context) //+kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch;create;update;patch;delete; //+kubebuilder:rbac:groups=cert-manager.io,resources=issuers,verbs=get;list;watch; //+kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete; +//+kubebuilder:rbac:groups=core.openstack.org,resources=openstackcontrolplanes,verbs=get;list;watch; // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -183,6 +184,16 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context, // Error reading the object - requeue the request. return ctrl.Result{}, err } + if err = nodeSetInstance.Spec.ValidateTLS(instance.GetNamespace(), r.Client, ctx); err != nil { + Log.Info("error while comparing TLS settings of nodeset %s with control plane: %w", nodeSet, err) + instance.Status.Conditions.MarkFalse( + dataplanev1.SetupReadyCondition, + condition.ErrorReason, + condition.SeverityError, + dataplanev1.DataPlaneNodeSetErrorMessage, + err.Error()) + return ctrl.Result{}, err + } nodeSets.Items = append(nodeSets.Items, *nodeSetInstance) } diff --git a/tests/functional/dataplane/base_test.go b/tests/functional/dataplane/base_test.go index 509a9a23f..5a4f1fade 100644 --- a/tests/functional/dataplane/base_test.go +++ b/tests/functional/dataplane/base_test.go @@ -9,6 +9,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" infrav1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" @@ -344,6 +345,144 @@ func DefaultDataplaneGlobalService(name types.NamespacedName) map[string]interfa } } +func CreateOpenStackControlPlane(name types.NamespacedName, spec map[string]interface{}) client.Object { + + raw := map[string]interface{}{ + "apiVersion": "core.openstack.org/v1beta1", + "kind": "OpenStackControlPlane", + "metadata": map[string]interface{}{ + "name": name.Name, + "namespace": name.Namespace, + }, + "spec": spec, + } + return th.CreateUnstructured(raw) +} + +func GetTLSPublicSpec() map[string]interface{} { + return map[string]interface{}{ + "podLevel": map[string]interface{}{ + "enabled": false, + }, + } +} + +func GetTLSeCustomIssuerSpec() map[string]interface{} { + return map[string]interface{}{ + "ingress": map[string]interface{}{ + "enabled": true, + + "ca": map[string]interface{}{ + "customIssuer": "custom-issuer", + "duration": "100h", + }, + "cert": map[string]interface{}{ + "duration": "10h", + }, + }, + "podLevel": map[string]interface{}{ + "enabled": true, + "internal": map[string]interface{}{ + "ca": map[string]interface{}{ + "duration": "100h", + }, + "cert": map[string]interface{}{ + "duration": "10h", + }, + }, + "ovn": map[string]interface{}{ + "ca": map[string]interface{}{ + "duration": "100h", + }, + "cert": map[string]interface{}{ + "duration": "10h", + }, + }, + }, + } +} + +func GetDefaultOpenStackControlPlaneSpec(enableTLS bool) map[string]interface{} { + memcachedTemplate := map[string]interface{}{ + "memcached": map[string]interface{}{ + "replicas": 1, + }, + } + rabbitTemplate := map[string]interface{}{ + "rabbitmq": map[string]interface{}{ + "replicas": 1, + }, + "rabbitmq-cell1": map[string]interface{}{ + "replicas": 1, + }, + } + galeraTemplate := map[string]interface{}{ + "openstack": map[string]interface{}{ + "storageRequest": "500M", + }, + "openstack-cell1": map[string]interface{}{ + "storageRequest": "500M", + }, + } + keystoneTemplate := map[string]interface{}{ + "databaseInstance": "keystone", + "secret": "osp-secret", + } + + return map[string]interface{}{ + "secret": "osp-secret", + "storageClass": "local-storage", + "galera": map[string]interface{}{ + "enabled": true, + "templates": galeraTemplate, + }, + "rabbitmq": map[string]interface{}{ + "enabled": true, + "templates": rabbitTemplate, + }, + "memcached": map[string]interface{}{ + "enabled": true, + "templates": memcachedTemplate, + }, + "keystone": map[string]interface{}{ + "enabled": true, + "template": keystoneTemplate, + }, + "tls": map[string]interface{}{ + "ingress": map[string]interface{}{ + "enabled": enableTLS, + + "ca": map[string]interface{}{ + "customIssuer": "custom-issuer", + "duration": "100h", + }, + "cert": map[string]interface{}{ + "duration": "10h", + }, + }, + "podLevel": map[string]interface{}{ + "enabled": enableTLS, + "internal": map[string]interface{}{ + "ca": map[string]interface{}{ + "duration": "100h", + }, + "cert": map[string]interface{}{ + "duration": "10h", + }, + }, + "ovn": map[string]interface{}{ + "ca": map[string]interface{}{ + "duration": "100h", + }, + "cert": map[string]interface{}{ + "duration": "10h", + }, + }, + }, + }, + } +} + // Get resources // Retrieve OpenStackDataPlaneDeployment and check for errors diff --git a/tests/functional/dataplane/openstackdataplanedeployment_controller_test.go b/tests/functional/dataplane/openstackdataplanedeployment_controller_test.go index 556851ccf..ef3c7e645 100644 --- a/tests/functional/dataplane/openstackdataplanedeployment_controller_test.go +++ b/tests/functional/dataplane/openstackdataplanedeployment_controller_test.go @@ -18,6 +18,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" ) var _ = Describe("Dataplane Deployment Test", func() { @@ -36,6 +37,7 @@ var _ = Describe("Dataplane Deployment Test", func() { var dataplaneServiceName types.NamespacedName var dataplaneUpdateServiceName types.NamespacedName var dataplaneGlobalServiceName types.NamespacedName + var controlPlaneName types.NamespacedName BeforeEach(func() { dnsMasqName = types.NamespacedName{ @@ -98,6 +100,10 @@ var _ = Describe("Dataplane Deployment Test", func() { Name: "global-service", Namespace: namespace, } + controlPlaneName = types.NamespacedName{ + Name: "mock-control-plane", + Namespace: namespace, + } err := os.Setenv("OPERATOR_SERVICES", "../../../config/services") Expect(err).NotTo(HaveOccurred()) }) @@ -719,4 +725,208 @@ var _ = Describe("Dataplane Deployment Test", func() { ) }) }) + + When("A user sets TLSEnabled to true with control plane TLS disabled", func() { + BeforeEach(func() { + CreateSSHSecret(dataplaneSSHSecretName) + DeferCleanup(th.DeleteInstance, th.CreateSecret(neutronOvnMetadataSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(novaNeutronMetadataSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(novaCellComputeConfigSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(novaMigrationSSHKey, map[string][]byte{ + "ssh-privatekey": []byte("fake-ssh-private-key"), + "ssh-publickey": []byte("fake-ssh-public-key"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(ceilometerConfigSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + // DefaultDataPlanenodeSetSpec comes with two mock services, one marked for deployment on all nodesets + DeferCleanup(th.DeleteInstance, CreateDataplaneService(dataplaneServiceName, false)) + DeferCleanup(th.DeleteInstance, CreateDataplaneService(dataplaneGlobalServiceName, true)) + + DeferCleanup(th.DeleteService, dataplaneServiceName) + DeferCleanup(th.DeleteService, dataplaneGlobalServiceName) + DeferCleanup(th.DeleteInstance, CreateNetConfig(dataplaneNetConfigName, DefaultNetConfigSpec())) + DeferCleanup(th.DeleteInstance, CreateDNSMasq(dnsMasqName, DefaultDNSMasqSpec())) + SimulateDNSMasqComplete(dnsMasqName) + DeferCleanup(th.DeleteInstance, CreateDataplaneNodeSet(dataplaneNodeSetName, DefaultDataPlaneNodeSetSpec(dataplaneNodeSetName.Name))) + DeferCleanup(th.DeleteInstance, CreateDataplaneDeployment(dataplaneDeploymentName, DefaultDataPlaneDeploymentSpec())) + SimulateIPSetComplete(dataplaneNodeName) + SimulateDNSDataComplete(dataplaneNodeSetName) + + DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(controlPlaneName, GetDefaultOpenStackControlPlaneSpec(false))) + }) + + It("Should have Spec fields initialized", func() { + dataplaneDeploymentInstance := GetDataplaneDeployment(dataplaneDeploymentName) + expectedSpec := dataplanev1.OpenStackDataPlaneDeploymentSpec{ + NodeSets: []string{"edpm-compute-nodeset"}, + AnsibleTags: "", + AnsibleLimit: "", + AnsibleSkipTags: "", + DeploymentRequeueTime: 15, + ServicesOverride: nil, + BackoffLimit: ptr.To(int32(6)), + } + Expect(dataplaneDeploymentInstance.Spec).Should(Equal(expectedSpec)) + }) + + It("should have ready condiction set to false and input condition set to unknown", func() { + + nodeSet := dataplanev1.OpenStackDataPlaneNodeSet{} + baremetal := baremetalv1.OpenStackBaremetalSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeSet.Name, + Namespace: nodeSet.Namespace, + }, + } + // Create config map for OVN service + ovnConfigMapName := types.NamespacedName{ + Namespace: namespace, + Name: "ovncontroller-config", + } + mapData := map[string]interface{}{ + "ovsdb-config": "test-ovn-config", + } + th.CreateConfigMap(ovnConfigMapName, mapData) + + nodeSet = *GetDataplaneNodeSet(dataplaneNodeSetName) + + // Set baremetal provisioning conditions to True + Eventually(func(g Gomega) { + // OpenStackBaremetalSet has the same name as OpenStackDataPlaneNodeSet + g.Expect(th.K8sClient.Get(th.Ctx, dataplaneNodeSetName, &baremetal)).To(Succeed()) + baremetal.Status.Conditions.MarkTrue( + condition.ReadyCondition, + condition.ReadyMessage) + g.Expect(th.K8sClient.Status().Update(th.Ctx, &baremetal)).To(Succeed()) + + }, th.Timeout, th.Interval).Should(Succeed()) + + th.ExpectCondition( + dataplaneDeploymentName, + ConditionGetterFunc(DataplaneDeploymentConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + th.ExpectCondition( + dataplaneDeploymentName, + ConditionGetterFunc(DataplaneDeploymentConditionGetter), + condition.InputReadyCondition, + corev1.ConditionUnknown, + ) + }) + + }) + + When("A user sets TLSEnabled to true with control plane TLS enabled", func() { + BeforeEach(func() { + CreateSSHSecret(dataplaneSSHSecretName) + DeferCleanup(th.DeleteInstance, th.CreateSecret(neutronOvnMetadataSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(novaNeutronMetadataSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(novaCellComputeConfigSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(novaMigrationSSHKey, map[string][]byte{ + "ssh-privatekey": []byte("fake-ssh-private-key"), + "ssh-publickey": []byte("fake-ssh-public-key"), + })) + DeferCleanup(th.DeleteInstance, th.CreateSecret(ceilometerConfigSecretName, map[string][]byte{ + "fake_keys": []byte("blih"), + })) + // DefaultDataPlanenodeSetSpec comes with two mock services, one marked for deployment on all nodesets + DeferCleanup(th.DeleteInstance, CreateDataplaneService(dataplaneServiceName, false)) + DeferCleanup(th.DeleteInstance, CreateDataplaneService(dataplaneUpdateServiceName, false)) + CreateDataplaneService(dataplaneGlobalServiceName, true) + + DeferCleanup(th.DeleteService, dataplaneServiceName) + DeferCleanup(th.DeleteService, dataplaneGlobalServiceName) + DeferCleanup(th.DeleteInstance, CreateNetConfig(dataplaneNetConfigName, DefaultNetConfigSpec())) + DeferCleanup(th.DeleteInstance, CreateDNSMasq(dnsMasqName, DefaultDNSMasqSpec())) + SimulateDNSMasqComplete(dnsMasqName) + DeferCleanup(th.DeleteInstance, CreateDataplaneNodeSet(dataplaneNodeSetName, DefaultDataPlaneNodeSetSpec(dataplaneNodeSetName.Name))) + DeferCleanup(th.DeleteInstance, CreateDataplaneDeployment(dataplaneDeploymentName, DefaultDataPlaneDeploymentSpec())) + SimulateIPSetComplete(dataplaneNodeName) + SimulateDNSDataComplete(dataplaneNodeSetName) + + DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(controlPlaneName, GetDefaultOpenStackControlPlaneSpec(true))) + }) + + It("Should have Spec fields initialized", func() { + dataplaneDeploymentInstance := GetDataplaneDeployment(dataplaneDeploymentName) + expectedSpec := dataplanev1.OpenStackDataPlaneDeploymentSpec{ + NodeSets: []string{"edpm-compute-nodeset"}, + AnsibleTags: "", + AnsibleLimit: "", + AnsibleSkipTags: "", + DeploymentRequeueTime: 15, + ServicesOverride: nil, + BackoffLimit: ptr.To(int32(6)), + } + Expect(dataplaneDeploymentInstance.Spec).Should(Equal(expectedSpec)) + }) + + It("should have ready condiction set to false, input condition set to true and nodeset setup ready condition set to true", func() { + + nodeSet := dataplanev1.OpenStackDataPlaneNodeSet{} + baremetal := baremetalv1.OpenStackBaremetalSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeSet.Name, + Namespace: nodeSet.Namespace, + }, + } + // Create config map for OVN service + ovnConfigMapName := types.NamespacedName{ + Namespace: namespace, + Name: "ovncontroller-config", + } + mapData := map[string]interface{}{ + "ovsdb-config": "test-ovn-config", + } + th.CreateConfigMap(ovnConfigMapName, mapData) + + nodeSet = *GetDataplaneNodeSet(dataplaneNodeSetName) + + // Set baremetal provisioning conditions to True + Eventually(func(g Gomega) { + // OpenStackBaremetalSet has the same name as OpenStackDataPlaneNodeSet + g.Expect(th.K8sClient.Get(th.Ctx, dataplaneNodeSetName, &baremetal)).To(Succeed()) + baremetal.Status.Conditions.MarkTrue( + condition.ReadyCondition, + condition.ReadyMessage) + g.Expect(th.K8sClient.Status().Update(th.Ctx, &baremetal)).To(Succeed()) + + }, th.Timeout, th.Interval).Should(Succeed()) + + th.ExpectCondition( + dataplaneNodeSetName, + ConditionGetterFunc(DataplaneConditionGetter), + dataplanev1.SetupReadyCondition, + corev1.ConditionTrue, + ) + th.ExpectCondition( + dataplaneDeploymentName, + ConditionGetterFunc(DataplaneDeploymentConditionGetter), + condition.ReadyCondition, + corev1.ConditionFalse, + ) + th.ExpectCondition( + dataplaneDeploymentName, + ConditionGetterFunc(DataplaneDeploymentConditionGetter), + condition.InputReadyCondition, + corev1.ConditionTrue, + ) + }) + + }) + }) diff --git a/tests/functional/dataplane/service_test.go b/tests/functional/dataplane/service_test.go new file mode 100644 index 000000000..c1d5d9b2a --- /dev/null +++ b/tests/functional/dataplane/service_test.go @@ -0,0 +1,66 @@ +/* +Copyright 2023. + +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 ( + "os" + + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + "k8s.io/apimachinery/pkg/types" +) + +var _ = Describe("OpenstackDataplaneService Test", func() { + var dataplaneServiceName types.NamespacedName + BeforeEach(func() { + dataplaneServiceName = types.NamespacedName{ + Namespace: namespace, + Name: "configure-network", + } + }) + + When("A defined service resource is created", func() { + BeforeEach(func() { + os.Unsetenv("OPERATOR_SERVICES") + CreateDataplaneService(dataplaneServiceName, false) + DeferCleanup(th.DeleteService, dataplaneServiceName) + }) + + It("spec fields are set up", func() { + service := GetService(dataplaneServiceName) + Expect(service.Spec.Secrets).To(BeEmpty()) + Expect(service.Spec.Playbook).To(BeEmpty()) + Expect(service.Spec.ConfigMaps).To(BeEmpty()) + Expect(service.Spec.DeployOnAllNodeSets).To(BeFalse()) + }) + }) + + When("A defined service resource for all nodes is created", func() { + BeforeEach(func() { + os.Unsetenv("OPERATOR_SERVICES") + CreateDataplaneService(dataplaneServiceName, true) + DeferCleanup(th.DeleteService, dataplaneServiceName) + }) + + It("spec fields are set up", func() { + service := GetService(dataplaneServiceName) + Expect(service.Spec.Secrets).To(BeEmpty()) + Expect(service.Spec.Playbook).To(BeEmpty()) + Expect(service.Spec.ConfigMaps).To(BeEmpty()) + Expect(service.Spec.DeployOnAllNodeSets).To(BeTrue()) + }) + }) +}) diff --git a/tests/functional/dataplane/suite_test.go b/tests/functional/dataplane/suite_test.go index bb56410aa..d4de8155f 100644 --- a/tests/functional/dataplane/suite_test.go +++ b/tests/functional/dataplane/suite_test.go @@ -98,6 +98,9 @@ var _ = BeforeSuite(func() { infraCRDs, err := test.GetCRDDirFromModule( "github.com/openstack-k8s-operators/infra-operator/apis", gomod, "bases") Expect(err).ShouldNot(HaveOccurred()) + openstackCRDs, err := test.GetCRDDirFromModule( + "github.com/openstack-k8s-operators/openstack-operator/apis", gomod, "bases") + Expect(err).ShouldNot(HaveOccurred()) By("bootstrapping test environment") testEnv = &envtest.Environment{ @@ -106,6 +109,7 @@ var _ = BeforeSuite(func() { aeeCRDs, baremetalCRDs, infraCRDs, + openstackCRDs, }, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, @@ -177,6 +181,9 @@ var _ = BeforeSuite(func() { err = (&dataplanev1.OpenStackDataPlaneService{}).SetupWebhookWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) + err = (&openstackv1.OpenStackControlPlane{}).SetupWebhookWithManager(k8sManager) + Expect(err).NotTo(HaveOccurred()) + kclient, err := kubernetes.NewForConfig(cfg) Expect(err).ToNot(HaveOccurred(), "failed to create kclient") err = (&dataplanecontrollers.OpenStackDataPlaneNodeSetReconciler{