Skip to content

Commit

Permalink
Introducing spec level validation of dataplane/controlplane TLS consi…
Browse files Browse the repository at this point in the history
…stency

Verifies that TLS settings for nodeset are consistent with those
of existing control plane, if there is one and only one.

If there are multiple control planes the process will result in error,
same if it isn't possible to retrieve list of control planes.

Tests are included

Signed-off-by: Jiri Podivin <[email protected]>
  • Loading branch information
jpodivin committed Jun 14, 2024
1 parent 54e92e4 commit 74c030b
Show file tree
Hide file tree
Showing 8 changed files with 569 additions and 8 deletions.
48 changes: 48 additions & 0 deletions apis/dataplane/v1beta1/openstackdataplanenodeset_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -290,3 +292,49 @@ 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
// report error if there are multiple, or proceed if there are none
if len(controlPlanes.Items) > 1 {
err = fmt.Errorf("multiple control planes found in the namespace %s", namespace)
} else 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
}
11 changes: 11 additions & 0 deletions controllers/dataplane/openstackdataplanedeployment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}

Expand Down
32 changes: 32 additions & 0 deletions tests/functional/dataplane/base_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,38 @@ func DefaultDataplaneGlobalService(name types.NamespacedName) map[string]interfa
}
}

// Create simple OpenStackControlPlane
func CreateOpenStackControlPlane(name types.NamespacedName, tlsEnabled bool) *unstructured.Unstructured {

raw := map[string]interface{}{
"apiVersion": "core.openstack.org/v1beta1",
"kind": "OpenStackControlPlane",
"metadata": map[string]interface{}{
"name": name.Name,
"namespace": name.Namespace,
},
"spec": map[string]interface{}{
"secret": "osp-secret",
"storageClass": "local-storage",
"tls": map[string]interface{}{
"ingress": map[string]interface{}{
"enabled": tlsEnabled,
"ca": map[string]interface{}{
"duration": "100h",
},
"cert": map[string]interface{}{
"duration": "10h",
},
},
"podLevel": map[string]interface{}{
"enabled": tlsEnabled,
},
},
},
}
return th.CreateUnstructured(raw)
}

// Get resources

// Retrieve OpenStackDataPlaneDeployment and check for errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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{
Expand Down Expand Up @@ -98,7 +100,11 @@ var _ = Describe("Dataplane Deployment Test", func() {
Name: "global-service",
Namespace: namespace,
}
err := os.Setenv("OPERATOR_SERVICES", "../../../config/services")
controlPlaneName = types.NamespacedName{
Name: "mock-control-plane",
Namespace: namespace,
}
err := os.Setenv("OPERATOR_SERVICES", "../../config/services")
Expect(err).NotTo(HaveOccurred())
})

Expand Down Expand Up @@ -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, 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, 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,
)
})

})

})
Loading

0 comments on commit 74c030b

Please sign in to comment.