From c622f188fc7f952bd74869ca190ca5802870079d Mon Sep 17 00:00:00 2001 From: Adam Malcontenti-Wilson Date: Tue, 27 Aug 2024 17:15:02 +1000 Subject: [PATCH 1/4] feat: support setting EKS AuthenticationMode --- Dockerfile | 3 +- Makefile | 9 +- ...ster.x-k8s.io_awsmanagedcontrolplanes.yaml | 32 ++++- .../v1beta1/awsmanagedcontrolplane_types.go | 13 ++ controlplane/eks/api/v1beta1/types.go | 15 +++ .../api/v1beta1/zz_generated.conversion.go | 98 ++++++++++---- .../eks/api/v1beta1/zz_generated.deepcopy.go | 20 +++ .../v1beta2/awsmanagedcontrolplane_types.go | 15 ++- .../v1beta2/awsmanagedcontrolplane_webhook.go | 23 ++++ .../awsmanagedcontrolplane_webhook_test.go | 90 +++++++++++++ controlplane/eks/api/v1beta2/types.go | 15 +++ .../eks/api/v1beta2/zz_generated.deepcopy.go | 93 ++------------ pkg/cloud/services/eks/cluster.go | 66 ++++++++++ pkg/cloud/services/eks/cluster_test.go | 120 ++++++++++++++++++ 14 files changed, 498 insertions(+), 114 deletions(-) diff --git a/Dockerfile b/Dockerfile index 155b9519a7..b28186a6c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,10 +41,11 @@ COPY ./ ./ ARG package=. ARG ARCH ARG LDFLAGS +ARG GCFLAGS RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.local/share/golang \ - CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -extldflags '-static'" -o manager ${package} + CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -gcflags "${GCFLAGS}" -ldflags "${LDFLAGS} -extldflags '-static'" -o manager ${package} ENTRYPOINT [ "/start.sh", "/workspace/manager" ] # Copy the controller-manager into a thin image diff --git a/Makefile b/Makefile index 34fc79c2ac..6ade541578 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,9 @@ RBAC_ROOT ?= $(MANIFEST_ROOT)/rbac # Allow overriding the imagePullPolicy PULL_POLICY ?= Always +# Allow overriding the GCFLAGS +GCFLAGS ?= + # Set build time variables including version details LDFLAGS := $(shell source ./hack/version.sh; version::ldflags) @@ -387,12 +390,12 @@ binaries: managers clusterawsadm ## Builds and installs all binaries .PHONY: clusterawsadm clusterawsadm: ## Build clusterawsadm binary - go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/clusterawsadm ./cmd/clusterawsadm + go build -gcflags "$(GCFLAGS)" -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/clusterawsadm ./cmd/clusterawsadm .PHONY: docker-build docker-build: docker-pull-prerequisites ## Build the docker image for controller-manager - docker build --build-arg ARCH=$(ARCH) --build-arg builder_image=$(GO_CONTAINER_IMAGE) --build-arg LDFLAGS="$(LDFLAGS)" . -t $(CORE_CONTROLLER_IMG)-$(ARCH):$(TAG) + docker build --build-arg ARCH=$(ARCH) --build-arg builder_image=$(GO_CONTAINER_IMAGE) --build-arg GCFLAGS="$(GCFLAGS)" --build-arg LDFLAGS="$(LDFLAGS)" . -t $(CORE_CONTROLLER_IMG)-$(ARCH):$(TAG) .PHONY: docker-build-all ## Build all the architecture docker images docker-build-all: $(addprefix docker-build-,$(ALL_ARCH)) @@ -411,7 +414,7 @@ managers: ## Alias for manager-aws-infrastructure .PHONY: manager-aws-infrastructure manager-aws-infrastructure: ## Build manager binary - CGO_ENABLED=0 GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -extldflags '-static'" -o $(BIN_DIR)/manager . + CGO_ENABLED=0 GOARCH=${ARCH} go build -gcflags "${GCFLAGS}" -ldflags "${LDFLAGS} -extldflags '-static'" -o $(BIN_DIR)/manager . ##@ test: 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 cb64c61c82..be082d33f6 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -67,6 +67,21 @@ spec: description: AWSManagedControlPlaneSpec defines the desired state of an Amazon EKS Cluster. properties: + accessConfig: + description: AccessConfig specifies the access configuration information + for the cluster + properties: + authenticationMode: + default: CONFIG_MAP + description: |- + AuthenticationMode specifies the desired authentication mode for the cluster + Defaults to CONFIG_MAP + enum: + - CONFIG_MAP + - API + - API_AND_CONFIG_MAP + type: string + type: object additionalTags: additionalProperties: type: string @@ -2245,6 +2260,21 @@ spec: description: AWSManagedControlPlaneSpec defines the desired state of an Amazon EKS Cluster. properties: + accessConfig: + description: AccessConfig specifies the access configuration information + for the cluster + properties: + authenticationMode: + default: CONFIG_MAP + description: |- + AuthenticationMode specifies the desired authentication mode for the cluster + Defaults to CONFIG_MAP + enum: + - CONFIG_MAP + - API + - API_AND_CONFIG_MAP + type: string + type: object additionalTags: additionalProperties: type: string @@ -3053,7 +3083,7 @@ spec: type: object oidcIdentityProviderConfig: description: |- - IdentityProviderconfig is used to specify the oidc provider config + OIDCIdentityProviderConfig is used to specify the oidc provider config to be attached with this eks cluster properties: clientId: diff --git a/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go b/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go index a965bef381..97398e4e03 100644 --- a/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go +++ b/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go @@ -165,6 +165,10 @@ type AWSManagedControlPlaneSpec struct { //nolint: maligned // +optional OIDCIdentityProviderConfig *OIDCIdentityProviderConfig `json:"oidcIdentityProviderConfig,omitempty"` + // AccessConfig specifies the access configuration information for the cluster + // +optional + AccessConfig *AccessConfig `json:"accessConfig,omitempty"` + // DisableVPCCNI indicates that the Amazon VPC CNI should be disabled. With EKS clusters the // Amazon VPC CNI is automatically installed into the cluster. For clusters where you want // to use an alternate CNI this option provides a way to specify that the Amazon VPC CNI @@ -212,6 +216,15 @@ type EndpointAccess struct { Private *bool `json:"private,omitempty"` } +// AccessConfig represents the access configuration information for the cluster +type AccessConfig struct { + // AuthenticationMode specifies the desired authentication mode for the cluster + // Defaults to CONFIG_MAP + // +kubebuilder:default=CONFIG_MAP + // +kubebuilder:validation:Enum=CONFIG_MAP;API;API_AND_CONFIG_MAP + AuthenticationMode EKSAuthenticationMode `json:"authenticationMode,omitempty"` +} + // EncryptionConfig specifies the encryption configuration for the EKS clsuter. type EncryptionConfig struct { // Provider specifies the ARN or alias of the CMK (in AWS KMS) diff --git a/controlplane/eks/api/v1beta1/types.go b/controlplane/eks/api/v1beta1/types.go index 4fa88aa0a5..233254b88c 100644 --- a/controlplane/eks/api/v1beta1/types.go +++ b/controlplane/eks/api/v1beta1/types.go @@ -79,6 +79,21 @@ var ( EKSTokenMethodAWSCli = EKSTokenMethod("aws-cli") ) +// EKSAuthenticationMode defines the authentication mode for the cluster +type EKSAuthenticationMode string + +var ( + // EKSAuthenticationModeConfigMap indicates that only `aws-auth` ConfigMap will be used for authentication + EKSAuthenticationModeConfigMap = EKSAuthenticationMode("CONFIG_MAP") + + // EKSAuthenticationModeAPI indicates that only AWS Access Entries will be used for authentication + EKSAuthenticationModeAPI = EKSAuthenticationMode("API") + + // EKSAuthenticationModeAPIAndConfigMap indicates that both `aws-auth` ConfigMap and AWS Access Entries will + // be used for authentication + EKSAuthenticationModeAPIAndConfigMap = EKSAuthenticationMode("API_AND_CONFIG_MAP") +) + var ( // DefaultEKSControlPlaneRole is the name of the default IAM role to use for the EKS control plane // if no other role is supplied in the spec and if iam role creation is not enabled. The default diff --git a/controlplane/eks/api/v1beta1/zz_generated.conversion.go b/controlplane/eks/api/v1beta1/zz_generated.conversion.go index 9fe8517b2f..f2c761dc97 100644 --- a/controlplane/eks/api/v1beta1/zz_generated.conversion.go +++ b/controlplane/eks/api/v1beta1/zz_generated.conversion.go @@ -1,5 +1,5 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated +//go:build !ignore_autogenerated_conversions +// +build !ignore_autogenerated_conversions /* Copyright The Kubernetes Authors. @@ -27,9 +27,10 @@ import ( v1 "k8s.io/api/core/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" + apiv1beta1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta1" apiv1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" v1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2" - apiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + clusterapiapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" ) func init() { @@ -59,23 +60,23 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*AWSManagedControlPlaneSpec)(nil), (*v1beta2.AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControlPlaneSpec(a.(*AWSManagedControlPlaneSpec), b.(*v1beta2.AWSManagedControlPlaneSpec), scope) + if err := s.AddGeneratedConversionFunc((*AWSManagedControlPlaneStatus)(nil), (*v1beta2.AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPlaneStatus(a.(*AWSManagedControlPlaneStatus), b.(*v1beta2.AWSManagedControlPlaneStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.AWSManagedControlPlaneSpec)(nil), (*AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlaneSpec(a.(*v1beta2.AWSManagedControlPlaneSpec), b.(*AWSManagedControlPlaneSpec), scope) + if err := s.AddGeneratedConversionFunc((*v1beta2.AWSManagedControlPlaneStatus)(nil), (*AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(a.(*v1beta2.AWSManagedControlPlaneStatus), b.(*AWSManagedControlPlaneStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*AWSManagedControlPlaneStatus)(nil), (*v1beta2.AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPlaneStatus(a.(*AWSManagedControlPlaneStatus), b.(*v1beta2.AWSManagedControlPlaneStatus), scope) + if err := s.AddGeneratedConversionFunc((*AccessConfig)(nil), (*v1beta2.AccessConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(a.(*AccessConfig), b.(*v1beta2.AccessConfig), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.AWSManagedControlPlaneStatus)(nil), (*AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(a.(*v1beta2.AWSManagedControlPlaneStatus), b.(*AWSManagedControlPlaneStatus), scope) + if err := s.AddGeneratedConversionFunc((*v1beta2.AccessConfig)(nil), (*AccessConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(a.(*v1beta2.AccessConfig), b.(*AccessConfig), scope) }); err != nil { return err } @@ -224,7 +225,37 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.VpcCni)(nil), (*VpcCni)(nil), func(a, b interface{}, scope conversion.Scope) error { + if err := s.AddConversionFunc((*AWSManagedControlPlaneSpec)(nil), (*v1beta2.AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControlPlaneSpec(a.(*AWSManagedControlPlaneSpec), b.(*v1beta2.AWSManagedControlPlaneSpec), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*apiv1beta1.Bastion)(nil), (*apiv1beta2.Bastion)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Bastion_To_v1beta2_Bastion(a.(*apiv1beta1.Bastion), b.(*apiv1beta2.Bastion), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*apiv1beta1.NetworkSpec)(nil), (*apiv1beta2.NetworkSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_NetworkSpec_To_v1beta2_NetworkSpec(a.(*apiv1beta1.NetworkSpec), b.(*apiv1beta2.NetworkSpec), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*apiv1beta1.NetworkStatus)(nil), (*apiv1beta2.NetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_NetworkStatus_To_v1beta2_NetworkStatus(a.(*apiv1beta1.NetworkStatus), b.(*apiv1beta2.NetworkStatus), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*v1beta2.AWSManagedControlPlaneSpec)(nil), (*AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlaneSpec(a.(*v1beta2.AWSManagedControlPlaneSpec), b.(*AWSManagedControlPlaneSpec), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*apiv1beta2.Bastion)(nil), (*apiv1beta1.Bastion)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_Bastion_To_v1beta1_Bastion(a.(*apiv1beta2.Bastion), b.(*apiv1beta1.Bastion), scope) + }); err != nil { + return err + } + if err := s.AddConversionFunc((*v1beta2.VpcCni)(nil), (*VpcCni)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_VpcCni_To_v1beta1_VpcCni(a.(*v1beta2.VpcCni), b.(*VpcCni), scope) }); err != nil { return err @@ -233,7 +264,6 @@ func RegisterConversions(s *runtime.Scheme) error { } func autoConvert_v1beta1_AWSManagedControlPlane_To_v1beta2_AWSManagedControlPlane(in *AWSManagedControlPlane, out *v1beta2.AWSManagedControlPlane, s conversion.Scope) error { - out.TypeMeta = in.TypeMeta out.ObjectMeta = in.ObjectMeta if err := Convert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControlPlaneSpec(&in.Spec, &out.Spec, s); err != nil { return err @@ -250,7 +280,6 @@ func Convert_v1beta1_AWSManagedControlPlane_To_v1beta2_AWSManagedControlPlane(in } func autoConvert_v1beta2_AWSManagedControlPlane_To_v1beta1_AWSManagedControlPlane(in *v1beta2.AWSManagedControlPlane, out *AWSManagedControlPlane, s conversion.Scope) error { - out.TypeMeta = in.TypeMeta out.ObjectMeta = in.ObjectMeta if err := Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlaneSpec(&in.Spec, &out.Spec, s); err != nil { return err @@ -267,7 +296,6 @@ func Convert_v1beta2_AWSManagedControlPlane_To_v1beta1_AWSManagedControlPlane(in } func autoConvert_v1beta1_AWSManagedControlPlaneList_To_v1beta2_AWSManagedControlPlaneList(in *AWSManagedControlPlaneList, out *v1beta2.AWSManagedControlPlaneList, s conversion.Scope) error { - out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { in, out := &in.Items, &out.Items @@ -289,7 +317,6 @@ func Convert_v1beta1_AWSManagedControlPlaneList_To_v1beta2_AWSManagedControlPlan } func autoConvert_v1beta2_AWSManagedControlPlaneList_To_v1beta1_AWSManagedControlPlaneList(in *v1beta2.AWSManagedControlPlaneList, out *AWSManagedControlPlaneList, s conversion.Scope) error { - out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { in, out := &in.Items, &out.Items @@ -336,6 +363,7 @@ func autoConvert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControl out.AssociateOIDCProvider = in.AssociateOIDCProvider out.Addons = (*[]v1beta2.Addon)(unsafe.Pointer(in.Addons)) out.OIDCIdentityProviderConfig = (*v1beta2.OIDCIdentityProviderConfig)(unsafe.Pointer(in.OIDCIdentityProviderConfig)) + out.AccessConfig = (*v1beta2.AccessConfig)(unsafe.Pointer(in.AccessConfig)) // WARNING: in.DisableVPCCNI requires manual conversion: does not exist in peer-type if err := Convert_v1beta1_VpcCni_To_v1beta2_VpcCni(&in.VpcCni, &out.VpcCni, s); err != nil { return err @@ -357,8 +385,6 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl out.Version = (*string)(unsafe.Pointer(in.Version)) out.RoleName = (*string)(unsafe.Pointer(in.RoleName)) out.RoleAdditionalPolicies = (*[]string)(unsafe.Pointer(in.RoleAdditionalPolicies)) - // WARNING: in.RolePath requires manual conversion: does not exist in peer-type - // WARNING: in.RolePermissionsBoundary requires manual conversion: does not exist in peer-type out.Logging = (*ControlPlaneLoggingSpec)(unsafe.Pointer(in.Logging)) out.EncryptionConfig = (*EncryptionConfig)(unsafe.Pointer(in.EncryptionConfig)) out.AdditionalTags = *(*apiv1beta2.Tags)(unsafe.Pointer(&in.AdditionalTags)) @@ -375,10 +401,10 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl out.AssociateOIDCProvider = in.AssociateOIDCProvider out.Addons = (*[]Addon)(unsafe.Pointer(in.Addons)) out.OIDCIdentityProviderConfig = (*OIDCIdentityProviderConfig)(unsafe.Pointer(in.OIDCIdentityProviderConfig)) + out.AccessConfig = (*AccessConfig)(unsafe.Pointer(in.AccessConfig)) 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 @@ -388,7 +414,7 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl func autoConvert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPlaneStatus(in *AWSManagedControlPlaneStatus, out *v1beta2.AWSManagedControlPlaneStatus, s conversion.Scope) error { out.Network = in.Network - out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) + out.FailureDomains = *(*clusterapiapiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) out.Bastion = (*apiv1beta2.Instance)(unsafe.Pointer(in.Bastion)) if err := Convert_v1beta1_OIDCProviderStatus_To_v1beta2_OIDCProviderStatus(&in.OIDCProvider, &out.OIDCProvider, s); err != nil { return err @@ -397,7 +423,7 @@ func autoConvert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedContr out.Initialized = in.Initialized out.Ready = in.Ready out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) - out.Conditions = *(*apiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) + out.Conditions = *(*clusterapiapiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) out.Addons = *(*[]v1beta2.AddonState)(unsafe.Pointer(&in.Addons)) if err := Convert_v1beta1_IdentityProviderStatus_To_v1beta2_IdentityProviderStatus(&in.IdentityProviderStatus, &out.IdentityProviderStatus, s); err != nil { return err @@ -412,7 +438,7 @@ func Convert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPl func autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in *v1beta2.AWSManagedControlPlaneStatus, out *AWSManagedControlPlaneStatus, s conversion.Scope) error { out.Network = in.Network - out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) + out.FailureDomains = *(*clusterapiapiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) out.Bastion = (*apiv1beta2.Instance)(unsafe.Pointer(in.Bastion)) if err := Convert_v1beta2_OIDCProviderStatus_To_v1beta1_OIDCProviderStatus(&in.OIDCProvider, &out.OIDCProvider, s); err != nil { return err @@ -421,15 +447,39 @@ func autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedContr out.Initialized = in.Initialized out.Ready = in.Ready out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) - out.Conditions = *(*apiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) + out.Conditions = *(*clusterapiapiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) out.Addons = *(*[]AddonState)(unsafe.Pointer(&in.Addons)) if err := Convert_v1beta2_IdentityProviderStatus_To_v1beta1_IdentityProviderStatus(&in.IdentityProviderStatus, &out.IdentityProviderStatus, s); err != nil { return err } - // WARNING: in.Version requires manual conversion: does not exist in peer-type return nil } +// Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus is an autogenerated conversion function. +func Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in *v1beta2.AWSManagedControlPlaneStatus, out *AWSManagedControlPlaneStatus, s conversion.Scope) error { + return autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in, out, s) +} + +func autoConvert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(in *AccessConfig, out *v1beta2.AccessConfig, s conversion.Scope) error { + out.AuthenticationMode = v1beta2.EKSAuthenticationMode(in.AuthenticationMode) + return nil +} + +// Convert_v1beta1_AccessConfig_To_v1beta2_AccessConfig is an autogenerated conversion function. +func Convert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(in *AccessConfig, out *v1beta2.AccessConfig, s conversion.Scope) error { + return autoConvert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(in, out, s) +} + +func autoConvert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(in *v1beta2.AccessConfig, out *AccessConfig, s conversion.Scope) error { + out.AuthenticationMode = EKSAuthenticationMode(in.AuthenticationMode) + return nil +} + +// Convert_v1beta2_AccessConfig_To_v1beta1_AccessConfig is an autogenerated conversion function. +func Convert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(in *v1beta2.AccessConfig, out *AccessConfig, s conversion.Scope) error { + return autoConvert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(in, out, s) +} + func autoConvert_v1beta1_Addon_To_v1beta2_Addon(in *Addon, out *v1beta2.Addon, s conversion.Scope) error { out.Name = in.Name out.Version = in.Version diff --git a/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go b/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go index f6db3b2da0..73056c1469 100644 --- a/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go +++ b/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go @@ -170,6 +170,11 @@ func (in *AWSManagedControlPlaneSpec) DeepCopyInto(out *AWSManagedControlPlaneSp *out = new(OIDCIdentityProviderConfig) (*in).DeepCopyInto(*out) } + if in.AccessConfig != nil { + in, out := &in.AccessConfig, &out.AccessConfig + *out = new(AccessConfig) + **out = **in + } in.VpcCni.DeepCopyInto(&out.VpcCni) out.KubeProxy = in.KubeProxy } @@ -238,6 +243,21 @@ func (in *AWSManagedControlPlaneStatus) DeepCopy() *AWSManagedControlPlaneStatus return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessConfig) DeepCopyInto(out *AccessConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfig. +func (in *AccessConfig) DeepCopy() *AccessConfig { + if in == nil { + return nil + } + out := new(AccessConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Addon) DeepCopyInto(out *Addon) { *out = *in diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go index a36b35dda3..4aa5c81ab6 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go @@ -187,11 +187,15 @@ type AWSManagedControlPlaneSpec struct { //nolint: maligned // +optional Addons *[]Addon `json:"addons,omitempty"` - // IdentityProviderconfig is used to specify the oidc provider config + // OIDCIdentityProviderConfig is used to specify the oidc provider config // to be attached with this eks cluster // +optional OIDCIdentityProviderConfig *OIDCIdentityProviderConfig `json:"oidcIdentityProviderConfig,omitempty"` + // AccessConfig specifies the access configuration information for the cluster + // +optional + AccessConfig *AccessConfig `json:"accessConfig,omitempty"` + // VpcCni is used to set configuration options for the VPC CNI plugin // +optional VpcCni VpcCni `json:"vpcCni,omitempty"` @@ -248,6 +252,15 @@ type EndpointAccess struct { Private *bool `json:"private,omitempty"` } +// AccessConfig represents the access configuration information for the cluster +type AccessConfig struct { + // AuthenticationMode specifies the desired authentication mode for the cluster + // Defaults to CONFIG_MAP + // +kubebuilder:default=CONFIG_MAP + // +kubebuilder:validation:Enum=CONFIG_MAP;API;API_AND_CONFIG_MAP + AuthenticationMode EKSAuthenticationMode `json:"authenticationMode,omitempty"` +} + // EncryptionConfig specifies the encryption configuration for the EKS clsuter. type EncryptionConfig struct { // Provider specifies the ARN or alias of the CMK (in AWS KMS) diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go index ffb4e891b8..d9efa45058 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go @@ -140,6 +140,7 @@ func (*awsManagedControlPlaneWebhook) ValidateUpdate(ctx context.Context, oldObj allErrs = append(allErrs, r.validateEKSClusterNameSame(oldAWSManagedControlplane)...) allErrs = append(allErrs, r.validateEKSVersion(oldAWSManagedControlplane)...) allErrs = append(allErrs, r.Spec.Bastion.Validate()...) + allErrs = append(allErrs, r.validateAccessConfig(oldAWSManagedControlplane)...) allErrs = append(allErrs, r.validateIAMAuthConfig()...) allErrs = append(allErrs, r.validateSecondaryCIDR()...) allErrs = append(allErrs, r.validateEKSAddons()...) @@ -318,6 +319,28 @@ func validateEKSAddons(eksVersion *string, networkSpec infrav1.NetworkSpec, addo return allErrs } +func (r *AWSManagedControlPlane) validateAccessConfig(old *AWSManagedControlPlane) field.ErrorList { + var allErrs field.ErrorList + + // If accessConfig is already set, do not allow removal of it. + if old.Spec.AccessConfig != nil && r.Spec.AccessConfig == nil { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "accessConfig"), r.Spec.AccessConfig, "removing AccessConfig is not allowed after it has been enabled"), + ) + } + + // AuthenticationMode is ratcheting - do not allow downgrades + if old.Spec.AccessConfig != nil && old.Spec.AccessConfig.AuthenticationMode != r.Spec.AccessConfig.AuthenticationMode && + ((old.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeAPIAndConfigMap && r.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeConfigMap) || + old.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeAPI) { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "accessConfig", "authenticationMode"), r.Spec.AccessConfig.AuthenticationMode, "downgrading authentication mode is not allowed after it has been enabled"), + ) + } + + return allErrs +} + func (r *AWSManagedControlPlane) validateIAMAuthConfig() field.ErrorList { return validateIAMAuthConfig(r.Spec.IAMAuthenticatorConfig, field.NewPath("spec.iamAuthenticatorConfig")) } diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go index 276faa5b09..5cab0ff5aa 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go @@ -603,6 +603,96 @@ func TestWebhookUpdate(t *testing.T) { }, expectError: false, }, + { + name: "no change in access config", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + }, + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + }, + }, + expectError: false, + }, + { + name: "change in access config to nil", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + }, + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + }, + expectError: true, + }, + { + name: "change in access config from nil to valid", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + }, + }, + expectError: false, + }, + { + name: "change in access config auth mode from ApiAndConfigMap to API is allowed", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeAPIAndConfigMap, + }, + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeAPI, + }, + }, + expectError: false, + }, + { + name: "change in access config auth mode from API to Config Map is denied", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeAPI, + }, + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + }, + }, + expectError: true, + }, + { + name: "change in access config auth mode from APIAndConfigMap to Config Map is denied", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeAPIAndConfigMap, + }, + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + }, + }, + expectError: true, + }, { name: "change in encryption config to nil", oldClusterSpec: AWSManagedControlPlaneSpec{ diff --git a/controlplane/eks/api/v1beta2/types.go b/controlplane/eks/api/v1beta2/types.go index 622e4b9c3d..71ee2ac45a 100644 --- a/controlplane/eks/api/v1beta2/types.go +++ b/controlplane/eks/api/v1beta2/types.go @@ -79,6 +79,21 @@ var ( EKSTokenMethodAWSCli = EKSTokenMethod("aws-cli") ) +// EKSAuthenticationMode defines the authentication mode for the cluster +type EKSAuthenticationMode string + +var ( + // EKSAuthenticationModeConfigMap indicates that only `aws-auth` ConfigMap will be used for authentication + EKSAuthenticationModeConfigMap = EKSAuthenticationMode("CONFIG_MAP") + + // EKSAuthenticationModeAPI indicates that only AWS Access Entries will be used for authentication + EKSAuthenticationModeAPI = EKSAuthenticationMode("API") + + // EKSAuthenticationModeAPIAndConfigMap indicates that both `aws-auth` ConfigMap and AWS Access Entries will + // be used for authentication + EKSAuthenticationModeAPIAndConfigMap = EKSAuthenticationMode("API_AND_CONFIG_MAP") +) + var ( // DefaultEKSControlPlaneRole is the name of the default IAM role to use for the EKS control plane // if no other role is supplied in the spec and if iam role creation is not enabled. The default diff --git a/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go b/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go index 807613dc0d..71c30d3e9e 100644 --- a/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go +++ b/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go @@ -170,6 +170,11 @@ func (in *AWSManagedControlPlaneSpec) DeepCopyInto(out *AWSManagedControlPlaneSp *out = new(OIDCIdentityProviderConfig) (*in).DeepCopyInto(*out) } + if in.AccessConfig != nil { + in, out := &in.AccessConfig, &out.AccessConfig + *out = new(AccessConfig) + **out = **in + } in.VpcCni.DeepCopyInto(&out.VpcCni) out.KubeProxy = in.KubeProxy } @@ -226,11 +231,6 @@ func (in *AWSManagedControlPlaneStatus) DeepCopyInto(out *AWSManagedControlPlane } } out.IdentityProviderStatus = in.IdentityProviderStatus - if in.Version != nil { - in, out := &in.Version, &out.Version - *out = new(string) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneStatus. @@ -244,91 +244,16 @@ func (in *AWSManagedControlPlaneStatus) DeepCopy() *AWSManagedControlPlaneStatus } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AWSManagedControlPlaneTemplate) DeepCopyInto(out *AWSManagedControlPlaneTemplate) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplate. -func (in *AWSManagedControlPlaneTemplate) DeepCopy() *AWSManagedControlPlaneTemplate { - if in == nil { - return nil - } - out := new(AWSManagedControlPlaneTemplate) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AWSManagedControlPlaneTemplate) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AWSManagedControlPlaneTemplateList) DeepCopyInto(out *AWSManagedControlPlaneTemplateList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]AWSManagedControlPlaneTemplate, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateList. -func (in *AWSManagedControlPlaneTemplateList) DeepCopy() *AWSManagedControlPlaneTemplateList { - if in == nil { - return nil - } - out := new(AWSManagedControlPlaneTemplateList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *AWSManagedControlPlaneTemplateList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AWSManagedControlPlaneTemplateResource) DeepCopyInto(out *AWSManagedControlPlaneTemplateResource) { - *out = *in - in.Spec.DeepCopyInto(&out.Spec) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateResource. -func (in *AWSManagedControlPlaneTemplateResource) DeepCopy() *AWSManagedControlPlaneTemplateResource { - if in == nil { - return nil - } - out := new(AWSManagedControlPlaneTemplateResource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AWSManagedControlPlaneTemplateSpec) DeepCopyInto(out *AWSManagedControlPlaneTemplateSpec) { +func (in *AccessConfig) DeepCopyInto(out *AccessConfig) { *out = *in - in.Template.DeepCopyInto(&out.Template) } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateSpec. -func (in *AWSManagedControlPlaneTemplateSpec) DeepCopy() *AWSManagedControlPlaneTemplateSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfig. +func (in *AccessConfig) DeepCopy() *AccessConfig { if in == nil { return nil } - out := new(AWSManagedControlPlaneTemplateSpec) + out := new(AccessConfig) in.DeepCopyInto(out) return out } diff --git a/pkg/cloud/services/eks/cluster.go b/pkg/cloud/services/eks/cluster.go index a16e6d1d34..f3a71be8a1 100644 --- a/pkg/cloud/services/eks/cluster.go +++ b/pkg/cloud/services/eks/cluster.go @@ -121,6 +121,10 @@ func (s *Service) reconcileCluster(ctx context.Context) error { return errors.Wrap(err, "failed reconciling cluster config") } + if err := s.reconcileAccessConfig(cluster.AccessConfig); err != nil { + return errors.Wrap(err, "failed reconciling access config") + } + if err := s.reconcileLogging(ctx, cluster.Logging); err != nil { return errors.Wrap(err, "failed reconciling logging") } @@ -422,6 +426,13 @@ func (s *Service) createCluster(ctx context.Context, eksClusterName string) (*ek return nil, errors.Wrap(err, "couldn't create vpc config for cluster") } + var accessConfig *ekstypes.CreateAccessConfigRequest + if s.scope.ControlPlane.Spec.AccessConfig != nil && s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode != "" { + accessConfig = &ekstypes.CreateAccessConfigRequest{ + AuthenticationMode: string(s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode), + } + } + var netConfig *ekstypes.KubernetesNetworkConfigRequest if s.scope.VPC().IsIPv6Enabled() { netConfig = &ekstypes.KubernetesNetworkConfigRequest{ @@ -465,6 +476,7 @@ func (s *Service) createCluster(ctx context.Context, eksClusterName string) (*ek Name: aws.String(eksClusterName), Version: eksVersion, Logging: logging, + AccessConfig: accessConfig, EncryptionConfig: encryptionConfigs, ResourcesVpcConfig: vpcConfig, RoleArn: role.Arn, @@ -473,6 +485,10 @@ func (s *Service) createCluster(ctx context.Context, eksClusterName string) (*ek BootstrapSelfManagedAddons: bootstrapAddon, } + if err := input.Validate(); err != nil { + return nil, errors.Wrap(err, "created invalid CreateClusterInput") + } + var out *eks.CreateClusterOutput if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { if out, err = s.EKSClient.CreateCluster(ctx, input); err != nil { @@ -542,6 +558,56 @@ func (s *Service) reconcileClusterConfig(ctx context.Context, cluster *ekstypes. return nil } +func (s *Service) reconcileAccessConfig(accessConfig *ekstypes.AccessConfigResponse) error { + input := &eks.UpdateClusterConfigInput{Name: aws.String(s.scope.KubernetesClusterName())} + + if s.scope.ControlPlane.Spec.AccessConfig == nil || s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode == "" { + return nil + } + + expectedAuthenticationMode := string(s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode) + s.scope.Debug("Reconciling EKS Access Config for cluster", "cluster-name", s.scope.KubernetesClusterName(), "expected", expectedAuthenticationMode, "current", accessConfig.AuthenticationMode) + if expectedAuthenticationMode != accessConfig.AuthenticationMode { + input.AccessConfig = &eks.UpdateAccessConfigRequest{ + AuthenticationMode: aws.String(expectedAuthenticationMode), + } + } + + if input.AccessConfig != nil { + if err := input.Validate(); err != nil { + return errors.Wrap(err, "created invalid UpdateClusterConfigInput") + } + + if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { + if _, err := s.EKSClient.UpdateClusterConfig(input); err != nil { + if aerr, ok := err.(awserr.Error); ok { + return false, aerr + } + return false, err + } + + // Wait until status transitions to UPDATING because there's a short + // window after UpdateClusterVersion returns where the cluster + // status is ACTIVE and the update would be tried again + if err := s.EKSClient.WaitUntilClusterUpdating( + &eks.DescribeClusterInput{Name: aws.String(s.scope.KubernetesClusterName())}, + request.WithWaiterLogger(&awslog{s.GetLogger()}), + ); err != nil { + return false, err + } + + conditions.MarkTrue(s.scope.ControlPlane, ekscontrolplanev1.EKSControlPlaneUpdatingCondition) + record.Eventf(s.scope.ControlPlane, "InitiatedUpdateEKSControlPlane", "Initiated auth config update for EKS control plane %s", s.scope.KubernetesClusterName()) + return true, nil + }); err != nil { + record.Warnf(s.scope.ControlPlane, "FailedUpdateEKSControlPlane", "Failed to update EKS control plane auth config: %v", err) + return errors.Wrapf(err, "failed to update EKS cluster") + } + } + + return nil +} + func (s *Service) reconcileLogging(ctx context.Context, logging *ekstypes.Logging) error { input := &eks.UpdateClusterConfigInput{Name: aws.String(s.scope.KubernetesClusterName())} diff --git a/pkg/cloud/services/eks/cluster_test.go b/pkg/cloud/services/eks/cluster_test.go index 7e397f329e..eee3afb990 100644 --- a/pkg/cloud/services/eks/cluster_test.go +++ b/pkg/cloud/services/eks/cluster_test.go @@ -21,6 +21,7 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/awserr" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/aws/aws-sdk-go-v2/service/iam" @@ -474,6 +475,123 @@ func TestReconcileClusterVersion(t *testing.T) { } } +func TestReconcileAccessConfig(t *testing.T) { + clusterName := "default.cluster" + tests := []struct { + name string + expect func(m *mock_eksiface.MockEKSAPIMockRecorder) + expectError bool + }{ + { + name: "no upgrade necessary", + expect: func(m *mock_eksiface.MockEKSAPIMockRecorder) { + m. + DescribeCluster(gomock.Eq(context.TODO()), gomock.AssignableToTypeOf(&eks.DescribeClusterInput{})). + Return(&eks.DescribeClusterOutput{ + Cluster: &ekstypes.Cluster{ + Name: aws.String("default.cluster"), + AccessConfig: &ekstypes.AccessConfigResponse{ + AuthenticationMode: string(ekstypes.AuthenticationModeApiAndConfigMap), + }, + }, + }, nil) + }, + expectError: false, + }, + { + name: "needs upgrade", + expect: func(m *mock_eksiface.MockEKSAPIMockRecorder) { + m. + DescribeCluster(gomock.Eq(context.TODO()), gomock.AssignableToTypeOf(&eks.DescribeClusterInput{})). + Return(&eks.DescribeClusterOutput{ + Cluster: &ekstypes.Cluster{ + Name: aws.String("default.cluster"), + AccessConfig: &ekstypes.AccessConfigResponse{ + AuthenticationMode: string(ekstypes.AuthenticationModeConfigMap), + }, + }, + }, nil) + m.WaitUntilClusterUpdating( + gomock.Eq(context.TODO()), + gomock.AssignableToTypeOf(&eks.DescribeClusterInput{}), + gomock.Any(), + ).Return(nil) + m. + UpdateClusterConfig(gomock.Eq(context.TODO()), gomock.AssignableToTypeOf(&eks.UpdateClusterConfigInput{})). + Return(&eks.UpdateClusterConfigOutput{}, nil) + }, + expectError: false, + }, + { + name: "api error", + expect: func(m *mock_eksiface.MockEKSAPIMockRecorder) { + m. + DescribeCluster(gomock.Eq(context.TODO()), gomock.AssignableToTypeOf(&eks.DescribeClusterInput{})). + Return(&eks.DescribeClusterOutput{ + Cluster: &ekstypes.Cluster{ + Name: aws.String("default.cluster"), + AccessConfig: &ekstypes.AccessConfigResponse{ + AuthenticationMode: string(ekstypes.AuthenticationModeApi), + }, + }, + }, nil) + m. + UpdateClusterConfig(gomock.Eq(context.TODO()), gomock.AssignableToTypeOf(&eks.UpdateClusterConfigInput{})). + Return(&eks.UpdateClusterConfigOutput{}, awserr.New(string(ekstypes.ErrCodeInvalidParameterException), "Unsupported authentication mode update", nil)) + }, + expectError: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := NewWithT(t) + + mockControl := gomock.NewController(t) + defer mockControl.Finish() + + eksMock := mock_eksiface.NewMockEKSAPI(mockControl) + + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + _ = ekscontrolplanev1.AddToScheme(scheme) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + scope, err := scope.NewManagedControlPlaneScope(scope.ManagedControlPlaneScopeParams{ + Client: client, + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: clusterName, + }, + }, + ControlPlane: &ekscontrolplanev1.AWSManagedControlPlane{ + Spec: ekscontrolplanev1.AWSManagedControlPlaneSpec{ + EKSClusterName: clusterName, + AccessConfig: &ekscontrolplanev1.AccessConfig{ + AuthenticationMode: ekscontrolplanev1.EKSAuthenticationModeApiAndConfigMap, + }, + }, + }, + }) + g.Expect(err).To(BeNil()) + + tc.expect(eksMock.EXPECT()) + s := NewService(scope) + s.EKSClient = eksMock + + cluster, err := s.describeEKSCluster(context.TODO(), clusterName) + g.Expect(err).To(BeNil()) + + err = s.reconcileAccessConfig(cluster.AccessConfig) + if tc.expectError { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(err).To(BeNil()) + }) + } +} + func TestCreateCluster(t *testing.T) { clusterName := "cluster.default" version := aws.String("1.24") @@ -753,6 +871,7 @@ func TestCreateIPv6Cluster(t *testing.T) { eksMock.EXPECT().CreateCluster(context.TODO(), &eks.CreateClusterInput{ Name: aws.String("cluster-name"), Version: aws.String("1.22"), + RoleArn: aws.String("arn:role"), EncryptionConfig: []ekstypes.EncryptionConfig{ { Provider: &ekstypes.Provider{ @@ -776,6 +895,7 @@ func TestCreateIPv6Cluster(t *testing.T) { RoleName: aws.String("arn-role"), }).Return(&iam.GetRoleOutput{ Role: &iamtypes.Role{ + Arn: aws.String("arn:role"), RoleName: aws.String("arn-role"), }, }, nil) From 17aebd74b9e7197ea3fe0eb0ccbc0082c0b8a900 Mon Sep 17 00:00:00 2001 From: Josh French Date: Tue, 1 Jul 2025 18:03:07 -0400 Subject: [PATCH 2/4] feat: support setting EKS AuthenticationMode --- Dockerfile | 3 +- Makefile | 9 +- ...ster.x-k8s.io_awsmanagedcontrolplanes.yaml | 34 ++-- ...8s.io_awsmanagedcontrolplanetemplates.yaml | 22 +++ .../v1beta1/awsmanagedcontrolplane_types.go | 13 -- controlplane/eks/api/v1beta1/conversion.go | 1 + controlplane/eks/api/v1beta1/types.go | 15 -- .../api/v1beta1/zz_generated.conversion.go | 99 +++------- .../eks/api/v1beta1/zz_generated.deepcopy.go | 20 -- .../v1beta2/awsmanagedcontrolplane_types.go | 14 +- .../v1beta2/awsmanagedcontrolplane_webhook.go | 32 +++- .../awsmanagedcontrolplane_webhook_test.go | 61 ++++++ controlplane/eks/api/v1beta2/types.go | 12 +- .../eks/api/v1beta2/zz_generated.deepcopy.go | 102 +++++++++- pkg/cloud/services/eks/cluster.go | 37 ++-- pkg/cloud/services/eks/cluster_test.go | 86 ++++++++- test/e2e/data/e2e_eks_conf.yaml | 4 + ...-template-eks-auth-api-and-config-map.yaml | 38 ++++ ...-template-eks-auth-bootstrap-disabled.yaml | 38 ++++ test/e2e/suites/managed/eks_auth_test.go | 180 ++++++++++++++++++ test/e2e/suites/managed/helpers.go | 17 ++ 21 files changed, 646 insertions(+), 191 deletions(-) create mode 100644 test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml create mode 100644 test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml create mode 100644 test/e2e/suites/managed/eks_auth_test.go diff --git a/Dockerfile b/Dockerfile index b28186a6c4..155b9519a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,11 +41,10 @@ COPY ./ ./ ARG package=. ARG ARCH ARG LDFLAGS -ARG GCFLAGS RUN --mount=type=cache,target=/root/.cache/go-build \ --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.local/share/golang \ - CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -gcflags "${GCFLAGS}" -ldflags "${LDFLAGS} -extldflags '-static'" -o manager ${package} + CGO_ENABLED=0 GOOS=linux GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -extldflags '-static'" -o manager ${package} ENTRYPOINT [ "/start.sh", "/workspace/manager" ] # Copy the controller-manager into a thin image diff --git a/Makefile b/Makefile index 6ade541578..34fc79c2ac 100644 --- a/Makefile +++ b/Makefile @@ -137,9 +137,6 @@ RBAC_ROOT ?= $(MANIFEST_ROOT)/rbac # Allow overriding the imagePullPolicy PULL_POLICY ?= Always -# Allow overriding the GCFLAGS -GCFLAGS ?= - # Set build time variables including version details LDFLAGS := $(shell source ./hack/version.sh; version::ldflags) @@ -390,12 +387,12 @@ binaries: managers clusterawsadm ## Builds and installs all binaries .PHONY: clusterawsadm clusterawsadm: ## Build clusterawsadm binary - go build -gcflags "$(GCFLAGS)" -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/clusterawsadm ./cmd/clusterawsadm + go build -ldflags "$(LDFLAGS)" -o $(BIN_DIR)/clusterawsadm ./cmd/clusterawsadm .PHONY: docker-build docker-build: docker-pull-prerequisites ## Build the docker image for controller-manager - docker build --build-arg ARCH=$(ARCH) --build-arg builder_image=$(GO_CONTAINER_IMAGE) --build-arg GCFLAGS="$(GCFLAGS)" --build-arg LDFLAGS="$(LDFLAGS)" . -t $(CORE_CONTROLLER_IMG)-$(ARCH):$(TAG) + docker build --build-arg ARCH=$(ARCH) --build-arg builder_image=$(GO_CONTAINER_IMAGE) --build-arg LDFLAGS="$(LDFLAGS)" . -t $(CORE_CONTROLLER_IMG)-$(ARCH):$(TAG) .PHONY: docker-build-all ## Build all the architecture docker images docker-build-all: $(addprefix docker-build-,$(ALL_ARCH)) @@ -414,7 +411,7 @@ managers: ## Alias for manager-aws-infrastructure .PHONY: manager-aws-infrastructure manager-aws-infrastructure: ## Build manager binary - CGO_ENABLED=0 GOARCH=${ARCH} go build -gcflags "${GCFLAGS}" -ldflags "${LDFLAGS} -extldflags '-static'" -o $(BIN_DIR)/manager . + CGO_ENABLED=0 GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -extldflags '-static'" -o $(BIN_DIR)/manager . ##@ test: 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 be082d33f6..6bc339e9e3 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanes.yaml @@ -67,21 +67,6 @@ spec: description: AWSManagedControlPlaneSpec defines the desired state of an Amazon EKS Cluster. properties: - accessConfig: - description: AccessConfig specifies the access configuration information - for the cluster - properties: - authenticationMode: - default: CONFIG_MAP - description: |- - AuthenticationMode specifies the desired authentication mode for the cluster - Defaults to CONFIG_MAP - enum: - - CONFIG_MAP - - API - - API_AND_CONFIG_MAP - type: string - type: object additionalTags: additionalProperties: type: string @@ -2265,15 +2250,22 @@ spec: for the cluster properties: authenticationMode: - default: CONFIG_MAP + default: config_map description: |- AuthenticationMode specifies the desired authentication mode for the cluster - Defaults to CONFIG_MAP + Defaults to config_map enum: - - CONFIG_MAP - - API - - API_AND_CONFIG_MAP + - config_map + - api + - api_and_config_map type: string + bootstrapClusterCreatorAdminPermissions: + default: true + description: |- + BootstrapClusterCreatorAdminPermissions grants cluster admin permissions + to the IAM identity creating the cluster. Only applied during creation, + ignored when updating existing clusters. Defaults to true. + type: boolean type: object additionalTags: additionalProperties: @@ -3083,7 +3075,7 @@ spec: type: object oidcIdentityProviderConfig: description: |- - OIDCIdentityProviderConfig is used to specify the oidc provider config + IdentityProviderconfig is used to specify the oidc provider config to be attached with this eks cluster properties: clientId: 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 7a0abb3cf8..ad5c56c54b 100644 --- a/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml +++ b/config/crd/bases/controlplane.cluster.x-k8s.io_awsmanagedcontrolplanetemplates.yaml @@ -53,6 +53,28 @@ spec: description: AWSManagedControlPlaneSpec defines the desired state of an Amazon EKS Cluster. properties: + accessConfig: + description: AccessConfig specifies the access configuration + information for the cluster + properties: + authenticationMode: + default: config_map + description: |- + AuthenticationMode specifies the desired authentication mode for the cluster + Defaults to config_map + enum: + - config_map + - api + - api_and_config_map + type: string + bootstrapClusterCreatorAdminPermissions: + default: true + description: |- + BootstrapClusterCreatorAdminPermissions grants cluster admin permissions + to the IAM identity creating the cluster. Only applied during creation, + ignored when updating existing clusters. Defaults to true. + type: boolean + type: object additionalTags: additionalProperties: type: string diff --git a/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go b/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go index 97398e4e03..a965bef381 100644 --- a/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go +++ b/controlplane/eks/api/v1beta1/awsmanagedcontrolplane_types.go @@ -165,10 +165,6 @@ type AWSManagedControlPlaneSpec struct { //nolint: maligned // +optional OIDCIdentityProviderConfig *OIDCIdentityProviderConfig `json:"oidcIdentityProviderConfig,omitempty"` - // AccessConfig specifies the access configuration information for the cluster - // +optional - AccessConfig *AccessConfig `json:"accessConfig,omitempty"` - // DisableVPCCNI indicates that the Amazon VPC CNI should be disabled. With EKS clusters the // Amazon VPC CNI is automatically installed into the cluster. For clusters where you want // to use an alternate CNI this option provides a way to specify that the Amazon VPC CNI @@ -216,15 +212,6 @@ type EndpointAccess struct { Private *bool `json:"private,omitempty"` } -// AccessConfig represents the access configuration information for the cluster -type AccessConfig struct { - // AuthenticationMode specifies the desired authentication mode for the cluster - // Defaults to CONFIG_MAP - // +kubebuilder:default=CONFIG_MAP - // +kubebuilder:validation:Enum=CONFIG_MAP;API;API_AND_CONFIG_MAP - AuthenticationMode EKSAuthenticationMode `json:"authenticationMode,omitempty"` -} - // EncryptionConfig specifies the encryption configuration for the EKS clsuter. type EncryptionConfig struct { // Provider specifies the ARN or alias of the CMK (in AWS KMS) diff --git a/controlplane/eks/api/v1beta1/conversion.go b/controlplane/eks/api/v1beta1/conversion.go index 4039b113d4..0985ef66d5 100644 --- a/controlplane/eks/api/v1beta1/conversion.go +++ b/controlplane/eks/api/v1beta1/conversion.go @@ -117,6 +117,7 @@ func (r *AWSManagedControlPlane) ConvertTo(dstRaw conversion.Hub) error { dst.Spec.Partition = restored.Spec.Partition dst.Spec.RestrictPrivateSubnets = restored.Spec.RestrictPrivateSubnets + dst.Spec.AccessConfig = restored.Spec.AccessConfig dst.Spec.RolePath = restored.Spec.RolePath dst.Spec.RolePermissionsBoundary = restored.Spec.RolePermissionsBoundary dst.Status.Version = restored.Status.Version diff --git a/controlplane/eks/api/v1beta1/types.go b/controlplane/eks/api/v1beta1/types.go index 233254b88c..4fa88aa0a5 100644 --- a/controlplane/eks/api/v1beta1/types.go +++ b/controlplane/eks/api/v1beta1/types.go @@ -79,21 +79,6 @@ var ( EKSTokenMethodAWSCli = EKSTokenMethod("aws-cli") ) -// EKSAuthenticationMode defines the authentication mode for the cluster -type EKSAuthenticationMode string - -var ( - // EKSAuthenticationModeConfigMap indicates that only `aws-auth` ConfigMap will be used for authentication - EKSAuthenticationModeConfigMap = EKSAuthenticationMode("CONFIG_MAP") - - // EKSAuthenticationModeAPI indicates that only AWS Access Entries will be used for authentication - EKSAuthenticationModeAPI = EKSAuthenticationMode("API") - - // EKSAuthenticationModeAPIAndConfigMap indicates that both `aws-auth` ConfigMap and AWS Access Entries will - // be used for authentication - EKSAuthenticationModeAPIAndConfigMap = EKSAuthenticationMode("API_AND_CONFIG_MAP") -) - var ( // DefaultEKSControlPlaneRole is the name of the default IAM role to use for the EKS control plane // if no other role is supplied in the spec and if iam role creation is not enabled. The default diff --git a/controlplane/eks/api/v1beta1/zz_generated.conversion.go b/controlplane/eks/api/v1beta1/zz_generated.conversion.go index f2c761dc97..48f326b2dc 100644 --- a/controlplane/eks/api/v1beta1/zz_generated.conversion.go +++ b/controlplane/eks/api/v1beta1/zz_generated.conversion.go @@ -1,5 +1,5 @@ -//go:build !ignore_autogenerated_conversions -// +build !ignore_autogenerated_conversions +//go:build !ignore_autogenerated +// +build !ignore_autogenerated /* Copyright The Kubernetes Authors. @@ -27,10 +27,9 @@ import ( v1 "k8s.io/api/core/v1" conversion "k8s.io/apimachinery/pkg/conversion" runtime "k8s.io/apimachinery/pkg/runtime" - apiv1beta1 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta1" apiv1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2" v1beta2 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2" - clusterapiapiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" + apiv1beta1 "sigs.k8s.io/cluster-api/api/v1beta1" ) func init() { @@ -60,23 +59,23 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*AWSManagedControlPlaneStatus)(nil), (*v1beta2.AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPlaneStatus(a.(*AWSManagedControlPlaneStatus), b.(*v1beta2.AWSManagedControlPlaneStatus), scope) + if err := s.AddGeneratedConversionFunc((*AWSManagedControlPlaneSpec)(nil), (*v1beta2.AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControlPlaneSpec(a.(*AWSManagedControlPlaneSpec), b.(*v1beta2.AWSManagedControlPlaneSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.AWSManagedControlPlaneStatus)(nil), (*AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(a.(*v1beta2.AWSManagedControlPlaneStatus), b.(*AWSManagedControlPlaneStatus), scope) + if err := s.AddGeneratedConversionFunc((*v1beta2.AWSManagedControlPlaneSpec)(nil), (*AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlaneSpec(a.(*v1beta2.AWSManagedControlPlaneSpec), b.(*AWSManagedControlPlaneSpec), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*AccessConfig)(nil), (*v1beta2.AccessConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(a.(*AccessConfig), b.(*v1beta2.AccessConfig), scope) + if err := s.AddGeneratedConversionFunc((*AWSManagedControlPlaneStatus)(nil), (*v1beta2.AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPlaneStatus(a.(*AWSManagedControlPlaneStatus), b.(*v1beta2.AWSManagedControlPlaneStatus), scope) }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*v1beta2.AccessConfig)(nil), (*AccessConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(a.(*v1beta2.AccessConfig), b.(*AccessConfig), scope) + if err := s.AddGeneratedConversionFunc((*v1beta2.AWSManagedControlPlaneStatus)(nil), (*AWSManagedControlPlaneStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(a.(*v1beta2.AWSManagedControlPlaneStatus), b.(*AWSManagedControlPlaneStatus), scope) }); err != nil { return err } @@ -225,37 +224,7 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddConversionFunc((*AWSManagedControlPlaneSpec)(nil), (*v1beta2.AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControlPlaneSpec(a.(*AWSManagedControlPlaneSpec), b.(*v1beta2.AWSManagedControlPlaneSpec), scope) - }); err != nil { - return err - } - if err := s.AddConversionFunc((*apiv1beta1.Bastion)(nil), (*apiv1beta2.Bastion)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_Bastion_To_v1beta2_Bastion(a.(*apiv1beta1.Bastion), b.(*apiv1beta2.Bastion), scope) - }); err != nil { - return err - } - if err := s.AddConversionFunc((*apiv1beta1.NetworkSpec)(nil), (*apiv1beta2.NetworkSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_NetworkSpec_To_v1beta2_NetworkSpec(a.(*apiv1beta1.NetworkSpec), b.(*apiv1beta2.NetworkSpec), scope) - }); err != nil { - return err - } - if err := s.AddConversionFunc((*apiv1beta1.NetworkStatus)(nil), (*apiv1beta2.NetworkStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta1_NetworkStatus_To_v1beta2_NetworkStatus(a.(*apiv1beta1.NetworkStatus), b.(*apiv1beta2.NetworkStatus), scope) - }); err != nil { - return err - } - if err := s.AddConversionFunc((*v1beta2.AWSManagedControlPlaneSpec)(nil), (*AWSManagedControlPlaneSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlaneSpec(a.(*v1beta2.AWSManagedControlPlaneSpec), b.(*AWSManagedControlPlaneSpec), scope) - }); err != nil { - return err - } - if err := s.AddConversionFunc((*apiv1beta2.Bastion)(nil), (*apiv1beta1.Bastion)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_v1beta2_Bastion_To_v1beta1_Bastion(a.(*apiv1beta2.Bastion), b.(*apiv1beta1.Bastion), scope) - }); err != nil { - return err - } - if err := s.AddConversionFunc((*v1beta2.VpcCni)(nil), (*VpcCni)(nil), func(a, b interface{}, scope conversion.Scope) error { + if err := s.AddGeneratedConversionFunc((*v1beta2.VpcCni)(nil), (*VpcCni)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta2_VpcCni_To_v1beta1_VpcCni(a.(*v1beta2.VpcCni), b.(*VpcCni), scope) }); err != nil { return err @@ -264,6 +233,7 @@ func RegisterConversions(s *runtime.Scheme) error { } func autoConvert_v1beta1_AWSManagedControlPlane_To_v1beta2_AWSManagedControlPlane(in *AWSManagedControlPlane, out *v1beta2.AWSManagedControlPlane, s conversion.Scope) error { + out.TypeMeta = in.TypeMeta out.ObjectMeta = in.ObjectMeta if err := Convert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControlPlaneSpec(&in.Spec, &out.Spec, s); err != nil { return err @@ -280,6 +250,7 @@ func Convert_v1beta1_AWSManagedControlPlane_To_v1beta2_AWSManagedControlPlane(in } func autoConvert_v1beta2_AWSManagedControlPlane_To_v1beta1_AWSManagedControlPlane(in *v1beta2.AWSManagedControlPlane, out *AWSManagedControlPlane, s conversion.Scope) error { + out.TypeMeta = in.TypeMeta out.ObjectMeta = in.ObjectMeta if err := Convert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControlPlaneSpec(&in.Spec, &out.Spec, s); err != nil { return err @@ -296,6 +267,7 @@ func Convert_v1beta2_AWSManagedControlPlane_To_v1beta1_AWSManagedControlPlane(in } func autoConvert_v1beta1_AWSManagedControlPlaneList_To_v1beta2_AWSManagedControlPlaneList(in *AWSManagedControlPlaneList, out *v1beta2.AWSManagedControlPlaneList, s conversion.Scope) error { + out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { in, out := &in.Items, &out.Items @@ -317,6 +289,7 @@ func Convert_v1beta1_AWSManagedControlPlaneList_To_v1beta2_AWSManagedControlPlan } func autoConvert_v1beta2_AWSManagedControlPlaneList_To_v1beta1_AWSManagedControlPlaneList(in *v1beta2.AWSManagedControlPlaneList, out *AWSManagedControlPlaneList, s conversion.Scope) error { + out.TypeMeta = in.TypeMeta out.ListMeta = in.ListMeta if in.Items != nil { in, out := &in.Items, &out.Items @@ -363,7 +336,6 @@ func autoConvert_v1beta1_AWSManagedControlPlaneSpec_To_v1beta2_AWSManagedControl out.AssociateOIDCProvider = in.AssociateOIDCProvider out.Addons = (*[]v1beta2.Addon)(unsafe.Pointer(in.Addons)) out.OIDCIdentityProviderConfig = (*v1beta2.OIDCIdentityProviderConfig)(unsafe.Pointer(in.OIDCIdentityProviderConfig)) - out.AccessConfig = (*v1beta2.AccessConfig)(unsafe.Pointer(in.AccessConfig)) // WARNING: in.DisableVPCCNI requires manual conversion: does not exist in peer-type if err := Convert_v1beta1_VpcCni_To_v1beta2_VpcCni(&in.VpcCni, &out.VpcCni, s); err != nil { return err @@ -385,6 +357,8 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl out.Version = (*string)(unsafe.Pointer(in.Version)) out.RoleName = (*string)(unsafe.Pointer(in.RoleName)) out.RoleAdditionalPolicies = (*[]string)(unsafe.Pointer(in.RoleAdditionalPolicies)) + // WARNING: in.RolePath requires manual conversion: does not exist in peer-type + // WARNING: in.RolePermissionsBoundary requires manual conversion: does not exist in peer-type out.Logging = (*ControlPlaneLoggingSpec)(unsafe.Pointer(in.Logging)) out.EncryptionConfig = (*EncryptionConfig)(unsafe.Pointer(in.EncryptionConfig)) out.AdditionalTags = *(*apiv1beta2.Tags)(unsafe.Pointer(&in.AdditionalTags)) @@ -401,10 +375,11 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl out.AssociateOIDCProvider = in.AssociateOIDCProvider out.Addons = (*[]Addon)(unsafe.Pointer(in.Addons)) out.OIDCIdentityProviderConfig = (*OIDCIdentityProviderConfig)(unsafe.Pointer(in.OIDCIdentityProviderConfig)) - out.AccessConfig = (*AccessConfig)(unsafe.Pointer(in.AccessConfig)) + // WARNING: in.AccessConfig requires manual conversion: does not exist in peer-type 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 @@ -414,7 +389,7 @@ func autoConvert_v1beta2_AWSManagedControlPlaneSpec_To_v1beta1_AWSManagedControl func autoConvert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPlaneStatus(in *AWSManagedControlPlaneStatus, out *v1beta2.AWSManagedControlPlaneStatus, s conversion.Scope) error { out.Network = in.Network - out.FailureDomains = *(*clusterapiapiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) + out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) out.Bastion = (*apiv1beta2.Instance)(unsafe.Pointer(in.Bastion)) if err := Convert_v1beta1_OIDCProviderStatus_To_v1beta2_OIDCProviderStatus(&in.OIDCProvider, &out.OIDCProvider, s); err != nil { return err @@ -423,7 +398,7 @@ func autoConvert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedContr out.Initialized = in.Initialized out.Ready = in.Ready out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) - out.Conditions = *(*clusterapiapiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) + out.Conditions = *(*apiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) out.Addons = *(*[]v1beta2.AddonState)(unsafe.Pointer(&in.Addons)) if err := Convert_v1beta1_IdentityProviderStatus_To_v1beta2_IdentityProviderStatus(&in.IdentityProviderStatus, &out.IdentityProviderStatus, s); err != nil { return err @@ -438,7 +413,7 @@ func Convert_v1beta1_AWSManagedControlPlaneStatus_To_v1beta2_AWSManagedControlPl func autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in *v1beta2.AWSManagedControlPlaneStatus, out *AWSManagedControlPlaneStatus, s conversion.Scope) error { out.Network = in.Network - out.FailureDomains = *(*clusterapiapiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) + out.FailureDomains = *(*apiv1beta1.FailureDomains)(unsafe.Pointer(&in.FailureDomains)) out.Bastion = (*apiv1beta2.Instance)(unsafe.Pointer(in.Bastion)) if err := Convert_v1beta2_OIDCProviderStatus_To_v1beta1_OIDCProviderStatus(&in.OIDCProvider, &out.OIDCProvider, s); err != nil { return err @@ -447,39 +422,15 @@ func autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedContr out.Initialized = in.Initialized out.Ready = in.Ready out.FailureMessage = (*string)(unsafe.Pointer(in.FailureMessage)) - out.Conditions = *(*clusterapiapiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) + out.Conditions = *(*apiv1beta1.Conditions)(unsafe.Pointer(&in.Conditions)) out.Addons = *(*[]AddonState)(unsafe.Pointer(&in.Addons)) if err := Convert_v1beta2_IdentityProviderStatus_To_v1beta1_IdentityProviderStatus(&in.IdentityProviderStatus, &out.IdentityProviderStatus, s); err != nil { return err } + // WARNING: in.Version requires manual conversion: does not exist in peer-type return nil } -// Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus is an autogenerated conversion function. -func Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in *v1beta2.AWSManagedControlPlaneStatus, out *AWSManagedControlPlaneStatus, s conversion.Scope) error { - return autoConvert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPlaneStatus(in, out, s) -} - -func autoConvert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(in *AccessConfig, out *v1beta2.AccessConfig, s conversion.Scope) error { - out.AuthenticationMode = v1beta2.EKSAuthenticationMode(in.AuthenticationMode) - return nil -} - -// Convert_v1beta1_AccessConfig_To_v1beta2_AccessConfig is an autogenerated conversion function. -func Convert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(in *AccessConfig, out *v1beta2.AccessConfig, s conversion.Scope) error { - return autoConvert_v1beta1_AccessConfig_To_v1beta2_AccessConfig(in, out, s) -} - -func autoConvert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(in *v1beta2.AccessConfig, out *AccessConfig, s conversion.Scope) error { - out.AuthenticationMode = EKSAuthenticationMode(in.AuthenticationMode) - return nil -} - -// Convert_v1beta2_AccessConfig_To_v1beta1_AccessConfig is an autogenerated conversion function. -func Convert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(in *v1beta2.AccessConfig, out *AccessConfig, s conversion.Scope) error { - return autoConvert_v1beta2_AccessConfig_To_v1beta1_AccessConfig(in, out, s) -} - func autoConvert_v1beta1_Addon_To_v1beta2_Addon(in *Addon, out *v1beta2.Addon, s conversion.Scope) error { out.Name = in.Name out.Version = in.Version diff --git a/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go b/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go index 73056c1469..f6db3b2da0 100644 --- a/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go +++ b/controlplane/eks/api/v1beta1/zz_generated.deepcopy.go @@ -170,11 +170,6 @@ func (in *AWSManagedControlPlaneSpec) DeepCopyInto(out *AWSManagedControlPlaneSp *out = new(OIDCIdentityProviderConfig) (*in).DeepCopyInto(*out) } - if in.AccessConfig != nil { - in, out := &in.AccessConfig, &out.AccessConfig - *out = new(AccessConfig) - **out = **in - } in.VpcCni.DeepCopyInto(&out.VpcCni) out.KubeProxy = in.KubeProxy } @@ -243,21 +238,6 @@ func (in *AWSManagedControlPlaneStatus) DeepCopy() *AWSManagedControlPlaneStatus return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *AccessConfig) DeepCopyInto(out *AccessConfig) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfig. -func (in *AccessConfig) DeepCopy() *AccessConfig { - if in == nil { - return nil - } - out := new(AccessConfig) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Addon) DeepCopyInto(out *Addon) { *out = *in diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go index 4aa5c81ab6..9112863e35 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go @@ -187,7 +187,7 @@ type AWSManagedControlPlaneSpec struct { //nolint: maligned // +optional Addons *[]Addon `json:"addons,omitempty"` - // OIDCIdentityProviderConfig is used to specify the oidc provider config + // IdentityProviderconfig is used to specify the oidc provider config // to be attached with this eks cluster // +optional OIDCIdentityProviderConfig *OIDCIdentityProviderConfig `json:"oidcIdentityProviderConfig,omitempty"` @@ -255,10 +255,16 @@ type EndpointAccess struct { // AccessConfig represents the access configuration information for the cluster type AccessConfig struct { // AuthenticationMode specifies the desired authentication mode for the cluster - // Defaults to CONFIG_MAP - // +kubebuilder:default=CONFIG_MAP - // +kubebuilder:validation:Enum=CONFIG_MAP;API;API_AND_CONFIG_MAP + // Defaults to config_map + // +kubebuilder:default=config_map + // +kubebuilder:validation:Enum=config_map;api;api_and_config_map AuthenticationMode EKSAuthenticationMode `json:"authenticationMode,omitempty"` + + // BootstrapClusterCreatorAdminPermissions grants cluster admin permissions + // to the IAM identity creating the cluster. Only applied during creation, + // ignored when updating existing clusters. Defaults to true. + // +kubebuilder:default=true + BootstrapClusterCreatorAdminPermissions *bool `json:"bootstrapClusterCreatorAdminPermissions,omitempty"` } // EncryptionConfig specifies the encryption configuration for the EKS clsuter. diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go index d9efa45058..5554eff7c1 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go @@ -107,6 +107,7 @@ func (*awsManagedControlPlaneWebhook) ValidateCreate(_ context.Context, obj runt allErrs = append(allErrs, r.Spec.AdditionalTags.Validate()...) allErrs = append(allErrs, r.validateNetwork()...) allErrs = append(allErrs, r.validatePrivateDNSHostnameTypeOnLaunch()...) + allErrs = append(allErrs, r.validateAccessConfigCreate()...) if len(allErrs) == 0 { return nil, nil @@ -140,7 +141,7 @@ func (*awsManagedControlPlaneWebhook) ValidateUpdate(ctx context.Context, oldObj allErrs = append(allErrs, r.validateEKSClusterNameSame(oldAWSManagedControlplane)...) allErrs = append(allErrs, r.validateEKSVersion(oldAWSManagedControlplane)...) allErrs = append(allErrs, r.Spec.Bastion.Validate()...) - allErrs = append(allErrs, r.validateAccessConfig(oldAWSManagedControlplane)...) + allErrs = append(allErrs, r.validateAccessConfigUpdate(oldAWSManagedControlplane)...) allErrs = append(allErrs, r.validateIAMAuthConfig()...) allErrs = append(allErrs, r.validateSecondaryCIDR()...) allErrs = append(allErrs, r.validateEKSAddons()...) @@ -319,7 +320,7 @@ func validateEKSAddons(eksVersion *string, networkSpec infrav1.NetworkSpec, addo return allErrs } -func (r *AWSManagedControlPlane) validateAccessConfig(old *AWSManagedControlPlane) field.ErrorList { +func (r *AWSManagedControlPlane) validateAccessConfigUpdate(old *AWSManagedControlPlane) field.ErrorList { var allErrs field.ErrorList // If accessConfig is already set, do not allow removal of it. @@ -330,7 +331,8 @@ func (r *AWSManagedControlPlane) validateAccessConfig(old *AWSManagedControlPlan } // AuthenticationMode is ratcheting - do not allow downgrades - if old.Spec.AccessConfig != nil && old.Spec.AccessConfig.AuthenticationMode != r.Spec.AccessConfig.AuthenticationMode && + if old.Spec.AccessConfig != nil && r.Spec.AccessConfig != nil && + old.Spec.AccessConfig.AuthenticationMode != r.Spec.AccessConfig.AuthenticationMode && ((old.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeAPIAndConfigMap && r.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeConfigMap) || old.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeAPI) { allErrs = append(allErrs, @@ -338,6 +340,30 @@ func (r *AWSManagedControlPlane) validateAccessConfig(old *AWSManagedControlPlan ) } + // BootstrapClusterCreatorAdminPermissions only applies on create, but changes should not invalidate updates + if old.Spec.AccessConfig != nil && r.Spec.AccessConfig != nil && + old.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions != r.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions { + mcpLog.Info("Ignoring changes to BootstrapClusterCreatorAdminPermissions on cluster update", "old", old.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions, "new", r.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions) + } + + return allErrs +} + +func (r *AWSManagedControlPlane) validateAccessConfigCreate() field.ErrorList { + var allErrs field.ErrorList + + if r.Spec.AccessConfig != nil { + if r.Spec.AccessConfig.AuthenticationMode == EKSAuthenticationModeConfigMap && + r.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions != nil && + !*r.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions { + allErrs = append(allErrs, + field.Invalid(field.NewPath("spec", "accessConfig", "bootstrapClusterCreatorAdminPermissions"), + *r.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions, + "bootstrapClusterCreatorAdminPermissions must be true if cluster authentication mode is set to config_map"), + ) + } + } + return allErrs } diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go index 5cab0ff5aa..b23218886d 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go @@ -179,6 +179,7 @@ func TestWebhookCreate(t *testing.T) { secondaryCidr *string secondaryCidrBlocks []infrav1.VpcCidrBlock kubeProxy KubeProxy + accessConfig *AccessConfig }{ { name: "ekscluster specified", @@ -322,6 +323,47 @@ func TestWebhookCreate(t *testing.T) { Disable: true, }, }, + { + name: "BootstrapClusterCreatorAdminPermissions true with EKSAuthenticationModeConfigMap", + eksClusterName: "default_cluster1", + eksVersion: "v1.19", + expectError: false, + accessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + BootstrapClusterCreatorAdminPermissions: ptr.To(true), + }, + }, + { + name: "BootstrapClusterCreatorAdminPermissions false with EKSAuthenticationModeConfigMap", + eksClusterName: "default_cluster1", + eksVersion: "v1.19", + expectError: true, + expectErrorToContain: "bootstrapClusterCreatorAdminPermissions must be true if cluster authentication mode is set to CONFIG_MAP", + accessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeConfigMap, + BootstrapClusterCreatorAdminPermissions: ptr.To(false), + }, + }, + { + name: "BootstrapClusterCreatorAdminPermissions false with EKSAuthenticationModeAPIAndConfigMap", + eksClusterName: "default_cluster1", + eksVersion: "v1.19", + expectError: false, + accessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeAPIAndConfigMap, + BootstrapClusterCreatorAdminPermissions: ptr.To(false), + }, + }, + { + name: "BootstrapClusterCreatorAdminPermissions false with EKSAuthenticationModeAPI", + eksClusterName: "default_cluster1", + eksVersion: "v1.19", + expectError: false, + accessConfig: &AccessConfig{ + AuthenticationMode: EKSAuthenticationModeAPI, + BootstrapClusterCreatorAdminPermissions: ptr.To(false), + }, + }, } for _, tc := range tests { @@ -365,6 +407,9 @@ func TestWebhookCreate(t *testing.T) { if tc.secondaryCidr != nil { mcp.Spec.SecondaryCidrBlock = tc.secondaryCidr } + if tc.accessConfig != nil { + mcp.Spec.AccessConfig = tc.accessConfig + } err := testEnv.Create(ctx, mcp) @@ -693,6 +738,22 @@ func TestWebhookUpdate(t *testing.T) { }, expectError: true, }, + { + name: "change in access config bootstrap admin permissions is ignored", + oldClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + BootstrapClusterCreatorAdminPermissions: ptr.To(true), + }, + }, + newClusterSpec: AWSManagedControlPlaneSpec{ + EKSClusterName: "default_cluster1", + AccessConfig: &AccessConfig{ + BootstrapClusterCreatorAdminPermissions: ptr.To(false), + }, + }, + expectError: false, + }, { name: "change in encryption config to nil", oldClusterSpec: AWSManagedControlPlaneSpec{ diff --git a/controlplane/eks/api/v1beta2/types.go b/controlplane/eks/api/v1beta2/types.go index 71ee2ac45a..79f58f8e77 100644 --- a/controlplane/eks/api/v1beta2/types.go +++ b/controlplane/eks/api/v1beta2/types.go @@ -18,6 +18,7 @@ package v1beta2 import ( "fmt" + "strings" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -82,16 +83,21 @@ var ( // EKSAuthenticationMode defines the authentication mode for the cluster type EKSAuthenticationMode string +// APIValue returns the corresponding EKS API value for the authentication mode +func (e EKSAuthenticationMode) APIValue() ekstypes.AuthenticationMode { + return ekstypes.AuthenticationMode(strings.ToUpper(string(e))) +} + var ( // EKSAuthenticationModeConfigMap indicates that only `aws-auth` ConfigMap will be used for authentication - EKSAuthenticationModeConfigMap = EKSAuthenticationMode("CONFIG_MAP") + EKSAuthenticationModeConfigMap = EKSAuthenticationMode("config_map") // EKSAuthenticationModeAPI indicates that only AWS Access Entries will be used for authentication - EKSAuthenticationModeAPI = EKSAuthenticationMode("API") + EKSAuthenticationModeAPI = EKSAuthenticationMode("api") // EKSAuthenticationModeAPIAndConfigMap indicates that both `aws-auth` ConfigMap and AWS Access Entries will // be used for authentication - EKSAuthenticationModeAPIAndConfigMap = EKSAuthenticationMode("API_AND_CONFIG_MAP") + EKSAuthenticationModeAPIAndConfigMap = EKSAuthenticationMode("api_and_config_map") ) var ( diff --git a/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go b/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go index 71c30d3e9e..678a641e9c 100644 --- a/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go +++ b/controlplane/eks/api/v1beta2/zz_generated.deepcopy.go @@ -173,7 +173,7 @@ func (in *AWSManagedControlPlaneSpec) DeepCopyInto(out *AWSManagedControlPlaneSp if in.AccessConfig != nil { in, out := &in.AccessConfig, &out.AccessConfig *out = new(AccessConfig) - **out = **in + (*in).DeepCopyInto(*out) } in.VpcCni.DeepCopyInto(&out.VpcCni) out.KubeProxy = in.KubeProxy @@ -231,6 +231,11 @@ func (in *AWSManagedControlPlaneStatus) DeepCopyInto(out *AWSManagedControlPlane } } out.IdentityProviderStatus = in.IdentityProviderStatus + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneStatus. @@ -243,9 +248,104 @@ func (in *AWSManagedControlPlaneStatus) DeepCopy() *AWSManagedControlPlaneStatus return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedControlPlaneTemplate) DeepCopyInto(out *AWSManagedControlPlaneTemplate) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplate. +func (in *AWSManagedControlPlaneTemplate) DeepCopy() *AWSManagedControlPlaneTemplate { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedControlPlaneTemplate) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedControlPlaneTemplateList) DeepCopyInto(out *AWSManagedControlPlaneTemplateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AWSManagedControlPlaneTemplate, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateList. +func (in *AWSManagedControlPlaneTemplateList) DeepCopy() *AWSManagedControlPlaneTemplateList { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AWSManagedControlPlaneTemplateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedControlPlaneTemplateResource) DeepCopyInto(out *AWSManagedControlPlaneTemplateResource) { + *out = *in + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateResource. +func (in *AWSManagedControlPlaneTemplateResource) DeepCopy() *AWSManagedControlPlaneTemplateResource { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplateResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AWSManagedControlPlaneTemplateSpec) DeepCopyInto(out *AWSManagedControlPlaneTemplateSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AWSManagedControlPlaneTemplateSpec. +func (in *AWSManagedControlPlaneTemplateSpec) DeepCopy() *AWSManagedControlPlaneTemplateSpec { + if in == nil { + return nil + } + out := new(AWSManagedControlPlaneTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AccessConfig) DeepCopyInto(out *AccessConfig) { *out = *in + if in.BootstrapClusterCreatorAdminPermissions != nil { + in, out := &in.BootstrapClusterCreatorAdminPermissions, &out.BootstrapClusterCreatorAdminPermissions + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessConfig. diff --git a/pkg/cloud/services/eks/cluster.go b/pkg/cloud/services/eks/cluster.go index f3a71be8a1..b1b480e0b2 100644 --- a/pkg/cloud/services/eks/cluster.go +++ b/pkg/cloud/services/eks/cluster.go @@ -121,7 +121,7 @@ func (s *Service) reconcileCluster(ctx context.Context) error { return errors.Wrap(err, "failed reconciling cluster config") } - if err := s.reconcileAccessConfig(cluster.AccessConfig); err != nil { + if err := s.reconcileAccessConfig(ctx, cluster.AccessConfig); err != nil { return errors.Wrap(err, "failed reconciling access config") } @@ -429,10 +429,17 @@ func (s *Service) createCluster(ctx context.Context, eksClusterName string) (*ek var accessConfig *ekstypes.CreateAccessConfigRequest if s.scope.ControlPlane.Spec.AccessConfig != nil && s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode != "" { accessConfig = &ekstypes.CreateAccessConfigRequest{ - AuthenticationMode: string(s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode), + AuthenticationMode: s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode.APIValue(), } } + if s.scope.ControlPlane.Spec.AccessConfig != nil && s.scope.ControlPlane.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions != nil { + if accessConfig == nil { + accessConfig = &ekstypes.CreateAccessConfigRequest{} + } + accessConfig.BootstrapClusterCreatorAdminPermissions = s.scope.ControlPlane.Spec.AccessConfig.BootstrapClusterCreatorAdminPermissions + } + var netConfig *ekstypes.KubernetesNetworkConfigRequest if s.scope.VPC().IsIPv6Enabled() { netConfig = &ekstypes.KubernetesNetworkConfigRequest{ @@ -485,10 +492,6 @@ func (s *Service) createCluster(ctx context.Context, eksClusterName string) (*ek BootstrapSelfManagedAddons: bootstrapAddon, } - if err := input.Validate(); err != nil { - return nil, errors.Wrap(err, "created invalid CreateClusterInput") - } - var out *eks.CreateClusterOutput if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { if out, err = s.EKSClient.CreateCluster(ctx, input); err != nil { @@ -558,40 +561,34 @@ func (s *Service) reconcileClusterConfig(ctx context.Context, cluster *ekstypes. return nil } -func (s *Service) reconcileAccessConfig(accessConfig *ekstypes.AccessConfigResponse) error { +func (s *Service) reconcileAccessConfig(ctx context.Context, accessConfig *ekstypes.AccessConfigResponse) error { input := &eks.UpdateClusterConfigInput{Name: aws.String(s.scope.KubernetesClusterName())} if s.scope.ControlPlane.Spec.AccessConfig == nil || s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode == "" { return nil } - expectedAuthenticationMode := string(s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode) + expectedAuthenticationMode := s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode.APIValue() s.scope.Debug("Reconciling EKS Access Config for cluster", "cluster-name", s.scope.KubernetesClusterName(), "expected", expectedAuthenticationMode, "current", accessConfig.AuthenticationMode) if expectedAuthenticationMode != accessConfig.AuthenticationMode { - input.AccessConfig = &eks.UpdateAccessConfigRequest{ - AuthenticationMode: aws.String(expectedAuthenticationMode), + input.AccessConfig = &ekstypes.UpdateAccessConfigRequest{ + AuthenticationMode: expectedAuthenticationMode, } } if input.AccessConfig != nil { - if err := input.Validate(); err != nil { - return errors.Wrap(err, "created invalid UpdateClusterConfigInput") - } - if err := wait.WaitForWithRetryable(wait.NewBackoff(), func() (bool, error) { - if _, err := s.EKSClient.UpdateClusterConfig(input); err != nil { - if aerr, ok := err.(awserr.Error); ok { - return false, aerr - } + if _, err := s.EKSClient.UpdateClusterConfig(ctx, input); err != nil { return false, err } // Wait until status transitions to UPDATING because there's a short - // window after UpdateClusterVersion returns where the cluster + // window after UpdateClusterConfig returns where the cluster // status is ACTIVE and the update would be tried again if err := s.EKSClient.WaitUntilClusterUpdating( + ctx, &eks.DescribeClusterInput{Name: aws.String(s.scope.KubernetesClusterName())}, - request.WithWaiterLogger(&awslog{s.GetLogger()}), + s.scope.MaxWaitActiveUpdateDelete, ); err != nil { return false, err } diff --git a/pkg/cloud/services/eks/cluster_test.go b/pkg/cloud/services/eks/cluster_test.go index eee3afb990..b120226697 100644 --- a/pkg/cloud/services/eks/cluster_test.go +++ b/pkg/cloud/services/eks/cluster_test.go @@ -21,7 +21,6 @@ import ( "testing" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/aws/awserr" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/aws/aws-sdk-go-v2/service/iam" @@ -491,7 +490,7 @@ func TestReconcileAccessConfig(t *testing.T) { Cluster: &ekstypes.Cluster{ Name: aws.String("default.cluster"), AccessConfig: &ekstypes.AccessConfigResponse{ - AuthenticationMode: string(ekstypes.AuthenticationModeApiAndConfigMap), + AuthenticationMode: ekstypes.AuthenticationModeApiAndConfigMap, }, }, }, nil) @@ -507,7 +506,7 @@ func TestReconcileAccessConfig(t *testing.T) { Cluster: &ekstypes.Cluster{ Name: aws.String("default.cluster"), AccessConfig: &ekstypes.AccessConfigResponse{ - AuthenticationMode: string(ekstypes.AuthenticationModeConfigMap), + AuthenticationMode: ekstypes.AuthenticationModeConfigMap, }, }, }, nil) @@ -531,13 +530,13 @@ func TestReconcileAccessConfig(t *testing.T) { Cluster: &ekstypes.Cluster{ Name: aws.String("default.cluster"), AccessConfig: &ekstypes.AccessConfigResponse{ - AuthenticationMode: string(ekstypes.AuthenticationModeApi), + AuthenticationMode: ekstypes.AuthenticationModeApi, }, }, }, nil) m. UpdateClusterConfig(gomock.Eq(context.TODO()), gomock.AssignableToTypeOf(&eks.UpdateClusterConfigInput{})). - Return(&eks.UpdateClusterConfigOutput{}, awserr.New(string(ekstypes.ErrCodeInvalidParameterException), "Unsupported authentication mode update", nil)) + Return(&eks.UpdateClusterConfigOutput{}, errors.New("Unsupported authentication mode update")) }, expectError: true, }, @@ -568,7 +567,7 @@ func TestReconcileAccessConfig(t *testing.T) { Spec: ekscontrolplanev1.AWSManagedControlPlaneSpec{ EKSClusterName: clusterName, AccessConfig: &ekscontrolplanev1.AccessConfig{ - AuthenticationMode: ekscontrolplanev1.EKSAuthenticationModeApiAndConfigMap, + AuthenticationMode: ekscontrolplanev1.EKSAuthenticationModeAPIAndConfigMap, }, }, }, @@ -582,7 +581,7 @@ func TestReconcileAccessConfig(t *testing.T) { cluster, err := s.describeEKSCluster(context.TODO(), clusterName) g.Expect(err).To(BeNil()) - err = s.reconcileAccessConfig(cluster.AccessConfig) + err = s.reconcileAccessConfig(context.TODO(), cluster.AccessConfig) if tc.expectError { g.Expect(err).To(HaveOccurred()) return @@ -871,7 +870,6 @@ func TestCreateIPv6Cluster(t *testing.T) { eksMock.EXPECT().CreateCluster(context.TODO(), &eks.CreateClusterInput{ Name: aws.String("cluster-name"), Version: aws.String("1.22"), - RoleArn: aws.String("arn:role"), EncryptionConfig: []ekstypes.EncryptionConfig{ { Provider: &ekstypes.Provider{ @@ -895,7 +893,6 @@ func TestCreateIPv6Cluster(t *testing.T) { RoleName: aws.String("arn-role"), }).Return(&iam.GetRoleOutput{ Role: &iamtypes.Role{ - Arn: aws.String("arn:role"), RoleName: aws.String("arn-role"), }, }, nil) @@ -907,3 +904,74 @@ func TestCreateIPv6Cluster(t *testing.T) { _, err = s.createCluster(context.TODO(), "cluster-name") g.Expect(err).To(BeNil()) } + +func TestCreateClusterWithBootstrapClusterCreatorAdminPermissions(t *testing.T) { + g := NewWithT(t) + + mockControl := gomock.NewController(t) + defer mockControl.Finish() + + eksMock := mock_eksiface.NewMockEKSAPI(mockControl) + iamMock := mock_iamauth.NewMockIAMAPI(mockControl) + + scheme := runtime.NewScheme() + _ = infrav1.AddToScheme(scheme) + _ = ekscontrolplanev1.AddToScheme(scheme) + client := fake.NewClientBuilder().WithScheme(scheme).Build() + + clusterName := "test-cluster" + scope, err := scope.NewManagedControlPlaneScope(scope.ManagedControlPlaneScopeParams{ + Client: client, + Cluster: &clusterv1.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns", + Name: "capi-name", + }, + }, + ControlPlane: &ekscontrolplanev1.AWSManagedControlPlane{ + Spec: ekscontrolplanev1.AWSManagedControlPlaneSpec{ + EKSClusterName: clusterName, + Version: aws.String("1.24"), + RoleName: aws.String("arn:role"), + NetworkSpec: infrav1.NetworkSpec{ + Subnets: []infrav1.SubnetSpec{ + {ID: "1", AvailabilityZone: "us-west-2a"}, + {ID: "2", AvailabilityZone: "us-west-2b"}, + }, + }, + AccessConfig: &ekscontrolplanev1.AccessConfig{ + BootstrapClusterCreatorAdminPermissions: ptr.To(false), + }, + }, + }, + }) + g.Expect(err).To(BeNil()) + + eksMock.EXPECT().CreateCluster(context.TODO(), &eks.CreateClusterInput{ + Name: aws.String(clusterName), + Version: aws.String("1.24"), + ResourcesVpcConfig: &ekstypes.VpcConfigRequest{ + SubnetIds: []string{"1", "2"}, + }, + RoleArn: aws.String("arn:role"), + Tags: map[string]string{ + "kubernetes.io/cluster/test-cluster": "owned", + }, + AccessConfig: &ekstypes.CreateAccessConfigRequest{ + BootstrapClusterCreatorAdminPermissions: ptr.To(false), + }, + EncryptionConfig: []ekstypes.EncryptionConfig{}, + BootstrapSelfManagedAddons: aws.Bool(false), + }).Return(&eks.CreateClusterOutput{}, nil) + + iamMock.EXPECT().GetRole(gomock.Any(), gomock.Any()).Return(&iam.GetRoleOutput{ + Role: &iamtypes.Role{Arn: aws.String("arn:role")}, + }, nil) + + s := NewService(scope) + s.EKSClient = eksMock + s.IAMClient = iamMock + + _, err = s.createCluster(context.TODO(), clusterName) + g.Expect(err).To(BeNil()) +} diff --git a/test/e2e/data/e2e_eks_conf.yaml b/test/e2e/data/e2e_eks_conf.yaml index fe9fca78e3..b8230eace7 100644 --- a/test/e2e/data/e2e_eks_conf.yaml +++ b/test/e2e/data/e2e_eks_conf.yaml @@ -116,6 +116,10 @@ providers: 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" + - sourcePath: "./eks/cluster-template-eks-auth-api-and-config-map.yaml" + targetName: "cluster-template-eks-auth-api-and-config-map.yaml" + - sourcePath: "./eks/cluster-template-eks-auth-bootstrap-disabled.yaml" + targetName: "cluster-template-eks-auth-bootstrap-disabled.yaml" - sourcePath: "./infrastructure-aws/withclusterclass/kustomize_sources/eks-clusterclass/clusterclass-eks-e2e.yaml" - sourcePath: "./infrastructure-aws/withclusterclass/generated/cluster-template-eks-clusterclass.yaml" diff --git a/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml b/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml new file mode 100644 index 0000000000..a9adbbc9f4 --- /dev/null +++ b/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml @@ -0,0 +1,38 @@ +--- +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}" + accessConfig: + authenticationMode: api_and_config_map + bootstrapClusterCreatorAdminPermissions: true + identityRef: + kind: AWSClusterStaticIdentity + name: e2e-account \ No newline at end of file diff --git a/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml b/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml new file mode 100644 index 0000000000..9fb427c37d --- /dev/null +++ b/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml @@ -0,0 +1,38 @@ +--- +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}" + accessConfig: + authenticationMode: api_and_config_map + bootstrapClusterCreatorAdminPermissions: false + identityRef: + kind: AWSClusterStaticIdentity + name: e2e-account \ No newline at end of file diff --git a/test/e2e/suites/managed/eks_auth_test.go b/test/e2e/suites/managed/eks_auth_test.go new file mode 100644 index 0000000000..af59c31a2c --- /dev/null +++ b/test/e2e/suites/managed/eks_auth_test.go @@ -0,0 +1,180 @@ +//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 managed + +import ( + "context" + "fmt" + + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + ekscontrolplanev1 "sigs.k8s.io/cluster-api-provider-aws/v2/controlplane/eks/api/v1beta2" + "sigs.k8s.io/cluster-api-provider-aws/v2/test/e2e/shared" + "sigs.k8s.io/cluster-api/test/framework" + "sigs.k8s.io/cluster-api/util" +) + +// EKS authentication mode e2e tests. +var _ = ginkgo.Describe("[managed] [auth] EKS authentication mode tests", func() { + var ( + namespace *corev1.Namespace + ctx context.Context + specName = "auth" + clusterName string + ) + + shared.ConditionalIt(runGeneralTests, "should create a cluster with api_and_config_map authentication mode", func() { + ginkgo.By("should have a valid test configuration") + Expect(e2eCtx.Environment.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. BootstrapClusterProxy can't be nil") + Expect(e2eCtx.E2EConfig).ToNot(BeNil(), "Invalid argument. e2eConfig can't be nil when calling %s spec", specName) + Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.KubernetesVersion)) + + ctx = context.TODO() + namespace = shared.SetupSpecNamespace(ctx, specName, e2eCtx) + clusterName = fmt.Sprintf("%s-%s", specName, util.RandomString(6)) + eksClusterName := getEKSClusterName(namespace.Name, clusterName) + + ginkgo.By("should create an EKS control plane with api_and_config_map authentication mode") + ManagedClusterSpec(ctx, func() ManagedClusterSpecInput { + return ManagedClusterSpecInput{ + E2EConfig: e2eCtx.E2EConfig, + ConfigClusterFn: defaultConfigCluster, + BootstrapClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + AWSSession: e2eCtx.BootstrapUserAWSSession, + Namespace: namespace, + ClusterName: clusterName, + Flavour: EKSAuthAPIAndConfigMapFlavor, + ControlPlaneMachineCount: 1, + WorkerMachineCount: 0, + } + }) + + ginkgo.By("EKS cluster should be active") + verifyClusterActiveAndOwned(ctx, eksClusterName, e2eCtx.BootstrapUserAWSSession) + + ginkgo.By("verifying cluster has the correct authentication mode") + verifyClusterAuthenticationMode(ctx, eksClusterName, ekstypes.AuthenticationModeApiAndConfigMap, e2eCtx.BootstrapUserAWSSession) + + ginkgo.By("attempting to downgrade from api_and_config_map to config_map should fail") + controlPlaneName := fmt.Sprintf("%s-control-plane", clusterName) + controlPlane := &ekscontrolplanev1.AWSManagedControlPlane{} + err := e2eCtx.Environment.BootstrapClusterProxy.GetClient().Get(ctx, client.ObjectKey{ + Namespace: namespace.Name, + Name: controlPlaneName, + }, controlPlane) + Expect(err).ToNot(HaveOccurred(), "failed to get control plane") + + controlPlane.Spec.AccessConfig.AuthenticationMode = ekscontrolplanev1.EKSAuthenticationModeConfigMap + err = e2eCtx.Environment.BootstrapClusterProxy.GetClient().Update(ctx, controlPlane) + Expect(err).To(HaveOccurred(), "expected downgrade from api_and_config_map to config_map to fail") + + ginkgo.By("upgrading from api_and_config_map to api should succeed") + err = e2eCtx.Environment.BootstrapClusterProxy.GetClient().Get(ctx, client.ObjectKey{ + Namespace: namespace.Name, + Name: controlPlaneName, + }, controlPlane) + Expect(err).ToNot(HaveOccurred(), "failed to get control plane for upgrade") + + controlPlane.Spec.AccessConfig.AuthenticationMode = ekscontrolplanev1.EKSAuthenticationModeAPI + err = e2eCtx.Environment.BootstrapClusterProxy.GetClient().Update(ctx, controlPlane) + Expect(err).ToNot(HaveOccurred(), "expected upgrade from api_and_config_map to api to succeed") + + ginkgo.By("attempting to downgrade from api to api_and_config_map should fail") + err = e2eCtx.Environment.BootstrapClusterProxy.GetClient().Get(ctx, client.ObjectKey{ + Namespace: namespace.Name, + Name: controlPlaneName, + }, controlPlane) + Expect(err).ToNot(HaveOccurred(), "failed to get control plane for downgrade attempt") + + controlPlane.Spec.AccessConfig.AuthenticationMode = ekscontrolplanev1.EKSAuthenticationModeAPIAndConfigMap + err = e2eCtx.Environment.BootstrapClusterProxy.GetClient().Update(ctx, controlPlane) + Expect(err).To(HaveOccurred(), "expected downgrade from api to api_and_config_map to fail") + + cluster := framework.GetClusterByName(ctx, framework.GetClusterByNameInput{ + Getter: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + Namespace: namespace.Name, + Name: clusterName, + }) + Expect(cluster).NotTo(BeNil(), "couldn't find CAPI cluster") + + framework.DeleteCluster(ctx, framework.DeleteClusterInput{ + Deleter: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + Cluster: cluster, + }) + framework.WaitForClusterDeleted(ctx, framework.WaitForClusterDeletedInput{ + ClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + Cluster: cluster, + ClusterctlConfigPath: e2eCtx.Environment.ClusterctlConfigPath, + ArtifactFolder: e2eCtx.Settings.ArtifactFolder, + }, e2eCtx.E2EConfig.GetIntervals("", "wait-delete-cluster")...) + }) + + shared.ConditionalIt(runGeneralTests, "should create a cluster with bootstrapClusterCreatorAdminPermissions disabled", func() { + ginkgo.By("should have a valid test configuration") + Expect(e2eCtx.Environment.BootstrapClusterProxy).ToNot(BeNil(), "Invalid argument. BootstrapClusterProxy can't be nil") + Expect(e2eCtx.E2EConfig).ToNot(BeNil(), "Invalid argument. e2eConfig can't be nil when calling bootstrap spec") + Expect(e2eCtx.E2EConfig.Variables).To(HaveKey(shared.KubernetesVersion)) + + ctx = context.TODO() + namespace = shared.SetupSpecNamespace(ctx, "bootstrap", e2eCtx) + clusterName = fmt.Sprintf("bootstrap-%s", util.RandomString(6)) + eksClusterName := getEKSClusterName(namespace.Name, clusterName) + + ginkgo.By("should create an EKS control plane with bootstrapClusterCreatorAdminPermissions disabled") + ManagedClusterSpec(ctx, func() ManagedClusterSpecInput { + return ManagedClusterSpecInput{ + E2EConfig: e2eCtx.E2EConfig, + ConfigClusterFn: defaultConfigCluster, + BootstrapClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + AWSSession: e2eCtx.BootstrapUserAWSSession, + Namespace: namespace, + ClusterName: clusterName, + Flavour: EKSAuthBootstrapDisabledFlavor, + ControlPlaneMachineCount: 1, + WorkerMachineCount: 0, + } + }) + + ginkgo.By("EKS cluster should be active") + verifyClusterActiveAndOwned(ctx, eksClusterName, e2eCtx.BootstrapUserAWSSession) + + cluster := framework.GetClusterByName(ctx, framework.GetClusterByNameInput{ + Getter: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + Namespace: namespace.Name, + Name: clusterName, + }) + Expect(cluster).NotTo(BeNil(), "couldn't find CAPI cluster") + + framework.DeleteCluster(ctx, framework.DeleteClusterInput{ + Deleter: e2eCtx.Environment.BootstrapClusterProxy.GetClient(), + Cluster: cluster, + }) + framework.WaitForClusterDeleted(ctx, framework.WaitForClusterDeletedInput{ + ClusterProxy: e2eCtx.Environment.BootstrapClusterProxy, + Cluster: cluster, + ClusterctlConfigPath: e2eCtx.Environment.ClusterctlConfigPath, + ArtifactFolder: e2eCtx.Settings.ArtifactFolder, + }, e2eCtx.E2EConfig.GetIntervals("", "wait-delete-cluster")...) + }) +}) diff --git a/test/e2e/suites/managed/helpers.go b/test/e2e/suites/managed/helpers.go index 2922c70201..926d914248 100644 --- a/test/e2e/suites/managed/helpers.go +++ b/test/e2e/suites/managed/helpers.go @@ -50,6 +50,8 @@ const ( EKSIPv6ClusterFlavor = "eks-ipv6-cluster" EKSControlPlaneOnlyLegacyFlavor = "eks-control-plane-only-legacy" EKSClusterClassFlavor = "eks-clusterclass" + EKSAuthAPIAndConfigMapFlavor = "eks-auth-api-and-config-map" + EKSAuthBootstrapDisabledFlavor = "eks-auth-bootstrap-disabled" ) const ( @@ -106,6 +108,21 @@ func getEKSCluster(ctx context.Context, eksClusterName string, sess *aws.Config) return result.Cluster, err } +func verifyClusterAuthenticationMode(ctx context.Context, eksClusterName string, expectedAuthMode ekstypes.AuthenticationMode, sess *aws.Config) { + var ( + cluster *ekstypes.Cluster + err error + ) + Eventually(func() error { + cluster, err = getEKSCluster(ctx, eksClusterName, sess) + return err + }, clientRequestTimeout, clientRequestCheckInterval).Should(Succeed(), fmt.Sprintf("eventually failed trying to get EKS Cluster %q", eksClusterName)) + + Expect(cluster.AccessConfig).ToNot(BeNil(), "expecting AccessConfig to be set on the cluster") + Expect(cluster.AccessConfig.AuthenticationMode).To(BeEquivalentTo(expectedAuthMode), + fmt.Sprintf("expecting authentication mode to be %s, got %s", expectedAuthMode, cluster.AccessConfig.AuthenticationMode)) +} + func getEKSClusterAddon(ctx context.Context, eksClusterName, addonName string, sess *aws.Config) (*ekstypes.Addon, error) { eksClient := eks.NewFromConfig(*sess) From f1ee8c3aa0b013e4afd7484b2b8d0d98c3a0258c Mon Sep 17 00:00:00 2001 From: Josh French Date: Tue, 23 Sep 2025 08:38:37 -0400 Subject: [PATCH 3/4] Update controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go Co-authored-by: Damiano Donati --- .../eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go index b23218886d..40de7b369b 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go @@ -338,7 +338,7 @@ func TestWebhookCreate(t *testing.T) { eksClusterName: "default_cluster1", eksVersion: "v1.19", expectError: true, - expectErrorToContain: "bootstrapClusterCreatorAdminPermissions must be true if cluster authentication mode is set to CONFIG_MAP", + expectErrorToContain: "bootstrapClusterCreatorAdminPermissions must be true if cluster authentication mode is set to config_map", accessConfig: &AccessConfig{ AuthenticationMode: EKSAuthenticationModeConfigMap, BootstrapClusterCreatorAdminPermissions: ptr.To(false), From 0d906868c5af728a1fef1ba98905fe9d61b9ce0d Mon Sep 17 00:00:00 2001 From: Josh French Date: Wed, 24 Sep 2025 08:59:10 -0400 Subject: [PATCH 4/4] add EOF to new files --- .../data/eks/cluster-template-eks-auth-api-and-config-map.yaml | 3 ++- .../data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml b/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml index a9adbbc9f4..d9e3c541e6 100644 --- a/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml +++ b/test/e2e/data/eks/cluster-template-eks-auth-api-and-config-map.yaml @@ -35,4 +35,5 @@ spec: bootstrapClusterCreatorAdminPermissions: true identityRef: kind: AWSClusterStaticIdentity - name: e2e-account \ No newline at end of file + name: e2e-account + diff --git a/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml b/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml index 9fb427c37d..3655f9c1dc 100644 --- a/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml +++ b/test/e2e/data/eks/cluster-template-eks-auth-bootstrap-disabled.yaml @@ -35,4 +35,5 @@ spec: bootstrapClusterCreatorAdminPermissions: false identityRef: kind: AWSClusterStaticIdentity - name: e2e-account \ No newline at end of file + name: e2e-account +