Skip to content

Commit adf320c

Browse files
Resolve files with duplicate content, add tests
Signed-off-by: Danil-Grigorev <[email protected]>
1 parent 4a773f8 commit adf320c

File tree

7 files changed

+423
-59
lines changed

7 files changed

+423
-59
lines changed

cmd/plugin/cmd/init_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@ func TestInitProviders(t *testing.T) {
265265
opts: &initOptions{
266266
coreProvider: "cluster-api:capi-system:v1.8.0",
267267
infrastructureProviders: []string{
268-
"cluster-api:capi-system:v1.8.0",
269268
"aws:capa-operator-system",
270269
"docker:capd-operator-system",
271270
},

cmd/plugin/cmd/preload.go

Lines changed: 74 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ import (
2121
"fmt"
2222
"os"
2323
"strings"
24+
"time"
2425

2526
"github.com/spf13/cobra"
2627
corev1 "k8s.io/api/core/v1"
27-
apierrors "k8s.io/apimachinery/pkg/api/errors"
28-
"k8s.io/apimachinery/pkg/api/meta"
2928
kerrors "k8s.io/apimachinery/pkg/util/errors"
29+
"k8s.io/apimachinery/pkg/util/wait"
3030
"oras.land/oras-go/v2/registry/remote/auth"
3131
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
3232
providercontroller "sigs.k8s.io/cluster-api-operator/internal/controller"
@@ -207,50 +207,99 @@ func runPreLoad() error {
207207
configMaps = append(configMaps, configMap)
208208
}
209209

210-
errors := []error{}
211-
212-
if !loadOpts.existing {
213-
for _, cm := range configMaps {
214-
out, err := yaml.Marshal(cm)
215-
if err != nil {
216-
return fmt.Errorf("cannot serialize provider config map: %w", err)
217-
}
210+
if loadOpts.existing {
211+
client, err := CreateKubeClient(loadOpts.kubeconfig, "")
212+
if err != nil {
213+
return fmt.Errorf("cannot create a client: %w", err)
214+
}
218215

219-
fmt.Printf("---\n%s", string(out))
216+
existing, err := preloadExisting(ctx, client)
217+
if err != nil {
218+
return err
220219
}
221220

222-
return nil
221+
configMaps = append(configMaps, existing...)
223222
}
224223

225-
client, err := CreateKubeClient(loadOpts.kubeconfig, "")
226-
if err != nil {
227-
return fmt.Errorf("cannot create a client: %w", err)
224+
for _, cm := range configMaps {
225+
out, err := yaml.Marshal(cm)
226+
if err != nil {
227+
return fmt.Errorf("cannot serialize provider config map: %w", err)
228+
}
229+
230+
fmt.Printf("---\n%s", string(out))
228231
}
229232

233+
return nil
234+
}
235+
236+
// preloadExisting uses existing cluster kubeconfig to list providers and create configmaps with components for each provider.
237+
func preloadExisting(ctx context.Context, cl client.Client) ([]*corev1.ConfigMap, error) {
238+
errors := []error{}
239+
configMaps := []*corev1.ConfigMap{}
240+
230241
for _, list := range operatorv1.ProviderLists {
231-
maps, err := fetchProviders(ctx, client, list.(genericProviderList))
242+
list, ok := list.(genericProviderList)
243+
if !ok {
244+
log.V(5).Info("Expected to get GenericProviderList")
245+
continue
246+
}
247+
248+
list, ok = list.DeepCopyObject().(genericProviderList)
249+
if !ok {
250+
log.V(5).Info("Expected to get GenericProviderList")
251+
continue
252+
}
253+
254+
maps, err := fetchProviders(ctx, cl, list)
232255
configMaps = append(configMaps, maps...)
233256
errors = append(errors, err)
234257
}
235258

236-
for _, cm := range configMaps {
237-
out, err := yaml.Marshal(cm)
238-
if err != nil {
239-
return fmt.Errorf("cannot serialize provider config map: %w", err)
259+
return configMaps, kerrors.NewAggregate(errors)
260+
}
261+
262+
// retryWithExponentialBackoff repeats an operation until it passes or the exponential backoff times out.
263+
func retryWithExponentialBackoff(ctx context.Context, opts wait.Backoff, operation func(ctx context.Context) error) error {
264+
i := 0
265+
if err := wait.ExponentialBackoffWithContext(ctx, opts, func(ctx context.Context) (bool, error) {
266+
i++
267+
if err := operation(ctx); err != nil {
268+
if i < opts.Steps {
269+
log.V(5).Info("Retrying with backoff", "cause", err.Error())
270+
return false, nil
271+
}
272+
273+
return false, err
240274
}
241275

242-
fmt.Printf("---\n%s", string(out))
276+
return true, nil
277+
}); err != nil {
278+
return fmt.Errorf("action failed after %d attempts: %w", i, err)
243279
}
244280

245-
return kerrors.NewAggregate(errors)
281+
return nil
282+
}
283+
284+
// newReadBackoff creates a new API Machinery backoff parameter set suitable for use with CLI cluster operations.
285+
func newReadBackoff() wait.Backoff {
286+
// Return a exponential backoff configuration which returns durations for a total time of ~15s.
287+
// Example: 0, .25s, .6s, 1.2, 2.1s, 3.4s, 5.5s, 8s, 12s
288+
// Jitter is added as a random fraction of the duration multiplied by the jitter factor.
289+
return wait.Backoff{
290+
Duration: 250 * time.Millisecond,
291+
Factor: 1.5,
292+
Steps: 9,
293+
Jitter: 0.1,
294+
}
246295
}
247296

248297
func fetchProviders(ctx context.Context, cl client.Client, providerList genericProviderList) ([]*corev1.ConfigMap, error) {
249298
configMaps := []*corev1.ConfigMap{}
250299

251-
if err := cl.List(ctx, providerList, client.InNamespace("")); meta.IsNoMatchError(err) || apierrors.IsNotFound(err) {
252-
return configMaps, nil
253-
} else if err != nil {
300+
if err := retryWithExponentialBackoff(ctx, newReadBackoff(), func(ctx context.Context) error {
301+
return cl.List(ctx, providerList, client.InNamespace(""))
302+
}); err != nil {
254303
log.Error(err, fmt.Sprintf("Unable to list providers, %#v", err))
255304

256305
return configMaps, err

cmd/plugin/cmd/preload_test.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
Copyright 2025 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 cmd
18+
19+
import (
20+
"cmp"
21+
"os"
22+
"path"
23+
"testing"
24+
25+
. "github.com/onsi/gomega"
26+
corev1 "k8s.io/api/core/v1"
27+
"k8s.io/apimachinery/pkg/types"
28+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
29+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
30+
31+
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
32+
"sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider"
33+
)
34+
35+
type publishProvider struct {
36+
configMapName string
37+
provider genericprovider.GenericProvider
38+
metadataKey string
39+
componentsKey string
40+
metadataData []byte
41+
componentsData []byte
42+
}
43+
44+
type publishOptions struct {
45+
ociUrl string
46+
providers []publishProvider
47+
}
48+
49+
func TestPreloadCommand(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
customURL string
53+
publishOpts *publishOptions
54+
existingProviders []genericprovider.GenericProvider
55+
expectedConfigMaps int
56+
wantErr bool
57+
}{
58+
{
59+
name: "no providers",
60+
wantErr: false,
61+
},
62+
{
63+
name: "builtin core provider with OCI override",
64+
publishOpts: &publishOptions{
65+
ociUrl: "ttl.sh/cluster-api-operator-manifests:1m",
66+
providers: []publishProvider{{
67+
configMapName: "core-cluster-api-v1.9.3",
68+
provider: generateGenericProvider(clusterctlv1.CoreProviderType, "cluster-api", "default", "v1.9.3", "", ""),
69+
metadataKey: "metadata.yaml",
70+
metadataData: []byte("metadata"),
71+
componentsKey: "components.yaml",
72+
componentsData: []byte("components"),
73+
}},
74+
},
75+
expectedConfigMaps: 1,
76+
},
77+
{
78+
name: "multiple providers with OCI override",
79+
publishOpts: &publishOptions{
80+
ociUrl: "ttl.sh/cluster-api-operator-manifests:1m",
81+
providers: []publishProvider{{
82+
configMapName: "core-cluster-api-v1.9.3",
83+
provider: generateGenericProvider(clusterctlv1.CoreProviderType, "cluster-api", "default", "v1.9.3", "", ""),
84+
metadataKey: "core-cluster-api-v1.9.3-metadata.yaml",
85+
metadataData: []byte("metadata"),
86+
componentsKey: "core-cluster-api-v1.9.3-components.yaml",
87+
componentsData: []byte("components"),
88+
}, {
89+
configMapName: "infrastructure-docker-v1.9.3",
90+
provider: generateGenericProvider(clusterctlv1.InfrastructureProviderType, "docker", "default", "v1.9.3", "", ""),
91+
metadataKey: "infrastructure-docker-v1.9.3-metadata.yaml",
92+
metadataData: []byte("metadata"),
93+
componentsKey: "infrastructure-docker-v1.9.3-components.yaml",
94+
componentsData: []byte("components"),
95+
}},
96+
},
97+
expectedConfigMaps: 2,
98+
},
99+
{
100+
name: "custom url infra provider",
101+
existingProviders: []genericprovider.GenericProvider{
102+
func() genericprovider.GenericProvider {
103+
p := generateGenericProvider(clusterctlv1.InfrastructureProviderType, "docker", "default", "v1.9.3", "", "")
104+
spec := p.GetSpec()
105+
spec.FetchConfig = &operatorv1.FetchConfiguration{
106+
URL: "https://github.com/kubernetes-sigs/cluster-api/releases/latest/core-components.yaml",
107+
}
108+
p.SetSpec(spec)
109+
110+
return p
111+
}(),
112+
},
113+
expectedConfigMaps: 1,
114+
},
115+
{
116+
name: "regular core and infra provider",
117+
existingProviders: []genericprovider.GenericProvider{
118+
generateGenericProvider(clusterctlv1.CoreProviderType, "cluster-api", "default", "v1.9.3", "", ""),
119+
generateGenericProvider(clusterctlv1.InfrastructureProviderType, "docker", "default", "v1.9.3", "", ""),
120+
},
121+
expectedConfigMaps: 2,
122+
},
123+
{
124+
name: "OCI override with incorrect metadata key",
125+
publishOpts: &publishOptions{
126+
ociUrl: "ttl.sh/cluster-api-operator-manifests:1m",
127+
providers: []publishProvider{{
128+
configMapName: "core-cluster-api-v1.9.3",
129+
provider: generateGenericProvider(clusterctlv1.InfrastructureProviderType, "metadata-missing", "default", "v1.9.3", "", ""),
130+
metadataKey: "incorrect-metadata.yaml",
131+
metadataData: []byte("test"),
132+
componentsKey: "components.yaml",
133+
componentsData: []byte("test"),
134+
}},
135+
},
136+
wantErr: true,
137+
},
138+
{
139+
name: "OCI override with incorrect components key",
140+
publishOpts: &publishOptions{
141+
ociUrl: "ttl.sh/cluster-api-operator-manifests:1m",
142+
providers: []publishProvider{{
143+
configMapName: "core-cluster-api-v1.9.3",
144+
provider: generateGenericProvider(clusterctlv1.InfrastructureProviderType, "components-missing", "default", "v1.9.3", "", ""),
145+
metadataKey: "metadata.yaml",
146+
metadataData: []byte("test"),
147+
componentsKey: "incorrect-components.yaml",
148+
componentsData: []byte("test"),
149+
}},
150+
},
151+
wantErr: true,
152+
},
153+
}
154+
for _, tt := range tests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
g := NewWithT(t)
157+
158+
dir, err := os.MkdirTemp("", "manifests")
159+
defer func() {
160+
g.Expect(os.RemoveAll(dir)).To(Succeed())
161+
}()
162+
g.Expect(err).To(Succeed())
163+
164+
opts := cmp.Or(tt.publishOpts, &publishOptions{})
165+
if tt.publishOpts != nil && opts.ociUrl != "" {
166+
for _, provider := range opts.providers {
167+
err = os.WriteFile(path.Join(dir, provider.metadataKey), provider.metadataData, 0o777)
168+
g.Expect(err).To(Succeed())
169+
err = os.WriteFile(path.Join(dir, provider.componentsKey), provider.componentsData, 0o777)
170+
g.Expect(err).To(Succeed())
171+
}
172+
173+
g.Expect(publish(ctx, dir, opts.ociUrl)).To(Succeed())
174+
175+
for _, data := range opts.providers {
176+
spec := data.provider.GetSpec()
177+
spec.FetchConfig = &operatorv1.FetchConfiguration{
178+
OCI: opts.ociUrl,
179+
}
180+
data.provider.SetSpec(spec)
181+
g.Expect(env.Client.Create(ctx, data.provider)).To(Succeed())
182+
}
183+
}
184+
185+
resources := []ctrlclient.Object{}
186+
for _, provider := range tt.existingProviders {
187+
resources = append(resources, provider)
188+
}
189+
190+
for _, data := range opts.providers {
191+
resources = append(resources, data.provider)
192+
}
193+
194+
defer func() {
195+
g.Expect(env.CleanupAndWait(ctx, resources...)).To(Succeed())
196+
}()
197+
198+
for _, genericProvider := range tt.existingProviders {
199+
g.Expect(env.Client.Create(ctx, genericProvider)).To(Succeed())
200+
}
201+
202+
configMaps := []*corev1.ConfigMap{}
203+
204+
g.Eventually(func(g Gomega) {
205+
configMaps, err = preloadExisting(ctx, env)
206+
g.Expect(tt.expectedConfigMaps).To(Equal(len(configMaps)))
207+
if tt.wantErr {
208+
g.Expect(err).To(HaveOccurred())
209+
} else {
210+
g.Expect(err).NotTo(HaveOccurred())
211+
212+
maps := map[types.NamespacedName]*corev1.ConfigMap{}
213+
for _, cm := range configMaps {
214+
maps[ctrlclient.ObjectKeyFromObject(cm)] = cm
215+
}
216+
217+
for _, data := range opts.providers {
218+
cm, ok := maps[types.NamespacedName{
219+
Namespace: data.provider.GetNamespace(),
220+
Name: data.configMapName,
221+
}]
222+
223+
g.Expect(ok).To(BeTrue())
224+
225+
g.Expect(cm.Data["metadata"]).To(Equal(string(data.metadataData)))
226+
g.Expect(cm.Data["components"]).To(Equal(string(data.componentsData)))
227+
}
228+
}
229+
}, "15s", "1s").Should(Succeed())
230+
})
231+
}
232+
}

0 commit comments

Comments
 (0)