Skip to content

Commit 93002a8

Browse files
authored
fix: Copy ClusterClass and Templates only if they do not already exist (#1386)
**What problem does this PR solve?**: It is possible for some resource to exist, but a copy of it cannot be created. For example, CAPX validation became more strict without changing its API version. Templates already on the cluster remain usable. However, they cannot be copied, because the copy does not pass validation. Prior to this change, we always copied the template, and ignored the "already exists" error. Now we first check if the template exist, and only create it if it does not. **Which issue(s) this PR fixes**: Fixes NCN-110955 **How Has This Been Tested?**: <!-- Please describe the tests that you ran to verify your changes. Provide output from the tests and any manual steps needed to replicate the tests. --> New unit tests exercise the changes. **Special notes for your reviewer**: <!-- Use this to provide any additional information to the reviewers. This may include: - Best way to review the PR. - Where the author wants the most review attention on. - etc. -->
1 parent 3f8e22f commit 93002a8

File tree

2 files changed

+99
-60
lines changed

2 files changed

+99
-60
lines changed

pkg/controllers/namespacesync/copy.go

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99

1010
corev1 "k8s.io/api/core/v1"
11+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1112
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1213
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1314
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -29,17 +30,10 @@ func copyClusterClassAndTemplates(
2930
return fmt.Errorf("failed to get reference: %w", err)
3031
}
3132

32-
// Copy Template to target namespace
33+
// Copy Template to target namespace, if it does not exist there.
3334
targetTemplate := copyObjectForCreate(sourceTemplate, sourceTemplate.GetName(), namespace)
34-
35-
//nolint:gocritic // Inline error is checked.
36-
if err := w.Create(ctx, targetTemplate); client.IgnoreAlreadyExists(err) != nil {
37-
return fmt.Errorf(
38-
"failed to create %s %s: %w",
39-
targetTemplate.GetKind(),
40-
client.ObjectKeyFromObject(targetTemplate),
41-
err,
42-
)
35+
if err = createIfNotExists(ctx, templateReader, w, targetTemplate); err != nil {
36+
return fmt.Errorf("failed to create template: %w", err)
4337
}
4438

4539
// Update reference to point to newly created Template
@@ -51,14 +45,32 @@ func copyClusterClassAndTemplates(
5145
return fmt.Errorf("error processing references: %w", err)
5246
}
5347

54-
//nolint:gocritic // Inline error is checked.
55-
if err := w.Create(ctx, target); client.IgnoreAlreadyExists(err) != nil {
56-
return fmt.Errorf(
57-
"failed to create %s %s: %w",
58-
target.Kind,
59-
client.ObjectKeyFromObject(target),
60-
err,
61-
)
48+
// Copy ClusterClass to target namespace, if it does not exist there.
49+
if err := createIfNotExists(ctx, templateReader, w, target); err != nil {
50+
return fmt.Errorf("failed to create cluster class: %w", err)
51+
}
52+
return nil
53+
}
54+
55+
func createIfNotExists(ctx context.Context, r client.Reader, w client.Writer, obj client.Object) error {
56+
key := client.ObjectKeyFromObject(obj)
57+
gvk := obj.GetObjectKind().GroupVersionKind().String()
58+
59+
// Check if the resource exists.
60+
// We do not need the object itself, just the metadata, so we use PartialObjectMetadata.
61+
partial := &metav1.PartialObjectMetadata{}
62+
partial.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind())
63+
err := r.Get(ctx, key, partial)
64+
65+
if apierrors.IsNotFound(err) {
66+
// The resource does not exist, so create it.
67+
if err := w.Create(ctx, obj); err != nil {
68+
return fmt.Errorf("failed to create %s %s: %w", gvk, key, err)
69+
}
70+
return nil
71+
}
72+
if err != nil {
73+
return fmt.Errorf("failed to check if %s %s exists: %w", gvk, key, err)
6274
}
6375
return nil
6476
}

pkg/controllers/namespacesync/copy_test.go

Lines changed: 69 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
1616
"sigs.k8s.io/controller-runtime/pkg/client"
1717
"sigs.k8s.io/controller-runtime/pkg/client/fake"
18+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
1819

1920
"github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/internal/test/builder"
2021
)
@@ -24,53 +25,50 @@ const (
2425
targetNamespace = "target-ns"
2526
)
2627

27-
var errFakeCreate = errors.New("fake create error")
28-
29-
type mockWriter struct {
30-
client.Writer
31-
createErrOnKind string
32-
createdObjects []client.Object
33-
}
34-
35-
func (m *mockWriter) Create(
36-
ctx context.Context,
37-
obj client.Object,
38-
opts ...client.CreateOption,
39-
) error {
40-
if m.createErrOnKind != "" && obj.GetObjectKind().GroupVersionKind().Kind == m.createErrOnKind {
41-
return errFakeCreate
42-
}
43-
m.createdObjects = append(m.createdObjects, obj)
44-
// Fake setting of UID to simulate a real API server create.
45-
obj.SetUID("fake-uid")
46-
return nil
47-
}
28+
var (
29+
errFakeCreate = errors.New("fake create error")
30+
errFakeGet = errors.New("fake get error")
31+
)
4832

4933
func TestCopyClusterClassAndTemplates(t *testing.T) {
5034
g := NewWithT(t)
5135
ctx := context.Background()
5236

5337
testCases := []struct {
54-
name string
55-
createErrOnKind string
56-
expectErr error
57-
expectNumCopies int
38+
name string
39+
interceptorFuncs interceptor.Funcs
40+
kindThatFailsCreate string
41+
kindThatFailsGet string
42+
expectErr error
43+
expectNumCopies int
5844
}{
5945
{
6046
name: "should succeed if all objects are created",
6147
expectNumCopies: 6, // 1 ClusterClass + 5 templates
6248
},
6349
{
64-
name: "should fail if creating a template fails",
65-
createErrOnKind: "GenericInfrastructureClusterTemplate",
66-
expectErr: errFakeCreate,
67-
expectNumCopies: 0, // The first template create will fail.
50+
name: "should fail if creating a template fails",
51+
kindThatFailsCreate: "GenericInfrastructureClusterTemplate",
52+
expectErr: errFakeCreate,
53+
expectNumCopies: 0, // The first template create will fail.
54+
},
55+
{
56+
name: "should fail if creating the clusterclass fails",
57+
kindThatFailsCreate: "ClusterClass",
58+
expectErr: errFakeCreate,
59+
expectNumCopies: 5, // All 5 templates are created before ClusterClass.
60+
},
61+
{
62+
name: "should fail if getting a template fails",
63+
kindThatFailsGet: "GenericInfrastructureClusterTemplate",
64+
expectErr: errFakeGet,
65+
expectNumCopies: 0, // The first template get will fail.
6866
},
6967
{
70-
name: "should fail if creating the clusterclass fails",
71-
createErrOnKind: "ClusterClass",
72-
expectErr: errFakeCreate,
73-
expectNumCopies: 5, // All 5 templates are created before ClusterClass.
68+
name: "should fail if getting the clusterclass fails",
69+
kindThatFailsGet: "ClusterClass",
70+
expectErr: errFakeGet,
71+
expectNumCopies: 5, // All 5 templates are created before ClusterClass.
7472
},
7573
}
7674

@@ -85,15 +83,23 @@ func TestCopyClusterClassAndTemplates(t *testing.T) {
8583
initObjs = append(initObjs, template)
8684
}
8785

88-
fakeReader := fake.NewClientBuilder().WithRuntimeObjects(initObjs...).Build()
89-
mockWriter := &mockWriter{
90-
createErrOnKind: tc.createErrOnKind,
91-
}
86+
createdObjs := []client.Object{}
87+
fakeClient := fake.
88+
NewClientBuilder().
89+
WithInterceptorFuncs(
90+
interceptors(
91+
&createdObjs,
92+
tc.kindThatFailsGet,
93+
tc.kindThatFailsCreate,
94+
),
95+
).
96+
WithRuntimeObjects(initObjs...).
97+
Build()
9298

9399
err := copyClusterClassAndTemplates(
94100
ctx,
95-
mockWriter,
96-
fakeReader,
101+
fakeClient,
102+
fakeClient,
97103
sourceClusterClass,
98104
targetNamespace,
99105
)
@@ -105,18 +111,39 @@ func TestCopyClusterClassAndTemplates(t *testing.T) {
105111
g.Expect(err).ToNot(HaveOccurred())
106112
}
107113

108-
g.Expect(len(mockWriter.createdObjects)).To(Equal(tc.expectNumCopies))
114+
g.Expect(len(createdObjs)).To(Equal(tc.expectNumCopies))
109115

110-
for _, obj := range mockWriter.createdObjects {
116+
for _, obj := range createdObjs {
111117
g.Expect(obj.GetNamespace()).To(Equal(targetNamespace))
112118
g.Expect(obj.GetOwnerReferences()).To(BeEmpty())
113119
g.Expect(obj.GetUID()).ToNot(BeEmpty())
114-
g.Expect(obj.GetResourceVersion()).To(BeEmpty())
115120
}
116121
})
117122
}
118123
}
119124

125+
func interceptors(createdObjs *[]client.Object, kindThatFailsGet, kindThatFailsCreate string) interceptor.Funcs {
126+
return interceptor.Funcs{
127+
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
128+
if kindThatFailsGet != "" && obj.GetObjectKind().GroupVersionKind().Kind == kindThatFailsGet {
129+
return errFakeGet
130+
}
131+
return c.Get(ctx, key, obj, opts...)
132+
},
133+
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
134+
if kindThatFailsCreate != "" && obj.GetObjectKind().GroupVersionKind().Kind == kindThatFailsCreate {
135+
return errFakeCreate
136+
}
137+
138+
// Fake setting of UID to simulate a real API server create.
139+
obj.SetUID("fake-uid")
140+
141+
*createdObjs = append(*createdObjs, obj)
142+
return c.Create(ctx, obj, opts...)
143+
},
144+
}
145+
}
146+
120147
// newTestClusterClassAndTemplates is a helper to generate a valid ClusterClass with all its referenced templates.
121148
func newTestClusterClassAndTemplates(
122149
namespace,

0 commit comments

Comments
 (0)