diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml index a69fc00a41..d32673aeea 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -96,6 +96,11 @@ spec: description: Name is the name of the addon minLength: 2 type: string + preserveOnDelete: + description: |- + PreserveOnDelete indicates that the addon resources should be + preserved in the cluster on delete. + type: boolean serviceAccountRoleARN: description: ServiceAccountRoleArn is the ARN of an IAM role to bind to the addons service account @@ -2270,6 +2275,11 @@ spec: description: Name is the name of the addon minLength: 2 type: string + preserveOnDelete: + description: |- + PreserveOnDelete indicates that the addon resources should be + preserved in the cluster on delete. + type: boolean serviceAccountRoleARN: description: ServiceAccountRoleArn is the ARN of an IAM role to bind to the addons service account diff --git a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml index c92dd995e5..c2d620aab2 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml @@ -83,6 +83,11 @@ spec: description: Name is the name of the addon minLength: 2 type: string + preserveOnDelete: + description: |- + PreserveOnDelete indicates that the addon resources should be + preserved in the cluster on delete. + type: boolean serviceAccountRoleARN: description: ServiceAccountRoleArn is the ARN of an IAM role to bind to the addons service account diff --git a/controlplane/eks/api/v1beta1/types.go b/controlplane/eks/api/v1beta1/types.go index b2d80277ac..4fa88aa0a5 100644 --- a/controlplane/eks/api/v1beta1/types.go +++ b/controlplane/eks/api/v1beta1/types.go @@ -141,6 +141,10 @@ type Addon struct { // ServiceAccountRoleArn is the ARN of an IAM role to bind to the addons service account // +optional ServiceAccountRoleArn *string `json:"serviceAccountRoleARN,omitempty"` + // PreserveOnDelete indicates that the addon resources should be + // preserved in the cluster on delete. + // +optional + PreserveOnDelete bool `json:"preserveOnDelete,omitempty"` } // AddonResolution defines the method for resolving parameter conflicts. diff --git a/controlplane/eks/api/v1beta1/zz_generated.conversion.go b/controlplane/eks/api/v1beta1/zz_generated.conversion.go index b7bb9b0a6f..9fe8517b2f 100644 --- a/controlplane/eks/api/v1beta1/zz_generated.conversion.go +++ b/controlplane/eks/api/v1beta1/zz_generated.conversion.go @@ -436,6 +436,7 @@ func autoConvert_v1beta1_Addon_To_v1beta2_Addon(in *Addon, out *v1beta2.Addon, s out.Configuration = in.Configuration out.ConflictResolution = (*v1beta2.AddonResolution)(unsafe.Pointer(in.ConflictResolution)) out.ServiceAccountRoleArn = (*string)(unsafe.Pointer(in.ServiceAccountRoleArn)) + out.PreserveOnDelete = in.PreserveOnDelete return nil } @@ -450,6 +451,7 @@ func autoConvert_v1beta2_Addon_To_v1beta1_Addon(in *v1beta2.Addon, out *Addon, s out.Configuration = in.Configuration out.ConflictResolution = (*AddonResolution)(unsafe.Pointer(in.ConflictResolution)) out.ServiceAccountRoleArn = (*string)(unsafe.Pointer(in.ServiceAccountRoleArn)) + out.PreserveOnDelete = in.PreserveOnDelete return nil } diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go index 6a260562b0..276faa5b09 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go @@ -85,118 +85,52 @@ func TestDefaultingWebhook(t *testing.T) { resourceName: "cluster1", resourceNS: "default", expectHash: false, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "default_cluster1", - IdentityRef: defaultIdentityRef, - Bastion: defaultTestBastion, - NetworkSpec: defaultNetworkSpec, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, { name: "less than 100 chars, dot in name", resourceName: "team1.cluster1", resourceNS: "default", expectHash: false, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "default_team1_cluster1", - IdentityRef: defaultIdentityRef, - Bastion: defaultTestBastion, - NetworkSpec: defaultNetworkSpec, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_team1_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, { name: "more than 100 chars", resourceName: "abcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcdeabcde", resourceNS: "default", expectHash: true, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "capi_", - IdentityRef: defaultIdentityRef, - Bastion: defaultTestBastion, - NetworkSpec: defaultNetworkSpec, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "capi_", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, { name: "with patch", resourceName: "cluster1", resourceNS: "default", expectHash: false, - spec: AWSManagedControlPlaneSpec{ - Version: &vV1_17_1, - }, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "default_cluster1", - Version: &vV1_17_1, - IdentityRef: defaultIdentityRef, - Bastion: defaultTestBastion, - NetworkSpec: defaultNetworkSpec, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + spec: AWSManagedControlPlaneSpec{Version: &vV1_17_1}, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", Version: &vV1_17_1, IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, { name: "with allowed ip on bastion", resourceName: "cluster1", resourceNS: "default", expectHash: false, - spec: AWSManagedControlPlaneSpec{ - Bastion: infrav1.Bastion{ - AllowedCIDRBlocks: []string{"100.100.100.100/0"}, - }, - }, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "default_cluster1", - IdentityRef: defaultIdentityRef, - Bastion: infrav1.Bastion{ - AllowedCIDRBlocks: []string{"100.100.100.100/0"}, - }, - NetworkSpec: defaultNetworkSpec, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + spec: AWSManagedControlPlaneSpec{Bastion: infrav1.Bastion{AllowedCIDRBlocks: []string{"100.100.100.100/0"}}}, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: infrav1.Bastion{AllowedCIDRBlocks: []string{"100.100.100.100/0"}}, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, { name: "with CNI on network", resourceName: "cluster1", resourceNS: "default", expectHash: false, - spec: AWSManagedControlPlaneSpec{ - NetworkSpec: infrav1.NetworkSpec{ - CNI: &infrav1.CNISpec{}, - }, - }, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "default_cluster1", - IdentityRef: defaultIdentityRef, - Bastion: defaultTestBastion, - NetworkSpec: infrav1.NetworkSpec{ - CNI: &infrav1.CNISpec{}, - VPC: defaultVPCSpec, - }, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + spec: AWSManagedControlPlaneSpec{NetworkSpec: infrav1.NetworkSpec{CNI: &infrav1.CNISpec{}}}, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: infrav1.NetworkSpec{CNI: &infrav1.CNISpec{}, VPC: defaultVPCSpec}, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, { name: "secondary CIDR", resourceName: "cluster1", resourceNS: "default", expectHash: false, - expectSpec: AWSManagedControlPlaneSpec{ - EKSClusterName: "default_cluster1", - IdentityRef: defaultIdentityRef, - Bastion: defaultTestBastion, - NetworkSpec: defaultNetworkSpec, - SecondaryCidrBlock: nil, - TokenMethod: &EKSTokenMethodIAMAuthenticator, - BootstrapSelfManagedAddons: true, - }, + expectSpec: AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, SecondaryCidrBlock: nil, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true}, }, } diff --git a/controlplane/eks/api/v1beta2/types.go b/controlplane/eks/api/v1beta2/types.go index ba355089ef..622e4b9c3d 100644 --- a/controlplane/eks/api/v1beta2/types.go +++ b/controlplane/eks/api/v1beta2/types.go @@ -141,6 +141,10 @@ type Addon struct { // ServiceAccountRoleArn is the ARN of an IAM role to bind to the addons service account // +optional ServiceAccountRoleArn *string `json:"serviceAccountRoleARN,omitempty"` + // PreserveOnDelete indicates that the addon resources should be + // preserved in the cluster on delete. + // +optional + PreserveOnDelete bool `json:"preserveOnDelete,omitempty"` } // AddonResolution defines the method for resolving parameter conflicts. diff --git a/pkg/cloud/services/eks/addons.go b/pkg/cloud/services/eks/addons.go index be0160aca1..66e66f2e2b 100644 --- a/pkg/cloud/services/eks/addons.go +++ b/pkg/cloud/services/eks/addons.go @@ -207,6 +207,7 @@ func (s *Service) translateAPIToAddon(addons []ekscontrolplanev1.Addon) []*eksad Tags: ngTags(s.scope.Cluster.Name, s.scope.AdditionalTags()), ResolveConflict: conflict, ServiceAccountRoleARN: addon.ServiceAccountRoleArn, + Preserve: addon.PreserveOnDelete, } converted = append(converted, convertedAddon) diff --git a/pkg/eks/addons/plan.go b/pkg/eks/addons/plan.go index 6b975e509d..7df3ba6ced 100644 --- a/pkg/eks/addons/plan.go +++ b/pkg/eks/addons/plan.go @@ -86,7 +86,7 @@ func (a *plan) Create(_ context.Context) ([]planner.Procedure, error) { desired := a.getDesired(*installed.Name) if desired == nil { if *installed.Status != string(ekstypes.AddonStatusDeleting) { - procedures = append(procedures, &DeleteAddonProcedure{plan: a, name: *installed.Name}) + procedures = append(procedures, &DeleteAddonProcedure{plan: a, name: *installed.Name, preserve: installed.Preserve}) } procedures = append(procedures, &WaitAddonDeleteProcedure{plan: a, name: *installed.Name}) } diff --git a/pkg/eks/addons/plan_test.go b/pkg/eks/addons/plan_test.go index 38ca51d425..4ab88b3d45 100644 --- a/pkg/eks/addons/plan_test.go +++ b/pkg/eks/addons/plan_test.go @@ -41,6 +41,7 @@ func TestEKSAddonPlan(t *testing.T) { addonStatusUpdating := string(ekstypes.AddonStatusUpdating) addonStatusDeleting := string(ekstypes.AddonStatusDeleting) addonStatusCreating := string(ekstypes.AddonStatusCreating) + addonPreserve := false created := time.Now() maxActiveUpdateDeleteWait := 30 * time.Minute @@ -176,7 +177,7 @@ func TestEKSAddonPlan(t *testing.T) { createDesiredAddon(addon1Name, addon1version), }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive, false), }, expectCreateError: false, expectDoError: false, @@ -198,7 +199,7 @@ func TestEKSAddonPlan(t *testing.T) { createDesiredAddon(addon1Name, addon1version), }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusCreating), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusCreating, addonPreserve), }, expectCreateError: false, expectDoError: false, @@ -236,7 +237,7 @@ func TestEKSAddonPlan(t *testing.T) { createDesiredAddon(addon1Name, addon1Upgrade), }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive, addonPreserve), }, expectCreateError: false, expectDoError: false, @@ -258,7 +259,7 @@ func TestEKSAddonPlan(t *testing.T) { createDesiredAddon(addon1Name, addon1Upgrade), }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1Upgrade, addonARN, addonStatusUpdating), + createInstalledAddon(addon1Name, addon1Upgrade, addonARN, addonStatusUpdating, addonPreserve), }, expectCreateError: false, expectDoError: false, @@ -277,7 +278,7 @@ func TestEKSAddonPlan(t *testing.T) { createDesiredAddonExtraTag(addon1Name, addon1version), }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive, addonPreserve), }, expectCreateError: false, expectDoError: false, @@ -321,7 +322,7 @@ func TestEKSAddonPlan(t *testing.T) { createDesiredAddonExtraTag(addon1Name, addon1Upgrade), }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive, addonPreserve), }, expectCreateError: false, expectDoError: false, @@ -333,6 +334,7 @@ func TestEKSAddonPlan(t *testing.T) { DeleteAddon(gomock.Eq(context.TODO()), gomock.Eq(&eks.DeleteAddonInput{ AddonName: &addon1Name, ClusterName: &clusterName, + Preserve: false, })). Return(&eks.DeleteAddonOutput{ Addon: &ekstypes.Addon{ @@ -352,7 +354,39 @@ func TestEKSAddonPlan(t *testing.T) { }), maxActiveUpdateDeleteWait).Return(nil) }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive, addonPreserve), + }, + expectCreateError: false, + expectDoError: false, + }, + { + name: "1 installed and 0 desired - delete addon & preserve", + expect: func(m *mock_eksiface.MockEKSAPIMockRecorder) { + m. + DeleteAddon(gomock.Eq(context.TODO()), gomock.Eq(&eks.DeleteAddonInput{ + AddonName: &addon1Name, + ClusterName: &clusterName, + Preserve: true, + })). + Return(&eks.DeleteAddonOutput{ + Addon: &ekstypes.Addon{ + AddonArn: aws.String(addonARN), + AddonName: aws.String(addon1Name), + AddonVersion: aws.String(addon1version), + ClusterName: aws.String(clusterName), + CreatedAt: &created, + ModifiedAt: &created, + Status: ekstypes.AddonStatusDeleting, + Tags: createTags(), + }, + }, nil) + m.WaitUntilAddonDeleted(gomock.Eq(context.TODO()), gomock.Eq(&eks.DescribeAddonInput{ + AddonName: aws.String(addon1Name), + ClusterName: aws.String(clusterName), + }), maxActiveUpdateDeleteWait).Return(nil) + }, + installedAddons: []*EKSAddon{ + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusActive, true), }, expectCreateError: false, expectDoError: false, @@ -366,7 +400,7 @@ func TestEKSAddonPlan(t *testing.T) { }), maxActiveUpdateDeleteWait).Return(nil) }, installedAddons: []*EKSAddon{ - createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusDeleting), + createInstalledAddon(addon1Name, addon1version, addonARN, addonStatusDeleting, false), }, expectCreateError: false, expectDoError: false, @@ -442,10 +476,11 @@ func createDesiredAddonExtraTag(name, version string) *EKSAddon { } } -func createInstalledAddon(name, version, arn, status string) *EKSAddon { +func createInstalledAddon(name, version, arn, status string, preserve bool) *EKSAddon { desired := createDesiredAddon(name, version) desired.ARN = &arn desired.Status = &status + desired.Preserve = preserve return desired } diff --git a/pkg/eks/addons/procedures.go b/pkg/eks/addons/procedures.go index d43d114e8c..de1cff4af6 100644 --- a/pkg/eks/addons/procedures.go +++ b/pkg/eks/addons/procedures.go @@ -40,8 +40,9 @@ var ( // DeleteAddonProcedure is a procedure that will delete an EKS addon. type DeleteAddonProcedure struct { - plan *plan - name string + plan *plan + name string + preserve bool } // Do implements the logic for the procedure. @@ -49,6 +50,7 @@ func (p *DeleteAddonProcedure) Do(ctx context.Context) error { input := &eks.DeleteAddonInput{ AddonName: aws.String(p.name), ClusterName: aws.String(p.plan.clusterName), + Preserve: p.preserve, } if _, err := p.plan.eksClient.DeleteAddon(ctx, input); err != nil { diff --git a/pkg/eks/addons/types.go b/pkg/eks/addons/types.go index 9ddce2c176..a4dacd8dda 100644 --- a/pkg/eks/addons/types.go +++ b/pkg/eks/addons/types.go @@ -30,6 +30,7 @@ type EKSAddon struct { Configuration *string Tags infrav1.Tags ResolveConflict *string + Preserve bool ARN *string Status *string }