Skip to content

Commit d4663b2

Browse files
authored
Merge pull request #12561 from sbueringer/pr-improve-topo-name-validation
🌱 Improve validation of worker topology names in Cluster resource
2 parents 3adb251 + 50728e8 commit d4663b2

File tree

5 files changed

+108
-87
lines changed

5 files changed

+108
-87
lines changed

api/core/v1beta2/cluster_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,7 @@ type MachineDeploymentTopology struct {
836836
// +required
837837
// +kubebuilder:validation:MinLength=1
838838
// +kubebuilder:validation:MaxLength=63
839+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
839840
Name string `json:"name,omitempty"`
840841

841842
// failureDomain is the failure domain the machines will be created in.
@@ -1146,6 +1147,7 @@ type MachinePoolTopology struct {
11461147
// +required
11471148
// +kubebuilder:validation:MinLength=1
11481149
// +kubebuilder:validation:MaxLength=63
1150+
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`
11491151
Name string `json:"name,omitempty"`
11501152

11511153
// failureDomains is the list of failure domains the machine pool will be created in.

config/crd/bases/cluster.x-k8s.io_clusters.yaml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/topology/check/compatibility.go

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323

2424
"k8s.io/apimachinery/pkg/runtime/schema"
2525
"k8s.io/apimachinery/pkg/util/sets"
26-
"k8s.io/apimachinery/pkg/util/validation"
2726
"k8s.io/apimachinery/pkg/util/validation/field"
2827
"sigs.k8s.io/controller-runtime/pkg/client"
2928

@@ -287,19 +286,6 @@ func MachineDeploymentTopologiesAreValidAndDefinedInClusterClass(desired *cluste
287286
machineDeploymentClasses := mdClassNamesFromWorkerClass(clusterClass.Spec.Workers)
288287
names := sets.Set[string]{}
289288
for i, md := range desired.Spec.Topology.Workers.MachineDeployments {
290-
if errs := validation.IsValidLabelValue(md.Name); len(errs) != 0 {
291-
for _, err := range errs {
292-
allErrs = append(
293-
allErrs,
294-
field.Invalid(
295-
field.NewPath("spec", "topology", "workers", "machineDeployments").Index(i).Child("name"),
296-
md.Name,
297-
fmt.Sprintf("must be a valid label value %s", err),
298-
),
299-
)
300-
}
301-
}
302-
303289
if !machineDeploymentClasses.Has(md.Class) {
304290
allErrs = append(allErrs,
305291
field.Invalid(
@@ -348,19 +334,6 @@ func MachinePoolTopologiesAreValidAndDefinedInClusterClass(desired *clusterv1.Cl
348334
machinePoolClasses := mpClassNamesFromWorkerClass(clusterClass.Spec.Workers)
349335
names := sets.Set[string]{}
350336
for i, mp := range desired.Spec.Topology.Workers.MachinePools {
351-
if errs := validation.IsValidLabelValue(mp.Name); len(errs) != 0 {
352-
for _, err := range errs {
353-
allErrs = append(
354-
allErrs,
355-
field.Invalid(
356-
field.NewPath("spec", "topology", "workers", "machinePools").Index(i).Child("name"),
357-
mp.Name,
358-
fmt.Sprintf("must be a valid label value %s", err),
359-
),
360-
)
361-
}
362-
}
363-
364337
if !machinePoolClasses.Has(mp.Class) {
365338
allErrs = append(allErrs,
366339
field.Invalid(

internal/topology/check/compatibility_test.go

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,36 +1303,6 @@ func TestMachineDeploymentTopologiesAreUniqueAndDefinedInClusterClass(t *testing
13031303
Build(),
13041304
wantErr: false,
13051305
},
1306-
{
1307-
name: "fail if MachineDeploymentTopology name is longer than 63 characters",
1308-
clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
1309-
WithInfrastructureClusterTemplate(
1310-
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()).
1311-
WithControlPlaneTemplate(
1312-
builder.ControlPlane(metav1.NamespaceDefault, "cp1").Build()).
1313-
WithControlPlaneInfrastructureMachineTemplate(
1314-
builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "cpinfra1").Build()).
1315-
WithWorkerMachineDeploymentClasses(
1316-
*builder.MachineDeploymentClass("aa").
1317-
WithInfrastructureTemplate(
1318-
builder.InfrastructureMachineTemplate(metav1.NamespaceDefault, "infra1").Build()).
1319-
WithBootstrapTemplate(
1320-
builder.BootstrapTemplate(metav1.NamespaceDefault, "bootstrap1").Build()).
1321-
Build()).
1322-
Build(),
1323-
cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
1324-
WithTopology(
1325-
builder.ClusterTopology().
1326-
WithClass("class1").
1327-
WithVersion("v1.22.2").
1328-
WithMachineDeployment(
1329-
builder.MachineDeploymentTopology("machine-deployment-topology-name-that-has-longerthan63characterlooooooooooooooooooooooongname").
1330-
WithClass("aa").
1331-
Build()).
1332-
Build()).
1333-
Build(),
1334-
wantErr: true,
1335-
},
13361306
}
13371307
for _, tt := range tests {
13381308
t.Run(tt.name, func(t *testing.T) {
@@ -1510,36 +1480,6 @@ func TestMachinePoolTopologiesAreUniqueAndDefinedInClusterClass(t *testing.T) {
15101480
Build(),
15111481
wantErr: false,
15121482
},
1513-
{
1514-
name: "fail if MachinePoolTopology name is longer than 63 characters",
1515-
clusterClass: builder.ClusterClass(metav1.NamespaceDefault, "class1").
1516-
WithInfrastructureClusterTemplate(
1517-
builder.InfrastructureClusterTemplate(metav1.NamespaceDefault, "infra1").Build()).
1518-
WithControlPlaneTemplate(
1519-
builder.ControlPlane(metav1.NamespaceDefault, "cp1").Build()).
1520-
WithControlPlaneInfrastructureMachineTemplate(
1521-
builder.InfrastructureMachinePoolTemplate(metav1.NamespaceDefault, "cpinfra1").Build()).
1522-
WithWorkerMachinePoolClasses(
1523-
*builder.MachinePoolClass("aa").
1524-
WithInfrastructureTemplate(
1525-
builder.InfrastructureMachinePoolTemplate(metav1.NamespaceDefault, "infra1").Build()).
1526-
WithBootstrapTemplate(
1527-
builder.BootstrapTemplate(metav1.NamespaceDefault, "bootstrap1").Build()).
1528-
Build()).
1529-
Build(),
1530-
cluster: builder.Cluster(metav1.NamespaceDefault, "cluster1").
1531-
WithTopology(
1532-
builder.ClusterTopology().
1533-
WithClass("class1").
1534-
WithVersion("v1.22.2").
1535-
WithMachinePool(
1536-
builder.MachinePoolTopology("machine-pool-topology-name-that-has-longerthan63characterlooooooooooooooooooooooongname").
1537-
WithClass("aa").
1538-
Build()).
1539-
Build()).
1540-
Build(),
1541-
wantErr: true,
1542-
},
15431483
}
15441484
for _, tt := range tests {
15451485
t.Run(tt.name, func(t *testing.T) {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package test
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
25+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
26+
"sigs.k8s.io/cluster-api/util/test/builder"
27+
)
28+
29+
func Test_ValidateCluster(t *testing.T) {
30+
tests := []struct {
31+
name string
32+
mdTopologyName string
33+
mpTopologyName string
34+
cluster *clusterv1.Cluster
35+
wantErrs []string
36+
}{
37+
{
38+
name: "fail if names are not valid Kubernetes resource names",
39+
mdTopologyName: "under_score",
40+
mpTopologyName: "under_score",
41+
wantErrs: []string{
42+
"spec.topology.workers.machineDeployments[0].name: Invalid value: \"under_score\": spec.topology.workers.machineDeployments[0].name in body should match",
43+
"spec.topology.workers.machinePools[0].name: Invalid value: \"under_score\": spec.topology.workers.machinePools[0].name in body should match",
44+
},
45+
},
46+
{
47+
name: "fail if names are not valid Kubernetes label values",
48+
mdTopologyName: "forward/slash",
49+
mpTopologyName: "forward/slash",
50+
wantErrs: []string{
51+
"spec.topology.workers.machineDeployments[0].name: Invalid value: \"forward/slash\": spec.topology.workers.machineDeployments[0].name in body should match",
52+
"spec.topology.workers.machinePools[0].name: Invalid value: \"forward/slash\": spec.topology.workers.machinePools[0].name in body should match",
53+
},
54+
},
55+
{
56+
name: "fail if names are longer than 63 characters (the maximum length of a Kubernetes label value)",
57+
mdTopologyName: "machine-deployment-topology-name-that-has-longerthan63characterlooooooooooooooooooooooongname",
58+
mpTopologyName: "machine-pool-topology-name-that-has-longerthan63characterlooooooooooooooooooooooongname",
59+
wantErrs: []string{
60+
"spec.topology.workers.machineDeployments[0].name: Too long: may not be more than 63 bytes",
61+
"spec.topology.workers.machinePools[0].name: Too long: may not be more than 63 bytes",
62+
},
63+
},
64+
{
65+
name: "succeed with valid name",
66+
mdTopologyName: "valid-machine-deployment-topology-name",
67+
mpTopologyName: "valid-machine-pool-topology-name",
68+
},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.name, func(t *testing.T) {
73+
g := NewWithT(t)
74+
75+
cluster := builder.Cluster(metav1.NamespaceDefault, "cluster1").
76+
WithTopology(
77+
builder.ClusterTopology().
78+
WithClass("class1").
79+
WithVersion("v1.22.2").
80+
WithMachineDeployment(
81+
builder.MachineDeploymentTopology(tt.mdTopologyName).
82+
WithClass("aa").
83+
Build()).
84+
WithMachinePool(
85+
builder.MachinePoolTopology(tt.mpTopologyName).
86+
WithClass("aa").
87+
Build()).
88+
Build()).
89+
Build()
90+
91+
err := env.CreateAndWait(ctx, cluster)
92+
93+
if len(tt.wantErrs) > 0 {
94+
g.Expect(err).To(HaveOccurred())
95+
for _, wantErr := range tt.wantErrs {
96+
g.Expect(err.Error()).To(ContainSubstring(wantErr))
97+
}
98+
} else {
99+
g.Expect(err).ToNot(HaveOccurred())
100+
g.Expect(env.CleanupAndWait(ctx, cluster)).To(Succeed())
101+
}
102+
})
103+
}
104+
}

0 commit comments

Comments
 (0)