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 ef0bb24e20..5a9e82000c 100644 --- a/Makefile +++ b/Makefile @@ -134,6 +134,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) @@ -371,12 +374,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)) @@ -395,7 +398,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 345b3f4379..aaa39720c0 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 @@ -2102,6 +2117,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 @@ -2825,7 +2855,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 0ca9a64ebe..73370445ad 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 151772f75b..9229a8120c 100644 --- a/controlplane/eks/api/v1beta1/zz_generated.conversion.go +++ b/controlplane/eks/api/v1beta1/zz_generated.conversion.go @@ -70,6 +70,16 @@ func RegisterConversions(s *runtime.Scheme) error { }); 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) + }); 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) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*Addon)(nil), (*v1beta2.Addon)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_Addon_To_v1beta2_Addon(a.(*Addon), b.(*v1beta2.Addon), scope) }); err != nil { @@ -353,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 @@ -390,6 +401,7 @@ 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 } @@ -448,6 +460,26 @@ func Convert_v1beta2_AWSManagedControlPlaneStatus_To_v1beta1_AWSManagedControlPl 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 e2372492a6..118c717645 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 109752e573..cf114fb230 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_types.go @@ -164,11 +164,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"` @@ -219,6 +223,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 abda129f92..431b9d4382 100644 --- a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go +++ b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook.go @@ -123,6 +123,7 @@ func (r *AWSManagedControlPlane) ValidateUpdate(old runtime.Object) (admission.W 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()...) @@ -289,6 +290,28 @@ func (r *AWSManagedControlPlane) validateEKSAddons() field.ErrorList { 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 { var allErrs field.ErrorList diff --git a/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go b/controlplane/eks/api/v1beta2/awsmanagedcontrolplane_webhook_test.go index 7441040b8e..b5a2f7b722 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 1ef47215ce..0e8381f841 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 160f556db9..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 } @@ -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/pkg/cloud/services/eks/cluster.go b/pkg/cloud/services/eks/cluster.go index 62c990bd36..a6f3022923 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(cluster.Logging); err != nil { return errors.Wrap(err, "failed reconciling logging") } @@ -375,6 +379,13 @@ func (s *Service) createCluster(eksClusterName string) (*eks.Cluster, error) { return nil, errors.Wrap(err, "couldn't create vpc config for cluster") } + var accessConfig *eks.CreateAccessConfigRequest + if s.scope.ControlPlane.Spec.AccessConfig != nil && s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode != "" { + accessConfig = &eks.CreateAccessConfigRequest{ + AuthenticationMode: aws.String(string(s.scope.ControlPlane.Spec.AccessConfig.AuthenticationMode)), + } + } + var netConfig *eks.KubernetesNetworkConfigRequest if s.scope.VPC().IsIPv6Enabled() { netConfig = &eks.KubernetesNetworkConfigRequest{ @@ -416,6 +427,7 @@ func (s *Service) createCluster(eksClusterName string) (*eks.Cluster, error) { Name: aws.String(eksClusterName), Version: eksVersion, Logging: logging, + AccessConfig: accessConfig, EncryptionConfig: encryptionConfigs, ResourcesVpcConfig: vpcConfig, RoleArn: role.Arn, @@ -423,6 +435,10 @@ func (s *Service) createCluster(eksClusterName string) (*eks.Cluster, error) { KubernetesNetworkConfig: netConfig, } + 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(input); err != nil { @@ -501,6 +517,56 @@ func (s *Service) reconcileClusterConfig(cluster *eks.Cluster) error { return nil } +func (s *Service) reconcileAccessConfig(accessConfig *eks.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 != aws.StringValue(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(logging *eks.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 7079c62de5..b9c95c6946 100644 --- a/pkg/cloud/services/eks/cluster_test.go +++ b/pkg/cloud/services/eks/cluster_test.go @@ -20,6 +20,7 @@ import ( "testing" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/eks" "github.com/aws/aws-sdk-go/service/iam" "github.com/golang/mock/gomock" @@ -463,6 +464,121 @@ 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.AssignableToTypeOf(&eks.DescribeClusterInput{})). + Return(&eks.DescribeClusterOutput{ + Cluster: &eks.Cluster{ + Name: aws.String("default.cluster"), + AccessConfig: &eks.AccessConfigResponse{ + AuthenticationMode: aws.String(eks.AuthenticationModeApiAndConfigMap), + }, + }, + }, nil) + }, + expectError: false, + }, + { + name: "needs upgrade", + expect: func(m *mock_eksiface.MockEKSAPIMockRecorder) { + m. + DescribeCluster(gomock.AssignableToTypeOf(&eks.DescribeClusterInput{})). + Return(&eks.DescribeClusterOutput{ + Cluster: &eks.Cluster{ + Name: aws.String("default.cluster"), + AccessConfig: &eks.AccessConfigResponse{ + AuthenticationMode: aws.String(eks.AuthenticationModeConfigMap), + }, + }, + }, nil) + m.WaitUntilClusterUpdating( + gomock.AssignableToTypeOf(&eks.DescribeClusterInput{}), gomock.Any(), + ).Return(nil) + m. + UpdateClusterConfig(gomock.AssignableToTypeOf(&eks.UpdateClusterConfigInput{})). + Return(&eks.UpdateClusterConfigOutput{}, nil) + }, + expectError: false, + }, + { + name: "api error", + expect: func(m *mock_eksiface.MockEKSAPIMockRecorder) { + m. + DescribeCluster(gomock.AssignableToTypeOf(&eks.DescribeClusterInput{})). + Return(&eks.DescribeClusterOutput{ + Cluster: &eks.Cluster{ + Name: aws.String("default.cluster"), + AccessConfig: &eks.AccessConfigResponse{ + AuthenticationMode: aws.String(eks.AuthenticationModeApi), + }, + }, + }, nil) + m. + UpdateClusterConfig(gomock.AssignableToTypeOf(&eks.UpdateClusterConfigInput{})). + Return(&eks.UpdateClusterConfigOutput{}, awserr.New(eks.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: eks.AuthenticationModeApiAndConfigMap, + }, + }, + }, + }) + g.Expect(err).To(BeNil()) + + tc.expect(eksMock.EXPECT()) + s := NewService(scope) + s.EKSClient = eksMock + + cluster, err := s.describeEKSCluster(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") @@ -737,6 +853,7 @@ func TestCreateIPv6Cluster(t *testing.T) { eksMock.EXPECT().CreateCluster(&eks.CreateClusterInput{ Name: aws.String("cluster-name"), Version: aws.String("1.22"), + RoleArn: aws.String("arn:role"), EncryptionConfig: []*eks.EncryptionConfig{ { Provider: &eks.Provider{ @@ -759,6 +876,7 @@ func TestCreateIPv6Cluster(t *testing.T) { RoleName: aws.String("arn-role"), }).Return(&iam.GetRoleOutput{ Role: &iam.Role{ + Arn: aws.String("arn:role"), RoleName: ptr.To[string]("arn-role"), }, }, nil)