Skip to content

Commit 986ff0e

Browse files
authored
Merge pull request #399 from Danil-Grigorev/provider-upgrader
✨ Execute provider upgrades when a version changes
2 parents e3f4480 + be5bcf1 commit 986ff0e

File tree

6 files changed

+267
-59
lines changed

6 files changed

+267
-59
lines changed

api/v1alpha2/conditions_consts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const (
4747
// ComponentsFetchErrorReason documents that an error occurred fetching the components.
4848
ComponentsFetchErrorReason = "ComponentsFetchError"
4949

50+
// ComponentsUpgradeErrorReason documents that an error occurred while upgrading the components.
51+
ComponentsUpgradeErrorReason = "ComponentsUpgradeError"
52+
5053
// OldComponentsDeletionErrorReason documents that an error occurred deleting the old components prior to upgrading.
5154
OldComponentsDeletionErrorReason = "OldComponentsDeletionError"
5255

@@ -63,4 +66,7 @@ const (
6366
const (
6467
// ProviderInstalledCondition documents a Provider that has been installed.
6568
ProviderInstalledCondition clusterv1.ConditionType = "ProviderInstalled"
69+
70+
// ProviderUpgradedCondition documents a Provider that has been recently upgraded.
71+
ProviderUpgradedCondition clusterv1.ConditionType = "ProviderUpgraded"
6672
)

internal/controller/client_proxy.go

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,83 @@ import (
2424

2525
"k8s.io/apimachinery/pkg/api/meta"
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
2728
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2829
"k8s.io/client-go/rest"
2930
"k8s.io/klog/v2"
31+
32+
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
33+
clusterctlv1 "sigs.k8s.io/cluster-api/cmd/clusterctl/api/v1alpha3"
3034
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/cluster"
35+
"sigs.k8s.io/cluster-api/cmd/clusterctl/client/repository"
3136
"sigs.k8s.io/controller-runtime/pkg/client"
3237
)
3338

39+
// clientProxy implements the Proxy interface from the clusterctl. It is used to
40+
// interact with the management cluster.
41+
type clientProxy struct {
42+
client.Client
43+
}
44+
45+
func (c clientProxy) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
46+
switch l := list.(type) {
47+
case *clusterctlv1.ProviderList:
48+
return listProviders(ctx, c.Client, l)
49+
default:
50+
return c.Client.List(ctx, l, opts...)
51+
}
52+
}
53+
54+
func (c clientProxy) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
55+
switch o := obj.(type) {
56+
case *clusterctlv1.Provider:
57+
return nil
58+
default:
59+
return c.Client.Get(ctx, key, o, opts...)
60+
}
61+
}
62+
63+
func (c clientProxy) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
64+
switch o := obj.(type) {
65+
case *clusterctlv1.Provider:
66+
return nil
67+
default:
68+
return c.Client.Patch(ctx, o, patch, opts...)
69+
}
70+
}
71+
72+
func listProviders(ctx context.Context, cl client.Client, list *clusterctlv1.ProviderList) error {
73+
providers := []operatorv1.GenericProviderList{
74+
&operatorv1.CoreProviderList{},
75+
&operatorv1.InfrastructureProviderList{},
76+
&operatorv1.BootstrapProviderList{},
77+
&operatorv1.ControlPlaneProviderList{},
78+
&operatorv1.AddonProviderList{},
79+
&operatorv1.IPAMProviderList{},
80+
}
81+
82+
for _, group := range providers {
83+
g, ok := group.(client.ObjectList)
84+
if !ok {
85+
continue
86+
}
87+
88+
if err := cl.List(ctx, g); err != nil {
89+
return err
90+
}
91+
92+
for _, p := range group.GetItems() {
93+
list.Items = append(list.Items, getProvider(p, ""))
94+
}
95+
}
96+
97+
return nil
98+
}
99+
34100
// controllerProxy implements the Proxy interface from the clusterctl. It is used to
35101
// interact with the management cluster.
36102
type controllerProxy struct {
37-
ctrlClient client.Client
103+
ctrlClient clientProxy
38104
ctrlConfig *rest.Config
39105
}
40106

@@ -153,3 +219,25 @@ func listObjByGVK(ctx context.Context, c client.Client, groupVersion, kind strin
153219

154220
return objList, nil
155221
}
222+
223+
type repositoryProxy struct {
224+
repository.Client
225+
226+
components repository.Components
227+
}
228+
229+
type repositoryClient struct {
230+
components repository.Components
231+
}
232+
233+
func (r repositoryClient) Raw(ctx context.Context, options repository.ComponentsOptions) ([]byte, error) {
234+
return nil, nil
235+
}
236+
237+
func (r repositoryClient) Get(ctx context.Context, options repository.ComponentsOptions) (repository.Components, error) {
238+
return r.components, nil
239+
}
240+
241+
func (r repositoryProxy) Components() repository.ComponentsClient {
242+
return repositoryClient{r.components}
243+
}

internal/controller/genericprovider_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,9 @@ func (r *GenericProviderReconciler) reconcile(ctx context.Context, provider gene
157157
reconciler.downloadManifests,
158158
reconciler.load,
159159
reconciler.fetch,
160-
reconciler.preInstall,
160+
reconciler.upgrade,
161161
reconciler.install,
162+
reconciler.reportStatus,
162163
}
163164

164165
res := reconcile.Result{}

internal/controller/genericprovider_controller_test.go

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ package controller
1818

1919
import (
2020
"testing"
21+
"time"
2122

2223
. "github.com/onsi/gomega"
24+
appsv1 "k8s.io/api/apps/v1"
2325
corev1 "k8s.io/api/core/v1"
2426
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2527
"k8s.io/apimachinery/pkg/runtime"
@@ -39,7 +41,7 @@ apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
3941
releaseSeries:
4042
- major: 0
4143
minor: 4
42-
contract: v1alpha4
44+
contract: v1beta1
4345
`
4446
testComponents = `
4547
apiVersion: apps/v1
@@ -215,22 +217,47 @@ func TestReconcilerPreflightConditions(t *testing.T) {
215217
}
216218
}
217219

218-
func TestUpgradeDowngradeProvider(t *testing.T) {
220+
func TestAirGappedUpgradeDowngradeProvider(t *testing.T) {
221+
currentVersion := "v999.9.2"
222+
futureMetadata := `
223+
apiVersion: clusterctl.cluster.x-k8s.io/v1alpha3
224+
releaseSeries:
225+
- major: 999
226+
minor: 9
227+
contract: v1beta1
228+
`
229+
230+
dummyFutureConfigMap := func(ns, name string) *corev1.ConfigMap {
231+
return &corev1.ConfigMap{
232+
ObjectMeta: metav1.ObjectMeta{
233+
Name: name,
234+
Namespace: ns,
235+
Labels: map[string]string{
236+
"test": "dummy-config",
237+
},
238+
},
239+
Data: map[string]string{
240+
"metadata": futureMetadata,
241+
"components": testComponents,
242+
},
243+
}
244+
}
245+
219246
testCases := []struct {
220247
name string
221248
newVersion string
222249
}{
223250
{
224251
name: "same provider version",
225-
newVersion: "v0.4.2",
252+
newVersion: "v999.9.2",
226253
},
227254
{
228255
name: "upgrade provider version",
229-
newVersion: "v0.4.3",
256+
newVersion: "v999.9.3",
230257
},
231258
{
232259
name: "downgrade provider version",
233-
newVersion: "v0.4.1",
260+
newVersion: "v999.9.1",
234261
},
235262
}
236263

@@ -244,7 +271,7 @@ func TestUpgradeDowngradeProvider(t *testing.T) {
244271
},
245272
Spec: operatorv1.CoreProviderSpec{
246273
ProviderSpec: operatorv1.ProviderSpec{
247-
Version: testCurrentVersion,
274+
Version: currentVersion,
248275
},
249276
},
250277
}
@@ -254,7 +281,7 @@ func TestUpgradeDowngradeProvider(t *testing.T) {
254281
t.Log("Ensure namespace exists", namespace)
255282
g.Expect(env.EnsureNamespaceExists(ctx, namespace)).To(Succeed())
256283

257-
g.Expect(env.CreateAndWait(ctx, dummyConfigMap(namespace, testCurrentVersion))).To(Succeed())
284+
g.Expect(env.CreateAndWait(ctx, dummyFutureConfigMap(namespace, currentVersion))).To(Succeed())
258285

259286
insertDummyConfig(provider)
260287
provider.SetNamespace(namespace)
@@ -266,7 +293,7 @@ func TestUpgradeDowngradeProvider(t *testing.T) {
266293
return false
267294
}
268295

269-
if provider.GetStatus().InstalledVersion == nil || *provider.GetStatus().InstalledVersion != testCurrentVersion {
296+
if provider.GetStatus().InstalledVersion == nil || *provider.GetStatus().InstalledVersion != currentVersion {
270297
return false
271298
}
272299

@@ -283,13 +310,16 @@ func TestUpgradeDowngradeProvider(t *testing.T) {
283310
}, timeout).Should(BeEquivalentTo(true))
284311

285312
// creating another configmap with another version
286-
if tc.newVersion != testCurrentVersion {
287-
g.Expect(env.CreateAndWait(ctx, dummyConfigMap(namespace, tc.newVersion))).To(Succeed())
313+
if tc.newVersion != currentVersion {
314+
g.Expect(env.CreateAndWait(ctx, dummyFutureConfigMap(namespace, tc.newVersion))).To(Succeed())
288315
}
289316

290317
// Change provider version
291318
providerSpec := provider.GetSpec()
292319
providerSpec.Version = tc.newVersion
320+
providerSpec.Deployment = &operatorv1.DeploymentSpec{
321+
Replicas: pointer.Int(2),
322+
}
293323
provider.SetSpec(providerSpec)
294324

295325
// Set label (needed to start a reconciliation of the provider)
@@ -315,23 +345,68 @@ func TestUpgradeDowngradeProvider(t *testing.T) {
315345
return false
316346
}
317347

348+
allFound := false
318349
for _, cond := range provider.GetStatus().Conditions {
319350
if cond.Type == operatorv1.PreflightCheckCondition {
320351
t.Log(t.Name(), provider.GetName(), cond)
321352
if cond.Status == corev1.ConditionTrue {
322-
return true
353+
allFound = true
354+
break
323355
}
324356
}
325357
}
326358

327-
return false
359+
if !allFound {
360+
return false
361+
}
362+
363+
allFound = tc.newVersion == currentVersion
364+
for _, cond := range provider.GetStatus().Conditions {
365+
if cond.Type == operatorv1.ProviderUpgradedCondition {
366+
t.Log(t.Name(), provider.GetName(), cond)
367+
if cond.Status == corev1.ConditionTrue {
368+
allFound = tc.newVersion != currentVersion
369+
break
370+
}
371+
}
372+
}
373+
374+
if !allFound {
375+
return false
376+
}
377+
378+
// Ensure customization occurred
379+
dep := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
380+
Namespace: provider.Namespace,
381+
Name: "capd-controller-manager",
382+
}}
383+
if err := env.Get(ctx, client.ObjectKeyFromObject(dep), dep); err != nil {
384+
return false
385+
}
386+
387+
return dep.Spec.Replicas != nil && *dep.Spec.Replicas == 2
328388
}, timeout).Should(BeEquivalentTo(true))
329389

390+
g.Consistently(func() bool {
391+
allSet := tc.newVersion == currentVersion
392+
for _, cond := range provider.GetStatus().Conditions {
393+
if cond.Type == operatorv1.ProviderUpgradedCondition {
394+
t.Log(t.Name(), provider.GetName(), cond)
395+
if cond.Status == corev1.ConditionTrue {
396+
allSet = tc.newVersion != currentVersion
397+
break
398+
}
399+
}
400+
}
401+
402+
return allSet
403+
}, 2*time.Second).Should(BeTrue())
404+
330405
// Clean up
331406
objs := []client.Object{provider}
332407
objs = append(objs, &corev1.ConfigMap{
333408
ObjectMeta: metav1.ObjectMeta{
334-
Name: testCurrentVersion,
409+
Name: currentVersion,
335410
Namespace: namespace,
336411
},
337412
})

internal/controller/manifests_downloader.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu
6868

6969
exists, err := p.checkConfigMapExists(ctx, labelSelector)
7070
if err != nil {
71-
return reconcile.Result{}, wrapPhaseError(err, "failed to check that config map with manifests exists")
71+
return reconcile.Result{}, wrapPhaseError(err, "failed to check that config map with manifests exists", operatorv1.ProviderInstalledCondition)
7272
}
7373

7474
if exists {
@@ -83,7 +83,7 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu
8383
if err != nil {
8484
err = fmt.Errorf("failed to create repo from provider url for provider %q: %w", p.provider.GetName(), err)
8585

86-
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
86+
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
8787
}
8888

8989
spec := p.provider.GetSpec()
@@ -101,22 +101,22 @@ func (p *phaseReconciler) downloadManifests(ctx context.Context) (reconcile.Resu
101101
if err != nil {
102102
err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", metadataFile, p.provider.GetName(), err)
103103

104-
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
104+
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
105105
}
106106

107107
componentsFile, err := repo.GetFile(ctx, spec.Version, repo.ComponentsPath())
108108
if err != nil {
109109
err = fmt.Errorf("failed to read %q from the repository for provider %q: %w", componentsFile, p.provider.GetName(), err)
110110

111-
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
111+
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
112112
}
113113

114114
withCompression := needToCompress(metadataFile, componentsFile)
115115

116116
if err := p.createManifestsConfigMap(ctx, metadataFile, componentsFile, withCompression); err != nil {
117117
err = fmt.Errorf("failed to create config map for provider %q: %w", p.provider.GetName(), err)
118118

119-
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason)
119+
return reconcile.Result{}, wrapPhaseError(err, operatorv1.ComponentsFetchErrorReason, operatorv1.ProviderInstalledCondition)
120120
}
121121

122122
return reconcile.Result{}, nil

0 commit comments

Comments
 (0)