Skip to content
This repository was archived by the owner on Jun 25, 2024. It is now read-only.

Commit 3fee74c

Browse files
committed
Introducing spec level validation of dataplane/controlplane TLS consistency
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 <jpodivin@redhat.com>
1 parent 319ec0e commit 3fee74c

File tree

10 files changed

+398
-4
lines changed

10 files changed

+398
-4
lines changed

api/v1beta1/openstackdataplanenodeset_types.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
"context"
2021
"fmt"
2122

2223
"golang.org/x/exp/slices"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
2325

2426
infranetworkv1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1"
2527
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
@@ -290,3 +292,48 @@ func (r *OpenStackDataPlaneNodeSetSpec) duplicateNodeCheck(nodeSetList *OpenStac
290292

291293
return
292294
}
295+
296+
// Compare TLS settings of control plane and data plane
297+
// if control plane name is specified attempt to retrieve it
298+
// otherwise get any control plane in the namespace
299+
func (r *OpenStackDataPlaneNodeSetSpec) ValidateTLS(namespace string, reconcilerClient client.Client, ctx context.Context) error {
300+
var err error
301+
302+
controlPlanes := openstackv1.OpenStackControlPlaneList{}
303+
opts := client.ListOptions{
304+
Namespace: namespace,
305+
}
306+
307+
// Attempt to get list of all ControlPlanes fail if that isn't possible
308+
if err = reconcilerClient.List(ctx, &controlPlanes, &opts); err != nil {
309+
return err
310+
}
311+
312+
// Verify TLS status of control plane only if there is a single one
313+
// report error if there are multiple, or proceed if there are none
314+
if len(controlPlanes.Items) > 1 {
315+
err = fmt.Errorf("multiple control planes found in the namespace %v", namespace)
316+
} else if len(controlPlanes.Items) == 1 {
317+
controlPlane := controlPlanes.Items[0]
318+
err = r.TLSMatch(controlPlane)
319+
}
320+
321+
return err
322+
}
323+
324+
// Do TLS flags match in control plane ingress, pods and data plane
325+
func (r *OpenStackDataPlaneNodeSetSpec) TLSMatch(controlPlane openstackv1.OpenStackControlPlane) *field.Error {
326+
327+
if controlPlane.Spec.TLS.Ingress.Enabled != r.TLSEnabled || controlPlane.Spec.TLS.PodLevel.Enabled != r.TLSEnabled {
328+
329+
return field.Forbidden(
330+
field.NewPath("spec.tlsEnabled"),
331+
fmt.Sprintf(
332+
"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",
333+
controlPlane.Name,
334+
r.TLSEnabled,
335+
controlPlane.Spec.TLS.Ingress.Enabled,
336+
controlPlane.Spec.TLS.PodLevel.Enabled))
337+
}
338+
return nil
339+
}

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,14 @@ rules:
185185
- patch
186186
- update
187187
- watch
188+
- apiGroups:
189+
- core.openstack.org
190+
resources:
191+
- openstackcontrolplanes
192+
verbs:
193+
- get
194+
- list
195+
- watch
188196
- apiGroups:
189197
- core.openstack.org
190198
resources:

controllers/openstackdataplanedeployment_controller.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ func (r *OpenStackDataPlaneDeploymentReconciler) GetLogger(ctx context.Context)
6262
//+kubebuilder:rbac:groups=discovery.k8s.io,resources=endpointslices,verbs=get;list;watch;create;update;patch;delete;
6363
//+kubebuilder:rbac:groups=cert-manager.io,resources=issuers,verbs=get;list;watch;
6464
//+kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete;
65+
//+kubebuilder:rbac:groups=core.openstack.org,resources=openstackcontrolplanes,verbs=get;list;watch;
6566

6667
// Reconcile is part of the main kubernetes reconciliation loop which aims to
6768
// move the current state of the cluster closer to the desired state.
@@ -183,6 +184,17 @@ func (r *OpenStackDataPlaneDeploymentReconciler) Reconcile(ctx context.Context,
183184
// Error reading the object - requeue the request.
184185
return ctrl.Result{}, err
185186
}
187+
err = nodeSetInstance.Spec.ValidateTLS(instance.GetNamespace(), r.Client, ctx)
188+
if err != nil {
189+
Log.Info("error while comparing TLS settings of nodeset %s with control plane: %w", nodeSet, err)
190+
instance.Status.Conditions.MarkFalse(
191+
dataplanev1.SetupReadyCondition,
192+
condition.ErrorReason,
193+
condition.SeverityError,
194+
dataplanev1.DataPlaneNodeSetErrorMessage,
195+
err.Error())
196+
return ctrl.Result{}, err
197+
}
186198
nodeSets.Items = append(nodeSets.Items, *nodeSetInstance)
187199
}
188200

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ require (
2828
k8s.io/api v0.28.10
2929
k8s.io/apimachinery v0.28.10
3030
k8s.io/client-go v0.28.10
31+
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0
3132
sigs.k8s.io/controller-runtime v0.16.6
3233
)
3334

@@ -113,7 +114,6 @@ require (
113114
k8s.io/component-base v0.28.10 // indirect
114115
k8s.io/klog/v2 v2.120.1 // indirect
115116
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
116-
k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 // indirect
117117
sigs.k8s.io/gateway-api v0.8.0 // indirect
118118
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
119119
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect

tests/functional/base_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1010
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1111
"k8s.io/apimachinery/pkg/types"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
1213

1314
dataplanev1 "github.com/openstack-k8s-operators/dataplane-operator/api/v1beta1"
1415
infrav1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1"
@@ -344,6 +345,38 @@ func DefaultDataplaneGlobalService(name types.NamespacedName) map[string]interfa
344345
}
345346
}
346347

348+
// Create simple OpenStackControlPlane
349+
func CreateOpenStackControlPlane(name types.NamespacedName, tlsEnabled bool) client.Object {
350+
351+
raw := map[string]interface{}{
352+
"apiVersion": "core.openstack.org/v1beta1",
353+
"kind": "OpenStackControlPlane",
354+
"metadata": map[string]interface{}{
355+
"name": name.Name,
356+
"namespace": name.Namespace,
357+
},
358+
"spec": map[string]interface{}{
359+
"secret": "osp-secret",
360+
"storageClass": "local-storage",
361+
"tls": map[string]interface{}{
362+
"ingress": map[string]interface{}{
363+
"enabled": tlsEnabled,
364+
"ca": map[string]interface{}{
365+
"duration": "100h",
366+
},
367+
"cert": map[string]interface{}{
368+
"duration": "10h",
369+
},
370+
},
371+
"podLevel": map[string]interface{}{
372+
"enabled": tlsEnabled,
373+
},
374+
},
375+
},
376+
}
377+
return th.CreateUnstructured(raw)
378+
}
379+
347380
// Get resources
348381

349382
// Retrieve OpenStackDataPlaneDeployment and check for errors

tests/functional/openstackdataplanedeployment_controller_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1"
1717
corev1 "k8s.io/api/core/v1"
1818
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
19+
"k8s.io/utils/ptr"
1920

2021
"k8s.io/apimachinery/pkg/types"
2122
)
@@ -36,6 +37,7 @@ var _ = Describe("Dataplane Deployment Test", func() {
3637
var dataplaneServiceName types.NamespacedName
3738
var dataplaneUpdateServiceName types.NamespacedName
3839
var dataplaneGlobalServiceName types.NamespacedName
40+
var controlPlaneName types.NamespacedName
3941

4042
BeforeEach(func() {
4143
dnsMasqName = types.NamespacedName{
@@ -719,4 +721,106 @@ var _ = Describe("Dataplane Deployment Test", func() {
719721
)
720722
})
721723
})
724+
725+
When("A user sets TLSEnabled to true with control plane TLS disabled", func() {
726+
BeforeEach(func() {
727+
controlPlaneName = types.NamespacedName{
728+
Name: "mock-control-plane",
729+
Namespace: namespace,
730+
}
731+
CreateSSHSecret(dataplaneSSHSecretName)
732+
DeferCleanup(th.DeleteInstance, th.CreateSecret(neutronOvnMetadataSecretName, map[string][]byte{
733+
"fake_keys": []byte("blih"),
734+
}))
735+
DeferCleanup(th.DeleteInstance, th.CreateSecret(novaNeutronMetadataSecretName, map[string][]byte{
736+
"fake_keys": []byte("blih"),
737+
}))
738+
DeferCleanup(th.DeleteInstance, th.CreateSecret(novaCellComputeConfigSecretName, map[string][]byte{
739+
"fake_keys": []byte("blih"),
740+
}))
741+
DeferCleanup(th.DeleteInstance, th.CreateSecret(novaMigrationSSHKey, map[string][]byte{
742+
"ssh-privatekey": []byte("fake-ssh-private-key"),
743+
"ssh-publickey": []byte("fake-ssh-public-key"),
744+
}))
745+
DeferCleanup(th.DeleteInstance, th.CreateSecret(ceilometerConfigSecretName, map[string][]byte{
746+
"fake_keys": []byte("blih"),
747+
}))
748+
// DefaultDataPlanenodeSetSpec comes with two mock services, one marked for deployment on all nodesets
749+
CreateDataplaneService(dataplaneServiceName, false)
750+
CreateDataplaneService(dataplaneGlobalServiceName, true)
751+
752+
DeferCleanup(th.DeleteService, dataplaneServiceName)
753+
DeferCleanup(th.DeleteService, dataplaneGlobalServiceName)
754+
DeferCleanup(th.DeleteInstance, CreateNetConfig(dataplaneNetConfigName, DefaultNetConfigSpec()))
755+
DeferCleanup(th.DeleteInstance, CreateDNSMasq(dnsMasqName, DefaultDNSMasqSpec()))
756+
SimulateDNSMasqComplete(dnsMasqName)
757+
DeferCleanup(th.DeleteInstance, CreateDataplaneNodeSet(dataplaneNodeSetName, DefaultDataPlaneNodeSetSpec(dataplaneNodeSetName.Name)))
758+
DeferCleanup(th.DeleteInstance, CreateDataplaneDeployment(dataplaneDeploymentName, DefaultDataPlaneDeploymentSpec()))
759+
SimulateIPSetComplete(dataplaneNodeName)
760+
SimulateDNSDataComplete(dataplaneNodeSetName)
761+
762+
DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(controlPlaneName, false))
763+
})
764+
765+
It("Should have Spec fields initialized", func() {
766+
dataplaneDeploymentInstance := GetDataplaneDeployment(dataplaneDeploymentName)
767+
expectedSpec := dataplanev1.OpenStackDataPlaneDeploymentSpec{
768+
NodeSets: []string{"edpm-compute-nodeset"},
769+
AnsibleTags: "",
770+
AnsibleLimit: "",
771+
AnsibleSkipTags: "",
772+
DeploymentRequeueTime: 15,
773+
ServicesOverride: nil,
774+
BackoffLimit: ptr.To(int32(6)),
775+
}
776+
Expect(dataplaneDeploymentInstance.Spec).Should(Equal(expectedSpec))
777+
})
778+
779+
It("should have ready condiction set to false and input condition set to unknown", func() {
780+
781+
nodeSet := dataplanev1.OpenStackDataPlaneNodeSet{}
782+
baremetal := baremetalv1.OpenStackBaremetalSet{
783+
ObjectMeta: metav1.ObjectMeta{
784+
Name: nodeSet.Name,
785+
Namespace: nodeSet.Namespace,
786+
},
787+
}
788+
// Create config map for OVN service
789+
ovnConfigMapName := types.NamespacedName{
790+
Namespace: namespace,
791+
Name: "ovncontroller-config",
792+
}
793+
mapData := map[string]interface{}{
794+
"ovsdb-config": "test-ovn-config",
795+
}
796+
th.CreateConfigMap(ovnConfigMapName, mapData)
797+
798+
nodeSet = *GetDataplaneNodeSet(dataplaneNodeSetName)
799+
800+
// Set baremetal provisioning conditions to True
801+
Eventually(func(g Gomega) {
802+
// OpenStackBaremetalSet has the same name as OpenStackDataPlaneNodeSet
803+
g.Expect(th.K8sClient.Get(th.Ctx, dataplaneNodeSetName, &baremetal)).To(Succeed())
804+
baremetal.Status.Conditions.MarkTrue(
805+
condition.ReadyCondition,
806+
condition.ReadyMessage)
807+
g.Expect(th.K8sClient.Status().Update(th.Ctx, &baremetal)).To(Succeed())
808+
809+
}, th.Timeout, th.Interval).Should(Succeed())
810+
811+
th.ExpectCondition(
812+
dataplaneDeploymentName,
813+
ConditionGetterFunc(DataplaneDeploymentConditionGetter),
814+
condition.ReadyCondition,
815+
corev1.ConditionFalse,
816+
)
817+
th.ExpectCondition(
818+
dataplaneDeploymentName,
819+
ConditionGetterFunc(DataplaneDeploymentConditionGetter),
820+
condition.InputReadyCondition,
821+
corev1.ConditionUnknown,
822+
)
823+
})
824+
825+
})
722826
})

tests/functional/suite_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/google/uuid"
2727
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
2828
. "github.com/onsi/gomega" //revive:disable:dot-imports
29+
openstackv1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1"
2930
"k8s.io/client-go/kubernetes"
3031
"k8s.io/client-go/kubernetes/scheme"
3132
ctrl "sigs.k8s.io/controller-runtime"
@@ -46,7 +47,6 @@ import (
4647
infrav1 "github.com/openstack-k8s-operators/infra-operator/apis/network/v1beta1"
4748
aee "github.com/openstack-k8s-operators/openstack-ansibleee-operator/api/v1beta1"
4849
baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1"
49-
openstackv1 "github.com/openstack-k8s-operators/openstack-operator/apis/core/v1beta1"
5050

5151
//revive:disable-next-line:dot-imports
5252
. "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers"

tests/kuttl/tests/dataplane-deploy-global-service-test/00-assert.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ spec:
3333
- libvirt
3434
- nova
3535
- custom-global-service
36-
tlsEnabled: false
36+
tlsEnabled: true
3737
env:
3838
- name: ANSIBLE_FORCE_COLOR
3939
value: "True"

0 commit comments

Comments
 (0)