Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions api/v1beta1/awsidentity_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import (
// ConvertTo converts the v1beta1 AWSClusterControllerIdentity receiver to a v1beta2 AWSClusterControllerIdentity.
func (src *AWSClusterControllerIdentity) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*infrav1.AWSClusterControllerIdentity)
return Convert_v1beta1_AWSClusterControllerIdentity_To_v1beta2_AWSClusterControllerIdentity(src, dst, nil)
if err := Convert_v1beta1_AWSClusterControllerIdentity_To_v1beta2_AWSClusterControllerIdentity(src, dst, nil); err != nil {
return err
}
// Ensure the APIVersion is set correctly after conversion
dst.APIVersion = infrav1.GroupVersion.String()
return nil
}

// ConvertFrom converts the v1beta2 AWSClusterControllerIdentity to a v1beta1 AWSClusterControllerIdentity.
Expand All @@ -38,7 +43,12 @@ func (dst *AWSClusterControllerIdentity) ConvertFrom(srcRaw conversion.Hub) erro
// ConvertTo converts the v1beta1 AWSClusterControllerIdentityList receiver to a v1beta2 AWSClusterControllerIdentityList.
func (src *AWSClusterControllerIdentityList) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*infrav1.AWSClusterControllerIdentityList)
return Convert_v1beta1_AWSClusterControllerIdentityList_To_v1beta2_AWSClusterControllerIdentityList(src, dst, nil)
if err := Convert_v1beta1_AWSClusterControllerIdentityList_To_v1beta2_AWSClusterControllerIdentityList(src, dst, nil); err != nil {
return err
}
// Ensure the APIVersion is set correctly after conversion
dst.APIVersion = infrav1.GroupVersion.String()
return nil
}

// ConvertFrom converts the v1beta2 AWSClusterControllerIdentityList to a v1beta1 AWSClusterControllerIdentityList.
Expand Down
208 changes: 208 additions & 0 deletions test/e2e/suites/unmanaged/unmanaged_conversion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//go:build e2e
// +build e2e

/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package unmanaged

import (
"context"
"fmt"
"os/exec"
"strings"
"time"

"github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"

infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
"sigs.k8s.io/cluster-api-provider-aws/v2/test/e2e/shared"
)

var _ = ginkgo.Context("[unmanaged] [conversion]", func() {
var (
ctx context.Context
)

ginkgo.BeforeEach(func() {
ctx = context.TODO()
})

ginkgo.Describe("AWSClusterControllerIdentity conversion", func() {
ginkgo.It("should successfully convert v1beta1 to v1beta2", func() {
specName := "conversion-awsclustercontrolleridentity"
namespace := shared.SetupSpecNamespace(ctx, specName, e2eCtx)
defer shared.DumpSpecResourcesAndCleanup(ctx, specName, namespace, e2eCtx)

bootstrapClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()
kubeconfigPath := e2eCtx.Environment.BootstrapClusterProxy.GetKubeconfigPath()

ginkgo.By("Creating AWSClusterControllerIdentity in v1beta1 format using kubectl")
// Create the resource using kubectl with explicit v1beta1 APIVersion
// This simulates a resource created by an old provider version
v1beta1YAML := fmt.Sprintf(`apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: AWSClusterControllerIdentity
metadata:
name: %s
spec:
allowedNamespaces: {}
`, infrav1.AWSClusterControllerIdentityName)

// Delete existing if present
existing := &infrav1.AWSClusterControllerIdentity{}
err := bootstrapClient.Get(ctx, client.ObjectKey{Name: infrav1.AWSClusterControllerIdentityName}, existing)
if err == nil {
Expect(bootstrapClient.Delete(ctx, existing)).To(Succeed())
// Wait for deletion
Eventually(func() bool {
err := bootstrapClient.Get(ctx, client.ObjectKey{Name: infrav1.AWSClusterControllerIdentityName}, existing)
return apierrors.IsNotFound(err)
}, 30*time.Second, 1*time.Second).Should(BeTrue())
}

// Create using kubectl apply
cmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath, "apply", "-f", "-")

Check failure on line 80 in test/e2e/suites/unmanaged/unmanaged_conversion_test.go

View workflow job for this annotation

GitHub Actions / lint

G204: Subprocess launched with variable (gosec)
cmd.Stdin = strings.NewReader(v1beta1YAML)
output, err := cmd.CombinedOutput()
if err != nil && !strings.Contains(string(output), "already exists") {
Expect(err).NotTo(HaveOccurred(), "Failed to create v1beta1 AWSClusterControllerIdentity: %s", string(output))
}

ginkgo.By("Verifying resource exists via kubectl get")
// Use kubectl to get the resource and verify it can be retrieved
// This will trigger conversion if needed
getCmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,

Check failure on line 90 in test/e2e/suites/unmanaged/unmanaged_conversion_test.go

View workflow job for this annotation

GitHub Actions / lint

G204: Subprocess launched with variable (gosec)
"get", "awsclustercontrolleridentity", infrav1.AWSClusterControllerIdentityName,
"-o", "jsonpath={.apiVersion}")
output, err = getCmd.CombinedOutput()
Expect(err).NotTo(HaveOccurred(), "Failed to get AWSClusterControllerIdentity: %s", string(output))

apiVersion := strings.TrimSpace(string(output))
ginkgo.By(fmt.Sprintf("Resource APIVersion: %s", apiVersion))

// Verify the APIVersion is v1beta2 (converted from v1beta1)
Expect(apiVersion).To(Equal("infrastructure.cluster.x-k8s.io/v1beta2"),
"Expected APIVersion to be v1beta2 after conversion, got %s", apiVersion)

ginkgo.By("Verifying resource can be retrieved using v1beta2 client")
// Verify we can get it using the v1beta2 client
v1beta2Identity := &infrav1.AWSClusterControllerIdentity{}
err = bootstrapClient.Get(ctx, client.ObjectKey{
Name: infrav1.AWSClusterControllerIdentityName,
}, v1beta2Identity)
Expect(err).NotTo(HaveOccurred(), "Failed to get AWSClusterControllerIdentity using v1beta2 client")

// Verify the APIVersion is correct
Expect(v1beta2Identity.APIVersion).To(Equal(infrav1.GroupVersion.String()),
"Expected APIVersion to be %s, got %s", infrav1.GroupVersion.String(), v1beta2Identity.APIVersion)

ginkgo.By("Verifying conversion webhook works by listing resources")
// List all AWSClusterControllerIdentity resources to ensure conversion works for list operations
identityList := &infrav1.AWSClusterControllerIdentityList{}
err = bootstrapClient.List(ctx, identityList)
Expect(err).NotTo(HaveOccurred(), "Failed to list AWSClusterControllerIdentity resources")

// Verify at least one item exists and has correct APIVersion
Expect(len(identityList.Items)).To(BeNumerically(">=", 1),
"Expected at least one AWSClusterControllerIdentity in the list")
Expect(identityList.Items[0].APIVersion).To(Equal(infrav1.GroupVersion.String()),
"Expected list item APIVersion to be %s, got %s", infrav1.GroupVersion.String(), identityList.Items[0].APIVersion)

ginkgo.By("PASSED! Conversion webhook successfully converts v1beta1 to v1beta2")
})

ginkgo.It("should handle conversion during provider upgrade", func() {
specName := "conversion-upgrade-awsclustercontrolleridentity"
namespace := shared.SetupSpecNamespace(ctx, specName, e2eCtx)
defer shared.DumpSpecResourcesAndCleanup(ctx, specName, namespace, e2eCtx)

bootstrapClient := e2eCtx.Environment.BootstrapClusterProxy.GetClient()
kubeconfigPath := e2eCtx.Environment.BootstrapClusterProxy.GetKubeconfigPath()

ginkgo.By("Step 1: Creating AWSClusterControllerIdentity in v1beta1 format (simulating old provider)")
// Create the resource using kubectl with explicit v1beta1 APIVersion
// This simulates a resource created by an old provider version
v1beta1YAML := fmt.Sprintf(`apiVersion: infrastructure.cluster.x-k8s.io/v1beta1
kind: AWSClusterControllerIdentity
metadata:
name: %s
spec:
allowedNamespaces: {}
`, infrav1.AWSClusterControllerIdentityName)

// Delete if exists first to ensure clean state
existing := &infrav1.AWSClusterControllerIdentity{}
err := bootstrapClient.Get(ctx, client.ObjectKey{Name: infrav1.AWSClusterControllerIdentityName}, existing)
if err == nil {
Expect(bootstrapClient.Delete(ctx, existing)).To(Succeed())
// Wait for deletion
Eventually(func() bool {
err := bootstrapClient.Get(ctx, client.ObjectKey{Name: infrav1.AWSClusterControllerIdentityName}, existing)
return apierrors.IsNotFound(err)
}, 30*time.Second, 1*time.Second).Should(BeTrue())
}

// Create using kubectl apply
createCmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath, "apply", "-f", "-")

Check failure on line 162 in test/e2e/suites/unmanaged/unmanaged_conversion_test.go

View workflow job for this annotation

GitHub Actions / lint

G204: Subprocess launched with variable (gosec)
createCmd.Stdin = strings.NewReader(v1beta1YAML)
output, err := createCmd.CombinedOutput()
Expect(err).NotTo(HaveOccurred(), "Failed to create v1beta1 AWSClusterControllerIdentity: %s", string(output))

ginkgo.By("Step 2: Verifying resource exists with v1beta1 APIVersion")
// Verify it was created with v1beta1 by checking the stored version
// Note: The stored version might be v1beta2 if conversion already happened,
// but we can verify conversion works when we get it

ginkgo.By("Step 3: Simulating provider upgrade - getting resource should trigger conversion")
// When we get the resource using v1beta2 client, Kubernetes will request conversion
// This simulates what happens when the provider is upgraded and the webhook handles conversion
v1beta2Identity := &infrav1.AWSClusterControllerIdentity{}
err = bootstrapClient.Get(ctx, client.ObjectKey{
Name: infrav1.AWSClusterControllerIdentityName,
}, v1beta2Identity)

Expect(err).NotTo(HaveOccurred(), "Failed to get AWSClusterControllerIdentity after upgrade - conversion webhook may have failed")

ginkgo.By("Step 4: Verifying converted resource has correct APIVersion")
// Verify the APIVersion is correct after conversion
Expect(v1beta2Identity.APIVersion).To(Equal(infrav1.GroupVersion.String()),
"Expected APIVersion to be %s after conversion, got %s. This indicates the conversion webhook fix is working.",
infrav1.GroupVersion.String(), v1beta2Identity.APIVersion)

ginkgo.By("Step 5: Verifying resource can be retrieved via kubectl")
// Verify via kubectl that the resource is accessible and has correct APIVersion
kubectlCmd := exec.Command("kubectl", "--kubeconfig", kubeconfigPath,

Check failure on line 190 in test/e2e/suites/unmanaged/unmanaged_conversion_test.go

View workflow job for this annotation

GitHub Actions / lint

G204: Subprocess launched with variable (gosec)
"get", "awsclustercontrolleridentity", infrav1.AWSClusterControllerIdentityName,
"-o", "jsonpath={.apiVersion}")
output, err = kubectlCmd.CombinedOutput()
Expect(err).NotTo(HaveOccurred(), "kubectl get failed: %s", string(output))

apiVersion := strings.TrimSpace(string(output))
Expect(apiVersion).To(Equal("infrastructure.cluster.x-k8s.io/v1beta2"),
"kubectl get returned wrong APIVersion: expected v1beta2, got %s", apiVersion)

ginkgo.By("Step 6: Verifying resource spec is preserved after conversion")
// Verify the spec is preserved correctly
Expect(v1beta2Identity.Spec.AllowedNamespaces).NotTo(BeNil(),
"Spec.AllowedNamespaces should be preserved after conversion")

ginkgo.By("PASSED! Provider upgrade conversion test successful")
})
})
})
Loading