Skip to content

Commit 1c66738

Browse files
authored
✨ Call GenerateUpgradePlanRequest Runtime Extension (#12903)
* call GenerateUpgradePlanRequest Runtime Extension Signed-off-by: sivchari <shibuuuu5@gmail.com> * add unit test for UpgradePlan extension Signed-off-by: sivchari <shibuuuu5@gmail.com> * fix lint error Signed-off-by: sivchari <shibuuuu5@gmail.com> * fix review findings Signed-off-by: sivchari <shibuuuu5@gmail.com> --------- Signed-off-by: sivchari <shibuuuu5@gmail.com>
1 parent 45159ff commit 1c66738

File tree

4 files changed

+182
-9
lines changed

4 files changed

+182
-9
lines changed

exp/topology/desiredstate/desired_state.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,14 @@ func (g *generator) Generate(ctx context.Context, s *scope.Scope) (*scope.Cluste
113113
}
114114

115115
// Compute the upgradePlan.
116-
// By default CAPI allows to upgrade only by one minor, but if the cluster class defines a list of Kubernetes versions,
117-
// the upgrade plan will be inferred from those versions.
116+
// By default CAPI allows to upgrade only by one minor, but if the cluster class defines an upgrade plan extension,
117+
// the upgrade plan will be computed by calling the extension. Otherwise, if the cluster class defines a list of
118+
// Kubernetes versions, the upgrade plan will be inferred from those versions.
119+
// Runtime extension takes precedence if defined.
118120
getUpgradePlan := GetUpgradePlanOneMinor
119-
if len(s.Blueprint.ClusterClass.Spec.KubernetesVersions) > 0 {
121+
if s.Blueprint.ClusterClass.Spec.Upgrade.External.GenerateUpgradePlanExtension != "" {
122+
getUpgradePlan = GetUpgradePlanFromExtension(g.RuntimeClient, s.Current.Cluster, s.Blueprint.ClusterClass.Spec.Upgrade.External.GenerateUpgradePlanExtension)
123+
} else if len(s.Blueprint.ClusterClass.Spec.KubernetesVersions) > 0 {
120124
getUpgradePlan = GetUpgradePlanFromClusterClassVersions(s.Blueprint.ClusterClass.Spec.KubernetesVersions)
121125
}
122126
if err := ComputeUpgradePlan(ctx, s, getUpgradePlan); err != nil {
@@ -1631,7 +1635,7 @@ func getOwnerReferenceFrom(obj, owner client.Object) *metav1.OwnerReference {
16311635
return nil
16321636
}
16331637

1634-
func cleanupCluster(cluster *clusterv1beta1.Cluster) *clusterv1beta1.Cluster {
1638+
func cleanupV1Beta1Cluster(cluster *clusterv1beta1.Cluster) *clusterv1beta1.Cluster {
16351639
// Optimize size of Cluster by not sending status, the managedFields and some specific annotations.
16361640
cluster.SetManagedFields(nil)
16371641

@@ -1646,3 +1650,19 @@ func cleanupCluster(cluster *clusterv1beta1.Cluster) *clusterv1beta1.Cluster {
16461650
cluster.Status = clusterv1beta1.ClusterStatus{}
16471651
return cluster
16481652
}
1653+
1654+
func cleanupCluster(cluster *clusterv1.Cluster) *clusterv1.Cluster {
1655+
// Optimize size of Cluster by not sending status, the managedFields and some specific annotations.
1656+
cluster.SetManagedFields(nil)
1657+
1658+
// The conversion that we run before calling cleanupCluster does not clone annotations
1659+
// So we have to do it here to not modify the original Cluster.
1660+
if cluster.Annotations != nil {
1661+
annotations := maps.Clone(cluster.Annotations)
1662+
delete(annotations, corev1.LastAppliedConfigAnnotation)
1663+
delete(annotations, conversion.DataAnnotation)
1664+
cluster.Annotations = annotations
1665+
}
1666+
cluster.Status = clusterv1.ClusterStatus{}
1667+
return cluster
1668+
}

exp/topology/desiredstate/lifecycle_hooks.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (g *generator) callBeforeClusterUpgradeHook(ctx context.Context, s *scope.S
7979
}
8080

8181
hookRequest := &runtimehooksv1.BeforeClusterUpgradeRequest{
82-
Cluster: *cleanupCluster(v1beta1Cluster),
82+
Cluster: *cleanupV1Beta1Cluster(v1beta1Cluster),
8383
FromKubernetesVersion: *currentVersion,
8484
ToKubernetesVersion: topologyVersion,
8585
ControlPlaneUpgrades: toUpgradeStep(s.UpgradeTracker.ControlPlane.UpgradePlan),
@@ -119,7 +119,7 @@ func (g *generator) callBeforeControlPlaneUpgradeHook(ctx context.Context, s *sc
119119
}
120120

121121
hookRequest := &runtimehooksv1.BeforeControlPlaneUpgradeRequest{
122-
Cluster: *cleanupCluster(v1beta1Cluster),
122+
Cluster: *cleanupV1Beta1Cluster(v1beta1Cluster),
123123
FromKubernetesVersion: *currentVersion,
124124
ToKubernetesVersion: nextVersion,
125125
ControlPlaneUpgrades: toUpgradeStep(s.UpgradeTracker.ControlPlane.UpgradePlan),
@@ -161,7 +161,7 @@ func (g *generator) callAfterControlPlaneUpgradeHook(ctx context.Context, s *sco
161161

162162
// Call all the registered extension for the hook.
163163
hookRequest := &runtimehooksv1.AfterControlPlaneUpgradeRequest{
164-
Cluster: *cleanupCluster(v1beta1Cluster),
164+
Cluster: *cleanupV1Beta1Cluster(v1beta1Cluster),
165165
KubernetesVersion: *currentVersion,
166166
ControlPlaneUpgrades: toUpgradeStep(s.UpgradeTracker.ControlPlane.UpgradePlan),
167167
WorkersUpgrades: toUpgradeStep(s.UpgradeTracker.MachineDeployments.UpgradePlan, s.UpgradeTracker.MachinePools.UpgradePlan),
@@ -204,7 +204,7 @@ func (g *generator) callBeforeWorkersUpgradeHook(ctx context.Context, s *scope.S
204204
}
205205

206206
hookRequest := &runtimehooksv1.BeforeWorkersUpgradeRequest{
207-
Cluster: *cleanupCluster(v1beta1Cluster),
207+
Cluster: *cleanupV1Beta1Cluster(v1beta1Cluster),
208208
FromKubernetesVersion: *currentVersion,
209209
ToKubernetesVersion: nextVersion,
210210
ControlPlaneUpgrades: toUpgradeStep(s.UpgradeTracker.ControlPlane.UpgradePlan),
@@ -251,7 +251,7 @@ func (g *generator) callAfterWorkersUpgradeHook(ctx context.Context, s *scope.Sc
251251

252252
// Call all the registered extension for the hook.
253253
hookRequest := &runtimehooksv1.AfterWorkersUpgradeRequest{
254-
Cluster: *cleanupCluster(v1beta1Cluster),
254+
Cluster: *cleanupV1Beta1Cluster(v1beta1Cluster),
255255
KubernetesVersion: *currentVersion,
256256
ControlPlaneUpgrades: toUpgradeStep(s.UpgradeTracker.ControlPlane.UpgradePlan),
257257
WorkersUpgrades: toUpgradeStep(s.UpgradeTracker.MachineDeployments.UpgradePlan, s.UpgradeTracker.MachinePools.UpgradePlan),

exp/topology/desiredstate/upgrade_plan.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ import (
2525
"github.com/pkg/errors"
2626
"k8s.io/apimachinery/pkg/util/sets"
2727

28+
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
29+
runtimehooksv1 "sigs.k8s.io/cluster-api/api/runtime/hooks/v1alpha1"
30+
runtimeclient "sigs.k8s.io/cluster-api/exp/runtime/client"
2831
"sigs.k8s.io/cluster-api/exp/topology/scope"
32+
"sigs.k8s.io/cluster-api/feature"
2933
"sigs.k8s.io/cluster-api/internal/contract"
3034
"sigs.k8s.io/cluster-api/util/version"
3135
)
@@ -405,3 +409,39 @@ func GetUpgradePlanFromClusterClassVersions(clusterClassVersions []string) func(
405409
return simplifiedUpgradePlan, nil, nil
406410
}
407411
}
412+
413+
// GetUpgradePlanFromExtension returns an upgrade plan by calling the GenerateUpgradePlan runtime extension.
414+
func GetUpgradePlanFromExtension(runtimeClient runtimeclient.Client, cluster *clusterv1.Cluster, extensionName string) func(ctx context.Context, desiredVersion, currentControlPlaneVersion, currentMinWorkersVersion string) ([]string, []string, error) {
415+
return func(ctx context.Context, desiredVersion, currentControlPlaneVersion, currentMinWorkersVersion string) ([]string, []string, error) {
416+
if !feature.Gates.Enabled(feature.RuntimeSDK) {
417+
return nil, nil, errors.Errorf("can not use GenerateUpgradePlan extension %q if RuntimeSDK feature flag is disabled", extensionName)
418+
}
419+
420+
// Prepare the request.
421+
req := &runtimehooksv1.GenerateUpgradePlanRequest{
422+
Cluster: *cleanupCluster(cluster.DeepCopy()),
423+
FromControlPlaneKubernetesVersion: currentControlPlaneVersion,
424+
FromWorkersKubernetesVersion: currentMinWorkersVersion,
425+
ToKubernetesVersion: desiredVersion,
426+
}
427+
428+
// Call the extension.
429+
resp := &runtimehooksv1.GenerateUpgradePlanResponse{}
430+
if err := runtimeClient.CallExtension(ctx, runtimehooksv1.GenerateUpgradePlan, cluster, extensionName, req, resp); err != nil {
431+
return nil, nil, errors.Wrap(err, "failed to get upgrade plan from extension")
432+
}
433+
434+
// Convert UpgradeStep to string slice.
435+
controlPlaneUpgradePlan := make([]string, len(resp.ControlPlaneUpgrades))
436+
for i, step := range resp.ControlPlaneUpgrades {
437+
controlPlaneUpgradePlan[i] = step.Version
438+
}
439+
440+
workersUpgradePlan := make([]string, len(resp.WorkersUpgrades))
441+
for i, step := range resp.WorkersUpgrades {
442+
workersUpgradePlan[i] = step.Version
443+
}
444+
445+
return controlPlaneUpgradePlan, workersUpgradePlan, nil
446+
}
447+
}

exp/topology/desiredstate/upgrade_plan_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ import (
2222

2323
"github.com/blang/semver/v4"
2424
. "github.com/onsi/gomega"
25+
"github.com/pkg/errors"
26+
utilfeature "k8s.io/component-base/featuregate/testing"
2527

2628
clusterv1 "sigs.k8s.io/cluster-api/api/core/v1beta2"
29+
runtimehooksv1 "sigs.k8s.io/cluster-api/api/runtime/hooks/v1alpha1"
30+
runtimecatalog "sigs.k8s.io/cluster-api/exp/runtime/catalog"
2731
"sigs.k8s.io/cluster-api/exp/topology/scope"
32+
"sigs.k8s.io/cluster-api/feature"
33+
fakeruntimeclient "sigs.k8s.io/cluster-api/internal/runtime/client/fake"
2834
"sigs.k8s.io/cluster-api/util/test/builder"
2935
)
3036

@@ -1131,3 +1137,110 @@ func TestGetUpgradePlanFromClusterClassVersions(t *testing.T) {
11311137
})
11321138
}
11331139
}
1140+
1141+
func TestGetUpgradePlanFromExtension(t *testing.T) {
1142+
g := NewWithT(t)
1143+
ctx := t.Context()
1144+
1145+
// Enable RuntimeSDK feature flag
1146+
utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)
1147+
1148+
// Create a test cluster
1149+
cluster := builder.Cluster("test-namespace", "test-cluster").Build()
1150+
1151+
// Create catalog and register runtime hooks
1152+
catalog := runtimecatalog.New()
1153+
_ = runtimehooksv1.AddToCatalog(catalog)
1154+
1155+
// Create fake runtime client
1156+
extensionResponse := &runtimehooksv1.GenerateUpgradePlanResponse{
1157+
CommonResponse: runtimehooksv1.CommonResponse{
1158+
Status: runtimehooksv1.ResponseStatusSuccess,
1159+
},
1160+
ControlPlaneUpgrades: []runtimehooksv1.UpgradeStep{
1161+
{Version: "v1.32.0"},
1162+
{Version: "v1.33.0"},
1163+
},
1164+
WorkersUpgrades: []runtimehooksv1.UpgradeStep{
1165+
{Version: "v1.33.0"},
1166+
},
1167+
}
1168+
fakeRuntimeClient := fakeruntimeclient.NewRuntimeClientBuilder().
1169+
WithCatalog(catalog).
1170+
WithCallExtensionResponses(map[string]runtimehooksv1.ResponseObject{
1171+
"test-extension": extensionResponse,
1172+
}).
1173+
Build()
1174+
1175+
// Call GetUpgradePlanFromExtension
1176+
f := GetUpgradePlanFromExtension(fakeRuntimeClient, cluster, "test-extension")
1177+
controlPlaneUpgradePlan, workersUpgradePlan, err := f(ctx, "v1.33.0", "v1.31.0", "v1.31.0")
1178+
1179+
g.Expect(err).ToNot(HaveOccurred())
1180+
g.Expect(controlPlaneUpgradePlan).To(Equal([]string{"v1.32.0", "v1.33.0"}))
1181+
g.Expect(workersUpgradePlan).To(Equal([]string{"v1.33.0"}))
1182+
}
1183+
1184+
func TestGetUpgradePlanFromExtension_Errors(t *testing.T) {
1185+
tests := []struct {
1186+
name string
1187+
enableRuntimeSDK bool
1188+
extensionError error
1189+
desiredVersion string
1190+
currentControlPlaneVersion string
1191+
currentMinWorkersVersion string
1192+
wantErrMessage string
1193+
}{
1194+
{
1195+
name: "fails when RuntimeSDK feature flag is disabled",
1196+
enableRuntimeSDK: false,
1197+
desiredVersion: "v1.33.0",
1198+
currentControlPlaneVersion: "v1.31.0",
1199+
currentMinWorkersVersion: "v1.31.0",
1200+
wantErrMessage: "can not use GenerateUpgradePlan extension \"test-extension\" if RuntimeSDK feature flag is disabled",
1201+
},
1202+
{
1203+
name: "fails when extension call returns error",
1204+
enableRuntimeSDK: true,
1205+
desiredVersion: "v1.33.0",
1206+
currentControlPlaneVersion: "v1.31.0",
1207+
currentMinWorkersVersion: "v1.31.0",
1208+
extensionError: errors.New("extension call error"),
1209+
wantErrMessage: "failed to get upgrade plan from extension: extension call error",
1210+
},
1211+
}
1212+
1213+
for _, tt := range tests {
1214+
t.Run(tt.name, func(t *testing.T) {
1215+
g := NewWithT(t)
1216+
ctx := t.Context()
1217+
1218+
// Enable/disable RuntimeSDK feature flag
1219+
utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, tt.enableRuntimeSDK)
1220+
1221+
// Create a test cluster
1222+
cluster := builder.Cluster("test-namespace", "test-cluster").Build()
1223+
1224+
// Create catalog and register runtime hooks
1225+
catalog := runtimecatalog.New()
1226+
_ = runtimehooksv1.AddToCatalog(catalog)
1227+
1228+
// Create fake runtime client
1229+
fakeRuntimeClientBuilder := fakeruntimeclient.NewRuntimeClientBuilder().
1230+
WithCatalog(catalog)
1231+
if tt.extensionError != nil {
1232+
fakeRuntimeClientBuilder.WithCallExtensionValidations(func(_ string, _ runtimehooksv1.RequestObject) error {
1233+
return tt.extensionError
1234+
})
1235+
}
1236+
fakeRuntimeClient := fakeRuntimeClientBuilder.Build()
1237+
1238+
// Call GetUpgradePlanFromExtension
1239+
f := GetUpgradePlanFromExtension(fakeRuntimeClient, cluster, "test-extension")
1240+
_, _, err := f(ctx, tt.desiredVersion, tt.currentControlPlaneVersion, tt.currentMinWorkersVersion)
1241+
1242+
g.Expect(err).To(HaveOccurred())
1243+
g.Expect(err.Error()).To(Equal(tt.wantErrMessage))
1244+
})
1245+
}
1246+
}

0 commit comments

Comments
 (0)