Skip to content

Commit 1ff9019

Browse files
🌱 Enforce skip upgrade policy in clusterctl (#12017)
* Enforce skip upgrade policy in clusterctl * Address feedback
1 parent cdf790e commit 1ff9019

File tree

3 files changed

+151
-11
lines changed

3 files changed

+151
-11
lines changed

cmd/clusterctl/client/cluster/upgrader.go

+35
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"sort"
2222
"time"
2323

24+
"github.com/blang/semver/v4"
2425
"github.com/pkg/errors"
2526
appsv1 "k8s.io/api/apps/v1"
2627
"k8s.io/apimachinery/pkg/util/sets"
@@ -272,6 +273,10 @@ func (u *providerUpgrader) createCustomPlan(ctx context.Context, upgradeItems []
272273
return nil, errors.Errorf("unable to complete that upgrade: the provider %s in not part of the management cluster", upgradeItem.InstanceName())
273274
}
274275

276+
if upgradeItem.Version == "" {
277+
upgradeItem.Version = provider.Version
278+
}
279+
275280
// Retrieves the contract that is supported by the target version of the provider.
276281
contract, err := u.getProviderContractByVersion(ctx, *provider, upgradeItem.NextVersion)
277282
if err != nil {
@@ -358,6 +363,36 @@ func (u *providerUpgrader) doUpgrade(ctx context.Context, upgradePlan *UpgradePl
358363
}
359364
}
360365

366+
// Block unsupported skip upgrades for Core, Kubeadm Bootstrap, Kubeadm ControlPlane.
367+
// NOTE: in future we might consider extending the clusterctl contract to support enforcing of skip upgrade
368+
// rules for out of tree providers.
369+
minVersionSkew := semver.MustParse("1.10.0")
370+
for _, upgradeItem := range upgradePlan.Providers {
371+
if !(upgradeItem.Type == string(clusterctlv1.CoreProviderType) ||
372+
(upgradeItem.Type == string(clusterctlv1.BootstrapProviderType) && upgradeItem.ProviderName == config.KubeadmBootstrapProviderName) ||
373+
(upgradeItem.Type == string(clusterctlv1.ControlPlaneProviderType) && upgradeItem.ProviderName == config.KubeadmControlPlaneProviderName)) {
374+
continue
375+
}
376+
377+
currentVersion, err := semver.ParseTolerant(upgradeItem.Version)
378+
if err != nil {
379+
return errors.Wrapf(err, "failed to parse current version for %s provider", upgradeItem.InstanceName())
380+
}
381+
382+
if currentVersion.LT(minVersionSkew) {
383+
continue
384+
}
385+
386+
nextVersion, err := semver.ParseTolerant(upgradeItem.NextVersion)
387+
if err != nil {
388+
return errors.Wrapf(err, "failed to parse next version for %s provider", upgradeItem.InstanceName())
389+
}
390+
391+
if nextVersion.Minor > currentVersion.Minor+3 {
392+
return errors.Errorf("upgrade for %s provider can't skip more than 3 versions", upgradeItem.InstanceName())
393+
}
394+
}
395+
361396
// Ensure Providers are updated in the following order: Core, Bootstrap, ControlPlane, Infrastructure.
362397
providers := upgradePlan.Providers
363398
sort.Slice(providers, func(a, b int) bool {

cmd/clusterctl/client/cluster/upgrader_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -1034,6 +1034,117 @@ func Test_providerUpgrader_ApplyCustomPlan(t *testing.T) {
10341034
errorMsg: "detected multiple instances of the same provider",
10351035
opts: UpgradeOptions{},
10361036
},
1037+
{
1038+
name: "fails to upgrade to current contract when violating core provider skip version rules",
1039+
fields: fields{
1040+
reader: test.NewFakeReader().
1041+
WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com"),
1042+
repository: map[string]repository.Repository{
1043+
"cluster-api": repository.NewMemoryRepository().
1044+
WithVersions("v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0").
1045+
WithMetadata("v1.14.0", &clusterctlv1.Metadata{
1046+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
1047+
{Major: 1, Minor: 10, Contract: test.PreviousCAPIContractNotSupported},
1048+
{Major: 1, Minor: 11, Contract: test.CurrentCAPIContract},
1049+
{Major: 1, Minor: 12, Contract: test.CurrentCAPIContract},
1050+
{Major: 1, Minor: 13, Contract: test.CurrentCAPIContract},
1051+
{Major: 1, Minor: 14, Contract: test.CurrentCAPIContract},
1052+
},
1053+
}),
1054+
},
1055+
proxy: test.NewFakeProxy().
1056+
WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.10.0", "cluster-api-system"),
1057+
},
1058+
providersToUpgrade: []UpgradeItem{
1059+
{
1060+
Provider: fakeProvider("cluster-api", clusterctlv1.CoreProviderType, "v1.10.0", "cluster-api-system"),
1061+
NextVersion: "v1.14.0",
1062+
},
1063+
},
1064+
wantErr: true,
1065+
errorMsg: "upgrade for cluster-api-system/cluster-api provider can't skip more than 3 versions",
1066+
opts: UpgradeOptions{},
1067+
},
1068+
{
1069+
name: "fails to upgrade to current contract when violating kubeadm bootstrap provider skip version rules",
1070+
fields: fields{
1071+
reader: test.NewFakeReader().
1072+
WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
1073+
WithProvider(config.KubeadmBootstrapProviderName, clusterctlv1.BootstrapProviderType, "https://somewhere.com"),
1074+
repository: map[string]repository.Repository{
1075+
"cluster-api": repository.NewMemoryRepository().
1076+
WithVersions("v1.0.0").
1077+
WithMetadata("v1.0.0", &clusterctlv1.Metadata{
1078+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
1079+
{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
1080+
},
1081+
}),
1082+
"bootstrap-kubeadm": repository.NewMemoryRepository().
1083+
WithVersions("v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0").
1084+
WithMetadata("v1.14.0", &clusterctlv1.Metadata{
1085+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
1086+
{Major: 1, Minor: 10, Contract: test.PreviousCAPIContractNotSupported},
1087+
{Major: 1, Minor: 11, Contract: test.CurrentCAPIContract},
1088+
{Major: 1, Minor: 12, Contract: test.CurrentCAPIContract},
1089+
{Major: 1, Minor: 13, Contract: test.CurrentCAPIContract},
1090+
{Major: 1, Minor: 14, Contract: test.CurrentCAPIContract},
1091+
},
1092+
}),
1093+
},
1094+
proxy: test.NewFakeProxy().
1095+
WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
1096+
WithProviderInventory(config.KubeadmBootstrapProviderName, clusterctlv1.BootstrapProviderType, "v1.10.0", "cluster-api-system"),
1097+
},
1098+
providersToUpgrade: []UpgradeItem{
1099+
{
1100+
Provider: fakeProvider(config.KubeadmBootstrapProviderName, clusterctlv1.BootstrapProviderType, "v1.10.0", "cluster-api-system"),
1101+
NextVersion: "v1.14.0",
1102+
},
1103+
},
1104+
wantErr: true,
1105+
errorMsg: "upgrade for cluster-api-system/bootstrap-kubeadm provider can't skip more than 3 versions",
1106+
opts: UpgradeOptions{},
1107+
},
1108+
{
1109+
name: "fails to upgrade to current contract when violating kubeadm control plane provider skip version rules",
1110+
fields: fields{
1111+
reader: test.NewFakeReader().
1112+
WithProvider("cluster-api", clusterctlv1.CoreProviderType, "https://somewhere.com").
1113+
WithProvider(config.KubeadmControlPlaneProviderName, clusterctlv1.ControlPlaneProviderType, "https://somewhere.com"),
1114+
repository: map[string]repository.Repository{
1115+
"cluster-api": repository.NewMemoryRepository().
1116+
WithVersions("v1.0.0").
1117+
WithMetadata("v1.0.0", &clusterctlv1.Metadata{
1118+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
1119+
{Major: 1, Minor: 0, Contract: test.CurrentCAPIContract},
1120+
},
1121+
}),
1122+
"control-plane-kubeadm": repository.NewMemoryRepository().
1123+
WithVersions("v1.10.0", "v1.11.0", "v1.12.0", "v1.13.0", "v1.14.0").
1124+
WithMetadata("v1.14.0", &clusterctlv1.Metadata{
1125+
ReleaseSeries: []clusterctlv1.ReleaseSeries{
1126+
{Major: 1, Minor: 10, Contract: test.PreviousCAPIContractNotSupported},
1127+
{Major: 1, Minor: 11, Contract: test.CurrentCAPIContract},
1128+
{Major: 1, Minor: 12, Contract: test.CurrentCAPIContract},
1129+
{Major: 1, Minor: 13, Contract: test.CurrentCAPIContract},
1130+
{Major: 1, Minor: 14, Contract: test.CurrentCAPIContract},
1131+
},
1132+
}),
1133+
},
1134+
proxy: test.NewFakeProxy().
1135+
WithProviderInventory("cluster-api", clusterctlv1.CoreProviderType, "v1.0.0", "cluster-api-system").
1136+
WithProviderInventory(config.KubeadmControlPlaneProviderName, clusterctlv1.ControlPlaneProviderType, "v1.10.0", "cluster-api-system"),
1137+
},
1138+
providersToUpgrade: []UpgradeItem{
1139+
{
1140+
Provider: fakeProvider(config.KubeadmControlPlaneProviderName, clusterctlv1.ControlPlaneProviderType, "v1.10.0", "cluster-api-system"),
1141+
NextVersion: "v1.14.0",
1142+
},
1143+
},
1144+
wantErr: true,
1145+
errorMsg: "upgrade for cluster-api-system/control-plane-kubeadm provider can't skip more than 3 versions",
1146+
opts: UpgradeOptions{},
1147+
},
10371148
}
10381149

10391150
for _, tt := range tests {

docs/book/src/clusterctl/commands/upgrade.md

+5-11
Original file line numberDiff line numberDiff line change
@@ -77,19 +77,13 @@ clusterctl upgrade apply \
7777

7878
<aside class="note warning">
7979

80-
<h1>Clusterctl upgrade test coverage</h1>
80+
<h1>Skip upgrades</h1>
8181

82-
Cluster API only tests a subset of possible clusterctl upgrade paths as otherwise the test matrix would be overwhelming.
83-
Untested upgrade paths are not blocked by clusterctl and should work in general, but users
84-
intending to perform an upgrade path not tested by us should do their own validation to ensure the operation works correctly.
82+
Please check providers documentation before performing skip upgrades (skip minor versions).
83+
Not supported skip upgrades might lead to non functional management clusters.
8584

86-
The following is an example of the tested upgrade paths for v1.10:
87-
88-
| From | To | Note |
89-
|------|-------|-------------------------------|
90-
| v1.7 | v1.10 | n-3 --> n (v1.7 is v1.10 - 3) |
91-
| v1.8 | v1.10 | n-2 --> n (v1.8 is v1.10 - 2) |
92-
| v1.9 | v1.10 | n-1 --> n (v1.9 is v1.10 - 1) |
85+
For Core provider, Kubeadm bootstrap provider, Kubeadm control plane provider and Docker infrastructure provider
86+
please look at [skip upgrades](../../reference/versions.md#skip-upgrades) rules.
9387

9488
</aside>
9589

0 commit comments

Comments
 (0)