Repository: kubernetes-sigs/cluster-api-provider-aws

Issue:
Add `Status.Conditions` to AWSManagedCluster and  ROSACluster
/kind feature

As the CAPI InfraCluster provider contract has been updated (see https://github.com/kubernetes-sigs/cluster-api/pull/10519), we need to update the CAPA  InfraCluster implementations. This involves adding a `Paused` condition.

Currently, both `AWSManagedCluster` and `ROSACluster` do not have a `Conditions` field in their `Status`, so no conditions can be set.


**Describe the solution you'd like**
[A clear and concise description of what you want to happen.]

both `AWSManagedCluster` and `ROSACluster` have a `Conditions` field in their `Status`.


Discussion:
@theobarberbany: cc @nrb @damdo @nrb: /triage accepted
/assign @theobarberbany 
/milestone v2.8

Solution patch:
diff --git a/api/v1beta2/awsmanagedcluster_types.go b/api/v1beta2/awsmanagedcluster_types.go
index 587ace765..67d9b2fc9 100644
--- a/api/v1beta2/awsmanagedcluster_types.go
+++ b/api/v1beta2/awsmanagedcluster_types.go
@@ -38,6 +38,10 @@ type AWSManagedClusterStatus struct {
 	// FailureDomains specifies a list fo available availability zones that can be used
 	// +optional
 	FailureDomains clusterv1.FailureDomains `json:"failureDomains,omitempty"`
+
+	// Conditions defines current service state of the AWSManagedCluster.
+	// +optional
+	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
@@ -69,3 +73,15 @@ type AWSManagedClusterList struct {
 func init() {
 	SchemeBuilder.Register(&AWSManagedCluster{}, &AWSManagedClusterList{})
 }
+
+// GetConditions returns the observations of the operational state of the
+// AWSManagedCluster resource.
+func (r *AWSManagedCluster) GetConditions() clusterv1.Conditions {
+	return r.Status.Conditions
+}
+
+// SetConditions sets the underlying service state of the AWSManagedCluster to
+// the predescribed clusterv1.Conditions.
+func (r *AWSManagedCluster) SetConditions(conditions clusterv1.Conditions) {
+	r.Status.Conditions = conditions
+}
diff --git a/api/v1beta2/zz_generated.deepcopy.go b/api/v1beta2/zz_generated.deepcopy.go
index b66c06633..a3ef61f24 100644
--- a/api/v1beta2/zz_generated.deepcopy.go
+++ b/api/v1beta2/zz_generated.deepcopy.go
@@ -1024,6 +1024,13 @@ func (in *AWSManagedClusterStatus) DeepCopyInto(out *AWSManagedClusterStatus) {
 			(*out)[key] = *val.DeepCopy()
 		}
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make(v1beta1.Conditions, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedClusterStatus.
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 055e5d987..0a5464ff7 100644
--- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml
+++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml
@@ -2194,6 +2194,13 @@ spec:
                       will be the default.
                     type: string
                 type: object
+              bootstrapSelfManagedAddons:
+                default: true
+                description: |-
+                  BootstrapSelfManagedAddons is used to set configuration options for
+                  bare EKS cluster without EKS default networking addons
+                  If you set this value to false when creating a cluster, the default networking add-ons will not be installed
+                type: boolean
               controlPlaneEndpoint:
                 description: ControlPlaneEndpoint represents the endpoint used to
                   communicate with the control plane.
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclusters.yaml
index d9beb6483..ae196478d 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_awsmanagedclusters.yaml
@@ -76,6 +76,51 @@ spec:
           status:
             description: AWSManagedClusterStatus defines the observed state of AWSManagedCluster
             properties:
+              conditions:
+                description: Conditions defines current service state of the AWSManagedCluster.
+                items:
+                  description: Condition defines an observation of a Cluster API resource
+                    operational state.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        Last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed. If that is not known, then using the time when
+                        the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        A human readable message indicating details about the transition.
+                        This field may be empty.
+                      type: string
+                    reason:
+                      description: |-
+                        The reason for the condition's last transition in CamelCase.
+                        The specific API may choose whether or not this field is considered a guaranteed API.
+                        This field may be empty.
+                      type: string
+                    severity:
+                      description: |-
+                        severity provides an explicit classification of Reason code, so the users or machines can immediately
+                        understand the current situation and act accordingly.
+                        The Severity field MUST be set only when Status=False.
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      type: string
+                    type:
+                      description: |-
+                        type of condition in CamelCase or in foo.example.com/CamelCase.
+                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions
+                        can be useful (see .node.status.conditions), the ability to deconflict is important.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - status
+                  - type
+                  type: object
+                type: array
               failureDomains:
                 additionalProperties:
                   description: |-
diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaclusters.yaml
index 489cd07db..c31ff0260 100644
--- a/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaclusters.yaml
+++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_rosaclusters.yaml
@@ -76,6 +76,51 @@ spec:
           status:
             description: ROSAClusterStatus defines the observed state of ROSACluster.
             properties:
+              conditions:
+                description: Conditions defines current service state of the ROSACluster.
+                items:
+                  description: Condition defines an observation of a Cluster API resource
+                    operational state.
+                  properties:
+                    lastTransitionTime:
+                      description: |-
+                        Last time the condition transitioned from one status to another.
+                        This should be when the underlying condition changed. If that is not known, then using the time when
+                        the API field changed is acceptable.
+                      format: date-time
+                      type: string
+                    message:
+                      description: |-
+                        A human readable message indicating details about the transition.
+                        This field may be empty.
+                      type: string
+                    reason:
+                      description: |-
+                        The reason for the condition's last transition in CamelCase.
+                        The specific API may choose whether or not this field is considered a guaranteed API.
+                        This field may be empty.
+                      type: string
+                    severity:
+                      description: |-
+                        severity provides an explicit classification of Reason code, so the users or machines can immediately
+                        understand the current situation and act accordingly.
+                        The Severity field MUST be set only when Status=False.
+                      type: string
+                    status:
+                      description: status of the condition, one of True, False, Unknown.
+                      type: string
+                    type:
+                      description: |-
+                        type of condition in CamelCase or in foo.example.com/CamelCase.
+                        Many .condition.type values are consistent across resources like Available, but because arbitrary conditions
+                        can be useful (see .node.status.conditions), the ability to deconflict is important.
+                      type: string
+                  required:
+                  - lastTransitionTime
+                  - status
+                  - type
+                  type: object
+                type: array
               failureDomains:
                 additionalProperties:
                   description: |-
diff --git a/controllers/awsmanagedcluster_controller.go b/controllers/awsmanagedcluster_controller.go
index 3f5c62930..17da9578b 100644
--- a/controllers/awsmanagedcluster_controller.go
+++ b/controllers/awsmanagedcluster_controller.go
@@ -35,9 +35,9 @@ import (
 	infrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
 	ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger"
+	"sigs.k8s.io/cluster-api-provider-aws/v2/util/paused"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
@@ -78,11 +78,6 @@ func (r *AWSManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re
 		return reconcile.Result{}, nil
 	}
 
-	if annotations.IsPaused(cluster, awsManagedCluster) {
-		log.Info("AWSManagedCluster or linked Cluster is marked as paused. Won't reconcile")
-		return reconcile.Result{}, nil
-	}
-
 	log = log.WithValues("cluster", cluster.Name)
 
 	controlPlane := &ekscontrolplanev1.AWSManagedControlPlane{}
@@ -95,6 +90,10 @@ func (r *AWSManagedClusterReconciler) Reconcile(ctx context.Context, req ctrl.Re
 		return reconcile.Result{}, fmt.Errorf("failed to get control plane ref: %w", err)
 	}
 
+	if isPaused, conditionChanged, err := paused.EnsurePausedCondition(ctx, r.Client, cluster, awsManagedCluster); err != nil || isPaused || conditionChanged {
+		return ctrl.Result{}, err
+	}
+
 	log = log.WithValues("controlPlane", controlPlaneRef.Name)
 
 	patchHelper, err := patch.NewHelper(awsManagedCluster, r.Client)
@@ -124,7 +123,7 @@ func (r *AWSManagedClusterReconciler) SetupWithManager(ctx context.Context, mgr
 	controller, err := ctrl.NewControllerManagedBy(mgr).
 		WithOptions(options).
 		For(awsManagedCluster).
-		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(mgr.GetScheme(), ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
+		WithEventFilter(predicates.ResourceHasFilterLabel(mgr.GetScheme(), ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
 		WithEventFilter(predicates.ResourceIsNotExternallyManaged(mgr.GetScheme(), log.GetLogger())).
 		Build(r)
 
@@ -132,11 +131,11 @@ func (r *AWSManagedClusterReconciler) SetupWithManager(ctx context.Context, mgr
 		return fmt.Errorf("error creating controller: %w", err)
 	}
 
-	// Add a watch for clusterv1.Cluster unpaise
+	// Add a watch for clusterv1.Cluster unpause
 	if err = controller.Watch(
 		source.Kind[client.Object](mgr.GetCache(), &clusterv1.Cluster{},
 			handler.EnqueueRequestsFromMapFunc(util.ClusterToInfrastructureMapFunc(ctx, infrav1.GroupVersion.WithKind("AWSManagedCluster"), mgr.GetClient(), &infrav1.AWSManagedCluster{})),
-			predicates.ClusterUnpaused(mgr.GetScheme(), log.GetLogger())),
+			predicates.ClusterPausedTransitions(mgr.GetScheme(), log.GetLogger())),
 	); err != nil {
 		return fmt.Errorf("failed adding a watch for ready clusters: %w", err)
 	}
diff --git a/controllers/rosacluster_controller.go b/controllers/rosacluster_controller.go
index 0b061f185..bd880fa42 100644
--- a/controllers/rosacluster_controller.go
+++ b/controllers/rosacluster_controller.go
@@ -37,9 +37,9 @@ import (
 	expinfrav1 "sigs.k8s.io/cluster-api-provider-aws/v2/exp/api/v1beta2"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/cloud/scope"
 	"sigs.k8s.io/cluster-api-provider-aws/v2/pkg/logger"
+	"sigs.k8s.io/cluster-api-provider-aws/v2/util/paused"
 	clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
 	"sigs.k8s.io/cluster-api/util"
-	"sigs.k8s.io/cluster-api/util/annotations"
 	"sigs.k8s.io/cluster-api/util/patch"
 	"sigs.k8s.io/cluster-api/util/predicates"
 )
@@ -82,9 +82,8 @@ func (r *ROSAClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 		return reconcile.Result{}, nil
 	}
 
-	if annotations.IsPaused(cluster, rosaCluster) {
-		log.Info("ROSACluster or linked Cluster is marked as paused. Won't reconcile")
-		return reconcile.Result{}, nil
+	if isPaused, conditionChanged, err := paused.EnsurePausedCondition(ctx, r.Client, cluster, rosaCluster); err != nil || isPaused || conditionChanged {
+		return ctrl.Result{}, err
 	}
 
 	log = log.WithValues("cluster", cluster.Name)
@@ -127,7 +126,7 @@ func (r *ROSAClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.M
 	controller, err := ctrl.NewControllerManagedBy(mgr).
 		WithOptions(options).
 		For(rosaCluster).
-		WithEventFilter(predicates.ResourceNotPausedAndHasFilterLabel(mgr.GetScheme(), ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
+		WithEventFilter(predicates.ResourceHasFilterLabel(mgr.GetScheme(), ctrl.LoggerFrom(ctx), r.WatchFilterValue)).
 		Build(r)
 
 	if err != nil {
@@ -138,7 +137,7 @@ func (r *ROSAClusterReconciler) SetupWithManager(ctx context.Context, mgr ctrl.M
 	if err = controller.Watch(
 		source.Kind[client.Object](mgr.GetCache(), &clusterv1.Cluster{},
 			handler.EnqueueRequestsFromMapFunc(util.ClusterToInfrastructureMapFunc(ctx, infrav1.GroupVersion.WithKind("ROSACluster"), mgr.GetClient(), &expinfrav1.ROSACluster{})),
-			predicates.ClusterUnpaused(mgr.GetScheme(), log.GetLogger())),
+			predicates.ClusterPausedTransitions(mgr.GetScheme(), log.GetLogger())),
 	); err != nil {
 		return fmt.Errorf("failed adding a watch for ready clusters: %w", err)
 	}
diff --git a/controlplane/eks/api/v1beta1/conversion.go b/controlplane/eks/api/v1beta1/conversion.go
index bb089b4cf..d0b894067 100644
--- a/controlplane/eks/api/v1beta1/conversion.go
+++ b/controlplane/eks/api/v1beta1/conversion.go
@@ -43,7 +43,7 @@ func (r *AWSManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error {
 	dst.Spec.Partition = restored.Spec.Partition
 	dst.Spec.RestrictPrivateSubnets = restored.Spec.RestrictPrivateSubnets
 	dst.Status.Version = restored.Status.Version
-
+	dst.Spec.BootstrapSelfManagedAddons = restored.Spec.BootstrapSelfManagedAddons
 	return nil
 }
 
diff --git a/controlplane/eks/api/v1beta1/zz_generated.conversion.go b/controlplane/eks/api/v1beta1/zz_generated.conversion.go
index 55d9b204b..d300d0434 100644
--- a/controlplane/eks/api/v1beta1/zz_generated.conversion.go
+++ b/controlplane/eks/api/v1beta1/zz_generated.conversion.go
@@ -403,6 +403,7 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl
 	if err := Convert_v1beta2_VpcCni_To_v1beta1_VpcCni(&in.VpcCni, &out.VpcCni, s); err != nil {
 		return err
 	}
+	// WARNING: in.BootstrapSelfManagedAddons requires manual conversion: does not exist in peer-type
 	// WARNING: in.RestrictPrivateSubnets requires manual conversion: does not exist in peer-type
 	if err := Convert_v1beta2_KubeProxy_To_v1beta1_KubeProxy(&in.KubeProxy, &out.KubeProxy, s); err != nil {
 		return err
diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go
index ac386461a..9106f4d25 100644
--- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go
+++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go
@@ -173,6 +173,12 @@ type AWSManagedControlPlaneSpec struct { //nolint: maligned
 	// +optional
 	VpcCni VpcCni `json:"vpcCni,omitempty"`
 
+	// BootstrapSelfManagedAddons is used to set configuration options for
+	// bare EKS cluster without EKS default networking addons
+	// If you set this value to false when creating a cluster, the default networking add-ons will not be installed
+	// +kubebuilder:default=true
+	BootstrapSelfManagedAddons bool `json:"bootstrapSelfManagedAddons,omitempty"`
+
 	// RestrictPrivateSubnets indicates that the EKS control plane should only use private subnets.
 	// +kubebuilder:default=false
 	RestrictPrivateSubnets bool `json:"restrictPrivateSubnets,omitempty"`
diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go
index abda129f9..47025a9ef 100644
--- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go
+++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go
@@ -502,4 +502,7 @@ func (r *AWSManagedControlPlane) Default() {
 
 	infrav1.SetDefaults_Bastion(&r.Spec.Bastion)
 	infrav1.SetDefaults_NetworkSpec(&r.Spec.NetworkSpec)
+
+	// Set default value for BootstrapSelfManagedAddons
+	r.Spec.BootstrapSelfManagedAddons = true
 }
diff --git a/exp/api/v1beta2/rosacluster_types.go b/exp/api/v1beta2/rosacluster_types.go
index 1b3ffa5d7..3303125d1 100644
--- a/exp/api/v1beta2/rosacluster_types.go
+++ b/exp/api/v1beta2/rosacluster_types.go
@@ -38,6 +38,10 @@ type ROSAClusterStatus struct {
 	// FailureDomains specifies a list fo available availability zones that can be used
 	// +optional
 	FailureDomains clusterv1.FailureDomains `json:"failureDomains,omitempty"`
+
+	// Conditions defines current service state of the ROSACluster.
+	// +optional
+	Conditions clusterv1.Conditions `json:"conditions,omitempty"`
 }
 
 // +kubebuilder:object:root=true
@@ -66,6 +70,18 @@ type ROSAClusterList struct {
 	Items           []ROSACluster `json:"items"`
 }
 
+// GetConditions returns the observations of the operational state of the
+// ROSACluster resource.
+func (r *ROSACluster) GetConditions() clusterv1.Conditions {
+	return r.Status.Conditions
+}
+
+// SetConditions sets the underlying service state of the ROSACluster to the
+// predescribed clusterv1.Conditions.
+func (r *ROSACluster) SetConditions(conditions clusterv1.Conditions) {
+	r.Status.Conditions = conditions
+}
+
 func init() {
 	SchemeBuilder.Register(&ROSACluster{}, &ROSAClusterList{})
 }
diff --git a/exp/api/v1beta2/zz_generated.deepcopy.go b/exp/api/v1beta2/zz_generated.deepcopy.go
index 3bea8430c..3332aaeed 100644
--- a/exp/api/v1beta2/zz_generated.deepcopy.go
+++ b/exp/api/v1beta2/zz_generated.deepcopy.go
@@ -977,6 +977,13 @@ func (in *ROSAClusterStatus) DeepCopyInto(out *ROSAClusterStatus) {
 			(*out)[key] = *val.DeepCopy()
 		}
 	}
+	if in.Conditions != nil {
+		in, out := &in.Conditions, &out.Conditions
+		*out = make(v1beta1.Conditions, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ROSAClusterStatus.
diff --git a/pkg/cloud/scope/managedcontrolplane.go b/pkg/cloud/scope/managedcontrolplane.go
index 14ded9226..5f8b9cd93 100644
--- a/pkg/cloud/scope/managedcontrolplane.go
+++ b/pkg/cloud/scope/managedcontrolplane.go
@@ -424,6 +424,11 @@ func (s *ManagedControlPlaneScope) DisableVPCCNI() bool {
 	return s.ControlPlane.Spec.VpcCni.Disable
 }
 
+// BootstrapSelfManagedAddons returns whether the AWS EKS networking addons should be disabled.
+func (s *ManagedControlPlaneScope) BootstrapSelfManagedAddons() bool {
+	return s.ControlPlane.Spec.BootstrapSelfManagedAddons
+}
+
 // VpcCni returns a list of environment variables to apply to the `aws-node` DaemonSet.
 func (s *ManagedControlPlaneScope) VpcCni() ekscontrolplanev1.VpcCni {
 	return s.ControlPlane.Spec.VpcCni
diff --git a/pkg/cloud/services/eks/cluster.go b/pkg/cloud/services/eks/cluster.go
index 44f6e2eb0..7dac037d8 100644
--- a/pkg/cloud/services/eks/cluster.go
+++ b/pkg/cloud/services/eks/cluster.go
@@ -473,6 +473,11 @@ func (s *Service) createCluster(eksClusterName string) (*eks.Cluster, error) {
 		Tags:                    tags,
 		KubernetesNetworkConfig: netConfig,
 	}
+	// Only set BootstrapSelfManagedAddons if it's explicitly set to false in the spec
+	// Default is true, so we don't need to set it in that case
+	if !s.scope.BootstrapSelfManagedAddons() {
+		input.BootstrapSelfManagedAddons = aws.Bool(false)
+	}
 
 	var out *eks.CreateClusterOutput
 	if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) {



Test patch:
diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go
index 7441040b8..6fe1b62c2 100644
--- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go
+++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go
@@ -85,21 +85,21 @@ func TestDefaultingWebhook(t *testing.T) {
 			resourceName: "cluster1",
 			resourceNS:   "default",
 			expectHash:   false,
-			expectSpec:   AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator},
+			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},
+			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},
+			expectSpec:   AWSManagedControlPlaneSpec{EKSClusterName: "capi_", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true},
 		},
 		{
 			name:         "with patch",
@@ -107,7 +107,7 @@ func TestDefaultingWebhook(t *testing.T) {
 			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},
+			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",
@@ -115,7 +115,7 @@ func TestDefaultingWebhook(t *testing.T) {
 			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},
+			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",
@@ -123,14 +123,14 @@ func TestDefaultingWebhook(t *testing.T) {
 			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},
+			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},
+			expectSpec:   AWSManagedControlPlaneSpec{EKSClusterName: "default_cluster1", IdentityRef: defaultIdentityRef, Bastion: defaultTestBastion, NetworkSpec: defaultNetworkSpec, SecondaryCidrBlock: nil, TokenMethod: &EKSTokenMethodIAMAuthenticator, BootstrapSelfManagedAddons: true},
 		},
 	}
 
diff --git a/pkg/cloud/services/eks/cluster_test.go b/pkg/cloud/services/eks/cluster_test.go
index 7079c62de..c01c945a5 100644
--- a/pkg/cloud/services/eks/cluster_test.go
+++ b/pkg/cloud/services/eks/cluster_test.go
@@ -519,10 +519,11 @@ func TestCreateCluster(t *testing.T) {
 				},
 				ControlPlane: &ekscontrolplanev1.AWSManagedControlPlane{
 					Spec: ekscontrolplanev1.AWSManagedControlPlaneSpec{
-						EKSClusterName: clusterName,
-						Version:        version,
-						RoleName:       tc.role,
-						NetworkSpec:    infrav1.NetworkSpec{Subnets: tc.subnets},
+						EKSClusterName:             clusterName,
+						Version:                    version,
+						RoleName:                   tc.role,
+						NetworkSpec:                infrav1.NetworkSpec{Subnets: tc.subnets},
+						BootstrapSelfManagedAddons: false,
 					},
 				},
 			})
@@ -541,9 +542,10 @@ func TestCreateCluster(t *testing.T) {
 					ResourcesVpcConfig: &eks.VpcConfigRequest{
 						SubnetIds: subnetIDs,
 					},
-					RoleArn: tc.role,
-					Tags:    tc.tags,
-					Version: version,
+					RoleArn:                    tc.role,
+					Tags:                       tc.tags,
+					Version:                    version,
+					BootstrapSelfManagedAddons: aws.Bool(false),
 				}).Return(&eks.CreateClusterOutput{}, nil)
 			}
 			s := NewService(scope)
@@ -728,7 +730,8 @@ func TestCreateIPv6Cluster(t *testing.T) {
 					},
 					VPC: vpcSpec,
 				},
-				EncryptionConfig: encryptionConfig,
+				EncryptionConfig:           encryptionConfig,
+				BootstrapSelfManagedAddons: false,
 			},
 		},
 	})
@@ -754,6 +757,7 @@ func TestCreateIPv6Cluster(t *testing.T) {
 		Tags: map[string]*string{
 			"kubernetes.io/cluster/cluster-name": ptr.To[string]("owned"),
 		},
+		BootstrapSelfManagedAddons: aws.Bool(false),
 	}).Return(&eks.CreateClusterOutput{}, nil)
 	iamMock.EXPECT().GetRole(&iam.GetRoleInput{
 		RoleName: aws.String("arn-role"),
diff --git a/test/e2e/data/e2e_eks_conf.yaml b/test/e2e/data/e2e_eks_conf.yaml
index c3f893afa..156c0f703 100644
--- a/test/e2e/data/e2e_eks_conf.yaml
+++ b/test/e2e/data/e2e_eks_conf.yaml
@@ -114,6 +114,8 @@ providers:
         targetName: "cluster-template-eks-ipv6-cluster.yaml"
       - sourcePath: "./eks/cluster-template-eks-control-plane-only-legacy.yaml"
         targetName: "cluster-template-eks-control-plane-only-legacy.yaml"
+      - sourcePath: "./eks/cluster-template-eks-control-plane-bare-eks.yaml"
+        targetName: "cluster-template-eks-control-plane-bare-eks.yaml"
 
 variables:
   KUBERNETES_VERSION: "v1.31.0"
diff --git a/test/e2e/data/eks/cluster-template-eks-control-plane-bare-eks.yaml b/test/e2e/data/eks/cluster-template-eks-control-plane-bare-eks.yaml
new file mode 100644
index 000000000..b2cd153bb
--- /dev/null
+++ b/test/e2e/data/eks/cluster-template-eks-control-plane-bare-eks.yaml
@@ -0,0 +1,36 @@
+---
+apiVersion: cluster.x-k8s.io/v1beta1
+kind: Cluster
+metadata:
+  name: "${CLUSTER_NAME}"
+spec:
+  clusterNetwork:
+    pods:
+      cidrBlocks: ["192.168.0.0/16"]
+  infrastructureRef:
+    kind: AWSManagedCluster
+    apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
+    name: "${CLUSTER_NAME}"
+  controlPlaneRef:
+    kind: AWSManagedControlPlane
+    apiVersion: controlplane.cluster.x-k8s.io/v1beta2
+    name: "${CLUSTER_NAME}-control-plane"
+---
+kind: AWSManagedCluster
+apiVersion: infrastructure.cluster.x-k8s.io/v1beta2
+metadata:
+  name: "${CLUSTER_NAME}"
+spec: {}
+---
+kind: AWSManagedControlPlane
+apiVersion: controlplane.cluster.x-k8s.io/v1beta2
+metadata:
+  name: "${CLUSTER_NAME}-control-plane"
+spec:
+  region: "${AWS_REGION}"
+  sshKeyName: "${AWS_SSH_KEY_NAME}"
+  version: "${KUBERNETES_VERSION}"
+  identityRef:
+    kind: AWSClusterStaticIdentity
+    name: e2e-account
+  bootstrapSelfManagedAddons: false