Skip to content

Commit 5d11aa7

Browse files
Add e2e test to cover crossNamespace provisionging
- Prevent duplicate CC creation in template processing - Test runtimextension integration - Add changes to CC rebase e2e test - Add a note CLUSTER_CLASS_NAMESPACE to the clusterctl contract Signed-off-by: Danil-Grigorev <[email protected]> Co-authored-by: Christian Schlotter <[email protected]>
1 parent c0cf7c6 commit 5d11aa7

File tree

13 files changed

+329
-60
lines changed

13 files changed

+329
-60
lines changed

cmd/clusterctl/client/clusterclass.go

+11-9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import (
2323
"github.com/pkg/errors"
2424
apierrors "k8s.io/apimachinery/pkg/api/errors"
2525
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
26+
"k8s.io/apimachinery/pkg/types"
27+
"k8s.io/klog/v2"
2628
"sigs.k8s.io/controller-runtime/pkg/client"
2729

2830
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
@@ -34,7 +36,7 @@ import (
3436
// addClusterClassIfMissing returns a Template that includes the base template and adds any cluster class definitions that
3537
// are references in the template. If the cluster class referenced already exists in the cluster it is not added to the
3638
// template.
37-
func addClusterClassIfMissing(ctx context.Context, template Template, clusterClassClient repository.ClusterClassClient, clusterClient cluster.Client, targetNamespace string, listVariablesOnly bool) (Template, error) {
39+
func addClusterClassIfMissing(ctx context.Context, template Template, clusterClassClient repository.ClusterClassClient, clusterClient cluster.Client, listVariablesOnly bool) (Template, error) {
3840
classes, err := clusterClassNamesFromTemplate(template)
3941
if err != nil {
4042
return nil, err
@@ -44,7 +46,7 @@ func addClusterClassIfMissing(ctx context.Context, template Template, clusterCla
4446
return template, nil
4547
}
4648

47-
clusterClassesTemplate, err := fetchMissingClusterClassTemplates(ctx, clusterClassClient, clusterClient, classes, targetNamespace, listVariablesOnly)
49+
clusterClassesTemplate, err := fetchMissingClusterClassTemplates(ctx, clusterClassClient, clusterClient, classes, listVariablesOnly)
4850
if err != nil {
4951
return nil, err
5052
}
@@ -62,8 +64,8 @@ func addClusterClassIfMissing(ctx context.Context, template Template, clusterCla
6264
// clusterClassNamesFromTemplate returns the list of ClusterClasses referenced
6365
// by clusters defined in the template. If not clusters are defined in the template
6466
// or if no cluster uses a cluster class it returns an empty list.
65-
func clusterClassNamesFromTemplate(template Template) ([]string, error) {
66-
classes := []string{}
67+
func clusterClassNamesFromTemplate(template Template) ([]types.NamespacedName, error) {
68+
classes := []types.NamespacedName{}
6769

6870
// loop through all the objects and if the object is a cluster
6971
// check and see if cluster.spec.topology.class is defined.
@@ -80,14 +82,14 @@ func clusterClassNamesFromTemplate(template Template) ([]string, error) {
8082
if cluster.Spec.Topology == nil {
8183
continue
8284
}
83-
classes = append(classes, cluster.GetClassKey().Name)
85+
classes = append(classes, cluster.GetClassKey())
8486
}
8587
return classes, nil
8688
}
8789

8890
// fetchMissingClusterClassTemplates returns a list of templates for ClusterClasses that do not yet exist
8991
// in the cluster. If the cluster is not initialized, all the ClusterClasses are added.
90-
func fetchMissingClusterClassTemplates(ctx context.Context, clusterClassClient repository.ClusterClassClient, clusterClient cluster.Client, classes []string, targetNamespace string, listVariablesOnly bool) (Template, error) {
92+
func fetchMissingClusterClassTemplates(ctx context.Context, clusterClassClient repository.ClusterClassClient, clusterClient cluster.Client, classes []types.NamespacedName, listVariablesOnly bool) (Template, error) {
9193
// first check if the cluster is initialized.
9294
// If it is initialized:
9395
// For every ClusterClass check if it already exists in the cluster.
@@ -118,7 +120,7 @@ func fetchMissingClusterClassTemplates(ctx context.Context, clusterClassClient r
118120
templates := []repository.Template{}
119121
for _, class := range classes {
120122
if clusterInitialized {
121-
exists, err := clusterClassExists(ctx, c, class, targetNamespace)
123+
exists, err := clusterClassExists(ctx, c, class.Name, class.Namespace)
122124
if err != nil {
123125
return nil, err
124126
}
@@ -128,7 +130,7 @@ func fetchMissingClusterClassTemplates(ctx context.Context, clusterClassClient r
128130
}
129131
// The cluster is either not initialized or the ClusterClass does not yet exist in the cluster.
130132
// Fetch the cluster class to install.
131-
clusterClassTemplate, err := clusterClassClient.Get(ctx, class, targetNamespace, listVariablesOnly)
133+
clusterClassTemplate, err := clusterClassClient.Get(ctx, class.Name, class.Namespace, listVariablesOnly)
132134
if err != nil {
133135
return nil, errors.Wrapf(err, "failed to get the cluster class template for %q", class)
134136
}
@@ -142,7 +144,7 @@ func fetchMissingClusterClassTemplates(ctx context.Context, clusterClassClient r
142144
if exists, err := objExists(ctx, c, obj); err != nil {
143145
return nil, err
144146
} else if exists {
145-
return nil, fmt.Errorf("%s(%s) already exists in the cluster", obj.GetName(), obj.GetObjectKind().GroupVersionKind())
147+
return nil, fmt.Errorf("%s(%s) already exists in the cluster", klog.KObj(&obj), obj.GetObjectKind().GroupVersionKind())
146148
}
147149
}
148150
}

cmd/clusterctl/client/clusterclass_test.go

+41-5
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ func TestAddClusterClassIfMissing(t *testing.T) {
100100
objs []client.Object
101101
clusterClassTemplateContent []byte
102102
targetNamespace string
103+
clusterClassNamespace string
103104
listVariablesOnly bool
104105
wantClusterClassInTemplate bool
105106
wantError bool
@@ -114,6 +115,28 @@ func TestAddClusterClassIfMissing(t *testing.T) {
114115
wantClusterClassInTemplate: true,
115116
wantError: false,
116117
},
118+
{
119+
name: "should add the cluster class from a different namespace to the template if cluster is not initialized",
120+
clusterInitialized: false,
121+
objs: []client.Object{},
122+
targetNamespace: "ns1",
123+
clusterClassNamespace: "ns2",
124+
clusterClassTemplateContent: clusterClassYAML("ns2", "dev"),
125+
listVariablesOnly: false,
126+
wantClusterClassInTemplate: true,
127+
wantError: false,
128+
},
129+
{
130+
name: "should add the cluster class form the same explicitly specified namespace to the template if cluster is not initialized",
131+
clusterInitialized: false,
132+
objs: []client.Object{},
133+
targetNamespace: "ns1",
134+
clusterClassNamespace: "ns1",
135+
clusterClassTemplateContent: clusterClassYAML("ns1", "dev"),
136+
listVariablesOnly: false,
137+
wantClusterClassInTemplate: true,
138+
wantError: false,
139+
},
117140
{
118141
name: "should add the cluster class to the template if cluster is initialized and cluster class is not installed",
119142
clusterInitialized: true,
@@ -189,17 +212,21 @@ func TestAddClusterClassIfMissing(t *testing.T) {
189212

190213
clusterClassClient := repository1.ClusterClasses("v1.0.0")
191214

192-
clusterWithTopology := []byte(fmt.Sprintf("apiVersion: %s\n", clusterv1.GroupVersion.String()) +
215+
clusterWithTopology := fmt.Sprintf("apiVersion: %s\n", clusterv1.GroupVersion.String()) +
193216
"kind: Cluster\n" +
194217
"metadata:\n" +
195218
" name: cluster-dev\n" +
196219
fmt.Sprintf(" namespace: %s\n", tt.targetNamespace) +
197220
"spec:\n" +
198221
" topology:\n" +
199-
" class: dev")
222+
" class: dev"
223+
224+
if tt.clusterClassNamespace != "" {
225+
clusterWithTopology = fmt.Sprintf("%s\n classNamespace: %s", clusterWithTopology, tt.clusterClassNamespace)
226+
}
200227

201228
baseTemplate, err := repository.NewTemplate(repository.TemplateInput{
202-
RawArtifact: clusterWithTopology,
229+
RawArtifact: []byte(clusterWithTopology),
203230
ConfigVariablesClient: test.NewFakeVariableClient(),
204231
Processor: yaml.NewSimpleProcessor(),
205232
TargetNamespace: tt.targetNamespace,
@@ -210,13 +237,22 @@ func TestAddClusterClassIfMissing(t *testing.T) {
210237
}
211238

212239
g := NewWithT(t)
213-
template, err := addClusterClassIfMissing(ctx, baseTemplate, clusterClassClient, cluster, tt.targetNamespace, tt.listVariablesOnly)
240+
template, err := addClusterClassIfMissing(ctx, baseTemplate, clusterClassClient, cluster, tt.listVariablesOnly)
214241
if tt.wantError {
215242
g.Expect(err).To(HaveOccurred())
216243
} else {
217244
if tt.wantClusterClassInTemplate {
218-
g.Expect(template.Objs()).To(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
245+
if tt.clusterClassNamespace == tt.targetNamespace {
246+
g.Expect(template.Objs()).To(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
247+
} else if tt.clusterClassNamespace != "" {
248+
g.Expect(template.Objs()).To(ContainElement(MatchClusterClass("dev", tt.clusterClassNamespace)))
249+
g.Expect(template.Objs()).ToNot(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
250+
} else {
251+
g.Expect(template.Objs()).To(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
252+
g.Expect(template.Objs()).ToNot(ContainElement(MatchClusterClass("dev", tt.clusterClassNamespace)))
253+
}
219254
} else {
255+
g.Expect(template.Objs()).NotTo(ContainElement(MatchClusterClass("dev", tt.clusterClassNamespace)))
220256
g.Expect(template.Objs()).NotTo(ContainElement(MatchClusterClass("dev", tt.targetNamespace)))
221257
}
222258
}

cmd/clusterctl/client/config.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ func (c *clusterctlClient) getTemplateFromRepository(ctx context.Context, cluste
344344

345345
clusterClassClient := repo.ClusterClasses(version)
346346

347-
template, err = addClusterClassIfMissing(ctx, template, clusterClassClient, cluster, targetNamespace, listVariablesOnly)
347+
template, err = addClusterClassIfMissing(ctx, template, clusterClassClient, cluster, listVariablesOnly)
348348
if err != nil {
349349
return nil, err
350350
}

cmd/clusterctl/client/repository/template.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ limitations under the License.
1717
package repository
1818

1919
import (
20-
"fmt"
21-
2220
"github.com/pkg/errors"
2321
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2422
"k8s.io/apimachinery/pkg/util/sets"
@@ -144,7 +142,7 @@ func NewTemplate(input TemplateInput) (Template, error) {
144142

145143
// MergeTemplates merges the provided Templates into one Template.
146144
// Notes on the merge operation:
147-
// - The merge operation returns an error if all the templates do not have the same TargetNamespace.
145+
// - The merge operation sets targetNamespace empty if all the templates do not share the same TargetNamespace.
148146
// - The Variables of the resulting template is a union of all Variables in the templates.
149147
// - The default value is picked from the first template that defines it.
150148
// The defaults of the same variable in the subsequent templates will be ignored.
@@ -173,7 +171,7 @@ func MergeTemplates(templates ...Template) (Template, error) {
173171
}
174172

175173
if merged.targetNamespace != tmpl.TargetNamespace() {
176-
return nil, fmt.Errorf("cannot merge templates with different targetNamespaces")
174+
merged.targetNamespace = ""
177175
}
178176

179177
merged.objs = append(merged.objs, tmpl.Objs()...)

docs/book/src/developer/providers/contracts/clusterctl.md

+6
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,12 @@ ClusterClass definitions MUST be stored in the same location as the component YA
416416
in the Cluster template; Cluster template files using a ClusterClass are usually simpler because they are no longer
417417
required to have all the templates.
418418

419+
Additionally, namespace of the ClusterClass can differ from the Cluster. This requires specifying
420+
Cluster.spec.topology.classNamespace field in the Cluster template;
421+
Cluster template may define classNamespace as `classNamespace: ${CLUSTER_CLASS_NAMESPACE:=""}`, which would allow to
422+
optionally specify namespace of the referred ClusterClass via env. Empty or missing value is uses Cluster namespace
423+
by default.
424+
419425
Each provider should create user facing documentation with the list of available ClusterClass definitions.
420426

421427
#### Target namespace

test/e2e/cluster_upgrade_runtimesdk.go

+27-4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ type ClusterUpgradeWithRuntimeSDKSpecInput struct {
102102

103103
// ExtensionServiceName is the name of the service to configure in the test-namespace scoped ExtensionConfig.
104104
ExtensionServiceName string
105+
106+
// DeployClusterClassInSeparateNamespace defines if the ClusterClass should be deployed in a separate namespace.
107+
DeployClusterClassInSeparateNamespace bool
105108
}
106109

107110
// ClusterUpgradeWithRuntimeSDKSpec implements a spec that upgrades a cluster and runs the Kubernetes conformance suite.
@@ -116,9 +119,9 @@ func ClusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() Cl
116119
)
117120

118121
var (
119-
input ClusterUpgradeWithRuntimeSDKSpecInput
120-
namespace *corev1.Namespace
121-
cancelWatches context.CancelFunc
122+
input ClusterUpgradeWithRuntimeSDKSpecInput
123+
namespace, clusterClassNamespace *corev1.Namespace
124+
cancelWatches context.CancelFunc
122125

123126
controlPlaneMachineCount int64
124127
workerMachineCount int64
@@ -158,6 +161,10 @@ func ClusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() Cl
158161

159162
// Set up a Namespace where to host objects for this spec and create a watcher for the Namespace events.
160163
namespace, cancelWatches = framework.SetupSpecNamespace(ctx, specName, input.BootstrapClusterProxy, input.ArtifactFolder, input.PostNamespaceCreated)
164+
if input.DeployClusterClassInSeparateNamespace {
165+
clusterClassNamespace = framework.CreateNamespace(ctx, framework.CreateNamespaceInput{Creator: input.BootstrapClusterProxy.GetClient(), Name: fmt.Sprintf("%s-clusterclass", namespace.Name)}, "40s", "10s")
166+
Expect(clusterClassNamespace).ToNot(BeNil(), "Failed to create namespace")
167+
}
161168
clusterName = fmt.Sprintf("%s-%s", specName, util.RandomString(6))
162169
clusterResources = new(clusterctl.ApplyClusterTemplateAndWaitResult)
163170
})
@@ -171,11 +178,16 @@ func ClusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() Cl
171178

172179
By("Deploy Test Extension ExtensionConfig")
173180

181+
namespaces := []string{namespace.Name}
182+
if input.DeployClusterClassInSeparateNamespace {
183+
namespaces = append(namespaces, clusterClassNamespace.Name)
184+
}
185+
174186
// In this test we are defaulting all handlers to blocking because we expect the handlers to block the
175187
// cluster lifecycle by default. Setting defaultAllHandlersToBlocking to true enforces that the test-extension
176188
// automatically creates the ConfigMap with blocking preloaded responses.
177189
Expect(input.BootstrapClusterProxy.GetClient().Create(ctx,
178-
extensionConfig(input.ExtensionConfigName, input.ExtensionServiceNamespace, input.ExtensionServiceName, true, namespace.Name))).
190+
extensionConfig(input.ExtensionConfigName, input.ExtensionServiceNamespace, input.ExtensionServiceName, true, namespaces...))).
179191
To(Succeed(), "Failed to create the extension config")
180192

181193
By("Creating a workload cluster; creation waits for BeforeClusterCreateHook to gate the operation")
@@ -194,6 +206,9 @@ func ClusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() Cl
194206
// This is used to template the name of the ExtensionConfig into the ClusterClass.
195207
"EXTENSION_CONFIG_NAME": input.ExtensionConfigName,
196208
}
209+
if input.DeployClusterClassInSeparateNamespace {
210+
variables["CLUSTER_CLASS_NAMESPACE"] = clusterClassNamespace.Name
211+
}
197212

198213
clusterctl.ApplyClusterTemplateAndWait(ctx, clusterctl.ApplyClusterTemplateAndWaitInput{
199214
ClusterProxy: input.BootstrapClusterProxy,
@@ -341,6 +356,14 @@ func ClusterUpgradeWithRuntimeSDKSpec(ctx context.Context, inputGetter func() Cl
341356
Deleter: input.BootstrapClusterProxy.GetClient(),
342357
Name: namespace.Name,
343358
})
359+
360+
if input.DeployClusterClassInSeparateNamespace {
361+
Byf("Deleting namespace used for hosting the %q test spec ClusterClass", specName)
362+
framework.DeleteNamespace(ctx, framework.DeleteNamespaceInput{
363+
Deleter: input.BootstrapClusterProxy.GetClient(),
364+
Name: clusterClassNamespace.Name,
365+
})
366+
}
344367
}
345368
cancelWatches()
346369
})

test/e2e/cluster_upgrade_runtimesdk_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,37 @@ var _ = Describe("When upgrading a workload cluster using ClusterClass with Runt
6060
}
6161
})
6262
})
63+
64+
var _ = Describe("When upgrading a workload cluster using ClusterClass in a different NS with RuntimeSDK [ClusterClass]", Label("ClusterClass"), func() {
65+
ClusterUpgradeWithRuntimeSDKSpec(ctx, func() ClusterUpgradeWithRuntimeSDKSpecInput {
66+
version, err := semver.ParseTolerant(e2eConfig.GetVariable(KubernetesVersionUpgradeFrom))
67+
Expect(err).ToNot(HaveOccurred(), "Invalid argument, KUBERNETES_VERSION_UPGRADE_FROM is not a valid version")
68+
if version.LT(semver.MustParse("1.24.0")) {
69+
Fail("This test only supports upgrades from Kubernetes >= v1.24.0")
70+
}
71+
72+
return ClusterUpgradeWithRuntimeSDKSpecInput{
73+
E2EConfig: e2eConfig,
74+
ClusterctlConfigPath: clusterctlConfigPath,
75+
BootstrapClusterProxy: bootstrapClusterProxy,
76+
ArtifactFolder: artifactFolder,
77+
SkipCleanup: skipCleanup,
78+
InfrastructureProvider: ptr.To("docker"),
79+
PostUpgrade: func(proxy framework.ClusterProxy, namespace, clusterName string) {
80+
// This check ensures that the resourceVersions are stable, i.e. it verifies there are no
81+
// continuous reconciles when everything should be stable.
82+
framework.ValidateResourceVersionStable(ctx, proxy, namespace, clusterctlcluster.FilterClusterObjectsWithNameFilter(clusterName))
83+
},
84+
// "upgrades" is the same as the "topology" flavor but with an additional MachinePool.
85+
Flavor: ptr.To("upgrades-runtimesdk"),
86+
DeployClusterClassInSeparateNamespace: true,
87+
// The runtime extension gets deployed to the test-extension-system namespace and is exposed
88+
// by the test-extension-webhook-service.
89+
// The below values are used when creating the cluster-wide ExtensionConfig to refer
90+
// the actual service.
91+
ExtensionServiceNamespace: "test-extension-system",
92+
ExtensionServiceName: "test-extension-webhook-service",
93+
ExtensionConfigName: "k8s-upgrade-with-runtimesdk-cross-ns",
94+
}
95+
})
96+
})

0 commit comments

Comments
 (0)