Skip to content

Commit ea14c4d

Browse files
🌱 Fix lifecycle hooks conversions (#12507)
* Fix lifecycle hooks conversions * Address feedback * More comments
1 parent 9acc8fe commit ea14c4d

File tree

6 files changed

+211
-73
lines changed

6 files changed

+211
-73
lines changed

exp/topology/desiredstate/desired_state.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ func (g *generator) computeControlPlaneVersion(ctx context.Context, s *scope.Sco
533533
// hook because we didn't go through an upgrade or we already called the hook after the upgrade.
534534
if hooks.IsPending(runtimehooksv1.AfterControlPlaneUpgrade, s.Current.Cluster) {
535535
v1beta1Cluster := &clusterv1beta1.Cluster{}
536-
if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(s.Current.Cluster, v1beta1Cluster, nil); err != nil {
536+
if err := v1beta1Cluster.ConvertFrom(s.Current.Cluster); err != nil {
537537
return "", errors.Wrap(err, "error converting Cluster to v1beta1 Cluster")
538538
}
539539

@@ -607,7 +607,7 @@ func (g *generator) computeControlPlaneVersion(ctx context.Context, s *scope.Sco
607607
// At this point the control plane and the machine deployments are stable and we are almost ready to pick
608608
// up the desiredVersion. Call the BeforeClusterUpgrade hook before picking up the desired version.
609609
v1beta1Cluster := &clusterv1beta1.Cluster{}
610-
if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(s.Current.Cluster, v1beta1Cluster, nil); err != nil {
610+
if err := v1beta1Cluster.ConvertFrom(s.Current.Cluster); err != nil {
611611
return "", errors.Wrap(err, "error converting Cluster to v1beta1 Cluster")
612612
}
613613

exp/topology/desiredstate/desired_state_test.go

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@ package desiredstate
1818

1919
import (
2020
"encoding/json"
21-
"errors"
2221
"fmt"
22+
"maps"
2323
"strings"
2424
"testing"
2525
"time"
2626

2727
"github.com/google/go-cmp/cmp"
2828
. "github.com/onsi/gomega"
29+
"github.com/pkg/errors"
2930
corev1 "k8s.io/api/core/v1"
3031
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
32+
apiequality "k8s.io/apimachinery/pkg/api/equality"
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3335
"k8s.io/apimachinery/pkg/runtime"
36+
"k8s.io/apimachinery/pkg/runtime/schema"
3437
"k8s.io/apimachinery/pkg/util/intstr"
3538
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
3639
utilfeature "k8s.io/component-base/featuregate/testing"
@@ -962,6 +965,27 @@ func TestComputeControlPlane(t *testing.T) {
962965
}
963966

964967
func TestComputeControlPlaneVersion(t *testing.T) {
968+
var testGVKs = []schema.GroupVersionKind{
969+
{
970+
Group: "refAPIGroup1",
971+
Kind: "refKind1",
972+
Version: "v1beta4",
973+
},
974+
}
975+
976+
apiVersionGetter := func(gk schema.GroupKind) (string, error) {
977+
for _, gvk := range testGVKs {
978+
if gvk.GroupKind() == gk {
979+
return schema.GroupVersion{
980+
Group: gk.Group,
981+
Version: gvk.Version,
982+
}.String(), nil
983+
}
984+
}
985+
return "", fmt.Errorf("unknown GroupVersionKind: %v", gk)
986+
}
987+
clusterv1beta1.SetAPIVersionGetter(apiVersionGetter)
988+
965989
t.Run("Compute control plane version under various circumstances", func(t *testing.T) {
966990
utilfeature.SetFeatureGateDuringTest(t, feature.Gates, feature.RuntimeSDK, true)
967991

@@ -1208,6 +1232,14 @@ func TestComputeControlPlaneVersion(t *testing.T) {
12081232
conversion.DataAnnotation: "should be cleaned up",
12091233
},
12101234
},
1235+
// Add some more fields to check that conversion implemented when calling RuntimeExtension are properly handled.
1236+
Spec: clusterv1.ClusterSpec{
1237+
InfrastructureRef: &clusterv1.ContractVersionedObjectReference{
1238+
APIGroup: "refAPIGroup1",
1239+
Kind: "refKind1",
1240+
Name: "refName1",
1241+
},
1242+
},
12111243
},
12121244
ControlPlane: &scope.ControlPlaneState{Object: tt.controlPlaneObj},
12131245
},
@@ -1229,7 +1261,7 @@ func TestComputeControlPlaneVersion(t *testing.T) {
12291261
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
12301262
beforeClusterUpgradeGVH: tt.hookResponse,
12311263
}).
1232-
WithCallAllExtensionValidations(validateCleanupCluster).
1264+
WithCallAllExtensionValidations(validateClusterParameter(s.Current.Cluster)).
12331265
Build()
12341266

12351267
fakeClient := fake.NewClientBuilder().WithScheme(fakeScheme).WithObjects(s.Current.Cluster).Build()
@@ -1549,7 +1581,7 @@ func TestComputeControlPlaneVersion(t *testing.T) {
15491581
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
15501582
afterControlPlaneUpgradeGVH: tt.hookResponse,
15511583
}).
1552-
WithCallAllExtensionValidations(validateCleanupCluster).
1584+
WithCallAllExtensionValidations(validateClusterParameter(tt.s.Current.Cluster)).
15531585
WithCatalog(catalog).
15541586
Build()
15551587

@@ -3553,25 +3585,53 @@ func TestCalculateRefDesiredAPIVersion(t *testing.T) {
35533585
}
35543586
}
35553587

3556-
func validateCleanupCluster(req runtimehooksv1.RequestObject) error {
3557-
var cluster clusterv1beta1.Cluster
3558-
switch req := req.(type) {
3559-
case *runtimehooksv1.BeforeClusterUpgradeRequest:
3560-
cluster = req.Cluster
3561-
case *runtimehooksv1.AfterControlPlaneUpgradeRequest:
3562-
cluster = req.Cluster
3563-
default:
3564-
return fmt.Errorf("unhandled request type %T", req)
3565-
}
3588+
func validateClusterParameter(originalCluster *clusterv1.Cluster) func(req runtimehooksv1.RequestObject) error {
3589+
// return a func that allows to check if expected transformations are applied to the Cluster parameter which is
3590+
// included in the payload for lifecycle hooks calls.
3591+
return func(req runtimehooksv1.RequestObject) error {
3592+
var cluster clusterv1beta1.Cluster
3593+
switch req := req.(type) {
3594+
case *runtimehooksv1.BeforeClusterUpgradeRequest:
3595+
cluster = req.Cluster
3596+
case *runtimehooksv1.AfterControlPlaneUpgradeRequest:
3597+
cluster = req.Cluster
3598+
default:
3599+
return fmt.Errorf("unhandled request type %T", req)
3600+
}
35663601

3567-
if cluster.GetManagedFields() != nil {
3568-
return errors.New("managedFields should have been cleaned up")
3569-
}
3570-
if _, ok := cluster.Annotations[corev1.LastAppliedConfigAnnotation]; ok {
3571-
return errors.New("last-applied-configuration annotation should have been cleaned up")
3572-
}
3573-
if _, ok := cluster.Annotations[conversion.DataAnnotation]; ok {
3574-
return errors.New("conversion annotation should have been cleaned up")
3602+
// check if managed fields and well know annotations have been removed from the Cluster parameter included in the payload lifecycle hooks calls.
3603+
if cluster.GetManagedFields() != nil {
3604+
return errors.New("managedFields should have been cleaned up")
3605+
}
3606+
if _, ok := cluster.Annotations[corev1.LastAppliedConfigAnnotation]; ok {
3607+
return errors.New("last-applied-configuration annotation should have been cleaned up")
3608+
}
3609+
if _, ok := cluster.Annotations[conversion.DataAnnotation]; ok {
3610+
return errors.New("conversion annotation should have been cleaned up")
3611+
}
3612+
3613+
// check the Cluster parameter included in the payload lifecycle hooks calls has been properly converted from v1beta2 to v1beta1.
3614+
// Note: to perform this check we convert the parameter back to v1beta2 and compare with the original cluster +/- expected transformations.
3615+
v1beta2Cluster := &clusterv1.Cluster{}
3616+
if err := cluster.ConvertTo(v1beta2Cluster); err != nil {
3617+
return err
3618+
}
3619+
3620+
originalClusterCopy := originalCluster.DeepCopy()
3621+
originalClusterCopy.SetManagedFields(nil)
3622+
if originalClusterCopy.Annotations != nil {
3623+
annotations := maps.Clone(cluster.Annotations)
3624+
delete(annotations, corev1.LastAppliedConfigAnnotation)
3625+
delete(annotations, conversion.DataAnnotation)
3626+
originalClusterCopy.Annotations = annotations
3627+
}
3628+
3629+
// drop conditions, it is not possible to round trip without the data annotation.
3630+
originalClusterCopy.Status.Conditions = nil
3631+
3632+
if !apiequality.Semantic.DeepEqual(originalClusterCopy, v1beta2Cluster) {
3633+
return errors.Errorf("call to extension is not passing the expected cluster object: %s", cmp.Diff(originalClusterCopy, v1beta2Cluster))
3634+
}
3635+
return nil
35753636
}
3576-
return nil
35773637
}

internal/controllers/topology/cluster/cluster_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ func (r *Reconciler) callBeforeClusterCreateHook(ctx context.Context, s *scope.S
432432

433433
if s.Current.Cluster.Spec.InfrastructureRef == nil && s.Current.Cluster.Spec.ControlPlaneRef == nil {
434434
v1beta1Cluster := &clusterv1beta1.Cluster{}
435-
if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(s.Current.Cluster, v1beta1Cluster, nil); err != nil {
435+
if err := v1beta1Cluster.ConvertFrom(s.Current.Cluster); err != nil {
436436
return ctrl.Result{}, errors.Wrap(err, "error converting Cluster to v1beta1 Cluster")
437437
}
438438

@@ -525,7 +525,7 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, cluster *clusterv1.Clu
525525
if feature.Gates.Enabled(feature.RuntimeSDK) {
526526
if !hooks.IsOkToDelete(cluster) {
527527
v1beta1Cluster := &clusterv1beta1.Cluster{}
528-
if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(cluster, v1beta1Cluster, nil); err != nil {
528+
if err := v1beta1Cluster.ConvertFrom(cluster); err != nil {
529529
return ctrl.Result{}, errors.Wrap(err, "error converting Cluster to v1beta1 Cluster")
530530
}
531531

internal/controllers/topology/cluster/cluster_controller_test.go

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ limitations under the License.
1717
package cluster
1818

1919
import (
20-
"errors"
2120
"fmt"
21+
"maps"
2222
"testing"
2323
"time"
2424

25+
"github.com/google/go-cmp/cmp"
2526
. "github.com/onsi/gomega"
27+
"github.com/pkg/errors"
2628
corev1 "k8s.io/api/core/v1"
2729
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
30+
apiequality "k8s.io/apimachinery/pkg/api/equality"
2831
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2932
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3033
utilfeature "k8s.io/component-base/featuregate/testing"
@@ -582,7 +585,7 @@ func TestClusterReconciler_reconcileDelete(t *testing.T) {
582585
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
583586
beforeClusterDeleteGVH: tt.hookResponse,
584587
}).
585-
WithCallAllExtensionValidations(validateCleanupCluster).
588+
WithCallAllExtensionValidations(validateClusterParameter(tt.cluster)).
586589
WithCatalog(catalog).
587590
Build()
588591

@@ -727,18 +730,6 @@ func TestReconciler_callBeforeClusterCreateHook(t *testing.T) {
727730
for _, tt := range tests {
728731
t.Run(tt.name, func(t *testing.T) {
729732
g := NewWithT(t)
730-
731-
runtimeClient := fakeruntimeclient.NewRuntimeClientBuilder().
732-
WithCatalog(catalog).
733-
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
734-
gvh: tt.hookResponse,
735-
}).
736-
WithCallAllExtensionValidations(validateCleanupCluster).
737-
Build()
738-
739-
r := &Reconciler{
740-
RuntimeClient: runtimeClient,
741-
}
742733
s := &scope.Scope{
743734
Current: &scope.ClusterState{
744735
Cluster: &clusterv1.Cluster{
@@ -764,6 +755,18 @@ func TestReconciler_callBeforeClusterCreateHook(t *testing.T) {
764755
},
765756
HookResponseTracker: scope.NewHookResponseTracker(),
766757
}
758+
759+
runtimeClient := fakeruntimeclient.NewRuntimeClientBuilder().
760+
WithCatalog(catalog).
761+
WithCallAllExtensionResponses(map[runtimecatalog.GroupVersionHook]runtimehooksv1.ResponseObject{
762+
gvh: tt.hookResponse,
763+
}).
764+
WithCallAllExtensionValidations(validateClusterParameter(s.Current.Cluster)).
765+
Build()
766+
767+
r := &Reconciler{
768+
RuntimeClient: runtimeClient,
769+
}
767770
res, err := r.callBeforeClusterCreateHook(ctx, s)
768771
if tt.wantErr {
769772
g.Expect(err).To(HaveOccurred())
@@ -1716,29 +1719,57 @@ func TestClusterClassToCluster(t *testing.T) {
17161719
}
17171720
}
17181721

1719-
func validateCleanupCluster(req runtimehooksv1.RequestObject) error {
1720-
var cluster clusterv1beta1.Cluster
1721-
switch req := req.(type) {
1722-
case *runtimehooksv1.BeforeClusterCreateRequest:
1723-
cluster = req.Cluster
1724-
case *runtimehooksv1.AfterControlPlaneInitializedRequest:
1725-
cluster = req.Cluster
1726-
case *runtimehooksv1.AfterClusterUpgradeRequest:
1727-
cluster = req.Cluster
1728-
case *runtimehooksv1.BeforeClusterDeleteRequest:
1729-
cluster = req.Cluster
1730-
default:
1731-
return fmt.Errorf("unhandled request type %T", req)
1732-
}
1722+
func validateClusterParameter(originalCluster *clusterv1.Cluster) func(req runtimehooksv1.RequestObject) error {
1723+
// return a func that allows to check if expected transformations are applied to the Cluster parameter which is
1724+
// included in the payload for lifecycle hooks calls.
1725+
return func(req runtimehooksv1.RequestObject) error {
1726+
var cluster clusterv1beta1.Cluster
1727+
switch req := req.(type) {
1728+
case *runtimehooksv1.BeforeClusterCreateRequest:
1729+
cluster = req.Cluster
1730+
case *runtimehooksv1.AfterControlPlaneInitializedRequest:
1731+
cluster = req.Cluster
1732+
case *runtimehooksv1.AfterClusterUpgradeRequest:
1733+
cluster = req.Cluster
1734+
case *runtimehooksv1.BeforeClusterDeleteRequest:
1735+
cluster = req.Cluster
1736+
default:
1737+
return fmt.Errorf("unhandled request type %T", req)
1738+
}
17331739

1734-
if cluster.GetManagedFields() != nil {
1735-
return errors.New("managedFields should have been cleaned up")
1736-
}
1737-
if _, ok := cluster.Annotations[corev1.LastAppliedConfigAnnotation]; ok {
1738-
return errors.New("last-applied-configuration annotation should have been cleaned up")
1739-
}
1740-
if _, ok := cluster.Annotations[conversion.DataAnnotation]; ok {
1741-
return errors.New("conversion annotation should have been cleaned up")
1740+
// check if managed fields and well know annotations have been removed from the Cluster parameter included in the payload lifecycle hooks calls.
1741+
if cluster.GetManagedFields() != nil {
1742+
return errors.New("managedFields should have been cleaned up")
1743+
}
1744+
if _, ok := cluster.Annotations[corev1.LastAppliedConfigAnnotation]; ok {
1745+
return errors.New("last-applied-configuration annotation should have been cleaned up")
1746+
}
1747+
if _, ok := cluster.Annotations[conversion.DataAnnotation]; ok {
1748+
return errors.New("conversion annotation should have been cleaned up")
1749+
}
1750+
1751+
// check the Cluster parameter included in the payload lifecycle hooks calls has been properly converted from v1beta2 to v1beta1.
1752+
// Note: to perform this check we convert the parameter back to v1beta2 and compare with the original cluster +/- expected transformations.
1753+
v1beta2Cluster := &clusterv1.Cluster{}
1754+
if err := cluster.ConvertTo(v1beta2Cluster); err != nil {
1755+
return err
1756+
}
1757+
1758+
originalClusterCopy := originalCluster.DeepCopy()
1759+
originalClusterCopy.SetManagedFields(nil)
1760+
if originalClusterCopy.Annotations != nil {
1761+
annotations := maps.Clone(cluster.Annotations)
1762+
delete(annotations, corev1.LastAppliedConfigAnnotation)
1763+
delete(annotations, conversion.DataAnnotation)
1764+
originalClusterCopy.Annotations = annotations
1765+
}
1766+
1767+
// drop conditions, it is not possible to round trip without the data annotation.
1768+
originalClusterCopy.Status.Conditions = nil
1769+
1770+
if !apiequality.Semantic.DeepEqual(originalClusterCopy, v1beta2Cluster) {
1771+
return errors.Errorf("call to extension is not passing the expected cluster object: %s", cmp.Diff(originalClusterCopy, v1beta2Cluster))
1772+
}
1773+
return nil
17421774
}
1743-
return nil
17441775
}

internal/controllers/topology/cluster/reconcile_state.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ func (r *Reconciler) callAfterControlPlaneInitialized(ctx context.Context, s *sc
197197
if hooks.IsPending(runtimehooksv1.AfterControlPlaneInitialized, s.Current.Cluster) {
198198
if isControlPlaneInitialized(s.Current.Cluster) {
199199
v1beta1Cluster := &clusterv1beta1.Cluster{}
200-
if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(s.Current.Cluster, v1beta1Cluster, nil); err != nil {
200+
if err := v1beta1Cluster.ConvertFrom(s.Current.Cluster); err != nil {
201201
return errors.Wrap(err, "error converting Cluster to v1beta1 Cluster")
202202
}
203203

@@ -250,7 +250,7 @@ func (r *Reconciler) callAfterClusterUpgrade(ctx context.Context, s *scope.Scope
250250
!s.UpgradeTracker.MachinePools.IsAnyPendingUpgrade() && // No MachinePools are pending an upgrade
251251
!s.UpgradeTracker.MachinePools.DeferredUpgrade() { // No MachinePools have deferred an upgrade
252252
v1beta1Cluster := &clusterv1beta1.Cluster{}
253-
if err := clusterv1beta1.Convert_v1beta2_Cluster_To_v1beta1_Cluster(s.Current.Cluster, v1beta1Cluster, nil); err != nil {
253+
if err := v1beta1Cluster.ConvertFrom(s.Current.Cluster); err != nil {
254254
return errors.Wrap(err, "error converting Cluster to v1beta1 Cluster")
255255
}
256256

0 commit comments

Comments
 (0)