From 57824946f30ec512f9b33d48d51da7bf0788b5b5 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:53:49 +0300 Subject: [PATCH 01/35] add new addon fields required for pod identity support --- pkg/apis/eksctl.io/v1alpha5/addon.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index 4fd5792a20..1d6bcaa085 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -35,6 +35,10 @@ type Addon struct { // ResolveConflicts determines how to resolve field value conflicts for an EKS add-on // if a value was changed from default ResolveConflicts ekstypes.ResolveConflicts `json:"resolveConflicts,omitempty"` + // ResolvePodIdentityConflicts + ResolvePodIdentityConflicts ekstypes.ResolveConflicts `json:"resolvePodIdentityConflicts,omitempty"` + // PodIdentityAssociations + PodIdentityAssociations []PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` // ConfigurationValues defines the set of configuration properties for add-ons. // For now, all properties will be specified as a JSON string // and have to respect the schema from DescribeAddonConfiguration. From 1d5b861430d3e2eca6857f0c7f74b533d44ae23e Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:12:46 +0300 Subject: [PATCH 02/35] ammend create addon command to create roles for pod identity associations --- pkg/actions/addon/addon.go | 26 ++++- pkg/actions/addon/create.go | 176 +++++++++++++++++------------ pkg/actions/addon/tasks.go | 24 +++- pkg/actions/addon/update.go | 4 +- pkg/apis/eksctl.io/v1alpha5/iam.go | 3 + pkg/ctl/cmdutils/configfile.go | 17 ++- pkg/ctl/create/addon.go | 48 +++++++- 7 files changed, 213 insertions(+), 85 deletions(-) diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index 2f5a42136d..fda2600207 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -92,21 +92,28 @@ func (a *Manager) waitForAddonToBeActive(ctx context.Context, addon *api.Addon, return nil } -func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon) (string, error) { +func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon) (string, bool, error) { addonInfos, err := a.describeVersions(ctx, addon) if err != nil { - return "", err + return "", false, err } if len(addonInfos.Addons) == 0 || len(addonInfos.Addons[0].AddonVersions) == 0 { - return "", fmt.Errorf("no versions available for %q", addon.Name) + return "", false, fmt.Errorf("no versions available for %q", addon.Name) } addonVersion := addon.Version var versions []*version.Version for _, addonVersionInfo := range addonInfos.Addons[0].AddonVersions { + // if not specified, will install default version + if addonVersion == "" && addonVersionInfo.Compatibilities[0].DefaultVersion { + return *addonVersionInfo.AddonVersion, addonVersionInfo.RequiresIamPermissions, nil + } else if addonVersion == "" { + continue + } + v, err := a.parseVersion(*addonVersionInfo.AddonVersion) if err != nil { - return "", err + return "", false, err } if addonVersion == "latest" || strings.Contains(*addonVersionInfo.AddonVersion, addonVersion) { @@ -115,13 +122,20 @@ func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon } if len(versions) == 0 { - return "", fmt.Errorf("no version(s) found matching %q for %q", addonVersion, addon.Name) + return "", false, fmt.Errorf("no version(s) found matching %q for %q", addonVersion, addon.Name) } sort.SliceStable(versions, func(i, j int) bool { return versions[j].LessThan(versions[i]) }) - return versions[0].Original(), nil + + requireIAMPermissions := false + for _, addonVersionInfo := range addonInfos.Addons[0].AddonVersions { + if *addonVersionInfo.AddonVersion == versions[0].Original() { + requireIAMPermissions = addonVersionInfo.RequiresIamPermissions + } + } + return versions[0].Original(), requireIAMPermissions, nil } func (a *Manager) makeAddonName(name string) string { diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 031f5e6980..68f5d0cf61 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -26,7 +26,7 @@ const ( ) func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time.Duration) error { - // First check if the addon is already present as an EKS managed addon + // check if the addon is already present as an EKS managed addon // in a state different from CREATE_FAILED, and if so, don't re-create var notFoundErr *ekstypes.ResourceNotFoundException summary, err := a.eksAPI.DescribeAddon(ctx, &eks.DescribeAddonInput{ @@ -39,17 +39,13 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time // if the addon already exists AND it is not in CREATE_FAILED state if err == nil && summary.Addon.Status != ekstypes.AddonStatusCreateFailed { - logger.Info("Addon %s is already present in this cluster, as an EKS managed addon, and won't be re-created", addon.Name) + logger.Info("addon %s is already present on the cluster, as an EKS managed addon, skipping creation", addon.Name) return nil } - version := addon.Version - if version != "" { - var err error - version, err = a.getLatestMatchingVersion(ctx, addon) - if err != nil { - return fmt.Errorf("failed to fetch version %s for addon %s: %w", version, addon.Name, err) - } + version, requiresIAMPermissions, err := a.getLatestMatchingVersion(ctx, addon) + if err != nil { + return fmt.Errorf("failed to fetch version %s for addon %s: %w", version, addon.Name, err) } var configurationValues *string if addon.ConfigurationValues != "" { @@ -74,52 +70,96 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time logger.Debug("resolve conflicts set to %s", createAddonInput.ResolveConflicts) logger.Debug("addon: %v", addon) - namespace, serviceAccount := a.getKnownServiceAccountLocation(addon) if len(addon.Tags) > 0 { createAddonInput.Tags = addon.Tags } - if a.withOIDC { - if addon.ServiceAccountRoleARN != "" { - logger.Info("using provided ServiceAccountRoleARN %q", addon.ServiceAccountRoleARN) - createAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN - } else if hasPoliciesSet(addon) { - outputRole, err := a.createRole(ctx, addon, namespace, serviceAccount) + + getRecommendedPolicies := func(ctx context.Context, addon *api.Addon) ([]ekstypes.AddonPodIdentityConfiguration, error) { + output, err := a.eksAPI.DescribeAddonConfiguration(ctx, &eks.DescribeAddonConfigurationInput{ + AddonName: &addon.Name, + AddonVersion: &addon.Version, + }) + if err != nil { + return nil, fmt.Errorf("describing configuration for addon %s: %w", addon.Name, err) + } + return output.PodIdentityConfiguration, nil + } + + if requiresIAMPermissions { + switch { + case len(addon.PodIdentityAssociations) > 0: + logger.Info("pod identity associations were specified for addon %s, will use those to provide required IAM permissions, other settings such as IRSA will be ignored") + for _, pia := range addon.PodIdentityAssociations { + roleARN, err := a.createRoleForPodIdentity(ctx, addon.Name, pia) + if err != nil { + return err + } + createAddonInput.PodIdentityAssociations = append(createAddonInput.PodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ + RoleArn: &roleARN, + ServiceAccount: &pia.ServiceAccountName, + }) + } + + case a.clusterConfig.IAM.AutoCreatePodIdentityAssociations: + logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended policies for addon %s", addon.Name) + recommendedPoliciesBySA, err := getRecommendedPolicies(ctx, addon) if err != nil { return err } - createAddonInput.ServiceAccountRoleArn = &outputRole - } else { - policyDocument, policyARNs, wellKnownPolicies := a.getRecommendedPolicies(addon) - if len(policyARNs) != 0 || policyDocument != nil || wellKnownPolicies != nil { - logger.Info("creating role using recommended policies") - addon.AttachPolicyARNs = policyARNs - addon.AttachPolicy = policyDocument - resourceSet := builder.NewIAMRoleResourceSetWithAttachPolicy(addon.Name, namespace, serviceAccount, addon.PermissionsBoundary, addon.AttachPolicy, a.oidcManager) - if len(policyARNs) != 0 { - resourceSet = builder.NewIAMRoleResourceSetWithAttachPolicyARNs(addon.Name, namespace, serviceAccount, addon.PermissionsBoundary, addon.AttachPolicyARNs, a.oidcManager) - } - if wellKnownPolicies != nil { - addon.WellKnownPolicies = *wellKnownPolicies - resourceSet = builder.NewIAMRoleResourceSetWithWellKnownPolicies(addon.Name, namespace, serviceAccount, addon.PermissionsBoundary, addon.WellKnownPolicies, a.oidcManager) - } - if err := resourceSet.AddAllResources(); err != nil { - return err - } - err := a.createStack(ctx, resourceSet, addon) + if len(recommendedPoliciesBySA) == 0 { + logger.Info("no recommended policies found for addon %s, proceeding without adding any IAM permissions", addon.Name) + break + } + for _, p := range recommendedPoliciesBySA { + roleARN, err := a.createRoleForPodIdentity(ctx, addon.Name, api.PodIdentityAssociation{ + ServiceAccountName: *p.ServiceAccount, + PermissionPolicyARNs: p.RecommendedManagedPolicies, + }) if err != nil { return err } - createAddonInput.ServiceAccountRoleArn = &resourceSet.OutputRole - } else { - logger.Info("no recommended policies found, proceeding without any IAM") + createAddonInput.PodIdentityAssociations = append(createAddonInput.PodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ + RoleArn: &roleARN, + ServiceAccount: p.ServiceAccount, + }) } - } - } else { - //if any sort of policy is set or could be set, log a warning - policyDocument, policyARNs, wellKnownPolicies := a.getRecommendedPolicies(addon) - if addon.ServiceAccountRoleARN != "" || hasPoliciesSet(addon) || len(policyARNs) != 0 || policyDocument != nil || wellKnownPolicies != nil { - logger.Warning("OIDC is disabled but policies are required/specified for this addon. Users are responsible for attaching the policies to all nodegroup roles") + + case a.withOIDC: + if addon.ServiceAccountRoleARN != "" { + logger.Info("using provided ServiceAccountRoleARN %q", addon.ServiceAccountRoleARN) + createAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN + break + } + + if !hasPoliciesSet(addon) { + if addon.CanonicalName() == api.VPCCNIAddon && a.clusterConfig.IPv6Enabled() { + addon.AttachPolicy = makeIPv6VPCCNIPolicyDocument(api.Partitions.ForRegion(a.clusterConfig.Metadata.Region)) + } else { + recommendedPoliciesBySA, err := getRecommendedPolicies(ctx, addon) + if err != nil { + return err + } + if len(recommendedPoliciesBySA) == 0 { + logger.Info("no recommended policies found for addon %s, proceeding without adding any IAM permissions", addon.Name) + break + } + logger.Info("creating role using recommended policies for addon %s", addon.Name) + for _, p := range recommendedPoliciesBySA { + addon.AttachPolicyARNs = append(addon.AttachPolicyARNs, p.RecommendedManagedPolicies...) + } + } + } + + namespace, serviceAccount := a.getKnownServiceAccountLocation(addon) + roleARN, err := a.createRoleForIRSA(ctx, addon, namespace, serviceAccount) + if err != nil { + return err + } + createAddonInput.ServiceAccountRoleArn = &roleARN + + default: + logger.Warning("addon %s requires IAM permissions; please set \"iam.AutoCreatePodIdentityAssociations\" to true or specify them manually via \"addon.PodIdentityAssociations\"") } } @@ -139,6 +179,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time logger.Info("creating addon") output, err := a.eksAPI.CreateAddon(ctx, createAddonInput) if err != nil { + // TODO: gracefully handle scenario where pod identity association already exists return errors.Wrapf(err, "failed to create addon %q", addon.Name) } @@ -254,27 +295,6 @@ func (a *Manager) patchAWSNodeDaemonSet(ctx context.Context) error { return nil } -func (a *Manager) getRecommendedPolicies(addon *api.Addon) (api.InlineDocument, []string, *api.WellKnownPolicies) { - // API isn't case-sensitive - switch addon.CanonicalName() { - case api.VPCCNIAddon: - if a.clusterConfig.IPv6Enabled() { - return makeIPv6VPCCNIPolicyDocument(api.Partitions.ForRegion(a.clusterConfig.Metadata.Region)), nil, nil - } - return nil, []string{fmt.Sprintf("arn:%s:iam::aws:policy/%s", api.Partitions.ForRegion(a.clusterConfig.Metadata.Region), api.IAMPolicyAmazonEKSCNIPolicy)}, nil - case api.AWSEBSCSIDriverAddon: - return nil, nil, &api.WellKnownPolicies{ - EBSCSIController: true, - } - case api.AWSEFSCSIDriverAddon: - return nil, nil, &api.WellKnownPolicies{ - EFSCSIController: true, - } - default: - return nil, nil, nil - } -} - func (a *Manager) getKnownServiceAccountLocation(addon *api.Addon) (string, string) { // API isn't case sensitive switch addon.CanonicalName() { @@ -290,17 +310,26 @@ func hasPoliciesSet(addon *api.Addon) bool { return len(addon.AttachPolicyARNs) != 0 || addon.WellKnownPolicies.HasPolicy() || addon.AttachPolicy != nil } -func (a *Manager) createRole(ctx context.Context, addon *api.Addon, namespace, serviceAccount string) (string, error) { - resourceSet, err := a.createRoleResourceSet(addon, namespace, serviceAccount) - - if err != nil { +func (a *Manager) createRoleForPodIdentity(ctx context.Context, addonName string, pia api.PodIdentityAssociation) (string, error) { + resourceSet := builder.NewIAMRoleResourceSetForPodIdentity(&pia) + if err := resourceSet.AddAllResources(); err != nil { return "", err } + if err := a.createStack(ctx, resourceSet, addonName, fmt.Sprintf("podidentityrole-%s", pia.ServiceAccountName)); err != nil { + return "", err + } + return pia.RoleARN, nil +} - err = a.createStack(ctx, resourceSet, addon) +func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, namespace, serviceAccount string) (string, error) { + logger.Warning("providing required IAM permissions via OIDC has been deprecated for addon %s; please use \"eksctl utils migrate-to-pod-identities\" after addon is created") + resourceSet, err := a.createRoleResourceSet(addon, namespace, serviceAccount) if err != nil { return "", err } + if err := a.createStack(ctx, resourceSet, addon.Name, "IRSA"); err != nil { + return "", err + } return resourceSet.OutputRole, nil } @@ -319,14 +348,15 @@ func (a *Manager) createRoleResourceSet(addon *api.Addon, namespace, serviceAcco return resourceSet, resourceSet.AddAllResources() } -func (a *Manager) createStack(ctx context.Context, resourceSet builder.ResourceSetReader, addon *api.Addon) error { +func (a *Manager) createStack(ctx context.Context, resourceSet builder.ResourceSetReader, addonName, stackNameSuffix string) error { errChan := make(chan error) tags := map[string]string{ - api.AddonNameTag: addon.Name, + api.AddonNameTag: addonName, } - err := a.stackManager.CreateStack(ctx, a.makeAddonName(addon.Name), resourceSet, tags, nil, errChan) + stackName := fmt.Sprintf("%s-%s", a.makeAddonName(addonName), stackNameSuffix) + err := a.stackManager.CreateStack(ctx, stackName, resourceSet, tags, nil, errChan) if err != nil { return err } diff --git a/pkg/actions/addon/tasks.go b/pkg/actions/addon/tasks.go index 60ff4f7f14..aec972e358 100644 --- a/pkg/actions/addon/tasks.go +++ b/pkg/actions/addon/tasks.go @@ -21,7 +21,8 @@ func CreateAddonTasks(ctx context.Context, cfg *api.ClusterConfig, clusterProvid var preAddons []*api.Addon var postAddons []*api.Addon for _, addon := range cfg.Addons { - if strings.EqualFold(addon.Name, api.VPCCNIAddon) { + if strings.EqualFold(addon.Name, api.VPCCNIAddon) || + strings.EqualFold(addon.Name, api.PodIdentityAgentAddon) { preAddons = append(preAddons, addon) } else { postAddons = append(postAddons, addon) @@ -90,7 +91,28 @@ func (t *createAddonTask) Do(errorCh chan error) error { return err } + // always install EKS Pod Identity Agent Addon first, if present, + // as other addons might require IAM permissions for _, a := range t.addons { + if a.CanonicalName() != api.PodIdentityAgentAddon { + continue + } + if t.forceAll { + a.Force = true + } + err := addonManager.Create(t.ctx, a, t.timeout) + if err != nil { + go func() { + errorCh <- err + }() + return err + } + } + + for _, a := range t.addons { + if a.CanonicalName() == api.PodIdentityAgentAddon { + continue + } if t.forceAll { a.Force = true } diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 121b79e934..b6aec20ed7 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -47,7 +47,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, waitTimeout time updateAddonInput.AddonVersion = &summary.Version } else { - version, err := a.getLatestMatchingVersion(ctx, addon) + version, _, err := a.getLatestMatchingVersion(ctx, addon) if err != nil { return fmt.Errorf("failed to fetch addon version: %w", err) } @@ -103,7 +103,7 @@ func (a *Manager) updateWithNewPolicies(ctx context.Context, addon *api.Addon) ( namespace, serviceAccount := a.getKnownServiceAccountLocation(addon) if stack == nil { - return a.createRole(ctx, addon, namespace, serviceAccount) + return a.createRoleForIRSA(ctx, addon, namespace, serviceAccount) } createNewTemplate, err := a.createNewTemplate(addon, namespace, serviceAccount) diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index 4f36a93868..43987480c0 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -53,6 +53,9 @@ type ClusterIAM struct { // +optional ServiceAccounts []*ClusterIAMServiceAccount `json:"serviceAccounts,omitempty"` + // AutoCreatePodIdentityAssociations + AutoCreatePodIdentityAssociations bool `json:"autoCreatePodIdentityAssociations,omitempty"` + // pod identity associations to create in the cluster. // See [Pod Identity Associations](TBD) // +optional diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 040b2133a5..975095bd2e 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -303,7 +303,22 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. } } - if clusterConfig.IAM != nil && len(clusterConfig.IAM.PodIdentityAssociations) > 0 { + shallCreatePodIdentityAssociations := func(cfg *api.ClusterConfig) bool { + if cfg.IAM != nil && len(cfg.IAM.PodIdentityAssociations) > 0 { + return true + } + for _, addon := range clusterConfig.Addons { + if cfg.IAM != nil && cfg.IAM.AutoCreatePodIdentityAssociations { + return true + } + if len(addon.PodIdentityAssociations) > 0 { + return true + } + } + return false + } + + if shallCreatePodIdentityAssociations(clusterConfig) { addonNames := []string{} for _, addon := range clusterConfig.Addons { addonNames = append(addonNames, addon.Name) diff --git a/pkg/ctl/create/addon.go b/pkg/ctl/create/addon.go index f89b21fe5d..286e03761d 100644 --- a/pkg/ctl/create/addon.go +++ b/pkg/ctl/create/addon.go @@ -13,7 +13,9 @@ import ( "github.com/spf13/pflag" "github.com/weaveworks/eksctl/pkg/actions/addon" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/awsapi" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" ) @@ -67,8 +69,8 @@ func createAddonCmd(cmd *cmdutils.Cmd) { return err } - if !oidcProviderExists { - logger.Warning("no IAM OIDC provider associated with cluster, try 'eksctl utils associate-iam-oidc-provider --region=%s --cluster=%s'", cmd.ClusterConfig.Metadata.Region, cmd.ClusterConfig.Metadata.Name) + if err := validatePodIdentityAgentAddon(ctx, clusterProvider.AWSProvider.EKS(), cmd.ClusterConfig); err != nil { + return err } stackManager := clusterProvider.NewStackManager(cmd.ClusterConfig) @@ -91,7 +93,24 @@ func createAddonCmd(cmd *cmdutils.Cmd) { return err } + // always install EKS Pod Identity Agent Addon first, if present, + // as other addons might require IAM permissions + for _, a := range cmd.ClusterConfig.Addons { + if a.CanonicalName() != api.PodIdentityAgentAddon { + continue + } + if force { //force is specified at cmdline level + a.Force = true + } + if err := addonManager.Create(ctx, a, cmd.ProviderConfig.WaitTimeout); err != nil { + return err + } + } + for _, a := range cmd.ClusterConfig.Addons { + if a.CanonicalName() == api.PodIdentityAgentAddon { + continue + } if force { //force is specified at cmdline level a.Force = true } @@ -103,3 +122,28 @@ func createAddonCmd(cmd *cmdutils.Cmd) { return nil } } + +func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg *api.ClusterConfig) error { + isPodIdentityAgentInstalled, err := podidentityassociation.IsPodIdentityAgentInstalled(ctx, eksAPI, cfg.Metadata.Name) + if err != nil { + return err + } + + shallCreatePodIdentityAssociations := cfg.IAM.AutoCreatePodIdentityAssociations + podIdentityAgentFoundInConfig := false + for _, a := range cfg.Addons { + if a.CanonicalName() == api.PodIdentityAgentAddon { + podIdentityAgentFoundInConfig = true + } + if len(a.PodIdentityAssociations) > 0 { + shallCreatePodIdentityAssociations = true + } + } + + if shallCreatePodIdentityAssociations && !isPodIdentityAgentInstalled && !podIdentityAgentFoundInConfig { + suggestion := fmt.Sprintf("please enable it using `eksctl create addon --cluster=%s --name=%s`, or by adding it to the config file", cfg.Metadata.Name, api.PodIdentityAgentAddon) + return api.ErrPodIdentityAgentNotInstalled(suggestion) + } + + return nil +} From cc94cf18fcb4ea73c529fee625524f3846076ab0 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Tue, 30 Apr 2024 13:57:37 +0300 Subject: [PATCH 03/35] ammend delete addon command to delete roles for pod identity associations --- pkg/actions/addon/addon.go | 8 ++++++++ pkg/actions/addon/create.go | 9 +++++---- pkg/actions/addon/delete.go | 30 ++++++++++++++++-------------- pkg/cfn/manager/api.go | 5 +++++ pkg/cfn/manager/iam.go | 1 - 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index fda2600207..e620188d47 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -138,6 +138,14 @@ func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon return versions[0].Original(), requireIAMPermissions, nil } +func (a *Manager) makeAddonIRSAName(name string) string { + return fmt.Sprintf("eksctl-%s-addon-%s-IRSA", a.clusterConfig.Metadata.Name, name) +} + +func (a *Manager) makeAddonPodIdentityName(addonName, serviceAccountName string) string { + return fmt.Sprintf("eksctl-%s-addon-%s-podidentityrole-%s", a.clusterConfig.Metadata.Name, addonName, serviceAccountName) +} + func (a *Manager) makeAddonName(name string) string { return fmt.Sprintf("eksctl-%s-addon-%s", a.clusterConfig.Metadata.Name, name) } diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 68f5d0cf61..fce8643583 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -315,7 +315,8 @@ func (a *Manager) createRoleForPodIdentity(ctx context.Context, addonName string if err := resourceSet.AddAllResources(); err != nil { return "", err } - if err := a.createStack(ctx, resourceSet, addonName, fmt.Sprintf("podidentityrole-%s", pia.ServiceAccountName)); err != nil { + if err := a.createStack(ctx, resourceSet, addonName, + a.makeAddonPodIdentityName(addonName, pia.ServiceAccountName)); err != nil { return "", err } return pia.RoleARN, nil @@ -327,7 +328,8 @@ func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, names if err != nil { return "", err } - if err := a.createStack(ctx, resourceSet, addon.Name, "IRSA"); err != nil { + if err := a.createStack(ctx, resourceSet, addon.Name, + a.makeAddonIRSAName(addon.Name)); err != nil { return "", err } return resourceSet.OutputRole, nil @@ -348,14 +350,13 @@ func (a *Manager) createRoleResourceSet(addon *api.Addon, namespace, serviceAcco return resourceSet, resourceSet.AddAllResources() } -func (a *Manager) createStack(ctx context.Context, resourceSet builder.ResourceSetReader, addonName, stackNameSuffix string) error { +func (a *Manager) createStack(ctx context.Context, resourceSet builder.ResourceSetReader, addonName, stackName string) error { errChan := make(chan error) tags := map[string]string{ api.AddonNameTag: addonName, } - stackName := fmt.Sprintf("%s-%s", a.makeAddonName(addonName), stackNameSuffix) err := a.stackManager.CreateStack(ctx, stackName, resourceSet, tags, nil, errChan) if err != nil { return err diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index c66d0cf584..cf76d31b9c 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -4,14 +4,13 @@ import ( "context" "errors" "fmt" + "strings" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" - "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/utils/tasks" ) @@ -32,24 +31,27 @@ func (a *Manager) Delete(ctx context.Context, addon *api.Addon) error { logger.Info("deleted addon: %s", addon.Name) } - stack, err := a.stackManager.DescribeStack(ctx, &manager.Stack{StackName: aws.String(a.makeAddonName(addon.Name))}) + deleteAddonIAMTasks, err := NewRemover(a.stackManager).DeleteAddonIAMTasks(ctx, true) if err != nil { - if !manager.IsStackDoesNotExistError(err) { - return fmt.Errorf("failed to get stack: %w", err) - } + return err } - if stack != nil { + if deleteAddonIAMTasks.Len() > 0 { logger.Info("deleting associated IAM stacks") - if _, err = a.stackManager.DeleteStackBySpec(ctx, stack); err != nil { - return fmt.Errorf("failed to delete cloudformation stack %q: %v", a.makeAddonName(addon.Name), err) + logger.Info(deleteAddonIAMTasks.Describe()) + if errs := deleteAddonIAMTasks.DoAllSync(); len(errs) > 0 { + var allErrs []string + for _, err := range errs { + allErrs = append(allErrs, err.Error()) + } + return fmt.Errorf(strings.Join(allErrs, "\n")) } + logger.Info("all tasks were completed successfully") + } else if addonExists { + logger.Info("no associated IAM stacks found") } else { - if addonExists { - logger.Info("no associated IAM stacks found") - } else { - return errors.New("could not find addon or associated IAM stack to delete") - } + return errors.New("could not find addon or associated IAM stack to delete") } + return nil } diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index 625ad3eba9..fee9d026b8 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -511,6 +511,11 @@ func (c *StackCollection) ListClusterStackNames(ctx context.Context) ([]string, return c.ListStackNames(ctx, clusterStackRegex) } +// ListAccessEntryStackNames lists the stack names for all access entries in the specified cluster. +func (c *StackCollection) ListAddonIAMStackNames(ctx context.Context, clusterName, addonName string) ([]string, error) { + return c.ListStackNames(ctx, fmt.Sprintf("^eksctl-%s-addon-%s-*", clusterName, addonName)) +} + // ListAccessEntryStackNames lists the stack names for all access entries in the specified cluster. func (c *StackCollection) ListAccessEntryStackNames(ctx context.Context, clusterName string) ([]string, error) { return c.ListStackNames(ctx, fmt.Sprintf("^eksctl-%s-accessentry-*", clusterName)) diff --git a/pkg/cfn/manager/iam.go b/pkg/cfn/manager/iam.go index c688348434..7758964f1f 100644 --- a/pkg/cfn/manager/iam.go +++ b/pkg/cfn/manager/iam.go @@ -148,7 +148,6 @@ func (c *StackCollection) GetIAMAddonsStacks(ctx context.Context) ([]*Stack, err iamAddonStacks = append(iamAddonStacks, s) } } - logger.Debug("iamserviceaccounts = %v", iamAddonStacks) return iamAddonStacks, nil } From a645a6e4a0571eec730b1b475be9cde9d547ea02 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Tue, 30 Apr 2024 20:07:55 +0300 Subject: [PATCH 04/35] small tweaks --- pkg/actions/addon/create.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index fce8643583..e298c089e1 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -78,7 +78,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time getRecommendedPolicies := func(ctx context.Context, addon *api.Addon) ([]ekstypes.AddonPodIdentityConfiguration, error) { output, err := a.eksAPI.DescribeAddonConfiguration(ctx, &eks.DescribeAddonConfigurationInput{ AddonName: &addon.Name, - AddonVersion: &addon.Version, + AddonVersion: &version, }) if err != nil { return nil, fmt.Errorf("describing configuration for addon %s: %w", addon.Name, err) @@ -89,7 +89,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time if requiresIAMPermissions { switch { case len(addon.PodIdentityAssociations) > 0: - logger.Info("pod identity associations were specified for addon %s, will use those to provide required IAM permissions, other settings such as IRSA will be ignored") + logger.Info("pod identity associations were specified for addon %s, will use those to provide required IAM permissions, other settings such as IRSA will be ignored", addon.Name) for _, pia := range addon.PodIdentityAssociations { roleARN, err := a.createRoleForPodIdentity(ctx, addon.Name, pia) if err != nil { From 3c535efebacd63fa6e56f1d078be23098dcc1c26 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 30 Apr 2024 04:11:27 +0530 Subject: [PATCH 05/35] Support updating podIdentityAssociations for addons --- .mockery.yaml | 17 + pkg/actions/addon/create.go | 2 +- pkg/actions/addon/mocks/IAMRoleCreator.go | 57 +++ pkg/actions/addon/mocks/IAMRoleUpdater.go | 64 +++ .../addon/mocks/PodIdentityIAMUpdater.go | 61 +++ pkg/actions/addon/podidentityassociation.go | 98 ++++ .../addon/podidentityassociation_test.go | 471 ++++++++++++++++++ pkg/actions/addon/update.go | 32 +- pkg/actions/addon/update_test.go | 36 +- pkg/actions/podidentityassociation/creator.go | 20 +- .../iam_role_creator.go | 45 ++ .../iam_role_updater.go | 114 +++++ pkg/actions/podidentityassociation/tasks.go | 33 -- pkg/actions/podidentityassociation/updater.go | 149 +++--- pkg/cfn/manager/api.go | 4 +- pkg/cfn/manager/mocks/NodeGroupResourceSet.go | 20 + pkg/ctl/cmdutils/pod_identity_association.go | 11 +- pkg/ctl/update/addon.go | 16 +- 18 files changed, 1087 insertions(+), 163 deletions(-) create mode 100644 pkg/actions/addon/mocks/IAMRoleCreator.go create mode 100644 pkg/actions/addon/mocks/IAMRoleUpdater.go create mode 100644 pkg/actions/addon/mocks/PodIdentityIAMUpdater.go create mode 100644 pkg/actions/addon/podidentityassociation.go create mode 100644 pkg/actions/addon/podidentityassociation_test.go create mode 100644 pkg/actions/podidentityassociation/iam_role_creator.go create mode 100644 pkg/actions/podidentityassociation/iam_role_updater.go diff --git a/.mockery.yaml b/.mockery.yaml index 89ece3fdd4..99fca2b58a 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -48,3 +48,20 @@ packages: config: dir: "{{.InterfaceDir}}/mocks" outpkg: mocks + + github.com/weaveworks/eksctl/pkg/actions/addon: + interfaces: + IAMRoleCreator: + config: + dir: "{{.InterfaceDir}}/mocks" + outpkg: mocks + + IAMRoleUpdater: + config: + dir: "{{.InterfaceDir}}/mocks" + outpkg: mocks + + PodIdentityIAMUpdater: + config: + dir: "{{.InterfaceDir}}/mocks" + outpkg: mocks diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index e298c089e1..026d7b2080 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -296,7 +296,7 @@ func (a *Manager) patchAWSNodeDaemonSet(ctx context.Context) error { } func (a *Manager) getKnownServiceAccountLocation(addon *api.Addon) (string, string) { - // API isn't case sensitive + // API isn't case-sensitive. switch addon.CanonicalName() { case api.VPCCNIAddon: logger.Debug("found known service account location %s/%s", api.AWSNodeMeta.Namespace, api.AWSNodeMeta.Name) diff --git a/pkg/actions/addon/mocks/IAMRoleCreator.go b/pkg/actions/addon/mocks/IAMRoleCreator.go new file mode 100644 index 0000000000..7d4ab28546 --- /dev/null +++ b/pkg/actions/addon/mocks/IAMRoleCreator.go @@ -0,0 +1,57 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + v1alpha5 "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +// IAMRoleCreator is an autogenerated mock type for the IAMRoleCreator type +type IAMRoleCreator struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, podIdentityAssociation +func (_m *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *v1alpha5.PodIdentityAssociation) (string, error) { + ret := _m.Called(ctx, podIdentityAssociation) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha5.PodIdentityAssociation) (string, error)); ok { + return rf(ctx, podIdentityAssociation) + } + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha5.PodIdentityAssociation) string); ok { + r0 = rf(ctx, podIdentityAssociation) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *v1alpha5.PodIdentityAssociation) error); ok { + r1 = rf(ctx, podIdentityAssociation) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewIAMRoleCreator creates a new instance of IAMRoleCreator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIAMRoleCreator(t interface { + mock.TestingT + Cleanup(func()) +}) *IAMRoleCreator { + mock := &IAMRoleCreator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/actions/addon/mocks/IAMRoleUpdater.go b/pkg/actions/addon/mocks/IAMRoleUpdater.go new file mode 100644 index 0000000000..87a464377d --- /dev/null +++ b/pkg/actions/addon/mocks/IAMRoleUpdater.go @@ -0,0 +1,64 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + podidentityassociation "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" +) + +// IAMRoleUpdater is an autogenerated mock type for the IAMRoleUpdater type +type IAMRoleUpdater struct { + mock.Mock +} + +// Update provides a mock function with given fields: ctx, updateConfig, podIdentityAssociationID +func (_m *IAMRoleUpdater) Update(ctx context.Context, updateConfig *podidentityassociation.UpdateConfig, podIdentityAssociationID string) (string, bool, error) { + ret := _m.Called(ctx, updateConfig, podIdentityAssociationID) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 string + var r1 bool + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, *podidentityassociation.UpdateConfig, string) (string, bool, error)); ok { + return rf(ctx, updateConfig, podIdentityAssociationID) + } + if rf, ok := ret.Get(0).(func(context.Context, *podidentityassociation.UpdateConfig, string) string); ok { + r0 = rf(ctx, updateConfig, podIdentityAssociationID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, *podidentityassociation.UpdateConfig, string) bool); ok { + r1 = rf(ctx, updateConfig, podIdentityAssociationID) + } else { + r1 = ret.Get(1).(bool) + } + + if rf, ok := ret.Get(2).(func(context.Context, *podidentityassociation.UpdateConfig, string) error); ok { + r2 = rf(ctx, updateConfig, podIdentityAssociationID) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// NewIAMRoleUpdater creates a new instance of IAMRoleUpdater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIAMRoleUpdater(t interface { + mock.TestingT + Cleanup(func()) +}) *IAMRoleUpdater { + mock := &IAMRoleUpdater{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go new file mode 100644 index 0000000000..1fc1c10dbc --- /dev/null +++ b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go @@ -0,0 +1,61 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + types "github.com/aws/aws-sdk-go-v2/service/eks/types" + mock "github.com/stretchr/testify/mock" + + v1alpha5 "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +// PodIdentityIAMUpdater is an autogenerated mock type for the PodIdentityIAMUpdater type +type PodIdentityIAMUpdater struct { + mock.Mock +} + +// UpdateRole provides a mock function with given fields: ctx, podIdentityAssociations +func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []v1alpha5.PodIdentityAssociation) ([]types.AddonPodIdentityAssociations, error) { + ret := _m.Called(ctx, podIdentityAssociations) + + if len(ret) == 0 { + panic("no return value specified for UpdateRole") + } + + var r0 []types.AddonPodIdentityAssociations + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation) ([]types.AddonPodIdentityAssociations, error)); ok { + return rf(ctx, podIdentityAssociations) + } + if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation) []types.AddonPodIdentityAssociations); ok { + r0 = rf(ctx, podIdentityAssociations) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]types.AddonPodIdentityAssociations) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []v1alpha5.PodIdentityAssociation) error); ok { + r1 = rf(ctx, podIdentityAssociations) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewPodIdentityIAMUpdater creates a new instance of PodIdentityIAMUpdater. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewPodIdentityIAMUpdater(t interface { + mock.TestingT + Cleanup(func()) +}) *PodIdentityIAMUpdater { + mock := &PodIdentityIAMUpdater{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go new file mode 100644 index 0000000000..b16762989a --- /dev/null +++ b/pkg/actions/addon/podidentityassociation.go @@ -0,0 +1,98 @@ +package addon + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +type PodIdentityStackLister interface { + ListPodIdentityStackNames(ctx context.Context) ([]string, error) +} + +type EKSPodIdentityDescriber interface { + ListPodIdentityAssociations(ctx context.Context, params *eks.ListPodIdentityAssociationsInput, optFns ...func(*eks.Options)) (*eks.ListPodIdentityAssociationsOutput, error) + DescribePodIdentityAssociation(ctx context.Context, params *eks.DescribePodIdentityAssociationInput, optFns ...func(*eks.Options)) (*eks.DescribePodIdentityAssociationOutput, error) +} + +type IAMRoleCreator interface { + Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation) (roleARN string, err error) +} + +type IAMRoleUpdater interface { + Update(ctx context.Context, updateConfig *podidentityassociation.UpdateConfig, podIdentityAssociationID string) (roleARN string, hasChanged bool, err error) +} + +// PodIdentityAssociationUpdater creates or updates IAM resources for pod identities associated with an addon. +type PodIdentityAssociationUpdater struct { + ClusterName string + IAMRoleCreator IAMRoleCreator + IAMRoleUpdater IAMRoleUpdater + PodIdentityStackLister PodIdentityStackLister + EKSPodIdentityDescriber EKSPodIdentityDescriber +} + +// TODO + +func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) ([]ekstypes.AddonPodIdentityAssociations, error) { + var addonPodIdentityAssociations []ekstypes.AddonPodIdentityAssociations + for _, pia := range podIdentityAssociations { + output, err := p.EKSPodIdentityDescriber.ListPodIdentityAssociations(ctx, &eks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(p.ClusterName), + Namespace: aws.String(pia.Namespace), + ServiceAccount: aws.String(pia.ServiceAccountName), + }) + if err != nil { + return nil, fmt.Errorf("listing pod identity associations: %w", err) + } + roleARN := pia.RoleARN + switch len(output.Associations) { + default: + // TODO: does the API return a not found error if no association exists? + return nil, fmt.Errorf("expected to find exactly 1 pod identity association for %s; got %d", pia.NameString(), len(output.Associations)) + case 0: + // Create IAM resources. + if roleARN == "" { + var err error + if roleARN, err = p.IAMRoleCreator.Create(ctx, &pia); err != nil { + return nil, err + } + } + case 1: + // Update IAM resources if required. + output, err := p.EKSPodIdentityDescriber.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ + ClusterName: aws.String(p.ClusterName), + AssociationId: output.Associations[0].AssociationId, + }) + if err != nil { + return nil, err + } + // TODO: avoid repeating this call. + roleStackNames, err := p.PodIdentityStackLister.ListPodIdentityStackNames(ctx) + if err != nil { + return nil, fmt.Errorf("error listing stack names for pod identity associations: %w", err) + } + updateConfig, err := podidentityassociation.MakeRoleUpdateConfig(pia, *output.Association, roleStackNames) + if err != nil { + return nil, err + } + if updateConfig.HasIAMResourcesStack { + // TODO: if no pod identity has changed, skip update? + if roleARN, _, err = p.IAMRoleUpdater.Update(ctx, updateConfig, *output.Association.AssociationId); err != nil { + return nil, err + } + } + } + addonPodIdentityAssociations = append(addonPodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ + RoleArn: aws.String(roleARN), + ServiceAccount: aws.String(pia.ServiceAccountName), + }) + } + return addonPodIdentityAssociations, nil +} diff --git a/pkg/actions/addon/podidentityassociation_test.go b/pkg/actions/addon/podidentityassociation_test.go new file mode 100644 index 0000000000..3a62be8ba2 --- /dev/null +++ b/pkg/actions/addon/podidentityassociation_test.go @@ -0,0 +1,471 @@ +package addon_test + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/stretchr/testify/mock" + "github.com/weaveworks/eksctl/pkg/actions/addon" + "github.com/weaveworks/eksctl/pkg/actions/addon/mocks" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/fakes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + managerfakes "github.com/weaveworks/eksctl/pkg/cfn/manager/fakes" + "github.com/weaveworks/eksctl/pkg/eks/mocksv2" + "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" +) + +var _ = Describe("Update Pod Identity Association", func() { + type piaMocks struct { + stackManager *fakes.FakeStackUpdater + roleCreator *mocks.IAMRoleCreator + roleUpdater *mocks.IAMRoleUpdater + eks *mocksv2.EKS + } + type updateEntry struct { + podIdentityAssociations []api.PodIdentityAssociation + mockCalls func(m piaMocks) + + expectedCalls func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) + expectedAddonPodIdentityAssociations []ekstypes.AddonPodIdentityAssociations + + expectedErr string + } + + const clusterName = "test" + + makeID := func(i int) string { + return fmt.Sprintf("a-%d", i+1) + } + type listPodIdentityInput struct { + namespace string + serviceAccount string + } + defaultListPodIdentityInputs := []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + }, + { + namespace: "kube-system", + serviceAccount: "aws-ebs-csi-driver", + }, + { + namespace: "karpenter", + serviceAccount: "karpenter", + }, + } + mockListPodIdentityAssociations := func(eksAPI *mocksv2.EKS, hasAssociation bool, listInputs []listPodIdentityInput) { + for i, listInput := range listInputs { + var associations []ekstypes.PodIdentityAssociationSummary + if hasAssociation { + associations = []ekstypes.PodIdentityAssociationSummary{ + { + Namespace: aws.String(listInput.namespace), + ServiceAccount: aws.String(listInput.serviceAccount), + AssociationId: aws.String(makeID(i)), + }, + } + } + eksAPI.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(clusterName), + Namespace: aws.String(listInput.namespace), + ServiceAccount: aws.String(listInput.serviceAccount), + }).Return(&eks.ListPodIdentityAssociationsOutput{ + Associations: associations, + }, nil).Once() + } + } + + mockDescribePodIdentityAssociation := func(eksAPI *mocksv2.EKS, roleARNs ...string) { + for i, roleARN := range roleARNs { + id := aws.String(makeID(i)) + eksAPI.On("DescribePodIdentityAssociation", mock.Anything, &eks.DescribePodIdentityAssociationInput{ + ClusterName: aws.String(clusterName), + AssociationId: id, + }).Return(&eks.DescribePodIdentityAssociationOutput{ + Association: &ekstypes.PodIdentityAssociation{ + AssociationId: id, + RoleArn: aws.String(roleARN), + }, + }, nil).Once() + } + } + + DescribeTable("update pod identity association", func(e updateEntry) { + provider := mockprovider.NewMockProvider() + var ( + roleCreator mocks.IAMRoleCreator + roleUpdater mocks.IAMRoleUpdater + stackUpdater fakes.FakeStackUpdater + ) + + piaUpdater := &addon.PodIdentityAssociationUpdater{ + ClusterName: clusterName, + IAMRoleCreator: &roleCreator, + IAMRoleUpdater: &roleUpdater, + PodIdentityStackLister: &stackUpdater, + EKSPodIdentityDescriber: provider.MockEKS(), + } + if e.mockCalls != nil { + e.mockCalls(piaMocks{ + stackManager: &stackUpdater, + roleCreator: &roleCreator, + roleUpdater: &roleUpdater, + eks: provider.MockEKS(), + }) + } + addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations) + if e.expectedErr != "" { + Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) + return + } + Expect(err).NotTo(HaveOccurred()) + Expect(addonPodIdentityAssociations).To(Equal(e.expectedAddonPodIdentityAssociations)) + t := GinkgoT() + roleCreator.AssertExpectations(t) + roleUpdater.AssertExpectations(t) + provider.MockEKS().AssertExpectations(t) + }, + Entry("addon contains pod identity that does not exist", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + }, + mockCalls: func(m piaMocks) { + m.eks.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(clusterName), + Namespace: aws.String("kube-system"), + ServiceAccount: aws.String("vpc-cni"), + }).Return(&eks.ListPodIdentityAssociationsOutput{}, nil) + + m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }).Return("role-1", nil) + + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("role-1"), + }, + }, + }), + + Entry("addon contains pod identities, some of which do not exist", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + }, + { + Namespace: "karpenter", + ServiceAccountName: "karpenter", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + }, + }) + mockDescribePodIdentityAssociation(m.eks, "cni-role") + mockListPodIdentityAssociations(m.eks, false, []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "aws-ebs-csi-driver", + }, + { + namespace: "karpenter", + serviceAccount: "karpenter", + }, + }) + + m.roleUpdater.On("Update", mock.Anything, &podidentityassociation.UpdateConfig{ + PodIdentityAssociation: api.PodIdentityAssociation{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + AssociationID: "a-1", + HasIAMResourcesStack: true, + StackName: "kube-system-vpc-cni", + }, "a-1").Return("cni-role-2", false, nil).Once() + m.stackManager.ListPodIdentityStackNamesReturns([]string{"kube-system-vpc-cni", "extra-stack"}, nil) + + m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + }).Return("csi-role", nil).Once() + m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ + Namespace: "karpenter", + ServiceAccountName: "karpenter", + }).Return("karpenter-role", nil).Once() + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("cni-role-2"), + }, + { + ServiceAccount: aws.String("aws-ebs-csi-driver"), + RoleArn: aws.String("csi-role"), + }, + { + ServiceAccount: aws.String("karpenter"), + RoleArn: aws.String("karpenter-role"), + }, + }, + }), + + Entry("addon contains pod identities that already exist", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + }, + { + Namespace: "karpenter", + ServiceAccountName: "karpenter", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, true, defaultListPodIdentityInputs) + mockDescribePodIdentityAssociation(m.eks, "cni-role", "csi-role", "karpenter-role") + + for i, updateInput := range []struct { + namespace string + serviceAccount string + hasIAMResourcesStack bool + stackName string + returnRole string + }{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + hasIAMResourcesStack: true, + stackName: "kube-system-vpc-cni", + returnRole: "cni-role-2", + }, + { + namespace: "kube-system", + serviceAccount: "aws-ebs-csi-driver", + hasIAMResourcesStack: true, + stackName: "kube-system-aws-ebs-csi-driver", + returnRole: "csi-role-2", + }, + { + namespace: "karpenter", + serviceAccount: "karpenter", + hasIAMResourcesStack: true, + stackName: "karpenter-karpenter", + returnRole: "karpenter-role-2", + }, + } { + id := makeID(i) + m.roleUpdater.On("Update", mock.Anything, &podidentityassociation.UpdateConfig{ + PodIdentityAssociation: api.PodIdentityAssociation{ + Namespace: updateInput.namespace, + ServiceAccountName: updateInput.serviceAccount, + }, + AssociationID: id, + HasIAMResourcesStack: true, + StackName: updateInput.stackName, + }, id).Return(updateInput.returnRole, false, nil).Once() + } + m.stackManager.ListPodIdentityStackNamesReturns([]string{"kube-system-vpc-cni", "kube-system-aws-ebs-csi-driver", "karpenter-karpenter", "extra-stack"}, nil) + + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("cni-role-2"), + }, + { + ServiceAccount: aws.String("aws-ebs-csi-driver"), + RoleArn: aws.String("csi-role-2"), + }, + { + ServiceAccount: aws.String("karpenter"), + RoleArn: aws.String("karpenter-role-2"), + }, + }, + }), + + Entry("addon contains pod identities that do not exist and have a pre-existing roleARN", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + RoleARN: "role-1", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + RoleARN: "role-2", + }, + { + Namespace: "karpenter", + ServiceAccountName: "karpenter", + RoleARN: "role-3", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, false, defaultListPodIdentityInputs) + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("role-1"), + }, + { + ServiceAccount: aws.String("aws-ebs-csi-driver"), + RoleArn: aws.String("role-2"), + }, + { + ServiceAccount: aws.String("karpenter"), + RoleArn: aws.String("role-3"), + }, + }, + }), + + Entry("addon contains pod identities that already exist and have a pre-existing roleARN", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + RoleARN: "role-1", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + RoleARN: "role-2", + }, + { + Namespace: "karpenter", + ServiceAccountName: "karpenter", + RoleARN: "role-3", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, false, defaultListPodIdentityInputs) + + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("role-1"), + }, + { + ServiceAccount: aws.String("aws-ebs-csi-driver"), + RoleArn: aws.String("role-2"), + }, + { + ServiceAccount: aws.String("karpenter"), + RoleArn: aws.String("role-3"), + }, + }, + }), + + Entry("addon contains pod identities created by eksctl but are being updated with a new roleARN", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + RoleARN: "role-1", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + RoleARN: "role-2", + }, + { + Namespace: "karpenter", + ServiceAccountName: "karpenter", + RoleARN: "karpenter-role", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + }, + { + namespace: "kube-system", + serviceAccount: "aws-ebs-csi-driver", + }, + { + namespace: "karpenter", + serviceAccount: "karpenter", + }, + }) + mockDescribePodIdentityAssociation(m.eks, "role-1", "role-2", "role-3") + m.stackManager.ListPodIdentityStackNamesReturns([]string{"karpenter-karpenter"}, nil) + }, + expectedErr: "cannot change podIdentityAssociation.roleARN since the role was created by eksctl", + }), + + Entry("addon contains pod identity created with a pre-existing roleARN and is being updated", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + RoleARN: "vpc-cni-role-2", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + }, + }) + mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") + + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + RoleArn: aws.String("vpc-cni-role-2"), + ServiceAccount: aws.String("vpc-cni"), + }, + }, + }), + + Entry("addon contains pod identity created with a pre-existing roleARN but it is no longer set", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + }, + }) + mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") + }, + expectedErr: "podIdentityAssociation.roleARN is required since the role was not created by eksctl", + }), + ) +}) diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index b6aec20ed7..547040128e 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -15,7 +15,13 @@ import ( "github.com/weaveworks/eksctl/pkg/cfn/manager" ) -func (a *Manager) Update(ctx context.Context, addon *api.Addon, waitTimeout time.Duration) error { +// PodIdentityIAMUpdater creates or updates IAM resources for pod identity associations. +type PodIdentityIAMUpdater interface { + // UpdateRole creates or updates IAM resources for podIdentityAssociations. + UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) ([]ekstypes.AddonPodIdentityAssociations, error) +} + +func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUpdater PodIdentityIAMUpdater, waitTimeout time.Duration) error { logger.Debug("addon: %v", addon) var configurationValues *string @@ -59,18 +65,24 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, waitTimeout time updateAddonInput.AddonVersion = &version } - //check if we have been provided a different set of policies/role - if addon.ServiceAccountRoleARN != "" { - updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN - } else if hasPoliciesSet(addon) { - serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) + if len(addon.PodIdentityAssociations) > 0 { + // TODO + addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, addon.PodIdentityAssociations) if err != nil { - return err + return fmt.Errorf("updating pod identity associations: %w", err) } - updateAddonInput.ServiceAccountRoleArn = &serviceAccountRoleARN + updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations } else { - //preserve current role - if summary.IAMRole != "" { + // check if we have been provided a different set of policies/role + if addon.ServiceAccountRoleARN != "" { + updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN + } else if hasPoliciesSet(addon) { + serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) + if err != nil { + return err + } + updateAddonInput.ServiceAccountRoleArn = &serviceAccountRoleARN + } else if summary.IAMRole != "" { // Preserve current role. updateAddonInput.ServiceAccountRoleArn = &summary.IAMRole } } diff --git a/pkg/actions/addon/update_test.go b/pkg/actions/addon/update_test.go index b0f4feb892..cc46dafbde 100644 --- a/pkg/actions/addon/update_test.go +++ b/pkg/actions/addon/update_test.go @@ -18,6 +18,7 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/addon/fakes" + "github.com/weaveworks/eksctl/pkg/actions/addon/mocks" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/builder" "github.com/weaveworks/eksctl/pkg/cfn/manager" @@ -105,6 +106,7 @@ var _ = Describe("Update", func() { }) When("EKS returns an UpdateAddonOutput", func() { + var podIdentityIAMUpdater mocks.PodIdentityIAMUpdater BeforeEach(func() { mockProvider.MockEKS().On("UpdateAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { Expect(args).To(HaveLen(2)) @@ -119,7 +121,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", Force: true, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) @@ -139,7 +141,7 @@ var _ = Describe("Update", func() { err := addonManager.Update(context.Background(), &api.Addon{ Name: "my-addon", Version: "", - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(output.String()).To(ContainSubstring("no new version provided, preserving existing version")) @@ -157,7 +159,7 @@ var _ = Describe("Update", func() { err := addonManager.Update(context.Background(), &api.Addon{ Name: "my-addon", Version: "1.7.5", - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) @@ -174,7 +176,7 @@ var _ = Describe("Update", func() { err := addonManager.Update(context.Background(), &api.Addon{ Name: "my-addon", Version: "latest", - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) @@ -192,7 +194,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "1.7.8", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(ContainSubstring("no version(s) found matching \"1.7.8\" for \"my-addon\""))) }) @@ -216,7 +218,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", Force: true, - }, waitTimeout) + }, &podIdentityIAMUpdater, waitTimeout) Expect(err).NotTo(HaveOccurred()) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) Expect(*describeAddonInput.AddonName).To(Equal("my-addon")) @@ -244,7 +246,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", Force: true, - }, waitTimeout) + }, &podIdentityIAMUpdater, waitTimeout) Expect(err).To(MatchError(`addon status transitioned to "DEGRADED"`)) }) }) @@ -257,7 +259,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", ServiceAccountRoleARN: "new-arn", - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) @@ -286,7 +288,7 @@ var _ = Describe("Update", func() { Name: "vpc-cni", Version: "v1.0.0-eksbuild.2", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) @@ -313,7 +315,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) @@ -355,7 +357,7 @@ var _ = Describe("Update", func() { AttachPolicy: api.InlineDocument{ "foo": "policy-bar", }, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) @@ -383,7 +385,7 @@ var _ = Describe("Update", func() { AttachPolicy: api.InlineDocument{ "foo": "policy-bar", }, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) @@ -413,7 +415,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", ResolveConflicts: rc, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(updateAddonInput.ResolveConflicts).To(Equal(rc)) @@ -430,7 +432,7 @@ var _ = Describe("Update", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.2", ConfigurationValues: "{\"replicaCount\":3}", - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) Expect(aws.ToString(updateAddonInput.ConfigurationValues)).To(Equal("{\"replicaCount\":3}")) @@ -456,7 +458,7 @@ var _ = Describe("Update", func() { WellKnownPolicies: api.WellKnownPolicies{ AutoScaler: true, }, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) @@ -484,7 +486,7 @@ var _ = Describe("Update", func() { WellKnownPolicies: api.WellKnownPolicies{ AutoScaler: true, }, - }, 0) + }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) @@ -519,7 +521,7 @@ var _ = Describe("Update", func() { err := addonManager.Update(context.Background(), &api.Addon{ Name: "my-addon", - }, 0) + }, &mocks.PodIdentityIAMUpdater{}, 0) Expect(err).To(MatchError(`failed to update addon "my-addon": foo`)) Expect(*updateAddonInput.ClusterName).To(Equal("my-cluster")) Expect(*updateAddonInput.AddonName).To(Equal("my-addon")) diff --git a/pkg/actions/podidentityassociation/creator.go b/pkg/actions/podidentityassociation/creator.go index f479a8c536..3605d6a7b1 100644 --- a/pkg/actions/podidentityassociation/creator.go +++ b/pkg/actions/podidentityassociation/creator.go @@ -52,12 +52,20 @@ func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api IsSubTask: true, } if pia.RoleARN == "" { - piaCreationTasks.Append(&createIAMRoleTask{ - ctx: ctx, - info: fmt.Sprintf("create IAM role for pod identity association for service account %q", pia.NameString()), - clusterName: c.clusterName, - podIdentityAssociation: &pia, - stackCreator: c.stackCreator, + piaCreationTasks.Append(&tasks.GenericTask{ + Description: fmt.Sprintf("create IAM role for pod identity association for service account %q", pia.NameString()), + Doer: func() error { + roleCreator := &IAMRoleCreator{ + ClusterName: c.clusterName, + StackCreator: c.stackCreator, + } + roleARN, err := roleCreator.Create(ctx, &pia) + if err != nil { + return err + } + pia.RoleARN = roleARN + return nil + }, }) } if pia.CreateServiceAccount { diff --git a/pkg/actions/podidentityassociation/iam_role_creator.go b/pkg/actions/podidentityassociation/iam_role_creator.go new file mode 100644 index 0000000000..be5e34f712 --- /dev/null +++ b/pkg/actions/podidentityassociation/iam_role_creator.go @@ -0,0 +1,45 @@ +package podidentityassociation + +import ( + "context" + "fmt" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/builder" +) + +type IAMRoleCreator struct { + ClusterName string + StackCreator StackCreator +} + +func (r *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation) (string, error) { + rs := builder.NewIAMRoleResourceSetForPodIdentity(podIdentityAssociation) + if err := rs.AddAllResources(); err != nil { + return "", err + } + if podIdentityAssociation.Tags == nil { + podIdentityAssociation.Tags = make(map[string]string) + } + podIdentityAssociation.Tags[api.PodIdentityAssociationNameTag] = Identifier{ + Namespace: podIdentityAssociation.Namespace, + ServiceAccountName: podIdentityAssociation.ServiceAccountName, + }.IDString() + + stackName := MakeStackName(r.ClusterName, podIdentityAssociation.Namespace, podIdentityAssociation.ServiceAccountName) + stackCh := make(chan error) + if err := r.StackCreator.CreateStack(ctx, stackName, rs, podIdentityAssociation.Tags, nil, stackCh); err != nil { + return "", fmt.Errorf("creating IAM role for pod identity association for service account %s in namespace %s: %w", + podIdentityAssociation.ServiceAccountName, podIdentityAssociation.Namespace, err) + } + select { + case err := <-stackCh: + if err != nil { + return "", err + } + return podIdentityAssociation.RoleARN, nil + case <-ctx.Done(): + return "", fmt.Errorf("timed out waiting for creation of IAM resources for pod identity association %s: %w", + podIdentityAssociation.NameString(), ctx.Err()) + } +} diff --git a/pkg/actions/podidentityassociation/iam_role_updater.go b/pkg/actions/podidentityassociation/iam_role_updater.go new file mode 100644 index 0000000000..9f02cec208 --- /dev/null +++ b/pkg/actions/podidentityassociation/iam_role_updater.go @@ -0,0 +1,114 @@ +package podidentityassociation + +import ( + "context" + "fmt" + "github.com/aws/aws-sdk-go-v2/aws" + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/kris-nova/logger" + "github.com/pkg/errors" + "github.com/weaveworks/eksctl/pkg/cfn/builder" + "github.com/weaveworks/eksctl/pkg/cfn/manager" + "golang.org/x/exp/slices" + "time" +) + +// IAMRoleUpdater updates IAM resources for pod identity associations. +type IAMRoleUpdater struct { + // StackUpdater updates CloudFormation stacks. + StackUpdater StackUpdater +} + +// Update updates IAM resources for updateConfig and returns an IAM role ARN upon success. The boolean return value reports +// whether the IAM resources have changed or not. +func (u *IAMRoleUpdater) Update(ctx context.Context, updateConfig *UpdateConfig, podIdentityAssociationID string) (string, bool, error) { + stack, err := u.StackUpdater.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(updateConfig.StackName), + }) + if err != nil { + return "", false, fmt.Errorf("describing IAM resources stack %q: %w", updateConfig.StackName, err) + } + if updateConfig.PodIdentityAssociation.RoleName != "" && !slices.Contains(stack.Capabilities, cfntypes.CapabilityCapabilityNamedIam) { + return "", false, errors.New("cannot update role name if the pod identity association was not created with a role name") + } + rs := builder.NewIAMRoleResourceSetForPodIdentity(&updateConfig.PodIdentityAssociation) + if err := rs.AddAllResources(); err != nil { + return "", false, fmt.Errorf("adding resources to CloudFormation template: %w", err) + } + template, err := rs.RenderJSON() + if err != nil { + return "", false, fmt.Errorf("generating CloudFormation template: %w", err) + } + if err := u.StackUpdater.MustUpdateStack(ctx, manager.UpdateStackOptions{ + StackName: updateConfig.StackName, + ChangeSetName: fmt.Sprintf("eksctl-%s-%s-update-%d", updateConfig.PodIdentityAssociation.Namespace, updateConfig.PodIdentityAssociation.ServiceAccountName, time.Now().Unix()), + Description: fmt.Sprintf("updating IAM resources stack %q for pod identity association %q", updateConfig.StackName, podIdentityAssociationID), + TemplateData: manager.TemplateBody(template), + Wait: true, + }); err != nil { + var noChangeErr *manager.NoChangeError + if errors.As(err, &noChangeErr) { + logger.Info("IAM resources for %q are already up-to-date", podIdentityAssociationID) + return updateConfig.PodIdentityAssociation.RoleARN, false, nil + } + return "", false, fmt.Errorf("updating IAM resources for pod identity association: %w", err) + } + logger.Info("updated IAM resources stack %q for %q", updateConfig.StackName, podIdentityAssociationID) + + stack, err = u.StackUpdater.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(updateConfig.StackName), + }) + if err != nil { + return "", false, fmt.Errorf("describing IAM resources stack: %w", err) + } + if err := rs.GetAllOutputs(*stack); err != nil { + return "", false, fmt.Errorf("error getting IAM role output from IAM resources stack: %w", err) + } + return updateConfig.PodIdentityAssociation.RoleARN, true, nil +} + +func (u *IAMRoleUpdater) updateStack(ctx context.Context, updateConfig *UpdateConfig, podIdentityAssociationID string) error { + stack, err := u.StackUpdater.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(updateConfig.StackName), + }) + if err != nil { + return fmt.Errorf("describing IAM resources stack %q: %w", updateConfig.StackName, err) + } + if updateConfig.PodIdentityAssociation.RoleName != "" && !slices.Contains(stack.Capabilities, cfntypes.CapabilityCapabilityNamedIam) { + return errors.New("cannot update role name if the pod identity association was not created with a role name") + } + rs := builder.NewIAMRoleResourceSetForPodIdentity(&updateConfig.PodIdentityAssociation) + if err := rs.AddAllResources(); err != nil { + return fmt.Errorf("adding resources to CloudFormation template: %w", err) + } + template, err := rs.RenderJSON() + if err != nil { + return fmt.Errorf("generating CloudFormation template: %w", err) + } + if err := u.StackUpdater.MustUpdateStack(ctx, manager.UpdateStackOptions{ + StackName: updateConfig.StackName, + ChangeSetName: fmt.Sprintf("eksctl-%s-%s-update-%d", updateConfig.PodIdentityAssociation.Namespace, updateConfig.PodIdentityAssociation.ServiceAccountName, time.Now().Unix()), + Description: fmt.Sprintf("updating IAM resources stack %q for pod identity association %q", updateConfig.StackName, podIdentityAssociationID), + TemplateData: manager.TemplateBody(template), + Wait: true, + }); err != nil { + var noChangeErr *manager.NoChangeError + if errors.As(err, &noChangeErr) { + logger.Info("IAM resources for %q are already up-to-date", podIdentityAssociationID) + return nil + } + return fmt.Errorf("updating IAM resources for pod identity association: %w", err) + } + logger.Info("updated IAM resources stack %q for %q", updateConfig.StackName, podIdentityAssociationID) + + stack, err = u.StackUpdater.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(updateConfig.StackName), + }) + if err != nil { + return fmt.Errorf("describing IAM resources stack: %w", err) + } + if err := rs.GetAllOutputs(*stack); err != nil { + return fmt.Errorf("error getting IAM role output from IAM resources stack: %w", err) + } + return nil +} diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index a130bea456..4d01a359e7 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -21,39 +21,6 @@ import ( "github.com/weaveworks/eksctl/pkg/utils/tasks" ) -type createIAMRoleTask struct { - ctx context.Context - info string - clusterName string - podIdentityAssociation *api.PodIdentityAssociation - stackCreator StackCreator -} - -func (t *createIAMRoleTask) Describe() string { - return t.info -} - -func (t *createIAMRoleTask) Do(errorCh chan error) error { - rs := builder.NewIAMRoleResourceSetForPodIdentity(t.podIdentityAssociation) - if err := rs.AddAllResources(); err != nil { - return err - } - if t.podIdentityAssociation.Tags == nil { - t.podIdentityAssociation.Tags = make(map[string]string) - } - t.podIdentityAssociation.Tags[api.PodIdentityAssociationNameTag] = Identifier{ - Namespace: t.podIdentityAssociation.Namespace, - ServiceAccountName: t.podIdentityAssociation.ServiceAccountName, - }.IDString() - - stackName := MakeStackName(t.clusterName, t.podIdentityAssociation.Namespace, t.podIdentityAssociation.ServiceAccountName) - if err := t.stackCreator.CreateStack(t.ctx, stackName, rs, t.podIdentityAssociation.Tags, nil, errorCh); err != nil { - return fmt.Errorf("creating IAM role for pod identity association for service account %s in namespace %s: %w", - t.podIdentityAssociation.ServiceAccountName, t.podIdentityAssociation.Namespace, err) - } - return nil -} - type createPodIdentityAssociationTask struct { ctx context.Context info string diff --git a/pkg/actions/podidentityassociation/updater.go b/pkg/actions/podidentityassociation/updater.go index ddd7c48702..df9c39de8f 100644 --- a/pkg/actions/podidentityassociation/updater.go +++ b/pkg/actions/podidentityassociation/updater.go @@ -5,19 +5,14 @@ import ( "errors" "fmt" "reflect" - "time" - - "golang.org/x/exp/slices" - - cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" - - "github.com/kris-nova/logger" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + + "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" - "github.com/weaveworks/eksctl/pkg/cfn/builder" "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/utils/apierrors" "github.com/weaveworks/eksctl/pkg/utils/tasks" @@ -50,11 +45,12 @@ type APIUpdater interface { UpdatePodIdentityAssociation(ctx context.Context, params *eks.UpdatePodIdentityAssociationInput, optFns ...func(*eks.Options)) (*eks.UpdatePodIdentityAssociationOutput, error) } -type updateConfig struct { - podIdentityAssociation api.PodIdentityAssociation - associationID string - hasIAMResourcesStack bool - stackName string +// UpdateConfig holds configuration for updating a pod identity association. +type UpdateConfig struct { + PodIdentityAssociation api.PodIdentityAssociation + AssociationID string + HasIAMResourcesStack bool + StackName string } // Update updates the specified pod identity associations. @@ -91,74 +87,42 @@ func (u *Updater) Update(ctx context.Context, podIdentityAssociations []api.PodI return runAllTasks(taskTree) } -func (u *Updater) update(ctx context.Context, updateConfig *updateConfig, podIdentityAssociationID string) error { - if !updateConfig.hasIAMResourcesStack { - return u.updatePodIdentityAssociation(ctx, updateConfig, podIdentityAssociationID) - } - - stack, err := u.StackUpdater.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(updateConfig.stackName), - }) - if err != nil { - return fmt.Errorf("describing IAM resources stack %q: %w", updateConfig.stackName, err) - } - if updateConfig.podIdentityAssociation.RoleName != "" && !slices.Contains(stack.Capabilities, cfntypes.CapabilityCapabilityNamedIam) { - return errors.New("cannot update role name if the pod identity association was not created with a role name") - } - rs := builder.NewIAMRoleResourceSetForPodIdentity(&updateConfig.podIdentityAssociation) - if err := rs.AddAllResources(); err != nil { - return fmt.Errorf("adding resources to CloudFormation template: %w", err) - } - template, err := rs.RenderJSON() - if err != nil { - return fmt.Errorf("generating CloudFormation template: %w", err) - } - if err := u.StackUpdater.MustUpdateStack(ctx, manager.UpdateStackOptions{ - StackName: updateConfig.stackName, - ChangeSetName: fmt.Sprintf("eksctl-%s-%s-update-%d", updateConfig.podIdentityAssociation.Namespace, updateConfig.podIdentityAssociation.ServiceAccountName, time.Now().Unix()), - Description: fmt.Sprintf("updating IAM resources stack %q for pod identity association %q", updateConfig.stackName, podIdentityAssociationID), - TemplateData: manager.TemplateBody(template), - Wait: true, - }); err != nil { - if _, ok := err.(*manager.NoChangeError); ok { - logger.Info("IAM resources for %q are already up-to-date", podIdentityAssociationID) +func (u *Updater) update(ctx context.Context, updateConfig *UpdateConfig, podIdentityAssociationID string) error { + roleARN := updateConfig.PodIdentityAssociation.RoleARN + if updateConfig.HasIAMResourcesStack { + roleUpdater := &IAMRoleUpdater{ + StackUpdater: u.StackUpdater, + } + newRoleARN, hasChanged, err := roleUpdater.Update(ctx, updateConfig, podIdentityAssociationID) + if err != nil { + return err + } + if !hasChanged { return nil } - return fmt.Errorf("updating IAM resources for pod identity association: %w", err) - } - logger.Info("updated IAM resources stack %q for %q", updateConfig.stackName, podIdentityAssociationID) - stack, err = u.StackUpdater.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(updateConfig.stackName), - }) - if err != nil { - return fmt.Errorf("describing IAM resources stack: %w", err) + roleARN = newRoleARN } - if err := rs.GetAllOutputs(*stack); err != nil { - return fmt.Errorf("error getting IAM role output from IAM resources stack: %w", err) - } - - return u.updatePodIdentityAssociation(ctx, updateConfig, podIdentityAssociationID) + return u.updatePodIdentityAssociation(ctx, roleARN, updateConfig, podIdentityAssociationID) } -func (u *Updater) updatePodIdentityAssociation(ctx context.Context, updateConfig *updateConfig, podIdentityAssociationID string) error { - roleARN := updateConfig.podIdentityAssociation.RoleARN +func (u *Updater) updatePodIdentityAssociation(ctx context.Context, roleARN string, updateConfig *UpdateConfig, podIdentityAssociationID string) error { if _, err := u.APIUpdater.UpdatePodIdentityAssociation(ctx, &eks.UpdatePodIdentityAssociationInput{ - AssociationId: aws.String(updateConfig.associationID), + AssociationId: aws.String(updateConfig.AssociationID), ClusterName: aws.String(u.ClusterName), RoleArn: aws.String(roleARN), }); err != nil { - return fmt.Errorf("updating pod identity association (associationID: %s, roleARN: %s): %w", updateConfig.associationID, roleARN, err) + return fmt.Errorf("updating pod identity association (associationID: %s, roleARN: %s): %w", updateConfig.AssociationID, roleARN, err) } logger.Info("updated role ARN %q for pod identity association %q", roleARN, podIdentityAssociationID) return nil } -func (u *Updater) makeUpdate(ctx context.Context, p api.PodIdentityAssociation, roleStackNames []string) (*updateConfig, error) { +func (u *Updater) makeUpdate(ctx context.Context, pia api.PodIdentityAssociation, roleStackNames []string) (*UpdateConfig, error) { const notFoundErrMsg = "pod identity association does not exist" output, err := u.APIUpdater.ListPodIdentityAssociations(ctx, &eks.ListPodIdentityAssociationsInput{ ClusterName: aws.String(u.ClusterName), - Namespace: aws.String(p.Namespace), - ServiceAccount: aws.String(p.ServiceAccountName), + Namespace: aws.String(pia.Namespace), + ServiceAccount: aws.String(pia.ServiceAccountName), }) if err != nil { if apierrors.IsNotFoundError(err) { @@ -179,32 +143,37 @@ func (u *Updater) makeUpdate(ctx context.Context, p api.PodIdentityAssociation, if err != nil { return nil, fmt.Errorf("error describing pod identity association: %w", err) } - stackName, hasStack := getIAMResourcesStack(roleStackNames, Identifier{ - Namespace: p.Namespace, - ServiceAccountName: p.ServiceAccountName, - }) - if hasStack { - if describeOutput.Association.RoleArn != nil && p.RoleARN != "" && p.RoleARN != *describeOutput.Association.RoleArn { - return nil, errors.New("cannot change podIdentityAssociation.roleARN since the role was created by eksctl") - } - } else { - if p.RoleARN == "" { - return nil, errors.New("podIdentityAssociation.roleARN is required since the role was not created by eksctl") - } - podIDWithRoleARN := api.PodIdentityAssociation{ - Namespace: p.Namespace, - ServiceAccountName: p.ServiceAccountName, - RoleARN: p.RoleARN, - } - if !reflect.DeepEqual(p, podIDWithRoleARN) { - return nil, errors.New("only namespace, serviceAccountName and roleARN can be specified if the role was not created by eksctl") - } + return MakeRoleUpdateConfig(pia, *describeOutput.Association, roleStackNames) + } +} + +// MakeRoleUpdateConfig builds an UpdateConfig for pia. +func MakeRoleUpdateConfig(pia api.PodIdentityAssociation, association ekstypes.PodIdentityAssociation, roleStackNames []string) (*UpdateConfig, error) { + stackName, hasStack := getIAMResourcesStack(roleStackNames, Identifier{ + Namespace: pia.Namespace, + ServiceAccountName: pia.ServiceAccountName, + }) + if hasStack { + if association.RoleArn != nil && pia.RoleARN != "" && pia.RoleARN != *association.RoleArn { + return nil, errors.New("cannot change podIdentityAssociation.roleARN since the role was created by eksctl") + } + } else { + if pia.RoleARN == "" { + return nil, errors.New("podIdentityAssociation.roleARN is required since the role was not created by eksctl") + } + podIDWithRoleARN := api.PodIdentityAssociation{ + Namespace: pia.Namespace, + ServiceAccountName: pia.ServiceAccountName, + RoleARN: pia.RoleARN, + } + if !reflect.DeepEqual(pia, podIDWithRoleARN) { + return nil, errors.New("only namespace, serviceAccountName and roleARN can be specified if the role was not created by eksctl") } - return &updateConfig{ - podIdentityAssociation: p, - associationID: *describeOutput.Association.AssociationId, - hasIAMResourcesStack: hasStack, - stackName: stackName, - }, nil } + return &UpdateConfig{ + PodIdentityAssociation: pia, + AssociationID: *association.AssociationId, + HasIAMResourcesStack: hasStack, + StackName: stackName, + }, nil } diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index fee9d026b8..71113ae9e3 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -165,8 +165,8 @@ func (c *StackCollection) DoCreateStackRequest(ctx context.Context, i *Stack, te // CreateStack with given name, stack builder instance and parameters; // any errors will be written to errs channel, when nil is written, -// assume completion, do not expect more then one error value on the -// channel, it's closed immediately after it is written to +// assume completion, do not expect more than one error value on the +// channel, it's closed immediately after it is written to. func (c *StackCollection) CreateStack(ctx context.Context, stackName string, resourceSet builder.ResourceSetReader, tags, parameters map[string]string, errs chan error) error { stack, err := c.createStackRequest(ctx, stackName, resourceSet, tags, parameters) if err != nil { diff --git a/pkg/cfn/manager/mocks/NodeGroupResourceSet.go b/pkg/cfn/manager/mocks/NodeGroupResourceSet.go index b7be64ac5a..0503b83664 100644 --- a/pkg/cfn/manager/mocks/NodeGroupResourceSet.go +++ b/pkg/cfn/manager/mocks/NodeGroupResourceSet.go @@ -19,6 +19,10 @@ type NodeGroupResourceSet struct { func (_m *NodeGroupResourceSet) AddAllResources(ctx context.Context) error { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for AddAllResources") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context) error); ok { r0 = rf(ctx) @@ -33,6 +37,10 @@ func (_m *NodeGroupResourceSet) AddAllResources(ctx context.Context) error { func (_m *NodeGroupResourceSet) GetAllOutputs(_a0 types.Stack) error { ret := _m.Called(_a0) + if len(ret) == 0 { + panic("no return value specified for GetAllOutputs") + } + var r0 error if rf, ok := ret.Get(0).(func(types.Stack) error); ok { r0 = rf(_a0) @@ -47,6 +55,10 @@ func (_m *NodeGroupResourceSet) GetAllOutputs(_a0 types.Stack) error { func (_m *NodeGroupResourceSet) RenderJSON() ([]byte, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for RenderJSON") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func() ([]byte, error)); ok { @@ -73,6 +85,10 @@ func (_m *NodeGroupResourceSet) RenderJSON() ([]byte, error) { func (_m *NodeGroupResourceSet) WithIAM() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for WithIAM") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() @@ -87,6 +103,10 @@ func (_m *NodeGroupResourceSet) WithIAM() bool { func (_m *NodeGroupResourceSet) WithNamedIAM() bool { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for WithNamedIAM") + } + var r0 bool if rf, ok := ret.Get(0).(func() bool); ok { r0 = rf() diff --git a/pkg/ctl/cmdutils/pod_identity_association.go b/pkg/ctl/cmdutils/pod_identity_association.go index 4df52700cb..85732fc3d3 100644 --- a/pkg/ctl/cmdutils/pod_identity_association.go +++ b/pkg/ctl/cmdutils/pod_identity_association.go @@ -108,6 +108,8 @@ func validatePodIdentityAssociation(l *commonClusterConfigLoader, options PodIde return nil } +// TODO: validate addon.podIdentityAssociations. +// TODO: Disallow setting IRSA and PIA fields simultaneously. func validatePodIdentityAssociationsForConfig(clusterConfig *api.ClusterConfig, isCreate bool) error { if clusterConfig.IAM == nil || len(clusterConfig.IAM.PodIdentityAssociations) == 0 { return errors.New("no iam.podIdentityAssociations specified in the config file") @@ -133,14 +135,17 @@ func validatePodIdentityAssociationsForConfig(clusterConfig *api.ClusterConfig, return fmt.Errorf("at least one of the following must be specified: %[1]s.roleARN, %[1]s.permissionPolicy, %[1]s.permissionPolicyARNs, %[1]s.wellKnownPolicies", path) } if pia.RoleARN != "" { + makeIncompatibleFieldErr := func(fieldName string) error { + return fmt.Errorf("%[1]s.%s cannot be specified when %[1]s.roleARN is set", path, fieldName) + } if len(pia.PermissionPolicy) > 0 { - return fmt.Errorf("%[1]s.permissionPolicy cannot be specified when %[1]s.roleARN is set", path) + return makeIncompatibleFieldErr("permissionPolicy") } if len(pia.PermissionPolicyARNs) > 0 { - return fmt.Errorf("%[1]s.permissionPolicyARNs cannot be specified when %[1]s.roleARN is set", path) + return makeIncompatibleFieldErr("permissionPolicyARNs") } if pia.WellKnownPolicies.HasPolicy() { - return fmt.Errorf("%[1]s.wellKnownPolicies cannot be specified when %[1]s.roleARN is set", path) + return makeIncompatibleFieldErr("wellKnownPolicies") } } } diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index 45e4122b1c..b5c9db0d64 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -11,6 +11,7 @@ import ( "github.com/spf13/pflag" "github.com/weaveworks/eksctl/pkg/actions/addon" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" ) @@ -91,11 +92,24 @@ func updateAddon(cmd *cmdutils.Cmd, force, wait bool) error { return err } + piaUpdater := &addon.PodIdentityAssociationUpdater{ + ClusterName: cmd.ClusterConfig.Metadata.Name, + IAMRoleCreator: &podidentityassociation.IAMRoleCreator{ + ClusterName: cmd.ClusterConfig.Metadata.Name, + StackCreator: stackManager, + }, + IAMRoleUpdater: &podidentityassociation.IAMRoleUpdater{ + StackUpdater: stackManager, + }, + EKSPodIdentityDescriber: clusterProvider.AWSProvider.EKS(), + PodIdentityStackLister: stackManager, + } + for _, a := range cmd.ClusterConfig.Addons { if force { //force is specified at cmdline level a.Force = true } - if err := addonManager.Update(ctx, a, cmd.ProviderConfig.WaitTimeout); err != nil { + if err := addonManager.Update(ctx, a, piaUpdater, cmd.ProviderConfig.WaitTimeout); err != nil { return err } } From 4be08c87681e27f4b3c787b34e99e2a9f3e82c3e Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 30 Apr 2024 04:28:09 +0530 Subject: [PATCH 06/35] Show addon.podIdentityAssociations in `get addon` --- pkg/actions/addon/get.go | 60 +++++++++++++++++++++++++---------- pkg/actions/addon/get_test.go | 10 +++--- pkg/actions/addon/update.go | 2 +- pkg/ctl/get/addon.go | 16 +++++++--- 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/pkg/actions/addon/get.go b/pkg/actions/addon/get.go index b94662a97a..d68b6db512 100644 --- a/pkg/actions/addon/get.go +++ b/pkg/actions/addon/get.go @@ -16,14 +16,21 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" ) +type PodIdentityAssociationSummary struct { + Namespace string + ServiceAccount string + RoleARN string +} + type Summary struct { - Name string - Version string - NewerVersion string - IAMRole string - Status string - ConfigurationValues string - Issues []Issue + Name string + Version string + NewerVersion string + IAMRole string + Status string + ConfigurationValues string + Issues []Issue + PodIdentityAssociations []PodIdentityAssociationSummary } type Issue struct { @@ -32,7 +39,7 @@ type Issue struct { ResourceIDs []string } -func (a *Manager) Get(ctx context.Context, addon *api.Addon) (Summary, error) { +func (a *Manager) Get(ctx context.Context, addon *api.Addon, includePodIdentityAssociations bool) (Summary, error) { logger.Debug("addon: %v", addon) output, err := a.eksAPI.DescribeAddon(ctx, &eks.DescribeAddonInput{ ClusterName: &a.clusterConfig.Metadata.Name, @@ -76,19 +83,38 @@ func (a *Manager) Get(ctx context.Context, addon *api.Addon) (Summary, error) { if output.Addon.ConfigurationValues != nil { configurationValues = *output.Addon.ConfigurationValues } + var podIdentityAssociations []PodIdentityAssociationSummary + if includePodIdentityAssociations { + for _, associationID := range output.Addon.PodIdentityAssociations { + output, err := a.eksAPI.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ + ClusterName: aws.String(a.clusterConfig.Metadata.Name), + AssociationId: aws.String(associationID), + }) + if err != nil { + return Summary{}, fmt.Errorf("describe pod identity association %q: %w", associationID, err) + } + association := output.Association + podIdentityAssociations = append(podIdentityAssociations, PodIdentityAssociationSummary{ + Namespace: *association.Namespace, + ServiceAccount: *association.ServiceAccount, + RoleARN: *association.RoleArn, + }) + } + } return Summary{ - Name: *output.Addon.AddonName, - Version: *output.Addon.AddonVersion, - IAMRole: serviceAccountRoleARN, - Status: string(output.Addon.Status), - NewerVersion: newerVersion, - ConfigurationValues: configurationValues, - Issues: issues, + Name: *output.Addon.AddonName, + Version: *output.Addon.AddonVersion, + IAMRole: serviceAccountRoleARN, + Status: string(output.Addon.Status), + NewerVersion: newerVersion, + ConfigurationValues: configurationValues, + PodIdentityAssociations: podIdentityAssociations, + Issues: issues, }, nil } -func (a *Manager) GetAll(ctx context.Context) ([]Summary, error) { +func (a *Manager) GetAll(ctx context.Context, includePodIdentityAssociations bool) ([]Summary, error) { logger.Info("getting all addons") output, err := a.eksAPI.ListAddons(ctx, &eks.ListAddonsInput{ ClusterName: &a.clusterConfig.Metadata.Name, @@ -99,7 +125,7 @@ func (a *Manager) GetAll(ctx context.Context) ([]Summary, error) { var summaries []Summary for _, addon := range output.Addons { - summary, err := a.Get(ctx, &api.Addon{Name: addon}) + summary, err := a.Get(ctx, &api.Addon{Name: addon}, includePodIdentityAssociations) if err != nil { return nil, err } diff --git a/pkg/actions/addon/get_test.go b/pkg/actions/addon/get_test.go index e49d837086..3f90349bc1 100644 --- a/pkg/actions/addon/get_test.go +++ b/pkg/actions/addon/get_test.go @@ -85,7 +85,7 @@ var _ = Describe("Get", func() { summary, err := manager.Get(context.Background(), &api.Addon{ Name: "my-addon", - }) + }, false) Expect(err).NotTo(HaveOccurred()) Expect(summary).To(Equal(addon.Summary{ Name: "my-addon", @@ -116,7 +116,7 @@ var _ = Describe("Get", func() { _, err := manager.Get(context.Background(), &api.Addon{ Name: "my-addon", - }) + }, false) Expect(err).To(MatchError(`failed to get addon "my-addon": foo`)) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) Expect(*describeAddonInput.AddonName).To(Equal("my-addon")) @@ -175,7 +175,7 @@ var _ = Describe("Get", func() { }, }, nil) - summary, err := manager.GetAll(context.Background()) + summary, err := manager.GetAll(context.Background(), false) Expect(err).NotTo(HaveOccurred()) Expect(summary).To(Equal([]addon.Summary{ { @@ -209,7 +209,7 @@ var _ = Describe("Get", func() { describeAddonInput = args[1].(*awseks.DescribeAddonInput) }).Return(nil, fmt.Errorf("foo")) - _, err := manager.GetAll(context.Background()) + _, err := manager.GetAll(context.Background(), false) Expect(err).To(MatchError(`failed to get addon "my-addon": foo`)) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) Expect(*describeAddonInput.AddonName).To(Equal("my-addon")) @@ -228,7 +228,7 @@ var _ = Describe("Get", func() { Addons: []string{"my-addon"}, }, fmt.Errorf("foo")) - _, err := manager.GetAll(context.Background()) + _, err := manager.GetAll(context.Background(), false) Expect(err).To(MatchError(`failed to list addons: foo`)) Expect(*listAddonsInput.ClusterName).To(Equal("my-cluster")) }) diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 547040128e..502cdeaccd 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -41,7 +41,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp logger.Debug("resolve conflicts set to %s", updateAddonInput.ResolveConflicts) - summary, err := a.Get(ctx, addon) + summary, err := a.Get(ctx, addon, false) if err != nil { return err } diff --git a/pkg/ctl/get/addon.go b/pkg/ctl/get/addon.go index 2e8e42b45c..481bf12eb2 100644 --- a/pkg/ctl/get/addon.go +++ b/pkg/ctl/get/addon.go @@ -84,12 +84,12 @@ func getAddon(cmd *cmdutils.Cmd, a *api.Addon, params *getCmdParams) error { var summaries []addon.Summary if a.Name == "" { - summaries, err = addonManager.GetAll(ctx) + summaries, err = addonManager.GetAll(ctx, true) if err != nil { return err } } else { - summary, err := addonManager.Get(ctx, a) + summary, err := addonManager.Get(ctx, a, true) if err != nil { return err } @@ -105,8 +105,13 @@ func getAddon(cmd *cmdutils.Cmd, a *api.Addon, params *getCmdParams) error { return err } - if params.output == printers.TableType { - addAddonSummaryTableColumns(printer.(*printers.TablePrinter)) + if tablePrinter, ok := printer.(*printers.TablePrinter); ok { + for _, summary := range summaries { + if len(summary.PodIdentityAssociations) > 0 { + logger.Info("to view pod identity associations for an addon, rerun the command with --output=json or --output=yaml") + } + } + addAddonSummaryTableColumns(tablePrinter) } if err := printer.PrintObjWithKind("addons", summaries, cmd.CobraCommand.OutOrStdout()); err != nil { @@ -145,4 +150,7 @@ func addAddonSummaryTableColumns(printer *printers.TablePrinter) { printer.AddColumn("CONFIGURATION VALUES", func(s addon.Summary) string { return s.ConfigurationValues }) + printer.AddColumn("POD IDENTITY ASSOCIATIONS", func(s addon.Summary) int { + return len(s.PodIdentityAssociations) + }) } From e07991183f1c36884bd1a000a71244491d5528f8 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 30 Apr 2024 04:53:43 +0530 Subject: [PATCH 07/35] Disallow updating podidentityassociations owned by addons --- pkg/actions/podidentityassociation/updater.go | 7 ++++- .../podidentityassociation/updater_test.go | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/actions/podidentityassociation/updater.go b/pkg/actions/podidentityassociation/updater.go index df9c39de8f..366d27c5cd 100644 --- a/pkg/actions/podidentityassociation/updater.go +++ b/pkg/actions/podidentityassociation/updater.go @@ -136,9 +136,14 @@ func (u *Updater) makeUpdate(ctx context.Context, pia api.PodIdentityAssociation case 0: return nil, errors.New(notFoundErrMsg) case 1: + association := output.Associations[0] + if association.OwnerArn != nil { + return nil, fmt.Errorf("cannot update podidentityassociation %s as it is in use by addon %s; "+ + "please use `eksctl update addon` instead", pia.NameString(), *association.OwnerArn) + } describeOutput, err := u.APIUpdater.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ ClusterName: aws.String(u.ClusterName), - AssociationId: output.Associations[0].AssociationId, + AssociationId: association.AssociationId, }) if err != nil { return nil, fmt.Errorf("error describing pod identity association: %w", err) diff --git a/pkg/actions/podidentityassociation/updater_test.go b/pkg/actions/podidentityassociation/updater_test.go index 7411f74881..635fd9ff4d 100644 --- a/pkg/actions/podidentityassociation/updater_test.go +++ b/pkg/actions/podidentityassociation/updater_test.go @@ -124,6 +124,37 @@ var _ = Describe("Pod Identity Update", func() { expectedErr: `error updating pod identity association "default/default": pod identity association does not exist: NotFoundException: not found`, }), + Entry("attempting to update a pod identity associated with an addon ", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podID := podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + } + mockListStackNames(stackManager, nil) + mockListPodIdentityAssociations(eksAPI, podID, []ekstypes.PodIdentityAssociationSummary{ + { + AssociationId: aws.String("a-1"), + OwnerArn: aws.String("vpc-cni"), + }, + }, nil) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) + eksAPI.AssertExpectations(GinkgoT()) + }, + expectedErr: "error updating pod identity association \"kube-system/vpc-cni\": cannot update podidentityassociation kube-system/vpc-cni as it is in use by addon vpc-cni; " + + "please use `eksctl update addon` instead", + }), + Entry("role ARN specified when the IAM resources were created by eksctl", updateEntry{ podIdentityAssociations: []api.PodIdentityAssociation{ { From 1e69cf0105f7bb7ca55d4c13065431b96d20002e Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 01:25:21 +0530 Subject: [PATCH 08/35] Show pod identities in `get addons`, use a pointer for addon.podIdentityAssociations --- pkg/actions/addon/create.go | 26 ++-- pkg/actions/addon/create_test.go | 56 ++++----- pkg/actions/addon/get.go | 27 +++- pkg/actions/addon/get_test.go | 54 +++++++- pkg/actions/addon/mocks/IAMRoleCreator.go | 18 +-- pkg/actions/addon/mocks/IAMRoleUpdater.go | 24 ++-- pkg/actions/addon/podidentityassociation.go | 50 ++++---- .../addon/podidentityassociation_test.go | 119 +++++++++++------- pkg/actions/addon/tasks.go | 51 ++++---- pkg/actions/addon/update.go | 16 ++- pkg/actions/podidentityassociation/creator.go | 2 +- .../iam_role_creator.go | 26 +++- .../iam_role_updater.go | 73 +++-------- .../podidentityassociation/migrator_test.go | 13 +- pkg/actions/podidentityassociation/tasks.go | 5 - pkg/actions/podidentityassociation/updater.go | 50 +++++--- pkg/apis/eksctl.io/v1alpha5/addon.go | 2 +- pkg/apis/eksctl.io/v1alpha5/types.go | 3 + pkg/cfn/manager/api.go | 5 +- pkg/ctl/cmdutils/configfile.go | 2 +- pkg/ctl/create/addon.go | 10 +- pkg/ctl/create/cluster.go | 6 +- pkg/ctl/get/addon.go | 18 ++- pkg/ctl/update/addon.go | 3 +- pkg/ctl/utils/migrate_to_pod_identity.go | 24 +++- 25 files changed, 402 insertions(+), 281 deletions(-) diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 026d7b2080..95b11f4b53 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -25,7 +25,7 @@ const ( kubeSystemNamespace = "kube-system" ) -func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time.Duration) error { +func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator IAMRoleCreator, waitTimeout time.Duration) error { // check if the addon is already present as an EKS managed addon // in a state different from CREATE_FAILED, and if so, don't re-create var notFoundErr *ekstypes.ResourceNotFoundException @@ -88,10 +88,10 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time if requiresIAMPermissions { switch { - case len(addon.PodIdentityAssociations) > 0: + case addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0: logger.Info("pod identity associations were specified for addon %s, will use those to provide required IAM permissions, other settings such as IRSA will be ignored", addon.Name) - for _, pia := range addon.PodIdentityAssociations { - roleARN, err := a.createRoleForPodIdentity(ctx, addon.Name, pia) + for _, pia := range *addon.PodIdentityAssociations { + roleARN, err := iamRoleCreator.Create(ctx, &pia, addon.Name) if err != nil { return err } @@ -112,10 +112,10 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, waitTimeout time break } for _, p := range recommendedPoliciesBySA { - roleARN, err := a.createRoleForPodIdentity(ctx, addon.Name, api.PodIdentityAssociation{ + roleARN, err := iamRoleCreator.Create(ctx, &api.PodIdentityAssociation{ ServiceAccountName: *p.ServiceAccount, PermissionPolicyARNs: p.RecommendedManagedPolicies, - }) + }, addon.Name) if err != nil { return err } @@ -310,20 +310,8 @@ func hasPoliciesSet(addon *api.Addon) bool { return len(addon.AttachPolicyARNs) != 0 || addon.WellKnownPolicies.HasPolicy() || addon.AttachPolicy != nil } -func (a *Manager) createRoleForPodIdentity(ctx context.Context, addonName string, pia api.PodIdentityAssociation) (string, error) { - resourceSet := builder.NewIAMRoleResourceSetForPodIdentity(&pia) - if err := resourceSet.AddAllResources(); err != nil { - return "", err - } - if err := a.createStack(ctx, resourceSet, addonName, - a.makeAddonPodIdentityName(addonName, pia.ServiceAccountName)); err != nil { - return "", err - } - return pia.RoleARN, nil -} - func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, namespace, serviceAccount string) (string, error) { - logger.Warning("providing required IAM permissions via OIDC has been deprecated for addon %s; please use \"eksctl utils migrate-to-pod-identities\" after addon is created") + logger.Warning("providing required IAM permissions via OIDC has been deprecated for addon %s; please use \"eksctl utils migrate-to-pod-identities\" after addon is created", addon.Name) resourceSet, err := a.createRoleResourceSet(addon, namespace, serviceAccount) if err != nil { return "", err diff --git a/pkg/actions/addon/create_test.go b/pkg/actions/addon/create_test.go index 04876686a3..4c574221e1 100644 --- a/pkg/actions/addon/create_test.go +++ b/pkg/actions/addon/create_test.go @@ -146,7 +146,7 @@ var _ = Describe("Create", func() { }) It("will try to re-create the addon", func() { - err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, 0) + err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, nil, 0) Expect(err).NotTo(HaveOccurred()) mockProvider.MockEKS().AssertNumberOfCalls(GinkgoT(), "CreateAddon", 1) }) @@ -169,9 +169,9 @@ var _ = Describe("Create", func() { output := &bytes.Buffer{} logger.Writer = output - err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, 0) + err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, nil, 0) Expect(err).NotTo(HaveOccurred()) - Expect(output.String()).To(ContainSubstring("Addon my-addon is already present in this cluster, as an EKS managed addon, and won't be re-created")) + Expect(output.String()).To(ContainSubstring("addon my-addon is already present on the cluster, as an EKS managed addon, skipping creation")) }) }) }) @@ -184,7 +184,7 @@ var _ = Describe("Create", func() { }).Return(nil, fmt.Errorf("test error")) }) It("returns an error", func() { - err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, 0) + err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, nil, 0) Expect(err).To(MatchError(`test error`)) }) }) @@ -197,7 +197,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: "my-addon", Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).To(MatchError(`failed to create addon "my-addon": foo`)) }) @@ -212,7 +212,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.1", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -235,7 +235,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "1.7.5", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -252,7 +252,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "1.7.5-eksbuild", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -269,7 +269,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "latest", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -286,7 +286,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "1.7.8", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(ContainSubstring("no version(s) found matching \"1.7.8\" for \"my-addon\""))) }) @@ -327,7 +327,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "latest", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).To(MatchError(ContainSubstring("failed to parse version \"totally not semver\":"))) }) }) @@ -355,7 +355,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "latest", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).To(MatchError(ContainSubstring("no versions available for \"my-addon\""))) }) }) @@ -398,7 +398,7 @@ var _ = Describe("Create", func() { }, }, nil).Once() } - err := manager.Create(context.Background(), &api.Addon{Name: e.addonName}, time.Nanosecond) + err := manager.Create(context.Background(), &api.Addon{Name: e.addonName}, nil, time.Nanosecond) Expect(err).NotTo(HaveOccurred()) mockProvider.MockEKS().AssertNumberOfCalls(GinkgoT(), "DescribeAddon", expectedDescribeCallsCount) }, @@ -425,7 +425,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "latest", ResolveConflicts: rc, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(createAddonInput.ResolveConflicts).To(Equal(rc)) @@ -443,7 +443,7 @@ var _ = Describe("Create", func() { ConfigurationValues: "{\"replicaCount\":3}", } It("sends the value to the AWS EKS API", func() { - err := manager.Create(context.Background(), addon, 0) + err := manager.Create(context.Background(), addon, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(*createAddonInput.ConfigurationValues).To(Equal(addon.ConfigurationValues)) @@ -461,7 +461,7 @@ var _ = Describe("Create", func() { Version: "v1.0.0-eksbuild.1", AttachPolicyARNs: []string{"arn-1"}, Force: true, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -491,7 +491,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: "my-addon", Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -518,7 +518,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: "my-addon", Version: "v1.0.0-eksbuild.1", - }, 5*time.Minute) + }, nil, 5*time.Minute) Expect(err).To(MatchError(`addon status transitioned to "DEGRADED"`)) }) }) @@ -530,7 +530,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: "my-addon", Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) @@ -560,7 +560,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: api.VPCCNIAddon, Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) @@ -593,7 +593,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: api.VPCCNIAddon, Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) @@ -618,7 +618,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: api.AWSEBSCSIDriverAddon, Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) @@ -643,7 +643,7 @@ var _ = Describe("Create", func() { err := manager.Create(context.Background(), &api.Addon{ Name: api.AWSEFSCSIDriverAddon, Version: "v1.0.0-eksbuild.1", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) @@ -671,7 +671,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.1", AttachPolicyARNs: []string{"arn-1"}, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) @@ -695,7 +695,7 @@ var _ = Describe("Create", func() { WellKnownPolicies: api.WellKnownPolicies{ AutoScaler: true, }, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) @@ -719,7 +719,7 @@ var _ = Describe("Create", func() { AttachPolicy: api.InlineDocument{ "foo": "policy-bar", }, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) @@ -741,7 +741,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.1", ServiceAccountRoleARN: "foo", - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) @@ -757,7 +757,7 @@ var _ = Describe("Create", func() { Name: "my-addon", Version: "v1.0.0-eksbuild.1", Tags: map[string]string{"foo": "bar", "fox": "brown"}, - }, 0) + }, nil, 0) Expect(err).NotTo(HaveOccurred()) Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) diff --git a/pkg/actions/addon/get.go b/pkg/actions/addon/get.go index d68b6db512..b7e6b7fea5 100644 --- a/pkg/actions/addon/get.go +++ b/pkg/actions/addon/get.go @@ -6,14 +6,13 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/blang/semver" - "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" - - "github.com/aws/aws-sdk-go-v2/service/eks" ) type PodIdentityAssociationSummary struct { @@ -85,7 +84,11 @@ func (a *Manager) Get(ctx context.Context, addon *api.Addon, includePodIdentityA } var podIdentityAssociations []PodIdentityAssociationSummary if includePodIdentityAssociations { - for _, associationID := range output.Addon.PodIdentityAssociations { + podIdentityAssociationIDs, err := toPodIdentityAssociationIDs(output.Addon.PodIdentityAssociations) + if err != nil { + return Summary{}, err + } + for _, associationID := range podIdentityAssociationIDs { output, err := a.eksAPI.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ ClusterName: aws.String(a.clusterConfig.Metadata.Name), AssociationId: aws.String(associationID), @@ -134,6 +137,22 @@ func (a *Manager) GetAll(ctx context.Context, includePodIdentityAssociations boo return summaries, nil } +func toPodIdentityAssociationIDs(podIdentityAssociationARNs []string) ([]string, error) { + var ret []string + for _, podIdentityAssociationARN := range podIdentityAssociationARNs { + parsed, err := arn.Parse(podIdentityAssociationARN) + if err != nil { + return nil, fmt.Errorf("parsing ARN %q: %w", podIdentityAssociationARN, err) + } + parts := strings.Split(parsed.Resource, "/") + if len(parts) != 3 { + return nil, fmt.Errorf("unexpected pod identity association ARN format: %q", parsed.String()) + } + ret = append(ret, parts[len(parts)-1]) + } + return ret, nil +} + func (a *Manager) findNewerVersions(ctx context.Context, addon *api.Addon) (string, error) { var newerVersions []string currentVersion, err := semver.Parse(strings.TrimPrefix(addon.Version, "v")) diff --git a/pkg/actions/addon/get_test.go b/pkg/actions/addon/get_test.go index 3f90349bc1..97c679f40e 100644 --- a/pkg/actions/addon/get_test.go +++ b/pkg/actions/addon/get_test.go @@ -34,7 +34,7 @@ var _ = Describe("Get", func() { }) Describe("Get", func() { - It("returns an addon", func() { + mockDescribeAddon := func(podIdentityAssociations ...string) { mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { Expect(args).To(HaveLen(2)) Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonVersionsInput{})) @@ -67,10 +67,11 @@ var _ = Describe("Get", func() { describeAddonInput = args[1].(*awseks.DescribeAddonInput) }).Return(&awseks.DescribeAddonOutput{ Addon: &ekstypes.Addon{ - AddonName: aws.String("my-addon"), - AddonVersion: aws.String("v1.1.0-eksbuild.1"), - ServiceAccountRoleArn: aws.String("foo"), - Status: "created", + AddonName: aws.String("my-addon"), + AddonVersion: aws.String("v1.1.0-eksbuild.1"), + ServiceAccountRoleArn: aws.String("foo"), + Status: "created", + PodIdentityAssociations: podIdentityAssociations, Health: &ekstypes.AddonHealth{ Issues: []ekstypes.AddonIssue{ { @@ -82,7 +83,9 @@ var _ = Describe("Get", func() { }, }, }, nil) - + } + It("returns an addon", func() { + mockDescribeAddon() summary, err := manager.Get(context.Background(), &api.Addon{ Name: "my-addon", }, false) @@ -106,6 +109,45 @@ var _ = Describe("Get", func() { Expect(*describeAddonInput.AddonName).To(Equal("my-addon")) }) + It("returns an addon with pod identity associations", func() { + mockDescribeAddon("arn:aws:eks:us-west-2:00:podidentityassociation/cluster/a-zkgxwyqoexvjka9a3") + mockProvider.MockEKS().On("DescribePodIdentityAssociation", mock.Anything, &awseks.DescribePodIdentityAssociationInput{ + AssociationId: aws.String("a-zkgxwyqoexvjka9a3"), + ClusterName: aws.String("my-cluster"), + }).Return(&awseks.DescribePodIdentityAssociationOutput{ + Association: &ekstypes.PodIdentityAssociation{ + RoleArn: aws.String("role-1"), + ServiceAccount: aws.String("default"), + Namespace: aws.String("default"), + }, + }, nil) + summary, err := manager.Get(context.Background(), &api.Addon{ + Name: "my-addon", + }, true) + Expect(err).NotTo(HaveOccurred()) + Expect(summary).To(Equal(addon.Summary{ + Name: "my-addon", + Version: "v1.1.0-eksbuild.1", + NewerVersion: "v1.1.0-eksbuild.4,v1.2.0-eksbuild.1", + IAMRole: "foo", + Status: "created", + Issues: []addon.Issue{ + { + Code: "1", + Message: "foo", + ResourceIDs: []string{"id-1"}, + }, + }, + PodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "default", + ServiceAccount: "default", + RoleARN: "role-1", + }, + }, + })) + }) + When("it fails to get the addon", func() { It("returns an error", func() { mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { diff --git a/pkg/actions/addon/mocks/IAMRoleCreator.go b/pkg/actions/addon/mocks/IAMRoleCreator.go index 7d4ab28546..9ccdc07362 100644 --- a/pkg/actions/addon/mocks/IAMRoleCreator.go +++ b/pkg/actions/addon/mocks/IAMRoleCreator.go @@ -14,9 +14,9 @@ type IAMRoleCreator struct { mock.Mock } -// Create provides a mock function with given fields: ctx, podIdentityAssociation -func (_m *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *v1alpha5.PodIdentityAssociation) (string, error) { - ret := _m.Called(ctx, podIdentityAssociation) +// Create provides a mock function with given fields: ctx, podIdentityAssociation, addonName +func (_m *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *v1alpha5.PodIdentityAssociation, addonName string) (string, error) { + ret := _m.Called(ctx, podIdentityAssociation, addonName) if len(ret) == 0 { panic("no return value specified for Create") @@ -24,17 +24,17 @@ func (_m *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *v1 var r0 string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *v1alpha5.PodIdentityAssociation) (string, error)); ok { - return rf(ctx, podIdentityAssociation) + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha5.PodIdentityAssociation, string) (string, error)); ok { + return rf(ctx, podIdentityAssociation, addonName) } - if rf, ok := ret.Get(0).(func(context.Context, *v1alpha5.PodIdentityAssociation) string); ok { - r0 = rf(ctx, podIdentityAssociation) + if rf, ok := ret.Get(0).(func(context.Context, *v1alpha5.PodIdentityAssociation, string) string); ok { + r0 = rf(ctx, podIdentityAssociation, addonName) } else { r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(context.Context, *v1alpha5.PodIdentityAssociation) error); ok { - r1 = rf(ctx, podIdentityAssociation) + if rf, ok := ret.Get(1).(func(context.Context, *v1alpha5.PodIdentityAssociation, string) error); ok { + r1 = rf(ctx, podIdentityAssociation, addonName) } else { r1 = ret.Error(1) } diff --git a/pkg/actions/addon/mocks/IAMRoleUpdater.go b/pkg/actions/addon/mocks/IAMRoleUpdater.go index 87a464377d..86a5eee3d4 100644 --- a/pkg/actions/addon/mocks/IAMRoleUpdater.go +++ b/pkg/actions/addon/mocks/IAMRoleUpdater.go @@ -6,7 +6,7 @@ import ( context "context" mock "github.com/stretchr/testify/mock" - podidentityassociation "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + v1alpha5 "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" ) // IAMRoleUpdater is an autogenerated mock type for the IAMRoleUpdater type @@ -14,9 +14,9 @@ type IAMRoleUpdater struct { mock.Mock } -// Update provides a mock function with given fields: ctx, updateConfig, podIdentityAssociationID -func (_m *IAMRoleUpdater) Update(ctx context.Context, updateConfig *podidentityassociation.UpdateConfig, podIdentityAssociationID string) (string, bool, error) { - ret := _m.Called(ctx, updateConfig, podIdentityAssociationID) +// Update provides a mock function with given fields: ctx, podIdentityAssociation, stackName, podIdentityAssociationID +func (_m *IAMRoleUpdater) Update(ctx context.Context, podIdentityAssociation v1alpha5.PodIdentityAssociation, stackName string, podIdentityAssociationID string) (string, bool, error) { + ret := _m.Called(ctx, podIdentityAssociation, stackName, podIdentityAssociationID) if len(ret) == 0 { panic("no return value specified for Update") @@ -25,23 +25,23 @@ func (_m *IAMRoleUpdater) Update(ctx context.Context, updateConfig *podidentitya var r0 string var r1 bool var r2 error - if rf, ok := ret.Get(0).(func(context.Context, *podidentityassociation.UpdateConfig, string) (string, bool, error)); ok { - return rf(ctx, updateConfig, podIdentityAssociationID) + if rf, ok := ret.Get(0).(func(context.Context, v1alpha5.PodIdentityAssociation, string, string) (string, bool, error)); ok { + return rf(ctx, podIdentityAssociation, stackName, podIdentityAssociationID) } - if rf, ok := ret.Get(0).(func(context.Context, *podidentityassociation.UpdateConfig, string) string); ok { - r0 = rf(ctx, updateConfig, podIdentityAssociationID) + if rf, ok := ret.Get(0).(func(context.Context, v1alpha5.PodIdentityAssociation, string, string) string); ok { + r0 = rf(ctx, podIdentityAssociation, stackName, podIdentityAssociationID) } else { r0 = ret.Get(0).(string) } - if rf, ok := ret.Get(1).(func(context.Context, *podidentityassociation.UpdateConfig, string) bool); ok { - r1 = rf(ctx, updateConfig, podIdentityAssociationID) + if rf, ok := ret.Get(1).(func(context.Context, v1alpha5.PodIdentityAssociation, string, string) bool); ok { + r1 = rf(ctx, podIdentityAssociation, stackName, podIdentityAssociationID) } else { r1 = ret.Get(1).(bool) } - if rf, ok := ret.Get(2).(func(context.Context, *podidentityassociation.UpdateConfig, string) error); ok { - r2 = rf(ctx, updateConfig, podIdentityAssociationID) + if rf, ok := ret.Get(2).(func(context.Context, v1alpha5.PodIdentityAssociation, string, string) error); ok { + r2 = rf(ctx, podIdentityAssociation, stackName, podIdentityAssociationID) } else { r2 = ret.Error(2) } diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go index b16762989a..d23eb6c723 100644 --- a/pkg/actions/addon/podidentityassociation.go +++ b/pkg/actions/addon/podidentityassociation.go @@ -3,30 +3,26 @@ package addon import ( "context" "fmt" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" ) -type PodIdentityStackLister interface { - ListPodIdentityStackNames(ctx context.Context) ([]string, error) -} - type EKSPodIdentityDescriber interface { ListPodIdentityAssociations(ctx context.Context, params *eks.ListPodIdentityAssociationsInput, optFns ...func(*eks.Options)) (*eks.ListPodIdentityAssociationsOutput, error) DescribePodIdentityAssociation(ctx context.Context, params *eks.DescribePodIdentityAssociationInput, optFns ...func(*eks.Options)) (*eks.DescribePodIdentityAssociationOutput, error) } type IAMRoleCreator interface { - Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation) (roleARN string, err error) + Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation, addonName string) (roleARN string, err error) } type IAMRoleUpdater interface { - Update(ctx context.Context, updateConfig *podidentityassociation.UpdateConfig, podIdentityAssociationID string) (roleARN string, hasChanged bool, err error) + Update(ctx context.Context, podIdentityAssociation api.PodIdentityAssociation, stackName string, podIdentityAssociationID string) (string, bool, error) } // PodIdentityAssociationUpdater creates or updates IAM resources for pod identities associated with an addon. @@ -34,13 +30,11 @@ type PodIdentityAssociationUpdater struct { ClusterName string IAMRoleCreator IAMRoleCreator IAMRoleUpdater IAMRoleUpdater - PodIdentityStackLister PodIdentityStackLister EKSPodIdentityDescriber EKSPodIdentityDescriber + StackDescriber podidentityassociation.StackDescriber } -// TODO - -func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) ([]ekstypes.AddonPodIdentityAssociations, error) { +func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string) ([]ekstypes.AddonPodIdentityAssociations, error) { var addonPodIdentityAssociations []ekstypes.AddonPodIdentityAssociations for _, pia := range podIdentityAssociations { output, err := p.EKSPodIdentityDescriber.ListPodIdentityAssociations(ctx, &eks.ListPodIdentityAssociationsInput{ @@ -60,7 +54,7 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent // Create IAM resources. if roleARN == "" { var err error - if roleARN, err = p.IAMRoleCreator.Create(ctx, &pia); err != nil { + if roleARN, err = p.IAMRoleCreator.Create(ctx, &pia, addonName); err != nil { return nil, err } } @@ -73,20 +67,34 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent if err != nil { return nil, err } - // TODO: avoid repeating this call. - roleStackNames, err := p.PodIdentityStackLister.ListPodIdentityStackNames(ctx) - if err != nil { - return nil, fmt.Errorf("error listing stack names for pod identity associations: %w", err) + stackName := podidentityassociation.MakeAddonPodIdentityStackName(p.ClusterName, addonName, pia.ServiceAccountName) + hasStack := true + if _, err := p.StackDescriber.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(stackName), + }); err != nil { + if !manager.IsStackDoesNotExistError(err) { + return nil, fmt.Errorf("describing IAM resources stack for pod identity association %s: %w", pia.NameString(), err) + } + hasStack = false } - updateConfig, err := podidentityassociation.MakeRoleUpdateConfig(pia, *output.Association, roleStackNames) - if err != nil { + + roleValidator := &podidentityassociation.RoleUpdateValidator{ + StackDescriber: p.StackDescriber, + } + if err := roleValidator.ValidateRoleUpdate(pia, *output.Association, hasStack); err != nil { return nil, err } - if updateConfig.HasIAMResourcesStack { - // TODO: if no pod identity has changed, skip update? - if roleARN, _, err = p.IAMRoleUpdater.Update(ctx, updateConfig, *output.Association.AssociationId); err != nil { + if hasStack { + // TODO: if no pod identity has changed, skip update. + newRoleARN, hasChanged, err := p.IAMRoleUpdater.Update(ctx, pia, stackName, *output.Association.AssociationId) + if err != nil { return nil, err } + if hasChanged { + roleARN = newRoleARN + } else { + roleARN = *output.Association.RoleArn + } } } addonPodIdentityAssociations = append(addonPodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ diff --git a/pkg/actions/addon/podidentityassociation_test.go b/pkg/actions/addon/podidentityassociation_test.go index 3a62be8ba2..3bfad3a29e 100644 --- a/pkg/actions/addon/podidentityassociation_test.go +++ b/pkg/actions/addon/podidentityassociation_test.go @@ -2,20 +2,26 @@ package addon_test import ( "context" + "errors" "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/aws/smithy-go" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/stretchr/testify/mock" + "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/addon/mocks" - "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/fakes" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + piamocks "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/mocks" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" managerfakes "github.com/weaveworks/eksctl/pkg/cfn/manager/fakes" "github.com/weaveworks/eksctl/pkg/eks/mocksv2" "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" @@ -23,10 +29,11 @@ import ( var _ = Describe("Update Pod Identity Association", func() { type piaMocks struct { - stackManager *fakes.FakeStackUpdater - roleCreator *mocks.IAMRoleCreator - roleUpdater *mocks.IAMRoleUpdater - eks *mocksv2.EKS + stackManager *fakes.FakeStackUpdater + stackDescriber *piamocks.StackDescriber + roleCreator *mocks.IAMRoleCreator + roleUpdater *mocks.IAMRoleUpdater + eks *mocksv2.EKS } type updateEntry struct { podIdentityAssociations []api.PodIdentityAssociation @@ -101,27 +108,29 @@ var _ = Describe("Update Pod Identity Association", func() { DescribeTable("update pod identity association", func(e updateEntry) { provider := mockprovider.NewMockProvider() var ( - roleCreator mocks.IAMRoleCreator - roleUpdater mocks.IAMRoleUpdater - stackUpdater fakes.FakeStackUpdater + roleCreator mocks.IAMRoleCreator + roleUpdater mocks.IAMRoleUpdater + stackUpdater fakes.FakeStackUpdater + stackDescriber piamocks.StackDescriber ) piaUpdater := &addon.PodIdentityAssociationUpdater{ ClusterName: clusterName, IAMRoleCreator: &roleCreator, IAMRoleUpdater: &roleUpdater, - PodIdentityStackLister: &stackUpdater, EKSPodIdentityDescriber: provider.MockEKS(), + StackDescriber: &stackDescriber, } if e.mockCalls != nil { e.mockCalls(piaMocks{ - stackManager: &stackUpdater, - roleCreator: &roleCreator, - roleUpdater: &roleUpdater, - eks: provider.MockEKS(), + stackManager: &stackUpdater, + stackDescriber: &stackDescriber, + roleCreator: &roleCreator, + roleUpdater: &roleUpdater, + eks: provider.MockEKS(), }) } - addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations) + addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations, "") if e.expectedErr != "" { Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) return @@ -150,7 +159,7 @@ var _ = Describe("Update Pod Identity Association", func() { m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "vpc-cni", - }).Return("role-1", nil) + }, "").Return("role-1", nil) }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ @@ -195,25 +204,22 @@ var _ = Describe("Update Pod Identity Association", func() { }, }) - m.roleUpdater.On("Update", mock.Anything, &podidentityassociation.UpdateConfig{ - PodIdentityAssociation: api.PodIdentityAssociation{ - Namespace: "kube-system", - ServiceAccountName: "vpc-cni", - }, - AssociationID: "a-1", - HasIAMResourcesStack: true, - StackName: "kube-system-vpc-cni", - }, "a-1").Return("cni-role-2", false, nil).Once() - m.stackManager.ListPodIdentityStackNamesReturns([]string{"kube-system-vpc-cni", "extra-stack"}, nil) + m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, "eksctl-test-addon--podidentityrole-vpc-cni", "a-1").Return("cni-role-2", true, nil).Once() + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), + }).Return(&manager.Stack{}, nil) m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "aws-ebs-csi-driver", - }).Return("csi-role", nil).Once() + }, "").Return("csi-role", nil).Once() m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "karpenter", ServiceAccountName: "karpenter", - }).Return("karpenter-role", nil).Once() + }, "").Return("karpenter-role", nil).Once() }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { @@ -247,6 +253,9 @@ var _ = Describe("Update Pod Identity Association", func() { }, }, mockCalls: func(m piaMocks) { + + // TODO: + //m.stackManager.DescribeStackStub mockListPodIdentityAssociations(m.eks, true, defaultListPodIdentityInputs) mockDescribePodIdentityAssociation(m.eks, "cni-role", "csi-role", "karpenter-role") @@ -280,18 +289,19 @@ var _ = Describe("Update Pod Identity Association", func() { }, } { id := makeID(i) - m.roleUpdater.On("Update", mock.Anything, &podidentityassociation.UpdateConfig{ - PodIdentityAssociation: api.PodIdentityAssociation{ - Namespace: updateInput.namespace, - ServiceAccountName: updateInput.serviceAccount, - }, - AssociationID: id, - HasIAMResourcesStack: true, - StackName: updateInput.stackName, - }, id).Return(updateInput.returnRole, false, nil).Once() - } - m.stackManager.ListPodIdentityStackNamesReturns([]string{"kube-system-vpc-cni", "kube-system-aws-ebs-csi-driver", "karpenter-karpenter", "extra-stack"}, nil) + stackName := fmt.Sprintf("eksctl-test-addon--podidentityrole-%s", updateInput.serviceAccount) + m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ + Namespace: updateInput.namespace, + ServiceAccountName: updateInput.serviceAccount, + }, stackName, id).Return(updateInput.returnRole, true, nil).Once() + + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String(stackName), + }).Return(&manager.Stack{ + StackName: aws.String(stackName), + }, nil) + } }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { @@ -384,7 +394,7 @@ var _ = Describe("Update Pod Identity Association", func() { }, }), - Entry("addon contains pod identities created by eksctl but are being updated with a new roleARN", updateEntry{ + Entry("addon contains pod identity IAM resources created by eksctl but are being updated with a new roleARN", updateEntry{ podIdentityAssociations: []api.PodIdentityAssociation{ { Namespace: "kube-system", @@ -418,7 +428,16 @@ var _ = Describe("Update Pod Identity Association", func() { }, }) mockDescribePodIdentityAssociation(m.eks, "role-1", "role-2", "role-3") - m.stackManager.ListPodIdentityStackNamesReturns([]string{"karpenter-karpenter"}, nil) + for _, serviceAccount := range []string{"vpc-cni", "aws-ebs-csi-driver"} { + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String(fmt.Sprintf("eksctl-test-addon--podidentityrole-%s", serviceAccount)), + }).Return(nil, &smithy.OperationError{ + Err: fmt.Errorf("ValidationError"), + }).Once() + } + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon--podidentityrole-karpenter"), + }).Return(&manager.Stack{}, nil).Once() }, expectedErr: "cannot change podIdentityAssociation.roleARN since the role was created by eksctl", }), @@ -439,7 +458,9 @@ var _ = Describe("Update Pod Identity Association", func() { }, }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") - + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), + }).Return(&manager.Stack{}, nil).Once() }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { @@ -447,6 +468,7 @@ var _ = Describe("Update Pod Identity Association", func() { ServiceAccount: aws.String("vpc-cni"), }, }, + expectedErr: "cannot change podIdentityAssociation.roleARN since the role was created by eksctl", }), Entry("addon contains pod identity created with a pre-existing roleARN but it is no longer set", updateEntry{ @@ -464,6 +486,11 @@ var _ = Describe("Update Pod Identity Association", func() { }, }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), + }).Return(nil, &smithy.OperationError{ + Err: errors.New("ValidationError"), + }) }, expectedErr: "podIdentityAssociation.roleARN is required since the role was not created by eksctl", }), diff --git a/pkg/actions/addon/tasks.go b/pkg/actions/addon/tasks.go index aec972e358..333cef4a68 100644 --- a/pkg/actions/addon/tasks.go +++ b/pkg/actions/addon/tasks.go @@ -15,7 +15,7 @@ import ( "github.com/weaveworks/eksctl/pkg/utils/tasks" ) -func CreateAddonTasks(ctx context.Context, cfg *api.ClusterConfig, clusterProvider *eks.ClusterProvider, forceAll bool, timeout time.Duration) (*tasks.TaskTree, *tasks.TaskTree) { +func CreateAddonTasks(ctx context.Context, cfg *api.ClusterConfig, clusterProvider *eks.ClusterProvider, iamRoleCreator IAMRoleCreator, forceAll bool, timeout time.Duration) (*tasks.TaskTree, *tasks.TaskTree) { preTasks := &tasks.TaskTree{Parallel: false} postTasks := &tasks.TaskTree{Parallel: false} var preAddons []*api.Addon @@ -29,31 +29,25 @@ func CreateAddonTasks(ctx context.Context, cfg *api.ClusterConfig, clusterProvid } } - preTasks.Append( - &createAddonTask{ - info: "create addons", - addons: preAddons, - ctx: ctx, - cfg: cfg, - clusterProvider: clusterProvider, - forceAll: forceAll, - timeout: timeout, - wait: false, - }, - ) - - postTasks.Append( - &createAddonTask{ - info: "create addons", - addons: postAddons, - ctx: ctx, - cfg: cfg, - clusterProvider: clusterProvider, - forceAll: forceAll, - timeout: timeout, - wait: cfg.HasNodes(), - }, - ) + preAddonsTask := createAddonTask{ + info: "create addons", + addons: preAddons, + ctx: ctx, + cfg: cfg, + clusterProvider: clusterProvider, + forceAll: forceAll, + timeout: timeout, + wait: false, + iamRoleCreator: iamRoleCreator, + } + + preTasks.Append(&preAddonsTask) + + postAddonsTask := preAddonsTask + postAddonsTask.addons = postAddons + postAddonsTask.wait = cfg.HasNodes() + postTasks.Append(&postAddonsTask) + return preTasks, postTasks } @@ -67,6 +61,7 @@ type createAddonTask struct { addons []*api.Addon forceAll, wait bool timeout time.Duration + iamRoleCreator IAMRoleCreator } func (t *createAddonTask) Describe() string { return t.info } @@ -100,7 +95,7 @@ func (t *createAddonTask) Do(errorCh chan error) error { if t.forceAll { a.Force = true } - err := addonManager.Create(t.ctx, a, t.timeout) + err := addonManager.Create(t.ctx, a, t.iamRoleCreator, t.timeout) if err != nil { go func() { errorCh <- err @@ -116,7 +111,7 @@ func (t *createAddonTask) Do(errorCh chan error) error { if t.forceAll { a.Force = true } - err := addonManager.Create(t.ctx, a, t.timeout) + err := addonManager.Create(t.ctx, a, t.iamRoleCreator, t.timeout) if err != nil { go func() { errorCh <- err diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 502cdeaccd..5dd5c8c8b4 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -18,7 +18,7 @@ import ( // PodIdentityIAMUpdater creates or updates IAM resources for pod identity associations. type PodIdentityIAMUpdater interface { // UpdateRole creates or updates IAM resources for podIdentityAssociations. - UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) ([]ekstypes.AddonPodIdentityAssociations, error) + UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string) ([]ekstypes.AddonPodIdentityAssociations, error) } func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUpdater PodIdentityIAMUpdater, waitTimeout time.Duration) error { @@ -41,7 +41,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp logger.Debug("resolve conflicts set to %s", updateAddonInput.ResolveConflicts) - summary, err := a.Get(ctx, addon, false) + summary, err := a.Get(ctx, addon, true) if err != nil { return err } @@ -65,9 +65,15 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp updateAddonInput.AddonVersion = &version } - if len(addon.PodIdentityAssociations) > 0 { - // TODO - addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, addon.PodIdentityAssociations) + if len(summary.PodIdentityAssociations) > 0 { + if addon.PodIdentityAssociations == nil { + return fmt.Errorf("addon %s has pod identity associations; to remove pod identity associations from an addon, "+ + "addon.podIdentityAssociations must be explicitly set to []", addon.Name) + } + } + + if addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0 { + addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, *addon.PodIdentityAssociations, addon.Name) if err != nil { return fmt.Errorf("updating pod identity associations: %w", err) } diff --git a/pkg/actions/podidentityassociation/creator.go b/pkg/actions/podidentityassociation/creator.go index 3605d6a7b1..3445a74dca 100644 --- a/pkg/actions/podidentityassociation/creator.go +++ b/pkg/actions/podidentityassociation/creator.go @@ -59,7 +59,7 @@ func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api ClusterName: c.clusterName, StackCreator: c.stackCreator, } - roleARN, err := roleCreator.Create(ctx, &pia) + roleARN, err := roleCreator.Create(ctx, &pia, "") if err != nil { return err } diff --git a/pkg/actions/podidentityassociation/iam_role_creator.go b/pkg/actions/podidentityassociation/iam_role_creator.go index be5e34f712..0cce033984 100644 --- a/pkg/actions/podidentityassociation/iam_role_creator.go +++ b/pkg/actions/podidentityassociation/iam_role_creator.go @@ -13,7 +13,9 @@ type IAMRoleCreator struct { StackCreator StackCreator } -func (r *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation) (string, error) { +// Create creates IAM resources for podIdentityAssociation. If podIdentityAssociation belongs to an addon, addonName +// must be non-empty. +func (r *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation, addonName string) (string, error) { rs := builder.NewIAMRoleResourceSetForPodIdentity(podIdentityAssociation) if err := rs.AddAllResources(); err != nil { return "", err @@ -21,12 +23,21 @@ func (r *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *api if podIdentityAssociation.Tags == nil { podIdentityAssociation.Tags = make(map[string]string) } - podIdentityAssociation.Tags[api.PodIdentityAssociationNameTag] = Identifier{ + podID := Identifier{ Namespace: podIdentityAssociation.Namespace, ServiceAccountName: podIdentityAssociation.ServiceAccountName, }.IDString() - stackName := MakeStackName(r.ClusterName, podIdentityAssociation.Namespace, podIdentityAssociation.ServiceAccountName) + var stackName string + if addonName != "" { + podIdentityAssociation.Tags[api.AddonNameTag] = addonName + podIdentityAssociation.Tags[api.AddonPodIdentityAssociationNameTag] = podID + stackName = MakeAddonPodIdentityStackName(r.ClusterName, addonName, podIdentityAssociation.ServiceAccountName) + } else { + podIdentityAssociation.Tags[api.PodIdentityAssociationNameTag] = podID + stackName = MakeStackName(r.ClusterName, podIdentityAssociation.Namespace, podIdentityAssociation.ServiceAccountName) + } + stackCh := make(chan error) if err := r.StackCreator.CreateStack(ctx, stackName, rs, podIdentityAssociation.Tags, nil, stackCh); err != nil { return "", fmt.Errorf("creating IAM role for pod identity association for service account %s in namespace %s: %w", @@ -43,3 +54,12 @@ func (r *IAMRoleCreator) Create(ctx context.Context, podIdentityAssociation *api podIdentityAssociation.NameString(), ctx.Err()) } } + +// MakeStackName creates a stack name for the specified access entry. +func MakeStackName(clusterName, namespace, serviceAccountName string) string { + return fmt.Sprintf("eksctl-%s-podidentityrole-%s-%s", clusterName, namespace, serviceAccountName) +} + +func MakeAddonPodIdentityStackName(clusterName, addonName, serviceAccountName string) string { + return fmt.Sprintf("eksctl-%s-addon-%s-podidentityrole-%s", clusterName, addonName, serviceAccountName) +} diff --git a/pkg/actions/podidentityassociation/iam_role_updater.go b/pkg/actions/podidentityassociation/iam_role_updater.go index 9f02cec208..63167a233d 100644 --- a/pkg/actions/podidentityassociation/iam_role_updater.go +++ b/pkg/actions/podidentityassociation/iam_role_updater.go @@ -3,10 +3,13 @@ package podidentityassociation import ( "context" "fmt" + "github.com/aws/aws-sdk-go-v2/aws" cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/kris-nova/logger" "github.com/pkg/errors" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/builder" "github.com/weaveworks/eksctl/pkg/cfn/manager" "golang.org/x/exp/slices" @@ -21,17 +24,17 @@ type IAMRoleUpdater struct { // Update updates IAM resources for updateConfig and returns an IAM role ARN upon success. The boolean return value reports // whether the IAM resources have changed or not. -func (u *IAMRoleUpdater) Update(ctx context.Context, updateConfig *UpdateConfig, podIdentityAssociationID string) (string, bool, error) { +func (u *IAMRoleUpdater) Update(ctx context.Context, podIdentityAssociation api.PodIdentityAssociation, stackName, podIdentityAssociationID string) (string, bool, error) { stack, err := u.StackUpdater.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(updateConfig.StackName), + StackName: aws.String(stackName), }) if err != nil { - return "", false, fmt.Errorf("describing IAM resources stack %q: %w", updateConfig.StackName, err) + return "", false, fmt.Errorf("describing IAM resources stack %q: %w", stackName, err) } - if updateConfig.PodIdentityAssociation.RoleName != "" && !slices.Contains(stack.Capabilities, cfntypes.CapabilityCapabilityNamedIam) { + if podIdentityAssociation.RoleName != "" && !slices.Contains(stack.Capabilities, cfntypes.CapabilityCapabilityNamedIam) { return "", false, errors.New("cannot update role name if the pod identity association was not created with a role name") } - rs := builder.NewIAMRoleResourceSetForPodIdentity(&updateConfig.PodIdentityAssociation) + rs := builder.NewIAMRoleResourceSetForPodIdentity(&podIdentityAssociation) if err := rs.AddAllResources(); err != nil { return "", false, fmt.Errorf("adding resources to CloudFormation template: %w", err) } @@ -40,73 +43,35 @@ func (u *IAMRoleUpdater) Update(ctx context.Context, updateConfig *UpdateConfig, return "", false, fmt.Errorf("generating CloudFormation template: %w", err) } if err := u.StackUpdater.MustUpdateStack(ctx, manager.UpdateStackOptions{ - StackName: updateConfig.StackName, - ChangeSetName: fmt.Sprintf("eksctl-%s-%s-update-%d", updateConfig.PodIdentityAssociation.Namespace, updateConfig.PodIdentityAssociation.ServiceAccountName, time.Now().Unix()), - Description: fmt.Sprintf("updating IAM resources stack %q for pod identity association %q", updateConfig.StackName, podIdentityAssociationID), + StackName: stackName, + ChangeSetName: fmt.Sprintf("eksctl-%s-%s-update-%d", podIdentityAssociation.Namespace, podIdentityAssociation.ServiceAccountName, time.Now().Unix()), + Description: fmt.Sprintf("updating IAM resources stack %q for pod identity association %q", stackName, podIdentityAssociationID), TemplateData: manager.TemplateBody(template), Wait: true, }); err != nil { var noChangeErr *manager.NoChangeError if errors.As(err, &noChangeErr) { logger.Info("IAM resources for %q are already up-to-date", podIdentityAssociationID) - return updateConfig.PodIdentityAssociation.RoleARN, false, nil + return podIdentityAssociation.RoleARN, false, nil } return "", false, fmt.Errorf("updating IAM resources for pod identity association: %w", err) } - logger.Info("updated IAM resources stack %q for %q", updateConfig.StackName, podIdentityAssociationID) + logger.Info("updated IAM resources stack %q for %q", stackName, podIdentityAssociationID) stack, err = u.StackUpdater.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(updateConfig.StackName), + StackName: aws.String(stackName), }) if err != nil { return "", false, fmt.Errorf("describing IAM resources stack: %w", err) } - if err := rs.GetAllOutputs(*stack); err != nil { - return "", false, fmt.Errorf("error getting IAM role output from IAM resources stack: %w", err) - } - return updateConfig.PodIdentityAssociation.RoleARN, true, nil -} -func (u *IAMRoleUpdater) updateStack(ctx context.Context, updateConfig *UpdateConfig, podIdentityAssociationID string) error { - stack, err := u.StackUpdater.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(updateConfig.StackName), - }) - if err != nil { - return fmt.Errorf("describing IAM resources stack %q: %w", updateConfig.StackName, err) - } - if updateConfig.PodIdentityAssociation.RoleName != "" && !slices.Contains(stack.Capabilities, cfntypes.CapabilityCapabilityNamedIam) { - return errors.New("cannot update role name if the pod identity association was not created with a role name") - } - rs := builder.NewIAMRoleResourceSetForPodIdentity(&updateConfig.PodIdentityAssociation) - if err := rs.AddAllResources(); err != nil { - return fmt.Errorf("adding resources to CloudFormation template: %w", err) - } - template, err := rs.RenderJSON() - if err != nil { - return fmt.Errorf("generating CloudFormation template: %w", err) + if err := populateRoleARN(rs, stack); err != nil { + return "", false, err } - if err := u.StackUpdater.MustUpdateStack(ctx, manager.UpdateStackOptions{ - StackName: updateConfig.StackName, - ChangeSetName: fmt.Sprintf("eksctl-%s-%s-update-%d", updateConfig.PodIdentityAssociation.Namespace, updateConfig.PodIdentityAssociation.ServiceAccountName, time.Now().Unix()), - Description: fmt.Sprintf("updating IAM resources stack %q for pod identity association %q", updateConfig.StackName, podIdentityAssociationID), - TemplateData: manager.TemplateBody(template), - Wait: true, - }); err != nil { - var noChangeErr *manager.NoChangeError - if errors.As(err, &noChangeErr) { - logger.Info("IAM resources for %q are already up-to-date", podIdentityAssociationID) - return nil - } - return fmt.Errorf("updating IAM resources for pod identity association: %w", err) - } - logger.Info("updated IAM resources stack %q for %q", updateConfig.StackName, podIdentityAssociationID) + return podIdentityAssociation.RoleARN, true, nil +} - stack, err = u.StackUpdater.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(updateConfig.StackName), - }) - if err != nil { - return fmt.Errorf("describing IAM resources stack: %w", err) - } +func populateRoleARN(rs builder.ResourceSet, stack *manager.Stack) error { if err := rs.GetAllOutputs(*stack); err != nil { return fmt.Errorf("error getting IAM role output from IAM resources stack: %w", err) } diff --git a/pkg/actions/podidentityassociation/migrator_test.go b/pkg/actions/podidentityassociation/migrator_test.go index aca95f588b..6e39d9425c 100644 --- a/pkg/actions/podidentityassociation/migrator_test.go +++ b/pkg/actions/podidentityassociation/migrator_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "os" + "time" "github.com/kris-nova/logger" "github.com/stretchr/testify/mock" @@ -40,6 +41,14 @@ type migrateToPodIdentityAssociationEntry struct { expectedErr string } +type addonCreator struct { + addonManager *addon.Manager +} + +func (a *addonCreator) Create(ctx context.Context, addon *api.Addon, waitTimeout time.Duration) error { + return a.addonManager.Create(ctx, addon, nil, waitTimeout) +} + var _ = Describe("Create", func() { var ( migrator *podidentityassociation.Migrator @@ -121,7 +130,7 @@ var _ = Describe("Create", func() { logger.Writer = output } - addonCreator, err := addon.New(api.NewClusterConfig(), mockProvider.MockEKS(), nil, false, nil, nil) + addonManager, err := addon.New(api.NewClusterConfig(), mockProvider.MockEKS(), nil, false, nil, nil) Expect(err).NotTo(HaveOccurred()) migrator = podidentityassociation.NewMigrator( @@ -130,7 +139,7 @@ var _ = Describe("Create", func() { mockProvider.MockIAM(), fakeStackUpdater, fakeClientset, - addonCreator, + &addonCreator{addonManager: addonManager}, ) err = migrator.MigrateToPodIdentity(context.Background(), e.options) diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index 4d01a359e7..d003358af1 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -217,11 +217,6 @@ func updateTrustStatements( return trustStatements, nil } -// MakeStackName creates a stack name for the specified access entry. -func MakeStackName(clusterName, namespace, serviceAccountName string) string { - return fmt.Sprintf("eksctl-%s-podidentityrole-%s-%s", clusterName, namespace, serviceAccountName) -} - func runAllTasks(taskTree *tasks.TaskTree) error { logger.Info(taskTree.Describe()) if errs := taskTree.DoAllSync(); len(errs) > 0 { diff --git a/pkg/actions/podidentityassociation/updater.go b/pkg/actions/podidentityassociation/updater.go index 366d27c5cd..68bfd78804 100644 --- a/pkg/actions/podidentityassociation/updater.go +++ b/pkg/actions/podidentityassociation/updater.go @@ -93,7 +93,7 @@ func (u *Updater) update(ctx context.Context, updateConfig *UpdateConfig, podIde roleUpdater := &IAMRoleUpdater{ StackUpdater: u.StackUpdater, } - newRoleARN, hasChanged, err := roleUpdater.Update(ctx, updateConfig, podIdentityAssociationID) + newRoleARN, hasChanged, err := roleUpdater.Update(ctx, updateConfig.PodIdentityAssociation, updateConfig.StackName, podIdentityAssociationID) if err != nil { return err } @@ -111,7 +111,7 @@ func (u *Updater) updatePodIdentityAssociation(ctx context.Context, roleARN stri ClusterName: aws.String(u.ClusterName), RoleArn: aws.String(roleARN), }); err != nil { - return fmt.Errorf("updating pod identity association (associationID: %s, roleARN: %s): %w", updateConfig.AssociationID, roleARN, err) + return fmt.Errorf("(associationID: %s, roleARN: %s): %w", updateConfig.AssociationID, roleARN, err) } logger.Info("updated role ARN %q for pod identity association %q", roleARN, podIdentityAssociationID) return nil @@ -148,23 +148,42 @@ func (u *Updater) makeUpdate(ctx context.Context, pia api.PodIdentityAssociation if err != nil { return nil, fmt.Errorf("error describing pod identity association: %w", err) } - return MakeRoleUpdateConfig(pia, *describeOutput.Association, roleStackNames) + stackName, hasStack := getIAMResourcesStack(roleStackNames, Identifier{ + Namespace: pia.Namespace, + ServiceAccountName: pia.ServiceAccountName, + }) + updateValidator := &RoleUpdateValidator{ + StackDescriber: u.StackUpdater, + } + if err := updateValidator.ValidateRoleUpdate(pia, *describeOutput.Association, hasStack); err != nil { + return nil, err + } + return &UpdateConfig{ + PodIdentityAssociation: pia, + AssociationID: *describeOutput.Association.AssociationId, + HasIAMResourcesStack: hasStack, + StackName: stackName, + }, nil } } -// MakeRoleUpdateConfig builds an UpdateConfig for pia. -func MakeRoleUpdateConfig(pia api.PodIdentityAssociation, association ekstypes.PodIdentityAssociation, roleStackNames []string) (*UpdateConfig, error) { - stackName, hasStack := getIAMResourcesStack(roleStackNames, Identifier{ - Namespace: pia.Namespace, - ServiceAccountName: pia.ServiceAccountName, - }) +type StackDescriber interface { + DescribeStack(context.Context, *manager.Stack) (*manager.Stack, error) +} + +type RoleUpdateValidator struct { + StackDescriber StackDescriber +} + +// ValidateRoleUpdate validates TODO and returns a boolean indicating whether a stack exists. +func (r *RoleUpdateValidator) ValidateRoleUpdate(pia api.PodIdentityAssociation, association ekstypes.PodIdentityAssociation, hasStack bool) error { if hasStack { if association.RoleArn != nil && pia.RoleARN != "" && pia.RoleARN != *association.RoleArn { - return nil, errors.New("cannot change podIdentityAssociation.roleARN since the role was created by eksctl") + return errors.New("cannot change podIdentityAssociation.roleARN since the role was created by eksctl") } } else { if pia.RoleARN == "" { - return nil, errors.New("podIdentityAssociation.roleARN is required since the role was not created by eksctl") + return errors.New("podIdentityAssociation.roleARN is required since the role was not created by eksctl") } podIDWithRoleARN := api.PodIdentityAssociation{ Namespace: pia.Namespace, @@ -172,13 +191,8 @@ func MakeRoleUpdateConfig(pia api.PodIdentityAssociation, association ekstypes.P RoleARN: pia.RoleARN, } if !reflect.DeepEqual(pia, podIDWithRoleARN) { - return nil, errors.New("only namespace, serviceAccountName and roleARN can be specified if the role was not created by eksctl") + return errors.New("only namespace, serviceAccountName and roleARN can be specified if the role was not created by eksctl") } } - return &UpdateConfig{ - PodIdentityAssociation: pia, - AssociationID: *association.AssociationId, - HasIAMResourcesStack: hasStack, - StackName: stackName, - }, nil + return nil } diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index 1d6bcaa085..0d4f039b16 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -38,7 +38,7 @@ type Addon struct { // ResolvePodIdentityConflicts ResolvePodIdentityConflicts ekstypes.ResolveConflicts `json:"resolvePodIdentityConflicts,omitempty"` // PodIdentityAssociations - PodIdentityAssociations []PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` + PodIdentityAssociations *[]PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` // ConfigurationValues defines the set of configuration properties for add-ons. // For now, all properties will be specified as a JSON string // and have to respect the schema from DescribeAddonConfiguration. diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index 6e545fcc09..a79a5a5614 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -300,6 +300,9 @@ const ( // PodIdentityAssociationNameTag defines the tag of Pod Identity Association name PodIdentityAssociationNameTag = "alpha.eksctl.io/podidentityassociation-name" + // AddonPodIdentityAssociationNameTag defines the tag name for an addon's pod identity association. + AddonPodIdentityAssociationNameTag = "alpha.eksctl.io/addon-podidentityassociation-name" + // AddonNameTag defines the tag of the IAM service account name AddonNameTag = "alpha.eksctl.io/addon-name" diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index 71113ae9e3..6cab4fa743 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -401,9 +401,8 @@ func (c *StackCollection) DescribeStack(ctx context.Context, i *Stack) (*Stack, } func IsStackDoesNotExistError(err error) bool { - awsError, ok := errors.Unwrap(errors.Unwrap(err)).(*smithy.OperationError) - return ok && strings.Contains(awsError.Error(), "ValidationError") - + var opErr *smithy.OperationError + return errors.As(err, &opErr) && strings.Contains(opErr.Error(), "ValidationError") } // GetManagedNodeGroupTemplate returns the template for a ManagedNodeGroup resource diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 975095bd2e..20b9169ffe 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -311,7 +311,7 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. if cfg.IAM != nil && cfg.IAM.AutoCreatePodIdentityAssociations { return true } - if len(addon.PodIdentityAssociations) > 0 { + if addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0 { return true } } diff --git a/pkg/ctl/create/addon.go b/pkg/ctl/create/addon.go index 286e03761d..013d5d15fc 100644 --- a/pkg/ctl/create/addon.go +++ b/pkg/ctl/create/addon.go @@ -93,6 +93,10 @@ func createAddonCmd(cmd *cmdutils.Cmd) { return err } + iamRoleCreator := &podidentityassociation.IAMRoleCreator{ + ClusterName: cmd.ClusterConfig.Metadata.Name, + StackCreator: stackManager, + } // always install EKS Pod Identity Agent Addon first, if present, // as other addons might require IAM permissions for _, a := range cmd.ClusterConfig.Addons { @@ -102,7 +106,7 @@ func createAddonCmd(cmd *cmdutils.Cmd) { if force { //force is specified at cmdline level a.Force = true } - if err := addonManager.Create(ctx, a, cmd.ProviderConfig.WaitTimeout); err != nil { + if err := addonManager.Create(ctx, a, iamRoleCreator, cmd.ProviderConfig.WaitTimeout); err != nil { return err } } @@ -114,7 +118,7 @@ func createAddonCmd(cmd *cmdutils.Cmd) { if force { //force is specified at cmdline level a.Force = true } - if err := addonManager.Create(ctx, a, cmd.ProviderConfig.WaitTimeout); err != nil { + if err := addonManager.Create(ctx, a, iamRoleCreator, cmd.ProviderConfig.WaitTimeout); err != nil { return err } } @@ -135,7 +139,7 @@ func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg * if a.CanonicalName() == api.PodIdentityAgentAddon { podIdentityAgentFoundInConfig = true } - if len(a.PodIdentityAssociations) > 0 { + if a.PodIdentityAssociations != nil && len(*a.PodIdentityAssociations) > 0 { shallCreatePodIdentityAssociations = true } } diff --git a/pkg/ctl/create/cluster.go b/pkg/ctl/create/cluster.go index 4f0743e2d1..7babc3d8d6 100644 --- a/pkg/ctl/create/cluster.go +++ b/pkg/ctl/create/cluster.go @@ -357,7 +357,11 @@ func doCreateCluster(cmd *cmdutils.Cmd, ngFilter *filter.NodeGroupFilter, params var preNodegroupAddons, postNodegroupAddons *tasks.TaskTree if len(cfg.Addons) > 0 { - preNodegroupAddons, postNodegroupAddons = addon.CreateAddonTasks(ctx, cfg, ctl, true, cmd.ProviderConfig.WaitTimeout) + iamRoleCreator := &podidentityassociation.IAMRoleCreator{ + ClusterName: cfg.Metadata.Name, + StackCreator: stackManager, + } + preNodegroupAddons, postNodegroupAddons = addon.CreateAddonTasks(ctx, cfg, ctl, iamRoleCreator, true, cmd.ProviderConfig.WaitTimeout) postClusterCreationTasks.Append(preNodegroupAddons) } diff --git a/pkg/ctl/get/addon.go b/pkg/ctl/get/addon.go index 481bf12eb2..d337005fdb 100644 --- a/pkg/ctl/get/addon.go +++ b/pkg/ctl/get/addon.go @@ -4,6 +4,8 @@ import ( "context" "fmt" "os" + "slices" + "strings" awseks "github.com/aws/aws-sdk-go-v2/service/eks" @@ -106,10 +108,10 @@ func getAddon(cmd *cmdutils.Cmd, a *api.Addon, params *getCmdParams) error { } if tablePrinter, ok := printer.(*printers.TablePrinter); ok { - for _, summary := range summaries { - if len(summary.PodIdentityAssociations) > 0 { - logger.Info("to view pod identity associations for an addon, rerun the command with --output=json or --output=yaml") - } + if slices.ContainsFunc(summaries, func(summary addon.Summary) bool { + return len(summary.PodIdentityAssociations) > 0 + }) { + logger.Info("to view pod identity associations for an addon, rerun the command with --output=json or --output=yaml") } addAddonSummaryTableColumns(tablePrinter) } @@ -150,7 +152,11 @@ func addAddonSummaryTableColumns(printer *printers.TablePrinter) { printer.AddColumn("CONFIGURATION VALUES", func(s addon.Summary) string { return s.ConfigurationValues }) - printer.AddColumn("POD IDENTITY ASSOCIATIONS", func(s addon.Summary) int { - return len(s.PodIdentityAssociations) + printer.AddColumn("POD IDENTITY ASSOCIATION ROLES", func(s addon.Summary) string { + var roleARNs []string + for _, pia := range s.PodIdentityAssociations { + roleARNs = append(roleARNs, pia.RoleARN) + } + return strings.Join(roleARNs, ",") }) } diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index b5c9db0d64..e7bae35691 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -3,7 +3,6 @@ package update import ( "context" "fmt" - awseks "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/kris-nova/logger" @@ -102,7 +101,7 @@ func updateAddon(cmd *cmdutils.Cmd, force, wait bool) error { StackUpdater: stackManager, }, EKSPodIdentityDescriber: clusterProvider.AWSProvider.EKS(), - PodIdentityStackLister: stackManager, + StackDescriber: stackManager, } for _, a := range cmd.ClusterConfig.Addons { diff --git a/pkg/ctl/utils/migrate_to_pod_identity.go b/pkg/ctl/utils/migrate_to_pod_identity.go index d704f83499..7a072150f0 100644 --- a/pkg/ctl/utils/migrate_to_pod_identity.go +++ b/pkg/ctl/utils/migrate_to_pod_identity.go @@ -3,6 +3,7 @@ package utils import ( "context" "fmt" + "time" "github.com/kris-nova/logger" "github.com/spf13/cobra" @@ -76,17 +77,34 @@ func doMigrateToPodIdentity(cmd *cmdutils.Cmd, options podidentityassociation.Po return err } - addonCreator, err := addon.New(cfg, ctl.AWSProvider.EKS(), nil, false, nil, nil) + addonManager, err := addon.New(cfg, ctl.AWSProvider.EKS(), nil, false, nil, nil) if err != nil { return fmt.Errorf("initializing addon creator %w", err) } + stackManager := ctl.NewStackManager(cfg) + iamRoleCreator := &podidentityassociation.IAMRoleCreator{ + ClusterName: cfg.Metadata.Name, + StackCreator: stackManager, + } return podidentityassociation.NewMigrator( cfg.Metadata.Name, ctl.AWSProvider.EKS(), ctl.AWSProvider.IAM(), - ctl.NewStackManager(cfg), + stackManager, clientSet, - addonCreator, + &addonCreator{ + addonManager: addonManager, + iamRoleCreator: iamRoleCreator, + }, ).MigrateToPodIdentity(ctx, options) } + +type addonCreator struct { + addonManager *addon.Manager + iamRoleCreator addon.IAMRoleCreator +} + +func (a *addonCreator) Create(ctx context.Context, addon *api.Addon, waitTimeout time.Duration) error { + return a.addonManager.Create(ctx, addon, a.iamRoleCreator, waitTimeout) +} From 961bd52625918403fdb6275921f825a0a5bb1ba4 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 01:26:04 +0530 Subject: [PATCH 09/35] Update mocks --- .mockery.yaml | 8 +++ .../addon/mocks/PodIdentityIAMUpdater.go | 18 +++--- .../mocks/StackDescriber.go | 60 +++++++++++++++++++ 3 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 pkg/actions/podidentityassociation/mocks/StackDescriber.go diff --git a/.mockery.yaml b/.mockery.yaml index 99fca2b58a..c0f48180f7 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -65,3 +65,11 @@ packages: config: dir: "{{.InterfaceDir}}/mocks" outpkg: mocks + + github.com/weaveworks/eksctl/pkg/actions/podidentityassociation: + interfaces: + StackDescriber: + config: + dir: "{{.InterfaceDir}}/mocks" + outpkg: mocks + diff --git a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go index 1fc1c10dbc..3f40f6b334 100644 --- a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go +++ b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go @@ -16,9 +16,9 @@ type PodIdentityIAMUpdater struct { mock.Mock } -// UpdateRole provides a mock function with given fields: ctx, podIdentityAssociations -func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []v1alpha5.PodIdentityAssociation) ([]types.AddonPodIdentityAssociations, error) { - ret := _m.Called(ctx, podIdentityAssociations) +// UpdateRole provides a mock function with given fields: ctx, podIdentityAssociations, addonName +func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []v1alpha5.PodIdentityAssociation, addonName string) ([]types.AddonPodIdentityAssociations, error) { + ret := _m.Called(ctx, podIdentityAssociations, addonName) if len(ret) == 0 { panic("no return value specified for UpdateRole") @@ -26,19 +26,19 @@ func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAsso var r0 []types.AddonPodIdentityAssociations var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation) ([]types.AddonPodIdentityAssociations, error)); ok { - return rf(ctx, podIdentityAssociations) + if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation, string) ([]types.AddonPodIdentityAssociations, error)); ok { + return rf(ctx, podIdentityAssociations, addonName) } - if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation) []types.AddonPodIdentityAssociations); ok { - r0 = rf(ctx, podIdentityAssociations) + if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation, string) []types.AddonPodIdentityAssociations); ok { + r0 = rf(ctx, podIdentityAssociations, addonName) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]types.AddonPodIdentityAssociations) } } - if rf, ok := ret.Get(1).(func(context.Context, []v1alpha5.PodIdentityAssociation) error); ok { - r1 = rf(ctx, podIdentityAssociations) + if rf, ok := ret.Get(1).(func(context.Context, []v1alpha5.PodIdentityAssociation, string) error); ok { + r1 = rf(ctx, podIdentityAssociations, addonName) } else { r1 = ret.Error(1) } diff --git a/pkg/actions/podidentityassociation/mocks/StackDescriber.go b/pkg/actions/podidentityassociation/mocks/StackDescriber.go new file mode 100644 index 0000000000..7d538658fa --- /dev/null +++ b/pkg/actions/podidentityassociation/mocks/StackDescriber.go @@ -0,0 +1,60 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" +) + +// StackDescriber is an autogenerated mock type for the StackDescriber type +type StackDescriber struct { + mock.Mock +} + +// DescribeStack provides a mock function with given fields: _a0, _a1 +func (_m *StackDescriber) DescribeStack(_a0 context.Context, _a1 *types.Stack) (*types.Stack, error) { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for DescribeStack") + } + + var r0 *types.Stack + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Stack) (*types.Stack, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.Stack) *types.Stack); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Stack) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.Stack) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewStackDescriber creates a new instance of StackDescriber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStackDescriber(t interface { + mock.TestingT + Cleanup(func()) +}) *StackDescriber { + mock := &StackDescriber{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} From 07d83f110290ea730520d3be8c2f425e36c50f5c Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 01:29:24 +0530 Subject: [PATCH 10/35] Fix deleting the specified addon instead of all addons --- pkg/actions/addon/addon.go | 1 + pkg/actions/addon/delete.go | 40 ++++++---- pkg/actions/addon/fakes/fake_stack_manager.go | 74 +++++++++++++++++++ 3 files changed, 102 insertions(+), 13 deletions(-) diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index e620188d47..d1eea8278b 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -31,6 +31,7 @@ type StackManager interface { DescribeStack(ctx context.Context, i *cfntypes.Stack) (*cfntypes.Stack, error) GetIAMAddonsStacks(ctx context.Context) ([]*cfntypes.Stack, error) UpdateStack(ctx context.Context, options manager.UpdateStackOptions) error + GetIAMAddonName(s *cfntypes.Stack) string } // CreateClientSet creates a Kubernetes ClientSet. diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index cf76d31b9c..3f4ee67f89 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" @@ -31,27 +30,23 @@ func (a *Manager) Delete(ctx context.Context, addon *api.Addon) error { logger.Info("deleted addon: %s", addon.Name) } - deleteAddonIAMTasks, err := NewRemover(a.stackManager).DeleteAddonIAMTasks(ctx, true) + deleteTask, err := NewRemover(a.stackManager).DeleteAddon(ctx, addon, false) if err != nil { return err } - if deleteAddonIAMTasks.Len() > 0 { + if deleteTask != nil { logger.Info("deleting associated IAM stacks") - logger.Info(deleteAddonIAMTasks.Describe()) - if errs := deleteAddonIAMTasks.DoAllSync(); len(errs) > 0 { - var allErrs []string - for _, err := range errs { - allErrs = append(allErrs, err.Error()) - } - return fmt.Errorf(strings.Join(allErrs, "\n")) + errCh := make(chan error) + if err := deleteTask.Do(errCh); err != nil { + return err } - logger.Info("all tasks were completed successfully") - } else if addonExists { + return <-errCh + } + if addonExists { logger.Info("no associated IAM stacks found") } else { return errors.New("could not find addon or associated IAM stack to delete") } - return nil } @@ -100,3 +95,22 @@ func (ar *Remover) DeleteAddonIAMTasks(ctx context.Context, wait bool) (*tasks.T } return taskTree, nil } + +func (ar *Remover) DeleteAddon(ctx context.Context, addon *api.Addon, wait bool) (tasks.Task, error) { + stacks, err := ar.stackManager.GetIAMAddonsStacks(ctx) + if err != nil { + return nil, err + } + for _, stack := range stacks { + if ar.stackManager.GetIAMAddonName(stack) == addon.Name { + return &deleteAddonIAMTask{ + ctx: ctx, + info: fmt.Sprintf("delete addon IAM %q", *stack.StackName), + stack: stack, + stackManager: ar.stackManager, + wait: wait, + }, nil + } + } + return nil, nil +} diff --git a/pkg/actions/addon/fakes/fake_stack_manager.go b/pkg/actions/addon/fakes/fake_stack_manager.go index 10d8493d0f..9b360538f3 100644 --- a/pkg/actions/addon/fakes/fake_stack_manager.go +++ b/pkg/actions/addon/fakes/fake_stack_manager.go @@ -69,6 +69,17 @@ type FakeStackManager struct { result1 *types.Stack result2 error } + GetIAMAddonNameStub func(*types.Stack) string + getIAMAddonNameMutex sync.RWMutex + getIAMAddonNameArgsForCall []struct { + arg1 *types.Stack + } + getIAMAddonNameReturns struct { + result1 string + } + getIAMAddonNameReturnsOnCall map[int]struct { + result1 string + } GetIAMAddonsStacksStub func(context.Context) ([]*types.Stack, error) getIAMAddonsStacksMutex sync.RWMutex getIAMAddonsStacksArgsForCall []struct { @@ -357,6 +368,67 @@ func (fake *FakeStackManager) DescribeStackReturnsOnCall(i int, result1 *types.S }{result1, result2} } +func (fake *FakeStackManager) GetIAMAddonName(arg1 *types.Stack) string { + fake.getIAMAddonNameMutex.Lock() + ret, specificReturn := fake.getIAMAddonNameReturnsOnCall[len(fake.getIAMAddonNameArgsForCall)] + fake.getIAMAddonNameArgsForCall = append(fake.getIAMAddonNameArgsForCall, struct { + arg1 *types.Stack + }{arg1}) + stub := fake.GetIAMAddonNameStub + fakeReturns := fake.getIAMAddonNameReturns + fake.recordInvocation("GetIAMAddonName", []interface{}{arg1}) + fake.getIAMAddonNameMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeStackManager) GetIAMAddonNameCallCount() int { + fake.getIAMAddonNameMutex.RLock() + defer fake.getIAMAddonNameMutex.RUnlock() + return len(fake.getIAMAddonNameArgsForCall) +} + +func (fake *FakeStackManager) GetIAMAddonNameCalls(stub func(*types.Stack) string) { + fake.getIAMAddonNameMutex.Lock() + defer fake.getIAMAddonNameMutex.Unlock() + fake.GetIAMAddonNameStub = stub +} + +func (fake *FakeStackManager) GetIAMAddonNameArgsForCall(i int) *types.Stack { + fake.getIAMAddonNameMutex.RLock() + defer fake.getIAMAddonNameMutex.RUnlock() + argsForCall := fake.getIAMAddonNameArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStackManager) GetIAMAddonNameReturns(result1 string) { + fake.getIAMAddonNameMutex.Lock() + defer fake.getIAMAddonNameMutex.Unlock() + fake.GetIAMAddonNameStub = nil + fake.getIAMAddonNameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeStackManager) GetIAMAddonNameReturnsOnCall(i int, result1 string) { + fake.getIAMAddonNameMutex.Lock() + defer fake.getIAMAddonNameMutex.Unlock() + fake.GetIAMAddonNameStub = nil + if fake.getIAMAddonNameReturnsOnCall == nil { + fake.getIAMAddonNameReturnsOnCall = make(map[int]struct { + result1 string + }) + } + fake.getIAMAddonNameReturnsOnCall[i] = struct { + result1 string + }{result1} +} + func (fake *FakeStackManager) GetIAMAddonsStacks(arg1 context.Context) ([]*types.Stack, error) { fake.getIAMAddonsStacksMutex.Lock() ret, specificReturn := fake.getIAMAddonsStacksReturnsOnCall[len(fake.getIAMAddonsStacksArgsForCall)] @@ -494,6 +566,8 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.deleteStackBySpecSyncMutex.RUnlock() fake.describeStackMutex.RLock() defer fake.describeStackMutex.RUnlock() + fake.getIAMAddonNameMutex.RLock() + defer fake.getIAMAddonNameMutex.RUnlock() fake.getIAMAddonsStacksMutex.RLock() defer fake.getIAMAddonsStacksMutex.RUnlock() fake.updateStackMutex.RLock() From bfb0a8b7ec695db80698eb591bbc8e6f828d646c Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 02:09:51 +0530 Subject: [PATCH 11/35] Disallow deletion of addon pod identities in `delete podidentityassociation` --- pkg/actions/podidentityassociation/deleter.go | 55 +++++++++++-------- .../podidentityassociation/deleter_test.go | 31 +++++++++++ 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/pkg/actions/podidentityassociation/deleter.go b/pkg/actions/podidentityassociation/deleter.go index a9d1aa533e..8abfca8087 100644 --- a/pkg/actions/podidentityassociation/deleter.go +++ b/pkg/actions/podidentityassociation/deleter.go @@ -122,7 +122,11 @@ func (d *Deleter) DeleteTasks(ctx context.Context, podIDs []Identifier) (*tasks. Parallel: false, IsSubTask: true, } - piaDeletionTasks.Append(d.makeDeleteTask(ctx, podID, roleStackNames)) + deleteTask, err := d.makeDeleteTask(ctx, podID, roleStackNames) + if err != nil { + return nil, err + } + piaDeletionTasks.Append(deleteTask) piaDeletionTasks.Append(&tasks.GenericTask{ Description: fmt.Sprintf("delete service account %q, if it exists and is managed by eksctl", podID.IDString()), Doer: func() error { @@ -140,47 +144,54 @@ func (d *Deleter) DeleteTasks(ctx context.Context, podIDs []Identifier) (*tasks. return taskTree, nil } -func (d *Deleter) makeDeleteTask(ctx context.Context, p Identifier, roleStackNames []string) tasks.Task { +func (d *Deleter) makeDeleteTask(ctx context.Context, p Identifier, roleStackNames []string) (tasks.Task, error) { podIdentityAssociationID := p.IDString() - return &tasks.GenericTask{ - Description: fmt.Sprintf("delete pod identity association %q", podIdentityAssociationID), - Doer: func() error { - if err := d.deletePodIdentityAssociation(ctx, p, roleStackNames, podIdentityAssociationID); err != nil { - return fmt.Errorf("error deleting pod identity association %q: %w", podIdentityAssociationID, err) - } - return nil - }, - } -} - -func (d *Deleter) deletePodIdentityAssociation(ctx context.Context, p Identifier, roleStackNames []string, podIdentityAssociationID string) error { output, err := d.APIDeleter.ListPodIdentityAssociations(ctx, &eks.ListPodIdentityAssociationsInput{ ClusterName: aws.String(d.ClusterName), Namespace: aws.String(p.Namespace), ServiceAccount: aws.String(p.ServiceAccountName), }) if err != nil { - return fmt.Errorf("listing pod identity associations: %w", err) + return nil, fmt.Errorf("listing pod identity associations: %w", err) } + switch len(output.Associations) { default: - return fmt.Errorf("expected to find only 1 pod identity association for %q; got %d", podIdentityAssociationID, len(output.Associations)) + return nil, fmt.Errorf("expected to find only 1 pod identity association for %q; got %d", podIdentityAssociationID, len(output.Associations)) case 0: logger.Warning("pod identity association %q not found", podIdentityAssociationID) case 1: - if _, err := d.APIDeleter.DeletePodIdentityAssociation(ctx, &eks.DeletePodIdentityAssociationInput{ - ClusterName: aws.String(d.ClusterName), - AssociationId: output.Associations[0].AssociationId, - }); err != nil { - return fmt.Errorf("deleting pod identity association: %w", err) + association := output.Associations[0] + if association.OwnerArn != nil { + return nil, fmt.Errorf("cannot delete podidentityassociation %s as it is in use by addon %s; "+ + "please use `eksctl update addon` or `eksctl delete addon` instead", p.IDString(), *association.OwnerArn) } } + return &tasks.GenericTask{ + Description: fmt.Sprintf("delete pod identity association %q", podIdentityAssociationID), + Doer: func() error { + if len(output.Associations) == 1 { + if _, err := d.APIDeleter.DeletePodIdentityAssociation(ctx, &eks.DeletePodIdentityAssociationInput{ + ClusterName: aws.String(d.ClusterName), + AssociationId: output.Associations[0].AssociationId, + }); err != nil { + return fmt.Errorf("deleting pod identity association: %w", err) + } + } + if err := d.deleteIAMResources(ctx, p, roleStackNames); err != nil { + return fmt.Errorf("error deleting pod identity association %q: %w", podIdentityAssociationID, err) + } + return nil + }, + }, nil +} +func (d *Deleter) deleteIAMResources(ctx context.Context, p Identifier, roleStackNames []string) error { stackName, hasStack := getIAMResourcesStack(roleStackNames, p) if !hasStack { return nil } - logger.Info("deleting IAM resources stack %q for pod identity association %q", stackName, podIdentityAssociationID) + logger.Info("deleting IAM resources stack %q for pod identity association %q", stackName, p.IDString()) return d.deleteRoleStack(ctx, stackName) } diff --git a/pkg/actions/podidentityassociation/deleter_test.go b/pkg/actions/podidentityassociation/deleter_test.go index b28ac506ba..2cef0e8e14 100644 --- a/pkg/actions/podidentityassociation/deleter_test.go +++ b/pkg/actions/podidentityassociation/deleter_test.go @@ -323,5 +323,36 @@ var _ = Describe("Pod Identity Deleter", func() { )) }, }), + + Entry("attempting to delete a pod identity associated with an addon", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, fakeClientSet *kubeclientfakes.Clientset, eksAPI *mocksv2.EKS) { + podID := podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + } + mockListStackNames(stackManager, []podidentityassociation.Identifier{podID}) + mockListPodIdentityAssociations(eksAPI, podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, []ekstypes.PodIdentityAssociationSummary{ + { + OwnerArn: aws.String("arn:aws:eks:us-west-2:00:addon/cluster/vpc-cni/14c7a7ae-78a2-2c58-609e-d80af6f7bb3e"), + }, + }, nil) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) + eksAPI.AssertExpectations(GinkgoT()) + }, + + expectedErr: "cannot delete podidentityassociation kube-system/vpc-cni as it is in use by addon arn:aws:eks:us-west-2:00:addon/cluster/vpc-cni/14c7a7ae-78a2-2c58-609e-d80af6f7bb3e; please use `eksctl update addon` or `eksctl delete addon` instead", + }), ) }) From 77f841da46a8f721784284f14532e00cdee00fb2 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 02:19:54 +0530 Subject: [PATCH 12/35] Show ownerARN in `get podidentityassociations` --- pkg/actions/podidentityassociation/getter.go | 9 +++++++-- pkg/actions/podidentityassociation/getter_test.go | 12 +++++++++++- pkg/ctl/get/pod_identity_association.go | 3 +++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/pkg/actions/podidentityassociation/getter.go b/pkg/actions/podidentityassociation/getter.go index c68a576b8c..0c4f0a551b 100644 --- a/pkg/actions/podidentityassociation/getter.go +++ b/pkg/actions/podidentityassociation/getter.go @@ -13,6 +13,7 @@ type Summary struct { Namespace string ServiceAccountName string RoleARN string + OwnerARN string } type Getter struct { @@ -54,12 +55,16 @@ func (g *Getter) GetPodIdentityAssociations(ctx context.Context, namespace, serv return summaries, fmt.Errorf("failed to describe pod identity association with associationID: %s", *a.AssociationId) } - summaries = append(summaries, Summary{ + summary := Summary{ AssociationARN: *describeOut.Association.AssociationArn, Namespace: *describeOut.Association.Namespace, ServiceAccountName: *describeOut.Association.ServiceAccount, RoleARN: *describeOut.Association.RoleArn, - }) + } + if describeOut.Association.OwnerArn != nil { + summary.OwnerARN = *describeOut.Association.OwnerArn + } + summaries = append(summaries, summary) } return summaries, nil diff --git a/pkg/actions/podidentityassociation/getter_test.go b/pkg/actions/podidentityassociation/getter_test.go index 270cfa67f7..9f8795783d 100644 --- a/pkg/actions/podidentityassociation/getter_test.go +++ b/pkg/actions/podidentityassociation/getter_test.go @@ -127,7 +127,8 @@ var _ = Describe("Get", func() { Once() mockDescribePodIdentityAssociation(mockProvider, associationID1, associationARN1, namespace1, serviceAccountName1) - mockDescribePodIdentityAssociation(mockProvider, associationID2, associationARN2, namespace1, serviceAccountName2) + mockDescribePodIdentityAssociationWithOwnerARN(mockProvider, associationID2, associationARN2, namespace1, serviceAccountName2, + "arn:aws:eks:us-west-2:00:addon/cluster/vpc-cni/14c7a7ae-78a2-2c58-609e-d80af6f7bb3e") mockDescribePodIdentityAssociation(mockProvider, associationID3, associationARN3, namespace2, serviceAccountName2) }, expectedAssociations: []podidentityassociation.Summary{ @@ -142,6 +143,7 @@ var _ = Describe("Get", func() { Namespace: namespace1, ServiceAccountName: serviceAccountName2, RoleARN: roleARN, + OwnerARN: "arn:aws:eks:us-west-2:00:addon/cluster/vpc-cni/14c7a7ae-78a2-2c58-609e-d80af6f7bb3e", }, { AssociationARN: associationARN3, @@ -248,6 +250,13 @@ var _ = Describe("Get", func() { func mockDescribePodIdentityAssociation( mp *mockprovider.MockProvider, associationID, associationARN, namespace, serviceAccount string, +) { + mockDescribePodIdentityAssociationWithOwnerARN(mp, associationID, associationARN, namespace, serviceAccount, "") +} + +func mockDescribePodIdentityAssociationWithOwnerARN( + mp *mockprovider.MockProvider, + associationID, associationARN, namespace, serviceAccount, ownerARN string, ) { mp.MockEKS(). On("DescribePodIdentityAssociation", mock.Anything, &awseks.DescribePodIdentityAssociationInput{ @@ -260,6 +269,7 @@ func mockDescribePodIdentityAssociation( Namespace: &namespace, ServiceAccount: &serviceAccount, RoleArn: &roleARN, + OwnerArn: &ownerARN, }, }, nil). Once() diff --git a/pkg/ctl/get/pod_identity_association.go b/pkg/ctl/get/pod_identity_association.go index 3dfa833318..edb7123295 100644 --- a/pkg/ctl/get/pod_identity_association.go +++ b/pkg/ctl/get/pod_identity_association.go @@ -92,4 +92,7 @@ func addPodIdentityAssociationSummaryTableColumns(printer *printers.TablePrinter printer.AddColumn("IAM ROLE ARN", func(s podidentityassociation.Summary) string { return s.RoleARN }) + printer.AddColumn("OWNER ARN", func(s podidentityassociation.Summary) string { + return s.OwnerARN + }) } From aeac061eb1ddc9154aca8a4bcc8996393b8f9bdf Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 03:49:43 +0530 Subject: [PATCH 13/35] Fix `create cluster` when iam.podIdentityAssociations is unset --- pkg/ctl/cmdutils/configfile.go | 6 ++++-- pkg/ctl/cmdutils/pod_identity_association.go | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 20b9169ffe..2cdd80066d 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -327,8 +327,10 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. suggestion := fmt.Sprintf("please add %q addon to the config file", api.PodIdentityAgentAddon) return api.ErrPodIdentityAgentNotInstalled(suggestion) } - if err := validatePodIdentityAssociationsForConfig(clusterConfig, true); err != nil { - return err + if clusterConfig.IAM != nil && len(clusterConfig.IAM.PodIdentityAssociations) > 0 { + if err := validatePodIdentityAssociations(clusterConfig.IAM.PodIdentityAssociations, true); err != nil { + return err + } } } diff --git a/pkg/ctl/cmdutils/pod_identity_association.go b/pkg/ctl/cmdutils/pod_identity_association.go index 85732fc3d3..ced5958310 100644 --- a/pkg/ctl/cmdutils/pod_identity_association.go +++ b/pkg/ctl/cmdutils/pod_identity_association.go @@ -114,8 +114,11 @@ func validatePodIdentityAssociationsForConfig(clusterConfig *api.ClusterConfig, if clusterConfig.IAM == nil || len(clusterConfig.IAM.PodIdentityAssociations) == 0 { return errors.New("no iam.podIdentityAssociations specified in the config file") } + return validatePodIdentityAssociations(clusterConfig.IAM.PodIdentityAssociations, isCreate) +} - for i, pia := range clusterConfig.IAM.PodIdentityAssociations { +func validatePodIdentityAssociations(podIdentityAssociations []api.PodIdentityAssociation, isCreate bool) error { + for i, pia := range podIdentityAssociations { path := fmt.Sprintf("podIdentityAssociations[%d]", i) if pia.Namespace == "" { return fmt.Errorf("%s.namespace must be set", path) @@ -149,7 +152,6 @@ func validatePodIdentityAssociationsForConfig(clusterConfig *api.ClusterConfig, } } } - return nil } From b9eea35564c7814e34323a255d65d7ea20ae4cab Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 7 May 2024 17:40:04 +0530 Subject: [PATCH 14/35] Delete IAM resources when addon.podIdentityAssociations is [] --- pkg/actions/addon/delete.go | 7 +- pkg/actions/addon/podidentityassociation.go | 32 +++- .../addon/podidentityassociation_test.go | 45 +++--- pkg/actions/addon/update.go | 22 +++ .../mocks/StackDeleter.go | 140 ++++++++++++++++++ .../mocks/StackDescriber.go | 60 -------- pkg/ctl/update/addon.go | 2 +- 7 files changed, 216 insertions(+), 92 deletions(-) create mode 100644 pkg/actions/podidentityassociation/mocks/StackDeleter.go delete mode 100644 pkg/actions/podidentityassociation/mocks/StackDescriber.go diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index 3f4ee67f89..318eb9eb1b 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -40,7 +40,12 @@ func (a *Manager) Delete(ctx context.Context, addon *api.Addon) error { if err := deleteTask.Do(errCh); err != nil { return err } - return <-errCh + select { + case err := <-errCh: + return err + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for deletion of addon %s: %w", addon.Name, ctx.Err()) + } } if addonExists { logger.Info("no associated IAM stacks found") diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go index d23eb6c723..b151fa099a 100644 --- a/pkg/actions/addon/podidentityassociation.go +++ b/pkg/actions/addon/podidentityassociation.go @@ -31,7 +31,7 @@ type PodIdentityAssociationUpdater struct { IAMRoleCreator IAMRoleCreator IAMRoleUpdater IAMRoleUpdater EKSPodIdentityDescriber EKSPodIdentityDescriber - StackDescriber podidentityassociation.StackDescriber + StackDeleter podidentityassociation.StackDeleter } func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string) ([]ekstypes.AddonPodIdentityAssociations, error) { @@ -69,7 +69,7 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent } stackName := podidentityassociation.MakeAddonPodIdentityStackName(p.ClusterName, addonName, pia.ServiceAccountName) hasStack := true - if _, err := p.StackDescriber.DescribeStack(ctx, &manager.Stack{ + if _, err := p.StackDeleter.DescribeStack(ctx, &manager.Stack{ StackName: aws.String(stackName), }); err != nil { if !manager.IsStackDoesNotExistError(err) { @@ -79,7 +79,7 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent } roleValidator := &podidentityassociation.RoleUpdateValidator{ - StackDescriber: p.StackDescriber, + StackDescriber: p.StackDeleter, } if err := roleValidator.ValidateRoleUpdate(pia, *output.Association, hasStack); err != nil { return nil, err @@ -104,3 +104,29 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent } return addonPodIdentityAssociations, nil } + +func (p *PodIdentityAssociationUpdater) DeleteRole(ctx context.Context, addonName, serviceAccountName string) (bool, error) { + stack, err := p.StackDeleter.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(podidentityassociation.MakeAddonPodIdentityStackName(p.ClusterName, addonName, serviceAccountName)), + }) + if err != nil { + if manager.IsStackDoesNotExistError(err) { + return false, nil + } + return false, fmt.Errorf("describing IAM resources stack for addon %s: %w", addonName, err) + } + + errCh := make(chan error) + if err := p.StackDeleter.DeleteStackBySpecSync(ctx, stack, errCh); err != nil { + return false, fmt.Errorf("deleting stack %s: %w", *stack.StackName, err) + } + select { + case err := <-errCh: + if err != nil { + return false, fmt.Errorf("deleting stack %s: %w", *stack.StackName, err) + } + return true, nil + case <-ctx.Done(): + return false, fmt.Errorf("timed out waiting for deletion of stack %s: %w", *stack.StackName, ctx.Err()) + } +} diff --git a/pkg/actions/addon/podidentityassociation_test.go b/pkg/actions/addon/podidentityassociation_test.go index 3bfad3a29e..ae733bcf1f 100644 --- a/pkg/actions/addon/podidentityassociation_test.go +++ b/pkg/actions/addon/podidentityassociation_test.go @@ -18,28 +18,24 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/addon/mocks" - "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/fakes" piamocks "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/mocks" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/manager" - managerfakes "github.com/weaveworks/eksctl/pkg/cfn/manager/fakes" "github.com/weaveworks/eksctl/pkg/eks/mocksv2" "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" ) var _ = Describe("Update Pod Identity Association", func() { type piaMocks struct { - stackManager *fakes.FakeStackUpdater - stackDescriber *piamocks.StackDescriber - roleCreator *mocks.IAMRoleCreator - roleUpdater *mocks.IAMRoleUpdater - eks *mocksv2.EKS + stackDeleter *piamocks.StackDeleter + roleCreator *mocks.IAMRoleCreator + roleUpdater *mocks.IAMRoleUpdater + eks *mocksv2.EKS } type updateEntry struct { podIdentityAssociations []api.PodIdentityAssociation mockCalls func(m piaMocks) - expectedCalls func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) expectedAddonPodIdentityAssociations []ekstypes.AddonPodIdentityAssociations expectedErr string @@ -108,10 +104,9 @@ var _ = Describe("Update Pod Identity Association", func() { DescribeTable("update pod identity association", func(e updateEntry) { provider := mockprovider.NewMockProvider() var ( - roleCreator mocks.IAMRoleCreator - roleUpdater mocks.IAMRoleUpdater - stackUpdater fakes.FakeStackUpdater - stackDescriber piamocks.StackDescriber + roleCreator mocks.IAMRoleCreator + roleUpdater mocks.IAMRoleUpdater + stackDeleter piamocks.StackDeleter ) piaUpdater := &addon.PodIdentityAssociationUpdater{ @@ -119,15 +114,14 @@ var _ = Describe("Update Pod Identity Association", func() { IAMRoleCreator: &roleCreator, IAMRoleUpdater: &roleUpdater, EKSPodIdentityDescriber: provider.MockEKS(), - StackDescriber: &stackDescriber, + StackDeleter: &stackDeleter, } if e.mockCalls != nil { e.mockCalls(piaMocks{ - stackManager: &stackUpdater, - stackDescriber: &stackDescriber, - roleCreator: &roleCreator, - roleUpdater: &roleUpdater, - eks: provider.MockEKS(), + stackDeleter: &stackDeleter, + roleCreator: &roleCreator, + roleUpdater: &roleUpdater, + eks: provider.MockEKS(), }) } addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations, "") @@ -208,7 +202,7 @@ var _ = Describe("Update Pod Identity Association", func() { Namespace: "kube-system", ServiceAccountName: "vpc-cni", }, "eksctl-test-addon--podidentityrole-vpc-cni", "a-1").Return("cni-role-2", true, nil).Once() - m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), }).Return(&manager.Stack{}, nil) @@ -253,9 +247,6 @@ var _ = Describe("Update Pod Identity Association", func() { }, }, mockCalls: func(m piaMocks) { - - // TODO: - //m.stackManager.DescribeStackStub mockListPodIdentityAssociations(m.eks, true, defaultListPodIdentityInputs) mockDescribePodIdentityAssociation(m.eks, "cni-role", "csi-role", "karpenter-role") @@ -296,7 +287,7 @@ var _ = Describe("Update Pod Identity Association", func() { ServiceAccountName: updateInput.serviceAccount, }, stackName, id).Return(updateInput.returnRole, true, nil).Once() - m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String(stackName), }).Return(&manager.Stack{ StackName: aws.String(stackName), @@ -429,13 +420,13 @@ var _ = Describe("Update Pod Identity Association", func() { }) mockDescribePodIdentityAssociation(m.eks, "role-1", "role-2", "role-3") for _, serviceAccount := range []string{"vpc-cni", "aws-ebs-csi-driver"} { - m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String(fmt.Sprintf("eksctl-test-addon--podidentityrole-%s", serviceAccount)), }).Return(nil, &smithy.OperationError{ Err: fmt.Errorf("ValidationError"), }).Once() } - m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String("eksctl-test-addon--podidentityrole-karpenter"), }).Return(&manager.Stack{}, nil).Once() }, @@ -458,7 +449,7 @@ var _ = Describe("Update Pod Identity Association", func() { }, }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") - m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), }).Return(&manager.Stack{}, nil).Once() }, @@ -486,7 +477,7 @@ var _ = Describe("Update Pod Identity Association", func() { }, }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") - m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), }).Return(nil, &smithy.OperationError{ Err: errors.New("ValidationError"), diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 5dd5c8c8b4..aac4aed452 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -3,6 +3,7 @@ package addon import ( "context" "fmt" + "golang.org/x/exp/slices" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -19,6 +20,8 @@ import ( type PodIdentityIAMUpdater interface { // UpdateRole creates or updates IAM resources for podIdentityAssociations. UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string) ([]ekstypes.AddonPodIdentityAssociations, error) + // DeleteRole deletes the IAM resources for the specified addon. + DeleteRole(ctx context.Context, addonName, serviceAccountName string) (bool, error) } func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUpdater PodIdentityIAMUpdater, waitTimeout time.Duration) error { @@ -65,11 +68,20 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp updateAddonInput.AddonVersion = &version } + var deleteServiceAccountIAMResources []string if len(summary.PodIdentityAssociations) > 0 { if addon.PodIdentityAssociations == nil { return fmt.Errorf("addon %s has pod identity associations; to remove pod identity associations from an addon, "+ "addon.podIdentityAssociations must be explicitly set to []", addon.Name) } + for _, pia := range summary.PodIdentityAssociations { + if !slices.ContainsFunc(*addon.PodIdentityAssociations, func(addonPodIdentity api.PodIdentityAssociation) bool { + return pia.ServiceAccount == addonPodIdentity.ServiceAccountName + }) { + deleteServiceAccountIAMResources = append(deleteServiceAccountIAMResources, pia.ServiceAccount) + } + } + updateAddonInput.PodIdentityAssociations = []ekstypes.AddonPodIdentityAssociations{} } if addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0 { @@ -103,6 +115,16 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp if output != nil { logger.Debug("%+v", output.Update) } + for _, serviceAccount := range deleteServiceAccountIAMResources { + logger.Info("deleting IAM resources for pod identity service account %s", serviceAccount) + deleted, err := podIdentityIAMUpdater.DeleteRole(ctx, addon.Name, serviceAccount) + if err != nil { + return fmt.Errorf("deleting IAM resources for addon %s: %w", addon.Name, err) + } + if deleted { + logger.Info("deleted IAM resources for addon %s", addon.Name) + } + } if waitTimeout > 0 { return a.waitForAddonToBeActive(ctx, addon, waitTimeout) } diff --git a/pkg/actions/podidentityassociation/mocks/StackDeleter.go b/pkg/actions/podidentityassociation/mocks/StackDeleter.go new file mode 100644 index 0000000000..597ec6dc74 --- /dev/null +++ b/pkg/actions/podidentityassociation/mocks/StackDeleter.go @@ -0,0 +1,140 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + + v1alpha5 "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +// StackDeleter is an autogenerated mock type for the StackDeleter type +type StackDeleter struct { + mock.Mock +} + +// DeleteStackBySpecSync provides a mock function with given fields: ctx, stack, errCh +func (_m *StackDeleter) DeleteStackBySpecSync(ctx context.Context, stack *types.Stack, errCh chan error) error { + ret := _m.Called(ctx, stack, errCh) + + if len(ret) == 0 { + panic("no return value specified for DeleteStackBySpecSync") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Stack, chan error) error); ok { + r0 = rf(ctx, stack, errCh) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DescribeStack provides a mock function with given fields: ctx, stack +func (_m *StackDeleter) DescribeStack(ctx context.Context, stack *types.Stack) (*types.Stack, error) { + ret := _m.Called(ctx, stack) + + if len(ret) == 0 { + panic("no return value specified for DescribeStack") + } + + var r0 *types.Stack + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *types.Stack) (*types.Stack, error)); ok { + return rf(ctx, stack) + } + if rf, ok := ret.Get(0).(func(context.Context, *types.Stack) *types.Stack); ok { + r0 = rf(ctx, stack) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Stack) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *types.Stack) error); ok { + r1 = rf(ctx, stack) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetIAMServiceAccounts provides a mock function with given fields: ctx +func (_m *StackDeleter) GetIAMServiceAccounts(ctx context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetIAMServiceAccounts") + } + + var r0 []*v1alpha5.ClusterIAMServiceAccount + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []*v1alpha5.ClusterIAMServiceAccount); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v1alpha5.ClusterIAMServiceAccount) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListPodIdentityStackNames provides a mock function with given fields: ctx +func (_m *StackDeleter) ListPodIdentityStackNames(ctx context.Context) ([]string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for ListPodIdentityStackNames") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewStackDeleter creates a new instance of StackDeleter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewStackDeleter(t interface { + mock.TestingT + Cleanup(func()) +}) *StackDeleter { + mock := &StackDeleter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/actions/podidentityassociation/mocks/StackDescriber.go b/pkg/actions/podidentityassociation/mocks/StackDescriber.go deleted file mode 100644 index 7d538658fa..0000000000 --- a/pkg/actions/podidentityassociation/mocks/StackDescriber.go +++ /dev/null @@ -1,60 +0,0 @@ -// Code generated by mockery v2.38.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - types "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" -) - -// StackDescriber is an autogenerated mock type for the StackDescriber type -type StackDescriber struct { - mock.Mock -} - -// DescribeStack provides a mock function with given fields: _a0, _a1 -func (_m *StackDescriber) DescribeStack(_a0 context.Context, _a1 *types.Stack) (*types.Stack, error) { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for DescribeStack") - } - - var r0 *types.Stack - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *types.Stack) (*types.Stack, error)); ok { - return rf(_a0, _a1) - } - if rf, ok := ret.Get(0).(func(context.Context, *types.Stack) *types.Stack); ok { - r0 = rf(_a0, _a1) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Stack) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *types.Stack) error); ok { - r1 = rf(_a0, _a1) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// NewStackDescriber creates a new instance of StackDescriber. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewStackDescriber(t interface { - mock.TestingT - Cleanup(func()) -}) *StackDescriber { - mock := &StackDescriber{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index e7bae35691..eaaf2ddc37 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -101,7 +101,7 @@ func updateAddon(cmd *cmdutils.Cmd, force, wait bool) error { StackUpdater: stackManager, }, EKSPodIdentityDescriber: clusterProvider.AWSProvider.EKS(), - StackDescriber: stackManager, + StackDeleter: stackManager, } for _, a := range cmd.ClusterConfig.Addons { From 1b0c2ca603a4b55980d0accc74887f7d827ff4fb Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Thu, 9 May 2024 13:07:15 +0300 Subject: [PATCH 15/35] take into account that not all EKS addons will support pod IDs at launch --- pkg/actions/addon/addon.go | 4 - pkg/actions/addon/create.go | 208 +++++++++++++++++++++------ pkg/actions/addon/delete.go | 47 ++---- pkg/actions/addon/tasks.go | 20 +++ pkg/apis/eksctl.io/v1alpha5/addon.go | 5 +- 5 files changed, 199 insertions(+), 85 deletions(-) diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index d1eea8278b..0f921d7e1f 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -143,10 +143,6 @@ func (a *Manager) makeAddonIRSAName(name string) string { return fmt.Sprintf("eksctl-%s-addon-%s-IRSA", a.clusterConfig.Metadata.Name, name) } -func (a *Manager) makeAddonPodIdentityName(addonName, serviceAccountName string) string { - return fmt.Sprintf("eksctl-%s-addon-%s-podidentityrole-%s", a.clusterConfig.Metadata.Name, addonName, serviceAccountName) -} - func (a *Manager) makeAddonName(name string) string { return fmt.Sprintf("eksctl-%s-addon-%s", a.clusterConfig.Metadata.Name, name) } diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 95b11f4b53..967e944eba 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/kris-nova/logger" @@ -21,8 +22,76 @@ import ( "github.com/weaveworks/eksctl/pkg/cfn/builder" ) +// define IRSA helper functions +var ( + hasPoliciesSet = func(addon *api.Addon) bool { + return len(addon.AttachPolicyARNs) != 0 || addon.WellKnownPolicies.HasPolicy() || addon.AttachPolicy != nil + } + hasRecommendedIRSAPolicies = func(addon *api.Addon) bool { + switch addon.CanonicalName() { + case api.VPCCNIAddon, api.AWSEBSCSIDriverAddon, api.AWSEFSCSIDriverAddon: + return true + default: + return false + } + } + shouldUseIRSA = func(addon *api.Addon) bool { + if addon.ServiceAccountRoleARN != "" || hasPoliciesSet(addon) || hasRecommendedIRSAPolicies(addon) { + return true + } + return false + } +) + +// define IAM permissions related warnings +var ( + updateAddonRecommended = func(supportsPodIDs bool) string { + path := "`addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`" + if supportsPodIDs { + path = "`addon.PodIdentityAssociations`" + } + return fmt.Sprintf("add all recommended policies to the config file, under %s, and run `eksctl update addon`", path) + } + iamPermissionsRecommended = func(addonName string, supportsPodIDs, shouldUpdateAddon bool) string { + method := "IRSA" + if supportsPodIDs { + method = "pod identity associations" + } + commandSuggestion := "run `eksctl utils migrate-to-pod-identity`" + if shouldUpdateAddon { + commandSuggestion = updateAddonRecommended(supportsPodIDs) + } + return fmt.Sprintf("the recommended way to provide IAM permissions for %q addon is via %s; after addon creation is completed, %s", addonName, method, commandSuggestion) + } + IRSADeprecatedWarning = func(addonName string) string { + return fmt.Sprintf("IRSA has been deprecated; %s", iamPermissionsRecommended(addonName, true, false)) + } + OIDCDisabledWarning = func(addonName string, supportsPodIDs, isIRSASetExplicitly bool) string { + irsaUsedMessage := fmt.Sprintf("recommended policies were found for %q addon", addonName) + if isIRSASetExplicitly { + irsaUsedMessage = fmt.Sprintf("IRSA config is set for %q addon", addonName) + } + suggestion := "users are responsible for attaching the policies to all nodegroup roles" + if supportsPodIDs { + suggestion = iamPermissionsRecommended(addonName, true, true) + } + return fmt.Sprintf("%s, but since OIDC is disabled on the cluster, eksctl cannot configure the requested permissions; %s", irsaUsedMessage, suggestion) + } + IAMPermissionsRequiredWarning = func(addonName string, supportsPodIDs bool) string { + suggestion := iamPermissionsRecommended(addonName, false, true) + if supportsPodIDs { + suggestion = iamPermissionsRecommended(addonName, true, true) + } + return fmt.Sprintf("IAM permissions are required for %q addon; %s", addonName, suggestion) + } + IAMPermissionsNotRequiredWarning = func(addonName string) string { + return fmt.Sprintf("IAM permissions are not required for %q addon; any IRSA configuration or pod identity associations will be ignored", addonName) + } +) + const ( - kubeSystemNamespace = "kube-system" + kubeSystemNamespace = "kube-system" + awsNodeServiceAccount = "aws-node" ) func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator IAMRoleCreator, waitTimeout time.Duration) error { @@ -44,6 +113,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I } version, requiresIAMPermissions, err := a.getLatestMatchingVersion(ctx, addon) + addon.Version = version if err != nil { return fmt.Errorf("failed to fetch version %s for addon %s: %w", version, addon.Name, err) } @@ -75,21 +145,18 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I createAddonInput.Tags = addon.Tags } - getRecommendedPolicies := func(ctx context.Context, addon *api.Addon) ([]ekstypes.AddonPodIdentityConfiguration, error) { - output, err := a.eksAPI.DescribeAddonConfiguration(ctx, &eks.DescribeAddonConfigurationInput{ - AddonName: &addon.Name, - AddonVersion: &version, - }) - if err != nil { - return nil, fmt.Errorf("describing configuration for addon %s: %w", addon.Name, err) - } - return output.PodIdentityConfiguration, nil + podIDConfig, supportsPodIDs, err := a.getRecommendedPoliciesForPodID(ctx, addon) + if err != nil { + return err } if requiresIAMPermissions { switch { case addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0: - logger.Info("pod identity associations were specified for addon %s, will use those to provide required IAM permissions, other settings such as IRSA will be ignored", addon.Name) + if !supportsPodIDs { + return fmt.Errorf("%q addon does not support pod identity associations; use IRSA instead", addon.Name) + } + logger.Info("pod identity associations were specified for %q addon; will use those to provide required IAM permissions, any IRSA settings will be ignored", addon.Name) for _, pia := range *addon.PodIdentityAssociations { roleARN, err := iamRoleCreator.Create(ctx, &pia, addon.Name) if err != nil { @@ -101,31 +168,49 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I }) } - case a.clusterConfig.IAM.AutoCreatePodIdentityAssociations: - logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended policies for addon %s", addon.Name) - recommendedPoliciesBySA, err := getRecommendedPolicies(ctx, addon) - if err != nil { - return err - } - if len(recommendedPoliciesBySA) == 0 { - logger.Info("no recommended policies found for addon %s, proceeding without adding any IAM permissions", addon.Name) + case a.clusterConfig.IAM.AutoCreatePodIdentityAssociations && supportsPodIDs: + logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will use recommended policies for %q addon, any IRSA settings will be ignored", addon.Name) + + if addon.CanonicalName() == api.VPCCNIAddon && a.clusterConfig.IPv6Enabled() { + roleARN, err := iamRoleCreator.Create(ctx, &api.PodIdentityAssociation{ + ServiceAccountName: awsNodeServiceAccount, + PermissionPolicy: makeIPv6VPCCNIPolicyDocument(api.Partitions.ForRegion(a.clusterConfig.Metadata.Region)), + }, addon.Name) + if err != nil { + return err + } + createAddonInput.PodIdentityAssociations = append(createAddonInput.PodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ + RoleArn: &roleARN, + ServiceAccount: aws.String(awsNodeServiceAccount), + }) break } - for _, p := range recommendedPoliciesBySA { + + for _, config := range podIDConfig { roleARN, err := iamRoleCreator.Create(ctx, &api.PodIdentityAssociation{ - ServiceAccountName: *p.ServiceAccount, - PermissionPolicyARNs: p.RecommendedManagedPolicies, + ServiceAccountName: *config.ServiceAccount, + PermissionPolicyARNs: config.RecommendedManagedPolicies, }, addon.Name) if err != nil { return err } createAddonInput.PodIdentityAssociations = append(createAddonInput.PodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ RoleArn: &roleARN, - ServiceAccount: p.ServiceAccount, + ServiceAccount: config.ServiceAccount, }) } - case a.withOIDC: + case shouldUseIRSA(addon): + if !a.withOIDC { + logger.Warning(OIDCDisabledWarning(addon.Name, supportsPodIDs, + /* isIRSASetExplicitly */ addon.ServiceAccountRoleARN != "" || hasPoliciesSet(addon))) + break + } + + if supportsPodIDs { + logger.Warning(IRSADeprecatedWarning(addon.Name)) + } + if addon.ServiceAccountRoleARN != "" { logger.Info("using provided ServiceAccountRoleARN %q", addon.ServiceAccountRoleARN) createAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN @@ -133,22 +218,10 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I } if !hasPoliciesSet(addon) { - if addon.CanonicalName() == api.VPCCNIAddon && a.clusterConfig.IPv6Enabled() { - addon.AttachPolicy = makeIPv6VPCCNIPolicyDocument(api.Partitions.ForRegion(a.clusterConfig.Metadata.Region)) - } else { - recommendedPoliciesBySA, err := getRecommendedPolicies(ctx, addon) - if err != nil { - return err - } - if len(recommendedPoliciesBySA) == 0 { - logger.Info("no recommended policies found for addon %s, proceeding without adding any IAM permissions", addon.Name) - break - } - logger.Info("creating role using recommended policies for addon %s", addon.Name) - for _, p := range recommendedPoliciesBySA { - addon.AttachPolicyARNs = append(addon.AttachPolicyARNs, p.RecommendedManagedPolicies...) - } - } + a.setRecommendedPoliciesForIRSA(addon) + logger.Info("creating role using recommended policies for %q addon", addon.Name) + } else { + logger.Info("creating role using provided policies for %q addon", addon.Name) } namespace, serviceAccount := a.getKnownServiceAccountLocation(addon) @@ -159,8 +232,11 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I createAddonInput.ServiceAccountRoleArn = &roleARN default: - logger.Warning("addon %s requires IAM permissions; please set \"iam.AutoCreatePodIdentityAssociations\" to true or specify them manually via \"addon.PodIdentityAssociations\"") + logger.Warning(IAMPermissionsRequiredWarning(addon.Name, supportsPodIDs)) } + + } else if (addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0) || shouldUseIRSA(addon) { + logger.Warning(IAMPermissionsNotRequiredWarning(addon.Name)) } if addon.CanonicalName() == api.VPCCNIAddon { @@ -179,7 +255,24 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I logger.Info("creating addon") output, err := a.eksAPI.CreateAddon(ctx, createAddonInput) if err != nil { - // TODO: gracefully handle scenario where pod identity association already exists + var resourceInUse *ekstypes.ResourceInUseException + if errors.As(err, &resourceInUse) { + defer func() { + deleteAddonIAMTasks, err := NewRemover(a.stackManager).DeleteAddonIAMTasksFiltered(ctx, addon.Name, false) + if err != nil { + logger.Warning("failed to cleanup IAM role stacks: %w; please remove any remaining stacks manually", err) + return + } + if err := runAllTasks(deleteAddonIAMTasks); err != nil { + logger.Warning("failed to cleanup IAM role stacks: %w; please remove any remaining stacks manually", err) + } + }() + var addonServiceAccounts []string + for _, config := range podIDConfig { + addonServiceAccounts = append(addonServiceAccounts, fmt.Sprintf("%q", *config.ServiceAccount)) + } + return fmt.Errorf("one or more service accounts corresponding to %q addon is already associated with a different IAM role; please delete all pre-existing pod identity associations corresponding to %s service account(s) in the addon's namespace, then re-try creating the addon", addon.Name, strings.Join(addonServiceAccounts, ",")) + } return errors.Wrapf(err, "failed to create addon %q", addon.Name) } @@ -306,8 +399,35 @@ func (a *Manager) getKnownServiceAccountLocation(addon *api.Addon) (string, stri } } -func hasPoliciesSet(addon *api.Addon) bool { - return len(addon.AttachPolicyARNs) != 0 || addon.WellKnownPolicies.HasPolicy() || addon.AttachPolicy != nil +func (a *Manager) getRecommendedPoliciesForPodID(ctx context.Context, addon *api.Addon) ([]ekstypes.AddonPodIdentityConfiguration, bool, error) { + output, err := a.eksAPI.DescribeAddonConfiguration(ctx, &eks.DescribeAddonConfigurationInput{ + AddonName: &addon.Name, + AddonVersion: &addon.Version, + }) + if err != nil { + return nil, false, fmt.Errorf("describing configuration for addon %s: %w", addon.Name, err) + } + return output.PodIdentityConfiguration, len(output.PodIdentityConfiguration) != 0, nil +} + +func (a *Manager) setRecommendedPoliciesForIRSA(addon *api.Addon) { + switch addon.CanonicalName() { + case api.VPCCNIAddon: + if a.clusterConfig.IPv6Enabled() { + addon.AttachPolicy = makeIPv6VPCCNIPolicyDocument(api.Partitions.ForRegion(a.clusterConfig.Metadata.Region)) + } + addon.AttachPolicyARNs = append(addon.AttachPolicyARNs, fmt.Sprintf("arn:%s:iam::aws:policy/%s", api.Partitions.ForRegion(a.clusterConfig.Metadata.Region), api.IAMPolicyAmazonEKSCNIPolicy)) + case api.AWSEBSCSIDriverAddon: + addon.WellKnownPolicies = api.WellKnownPolicies{ + EBSCSIController: true, + } + case api.AWSEFSCSIDriverAddon: + addon.WellKnownPolicies = api.WellKnownPolicies{ + EFSCSIController: true, + } + default: + return + } } func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, namespace, serviceAccount string) (string, error) { diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index 318eb9eb1b..6d3cc6b640 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -30,28 +30,19 @@ func (a *Manager) Delete(ctx context.Context, addon *api.Addon) error { logger.Info("deleted addon: %s", addon.Name) } - deleteTask, err := NewRemover(a.stackManager).DeleteAddon(ctx, addon, false) + deleteAddonIAMTasks, err := NewRemover(a.stackManager).DeleteAddonIAMTasksFiltered(ctx, addon.Name, true) if err != nil { return err } - if deleteTask != nil { + if deleteAddonIAMTasks.Len() > 0 { logger.Info("deleting associated IAM stacks") - errCh := make(chan error) - if err := deleteTask.Do(errCh); err != nil { - return err - } - select { - case err := <-errCh: - return err - case <-ctx.Done(): - return fmt.Errorf("timed out waiting for deletion of addon %s: %w", addon.Name, ctx.Err()) - } - } - if addonExists { + runAllTasks(deleteAddonIAMTasks) + } else if addonExists { logger.Info("no associated IAM stacks found") } else { - return errors.New("could not find addon or associated IAM stack to delete") + return errors.New("could not find addon or associated IAM stacks to delete") } + return nil } @@ -84,12 +75,19 @@ func NewRemover(stackManager StackManager) *Remover { } func (ar *Remover) DeleteAddonIAMTasks(ctx context.Context, wait bool) (*tasks.TaskTree, error) { + return ar.DeleteAddonIAMTasksFiltered(ctx, "", wait) +} + +func (ar *Remover) DeleteAddonIAMTasksFiltered(ctx context.Context, addonName string, wait bool) (*tasks.TaskTree, error) { stacks, err := ar.stackManager.GetIAMAddonsStacks(ctx) if err != nil { return nil, fmt.Errorf("failed to fetch addons stacks: %v", err) } taskTree := &tasks.TaskTree{Parallel: true} for _, s := range stacks { + if addonName != "" && ar.stackManager.GetIAMAddonName(s) != addonName { + continue + } taskTree.Append(&deleteAddonIAMTask{ ctx: ctx, info: fmt.Sprintf("delete addon IAM %q", *s.StackName), @@ -100,22 +98,3 @@ func (ar *Remover) DeleteAddonIAMTasks(ctx context.Context, wait bool) (*tasks.T } return taskTree, nil } - -func (ar *Remover) DeleteAddon(ctx context.Context, addon *api.Addon, wait bool) (tasks.Task, error) { - stacks, err := ar.stackManager.GetIAMAddonsStacks(ctx) - if err != nil { - return nil, err - } - for _, stack := range stacks { - if ar.stackManager.GetIAMAddonName(stack) == addon.Name { - return &deleteAddonIAMTask{ - ctx: ctx, - info: fmt.Sprintf("delete addon IAM %q", *stack.StackName), - stack: stack, - stackManager: ar.stackManager, - wait: wait, - }, nil - } - } - return nil, nil -} diff --git a/pkg/actions/addon/tasks.go b/pkg/actions/addon/tasks.go index 333cef4a68..800b301a75 100644 --- a/pkg/actions/addon/tasks.go +++ b/pkg/actions/addon/tasks.go @@ -9,6 +9,7 @@ import ( "k8s.io/client-go/kubernetes" cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/eks" @@ -150,3 +151,22 @@ func (t *deleteAddonIAMTask) Do(errorCh chan error) error { } return nil } + +func runAllTasks(taskTree *tasks.TaskTree) error { + logger.Debug(taskTree.Describe()) + if errs := taskTree.DoAllSync(); len(errs) > 0 { + var allErrs []string + for _, err := range errs { + allErrs = append(allErrs, err.Error()) + } + return fmt.Errorf(strings.Join(allErrs, "\n")) + } + completedAction := func() string { + if taskTree.PlanMode { + return "skipped" + } + return "completed successfully" + } + logger.Debug("all tasks were %s", completedAction()) + return nil +} diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index 0d4f039b16..290c95a1a1 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -35,9 +35,8 @@ type Addon struct { // ResolveConflicts determines how to resolve field value conflicts for an EKS add-on // if a value was changed from default ResolveConflicts ekstypes.ResolveConflicts `json:"resolveConflicts,omitempty"` - // ResolvePodIdentityConflicts - ResolvePodIdentityConflicts ekstypes.ResolveConflicts `json:"resolvePodIdentityConflicts,omitempty"` - // PodIdentityAssociations + // PodIdentityAssociations holds a list of associations to be configured for the addon + // +optional PodIdentityAssociations *[]PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` // ConfigurationValues defines the set of configuration properties for add-ons. // For now, all properties will be specified as a JSON string From 7e32105e762013582515e48b1431b9eb55599ae3 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Thu, 9 May 2024 15:56:30 +0300 Subject: [PATCH 16/35] add validations --- pkg/actions/addon/create.go | 38 ++-------- pkg/actions/addon/delete.go | 2 +- pkg/actions/addon/update.go | 7 +- pkg/apis/eksctl.io/v1alpha5/addon.go | 78 +++++++++++++++++++- pkg/apis/eksctl.io/v1alpha5/iam.go | 6 +- pkg/ctl/cmdutils/pod_identity_association.go | 4 +- pkg/ctl/update/pod_identity_association.go | 2 +- 7 files changed, 91 insertions(+), 46 deletions(-) diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 967e944eba..97f7e1d6c5 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -22,28 +22,6 @@ import ( "github.com/weaveworks/eksctl/pkg/cfn/builder" ) -// define IRSA helper functions -var ( - hasPoliciesSet = func(addon *api.Addon) bool { - return len(addon.AttachPolicyARNs) != 0 || addon.WellKnownPolicies.HasPolicy() || addon.AttachPolicy != nil - } - hasRecommendedIRSAPolicies = func(addon *api.Addon) bool { - switch addon.CanonicalName() { - case api.VPCCNIAddon, api.AWSEBSCSIDriverAddon, api.AWSEFSCSIDriverAddon: - return true - default: - return false - } - } - shouldUseIRSA = func(addon *api.Addon) bool { - if addon.ServiceAccountRoleARN != "" || hasPoliciesSet(addon) || hasRecommendedIRSAPolicies(addon) { - return true - } - return false - } -) - -// define IAM permissions related warnings var ( updateAddonRecommended = func(supportsPodIDs bool) string { path := "`addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`" @@ -152,7 +130,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I if requiresIAMPermissions { switch { - case addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0: + case addon.HasPodIDsSet(): if !supportsPodIDs { return fmt.Errorf("%q addon does not support pod identity associations; use IRSA instead", addon.Name) } @@ -200,10 +178,10 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I }) } - case shouldUseIRSA(addon): + case addon.HasIRSASet(): if !a.withOIDC { logger.Warning(OIDCDisabledWarning(addon.Name, supportsPodIDs, - /* isIRSASetExplicitly */ addon.ServiceAccountRoleARN != "" || hasPoliciesSet(addon))) + /* isIRSASetExplicitly */ addon.ServiceAccountRoleARN != "" || addon.HasIRSAPoliciesSet())) break } @@ -217,7 +195,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I break } - if !hasPoliciesSet(addon) { + if !addon.HasIRSAPoliciesSet() { a.setRecommendedPoliciesForIRSA(addon) logger.Info("creating role using recommended policies for %q addon", addon.Name) } else { @@ -235,7 +213,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I logger.Warning(IAMPermissionsRequiredWarning(addon.Name, supportsPodIDs)) } - } else if (addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0) || shouldUseIRSA(addon) { + } else if addon.HasPodIDsSet() || addon.HasIRSASet() { logger.Warning(IAMPermissionsNotRequiredWarning(addon.Name)) } @@ -271,7 +249,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I for _, config := range podIDConfig { addonServiceAccounts = append(addonServiceAccounts, fmt.Sprintf("%q", *config.ServiceAccount)) } - return fmt.Errorf("one or more service accounts corresponding to %q addon is already associated with a different IAM role; please delete all pre-existing pod identity associations corresponding to %s service account(s) in the addon's namespace, then re-try creating the addon", addon.Name, strings.Join(addonServiceAccounts, ",")) + return fmt.Errorf("creating addon: one or more service accounts corresponding to %q addon is already associated with a different IAM role; please delete all pre-existing pod identity associations corresponding to %s service account(s) in the addon's namespace, then re-try creating the addon", addon.Name, strings.Join(addonServiceAccounts, ",")) } return errors.Wrapf(err, "failed to create addon %q", addon.Name) } @@ -431,7 +409,6 @@ func (a *Manager) setRecommendedPoliciesForIRSA(addon *api.Addon) { } func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, namespace, serviceAccount string) (string, error) { - logger.Warning("providing required IAM permissions via OIDC has been deprecated for addon %s; please use \"eksctl utils migrate-to-pod-identities\" after addon is created", addon.Name) resourceSet, err := a.createRoleResourceSet(addon, namespace, serviceAccount) if err != nil { return "", err @@ -446,13 +423,10 @@ func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, names func (a *Manager) createRoleResourceSet(addon *api.Addon, namespace, serviceAccount string) (*builder.IAMRoleResourceSet, error) { var resourceSet *builder.IAMRoleResourceSet if len(addon.AttachPolicyARNs) != 0 { - logger.Info("creating role using provided policies ARNs") resourceSet = builder.NewIAMRoleResourceSetWithAttachPolicyARNs(addon.Name, namespace, serviceAccount, addon.PermissionsBoundary, addon.AttachPolicyARNs, a.oidcManager) } else if addon.WellKnownPolicies.HasPolicy() { - logger.Info("creating role using provided well known policies") resourceSet = builder.NewIAMRoleResourceSetWithWellKnownPolicies(addon.Name, namespace, serviceAccount, addon.PermissionsBoundary, addon.WellKnownPolicies, a.oidcManager) } else { - logger.Info("creating role using provided policies") resourceSet = builder.NewIAMRoleResourceSetWithAttachPolicy(addon.Name, namespace, serviceAccount, addon.PermissionsBoundary, addon.AttachPolicy, a.oidcManager) } return resourceSet, resourceSet.AddAllResources() diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index 6d3cc6b640..8d1e8f3dcc 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -35,7 +35,7 @@ func (a *Manager) Delete(ctx context.Context, addon *api.Addon) error { return err } if deleteAddonIAMTasks.Len() > 0 { - logger.Info("deleting associated IAM stacks") + logger.Info("deleting associated IAM stack(s)") runAllTasks(deleteAddonIAMTasks) } else if addonExists { logger.Info("no associated IAM stacks found") diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index aac4aed452..3f95e7cc2e 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -3,9 +3,10 @@ package addon import ( "context" "fmt" - "golang.org/x/exp/slices" "time" + "golang.org/x/exp/slices" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" @@ -84,7 +85,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp updateAddonInput.PodIdentityAssociations = []ekstypes.AddonPodIdentityAssociations{} } - if addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0 { + if addon.HasPodIDsSet() { addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, *addon.PodIdentityAssociations, addon.Name) if err != nil { return fmt.Errorf("updating pod identity associations: %w", err) @@ -94,7 +95,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp // check if we have been provided a different set of policies/role if addon.ServiceAccountRoleARN != "" { updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN - } else if hasPoliciesSet(addon) { + } else if addon.HasIRSAPoliciesSet() { serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) if err != nil { return err diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index 290c95a1a1..631abee8ec 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -59,17 +59,65 @@ func (a Addon) CanonicalName() string { } func (a Addon) Validate() error { + invalidAddonConfigErr := func(errorMsg string) error { + return fmt.Errorf("invalid configuration for %q addon: %s", a.Name, errorMsg) + } + if a.Name == "" { - return fmt.Errorf("name is required") + return invalidAddonConfigErr("name is required") } if !json.Valid([]byte(a.ConfigurationValues)) { if err := a.convertConfigurationValuesToJSON(); err != nil { - return fmt.Errorf("configurationValues: \"%s\" is not valid, supported format(s) are: JSON and YAML", a.ConfigurationValues) + return invalidAddonConfigErr(fmt.Sprintf("configurationValues: %q is not valid, supported format(s) are: JSON and YAML", a.ConfigurationValues)) + } + } + + if a.HasIRSAPoliciesSet() { + if a.HasPodIDsSet() { + return invalidAddonConfigErr("cannot set IRSA config (`addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time") + } + if err := a.checkAtMostOnePolicyProviderIsSet(); err != nil { + return invalidAddonConfigErr(err.Error()) + } + } + + if a.HasPodIDsSet() { + if a.CanonicalName() == PodIdentityAgentAddon { + return invalidAddonConfigErr(fmt.Sprintf("cannot set pod identity associtations for %q addon", PodIdentityAgentAddon)) + } + + for i, pia := range *a.PodIdentityAssociations { + path := fmt.Sprintf("podIdentityAssociations[%d]", i) + if pia.ServiceAccountName == "" { + return invalidAddonConfigErr(fmt.Sprintf("%s.serviceAccountName must be set", path)) + } + + if pia.RoleARN == "" && + len(pia.PermissionPolicy) == 0 && + len(pia.PermissionPolicyARNs) == 0 && + !pia.WellKnownPolicies.HasPolicy() { + return invalidAddonConfigErr(fmt.Sprintf("at least one of the following must be specified: %[1]s.roleARN, %[1]s.permissionPolicy, %[1]s.permissionPolicyARNs, %[1]s.wellKnownPolicies", path)) + } + + if pia.RoleARN != "" { + makeIncompatibleFieldErr := func(fieldName string) error { + return invalidAddonConfigErr(fmt.Sprintf("%[1]s.%s cannot be specified when %[1]s.roleARN is set", path, fieldName)) + } + if len(pia.PermissionPolicy) > 0 { + return makeIncompatibleFieldErr("permissionPolicy") + } + if len(pia.PermissionPolicyARNs) > 0 { + return makeIncompatibleFieldErr("permissionPolicyARNs") + } + if pia.WellKnownPolicies.HasPolicy() { + return makeIncompatibleFieldErr("wellKnownPolicies") + } + } } } - return a.checkOnlyOnePolicyProviderIsSet() + return nil } func (a *Addon) convertConfigurationValuesToJSON() (err error) { @@ -84,7 +132,7 @@ func (a *Addon) convertConfigurationValuesToJSON() (err error) { return err } -func (a Addon) checkOnlyOnePolicyProviderIsSet() error { +func (a Addon) checkAtMostOnePolicyProviderIsSet() error { setPolicyProviders := 0 if a.AttachPolicy != nil { setPolicyProviders++ @@ -107,3 +155,25 @@ func (a Addon) checkOnlyOnePolicyProviderIsSet() error { } return nil } + +func (a Addon) HasIRSAPoliciesSet() bool { + return len(a.AttachPolicyARNs) != 0 || a.WellKnownPolicies.HasPolicy() || a.AttachPolicy != nil + +} + +func (a Addon) HasIRSASet() bool { + return a.ServiceAccountRoleARN != "" || a.HasIRSAPoliciesSet() || a.hasIRSARecommendedPolicies() +} + +func (a Addon) hasIRSARecommendedPolicies() bool { + switch a.CanonicalName() { + case VPCCNIAddon, AWSEBSCSIDriverAddon, AWSEFSCSIDriverAddon: + return true + default: + return false + } +} + +func (a Addon) HasPodIDsSet() bool { + return a.PodIdentityAssociations != nil && len(*a.PodIdentityAssociations) > 0 +} diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index 43987480c0..d30274dafe 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -11,7 +11,7 @@ import ( // Commonly-used constants const ( AnnotationEKSRoleARN = "eks.amazonaws.com/role-arn" - EKSServicePrincipal = "pods.eks.amazonaws.com" + EKSServicePrincipal = "beta.pods.eks.aws.internal" ) var EKSServicePrincipalTrustStatement = IAMStatement{ @@ -53,7 +53,9 @@ type ClusterIAM struct { // +optional ServiceAccounts []*ClusterIAMServiceAccount `json:"serviceAccounts,omitempty"` - // AutoCreatePodIdentityAssociations + // AutoCreatePodIdentityAssociations specifies whether or not to automatically create pod identity associations + // for supported addons that require IAM permissions + // +optional AutoCreatePodIdentityAssociations bool `json:"autoCreatePodIdentityAssociations,omitempty"` // pod identity associations to create in the cluster. diff --git a/pkg/ctl/cmdutils/pod_identity_association.go b/pkg/ctl/cmdutils/pod_identity_association.go index ced5958310..bf951713c0 100644 --- a/pkg/ctl/cmdutils/pod_identity_association.go +++ b/pkg/ctl/cmdutils/pod_identity_association.go @@ -108,8 +108,6 @@ func validatePodIdentityAssociation(l *commonClusterConfigLoader, options PodIde return nil } -// TODO: validate addon.podIdentityAssociations. -// TODO: Disallow setting IRSA and PIA fields simultaneously. func validatePodIdentityAssociationsForConfig(clusterConfig *api.ClusterConfig, isCreate bool) error { if clusterConfig.IAM == nil || len(clusterConfig.IAM.PodIdentityAssociations) == 0 { return errors.New("no iam.podIdentityAssociations specified in the config file") @@ -119,7 +117,7 @@ func validatePodIdentityAssociationsForConfig(clusterConfig *api.ClusterConfig, func validatePodIdentityAssociations(podIdentityAssociations []api.PodIdentityAssociation, isCreate bool) error { for i, pia := range podIdentityAssociations { - path := fmt.Sprintf("podIdentityAssociations[%d]", i) + path := fmt.Sprintf("iam.podIdentityAssociations[%d]", i) if pia.Namespace == "" { return fmt.Errorf("%s.namespace must be set", path) } diff --git a/pkg/ctl/update/pod_identity_association.go b/pkg/ctl/update/pod_identity_association.go index e93eaeed4d..e4aa08c39c 100644 --- a/pkg/ctl/update/pod_identity_association.go +++ b/pkg/ctl/update/pod_identity_association.go @@ -34,7 +34,7 @@ func updatePodIdentityAssociation(cmd *cmdutils.Cmd) { cmd.FlagSetGroup.InFlagSet("Pod Identity Association", func(fs *pflag.FlagSet) { fs.StringVar(&options.Namespace, "namespace", "", "Namespace of the pod identity association") fs.StringVar(&options.ServiceAccountName, "service-account-name", "", "Service account name of the pod identity association") - fs.StringVar(&options.RoleARN, "role-arn", "", "Service account name of the pod identity association") + fs.StringVar(&options.RoleARN, "role-arn", "", "ARN of the IAM role to be associated with the service account") }) From cca112c7671a691398a97b7a0c813c46541a4351 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Thu, 9 May 2024 21:32:20 +0530 Subject: [PATCH 17/35] Migrate EKS addons to pod identity using the Addons API --- .mockery.yaml | 7 +- pkg/actions/addon/addon.go | 3 +- pkg/actions/addon/delete.go | 3 +- pkg/actions/addon/fakes/fake_stack_manager.go | 74 ---- pkg/actions/addon/get.go | 15 +- .../addon/mocks/PodIdentityIAMUpdater.go | 28 ++ .../podidentityassociation/addon_migrator.go | 207 ++++++++++ .../addon_migrator_test.go | 384 ++++++++++++++++++ .../addon_role_mapper.go | 45 ++ .../podidentityassociation/migrator.go | 75 ++-- .../podidentityassociation/migrator_test.go | 3 + .../mocks/RoleMigrator.go | 71 ++++ pkg/actions/podidentityassociation/tasks.go | 214 +++++----- pkg/apis/eksctl.io/v1alpha5/access_entry.go | 17 + pkg/apis/eksctl.io/v1alpha5/validation.go | 14 + pkg/cfn/manager/addon.go | 8 + pkg/cfn/manager/api.go | 2 +- pkg/cfn/manager/fakes/fake_stack_manager.go | 74 ---- pkg/cfn/manager/iam.go | 7 +- pkg/cfn/manager/interface.go | 1 - pkg/connector/connector.go | 20 +- 21 files changed, 944 insertions(+), 328 deletions(-) create mode 100644 pkg/actions/podidentityassociation/addon_migrator.go create mode 100644 pkg/actions/podidentityassociation/addon_migrator_test.go create mode 100644 pkg/actions/podidentityassociation/addon_role_mapper.go create mode 100644 pkg/actions/podidentityassociation/mocks/RoleMigrator.go create mode 100644 pkg/cfn/manager/addon.go diff --git a/.mockery.yaml b/.mockery.yaml index c0f48180f7..6e3e2b22d4 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -68,8 +68,11 @@ packages: github.com/weaveworks/eksctl/pkg/actions/podidentityassociation: interfaces: - StackDescriber: + StackDeleter: + config: + dir: "{{.InterfaceDir}}/mocks" + outpkg: mocks + RoleMigrator: config: dir: "{{.InterfaceDir}}/mocks" outpkg: mocks - diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index 0f921d7e1f..9a3d582b45 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -31,7 +31,6 @@ type StackManager interface { DescribeStack(ctx context.Context, i *cfntypes.Stack) (*cfntypes.Stack, error) GetIAMAddonsStacks(ctx context.Context) ([]*cfntypes.Stack, error) UpdateStack(ctx context.Context, options manager.UpdateStackOptions) error - GetIAMAddonName(s *cfntypes.Stack) string } // CreateClientSet creates a Kubernetes ClientSet. @@ -144,7 +143,7 @@ func (a *Manager) makeAddonIRSAName(name string) string { } func (a *Manager) makeAddonName(name string) string { - return fmt.Sprintf("eksctl-%s-addon-%s", a.clusterConfig.Metadata.Name, name) + return manager.MakeAddonStackName(a.clusterConfig.Metadata.Name, name) } func (a *Manager) parseVersion(v string) (*version.Version, error) { diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index 8d1e8f3dcc..3324605c45 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -10,6 +10,7 @@ import ( "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/utils/tasks" ) @@ -85,7 +86,7 @@ func (ar *Remover) DeleteAddonIAMTasksFiltered(ctx context.Context, addonName st } taskTree := &tasks.TaskTree{Parallel: true} for _, s := range stacks { - if addonName != "" && ar.stackManager.GetIAMAddonName(s) != addonName { + if addonName != "" && manager.GetIAMAddonName(s) != addonName { continue } taskTree.Append(&deleteAddonIAMTask{ diff --git a/pkg/actions/addon/fakes/fake_stack_manager.go b/pkg/actions/addon/fakes/fake_stack_manager.go index 9b360538f3..10d8493d0f 100644 --- a/pkg/actions/addon/fakes/fake_stack_manager.go +++ b/pkg/actions/addon/fakes/fake_stack_manager.go @@ -69,17 +69,6 @@ type FakeStackManager struct { result1 *types.Stack result2 error } - GetIAMAddonNameStub func(*types.Stack) string - getIAMAddonNameMutex sync.RWMutex - getIAMAddonNameArgsForCall []struct { - arg1 *types.Stack - } - getIAMAddonNameReturns struct { - result1 string - } - getIAMAddonNameReturnsOnCall map[int]struct { - result1 string - } GetIAMAddonsStacksStub func(context.Context) ([]*types.Stack, error) getIAMAddonsStacksMutex sync.RWMutex getIAMAddonsStacksArgsForCall []struct { @@ -368,67 +357,6 @@ func (fake *FakeStackManager) DescribeStackReturnsOnCall(i int, result1 *types.S }{result1, result2} } -func (fake *FakeStackManager) GetIAMAddonName(arg1 *types.Stack) string { - fake.getIAMAddonNameMutex.Lock() - ret, specificReturn := fake.getIAMAddonNameReturnsOnCall[len(fake.getIAMAddonNameArgsForCall)] - fake.getIAMAddonNameArgsForCall = append(fake.getIAMAddonNameArgsForCall, struct { - arg1 *types.Stack - }{arg1}) - stub := fake.GetIAMAddonNameStub - fakeReturns := fake.getIAMAddonNameReturns - fake.recordInvocation("GetIAMAddonName", []interface{}{arg1}) - fake.getIAMAddonNameMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeStackManager) GetIAMAddonNameCallCount() int { - fake.getIAMAddonNameMutex.RLock() - defer fake.getIAMAddonNameMutex.RUnlock() - return len(fake.getIAMAddonNameArgsForCall) -} - -func (fake *FakeStackManager) GetIAMAddonNameCalls(stub func(*types.Stack) string) { - fake.getIAMAddonNameMutex.Lock() - defer fake.getIAMAddonNameMutex.Unlock() - fake.GetIAMAddonNameStub = stub -} - -func (fake *FakeStackManager) GetIAMAddonNameArgsForCall(i int) *types.Stack { - fake.getIAMAddonNameMutex.RLock() - defer fake.getIAMAddonNameMutex.RUnlock() - argsForCall := fake.getIAMAddonNameArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeStackManager) GetIAMAddonNameReturns(result1 string) { - fake.getIAMAddonNameMutex.Lock() - defer fake.getIAMAddonNameMutex.Unlock() - fake.GetIAMAddonNameStub = nil - fake.getIAMAddonNameReturns = struct { - result1 string - }{result1} -} - -func (fake *FakeStackManager) GetIAMAddonNameReturnsOnCall(i int, result1 string) { - fake.getIAMAddonNameMutex.Lock() - defer fake.getIAMAddonNameMutex.Unlock() - fake.GetIAMAddonNameStub = nil - if fake.getIAMAddonNameReturnsOnCall == nil { - fake.getIAMAddonNameReturnsOnCall = make(map[int]struct { - result1 string - }) - } - fake.getIAMAddonNameReturnsOnCall[i] = struct { - result1 string - }{result1} -} - func (fake *FakeStackManager) GetIAMAddonsStacks(arg1 context.Context) ([]*types.Stack, error) { fake.getIAMAddonsStacksMutex.Lock() ret, specificReturn := fake.getIAMAddonsStacksReturnsOnCall[len(fake.getIAMAddonsStacksArgsForCall)] @@ -566,8 +494,6 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.deleteStackBySpecSyncMutex.RUnlock() fake.describeStackMutex.RLock() defer fake.describeStackMutex.RUnlock() - fake.getIAMAddonNameMutex.RLock() - defer fake.getIAMAddonNameMutex.RUnlock() fake.getIAMAddonsStacksMutex.RLock() defer fake.getIAMAddonsStacksMutex.RUnlock() fake.updateStackMutex.RLock() diff --git a/pkg/actions/addon/get.go b/pkg/actions/addon/get.go index b7e6b7fea5..184f146212 100644 --- a/pkg/actions/addon/get.go +++ b/pkg/actions/addon/get.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/blang/semver" @@ -138,19 +137,15 @@ func (a *Manager) GetAll(ctx context.Context, includePodIdentityAssociations boo } func toPodIdentityAssociationIDs(podIdentityAssociationARNs []string) ([]string, error) { - var ret []string + var piaIDs []string for _, podIdentityAssociationARN := range podIdentityAssociationARNs { - parsed, err := arn.Parse(podIdentityAssociationARN) + piaID, err := api.ToPodIdentityAssociationID(podIdentityAssociationARN) if err != nil { - return nil, fmt.Errorf("parsing ARN %q: %w", podIdentityAssociationARN, err) - } - parts := strings.Split(parsed.Resource, "/") - if len(parts) != 3 { - return nil, fmt.Errorf("unexpected pod identity association ARN format: %q", parsed.String()) + return nil, err } - ret = append(ret, parts[len(parts)-1]) + piaIDs = append(piaIDs, piaID) } - return ret, nil + return piaIDs, nil } func (a *Manager) findNewerVersions(ctx context.Context, addon *api.Addon) (string, error) { diff --git a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go index 3f40f6b334..66171a149a 100644 --- a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go +++ b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go @@ -16,6 +16,34 @@ type PodIdentityIAMUpdater struct { mock.Mock } +// DeleteRole provides a mock function with given fields: ctx, addonName, serviceAccountName +func (_m *PodIdentityIAMUpdater) DeleteRole(ctx context.Context, addonName string, serviceAccountName string) (bool, error) { + ret := _m.Called(ctx, addonName, serviceAccountName) + + if len(ret) == 0 { + panic("no return value specified for DeleteRole") + } + + var r0 bool + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (bool, error)); ok { + return rf(ctx, addonName, serviceAccountName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) bool); ok { + r0 = rf(ctx, addonName, serviceAccountName) + } else { + r0 = ret.Get(0).(bool) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, addonName, serviceAccountName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // UpdateRole provides a mock function with given fields: ctx, podIdentityAssociations, addonName func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []v1alpha5.PodIdentityAssociation, addonName string) ([]types.AddonPodIdentityAssociations, error) { ret := _m.Called(ctx, podIdentityAssociations, addonName) diff --git a/pkg/actions/podidentityassociation/addon_migrator.go b/pkg/actions/podidentityassociation/addon_migrator.go new file mode 100644 index 0000000000..f5865effcc --- /dev/null +++ b/pkg/actions/podidentityassociation/addon_migrator.go @@ -0,0 +1,207 @@ +package podidentityassociation + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "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" + + "github.com/kris-nova/logger" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/utils/tasks" +) + +// AddonMigrator migrates EKS managed addons using IRSAv1 to EKS Pod Identity. +type AddonMigrator struct { + ClusterName string + AddonServiceAccountRoleMapper AddonServiceAccountRoleMapper + IAMRoleGetter IAMRoleGetter + StackDescriber StackDescriber + EKSAddonsAPI EKSAddonsAPI + RoleMigrator RoleMigrator +} + +type IAMRoleGetter interface { + GetRole(ctx context.Context, params *iam.GetRoleInput, optFns ...func(*iam.Options)) (*iam.GetRoleOutput, error) +} + +type EKSAddonsAPI interface { + ListAddons(ctx context.Context, params *eks.ListAddonsInput, optFns ...func(*eks.Options)) (*eks.ListAddonsOutput, error) + DescribeAddon(ctx context.Context, params *eks.DescribeAddonInput, optFns ...func(*eks.Options)) (*eks.DescribeAddonOutput, error) + DescribeAddonConfiguration(ctx context.Context, params *eks.DescribeAddonConfigurationInput, optFns ...func(*eks.Options)) (*eks.DescribeAddonConfigurationOutput, error) + UpdateAddon(ctx context.Context, params *eks.UpdateAddonInput, optFns ...func(*eks.Options)) (*eks.UpdateAddonOutput, error) +} + +// A RoleMigrator updates an IAM role to use EKS Pod Identity. +type RoleMigrator interface { + UpdateTrustPolicyForOwnedRoleTask(ctx context.Context, roleName, serviceAccountName string, stack IRSAv1StackSummary, removeOIDCProviderTrustRelationship bool) tasks.Task + UpdateTrustPolicyForUnownedRoleTask(ctx context.Context, roleName string, removeOIDCProviderTrustRelationship bool) tasks.Task +} + +// Migrate migrates all EKS addons to use EKS Pod Identity. +func (a *AddonMigrator) Migrate(ctx context.Context) (*tasks.TaskTree, error) { + allTasks := &tasks.TaskTree{ + Parallel: true, + } + for serviceAccountRoleARN, addon := range a.AddonServiceAccountRoleMapper { + taskTree, err := a.migrateAddon(ctx, addon, serviceAccountRoleARN) + if err != nil { + return nil, fmt.Errorf("migrating addon %s: %w", *addon.AddonName, err) + } + if taskTree != nil { + allTasks.Append(taskTree) + } + } + return allTasks, nil +} + +func (a *AddonMigrator) migrateAddon(ctx context.Context, addon *ekstypes.Addon, serviceAccountRoleARN string) (*tasks.TaskTree, error) { + if len(addon.PodIdentityAssociations) > 0 { + logger.Info("addon %s is already using pod identity; skipping migration to pod identity", *addon.AddonName) + return nil, nil + } + addonConfig, err := a.EKSAddonsAPI.DescribeAddonConfiguration(ctx, &eks.DescribeAddonConfigurationInput{ + AddonName: addon.AddonName, + AddonVersion: addon.AddonVersion, + }) + if err != nil { + return nil, fmt.Errorf("describing pod identity configuration for addon %s: %w", *addon.AddonName, err) + } + if len(addonConfig.PodIdentityConfiguration) == 0 { + logger.Info("addon %s does not support pod identity; skipping migration to pod identity", *addon.AddonName) + return nil, nil + } + + logger.Info("migrating addon %s with service account %s to pod identity; OIDC provider trust relationship will also be removed", *addon.AddonName, *addon.ServiceAccountRoleArn) + roleName, err := api.RoleNameFromARN(serviceAccountRoleARN) + if err != nil { + return nil, fmt.Errorf("parsing role ARN %s: %w", serviceAccountRoleARN, err) + } + serviceAccount, err := a.getRoleServiceAccount(ctx, roleName) + if err != nil { + return nil, fmt.Errorf("extracting service account from role %s: %w", *addon.ServiceAccountRoleArn, err) + } + if serviceAccount == "" { + if len(addonConfig.PodIdentityConfiguration) != 1 { + logger.Info("cannot choose a service account as addon %s supports pod identity for multiple service accounts; "+ + "skipping migration to pod identity", *addon.AddonName) + return nil, nil + } + serviceAccount = *addonConfig.PodIdentityConfiguration[0].ServiceAccount + logger.Info("could not find service account to use for addon %s from existing IAM role; defaulting to %s", *addon.AddonName, serviceAccount) + } + + var addonTask tasks.TaskTree + addonTask.IsSubTask = true + stack, err := a.StackDescriber.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(manager.MakeAddonStackName(a.ClusterName, *addon.AddonName)), + }) + if err != nil { + if manager.IsStackDoesNotExistError(err) { + return &tasks.TaskTree{ + IsSubTask: true, + Tasks: []tasks.Task{ + a.RoleMigrator.UpdateTrustPolicyForUnownedRoleTask(ctx, roleName, true), + a.updateAddonToUsePodIdentity(ctx, addon, serviceAccount), + }, + }, nil + } + return nil, err + } + + return &tasks.TaskTree{ + IsSubTask: true, + Tasks: []tasks.Task{ + a.RoleMigrator.UpdateTrustPolicyForOwnedRoleTask(ctx, roleName, serviceAccount, toStackSummary(stack), true), + a.updateAddonToUsePodIdentity(ctx, addon, serviceAccount), + }, + }, nil +} + +func (a *AddonMigrator) updateAddonToUsePodIdentity(ctx context.Context, addon *ekstypes.Addon, serviceAccount string) tasks.Task { + return &tasks.GenericTask{ + Description: fmt.Sprintf("migrate addon %s to pod identity", *addon.AddonName), + Doer: func() error { + logger.Info("creating a pod identity for addon %s with service account %s", *addon.AddonName, serviceAccount) + if _, err := a.EKSAddonsAPI.UpdateAddon(ctx, &eks.UpdateAddonInput{ + AddonName: addon.AddonName, + AddonVersion: addon.AddonVersion, + ClusterName: addon.ClusterName, + ConfigurationValues: addon.ConfigurationValues, + PodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + RoleArn: addon.ServiceAccountRoleArn, + ServiceAccount: aws.String(serviceAccount), + }, + }, + }); err != nil { + return fmt.Errorf("updating addon %s to use pod identity for service account %s: %w", *addon.AddonName, serviceAccount, err) + } + return nil + }, + } +} + +func (a *AddonMigrator) getRoleServiceAccount(ctx context.Context, roleName string) (string, error) { + role, err := a.IAMRoleGetter.GetRole(ctx, &iam.GetRoleInput{ + RoleName: aws.String(roleName), + }) + assumeRolePolicyDoc, err := url.PathUnescape(*role.Role.AssumeRolePolicyDocument) + + if err != nil { + return "", fmt.Errorf("unquoting assume role policy document: %w", err) + } + var policyDoc api.IAMPolicyDocument + if err := json.Unmarshal([]byte(assumeRolePolicyDoc), &policyDoc); err != nil { + return "", fmt.Errorf("parsing assume role policy document: %w", err) + } + for _, stmt := range policyDoc.Statements { + if len(stmt.Condition) == 0 { + logger.Info("no IAM statements for IRSA found; skipping migration to pod identity") + return "", nil + } + conditions := map[string]map[string]string{} + if err := json.Unmarshal(stmt.Condition, &conditions); err != nil { + return "", fmt.Errorf("unmarshaling IAM statement condition: %w", err) + } + strEquals, ok := conditions["StringEquals"] + if !ok { + continue + } + for k, v := range strEquals { + if strings.HasSuffix(k, ":sub") && strings.HasPrefix(v, "system:serviceaccount:") { + parts := strings.Split(v, ":") + if len(parts) != 4 { + return "", fmt.Errorf("unexpected format %q for service account subject", v) + } + return parts[len(parts)-1], nil + } + } + } + return "", nil +} + +func toStackSummary(stack *cfntypes.Stack) IRSAv1StackSummary { + tags := map[string]string{} + for _, tag := range stack.Tags { + tags[*tag.Key] = *tag.Value + } + var capabilities []string + for _, capability := range stack.Capabilities { + capabilities = append(capabilities, string(capability)) + } + return IRSAv1StackSummary{ + Name: *stack.StackName, + Tags: tags, + Capabilities: capabilities, + } +} diff --git a/pkg/actions/podidentityassociation/addon_migrator_test.go b/pkg/actions/podidentityassociation/addon_migrator_test.go new file mode 100644 index 0000000000..7bb1fbe7a3 --- /dev/null +++ b/pkg/actions/podidentityassociation/addon_migrator_test.go @@ -0,0 +1,384 @@ +package podidentityassociation_test + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/aws/aws-sdk-go-v2/service/iam" + iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + "github.com/aws/smithy-go" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/stretchr/testify/mock" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/mocks" + "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/eks/mocksv2" + "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" + "github.com/weaveworks/eksctl/pkg/utils/tasks" +) + +var _ = Describe("Addon Migration", func() { + type addonMocks struct { + eksAddonsAPI *mocksv2.EKS + iamRoleGetter *mocksv2.IAM + stackDescriber *mocks.StackDeleter + roleMigrator *mocks.RoleMigrator + } + type migrateEntry struct { + mockCalls func(m addonMocks) + + expectedTasks bool + expectedErr string + } + + const clusterName = "cluster" + + mockAddonCalls := func(eksAddonsAPI *mocksv2.EKS) { + eksAddonsAPI.On("ListAddons", mock.Anything, &eks.ListAddonsInput{ + ClusterName: aws.String(clusterName), + }).Return(&eks.ListAddonsOutput{ + Addons: []string{"vpc-cni"}, + }, nil) + eksAddonsAPI.On("DescribeAddon", mock.Anything, &eks.DescribeAddonInput{ + AddonName: aws.String("vpc-cni"), + ClusterName: aws.String(clusterName), + }).Return(&eks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + ServiceAccountRoleArn: aws.String("arn:aws:iam::000:role/role-1"), + ClusterName: aws.String(clusterName), + ConfigurationValues: aws.String("{}"), + }, + }, nil) + } + + mockGetRole := func(iamRoleGetter *mocksv2.IAM, includeServiceAccountSubject bool) { + strEquals := map[string]string{ + "oidc.eks.us-west-2.amazonaws.com/id/00:aud": "sts.amazonaws.com", + } + if includeServiceAccountSubject { + strEquals["oidc.eks.us-west-2.amazonaws.com/id/00:sub"] = "system:serviceaccount:kube-system:aws-node" + } + + val, err := json.Marshal(strEquals) + Expect(err).NotTo(HaveOccurred()) + iamRoleGetter.On("GetRole", mock.Anything, &iam.GetRoleInput{ + RoleName: aws.String("role-1"), + }).Return(&iam.GetRoleOutput{ + Role: &iamtypes.Role{ + AssumeRolePolicyDocument: aws.String(fmt.Sprintf(` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::00:oidc-provider/oidc.eks.eu-north-1.amazonaws.com/id/00" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": %s + } + } + ] +}`, val)), + }, + }, nil) + } + + DescribeTable("update pod identity associations", func(e migrateEntry) { + var ( + provider = mockprovider.NewMockProvider() + stackDescriber mocks.StackDeleter + roleMigrator mocks.RoleMigrator + ) + + addonMocks := addonMocks{ + eksAddonsAPI: provider.MockEKS(), + iamRoleGetter: provider.MockIAM(), + stackDescriber: &stackDescriber, + roleMigrator: &roleMigrator, + } + e.mockCalls(addonMocks) + addonMapper, err := podidentityassociation.CreateAddonServiceAccountRoleMapper(context.Background(), clusterName, provider.MockEKS()) + Expect(err).NotTo(HaveOccurred()) + addonMigrator := &podidentityassociation.AddonMigrator{ + ClusterName: clusterName, + AddonServiceAccountRoleMapper: addonMapper, + IAMRoleGetter: provider.MockIAM(), + StackDescriber: &stackDescriber, + EKSAddonsAPI: provider.MockEKS(), + RoleMigrator: &roleMigrator, + } + taskTree, err := addonMigrator.Migrate(context.Background()) + if e.expectedErr != "" { + Expect(err).To(MatchError(e.expectedErr)) + } else { + Expect(err).NotTo(HaveOccurred()) + } + if e.expectedTasks { + Expect(taskTree.Tasks).NotTo(BeEmpty(), "expected tasks to be non-empty") + } else { + Expect(taskTree.Tasks).To(BeEmpty(), "expected tasks to be empty") + } + for _, err := range taskTree.DoAllSync() { + Expect(err).NotTo(HaveOccurred()) + } + for _, asserter := range []interface { + AssertExpectations(t mock.TestingT) bool + }{ + addonMocks.eksAddonsAPI, + addonMocks.iamRoleGetter, + addonMocks.stackDescriber, + addonMocks.roleMigrator, + } { + asserter.AssertExpectations(GinkgoT()) + } + }, + Entry("migrating an addon with unowned IAM resources", migrateEntry{ + mockCalls: func(m addonMocks) { + mockAddonCalls(m.eksAddonsAPI) + m.eksAddonsAPI.On("DescribeAddonConfiguration", mock.Anything, &eks.DescribeAddonConfigurationInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + }).Return(&eks.DescribeAddonConfigurationOutput{ + PodIdentityConfiguration: []ekstypes.AddonPodIdentityConfiguration{ + { + ServiceAccount: aws.String("aws-node"), + }, + }, + }, nil) + mockGetRole(m.iamRoleGetter, true) + + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String(fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni")), + }).Return(nil, &smithy.OperationError{ + Err: errors.New("ValidationError"), + }) + m.roleMigrator.On("UpdateTrustPolicyForUnownedRoleTask", mock.Anything, "role-1", true).Return(&tasks.GenericTask{ + Description: `update trust policy for unowned role "role-1"`, + Doer: func() error { + return nil + }, + }) + + m.eksAddonsAPI.On("UpdateAddon", mock.Anything, &eks.UpdateAddonInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + ClusterName: aws.String(clusterName), + ConfigurationValues: aws.String("{}"), + PodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + RoleArn: aws.String("arn:aws:iam::000:role/role-1"), + ServiceAccount: aws.String("aws-node"), + }, + }, + }).Return(&eks.UpdateAddonOutput{}, nil) + }, + + expectedTasks: true, + }), + + Entry("migrating an addon with owned IAM resources", migrateEntry{ + mockCalls: func(m addonMocks) { + mockAddonCalls(m.eksAddonsAPI) + m.eksAddonsAPI.On("DescribeAddonConfiguration", mock.Anything, &eks.DescribeAddonConfigurationInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + }).Return(&eks.DescribeAddonConfigurationOutput{ + PodIdentityConfiguration: []ekstypes.AddonPodIdentityConfiguration{ + { + ServiceAccount: aws.String("aws-node"), + }, + }, + }, nil) + mockGetRole(m.iamRoleGetter, true) + + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String(fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni")), + }).Return(&manager.Stack{ + StackName: aws.String(fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni")), + Tags: []cfntypes.Tag{ + { + Key: aws.String("alpha.eksctl.io/addon-name"), + Value: aws.String("vpc-cni"), + }, + }, + Capabilities: []cfntypes.Capability{cfntypes.CapabilityCapabilityIam}, + }, nil) + m.roleMigrator.On("UpdateTrustPolicyForOwnedRoleTask", mock.Anything, "role-1", "aws-node", podidentityassociation.IRSAv1StackSummary{ + Name: fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni"), + Tags: map[string]string{ + "alpha.eksctl.io/addon-name": "vpc-cni", + }, + Capabilities: []string{string(cfntypes.CapabilityCapabilityIam)}, + }, true).Return(&tasks.GenericTask{ + Description: `update trust policy for owned role "role-1"`, + Doer: func() error { + return nil + }, + }) + + m.eksAddonsAPI.On("UpdateAddon", mock.Anything, &eks.UpdateAddonInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + ClusterName: aws.String(clusterName), + ConfigurationValues: aws.String("{}"), + PodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + RoleArn: aws.String("arn:aws:iam::000:role/role-1"), + ServiceAccount: aws.String("aws-node"), + }, + }, + }).Return(&eks.UpdateAddonOutput{}, nil) + }, + + expectedTasks: true, + }), + + Entry("addon that does not support pod identity is skipped", migrateEntry{ + mockCalls: func(m addonMocks) { + mockAddonCalls(m.eksAddonsAPI) + m.eksAddonsAPI.On("DescribeAddonConfiguration", mock.Anything, &eks.DescribeAddonConfigurationInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + }).Return(&eks.DescribeAddonConfigurationOutput{}, nil) + }, + }), + + Entry("addon without service account in IAM role policy uses service account from addon configuration", migrateEntry{ + mockCalls: func(m addonMocks) { + mockAddonCalls(m.eksAddonsAPI) + m.eksAddonsAPI.On("DescribeAddonConfiguration", mock.Anything, &eks.DescribeAddonConfigurationInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + }).Return(&eks.DescribeAddonConfigurationOutput{ + PodIdentityConfiguration: []ekstypes.AddonPodIdentityConfiguration{ + { + ServiceAccount: aws.String("aws-node"), + }, + }, + }, nil) + mockGetRole(m.iamRoleGetter, false) + + m.stackDescriber.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String(fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni")), + }).Return(&manager.Stack{ + StackName: aws.String(fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni")), + Tags: []cfntypes.Tag{ + { + Key: aws.String("alpha.eksctl.io/addon-name"), + Value: aws.String("vpc-cni"), + }, + }, + Capabilities: []cfntypes.Capability{cfntypes.CapabilityCapabilityIam}, + }, nil) + m.roleMigrator.On("UpdateTrustPolicyForOwnedRoleTask", mock.Anything, "role-1", "aws-node", podidentityassociation.IRSAv1StackSummary{ + Name: fmt.Sprintf("eksctl-%s-addon-%s", clusterName, "vpc-cni"), + Tags: map[string]string{ + "alpha.eksctl.io/addon-name": "vpc-cni", + }, + Capabilities: []string{string(cfntypes.CapabilityCapabilityIam)}, + }, true).Return(&tasks.GenericTask{ + Description: `update trust policy for owned role "role-1"`, + Doer: func() error { + return nil + }, + }) + + m.eksAddonsAPI.On("UpdateAddon", mock.Anything, &eks.UpdateAddonInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + ClusterName: aws.String(clusterName), + ConfigurationValues: aws.String("{}"), + PodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + RoleArn: aws.String("arn:aws:iam::000:role/role-1"), + ServiceAccount: aws.String("aws-node"), + }, + }, + }).Return(&eks.UpdateAddonOutput{}, nil) + }, + + expectedTasks: true, + }), + + Entry("addon without service account in IAM role policy and multiple service accounts in addon configuration is skipped", migrateEntry{ + mockCalls: func(m addonMocks) { + mockAddonCalls(m.eksAddonsAPI) + m.eksAddonsAPI.On("DescribeAddonConfiguration", mock.Anything, &eks.DescribeAddonConfigurationInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + }).Return(&eks.DescribeAddonConfigurationOutput{ + PodIdentityConfiguration: []ekstypes.AddonPodIdentityConfiguration{ + { + ServiceAccount: aws.String("aws-node"), + }, + { + ServiceAccount: aws.String("ipam"), + }, + }, + }, nil) + mockGetRole(m.iamRoleGetter, false) + }, + + expectedTasks: false, + }), + + Entry("addon with Statement.Condition missing in IAM role is skipped", migrateEntry{ + mockCalls: func(m addonMocks) { + mockAddonCalls(m.eksAddonsAPI) + m.eksAddonsAPI.On("DescribeAddonConfiguration", mock.Anything, &eks.DescribeAddonConfigurationInput{ + AddonName: aws.String("vpc-cni"), + AddonVersion: aws.String("v1"), + }).Return(&eks.DescribeAddonConfigurationOutput{ + PodIdentityConfiguration: []ekstypes.AddonPodIdentityConfiguration{ + { + ServiceAccount: aws.String("aws-node"), + }, + { + ServiceAccount: aws.String("ipam"), + }, + }, + }, nil) + + m.iamRoleGetter.On("GetRole", mock.Anything, &iam.GetRoleInput{ + RoleName: aws.String("role-1"), + }).Return(&iam.GetRoleOutput{ + Role: &iamtypes.Role{ + AssumeRolePolicyDocument: aws.String(` +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "beta.pods.eks.aws.internal" + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ] + } + ] +} +`), + }, + }, nil) + }, + + expectedTasks: false, + }), + ) +}) diff --git a/pkg/actions/podidentityassociation/addon_role_mapper.go b/pkg/actions/podidentityassociation/addon_role_mapper.go new file mode 100644 index 0000000000..fbaf30d14b --- /dev/null +++ b/pkg/actions/podidentityassociation/addon_role_mapper.go @@ -0,0 +1,45 @@ +package podidentityassociation + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" +) + +// AddonServiceAccountRoleMapper maps service account role ARNs to EKS addons. +type AddonServiceAccountRoleMapper map[string]*ekstypes.Addon + +// CreateAddonServiceAccountRoleMapper creates an AddonServiceAccountRoleMapper that maps service account role ARNs to EKS addons. +func CreateAddonServiceAccountRoleMapper(ctx context.Context, clusterName string, eksAddonsAPI EKSAddonsAPI) (AddonServiceAccountRoleMapper, error) { + addonMapper := AddonServiceAccountRoleMapper{} + paginator := eks.NewListAddonsPaginator(eksAddonsAPI, &eks.ListAddonsInput{ + ClusterName: aws.String(clusterName), + }) + for paginator.HasMorePages() { + output, err := paginator.NextPage(ctx) + if err != nil { + return nil, fmt.Errorf("listing addons: %w", err) + } + for _, addonName := range output.Addons { + addon, err := eksAddonsAPI.DescribeAddon(ctx, &eks.DescribeAddonInput{ + ClusterName: aws.String(clusterName), + AddonName: aws.String(addonName), + }) + if err != nil { + return nil, err + } + if roleARN := addon.Addon.ServiceAccountRoleArn; roleARN != nil { + addonMapper[*roleARN] = addon.Addon + } + } + } + return addonMapper, nil +} + +// AddonForServiceAccountRole returns the addon used by roleARN. +func (m AddonServiceAccountRoleMapper) AddonForServiceAccountRole(roleARN string) *ekstypes.Addon { + return m[roleARN] +} diff --git a/pkg/actions/podidentityassociation/migrator.go b/pkg/actions/podidentityassociation/migrator.go index 08e1142042..5d36f8838a 100644 --- a/pkg/actions/podidentityassociation/migrator.go +++ b/pkg/actions/podidentityassociation/migrator.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strings" "time" corev1 "k8s.io/api/core/v1" @@ -107,8 +106,21 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity IsSubTask: true, } toBeCreated := []api.PodIdentityAssociation{} + + addonServiceAccountRoleMapper, err := CreateAddonServiceAccountRoleMapper(ctx, m.clusterName, m.eksAPI) + if err != nil { + return fmt.Errorf("creating addon service account role mapper: %w", err) + } + policyUpdater := &trustPolicyUpdater{ + iamAPI: m.iamAPI, + stackUpdater: m.stackUpdater, + } for _, sa := range serviceAccounts.Items { if roleARN, ok := sa.Annotations[api.AnnotationEKSRoleARN]; ok { + if mappedAddon := addonServiceAccountRoleMapper.AddonForServiceAccountRole(roleARN); mappedAddon != nil { + logger.Info("found service account %s but it is associated with EKS addon %s", sa.Name, *mappedAddon.AddonName) + continue + } // collect pod identity associations that need to be created toBeCreated = append(toBeCreated, api.PodIdentityAssociation{ ServiceAccountName: sa.Name, @@ -117,31 +129,21 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity }) // infer role name to use in IAM API inputs - roleName, err := getNameFromARN(roleARN) + roleName, err := api.RoleNameFromARN(roleARN) if err != nil { return err } // add updateTrustPolicyTasks if stackSummary, hasStack := resolver.GetStack(roleARN); hasStack { - updateTrustPolicyTasks.Append(&updateTrustPolicyForOwnedRole{ - ctx: ctx, - info: fmt.Sprintf("update trust policy for owned role %q", roleName), - roleName: roleName, - stack: stackSummary, - removeOIDCProviderTrustRelationship: options.RemoveOIDCProviderTrustRelationship, - iamAPI: m.iamAPI, - stackUpdater: m.stackUpdater, - }) + updateTrustPolicyTasks.Append( + policyUpdater.UpdateTrustPolicyForOwnedRoleTask(ctx, roleName, "", stackSummary, options.RemoveOIDCProviderTrustRelationship), + ) } else { - updateTrustPolicyTasks.Append(&updateTrustPolicyForUnownedRole{ - ctx: ctx, - info: fmt.Sprintf("update trust policy for unowned role %q", roleName), - roleName: roleName, - removeOIDCProviderTrustRelationship: options.RemoveOIDCProviderTrustRelationship, - iamAPI: m.iamAPI, - }) + updateTrustPolicyTasks.Append( + policyUpdater.UpdateTrustPolicyForUnownedRoleTask(ctx, roleName, options.RemoveOIDCProviderTrustRelationship), + ) } // add removeIRSAv1AnnotationTasks @@ -167,11 +169,31 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity }) } } - if updateTrustPolicyTasks.Len() == 0 { - logger.Info("no iamserviceacconts found, there is no need to migrate to pod identity") + + addonMigrator := &AddonMigrator{ + ClusterName: m.clusterName, + AddonServiceAccountRoleMapper: addonServiceAccountRoleMapper, + IAMRoleGetter: m.iamAPI, + StackDescriber: m.stackUpdater, + EKSAddonsAPI: m.eksAPI, + RoleMigrator: policyUpdater, + } + addonMigrationTasks, err := addonMigrator.Migrate(ctx) + if err != nil { + return fmt.Errorf("error migrating addons to use pod identity: %w", err) + } + if addonMigrationTasks.Len() == 0 && updateTrustPolicyTasks.Len() == 0 { + logger.Info("no iamserviceaccounts or addons found to migrate to pod identity") return nil } - taskTree.Append(&updateTrustPolicyTasks) + + if updateTrustPolicyTasks.Len() > 0 { + taskTree.Append(&updateTrustPolicyTasks) + } + if addonMigrationTasks.Len() > 0 { + addonMigrationTasks.IsSubTask = true + taskTree.Append(addonMigrationTasks) + } if removeIRSAv1AnnotationTasks.Len() > 0 { taskTree.Append(&removeIRSAv1AnnotationTasks) } @@ -184,7 +206,8 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity } // add suggestive logs - cmdutils.LogIntendedAction(taskTree.PlanMode, "migrate %d iamserviceaccount(s) to pod identity association(s) by executing the following tasks", len(toBeCreated)) + cmdutils.LogIntendedAction(taskTree.PlanMode, "migrate %d iamserviceaccount(s) and %d addon(s) to pod identity by executing the following tasks", + len(toBeCreated), addonMigrationTasks.Len()) defer cmdutils.LogPlanModeWarning(taskTree.PlanMode) return runAllTasks(&taskTree) @@ -204,14 +227,6 @@ func IsPodIdentityAgentInstalled(ctx context.Context, eksAPI awsapi.EKS, cluster return true, nil } -func getNameFromARN(roleARN string) (string, error) { - parts := strings.Split(roleARN, "/") - if len(parts) != 2 { - return "", fmt.Errorf("cannot parse role name from roleARN: %s", roleARN) - } - return parts[1], nil -} - type IRSAv1StackNameResolver map[string]IRSAv1StackSummary type IRSAv1StackSummary struct { diff --git a/pkg/actions/podidentityassociation/migrator_test.go b/pkg/actions/podidentityassociation/migrator_test.go index 6e39d9425c..beeb26a261 100644 --- a/pkg/actions/podidentityassociation/migrator_test.go +++ b/pkg/actions/podidentityassociation/migrator_test.go @@ -113,6 +113,9 @@ var _ = Describe("Create", func() { } mockProvider = mockprovider.NewMockProvider() + mockProvider.MockEKS().On("ListAddons", mock.Anything, &awseks.ListAddonsInput{ + ClusterName: aws.String(clusterName), + }).Return(&awseks.ListAddonsOutput{}, nil) if e.mockEKS != nil { e.mockEKS(mockProvider) } diff --git a/pkg/actions/podidentityassociation/mocks/RoleMigrator.go b/pkg/actions/podidentityassociation/mocks/RoleMigrator.go new file mode 100644 index 0000000000..6c25da54d7 --- /dev/null +++ b/pkg/actions/podidentityassociation/mocks/RoleMigrator.go @@ -0,0 +1,71 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + podidentityassociation "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + + tasks "github.com/weaveworks/eksctl/pkg/utils/tasks" +) + +// RoleMigrator is an autogenerated mock type for the RoleMigrator type +type RoleMigrator struct { + mock.Mock +} + +// UpdateTrustPolicyForOwnedRoleTask provides a mock function with given fields: ctx, roleName, serviceAccountName, stack, removeOIDCProviderTrustRelationship +func (_m *RoleMigrator) UpdateTrustPolicyForOwnedRoleTask(ctx context.Context, roleName string, serviceAccountName string, stack podidentityassociation.IRSAv1StackSummary, removeOIDCProviderTrustRelationship bool) tasks.Task { + ret := _m.Called(ctx, roleName, serviceAccountName, stack, removeOIDCProviderTrustRelationship) + + if len(ret) == 0 { + panic("no return value specified for UpdateTrustPolicyForOwnedRoleTask") + } + + var r0 tasks.Task + if rf, ok := ret.Get(0).(func(context.Context, string, string, podidentityassociation.IRSAv1StackSummary, bool) tasks.Task); ok { + r0 = rf(ctx, roleName, serviceAccountName, stack, removeOIDCProviderTrustRelationship) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(tasks.Task) + } + } + + return r0 +} + +// UpdateTrustPolicyForUnownedRoleTask provides a mock function with given fields: ctx, roleName, removeOIDCProviderTrustRelationship +func (_m *RoleMigrator) UpdateTrustPolicyForUnownedRoleTask(ctx context.Context, roleName string, removeOIDCProviderTrustRelationship bool) tasks.Task { + ret := _m.Called(ctx, roleName, removeOIDCProviderTrustRelationship) + + if len(ret) == 0 { + panic("no return value specified for UpdateTrustPolicyForUnownedRoleTask") + } + + var r0 tasks.Task + if rf, ok := ret.Get(0).(func(context.Context, string, bool) tasks.Task); ok { + r0 = rf(ctx, roleName, removeOIDCProviderTrustRelationship) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(tasks.Task) + } + } + + return r0 +} + +// NewRoleMigrator creates a new instance of RoleMigrator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewRoleMigrator(t interface { + mock.TestingT + Cleanup(func()) +}) *RoleMigrator { + mock := &RoleMigrator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index d003358af1..93be4e85cb 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -3,6 +3,7 @@ package podidentityassociation import ( "context" "encoding/json" + "errors" "fmt" "net/url" "strings" @@ -52,126 +53,115 @@ func (t *createPodIdentityAssociationTask) Do(errorCh chan error) error { return nil } -type updateTrustPolicyForOwnedRole struct { - ctx context.Context - info string - roleName string - stack IRSAv1StackSummary - removeOIDCProviderTrustRelationship bool - iamAPI awsapi.IAM - stackUpdater StackUpdater +type trustPolicyUpdater struct { + iamAPI awsapi.IAM + stackUpdater StackUpdater } -func (t *updateTrustPolicyForOwnedRole) Describe() string { - return t.info -} - -func (t *updateTrustPolicyForOwnedRole) Do(errorCh chan error) error { - defer close(errorCh) - - trustStatements, err := updateTrustStatements(t.removeOIDCProviderTrustRelationship, func() (*awsiam.GetRoleOutput, error) { - return t.iamAPI.GetRole(t.ctx, &awsiam.GetRoleInput{RoleName: &t.roleName}) - }) - if err != nil { - return fmt.Errorf("updating trust statements for role %s: %w", t.roleName, err) - } - - // build template for updating trust policy - rs := builder.NewIAMRoleResourceSetForPodIdentityWithTrustStatements(&api.PodIdentityAssociation{}, trustStatements) - if err := rs.AddAllResources(); err != nil { - return fmt.Errorf("adding resources to CloudFormation template: %w", err) - } - template, err := rs.RenderJSON() - if err != nil { - return fmt.Errorf("generating CloudFormation template: %w", err) - } +func (t *trustPolicyUpdater) UpdateTrustPolicyForOwnedRoleTask(ctx context.Context, roleName, serviceAccountName string, stack IRSAv1StackSummary, removeOIDCProviderTrustRelationship bool) tasks.Task { + return &tasks.GenericTask{ + Description: fmt.Sprintf("update trust policy for owned role %q", roleName), + Doer: func() error { + trustStatements, err := updateTrustStatements(removeOIDCProviderTrustRelationship, func() (*awsiam.GetRoleOutput, error) { + return t.iamAPI.GetRole(ctx, &awsiam.GetRoleInput{RoleName: &roleName}) + }) + if err != nil { + return fmt.Errorf("updating trust statements for role %s: %w", roleName, err) + } + + // build template for updating trust policy + rs := builder.NewIAMRoleResourceSetForPodIdentityWithTrustStatements(&api.PodIdentityAssociation{}, trustStatements) + if err := rs.AddAllResources(); err != nil { + return fmt.Errorf("adding resources to CloudFormation template: %w", err) + } + template, err := rs.RenderJSON() + if err != nil { + return fmt.Errorf("generating CloudFormation template: %w", err) + } + + // update stack tags to reflect migration to IRSAv2 + cfnTags := []cfntypes.Tag{} + for key, value := range stack.Tags { + if key == api.IAMServiceAccountNameTag && removeOIDCProviderTrustRelationship { + continue + } + cfnTags = append(cfnTags, cfntypes.Tag{ + Key: &key, + Value: &value, + }) + } + + getIAMServiceAccountName := func() string { + if serviceAccountName != "" { + return serviceAccountName + } + return strings.Replace(strings.Split(stack.Name, "-iamserviceaccount-")[1], "-", "/", 1) + } + cfnTags = append(cfnTags, cfntypes.Tag{ + Key: aws.String(api.PodIdentityAssociationNameTag), + Value: aws.String(getIAMServiceAccountName()), + }) + + // propagate capabilities + cfnCapabilities := []cfntypes.Capability{} + for _, c := range stack.Capabilities { + cfnCapabilities = append(cfnCapabilities, cfntypes.Capability(c)) + } + + if err := t.stackUpdater.MustUpdateStack(ctx, manager.UpdateStackOptions{ + Stack: &cfntypes.Stack{ + StackName: &stack.Name, + Tags: cfnTags, + Capabilities: cfnCapabilities, + }, + ChangeSetName: fmt.Sprintf("eksctl-%s-update-%d", roleName, time.Now().Unix()), + Description: fmt.Sprintf("updating IAM resources stack %q for role %q", stack.Name, roleName), + TemplateData: manager.TemplateBody(template), + Wait: true, + }); err != nil { + var noChangeErr *manager.NoChangeError + if errors.As(err, &noChangeErr) { + logger.Info("IAM resources for role %q are already up-to-date", roleName) + return nil + } + return fmt.Errorf("updating IAM resources for role %q: %w", roleName, err) + } + logger.Info("updated IAM resources stack %q for role %q", stack.Name, roleName) - // update stack tags to reflect migration to IRSAv2 - cfnTags := []cfntypes.Tag{} - for key, value := range t.stack.Tags { - if key == api.IAMServiceAccountNameTag && t.removeOIDCProviderTrustRelationship { - continue - } - cfnTags = append(cfnTags, cfntypes.Tag{ - Key: &key, - Value: &value, - }) - } - getIAMServiceAccountName := func() string { - return strings.Replace(strings.Split(t.stack.Name, "-iamserviceaccount-")[1], "-", "/", 1) - } - cfnTags = append(cfnTags, cfntypes.Tag{ - Key: aws.String(api.PodIdentityAssociationNameTag), - Value: aws.String(getIAMServiceAccountName()), - }) - - // propagate capabilities - cfnCapabilities := []cfntypes.Capability{} - for _, c := range t.stack.Capabilities { - cfnCapabilities = append(cfnCapabilities, cfntypes.Capability(c)) - } - - if err := t.stackUpdater.MustUpdateStack(t.ctx, manager.UpdateStackOptions{ - Stack: &cfntypes.Stack{ - StackName: &t.stack.Name, - Tags: cfnTags, - Capabilities: cfnCapabilities, - }, - ChangeSetName: fmt.Sprintf("eksctl-%s-update-%d", t.roleName, time.Now().Unix()), - Description: fmt.Sprintf("updating IAM resources stack %q for role %q", t.stack.Name, t.roleName), - TemplateData: manager.TemplateBody(template), - Wait: true, - }); err != nil { - if _, ok := err.(*manager.NoChangeError); ok { - logger.Info("IAM resources for role %q are already up-to-date", t.roleName) return nil - } - return fmt.Errorf("updating IAM resources for role %q: %w", t.roleName, err) + }, } - logger.Info("updated IAM resources stack %q for role %q", t.stack.Name, t.roleName) - - return nil -} - -type updateTrustPolicyForUnownedRole struct { - ctx context.Context - info string - roleName string - iamAPI awsapi.IAM - removeOIDCProviderTrustRelationship bool -} - -func (t *updateTrustPolicyForUnownedRole) Describe() string { - return t.info } -func (t *updateTrustPolicyForUnownedRole) Do(errorCh chan error) error { - defer close(errorCh) - - trustStatements, err := updateTrustStatements(t.removeOIDCProviderTrustRelationship, func() (*awsiam.GetRoleOutput, error) { - return t.iamAPI.GetRole(t.ctx, &awsiam.GetRoleInput{RoleName: &t.roleName}) - }) - if err != nil { - return fmt.Errorf("updating trust statements for role %s: %w", t.roleName, err) - } - - documentString, err := json.Marshal(api.IAMPolicyDocument{ - Version: "2012-10-17", - Statements: trustStatements, - }) - if err != nil { - return fmt.Errorf("marshalling trust policy document: %w", err) - } - - if _, err := t.iamAPI.UpdateAssumeRolePolicy(t.ctx, &awsiam.UpdateAssumeRolePolicyInput{ - RoleName: &t.roleName, - PolicyDocument: aws.String(string(documentString)), - }); err != nil { - return fmt.Errorf("updating trust policy for role %s: %w", t.roleName, err) +func (t *trustPolicyUpdater) UpdateTrustPolicyForUnownedRoleTask(ctx context.Context, roleName string, removeOIDCProviderTrustRelationship bool) tasks.Task { + return &tasks.GenericTask{ + Description: fmt.Sprintf("update trust policy for unowned role %q", roleName), + Doer: func() error { + trustStatements, err := updateTrustStatements(removeOIDCProviderTrustRelationship, func() (*awsiam.GetRoleOutput, error) { + return t.iamAPI.GetRole(ctx, &awsiam.GetRoleInput{RoleName: &roleName}) + }) + if err != nil { + return fmt.Errorf("updating trust statements for role %s: %w", roleName, err) + } + + documentString, err := json.Marshal(api.IAMPolicyDocument{ + Version: "2012-10-17", + Statements: trustStatements, + }) + if err != nil { + return fmt.Errorf("marshalling trust policy document: %w", err) + } + + if _, err := t.iamAPI.UpdateAssumeRolePolicy(ctx, &awsiam.UpdateAssumeRolePolicyInput{ + RoleName: &roleName, + PolicyDocument: aws.String(string(documentString)), + }); err != nil { + return fmt.Errorf("updating trust policy for role %s: %w", roleName, err) + } + logger.Info(fmt.Sprintf("updated trust policy for role %s", roleName)) + return nil + }, } - logger.Info(fmt.Sprintf("updated trust policy for role %s", t.roleName)) - - return nil } func updateTrustStatements( diff --git a/pkg/apis/eksctl.io/v1alpha5/access_entry.go b/pkg/apis/eksctl.io/v1alpha5/access_entry.go index c837f39feb..e770e90956 100644 --- a/pkg/apis/eksctl.io/v1alpha5/access_entry.go +++ b/pkg/apis/eksctl.io/v1alpha5/access_entry.go @@ -2,6 +2,7 @@ package v1alpha5 import ( "encoding/json" + "errors" "fmt" "strings" @@ -116,6 +117,22 @@ func MustParseARN(a string) ARN { return ARN(parsed) } +// RoleNameFromARN returns the role name for roleARN. +func RoleNameFromARN(roleARN string) (string, error) { + parsed, err := arn.Parse(roleARN) + if err != nil { + return "", err + } + parts := strings.Split(parsed.Resource, "/") + if len(parts) != 2 { + return "", errors.New("invalid format for role ARN") + } + if parts[0] != "role" { + return "", fmt.Errorf("expected resource type to be %q; got %q", "role", parts[0]) + } + return parts[1], nil +} + // validateAccessEntries validates accessEntries. func validateAccessEntries(accessEntries []AccessEntry) error { seen := make(map[ARN]struct{}) diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index 5a71fc2dcf..3810b12493 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -1656,6 +1656,20 @@ func ValidateSecretsEncryption(clusterConfig *ClusterConfig) error { return nil } +// ToPodIdentityAssociationID extracts the pod identity association ID from piaARN. +// The ARN is of the format: arn:aws:eks:us-west-2:000:podidentityassociation/cluster/a-d3dw7wfvxtoatujeg. +func ToPodIdentityAssociationID(piaARN string) (string, error) { + parsed, err := arn.Parse(piaARN) + if err != nil { + return "", fmt.Errorf("parsing ARN %q: %w", piaARN, err) + } + parts := strings.Split(parsed.Resource, "/") + if len(parts) != 3 { + return "", fmt.Errorf("unexpected pod identity association ARN format: %q", parsed.String()) + } + return parts[len(parts)-1], nil +} + func validateIAMIdentityMappings(clusterConfig *ClusterConfig) error { for _, mapping := range clusterConfig.IAMIdentityMappings { if err := mapping.Validate(); err != nil { diff --git a/pkg/cfn/manager/addon.go b/pkg/cfn/manager/addon.go new file mode 100644 index 0000000000..4a307878da --- /dev/null +++ b/pkg/cfn/manager/addon.go @@ -0,0 +1,8 @@ +package manager + +import "fmt" + +// MakeAddonStackName creates a stack name for clusterName and addonName. +func MakeAddonStackName(clusterName, addonName string) string { + return fmt.Sprintf("eksctl-%s-addon-%s", clusterName, addonName) +} diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index 6cab4fa743..f0026ba0a5 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -510,7 +510,7 @@ func (c *StackCollection) ListClusterStackNames(ctx context.Context) ([]string, return c.ListStackNames(ctx, clusterStackRegex) } -// ListAccessEntryStackNames lists the stack names for all access entries in the specified cluster. +// ListAddonIAMStackNames lists the stack names for all access entries in the specified cluster. func (c *StackCollection) ListAddonIAMStackNames(ctx context.Context, clusterName, addonName string) ([]string, error) { return c.ListStackNames(ctx, fmt.Sprintf("^eksctl-%s-addon-%s-*", clusterName, addonName)) } diff --git a/pkg/cfn/manager/fakes/fake_stack_manager.go b/pkg/cfn/manager/fakes/fake_stack_manager.go index 2f03f615c6..7cff173d40 100644 --- a/pkg/cfn/manager/fakes/fake_stack_manager.go +++ b/pkg/cfn/manager/fakes/fake_stack_manager.go @@ -331,17 +331,6 @@ type FakeStackManager struct { result1 *types.Stack result2 error } - GetIAMAddonNameStub func(*types.Stack) string - getIAMAddonNameMutex sync.RWMutex - getIAMAddonNameArgsForCall []struct { - arg1 *types.Stack - } - getIAMAddonNameReturns struct { - result1 string - } - getIAMAddonNameReturnsOnCall map[int]struct { - result1 string - } GetIAMAddonsStacksStub func(context.Context) ([]*types.Stack, error) getIAMAddonsStacksMutex sync.RWMutex getIAMAddonsStacksArgsForCall []struct { @@ -2341,67 +2330,6 @@ func (fake *FakeStackManager) GetFargateStackReturnsOnCall(i int, result1 *types }{result1, result2} } -func (fake *FakeStackManager) GetIAMAddonName(arg1 *types.Stack) string { - fake.getIAMAddonNameMutex.Lock() - ret, specificReturn := fake.getIAMAddonNameReturnsOnCall[len(fake.getIAMAddonNameArgsForCall)] - fake.getIAMAddonNameArgsForCall = append(fake.getIAMAddonNameArgsForCall, struct { - arg1 *types.Stack - }{arg1}) - stub := fake.GetIAMAddonNameStub - fakeReturns := fake.getIAMAddonNameReturns - fake.recordInvocation("GetIAMAddonName", []interface{}{arg1}) - fake.getIAMAddonNameMutex.Unlock() - if stub != nil { - return stub(arg1) - } - if specificReturn { - return ret.result1 - } - return fakeReturns.result1 -} - -func (fake *FakeStackManager) GetIAMAddonNameCallCount() int { - fake.getIAMAddonNameMutex.RLock() - defer fake.getIAMAddonNameMutex.RUnlock() - return len(fake.getIAMAddonNameArgsForCall) -} - -func (fake *FakeStackManager) GetIAMAddonNameCalls(stub func(*types.Stack) string) { - fake.getIAMAddonNameMutex.Lock() - defer fake.getIAMAddonNameMutex.Unlock() - fake.GetIAMAddonNameStub = stub -} - -func (fake *FakeStackManager) GetIAMAddonNameArgsForCall(i int) *types.Stack { - fake.getIAMAddonNameMutex.RLock() - defer fake.getIAMAddonNameMutex.RUnlock() - argsForCall := fake.getIAMAddonNameArgsForCall[i] - return argsForCall.arg1 -} - -func (fake *FakeStackManager) GetIAMAddonNameReturns(result1 string) { - fake.getIAMAddonNameMutex.Lock() - defer fake.getIAMAddonNameMutex.Unlock() - fake.GetIAMAddonNameStub = nil - fake.getIAMAddonNameReturns = struct { - result1 string - }{result1} -} - -func (fake *FakeStackManager) GetIAMAddonNameReturnsOnCall(i int, result1 string) { - fake.getIAMAddonNameMutex.Lock() - defer fake.getIAMAddonNameMutex.Unlock() - fake.GetIAMAddonNameStub = nil - if fake.getIAMAddonNameReturnsOnCall == nil { - fake.getIAMAddonNameReturnsOnCall = make(map[int]struct { - result1 string - }) - } - fake.getIAMAddonNameReturnsOnCall[i] = struct { - result1 string - }{result1} -} - func (fake *FakeStackManager) GetIAMAddonsStacks(arg1 context.Context) ([]*types.Stack, error) { fake.getIAMAddonsStacksMutex.Lock() ret, specificReturn := fake.getIAMAddonsStacksReturnsOnCall[len(fake.getIAMAddonsStacksArgsForCall)] @@ -4913,8 +4841,6 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.getClusterStackIfExistsMutex.RUnlock() fake.getFargateStackMutex.RLock() defer fake.getFargateStackMutex.RUnlock() - fake.getIAMAddonNameMutex.RLock() - defer fake.getIAMAddonNameMutex.RUnlock() fake.getIAMAddonsStacksMutex.RLock() defer fake.getIAMAddonsStacksMutex.RUnlock() fake.getIAMServiceAccountsMutex.RLock() diff --git a/pkg/cfn/manager/iam.go b/pkg/cfn/manager/iam.go index 7758964f1f..b26579430e 100644 --- a/pkg/cfn/manager/iam.go +++ b/pkg/cfn/manager/iam.go @@ -144,15 +144,16 @@ func (c *StackCollection) GetIAMAddonsStacks(ctx context.Context) ([]*Stack, err if s.StackStatus == types.StackStatusDeleteComplete { continue } - if c.GetIAMAddonName(s) != "" { + if GetIAMAddonName(s) != "" { iamAddonStacks = append(iamAddonStacks, s) } } return iamAddonStacks, nil } -func (*StackCollection) GetIAMAddonName(s *Stack) string { - for _, tag := range s.Tags { +// GetIAMAddonName returns the addon name for stack. +func GetIAMAddonName(stack *types.Stack) string { + for _, tag := range stack.Tags { if *tag.Key == api.AddonNameTag { return *tag.Value } diff --git a/pkg/cfn/manager/interface.go b/pkg/cfn/manager/interface.go index b87d0adbc7..64346463d1 100644 --- a/pkg/cfn/manager/interface.go +++ b/pkg/cfn/manager/interface.go @@ -62,7 +62,6 @@ type StackManager interface { GetAutoScalingGroupName(ctx context.Context, s *Stack) (string, error) GetClusterStackIfExists(ctx context.Context) (*Stack, error) GetFargateStack(ctx context.Context) (*Stack, error) - GetIAMAddonName(s *Stack) string GetIAMAddonsStacks(ctx context.Context) ([]*Stack, error) GetIAMServiceAccounts(ctx context.Context) ([]*api.ClusterIAMServiceAccount, error) GetKarpenterStack(ctx context.Context) (*Stack, error) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 4081c77d3a..596a5b1bf8 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -9,7 +9,6 @@ import ( "time" "github.com/aws/aws-sdk-go-v2/aws" - awsarn "github.com/aws/aws-sdk-go-v2/aws/arn" "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" @@ -226,7 +225,7 @@ func (c *EKSConnector) DeregisterCluster(ctx context.Context, clusterName string return errors.Wrap(err, "unexpected error deregistering cluster") } - roleName, err := roleNameFromARN(*clusterOutput.Cluster.ConnectorConfig.RoleArn) + roleName, err := api.RoleNameFromARN(*clusterOutput.Cluster.ConnectorConfig.RoleArn) if err != nil { return errors.Wrapf(err, "error parsing role ARN %q", *clusterOutput.Cluster.ConnectorConfig.RoleArn) } @@ -266,28 +265,13 @@ func (c *EKSConnector) deleteRole(ctx context.Context, roleName string) error { } func (c *EKSConnector) deleteRoleByARN(ctx context.Context, roleARN string) error { - connectorRoleName, err := roleNameFromARN(roleARN) + connectorRoleName, err := api.RoleNameFromARN(roleARN) if err != nil { return errors.Wrap(err, "error parsing connector role ARN") } return c.deleteRole(ctx, connectorRoleName) } -func roleNameFromARN(roleARN string) (string, error) { - parsed, err := awsarn.Parse(roleARN) - if err != nil { - return "", err - } - parts := strings.Split(parsed.Resource, "/") - if len(parts) != 2 { - return "", errors.New("invalid format for role ARN") - } - if parts[0] != "role" { - return "", errors.Errorf(`expected resource type to be "role"; got %q`, parts[0]) - } - return parts[1], nil -} - func (c *EKSConnector) ownsIAMRole(ctx context.Context, clusterName, roleName string) (bool, error) { roleOutput, err := c.Provider.IAM().GetRole(ctx, &iam.GetRoleInput{ RoleName: aws.String(roleName), From 8b9c4dbb77aefaa16d4cfb4816cc3fbea4c0d066 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Sun, 12 May 2024 17:17:36 +0300 Subject: [PATCH 18/35] add unit tests and update generated files --- pkg/actions/addon/addon.go | 7 +- pkg/actions/addon/create.go | 102 +- pkg/actions/addon/create_test.go | 1738 ++++++++++------- pkg/actions/addon/delete.go | 8 +- pkg/actions/addon/delete_test.go | 70 +- pkg/actions/addon/podidentityassociation.go | 1 + .../podidentityassociation/addon_migrator.go | 3 + .../iam_role_updater.go | 3 +- pkg/apis/eksctl.io/v1alpha5/addon.go | 19 +- pkg/apis/eksctl.io/v1alpha5/addon_test.go | 190 +- .../eksctl.io/v1alpha5/assets/schema.json | 16 + pkg/apis/eksctl.io/v1alpha5/iam.go | 2 +- .../v1alpha5/zz_generated.deepcopy.go | 11 + 13 files changed, 1401 insertions(+), 769 deletions(-) diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index 9a3d582b45..316b7c5147 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -105,7 +105,8 @@ func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon var versions []*version.Version for _, addonVersionInfo := range addonInfos.Addons[0].AddonVersions { // if not specified, will install default version - if addonVersion == "" && addonVersionInfo.Compatibilities[0].DefaultVersion { + if addonVersion == "" && len(addonVersionInfo.Compatibilities) > 0 && + addonVersionInfo.Compatibilities[0].DefaultVersion { return *addonVersionInfo.AddonVersion, addonVersionInfo.RequiresIamPermissions, nil } else if addonVersion == "" { continue @@ -138,10 +139,6 @@ func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon return versions[0].Original(), requireIAMPermissions, nil } -func (a *Manager) makeAddonIRSAName(name string) string { - return fmt.Sprintf("eksctl-%s-addon-%s-IRSA", a.clusterConfig.Metadata.Name, name) -} - func (a *Manager) makeAddonName(name string) string { return manager.MakeAddonStackName(a.clusterConfig.Metadata.Name, name) } diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 97f7e1d6c5..5da3dd65e7 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -24,7 +24,7 @@ import ( var ( updateAddonRecommended = func(supportsPodIDs bool) string { - path := "`addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`" + path := "`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`" if supportsPodIDs { path = "`addon.PodIdentityAssociations`" } @@ -81,19 +81,19 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I ClusterName: &a.clusterConfig.Metadata.Name, }) if err != nil && !errors.As(err, ¬FoundErr) { - return err + return fmt.Errorf("failed to describe addon: %w", err) } // if the addon already exists AND it is not in CREATE_FAILED state if err == nil && summary.Addon.Status != ekstypes.AddonStatusCreateFailed { - logger.Info("addon %s is already present on the cluster, as an EKS managed addon, skipping creation", addon.Name) + logger.Info("%q addon is already present on the cluster, as an EKS managed addon, skipping creation", addon.Name) return nil } version, requiresIAMPermissions, err := a.getLatestMatchingVersion(ctx, addon) addon.Version = version if err != nil { - return fmt.Errorf("failed to fetch version %s for addon %s: %w", version, addon.Name, err) + return err } var configurationValues *string if addon.ConfigurationValues != "" { @@ -123,31 +123,60 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I createAddonInput.Tags = addon.Tags } - podIDConfig, supportsPodIDs, err := a.getRecommendedPoliciesForPodID(ctx, addon) - if err != nil { - return err - } - if requiresIAMPermissions { + podIDConfig, supportsPodIDs, err := a.getRecommendedPoliciesForPodID(ctx, addon) + if err != nil { + return err + } + switch { + // firstly, check if the user has specifically defined pod identity associations case addon.HasPodIDsSet(): if !supportsPodIDs { - return fmt.Errorf("%q addon does not support pod identity associations; use IRSA instead", addon.Name) + return fmt.Errorf("%q addon does not support pod identity associations; use IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`) instead", addon.Name) } - logger.Info("pod identity associations were specified for %q addon; will use those to provide required IAM permissions, any IRSA settings will be ignored", addon.Name) + logger.Info("pod identity associations are set for %q addon; will use these to configure required IAM permissions", addon.Name) for _, pia := range *addon.PodIdentityAssociations { - roleARN, err := iamRoleCreator.Create(ctx, &pia, addon.Name) - if err != nil { - return err + roleARN := pia.RoleARN + if roleARN == "" { + if roleARN, err = iamRoleCreator.Create(ctx, &pia, addon.Name); err != nil { + return err + } } createAddonInput.PodIdentityAssociations = append(createAddonInput.PodIdentityAssociations, ekstypes.AddonPodIdentityAssociations{ - RoleArn: &roleARN, - ServiceAccount: &pia.ServiceAccountName, + RoleArn: aws.String(roleARN), + ServiceAccount: aws.String(pia.ServiceAccountName), }) } + // afterwards, check if the user has specifically defined IRSA config + case addon.HasIRSASet(): + if !a.withOIDC { + logger.Warning(OIDCDisabledWarning(addon.Name, supportsPodIDs, true)) + break + } + logger.Info("IRSA is set for %q addon; will use this to configure IAM permissions", addon.Name) + if supportsPodIDs { + logger.Warning(IRSADeprecatedWarning(addon.Name)) + } + + if addon.ServiceAccountRoleARN != "" { + logger.Info("using provided ServiceAccountRoleARN %q", addon.ServiceAccountRoleARN) + createAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN + break + } + + logger.Info("creating role using provided policies for %q addon", addon.Name) + namespace, serviceAccount := a.getKnownServiceAccountLocation(addon) + roleARN, err := a.createRoleForIRSA(ctx, addon, namespace, serviceAccount) + if err != nil { + return err + } + createAddonInput.ServiceAccountRoleArn = &roleARN + + // if neither podIDs nor IRSA are set explicitly, then check if podIDs should be created automatically case a.clusterConfig.IAM.AutoCreatePodIdentityAssociations && supportsPodIDs: - logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will use recommended policies for %q addon, any IRSA settings will be ignored", addon.Name) + logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for %q addon", addon.Name) if addon.CanonicalName() == api.VPCCNIAddon && a.clusterConfig.IPv6Enabled() { roleARN, err := iamRoleCreator.Create(ctx, &api.PodIdentityAssociation{ @@ -178,30 +207,17 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I }) } - case addon.HasIRSASet(): + // if podIDs are not supported, check for any recommended IRSA policies + case a.setRecommendedPoliciesForIRSA(addon): if !a.withOIDC { - logger.Warning(OIDCDisabledWarning(addon.Name, supportsPodIDs, - /* isIRSASetExplicitly */ addon.ServiceAccountRoleARN != "" || addon.HasIRSAPoliciesSet())) + logger.Warning(OIDCDisabledWarning(addon.Name, supportsPodIDs, false)) break } - if supportsPodIDs { logger.Warning(IRSADeprecatedWarning(addon.Name)) } - if addon.ServiceAccountRoleARN != "" { - logger.Info("using provided ServiceAccountRoleARN %q", addon.ServiceAccountRoleARN) - createAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN - break - } - - if !addon.HasIRSAPoliciesSet() { - a.setRecommendedPoliciesForIRSA(addon) - logger.Info("creating role using recommended policies for %q addon", addon.Name) - } else { - logger.Info("creating role using provided policies for %q addon", addon.Name) - } - + logger.Info("creating role using recommended policies for %q addon", addon.Name) namespace, serviceAccount := a.getKnownServiceAccountLocation(addon) roleARN, err := a.createRoleForIRSA(ctx, addon, namespace, serviceAccount) if err != nil { @@ -246,12 +262,12 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I } }() var addonServiceAccounts []string - for _, config := range podIDConfig { - addonServiceAccounts = append(addonServiceAccounts, fmt.Sprintf("%q", *config.ServiceAccount)) + for _, pia := range createAddonInput.PodIdentityAssociations { + addonServiceAccounts = append(addonServiceAccounts, fmt.Sprintf("%q", *pia.ServiceAccount)) } return fmt.Errorf("creating addon: one or more service accounts corresponding to %q addon is already associated with a different IAM role; please delete all pre-existing pod identity associations corresponding to %s service account(s) in the addon's namespace, then re-try creating the addon", addon.Name, strings.Join(addonServiceAccounts, ",")) } - return errors.Wrapf(err, "failed to create addon %q", addon.Name) + return fmt.Errorf("failed to create %q addon: %w", addon.Name, err) } if output != nil { @@ -383,18 +399,19 @@ func (a *Manager) getRecommendedPoliciesForPodID(ctx context.Context, addon *api AddonVersion: &addon.Version, }) if err != nil { - return nil, false, fmt.Errorf("describing configuration for addon %s: %w", addon.Name, err) + return nil, false, fmt.Errorf("failed to describe configuration for %q addon: %w", addon.Name, err) } return output.PodIdentityConfiguration, len(output.PodIdentityConfiguration) != 0, nil } -func (a *Manager) setRecommendedPoliciesForIRSA(addon *api.Addon) { +func (a *Manager) setRecommendedPoliciesForIRSA(addon *api.Addon) bool { switch addon.CanonicalName() { case api.VPCCNIAddon: if a.clusterConfig.IPv6Enabled() { addon.AttachPolicy = makeIPv6VPCCNIPolicyDocument(api.Partitions.ForRegion(a.clusterConfig.Metadata.Region)) + } else { + addon.AttachPolicyARNs = append(addon.AttachPolicyARNs, fmt.Sprintf("arn:%s:iam::aws:policy/%s", api.Partitions.ForRegion(a.clusterConfig.Metadata.Region), api.IAMPolicyAmazonEKSCNIPolicy)) } - addon.AttachPolicyARNs = append(addon.AttachPolicyARNs, fmt.Sprintf("arn:%s:iam::aws:policy/%s", api.Partitions.ForRegion(a.clusterConfig.Metadata.Region), api.IAMPolicyAmazonEKSCNIPolicy)) case api.AWSEBSCSIDriverAddon: addon.WellKnownPolicies = api.WellKnownPolicies{ EBSCSIController: true, @@ -404,8 +421,9 @@ func (a *Manager) setRecommendedPoliciesForIRSA(addon *api.Addon) { EFSCSIController: true, } default: - return + return false } + return true } func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, namespace, serviceAccount string) (string, error) { @@ -414,7 +432,7 @@ func (a *Manager) createRoleForIRSA(ctx context.Context, addon *api.Addon, names return "", err } if err := a.createStack(ctx, resourceSet, addon.Name, - a.makeAddonIRSAName(addon.Name)); err != nil { + a.makeAddonName(addon.Name)); err != nil { return "", err } return resourceSet.OutputRole, nil diff --git a/pkg/actions/addon/create_test.go b/pkg/actions/addon/create_test.go index 4c574221e1..6437cd76e0 100644 --- a/pkg/actions/addon/create_test.go +++ b/pkg/actions/addon/create_test.go @@ -4,13 +4,15 @@ import ( "bytes" "context" "fmt" + "os" "time" "k8s.io/client-go/kubernetes" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/eks" + awseks "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/kris-nova/logger" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -18,753 +20,1153 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/addon/fakes" + addonmocks "github.com/weaveworks/eksctl/pkg/actions/addon/mocks" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/builder" + "github.com/weaveworks/eksctl/pkg/eks/mocksv2" iamoidc "github.com/weaveworks/eksctl/pkg/iam/oidc" "github.com/weaveworks/eksctl/pkg/testutils" "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" ) +type createAddonEntry struct { + addon api.Addon + withOIDC bool + waitTimeout time.Duration + + mockClusterConfig func(clusterConfig *api.ClusterConfig) + mockIAM func(mockIAMRoleCreator *addonmocks.IAMRoleCreator) + mockEKS func(provider *mockprovider.MockProvider) + mockCFN func(stackManager *fakes.FakeStackManager) + mockK8s bool + + validateCreateAddonInput func(input *awseks.CreateAddonInput) + validateCustomLoggerOutput func(output string) + validateCFNCalls func(stackManager *fakes.FakeStackManager) + expectedErr string +} + var _ = Describe("Create", func() { var ( - manager *addon.Manager - withOIDC bool - oidc *iamoidc.OpenIDConnectManager - fakeStackManager *fakes.FakeStackManager - mockProvider *mockprovider.MockProvider - createAddonInput *eks.CreateAddonInput - returnedErr error - createStackReturnValue error - rawClient *testutils.FakeRawClient - clusterConfig *api.ClusterConfig + manager *addon.Manager + oidc *iamoidc.OpenIDConnectManager + fakeStackManager *fakes.FakeStackManager + mockProvider *mockprovider.MockProvider + createAddonInput *awseks.CreateAddonInput + mockIAMRoleCreator *addonmocks.IAMRoleCreator + fakeRawClient *testutils.FakeRawClient + err error + genericErr = fmt.Errorf("ERR") ) - BeforeEach(func() { - clusterConfig = &api.ClusterConfig{Metadata: &api.ClusterMeta{ - Version: "1.18", - Name: "my-cluster", - }} - withOIDC = true - returnedErr = nil - fakeStackManager = new(fakes.FakeStackManager) - mockProvider = mockprovider.NewMockProvider() - createStackReturnValue = nil - - fakeStackManager.CreateStackStub = func(_ context.Context, _ string, rs builder.ResourceSetReader, _ map[string]string, _ map[string]string, errs chan error) error { - go func() { - errs <- nil - }() - return createStackReturnValue + mockDescribeAddon := func(mockEKS *mocksv2.EKS, err error) { + if err == nil { + err = &ekstypes.ResourceNotFoundException{ + Message: aws.String(genericErr.Error()), + } } + mockEKS. + On("DescribeAddon", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonInput{})) + }). + Return(nil, err). + Once() + } - sampleAddons := testutils.LoadSamples("testdata/aws-node.json") - - rawClient = testutils.NewFakeRawClient() - - rawClient.AssumeObjectsMissing = true - - for _, item := range sampleAddons { - rc, err := rawClient.NewRawResource(item) - Expect(err).NotTo(HaveOccurred()) - _, err = rc.CreateOrReplace(false) - Expect(err).NotTo(HaveOccurred()) + mockDescribeAddonVersions := func(mockEKS *mocksv2.EKS, err error) { + var output *awseks.DescribeAddonVersionsOutput + if err == nil { + output = &awseks.DescribeAddonVersionsOutput{ + Addons: []ekstypes.AddonInfo{ + { + AddonVersions: []ekstypes.AddonVersionInfo{ + { + AddonVersion: aws.String("v1.0.0-eksbuild.1"), + RequiresIamPermissions: false, + }, + { + AddonVersion: aws.String("v1.7.5-eksbuild.1"), + RequiresIamPermissions: true, + Compatibilities: []ekstypes.Compatibility{ + { + ClusterVersion: aws.String(api.DefaultVersion), + DefaultVersion: true, + }, + }, + }, + { + AddonVersion: aws.String("v1.7.5-eksbuild.2"), + }, + { + AddonVersion: aws.String("v1.7.7-eksbuild.2"), + }, + { + AddonVersion: aws.String("v1.7.6"), + }, + }, + }, + }, + } } + mockEKS. + On("DescribeAddonVersions", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonVersionsInput{})) + }). + Return(output, err). + Once() + } - ct := rawClient.Collection - - Expect(ct.Updated()).To(BeEmpty()) - Expect(ct.Created()).NotTo(BeEmpty()) - Expect(ct.CreatedItems()).To(HaveLen(10)) - }) + mockDescribeAddonConfiguration := func(mockEKS *mocksv2.EKS, serviceAccountNames []string, err error) { + podIDConfig := []ekstypes.AddonPodIdentityConfiguration{} + for _, sa := range serviceAccountNames { + podIDConfig = append(podIDConfig, ekstypes.AddonPodIdentityConfiguration{ + ServiceAccount: &sa, + RecommendedManagedPolicies: []string{"arn:aws:iam::111122223333:policy/" + sa}, + }) + } + mockEKS. + On("DescribeAddonConfiguration", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonConfigurationInput{})) + }). + Return(&awseks.DescribeAddonConfigurationOutput{ + PodIdentityConfiguration: podIDConfig, + }, err). + Once() + } - JustBeforeEach(func() { - var err error + mockCreateAddon := func(mockEKS *mocksv2.EKS, err error) { + mockEKS. + On("CreateAddon", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.CreateAddonInput{})) + createAddonInput = args[1].(*awseks.CreateAddonInput) + }). + Return(&awseks.CreateAddonOutput{ + Addon: &ekstypes.Addon{}, + }, err). + Once() + } - oidc, err = iamoidc.NewOpenIDConnectManager(nil, "456123987123", "https://oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E", "aws", nil) - Expect(err).NotTo(HaveOccurred()) - oidc.ProviderARN = "arn:aws:iam::456123987123:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E" + DescribeTable("Create addon", func(e createAddonEntry) { + if e.addon.Name == "" { + e.addon.Name = "my-addon" + } - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonInput{})) - }).Return(nil, &ekstypes.ResourceNotFoundException{}).Once() + clusterConfig := api.NewClusterConfig() + if e.mockClusterConfig != nil { + e.mockClusterConfig(clusterConfig) + } - mockProvider.MockEKS().On("CreateAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.CreateAddonInput{})) - createAddonInput = args[1].(*eks.CreateAddonInput) - }).Return(nil, returnedErr) + mockIAMRoleCreator = new(addonmocks.IAMRoleCreator) + if e.mockIAM != nil { + e.mockIAM(mockIAMRoleCreator) + } - manager, err = addon.New(clusterConfig, mockProvider.EKS(), fakeStackManager, withOIDC, oidc, func() (kubernetes.Interface, error) { - return rawClient.ClientSet(), nil - }) - Expect(err).NotTo(HaveOccurred()) + fakeStackManager = new(fakes.FakeStackManager) + if e.mockCFN != nil { + e.mockCFN(fakeStackManager) + } - mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonVersionsInput{})) - }).Return(&eks.DescribeAddonVersionsOutput{ - Addons: []ekstypes.AddonInfo{ - { - AddonName: aws.String("my-addon"), - Type: aws.String("type"), - AddonVersions: []ekstypes.AddonVersionInfo{ - { - AddonVersion: aws.String("v1.0.0-eksbuild.1"), - }, - { - AddonVersion: aws.String("v1.7.5-eksbuild.1"), - }, - { - AddonVersion: aws.String("v1.7.5-eksbuild.2"), - }, - { - //not sure if all versions come with v prefix or not, so test a mix - AddonVersion: aws.String("v1.7.7-eksbuild.2"), - }, - { - AddonVersion: aws.String("v1.7.6"), - }, - }, - }, - }, - }, nil) - }) - - When("the addon is already present in the cluster, as an EKS managed addon", func() { - When("the addon is in CREATE_FAILED state", func() { - BeforeEach(func() { - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonInput{})) - }).Return(&eks.DescribeAddonOutput{ - Addon: &ekstypes.Addon{ - AddonName: aws.String("my-addon"), - Status: ekstypes.AddonStatusCreateFailed, - }, - }, nil) - }) + mockProvider = mockprovider.NewMockProvider() + if e.mockEKS != nil { + e.mockEKS(mockProvider) + } - It("will try to re-create the addon", func() { - err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, nil, 0) + fakeRawClient = testutils.NewFakeRawClient() + if e.mockK8s { + fakeRawClient.AssumeObjectsMissing = true + sampleAddons := testutils.LoadSamples("testdata/aws-node.json") + for _, item := range sampleAddons { + rc, err := fakeRawClient.NewRawResource(item) Expect(err).NotTo(HaveOccurred()) - mockProvider.MockEKS().AssertNumberOfCalls(GinkgoT(), "CreateAddon", 1) - }) - }) - - When("the addon is in another state", func() { - BeforeEach(func() { - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonInput{})) - }).Return(&eks.DescribeAddonOutput{ - Addon: &ekstypes.Addon{ - AddonName: aws.String("my-addon"), - Status: ekstypes.AddonStatusActive, - }, - }, nil) - }) - - It("won't re-create the addon", func() { - output := &bytes.Buffer{} - logger.Writer = output - - err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, nil, 0) + _, err = rc.CreateOrReplace(false) Expect(err).NotTo(HaveOccurred()) - Expect(output.String()).To(ContainSubstring("addon my-addon is already present on the cluster, as an EKS managed addon, skipping creation")) - }) - }) - }) + } - When("looking up if the addon is already present fails", func() { - BeforeEach(func() { - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonInput{})) - }).Return(nil, fmt.Errorf("test error")) - }) - It("returns an error", func() { - err := manager.Create(context.Background(), &api.Addon{Name: "my-addon"}, nil, 0) - Expect(err).To(MatchError(`test error`)) - }) - }) + ct := fakeRawClient.Collection + Expect(ct.Updated()).To(BeEmpty()) + Expect(ct.Created()).NotTo(BeEmpty()) + Expect(ct.CreatedItems()).To(HaveLen(10)) + } - When("it fails to create addon", func() { - BeforeEach(func() { - returnedErr = fmt.Errorf("foo") - }) - It("returns an error", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - }, nil, 0) - Expect(err).To(MatchError(`failed to create addon "my-addon": foo`)) + output := &bytes.Buffer{} + if e.validateCustomLoggerOutput != nil { + defer func() { + logger.Writer = os.Stdout + }() + logger.Writer = output + } - }) - }) + oidc, err = iamoidc.NewOpenIDConnectManager(nil, "111122223333", "https://oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E", "aws", nil) + Expect(err).NotTo(HaveOccurred()) - When("OIDC is disabled", func() { - BeforeEach(func() { - withOIDC = false - }) - It("creates the addons but not the policies", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(createAddonInput.ServiceAccountRoleArn).To(BeNil()) + manager, err = addon.New(clusterConfig, mockProvider.EKS(), fakeStackManager, e.withOIDC, oidc, func() (kubernetes.Interface, error) { + return fakeRawClient.ClientSet(), nil }) - }) + Expect(err).NotTo(HaveOccurred()) - When("version is specified", func() { - When("the versions are valid", func() { - BeforeEach(func() { - withOIDC = false - }) + err = manager.Create(context.Background(), &e.addon, mockIAMRoleCreator, e.waitTimeout) + if e.expectedErr != "" { + Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) + return + } + Expect(err).ToNot(HaveOccurred()) - When("version is set to a numeric value", func() { - It("discovers and uses the latest available version", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "1.7.5", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) + if e.validateCreateAddonInput != nil { + e.validateCreateAddonInput(createAddonInput) + } - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.7.5-eksbuild.2")) - Expect(createAddonInput.ServiceAccountRoleArn).To(BeNil()) - }) - }) + if e.validateCustomLoggerOutput != nil { + e.validateCustomLoggerOutput(output.String()) + } - When("version is set to an alphanumeric value", func() { - It("discovers and uses the latest available version", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "1.7.5-eksbuild", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) + if e.validateCFNCalls != nil { + e.validateCFNCalls(fakeStackManager) + } + }, + Entry("[API Error] fails to describe addon", createAddonEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), genericErr) + }, + expectedErr: "failed to describe addon", + }), - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.7.5-eksbuild.2")) - Expect(createAddonInput.ServiceAccountRoleArn).To(BeNil()) - }) - }) + Entry("[API Error] fails to describe addon versions", createAddonEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), genericErr) + }, + expectedErr: "failed to describe addon versions", + }), + + Entry("[API Error] fails to describe addon configuration", createAddonEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(provider.MockEKS(), []string{}, genericErr) + }, + expectedErr: "failed to describe configuration for \"my-addon\" addon", + }), + + Entry("[API Error] fails to create addon", createAddonEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(provider.MockEKS(), []string{}, nil) + mockCreateAddon(provider.MockEKS(), genericErr) + }, + expectedErr: "failed to create \"my-addon\" addon", + }), + + Entry("[API Error] fails to create IAM role for podID", createAddonEntry{ + addon: api.Addon{ + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "sa1", + PermissionPolicyARNs: []string{"arn:aws:iam::111122223333:policy/sa1"}, + }, + }, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + }, + mockIAM: func(mockIAMRoleCreator *addonmocks.IAMRoleCreator) { + mockIAMRoleCreator. + On("Create", mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(3)) + Expect(args[1]).To(BeAssignableToTypeOf(&api.PodIdentityAssociation{})) + Expect(args[1].(*api.PodIdentityAssociation).ServiceAccountName).To(Equal("sa1")) + Expect(args[1].(*api.PodIdentityAssociation).PermissionPolicyARNs).To(ConsistOf("arn:aws:iam::111122223333:policy/sa1")) + }). + Return("", genericErr). + Once() + }, + expectedErr: genericErr.Error(), + }), - When("version is set to latest", func() { - It("discovers and uses the latest available version", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "latest", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) + Entry("[API Error] fails to create IAM role for service account", createAddonEntry{ + addon: api.Addon{ + AttachPolicyARNs: []string{"arn:aws:iam::111122223333:policy/policy-name-1"}, + }, + withOIDC: true, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + go func() { + c <- nil + }() + Expect(rsr).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) + output, err := rsr.(*builder.IAMRoleResourceSet).RenderJSON() Expect(err).NotTo(HaveOccurred()) + Expect(string(output)).To(ContainSubstring("arn:aws:iam::111122223333:policy/policy-name-1")) + return genericErr + } + stackManager.CreateStackReturns(genericErr) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.CreateStackCallCount()).To(Equal(1)) + }, + expectedErr: genericErr.Error(), + }), - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.7.7-eksbuild.2")) - Expect(createAddonInput.ServiceAccountRoleArn).To(BeNil()) - }) - }) - - When("the version is set to a version that does not exist", func() { - It("returns an error", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "1.7.8", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("no version(s) found matching \"1.7.8\" for \"my-addon\""))) - }) - }) - }) - - When("the versions are invalid", func() { - BeforeEach(func() { - withOIDC = false - - mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonVersionsInput{})) - }).Return(&eks.DescribeAddonVersionsOutput{ - Addons: []ekstypes.AddonInfo{ - { + Entry("[Addon already exists] addon is in CREATE_FAILED state", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + provider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonInput{})) + }). + Return(&awseks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ AddonName: aws.String("my-addon"), - Type: aws.String("type"), - AddonVersions: []ekstypes.AddonVersionInfo{ - { - AddonVersion: aws.String("v1.7.5-eksbuild.1"), - }, - { - //not sure if all versions come with v prefix or not, so test a mix - AddonVersion: aws.String("v1.7.7-eksbuild.1"), - }, - { - AddonVersion: aws.String("totally not semver"), - }, - }, + Status: ekstypes.AddonStatusCreateFailed, }, - }, - }, nil) - }) + }, nil). + Once() - It("returns an error", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "latest", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).To(MatchError(ContainSubstring("failed to parse version \"totally not semver\":"))) - }) - }) - - When("there are no versions returned", func() { - BeforeEach(func() { - withOIDC = false - - mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { - Expect(args).To(HaveLen(2)) - Expect(args[1]).To(BeAssignableToTypeOf(&eks.DescribeAddonVersionsInput{})) - }).Return(&eks.DescribeAddonVersionsOutput{ - Addons: []ekstypes.AddonInfo{ - { - AddonName: aws.String("my-addon"), - Type: aws.String("type"), - AddonVersions: []ekstypes.AddonVersionInfo{}, + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).NotTo(ContainSubstring("addon is already present on the cluster, as an EKS managed addon, skipping creation")) + }, + }), + + Entry("[Addon already exists] addon is NOT in CREATE_FAILED state", createAddonEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + provider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonInput{})) + }). + Return(&awseks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ + AddonName: aws.String("my-addon"), + Status: ekstypes.AddonStatusActive, }, - }, - }, nil) - }) + }, nil). + Once() + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("addon is already present on the cluster, as an EKS managed addon, skipping creation")) + }, + }), + + Entry("[Resolve version] no version found", createAddonEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + provider.MockEKS(). + On("DescribeAddonVersions", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonVersionsInput{})) + }). + Return(&awseks.DescribeAddonVersionsOutput{ + Addons: []ekstypes.AddonInfo{{AddonVersions: []ekstypes.AddonVersionInfo{}}}, + }, nil). + Once() + }, + expectedErr: "no versions available for \"my-addon\"", + }), - It("returns an error", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "latest", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).To(MatchError(ContainSubstring("no versions available for \"my-addon\""))) - }) - }) - }) + Entry("[Resolve version] invalid version found", createAddonEntry{ + addon: api.Addon{ + Version: "latest", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + provider.MockEKS(). + On("DescribeAddonVersions", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonVersionsInput{})) + }). + Return(&awseks.DescribeAddonVersionsOutput{ + Addons: []ekstypes.AddonInfo{{AddonVersions: []ekstypes.AddonVersionInfo{ + { + AddonVersion: aws.String("totally not semver"), + }, + }}}, + }, nil). + Once() + }, + expectedErr: "failed to parse version \"totally not semver\":", + }), - type createAddonEntry struct { - addonName string - shouldWait bool - } + Entry("[Resolve version] missing", createAddonEntry{ + addon: api.Addon{ + Version: "1.100.0", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + }, + expectedErr: "no version(s) found matching \"1.100.0\" for \"my-addon\"", + }), - Context("cluster without nodes", func() { - BeforeEach(func() { - zeroNodeNG := &api.NodeGroupBase{ - ScalingConfig: &api.ScalingConfig{ - DesiredCapacity: aws.Int(0), - }, - } - clusterConfig.NodeGroups = []*api.NodeGroup{ - { - NodeGroupBase: zeroNodeNG, - }, - } - clusterConfig.ManagedNodeGroups = []*api.ManagedNodeGroup{ - { - NodeGroupBase: zeroNodeNG, - }, - } - }) + Entry("[Resolve version] latest", createAddonEntry{ + addon: api.Addon{ + Version: "latest", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(*input.AddonVersion).To(Equal("v1.7.7-eksbuild.2")) + }, + }), - DescribeTable("addons created with a waitTimeout when there are no active nodes", func(e createAddonEntry) { - expectedDescribeCallsCount := 1 - if e.shouldWait { - expectedDescribeCallsCount++ - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.MatchedBy(func(input *eks.DescribeAddonInput) bool { - return *input.AddonName == e.addonName - }), mock.Anything).Return(&eks.DescribeAddonOutput{ - Addon: &ekstypes.Addon{ - AddonName: aws.String(e.addonName), - Status: ekstypes.AddonStatusActive, - }, - }, nil).Once() - } - err := manager.Create(context.Background(), &api.Addon{Name: e.addonName}, nil, time.Nanosecond) - Expect(err).NotTo(HaveOccurred()) - mockProvider.MockEKS().AssertNumberOfCalls(GinkgoT(), "DescribeAddon", expectedDescribeCallsCount) - }, - Entry("should not wait for CoreDNS to become active", createAddonEntry{ - addonName: api.CoreDNSAddon, - }), - Entry("should not wait for Amazon EBS CSI driver to become active", createAddonEntry{ - addonName: api.AWSEBSCSIDriverAddon, - }), - Entry("should not wait for Amazon EFS CSI driver to become active", createAddonEntry{ - addonName: api.AWSEFSCSIDriverAddon, - }), - Entry("should wait for VPC CNI to become active", createAddonEntry{ - addonName: api.VPCCNIAddon, - shouldWait: true, - }), - ) - }) - - When("resolveConflicts is configured", func() { - DescribeTable("AWS EKS resolve conflicts matches value from cluster config", - func(rc ekstypes.ResolveConflicts) { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "latest", - ResolveConflicts: rc, - }, nil, 0) + Entry("[Resolve version] numeric value", createAddonEntry{ + addon: api.Addon{ + Version: "1.7.7", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(*input.AddonVersion).To(Equal("v1.7.7-eksbuild.2")) + }, + }), - Expect(err).NotTo(HaveOccurred()) - Expect(createAddonInput.ResolveConflicts).To(Equal(rc)) - }, - Entry("none", ekstypes.ResolveConflictsNone), - Entry("overwrite", ekstypes.ResolveConflictsOverwrite), - Entry("preserve", ekstypes.ResolveConflictsPreserve), - ) - }) - - When("configurationValues is configured", func() { - addon := &api.Addon{ - Name: "my-addon", - Version: "latest", - ConfigurationValues: "{\"replicaCount\":3}", - } - It("sends the value to the AWS EKS API", func() { - err := manager.Create(context.Background(), addon, nil, 0) + Entry("[Resolve version] alphanumeric value", createAddonEntry{ + addon: api.Addon{ + Version: "v1.7.5", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(*input.AddonVersion).To(Equal("v1.7.5-eksbuild.2")) + }, + }), - Expect(err).NotTo(HaveOccurred()) - Expect(*createAddonInput.ConfigurationValues).To(Equal(addon.ConfigurationValues)) - }) - }) + Entry("[ResolveConflicts] explicitly set to overwrite", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + ResolveConflicts: ekstypes.ResolveConflictsOverwrite, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.ResolveConflicts).To(Equal(ekstypes.ResolveConflictsOverwrite)) + }, + }), - When("force is true", func() { - BeforeEach(func() { - withOIDC = false - }) + Entry("[ResolveConflicts] implicitly set to overwrite by using `--force` flag", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + Force: true, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.ResolveConflicts).To(Equal(ekstypes.ResolveConflictsOverwrite)) + }, + }), - It("creates the addons but not the policies", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - AttachPolicyARNs: []string{"arn-1"}, - Force: true, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(createAddonInput.ResolveConflicts).To(Equal(ekstypes.ResolveConflictsOverwrite)) - Expect(createAddonInput.ServiceAccountRoleArn).To(BeNil()) - }) - }) + Entry("[ConfigurationValues] are set", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + ConfigurationValues: "{\"replicaCount\":3}", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(*input.ConfigurationValues).To(Equal("{\"replicaCount\":3}")) + }, + }), - When("wait is true", func() { - When("the addon creation succeeds", func() { - BeforeEach(func() { - withOIDC = false - }) + Entry("[Tags] are set", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + Tags: map[string]string{"foo": "bar", "fox": "brown"}, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.Tags["foo"]).To(Equal("bar")) + Expect(input.Tags["fox"]).To(Equal("brown")) + }, + }), - It("creates the addon and waits for it to be active", func() { - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything). - Return(&eks.DescribeAddonOutput{ + Entry("[Wait is true] addon creation succeeds", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + }, + waitTimeout: time.Nanosecond, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + // addon becomes active after creation + mockProvider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything, mock.Anything). + Return(&awseks.DescribeAddonOutput{ Addon: &ekstypes.Addon{ AddonName: aws.String("my-addon"), Status: ekstypes.AddonStatusActive, }, - }, nil) + }, nil). + Once() + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + }), - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) + Entry("[Wait is true] addon creation fails", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + }, + waitTimeout: time.Nanosecond, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + // addon becomes degraded after creation + mockProvider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything, mock.Anything). + Return(&awseks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ + AddonName: aws.String("my-addon"), + Status: ekstypes.AddonStatusDegraded, + }, + }, nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + expectedErr: "addon status transitioned to \"DEGRADED\"", + }), - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - }) - }) + Entry("[Cluster without nodegroups] should not wait for CoreDNS to become active", createAddonEntry{ + addon: api.Addon{ + Name: api.CoreDNSAddon, + Version: "1.0.0", + }, + waitTimeout: time.Nanosecond, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + // addon becomes degraded after creation + mockProvider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything, mock.Anything). + Return(&awseks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ + AddonName: aws.String("my-addon"), + Status: ekstypes.AddonStatusDegraded, + }, + }, nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + }), - When("the addon creation fails", func() { - BeforeEach(func() { - withOIDC = false - }) + Entry("[Cluster without nodegroups] should not wait for EBS CSI driver to become active", createAddonEntry{ + addon: api.Addon{ + Name: api.AWSEBSCSIDriverAddon, + Version: "1.0.0", + }, + waitTimeout: time.Nanosecond, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + // addon becomes degraded after creation + mockProvider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything, mock.Anything). + Return(&awseks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ + AddonName: aws.String("my-addon"), + Status: ekstypes.AddonStatusDegraded, + }, + }, nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + }), - It("returns an error", func() { - mockProvider.MockEKS().On("DescribeAddon", mock.Anything, mock.Anything, mock.Anything). - Return(&eks.DescribeAddonOutput{ + Entry("[Cluster without nodegroups] should not wait for EFS CSI driver to become active", createAddonEntry{ + addon: api.Addon{ + Name: api.AWSEFSCSIDriverAddon, + Version: "1.0.0", + }, + waitTimeout: time.Nanosecond, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider.MockEKS(), nil) + // addon becomes degraded after creation + mockProvider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything, mock.Anything). + Return(&awseks.DescribeAddonOutput{ Addon: &ekstypes.Addon{ AddonName: aws.String("my-addon"), Status: ekstypes.AddonStatusDegraded, }, }, nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockCreateAddon(provider.MockEKS(), nil) + }, + }), + + Entry("[RequiresIAMPermissions] podIDs set explicitly and NOT supportsPodIDs", createAddonEntry{ + addon: api.Addon{ + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "sa1", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + }, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + }, + expectedErr: "\"my-addon\" addon does not support pod identity associations; use IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`) instead", + }), + + Entry("[RequiresIAMPermissions] podIDs set explicitly and supportsPodIDs", createAddonEntry{ + addon: api.Addon{ + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "sa1", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + { + ServiceAccountName: "sa2", + RoleARN: "arn:aws:iam::111122223333:role/role-name-2", + }, + }, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1", "sa2"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(2)) + Expect(*input.PodIdentityAssociations[0].ServiceAccount).To(Equal("sa1")) + Expect(*input.PodIdentityAssociations[0].RoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + Expect(*input.PodIdentityAssociations[1].ServiceAccount).To(Equal("sa2")) + Expect(*input.PodIdentityAssociations[1].RoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-2")) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("pod identity associations are set for \"my-addon\" addon; will use these to configure required IAM permissions")) + }, + }), - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - }, nil, 5*time.Minute) - Expect(err).To(MatchError(`addon status transitioned to "DEGRADED"`)) - }) - }) - }) - - When("No policy/role is specified", func() { - When("we don't know the recommended policies for the specified addon", func() { - It("does not provide a role", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) + Entry("[RequiresIAMPermissions] `autoCreatePodIdentityAssociations:true` and NOT supportsPodIDs", createAddonEntry{ + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IAM permissions are required for \"my-addon\" addon; " + + "the recommended way to provide IAM permissions for \"my-addon\" addon is via IRSA; " + + "after addon creation is completed, add all recommended policies to the config file, " + + "under `addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`, " + + "and run `eksctl update addon`")) + }, + }), - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(createAddonInput.ServiceAccountRoleArn).To(BeNil()) - }) - }) + Entry("[RequiresIAMPermissions] `autoCreatePodIdentityAssociations:true` and supportsPodIDs", createAddonEntry{ + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockIAM: func(mockIAMRoleCreator *addonmocks.IAMRoleCreator) { + mockIAMRoleCreator. + On("Create", mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(3)) + Expect(args[1]).To(BeAssignableToTypeOf(&api.PodIdentityAssociation{})) + Expect(args[1].(*api.PodIdentityAssociation).ServiceAccountName).To(Equal("sa1")) + Expect(args[1].(*api.PodIdentityAssociation).PermissionPolicyARNs).To(ConsistOf("arn:aws:iam::111122223333:policy/sa1")) + }). + Return("arn:aws:iam::111122223333:role/sa1", nil). + Once() + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(1)) + Expect(*input.PodIdentityAssociations[0].ServiceAccount).To(Equal("sa1")) + Expect(*input.PodIdentityAssociations[0].RoleArn).To(Equal("arn:aws:iam::111122223333:role/sa1")) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for \"my-addon\" addon")) + }, + }), - When("we know the recommended policies for the specified addon", func() { - BeforeEach(func() { - fakeStackManager.CreateStackStub = func(_ context.Context, _ string, rs builder.ResourceSetReader, _ map[string]string, _ map[string]string, errs chan error) error { - go func() { - errs <- nil - }() - Expect(rs).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) - rs.(*builder.IAMRoleResourceSet).OutputRole = "role-arn" - return createStackReturnValue + Entry("[RequiresIAMPermissions] `autoCreatePodIdentityAssociations:true` and supportsPodIDs (vpc-cni && ipv6)", createAddonEntry{ + addon: api.Addon{ + Name: api.VPCCNIAddon, + }, + mockK8s: true, + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{ + IPFamily: "IPv6", } + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockIAM: func(mockIAMRoleCreator *addonmocks.IAMRoleCreator) { + mockIAMRoleCreator. + On("Create", mock.Anything, mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(3)) + Expect(args[1]).To(BeAssignableToTypeOf(&api.PodIdentityAssociation{})) + Expect(args[1].(*api.PodIdentityAssociation).ServiceAccountName).To(Equal("aws-node")) + Expect(args[1].(*api.PodIdentityAssociation).PermissionPolicy).NotTo(BeEmpty()) + }). + Return("arn:aws:iam::111122223333:role/aws-node", nil). + Once() + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(1)) + Expect(*input.PodIdentityAssociations[0].ServiceAccount).To(Equal("aws-node")) + Expect(*input.PodIdentityAssociations[0].RoleArn).To(Equal("arn:aws:iam::111122223333:role/aws-node")) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for \"vpc-cni\" addon")) + }, + }), + + Entry("[RequiresIAMPermissions] podIDs already exist on cluster", createAddonEntry{ + addon: api.Addon{ + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "sa1", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + { + ServiceAccountName: "sa2", + RoleARN: "arn:aws:iam::111122223333:role/role-name-2", + }, + }, + }, + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1", "sa2"}, nil) + mockCreateAddon(mockProvider.MockEKS(), &ekstypes.ResourceInUseException{}) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.DeleteStackBySpecReturns(nil, nil) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.DeleteStackBySpecCallCount()).To(Equal(2)) + }, + expectedErr: "creating addon: one or more service accounts corresponding to \"my-addon\" addon is already associated with a different IAM role; " + + "please delete all pre-existing pod identity associations corresponding to \"sa1\",\"sa2\" service account(s) in the addon's namespace, then re-try creating the addon", + }), - }) + Entry("[RequiresIAMPermissions] IRSA set explicitly and NOT supportsPodIDs", createAddonEntry{ + addon: api.Addon{ + ServiceAccountRoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + withOIDC: true, + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).NotTo(BeNil()) + Expect(*input.ServiceAccountRoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).NotTo(ContainSubstring("IRSA has been deprecated")) + }, + }), - When("it's the vpc-cni addon", func() { - Context("ipv4", func() { - It("creates a role with the recommended policies and attaches it to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: api.VPCCNIAddon, - Version: "v1.0.0-eksbuild.1", - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-vpc-cni")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: api.VPCCNIAddon, - })) - output, err := resourceSet.RenderJSON() - Expect(err).NotTo(HaveOccurred()) - Expect(string(output)).To(ContainSubstring("arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy")) - Expect(string(output)).To(ContainSubstring(":sub\":\"system:serviceaccount:kube-system:aws-node")) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal(api.VPCCNIAddon)) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(*createAddonInput.ServiceAccountRoleArn).To(Equal("role-arn")) - }) - }) - - Context("ipv6", func() { - BeforeEach(func() { - clusterConfig.VPC = api.NewClusterVPC(false) - clusterConfig.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{ - IPFamily: api.IPV6Family, - } - }) - - It("creates a role with the recommended policies and attaches it to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: api.VPCCNIAddon, - Version: "v1.0.0-eksbuild.1", - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-vpc-cni")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: api.VPCCNIAddon, - })) - output, err := resourceSet.RenderJSON() - Expect(err).NotTo(HaveOccurred()) - Expect(string(output)).To(ContainSubstring("AssignIpv6Addresses")) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal(api.VPCCNIAddon)) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(*createAddonInput.ServiceAccountRoleArn).To(Equal("role-arn")) - }) - }) - }) + Entry("[RequiresIAMPermissions] IRSA set explicitly and supportsPodIDs", createAddonEntry{ + addon: api.Addon{ + AttachPolicy: api.InlineDocument{ + "foo": "policy-bar", + }, + }, + withOIDC: true, + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + go func() { + c <- nil + }() + Expect(rsr).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) + output, err := rsr.(*builder.IAMRoleResourceSet).RenderJSON() + Expect(err).NotTo(HaveOccurred()) + Expect(string(output)).To(ContainSubstring("policy-bar")) + rsr.(*builder.IAMRoleResourceSet).OutputRole = "arn:aws:iam::111122223333:role/role-name-1" + return nil + } + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).NotTo(BeNil()) + Expect(*input.ServiceAccountRoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.CreateStackCallCount()).To(Equal(1)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IRSA has been deprecated; " + + "the recommended way to provide IAM permissions for \"my-addon\" addon is via pod identity associations; " + + "after addon creation is completed, run `eksctl utils migrate-to-pod-identity`")) + }, + }), - When("it's the aws-ebs-csi-driver addon", func() { - It("creates a role with the recommended policies and attaches it to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: api.AWSEBSCSIDriverAddon, - Version: "v1.0.0-eksbuild.1", - }, nil, 0) + Entry("[RequiresIAMPermissions] IRSA set implicitly (vpc-cni && ipv4)", createAddonEntry{ + addon: api.Addon{ + Name: api.VPCCNIAddon, + }, + withOIDC: true, + mockK8s: true, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"aws-node"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + go func() { + c <- nil + }() + Expect(rsr).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) + output, err := rsr.(*builder.IAMRoleResourceSet).RenderJSON() Expect(err).NotTo(HaveOccurred()) + Expect(string(output)).To(ContainSubstring("arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy")) + Expect(string(output)).To(ContainSubstring(":sub\":\"system:serviceaccount:kube-system:aws-node")) + rsr.(*builder.IAMRoleResourceSet).OutputRole = "arn:aws:iam::111122223333:role/role-name-1" + return nil + } + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).NotTo(BeNil()) + Expect(*input.ServiceAccountRoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.CreateStackCallCount()).To(Equal(1)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IRSA has been deprecated; " + + "the recommended way to provide IAM permissions for \"vpc-cni\" addon is via pod identity associations; " + + "after addon creation is completed, run `eksctl utils migrate-to-pod-identity`")) + }, + }), - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-aws-ebs-csi-driver")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: api.AWSEBSCSIDriverAddon, - })) - output, err := resourceSet.RenderJSON() + Entry("[RequiresIAMPermissions] IRSA set implicitly (vpc-cni && ipv6)", createAddonEntry{ + addon: api.Addon{ + Name: api.VPCCNIAddon, + }, + withOIDC: true, + mockK8s: true, + mockClusterConfig: func(clusterConfig *api.ClusterConfig) { + clusterConfig.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{ + IPFamily: "IPv6", + } + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"aws-node"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + go func() { + c <- nil + }() + Expect(rsr).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) + output, err := rsr.(*builder.IAMRoleResourceSet).RenderJSON() Expect(err).NotTo(HaveOccurred()) - Expect(string(output)).To(ContainSubstring("PolicyEBSCSIController")) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal(api.AWSEBSCSIDriverAddon)) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(*createAddonInput.ServiceAccountRoleArn).To(Equal("role-arn")) - }) - }) + Expect(string(output)).To(ContainSubstring("AssignIpv6Addresses")) + rsr.(*builder.IAMRoleResourceSet).OutputRole = "arn:aws:iam::111122223333:role/role-name-1" + return nil + } + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).NotTo(BeNil()) + Expect(*input.ServiceAccountRoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.CreateStackCallCount()).To(Equal(1)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IRSA has been deprecated; " + + "the recommended way to provide IAM permissions for \"vpc-cni\" addon is via pod identity associations; " + + "after addon creation is completed, run `eksctl utils migrate-to-pod-identity`")) + }, + }), - When("it's the aws-efs-csi-driver addon", func() { - It("creates a role with the recommended policies and attaches it to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: api.AWSEFSCSIDriverAddon, - Version: "v1.0.0-eksbuild.1", - }, nil, 0) + Entry("[RequiresIAMPermissions] IRSA set implicitly and supportsPodIDs (aws-ebs-csi-driver)", createAddonEntry{ + addon: api.Addon{ + Name: api.AWSEBSCSIDriverAddon, + }, + withOIDC: true, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + go func() { + c <- nil + }() + Expect(rsr).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) + output, err := rsr.(*builder.IAMRoleResourceSet).RenderJSON() Expect(err).NotTo(HaveOccurred()) + Expect(string(output)).To(ContainSubstring("PolicyEBSCSIController")) + rsr.(*builder.IAMRoleResourceSet).OutputRole = "arn:aws:iam::111122223333:role/role-name-1" + return nil + } + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).NotTo(BeNil()) + Expect(*input.ServiceAccountRoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.CreateStackCallCount()).To(Equal(1)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IRSA has been deprecated; " + + "the recommended way to provide IAM permissions for \"aws-ebs-csi-driver\" addon is via pod identity associations; " + + "after addon creation is completed, run `eksctl utils migrate-to-pod-identity`")) + }, + }), - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-aws-efs-csi-driver")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: api.AWSEFSCSIDriverAddon, - })) - output, err := resourceSet.RenderJSON() + Entry("[RequiresIAMPermissions] IRSA set implicitly and not supportsPodIDs (aws-efs-csi-driver)", createAddonEntry{ + addon: api.Addon{ + Name: api.AWSEFSCSIDriverAddon, + }, + withOIDC: true, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + mockCFN: func(stackManager *fakes.FakeStackManager) { + stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + go func() { + c <- nil + }() + Expect(rsr).To(BeAssignableToTypeOf(&builder.IAMRoleResourceSet{})) + output, err := rsr.(*builder.IAMRoleResourceSet).RenderJSON() Expect(err).NotTo(HaveOccurred()) Expect(string(output)).To(ContainSubstring("PolicyEFSCSIController")) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal(api.AWSEFSCSIDriverAddon)) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(*createAddonInput.ServiceAccountRoleArn).To(Equal("role-arn")) - }) - }) - }) - }) - - When("attachPolicyARNs is configured", func() { - It("uses AttachPolicyARNS to create a role to attach to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - AttachPolicyARNs: []string{"arn-1"}, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-my-addon")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: "my-addon", - })) - output, err := resourceSet.RenderJSON() - Expect(err).NotTo(HaveOccurred()) - Expect(string(output)).To(ContainSubstring("arn-1")) - }) - }) - - When("wellKnownPolicies is configured", func() { - It("uses wellKnownPolicies to create a role to attach to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - WellKnownPolicies: api.WellKnownPolicies{ - AutoScaler: true, - }, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-my-addon")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: "my-addon", - })) - output, err := resourceSet.RenderJSON() - Expect(err).NotTo(HaveOccurred()) - Expect(string(output)).To(ContainSubstring("autoscaling:SetDesiredCapacity")) - }) - }) + rsr.(*builder.IAMRoleResourceSet).OutputRole = "arn:aws:iam::111122223333:role/role-name-1" + return nil + } + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).NotTo(BeNil()) + Expect(*input.ServiceAccountRoleArn).To(Equal("arn:aws:iam::111122223333:role/role-name-1")) + }, + validateCFNCalls: func(stackManager *fakes.FakeStackManager) { + Expect(stackManager.CreateStackCallCount()).To(Equal(1)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).NotTo(ContainSubstring("IRSA has been deprecated")) + }, + }), - When("AttachPolicy is configured", func() { - It("uses AttachPolicy to create a role to attach to the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - AttachPolicy: api.InlineDocument{ - "foo": "policy-bar", + Entry("[RequiresIAMPermissions] OIDC is disabled and IRSA set explicitly", createAddonEntry{ + addon: api.Addon{ + ServiceAccountRoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).To(BeNil()) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IRSA config is set for \"my-addon\" addon, " + + "but since OIDC is disabled on the cluster, eksctl cannot configure the requested permissions; " + + "the recommended way to provide IAM permissions for \"my-addon\" addon is via pod identity associations; " + + "after addon creation is completed, add all recommended policies to the config file, under `addon.PodIdentityAssociations`, " + + "and run `eksctl update addon`")) + }, + }), + + Entry("[RequiresIAMPermissions] OIDC is disabled and IRSA set implicitly", createAddonEntry{ + addon: api.Addon{ + Name: api.AWSEFSCSIDriverAddon, + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).To(BeNil()) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("recommended policies were found for \"aws-efs-csi-driver\" addon, " + + "but since OIDC is disabled on the cluster, eksctl cannot configure the requested permissions; " + + "users are responsible for attaching the policies to all nodegroup roles")) + }, + }), + + Entry("[RequiresIAMPermissions] neither IRSA nor podIDs are being set and NOT supportsPodIDs", createAddonEntry{ + addon: api.Addon{}, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).To(BeNil()) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IAM permissions are required for \"my-addon\" addon; " + + "the recommended way to provide IAM permissions for \"my-addon\" addon is via IRSA; " + + "after addon creation is completed, add all recommended policies to the config file, " + + "under `addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`, " + + "and run `eksctl update addon`")) + }, + }), + + Entry("[RequiresIAMPermissions] neither IRSA nor podIDs are being set and supportsPodIDs", createAddonEntry{ + addon: api.Addon{}, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{"sa1"}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + Expect(input.ServiceAccountRoleArn).To(BeNil()) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IAM permissions are required for \"my-addon\" addon; " + + "the recommended way to provide IAM permissions for \"my-addon\" addon is via pod identity associations; " + + "after addon creation is completed, add all recommended policies to the config file, " + + "under `addon.PodIdentityAssociations`, and run `eksctl update addon`")) + }, + }), + + Entry("[RequiresIAMPermissions is false] podIDs set", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "sa1", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, }, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(1)) - _, name, resourceSet, tags, _, _ := fakeStackManager.CreateStackArgsForCall(0) - Expect(name).To(Equal("eksctl-my-cluster-addon-my-addon")) - Expect(resourceSet).NotTo(BeNil()) - Expect(tags).To(Equal(map[string]string{ - api.AddonNameTag: "my-addon", - })) - output, err := resourceSet.RenderJSON() - Expect(err).NotTo(HaveOccurred()) - Expect(string(output)).To(ContainSubstring("policy-bar")) - }) - }) - - When("serviceAccountRoleARN is configured", func() { - It("uses the serviceAccountRoleARN to create the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - ServiceAccountRoleARN: "foo", - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(*createAddonInput.ServiceAccountRoleArn).To(Equal("foo")) - }) - }) + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.PodIdentityAssociations).To(HaveLen(0)) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IAM permissions are not required for \"my-addon\" addon; " + + "any IRSA configuration or pod identity associations will be ignored")) + }, + }), - When("tags are configured", func() { - It("uses the Tags to create the addon", func() { - err := manager.Create(context.Background(), &api.Addon{ - Name: "my-addon", - Version: "v1.0.0-eksbuild.1", - Tags: map[string]string{"foo": "bar", "fox": "brown"}, - }, nil, 0) - Expect(err).NotTo(HaveOccurred()) - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(0)) - Expect(*createAddonInput.ClusterName).To(Equal("my-cluster")) - Expect(*createAddonInput.AddonName).To(Equal("my-addon")) - Expect(*createAddonInput.AddonVersion).To(Equal("v1.0.0-eksbuild.1")) - Expect(createAddonInput.Tags["foo"]).To(Equal("bar")) - Expect(createAddonInput.Tags["fox"]).To(Equal("brown")) - }) - }) + Entry("[RequiresIAMPermissions is false] IRSA set", createAddonEntry{ + addon: api.Addon{ + Version: "1.0.0", + ServiceAccountRoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(mockProvider.MockEKS(), nil) + mockDescribeAddonVersions(provider.MockEKS(), nil) + mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) + mockCreateAddon(mockProvider.MockEKS(), nil) + }, + validateCreateAddonInput: func(input *awseks.CreateAddonInput) { + Expect(input.ServiceAccountRoleArn).To(BeNil()) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("IAM permissions are not required for \"my-addon\" addon; " + + "any IRSA configuration or pod identity associations will be ignored")) + }, + }), + ) }) diff --git a/pkg/actions/addon/delete.go b/pkg/actions/addon/delete.go index 3324605c45..c545583fa4 100644 --- a/pkg/actions/addon/delete.go +++ b/pkg/actions/addon/delete.go @@ -31,17 +31,19 @@ func (a *Manager) Delete(ctx context.Context, addon *api.Addon) error { logger.Info("deleted addon: %s", addon.Name) } - deleteAddonIAMTasks, err := NewRemover(a.stackManager).DeleteAddonIAMTasksFiltered(ctx, addon.Name, true) + deleteAddonIAMTasks, err := NewRemover(a.stackManager).DeleteAddonIAMTasksFiltered(ctx, addon.Name, false) if err != nil { return err } if deleteAddonIAMTasks.Len() > 0 { logger.Info("deleting associated IAM stack(s)") - runAllTasks(deleteAddonIAMTasks) + if err := runAllTasks(deleteAddonIAMTasks); err != nil { + return err + } } else if addonExists { logger.Info("no associated IAM stacks found") } else { - return errors.New("could not find addon or associated IAM stacks to delete") + return errors.New("could not find addon or associated IAM stack(s) to delete") } return nil diff --git a/pkg/actions/addon/delete_test.go b/pkg/actions/addon/delete_test.go index c099a9b6dc..d0b05b1fa9 100644 --- a/pkg/actions/addon/delete_test.go +++ b/pkg/actions/addon/delete_test.go @@ -9,14 +9,11 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" awseks "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/aws/smithy-go" "github.com/stretchr/testify/mock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/pkg/errors" - "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/addon/fakes" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" @@ -51,16 +48,42 @@ var _ = Describe("Delete", func() { ClusterName: aws.String("my-cluster"), }).Return(&awseks.DeleteAddonOutput{}, nil) - fakeStackManager.DescribeStackReturns(&types.Stack{StackName: aws.String("eksctl-my-cluster-addon-my-addon")}, nil) + fakeStackManager.GetIAMAddonsStacksReturns([]*types.Stack{ + { + StackName: aws.String("eksctl-my-cluster-addon-my-addon"), + Tags: []types.Tag{{Key: aws.String(api.AddonNameTag), Value: aws.String("my-addon")}}, + }, + { + StackName: aws.String("eksctl-my-cluster-addon-my-addon-podidentityrole-sa1"), + Tags: []types.Tag{{Key: aws.String(api.AddonNameTag), Value: aws.String("my-addon")}}, + }, + { + StackName: aws.String("eksctl-my-cluster-addon-my-addon-podidentityrole-sa2"), + Tags: []types.Tag{{Key: aws.String(api.AddonNameTag), Value: aws.String("my-addon")}}, + }, + { + StackName: aws.String("eksctl-my-cluster-addon-not-my-addon"), + Tags: []types.Tag{{Key: aws.String(api.AddonNameTag), Value: aws.String("not-my-addon")}}, + }, + }, nil) err := manager.Delete(context.Background(), &api.Addon{ Name: "my-addon", }) Expect(err).NotTo(HaveOccurred()) - Expect(fakeStackManager.DeleteStackBySpecCallCount()).To(Equal(1)) - _, stack := fakeStackManager.DeleteStackBySpecArgsForCall(0) - Expect(*stack.StackName).To(Equal("eksctl-my-cluster-addon-my-addon")) + Expect(fakeStackManager.DeleteStackBySpecCallCount()).To(Equal(3)) + stackNames := []string{} + for i := 0; i <= 2; i++ { + _, stack := fakeStackManager.DeleteStackBySpecArgsForCall(i) + stackNames = append(stackNames, *stack.StackName) + + } + Expect(stackNames).To(ConsistOf( + "eksctl-my-cluster-addon-my-addon", + "eksctl-my-cluster-addon-my-addon-podidentityrole-sa1", + "eksctl-my-cluster-addon-my-addon-podidentityrole-sa2", + )) }) When("delete addon fails", func() { @@ -78,20 +101,20 @@ var _ = Describe("Delete", func() { }) }) - When("list stacks fails", func() { + When("fetching stacks fails", func() { It("only deletes the addon", func() { mockProvider.MockEKS().On("DeleteAddon", mock.Anything, &awseks.DeleteAddonInput{ AddonName: aws.String("my-addon"), ClusterName: aws.String("my-cluster"), }).Return(&awseks.DeleteAddonOutput{}, nil) - fakeStackManager.DescribeStackReturns(nil, fmt.Errorf("foo")) + fakeStackManager.GetIAMAddonsStacksReturns(nil, fmt.Errorf("foo")) err := manager.Delete(context.Background(), &api.Addon{ Name: "my-addon", }) - Expect(err).To(MatchError("failed to get stack: foo")) + Expect(err).To(MatchError(ContainSubstring("failed to fetch addons stacks"))) }) }) @@ -102,16 +125,19 @@ var _ = Describe("Delete", func() { ClusterName: aws.String("my-cluster"), }).Return(&awseks.DeleteAddonOutput{}, nil) - fakeStackManager.DeleteStackBySpecReturns(nil, fmt.Errorf("foo")) - fakeStackManager.DescribeStackReturns(&types.Stack{ - StackName: aws.String("eksctl-my-cluster-addon-my-addon"), + fakeStackManager.GetIAMAddonsStacksReturns([]*types.Stack{ + { + StackName: aws.String("eksctl-my-cluster-addon-my-addon"), + Tags: []types.Tag{{Key: aws.String(api.AddonNameTag), Value: aws.String("my-addon")}}, + }, }, nil) + fakeStackManager.DeleteStackBySpecReturns(nil, fmt.Errorf("foo")) err := manager.Delete(context.Background(), &api.Addon{ Name: "my-addon", }) - Expect(err).To(MatchError(`failed to delete cloudformation stack "eksctl-my-cluster-addon-my-addon": foo`)) + Expect(err).To(MatchError(`deleting addon IAM "eksctl-my-cluster-addon-my-addon": foo`)) Expect(fakeStackManager.DeleteStackBySpecCallCount()).To(Equal(1)) _, stack := fakeStackManager.DeleteStackBySpecArgsForCall(0) Expect(*stack.StackName).To(Equal("eksctl-my-cluster-addon-my-addon")) @@ -125,9 +151,7 @@ var _ = Describe("Delete", func() { ClusterName: aws.String("my-cluster"), }).Return(&awseks.DeleteAddonOutput{}, nil) - fakeStackManager.DescribeStackReturns(nil, errors.Wrap(&smithy.OperationError{ - Err: fmt.Errorf("ValidationError"), - }, "nope")) + fakeStackManager.GetIAMAddonsStacksReturns([]*types.Stack{}, nil) err := manager.Delete(context.Background(), &api.Addon{ Name: "my-addon", @@ -146,7 +170,11 @@ var _ = Describe("Delete", func() { ClusterName: aws.String("my-cluster"), }).Return(&awseks.DeleteAddonOutput{}, &ekstypes.ResourceNotFoundException{}) - fakeStackManager.DescribeStackReturns(&types.Stack{StackName: aws.String("eksctl-my-cluster-addon-my-addon")}, nil) + fakeStackManager.GetIAMAddonsStacksReturns([]*types.Stack{ + { + StackName: aws.String("eksctl-my-cluster-addon-my-addon"), + Tags: []types.Tag{{Key: aws.String(api.AddonNameTag), Value: aws.String("my-addon")}}, + }}, nil) err := manager.Delete(context.Background(), &api.Addon{ Name: "my-addon", @@ -167,14 +195,12 @@ var _ = Describe("Delete", func() { ClusterName: aws.String("my-cluster"), }).Return(&awseks.DeleteAddonOutput{}, &ekstypes.ResourceNotFoundException{}) - fakeStackManager.DescribeStackReturns(nil, errors.Wrap(&smithy.OperationError{ - Err: fmt.Errorf("ValidationError"), - }, "nope")) + fakeStackManager.GetIAMAddonsStacksReturns([]*types.Stack{}, nil) err := manager.Delete(context.Background(), &api.Addon{ Name: "my-addon", }) - Expect(err).To(MatchError("could not find addon or associated IAM stack to delete")) + Expect(err).To(MatchError("could not find addon or associated IAM stack(s) to delete")) Expect(fakeStackManager.DeleteStackBySpecCallCount()).To(Equal(0)) }) }) diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go index b151fa099a..64436da34f 100644 --- a/pkg/actions/addon/podidentityassociation.go +++ b/pkg/actions/addon/podidentityassociation.go @@ -3,6 +3,7 @@ package addon import ( "context" "fmt" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" diff --git a/pkg/actions/podidentityassociation/addon_migrator.go b/pkg/actions/podidentityassociation/addon_migrator.go index f5865effcc..ab12aa48af 100644 --- a/pkg/actions/podidentityassociation/addon_migrator.go +++ b/pkg/actions/podidentityassociation/addon_migrator.go @@ -155,6 +155,9 @@ func (a *AddonMigrator) getRoleServiceAccount(ctx context.Context, roleName stri role, err := a.IAMRoleGetter.GetRole(ctx, &iam.GetRoleInput{ RoleName: aws.String(roleName), }) + if err != nil { + return "", err + } assumeRolePolicyDoc, err := url.PathUnescape(*role.Role.AssumeRolePolicyDocument) if err != nil { diff --git a/pkg/actions/podidentityassociation/iam_role_updater.go b/pkg/actions/podidentityassociation/iam_role_updater.go index 63167a233d..d1a56ec351 100644 --- a/pkg/actions/podidentityassociation/iam_role_updater.go +++ b/pkg/actions/podidentityassociation/iam_role_updater.go @@ -9,11 +9,12 @@ import ( "github.com/kris-nova/logger" "github.com/pkg/errors" + "time" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/builder" "github.com/weaveworks/eksctl/pkg/cfn/manager" "golang.org/x/exp/slices" - "time" ) // IAMRoleUpdater updates IAM resources for pod identity associations. diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index 631abee8ec..b4b6e78051 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -6,6 +6,7 @@ import ( "strings" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/kris-nova/logger" "sigs.k8s.io/yaml" ) @@ -73,9 +74,9 @@ func (a Addon) Validate() error { } } - if a.HasIRSAPoliciesSet() { + if a.HasIRSASet() { if a.HasPodIDsSet() { - return invalidAddonConfigErr("cannot set IRSA config (`addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time") + return invalidAddonConfigErr("cannot set IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time") } if err := a.checkAtMostOnePolicyProviderIsSet(); err != nil { return invalidAddonConfigErr(err.Error()) @@ -89,6 +90,9 @@ func (a Addon) Validate() error { for i, pia := range *a.PodIdentityAssociations { path := fmt.Sprintf("podIdentityAssociations[%d]", i) + if pia.Namespace != "" { + logger.Warning("setting %s.namespace has no effect, as EKS API is responsible for automatically resolving the K8s namespace when creating/updating an addon with pod identity associations") + } if pia.ServiceAccountName == "" { return invalidAddonConfigErr(fmt.Sprintf("%s.serviceAccountName must be set", path)) } @@ -162,16 +166,7 @@ func (a Addon) HasIRSAPoliciesSet() bool { } func (a Addon) HasIRSASet() bool { - return a.ServiceAccountRoleARN != "" || a.HasIRSAPoliciesSet() || a.hasIRSARecommendedPolicies() -} - -func (a Addon) hasIRSARecommendedPolicies() bool { - switch a.CanonicalName() { - case VPCCNIAddon, AWSEBSCSIDriverAddon, AWSEFSCSIDriverAddon: - return true - default: - return false - } + return a.ServiceAccountRoleARN != "" || a.HasIRSAPoliciesSet() } func (a Addon) HasPodIDsSet() bool { diff --git a/pkg/apis/eksctl.io/v1alpha5/addon_test.go b/pkg/apis/eksctl.io/v1alpha5/addon_test.go index 414e4f71ec..4136c0b1a7 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon_test.go @@ -1,24 +1,26 @@ package v1alpha5_test import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" ) var _ = Describe("Addon", func() { Describe("Validating configuration", func() { When("name is not set", func() { It("errors", func() { - err := v1alpha5.Addon{}.Validate() - Expect(err).To(MatchError("name is required")) + err := api.Addon{}.Validate() + Expect(err).To(MatchError(ContainSubstring("name is required"))) }) }) DescribeTable("when configurationValues is in invalid format", func(configurationValues string) { - err := v1alpha5.Addon{ + err := api.Addon{ Name: "name", Version: "version", ConfigurationValues: configurationValues, @@ -31,7 +33,7 @@ var _ = Describe("Addon", func() { DescribeTable("when configurationValues is in valid format", func(configurationValues string) { - err := v1alpha5.Addon{ + err := api.Addon{ Name: "name", Version: "version", ConfigurationValues: configurationValues, @@ -44,17 +46,17 @@ var _ = Describe("Addon", func() { Entry("non-empty yaml", "replicaCount: 3"), ) - When("specifying more than one of serviceAccountRoleARN, attachPolicyARNs, attachPolicy", func() { + When("specifying more than one of serviceAccountRoleARN, attachPolicyARNs, attachPolicy, wellKnownPolicies", func() { It("errors", func() { - err := v1alpha5.Addon{ + err := api.Addon{ Name: "name", Version: "version", ServiceAccountRoleARN: "foo", AttachPolicyARNs: []string{"arn"}, }.Validate() - Expect(err).To(MatchError("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified")) + Expect(err).To(MatchError(ContainSubstring("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified"))) - err = v1alpha5.Addon{ + err = api.Addon{ Name: "name", Version: "version", AttachPolicy: map[string]interface{}{ @@ -62,9 +64,9 @@ var _ = Describe("Addon", func() { }, AttachPolicyARNs: []string{"arn"}, }.Validate() - Expect(err).To(MatchError("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified")) + Expect(err).To(MatchError(ContainSubstring("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified"))) - err = v1alpha5.Addon{ + err = api.Addon{ Name: "name", Version: "version", ServiceAccountRoleARN: "foo", @@ -72,20 +74,178 @@ var _ = Describe("Addon", func() { "foo": "bar", }, }.Validate() - Expect(err).To(MatchError("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified")) + Expect(err).To(MatchError(ContainSubstring("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified"))) - err = v1alpha5.Addon{ + err = api.Addon{ Name: "name", Version: "version", - WellKnownPolicies: v1alpha5.WellKnownPolicies{ + WellKnownPolicies: api.WellKnownPolicies{ AutoScaler: true, }, AttachPolicy: map[string]interface{}{ "foo": "bar", }, }.Validate() - Expect(err).To(MatchError("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified")) + Expect(err).To(MatchError(ContainSubstring("at most one of wellKnownPolicies, serviceAccountRoleARN, attachPolicyARNs and attachPolicy can be specified"))) }) }) + + type addonWithPodIDEntry struct { + addon api.Addon + expectedErr string + } + DescribeTable("pod identity associations", func(e addonWithPodIDEntry) { + err := e.addon.Validate() + if e.expectedErr != "" { + Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) + } else { + Expect(err).NotTo(HaveOccurred()) + } + }, + Entry("setting podIDs for eks-pod-identity-agent addon", addonWithPodIDEntry{ + addon: api.Addon{ + Name: api.PodIdentityAgentAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{{}}, + }, + expectedErr: "cannot set pod identity associtations for \"eks-pod-identity-agent\" addon", + }), + Entry("service account name is not set", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + PodIdentityAssociations: &[]api.PodIdentityAssociation{{}}, + }, + expectedErr: "podIdentityAssociations[0].serviceAccountName must be set", + }), + Entry("no IAM role or policies are set", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + }, + }, + }, + expectedErr: fmt.Sprintf("at least one of the following must be specified: %[1]s.roleARN, %[1]s.permissionPolicy, %[1]s.permissionPolicyARNs, %[1]s.wellKnownPolicies", "podIdentityAssociations[0]"), + }), + Entry("IAM role and permissionPolicy are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + PermissionPolicy: api.InlineDocument{ + "Version": "2012-10-17", + "Statement": []map[string]interface{}{ + { + "Effect": "Allow", + "Action": []string{ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:DescribeAutoScalingInstances", + }, + "Resource": "*", + }, + }, + }, + }, + }, + }, + expectedErr: fmt.Sprintf("%[1]s.permissionPolicy cannot be specified when %[1]s.roleARN is set", "podIdentityAssociations[0]"), + }), + Entry("IAM role and permissionPolicyARNs are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + PermissionPolicyARNs: []string{"arn:aws:iam::111122223333:policy/policy-name-1"}, + }, + }, + }, + expectedErr: fmt.Sprintf("%[1]s.permissionPolicyARNs cannot be specified when %[1]s.roleARN is set", "podIdentityAssociations[0]"), + }), + Entry("IAM role and permissionPolicyARNs are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + WellKnownPolicies: api.WellKnownPolicies{ + EBSCSIController: true, + }, + }, + }, + }, + expectedErr: fmt.Sprintf("%[1]s.wellKnownPolicies cannot be specified when %[1]s.roleARN is set", "podIdentityAssociations[0]"), + }), + Entry("podIDs and ServiceAccountRoleARN are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + ServiceAccountRoleARN: "arn:aws:iam::111122223333:role/role-name-1", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + }, + }, + expectedErr: "cannot set IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time", + }), + Entry("podIDs and AttachPolicyARNs are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + AttachPolicyARNs: []string{"arn:aws:iam::111122223333:policy/policy-name-1"}, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + }, + }, + expectedErr: "cannot set IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time", + }), + Entry("podIDs and AttachPolicy are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + AttachPolicy: api.InlineDocument{ + "Version": "2012-10-17", + "Statement": []map[string]interface{}{ + { + "Effect": "Allow", + "Action": []string{ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:DescribeAutoScalingInstances", + }, + "Resource": "*", + }, + }, + }, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + }, + }, + expectedErr: "cannot set IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time", + }), + Entry("podIDs and WellKnownPolicies are set at the same time", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + WellKnownPolicies: api.WellKnownPolicies{ + EBSCSIController: true, + }, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::111122223333:role/role-name-1", + }, + }, + }, + expectedErr: "cannot set IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy`, `addon.WellKnownPolicies`) and pod identity associations at the same time", + }), + ) }) }) diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index e3eb0a08c3..09d2e75ac0 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -189,6 +189,14 @@ "description": "ARN of the permissions' boundary to associate", "x-intellij-html-description": "ARN of the permissions' boundary to associate" }, + "podIdentityAssociations": { + "items": { + "$ref": "#/definitions/PodIdentityAssociation" + }, + "type": "array", + "description": "holds a list of associations to be configured for the addon", + "x-intellij-html-description": "holds a list of associations to be configured for the addon" + }, "publishers": { "items": { "type": "string" @@ -237,6 +245,7 @@ "wellKnownPolicies", "tags", "resolveConflicts", + "podIdentityAssociations", "configurationValues", "publishers", "types", @@ -500,6 +509,12 @@ }, "ClusterIAM": { "properties": { + "autoCreatePodIdentityAssociations": { + "type": "boolean", + "description": "specifies whether or not to automatically create pod identity associations for supported addons that require IAM permissions", + "x-intellij-html-description": "specifies whether or not to automatically create pod identity associations for supported addons that require IAM permissions", + "default": "false" + }, "fargatePodExecutionRoleARN": { "type": "string", "description": "role used by pods to access AWS APIs. This role is added to the Kubernetes RBAC for authorization. See [Pod Execution Role](https://docs.aws.amazon.com/eks/latest/userguide/pod-execution-role.html)", @@ -553,6 +568,7 @@ "fargatePodExecutionRolePermissionsBoundary", "withOIDC", "serviceAccounts", + "autoCreatePodIdentityAssociations", "podIdentityAssociations", "vpcResourceControllerPolicy" ], diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index d30274dafe..8e27ef6ecf 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -11,7 +11,7 @@ import ( // Commonly-used constants const ( AnnotationEKSRoleARN = "eks.amazonaws.com/role-arn" - EKSServicePrincipal = "beta.pods.eks.aws.internal" + EKSServicePrincipal = "pods.eks.amazonaws.com" ) var EKSServicePrincipalTrustStatement = IAMStatement{ diff --git a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go index 22e634f07b..590ce81cb2 100644 --- a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go +++ b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go @@ -200,6 +200,17 @@ func (in *Addon) DeepCopyInto(out *Addon) { (*out)[key] = val } } + if in.PodIdentityAssociations != nil { + in, out := &in.PodIdentityAssociations, &out.PodIdentityAssociations + *out = new([]PodIdentityAssociation) + if **in != nil { + in, out := *in, *out + *out = make([]PodIdentityAssociation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + } if in.Publishers != nil { in, out := &in.Publishers, &out.Publishers *out = make([]string, len(*in)) From 5a58bc4821e62d52798e71d265d1ad28bab69222 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Fri, 10 May 2024 15:52:11 +0530 Subject: [PATCH 19/35] Migrate: ignore pod identity associations that already exist Fixes #7753 --- pkg/actions/podidentityassociation/creator.go | 15 ++++++++------- .../podidentityassociation/migrator.go | 2 +- pkg/actions/podidentityassociation/tasks.go | 19 ++++++++++++++----- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/pkg/actions/podidentityassociation/creator.go b/pkg/actions/podidentityassociation/creator.go index 3445a74dca..ef43fb3ce7 100644 --- a/pkg/actions/podidentityassociation/creator.go +++ b/pkg/actions/podidentityassociation/creator.go @@ -38,10 +38,10 @@ func NewCreator(clusterName string, stackCreator StackCreator, eksAPI awsapi.EKS } func (c *Creator) CreatePodIdentityAssociations(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) error { - return runAllTasks(c.CreateTasks(ctx, podIdentityAssociations)) + return runAllTasks(c.CreateTasks(ctx, podIdentityAssociations, false)) } -func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) *tasks.TaskTree { +func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, ignorePodIdentityExistsErr bool) *tasks.TaskTree { taskTree := &tasks.TaskTree{ Parallel: true, } @@ -83,11 +83,12 @@ func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api }) } piaCreationTasks.Append(&createPodIdentityAssociationTask{ - ctx: ctx, - info: fmt.Sprintf("create pod identity association for service account %q", pia.NameString()), - clusterName: c.clusterName, - podIdentityAssociation: &pia, - eksAPI: c.eksAPI, + ctx: ctx, + info: fmt.Sprintf("create pod identity association for service account %q", pia.NameString()), + clusterName: c.clusterName, + podIdentityAssociation: &pia, + eksAPI: c.eksAPI, + ignorePodIdentityExistsErr: ignorePodIdentityExistsErr, }) taskTree.Append(piaCreationTasks) } diff --git a/pkg/actions/podidentityassociation/migrator.go b/pkg/actions/podidentityassociation/migrator.go index 5d36f8838a..c40930402d 100644 --- a/pkg/actions/podidentityassociation/migrator.go +++ b/pkg/actions/podidentityassociation/migrator.go @@ -199,7 +199,7 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity } // add tasks to create pod identity associations - createAssociationsTasks := NewCreator(m.clusterName, nil, m.eksAPI, m.clientSet).CreateTasks(ctx, toBeCreated) + createAssociationsTasks := NewCreator(m.clusterName, nil, m.eksAPI, m.clientSet).CreateTasks(ctx, toBeCreated, true) if createAssociationsTasks.Len() > 0 { createAssociationsTasks.IsSubTask = true taskTree.Append(createAssociationsTasks) diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index 93be4e85cb..68fe14b323 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" awseks "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" awsiam "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/kris-nova/logger" @@ -23,11 +24,12 @@ import ( ) type createPodIdentityAssociationTask struct { - ctx context.Context - info string - clusterName string - podIdentityAssociation *api.PodIdentityAssociation - eksAPI awsapi.EKS + ctx context.Context + info string + clusterName string + podIdentityAssociation *api.PodIdentityAssociation + eksAPI awsapi.EKS + ignorePodIdentityExistsErr bool } func (t *createPodIdentityAssociationTask) Describe() string { @@ -44,6 +46,13 @@ func (t *createPodIdentityAssociationTask) Do(errorCh chan error) error { ServiceAccount: &t.podIdentityAssociation.ServiceAccountName, Tags: t.podIdentityAssociation.Tags, }); err != nil { + if t.ignorePodIdentityExistsErr { + var inUseErr *ekstypes.ResourceInUseException + if errors.As(err, &inUseErr) { + logger.Info("pod identity association %s already exists", t.podIdentityAssociation.NameString()) + return nil + } + } return fmt.Errorf( "creating pod identity association for service account %q in namespace %q: %w", t.podIdentityAssociation.ServiceAccountName, t.podIdentityAssociation.Namespace, err) From 0afd1ac32b5ea19eae1eb22d8ed9d4606ce6752d Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Tue, 14 May 2024 14:55:00 +0300 Subject: [PATCH 20/35] add docs && tweak validation --- .../podidentityassociation/addon_migrator.go | 2 +- .../podidentityassociation/migrator.go | 5 +- pkg/apis/eksctl.io/v1alpha5/addon.go | 5 +- pkg/apis/eksctl.io/v1alpha5/addon_test.go | 19 +- pkg/ctl/cmdutils/configfile.go | 6 + pkg/ctl/update/addon.go | 1 + pkg/ctl/utils/migrate_to_pod_identity.go | 5 +- userdocs/src/getting-started.md | 2 + userdocs/src/usage/addons.md | 11 +- userdocs/src/usage/iamserviceaccounts.md | 3 + .../src/usage/pod-identity-associations.md | 213 ++++++++++++++++-- userdocs/theme/home.html | 1 + 12 files changed, 243 insertions(+), 30 deletions(-) diff --git a/pkg/actions/podidentityassociation/addon_migrator.go b/pkg/actions/podidentityassociation/addon_migrator.go index ab12aa48af..ed2c273a4a 100644 --- a/pkg/actions/podidentityassociation/addon_migrator.go +++ b/pkg/actions/podidentityassociation/addon_migrator.go @@ -81,7 +81,7 @@ func (a *AddonMigrator) migrateAddon(ctx context.Context, addon *ekstypes.Addon, return nil, nil } - logger.Info("migrating addon %s with service account %s to pod identity; OIDC provider trust relationship will also be removed", *addon.AddonName, *addon.ServiceAccountRoleArn) + logger.Info("migrating addon %s with serviceAccountRoleARN %s to pod identity; OIDC provider trust relationship will also be removed", *addon.AddonName, *addon.ServiceAccountRoleArn) roleName, err := api.RoleNameFromARN(serviceAccountRoleARN) if err != nil { return nil, fmt.Errorf("parsing role ARN %s: %w", serviceAccountRoleARN, err) diff --git a/pkg/actions/podidentityassociation/migrator.go b/pkg/actions/podidentityassociation/migrator.go index c40930402d..b762e89aca 100644 --- a/pkg/actions/podidentityassociation/migrator.go +++ b/pkg/actions/podidentityassociation/migrator.go @@ -27,9 +27,8 @@ type AddonCreator interface { type PodIdentityMigrationOptions struct { RemoveOIDCProviderTrustRelationship bool - // SkipAgentInstallation bool - Approve bool - Timeout time.Duration + Approve bool + Timeout time.Duration } type Migrator struct { diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index b4b6e78051..0e2ac15a9f 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -6,7 +6,6 @@ import ( "strings" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/kris-nova/logger" "sigs.k8s.io/yaml" ) @@ -90,8 +89,8 @@ func (a Addon) Validate() error { for i, pia := range *a.PodIdentityAssociations { path := fmt.Sprintf("podIdentityAssociations[%d]", i) - if pia.Namespace != "" { - logger.Warning("setting %s.namespace has no effect, as EKS API is responsible for automatically resolving the K8s namespace when creating/updating an addon with pod identity associations") + if pia.Namespace == "" { + return invalidAddonConfigErr(fmt.Sprintf("%s.namespace must be set", path)) } if pia.ServiceAccountName == "" { return invalidAddonConfigErr(fmt.Sprintf("%s.serviceAccountName must be set", path)) diff --git a/pkg/apis/eksctl.io/v1alpha5/addon_test.go b/pkg/apis/eksctl.io/v1alpha5/addon_test.go index 4136c0b1a7..ca04420825 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon_test.go @@ -109,11 +109,20 @@ var _ = Describe("Addon", func() { }, expectedErr: "cannot set pod identity associtations for \"eks-pod-identity-agent\" addon", }), - Entry("service account name is not set", addonWithPodIDEntry{ + Entry("namespace is not set", addonWithPodIDEntry{ addon: api.Addon{ Name: "name", PodIdentityAssociations: &[]api.PodIdentityAssociation{{}}, }, + expectedErr: "podIdentityAssociations[0].namespace must be set", + }), + Entry("service account name is not set", addonWithPodIDEntry{ + addon: api.Addon{ + Name: "name", + PodIdentityAssociations: &[]api.PodIdentityAssociation{{ + Namespace: "kube-system", + }}, + }, expectedErr: "podIdentityAssociations[0].serviceAccountName must be set", }), Entry("no IAM role or policies are set", addonWithPodIDEntry{ @@ -121,6 +130,7 @@ var _ = Describe("Addon", func() { Name: "name", PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", }, }, @@ -132,6 +142,7 @@ var _ = Describe("Addon", func() { Name: "name", PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", PermissionPolicy: api.InlineDocument{ @@ -157,6 +168,7 @@ var _ = Describe("Addon", func() { Name: "name", PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", PermissionPolicyARNs: []string{"arn:aws:iam::111122223333:policy/policy-name-1"}, @@ -170,6 +182,7 @@ var _ = Describe("Addon", func() { Name: "name", PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", WellKnownPolicies: api.WellKnownPolicies{ @@ -186,6 +199,7 @@ var _ = Describe("Addon", func() { ServiceAccountRoleARN: "arn:aws:iam::111122223333:role/role-name-1", PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", }, @@ -199,6 +213,7 @@ var _ = Describe("Addon", func() { AttachPolicyARNs: []string{"arn:aws:iam::111122223333:policy/policy-name-1"}, PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", }, @@ -224,6 +239,7 @@ var _ = Describe("Addon", func() { }, PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", }, @@ -239,6 +255,7 @@ var _ = Describe("Addon", func() { }, PodIdentityAssociations: &[]api.PodIdentityAssociation{ { + Namespace: "kube-system", ServiceAccountName: "aws-node", RoleARN: "arn:aws:iam::111122223333:role/role-name-1", }, diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 2cdd80066d..5b6316f876 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -303,6 +303,12 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. } } + for _, addon := range clusterConfig.Addons { + if err := addon.Validate(); err != nil { + return err + } + } + shallCreatePodIdentityAssociations := func(cfg *api.ClusterConfig) bool { if cfg.IAM != nil && len(cfg.IAM.PodIdentityAssociations) > 0 { return true diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index eaaf2ddc37..4b1def2e3a 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -3,6 +3,7 @@ package update import ( "context" "fmt" + awseks "github.com/aws/aws-sdk-go-v2/service/eks" "github.com/kris-nova/logger" diff --git a/pkg/ctl/utils/migrate_to_pod_identity.go b/pkg/ctl/utils/migrate_to_pod_identity.go index 7a072150f0..799241101b 100644 --- a/pkg/ctl/utils/migrate_to_pod_identity.go +++ b/pkg/ctl/utils/migrate_to_pod_identity.go @@ -18,15 +18,12 @@ func migrateToPodIdentityCmd(cmd *cmdutils.Cmd) { cfg := api.NewClusterConfig() cmd.ClusterConfig = cfg - cmd.SetDescription("migrate-to-pod-identity", "Updates the authentication mode for a cluster", "") + cmd.SetDescription("migrate-to-pod-identity", "Migrates all IRSA related config for a cluster to an equivalent pod identity associations config", "") var options podidentityassociation.PodIdentityMigrationOptions cmd.FlagSetGroup.InFlagSet("Authentication mode", func(fs *pflag.FlagSet) { fs.BoolVar(&options.RemoveOIDCProviderTrustRelationship, "remove-oidc-provider-trust-relationship", false, "Remove existing IRSAv1 OIDC provided entities") fs.BoolVar(&options.Approve, "approve", false, "Apply the changes") - - // fs.BoolVar(&options.SkipAgentInstallation, "skip-agent-installation", false, "Skip installing pod-identity-agent addon") - // cmdutils.AddIAMServiceAccountFilterFlags(fs, &cmd.Include, &cmd.Exclude) }) cmd.FlagSetGroup.InFlagSet("General", func(fs *pflag.FlagSet) { diff --git a/userdocs/src/getting-started.md b/userdocs/src/getting-started.md index c8bbce0af3..ef8f1c8df5 100644 --- a/userdocs/src/getting-started.md +++ b/userdocs/src/getting-started.md @@ -1,6 +1,8 @@ # Getting started !!! tip "New for 2024" + EKS Add-ons now support receiving IAM permissions via [EKS Pod Identity Associations](/usage/pod-identity-associations/#eks-add-ons-support-for-pod-identity-associations) + `eksctl` now supports AMIs based on AmazonLinux2023 !!! tip "eksctl main features in 2023" diff --git a/userdocs/src/usage/addons.md b/userdocs/src/usage/addons.md index 415c6996b2..ec27d6296c 100644 --- a/userdocs/src/usage/addons.md +++ b/userdocs/src/usage/addons.md @@ -4,9 +4,12 @@ EKS Add-Ons is a new feature that lets you enable and manage Kubernetes operatio software for your AWS EKS clusters. At launch, EKS add-ons supports controlling the launch and version of the AWS VPC CNI plugin through the EKS API -## Creating addons +## Creating addons (and providing IAM permissions via IRSA) -In your config file, you can specify the addons you want and (if required) the policies to attach to them: +!!! tip "New for 2024" +EKS Add-ons now support receiving IAM permissions, required to connect with AWS services outside of cluster, via [EKS Pod Identity Associations](/usage/pod-identity-associations/#eks-add-ons-support-for-pod-identity-associations) + +In your config file, you can specify the addons you want and (if required) the role or policies to attach to them: ```yaml apiVersion: eksctl.io/v1alpha5 @@ -24,7 +27,7 @@ addons: version: 1.7.5 tags: team: eks - # you can specify at most one of + # you can specify at most one of: attachPolicyARNs: - arn:aws:iam::account:policy/AmazonEKS_CNI_Policy # or @@ -103,7 +106,7 @@ eksctl get addons -f config.yaml ## Setting the addon's version -Setting the version of the addon is optional. If the `version` field is empty in the request sent by `eksctl`, the EKS API will set it to the default version for that specific addon. More information about which version is the default version for specific addons can be found in the AWS documentation about EKS. Note that the default version might not necessarily be the latest version available. +Setting the version of the addon is optional. If the `version` field is left empty `eksctl` will resolve the default version for the addon. More information about which version is the default version for specific addons can be found in the AWS documentation about EKS. Note that the default version might not necessarily be the latest version available. The addon version can be set to `latest`. Alternatively, the version can be set with the EKS build tag specified, such as `v1.7.5-eksbuild.1` or `v1.7.5-eksbuild.2`. It can also be set to the release version of the addon, such as `v1.7.5` or `1.7.5`, and the `eksbuild` suffix tag will be discovered and set for you. diff --git a/userdocs/src/usage/iamserviceaccounts.md b/userdocs/src/usage/iamserviceaccounts.md index 392c3b1fd2..4dcd72f920 100644 --- a/userdocs/src/usage/iamserviceaccounts.md +++ b/userdocs/src/usage/iamserviceaccounts.md @@ -1,5 +1,8 @@ # IAM Roles for Service Accounts +!!! tip "check out newer, improved feature" + `eksctl` supports configuring fine-grained permissions to EKS running apps via [EKS Pod Identity Associations](/usage/pod-identity-associations) + ## Introduction Amazon EKS supports [IAM Roles for Service Accounts (IRSA)][eks-user-guide] that allows cluster operators to map AWS IAM Roles to Kubernetes Service Accounts. diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md index cdd54fa939..2c51d27eeb 100644 --- a/userdocs/src/usage/pod-identity-associations.md +++ b/userdocs/src/usage/pod-identity-associations.md @@ -2,13 +2,13 @@ ## Introduction -AWS EKS has introduced a new enhanced mechanism called Pod Identity Association for cluster administrators to configure Kubernetes applications to receive IAM permissions required to connect with AWS services outside of the cluster. Pod Identity Association leverages IRSA however, it makes it configurable directly through EKS API, eliminating the need for using IAM API altogether. +AWS EKS has introduced a new enhanced mechanism called Pod Identity Association for cluster administrators to configure Kubernetes applications to receive IAM permissions required to connect with AWS services outside of the cluster. Pod Identity Association leverages IRSA, however, it makes it configurable directly through EKS API, eliminating the need for using IAM API altogether. As a result, IAM roles no longer need to reference an [OIDC provider](/usage/iamserviceaccounts/#how-it-works) and hence won't be tied to a single cluster anymore. This means, IAM roles can now be used across multiple EKS clusters without the need to update the role trust policy each time a new cluster is created. This in turn, eliminates the need for role duplication and simplifies the process of automating IRSA altogether. ## Prerequisites -Behind the scenes, the implementation of pod identity associations is running an agent as a daemonset on the worker nodes. To run the pre-requisite agent on the cluster, EKS provides a new add-on called EKS Pod Identity Agent. Therefore, creating pod identity associations (with `eksctl`) requires the `eks-pod-identity-agent` addon pre-installed on the cluster. This addon can be [created using `eksctl`](/usage/addons/#creating-addons) in the same fashion any other supported addon is, e.g. +Behind the scenes, the implementation of pod identity associations is running an agent as a daemonset on the worker nodes. To run the pre-requisite agent on the cluster, EKS provides a new add-on called EKS Pod Identity Agent. Therefore, creating pod identity associations (in general, and with `eksctl`) requires the `eks-pod-identity-agent` addon pre-installed on the cluster. This addon can be [created using `eksctl`](/usage/addons/#creating-addons) in the same fashion any other supported addon is, e.g. ``` eksctl create addon --cluster my-cluster --name eks-pod-identity-agent @@ -165,9 +165,177 @@ OR (to delete a single association) pass the `--namespace` and `--service-accoun eksctl delete podidentityassociation --cluster my-cluster --namespace default --service-account-name s3-reader ``` -## Migrating existing iamserviceaccounts to pod identity associations +## EKS Add-ons support for pod identity associations -`eksctl` has introduced a new utils command for migrating existing IAM Roles for service accounts to pod identity associations, i.e. +EKS Add-ons also support receiving IAM permissions via EKS Pod Identity Associations. The config file exposes two fields that allow configuring these: `addon.podIdentityAssociations` and `iam.autoCreatePodIdentityAssociations`. You can either explicitly configure the desired pod identity associations, using the former, or have `eksctl` automatically resolve (and apply) the recommended pod identity configuration, using the latter. + +???+ note +Not all EKS Add-ons will support pod identity associations at launch. For this case, required IAM permissions shall continue to be provided using [IRSA settings](/usage/addons/#creating-addons-and-providing-iam-permissions-via-irsa) + +### Creating addons with IAM permissions + +When creating an addon that requires IAM permissions, `eksctl` will first check if either pod identity associations or IRSA settings are being explicitly configured as part of the config file, and if so, use one of those to configure the permissions for the addon. e.g. + +```yaml +addons: +- name: vpc-cni + podIdentityAssociations: + - serviceAccountName: aws-node + permissionPolicyARNs: ["arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"] +``` + +and run + +```bash +eksctl create addon -f config.yaml +2024-05-13 15:38:58 [ℹ] pod identity associations are set for "vpc-cni" addon; will use these to configure required IAM permissions +``` + +???+ note +Setting both pod identities and IRSA at the same time is not allowed, and will result in a validation error. + +For EKS Add-ons that support pod identities, `eksctl` offers the option to automatically configure any recommended IAM permissions, on addon creation. This can be achieved by simply setting `iam.AutoCreatePodIdentityAssociations: true` in the config file. e.g. + +```yaml +iam: + autoCreatePodIdentityAssociations: true +# bear in mind that if either pod identity or IRSA configuration is explicitly set in the config file, +# iam.autoCreatePodIdentityAssociations won't have any effect. +addons: +- name: vpc-cni +``` + +and run + +```bash +eksctl create addon -f config.yaml +2024-05-13 15:38:58 [ℹ] "iam.AutoCreatePodIdentityAssociations" is set to true; will lookup recommended pod identity configuration for "vpc-cni" addon +``` + +### Updating addons with IAM permissions + +When updating an addon, specifying `addon.PodIdentityAssociations` will represent the single source of truth for the state that the addon shall have, after the update operation is completed. Behind the scenes, different types of operations are performed in order to achieve the desired state i.e. + +- create pod identites that are present in the config file, but missing on the cluster +- delete existing pod identites that were removed from the config file, together with any associated IAM resources +- update existing pod identities that are also present in the config file, and for which the set of IAM permissions has changed + +???+ note +The lifecycle of pod identity associations owned by EKS Add-ons is directly handled by the EKS Addons API, thus, using `eksctl update podidentityassociation` (to update IAM permissions) or `eksctl delete podidentityassociations` (to remove the association) is not supported for this type of associations. Instead, `eksctl update addon` or `eksctl delete addon` shall be used. + +Let's see an example for the above, starting by analyzing the initial pod identity config for the addon: + +```bash +eksctl get podidentityassociation --cluster my-cluster --namespace opentelemetry-operator-system --output json +[ + { + ... + "ServiceAccountName": "adot-col-prom-metrics", + "RoleARN": "arn:aws:iam::111122223333:role/eksctl-my-cluster-addon-adot-podident-Role1-JwrGA4mn1Ny8", + # OwnerARN is populated when the pod identity lifecycle is handled by the EKS Addons API + "OwnerARN": "arn:aws:eks:us-west-2:111122223333:addon/my-cluster/adot/b2c7bb45-4090-bf34-ec78-a2298b8643f6" + }, + { + ... + "ServiceAccountName": "adot-col-otlp-ingest", + "RoleARN": "arn:aws:iam::111122223333:role/eksctl-my-cluster-addon-adot-podident-Role1-Xc7qVg5fgCqr", + "OwnerARN": "arn:aws:eks:us-west-2:111122223333:addon/my-cluster/adot/b2c7bb45-4090-bf34-ec78-a2298b8643f6" + } +] +``` + +Now use the below configuration: + +```yaml +addons: +- name: adot + podIdentityAssociations: + + # For the first association, the permissions policy of the role will be updated + - serviceAccountName: adot-col-prom-metrics + permissionPolicyARNs: + #- arn:aws:iam::aws:policy/AmazonPrometheusRemoteWriteAccess + - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy + + # The second association will be deleted, as it's been removed from the config file + #- serviceAccountName: adot-col-otlp-ingest + # permissionPolicyARNs: + # - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess + + # The third association will be created, as it's been added to the config file + - serviceAccountName: adot-col-container-logs + permissionPolicyARNs: + - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy +``` + +and run + +```bash +eksctl update addon -f config.yaml +... +# updating the permission policy for the first association +2024-05-14 13:27:43 [ℹ] updating IAM resources stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-prom-metrics" for pod identity association "a-reaxk2uz1iknwazwj" +2024-05-14 13:27:44 [ℹ] waiting for CloudFormation changeset "eksctl-opentelemetry-operator-system-adot-col-prom-metrics-update-1715682463" for stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-prom-metrics" +2024-05-14 13:28:47 [ℹ] waiting for CloudFormation stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-prom-metrics" +2024-05-14 13:28:47 [ℹ] updated IAM resources stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-prom-metrics" for "a-reaxk2uz1iknwazwj" +# creating the IAM role for the second association +2024-05-14 13:28:48 [ℹ] deploying stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-container-logs" +2024-05-14 13:28:48 [ℹ] waiting for CloudFormation stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-container-logs" +2024-05-14 13:29:19 [ℹ] waiting for CloudFormation stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-container-logs" +# updating the addon, which handles the pod identity config changes behind the scenes +2024-05-14 13:29:19 [ℹ] updating addon +# deleting the IAM role for the third association +2024-05-14 13:29:19 [ℹ] deleting IAM resources for pod identity service account adot-col-otlp-ingest +2024-05-14 13:29:20 [ℹ] will delete stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-otlp-ingest" +2024-05-14 13:29:20 [ℹ] waiting for stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-otlp-ingest" to get deleted +2024-05-14 13:29:51 [ℹ] waiting for CloudFormation stack "eksctl-my-cluster-addon-adot-podidentityrole-adot-col-otlp-ingest" +2024-05-14 13:29:51 [ℹ] deleted IAM resources for addon adot +``` + +now check that pod identity config was updated correctly + +```bash +eksctl get podidentityassociation --cluster my-cluster --output json +[ + { + ... + "ServiceAccountName": "adot-col-prom-metrics", + "RoleARN": "arn:aws:iam::111122223333:role/eksctl-my-cluster-addon-adot-podident-Role1-nQAlp0KktS2A", + "OwnerARN": "arn:aws:eks:us-west-2:111122223333:addon/my-cluster/adot/1ec7bb63-8c4e-ca0a-f947-310c4b55052e" + }, + { + ... + "ServiceAccountName": "adot-col-otlp-ingest", + "RoleARN": "arn:aws:iam::111122223333:role/eksctl-my-cluster-addon-adot-podident-Role1-1k1XhAdziGzX", + "OwnerARN": "arn:aws:eks:us-west-2:111122223333:addon/my-cluster/adot/1ec7bb63-8c4e-ca0a-f947-310c4b55052e" + } +] +``` + + +To remove all pod identity associations from an addon, `addon.PodIdentityAssociations` must be explicitly set to `[]`, e.g. + +```yaml +addons: +- name: vpc-cni + # omitting the `podIdentityAssociations` field from the config file, + # instead of explicitly setting it to [], will result in a validation error + podIdentityAssociations: [] +``` + +and run + +```bash +eksctl update addon -f config.yaml +``` + +### Deleting addons with IAM permissions + +Deleting an addon will also remove all pod identities associated with the addon. Deleting the cluster will achieve the same effect, for all addons. Any IAM roles for pod identities, created by `eksctl`, will be deleted as-well. + +## Migrating existing iamserviceaccounts and addons to pod identity associations + +There is an `eksctl` utils command for migrating existing IAM Roles for service accounts to pod identity associations, i.e. ``` eksctl utils migrate-to-pod-identity --cluster my-cluster --approve @@ -176,16 +344,19 @@ eksctl utils migrate-to-pod-identity --cluster my-cluster --approve Behind the scenes, the command will apply the following steps: - install the `eks-pod-identity-agent` addon if not already active on the cluster -- identify all IAM Roles that are associated with K8s service accounts -- update the IAM trust policy of all roles, with an additional trusted entity, pointing to the new EKS Service principal (and, optionally, remove exising OIDC provider trust relationship) -- create pod identity associations between all identified roles and the respective service accounts +- identify all IAM Roles that are associated with iamserviceaccounts +- identify all IAM Roles that are associated with EKS addons that support pod identity associations +- update the IAM trust policy of all identified roles, with an additional trusted entity, pointing to the new EKS Service principal (and, optionally, remove exising OIDC provider trust relationship) +- create pod identity associations for filtered roles associated with iamserviceaccounts +- update EKS addons with pod identities (EKS API will create the pod identities behind the scenes) Running the command without the `--approve` flag will only output a plan consisting of a set of tasks reflecting the steps above, e.g. ```bash -[ℹ] (plan) would migrate 2 iamserviceaccount(s) to pod identity association(s) by executing the following tasks +[ℹ] (plan) would migrate 2 iamserviceaccount(s) and 2 addon(s) to pod identity association(s) by executing the following tasks [ℹ] (plan) -3 sequential tasks: { install eks-pod-identity-agent addon, +3 sequential tasks: { install eks-pod-identity-agent addon, + ## tasks for migrating the iamserviceaccounts 2 parallel sub-tasks: { update trust policy for owned role "eksctl-my-cluster-addon-iamserv-Role1-beYhlhzpwQte", update trust policy for unowned role "Unowned-Role1", @@ -193,21 +364,35 @@ Running the command without the `--approve` flag will only output a plan consist 2 parallel sub-tasks: { create pod identity association for service account "default/sa1", create pod identity association for service account "default/sa2", - } + }, + ## tasks for migrating the addons + 2 parallel sub-tasks: { + 2 sequential sub-tasks: { + update trust policy for owned role "eksctl-my-cluster-addon-aws-ebs-csi-d-Role1-9BMT7CgeSNvX", + migrate addon aws-ebs-csi-driver to pod identity, + }, + 2 sequential sub-tasks: { + update trust policy for owned role "eksctl-my-cluster-addon-vpc-cni-Role1-ePPlktZv2kjo", + migrate addon vpc-cni to pod identity, + }, + }, } [ℹ] all tasks were skipped [!] no changes were applied, run again with '--approve' to apply the changes ``` -Additionally, to delete the existing OIDC provider trust relationship from all IAM Roles, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. +The existing OIDC provider trust relationship is always being deleted from IAM Roles associated with EKS Add-ons. Additionally, to delete the existing OIDC provider trust relationship from IAM Roles associated with iamserviceaccounts, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. ``` eksctl utils migrate-to-pod-identity --cluster my-cluster --approve --remove-oidc-provider-trust-relationship ``` - ## Further references -[Official AWS Blog Post](https://aws.amazon.com/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/) +[Official AWS Blog Post on EKS Add-ons support for pod identities] //https://TBD + +[Official AWS Userdocs for EKS Add-ons support for pod identities] //https://TBD + +[Official AWS Blog Post on Pod Identity Associations](https://aws.amazon.com/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/) -[Official AWS userdocs](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) \ No newline at end of file +[Official AWS userdocs for Pod Identity Associations](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) \ No newline at end of file diff --git a/userdocs/theme/home.html b/userdocs/theme/home.html index 6bd0a3e3b5..9889279828 100644 --- a/userdocs/theme/home.html +++ b/userdocs/theme/home.html @@ -533,6 +533,7 @@

eksctl create cluster

Usage Outposts

Check out latest eksctl features

+

EKS Add-ons support receiving IAM permissions via EKS Pod Identity Associations.

Support for AMIs based on AmazonLinux2023

Configuring cluster access management via AWS EKS Access Entries.

Configuring fine-grained permissions to EKS running apps via EKS Pod Identity Associations.

From 3c4d4eecbc2618fe3f62bdcd887cccf7d3d981f4 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Wed, 15 May 2024 20:13:41 +0530 Subject: [PATCH 21/35] Delete old IRSA stack in `update addon` --- .mockery.yaml | 1 + pkg/actions/addon/get.go | 42 ++--- pkg/actions/addon/get_test.go | 29 +++- pkg/actions/addon/podidentityassociation.go | 78 ++++++--- .../addon/podidentityassociation_test.go | 161 ++++++++++++++++-- pkg/actions/addon/update.go | 12 +- .../mocks/StackDeleter.go | 123 +++++++++++++ pkg/actions/podidentityassociation/updater.go | 2 +- .../eksctl.io/v1alpha5/validation_test.go | 14 ++ pkg/ctl/get/addon.go | 4 +- 10 files changed, 396 insertions(+), 70 deletions(-) diff --git a/.mockery.yaml b/.mockery.yaml index 6e3e2b22d4..65814a6eda 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -70,6 +70,7 @@ packages: interfaces: StackDeleter: config: + with-expecter: true dir: "{{.InterfaceDir}}/mocks" outpkg: mocks RoleMigrator: diff --git a/pkg/actions/addon/get.go b/pkg/actions/addon/get.go index 184f146212..21be8fbc41 100644 --- a/pkg/actions/addon/get.go +++ b/pkg/actions/addon/get.go @@ -15,6 +15,7 @@ import ( ) type PodIdentityAssociationSummary struct { + AssociationID string Namespace string ServiceAccount string RoleARN string @@ -37,7 +38,7 @@ type Issue struct { ResourceIDs []string } -func (a *Manager) Get(ctx context.Context, addon *api.Addon, includePodIdentityAssociations bool) (Summary, error) { +func (a *Manager) Get(ctx context.Context, addon *api.Addon) (Summary, error) { logger.Debug("addon: %v", addon) output, err := a.eksAPI.DescribeAddon(ctx, &eks.DescribeAddonInput{ ClusterName: &a.clusterConfig.Metadata.Name, @@ -82,26 +83,25 @@ func (a *Manager) Get(ctx context.Context, addon *api.Addon, includePodIdentityA configurationValues = *output.Addon.ConfigurationValues } var podIdentityAssociations []PodIdentityAssociationSummary - if includePodIdentityAssociations { - podIdentityAssociationIDs, err := toPodIdentityAssociationIDs(output.Addon.PodIdentityAssociations) + podIdentityAssociationIDs, err := toPodIdentityAssociationIDs(output.Addon.PodIdentityAssociations) + if err != nil { + return Summary{}, err + } + for _, associationID := range podIdentityAssociationIDs { + output, err := a.eksAPI.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ + ClusterName: aws.String(a.clusterConfig.Metadata.Name), + AssociationId: aws.String(associationID), + }) if err != nil { - return Summary{}, err - } - for _, associationID := range podIdentityAssociationIDs { - output, err := a.eksAPI.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ - ClusterName: aws.String(a.clusterConfig.Metadata.Name), - AssociationId: aws.String(associationID), - }) - if err != nil { - return Summary{}, fmt.Errorf("describe pod identity association %q: %w", associationID, err) - } - association := output.Association - podIdentityAssociations = append(podIdentityAssociations, PodIdentityAssociationSummary{ - Namespace: *association.Namespace, - ServiceAccount: *association.ServiceAccount, - RoleARN: *association.RoleArn, - }) + return Summary{}, fmt.Errorf("describe pod identity association %q: %w", associationID, err) } + association := output.Association + podIdentityAssociations = append(podIdentityAssociations, PodIdentityAssociationSummary{ + Namespace: *association.Namespace, + ServiceAccount: *association.ServiceAccount, + RoleARN: *association.RoleArn, + AssociationID: *association.AssociationId, + }) } return Summary{ @@ -116,7 +116,7 @@ func (a *Manager) Get(ctx context.Context, addon *api.Addon, includePodIdentityA }, nil } -func (a *Manager) GetAll(ctx context.Context, includePodIdentityAssociations bool) ([]Summary, error) { +func (a *Manager) GetAll(ctx context.Context) ([]Summary, error) { logger.Info("getting all addons") output, err := a.eksAPI.ListAddons(ctx, &eks.ListAddonsInput{ ClusterName: &a.clusterConfig.Metadata.Name, @@ -127,7 +127,7 @@ func (a *Manager) GetAll(ctx context.Context, includePodIdentityAssociations boo var summaries []Summary for _, addon := range output.Addons { - summary, err := a.Get(ctx, &api.Addon{Name: addon}, includePodIdentityAssociations) + summary, err := a.Get(ctx, &api.Addon{Name: addon}) if err != nil { return nil, err } diff --git a/pkg/actions/addon/get_test.go b/pkg/actions/addon/get_test.go index 97c679f40e..6bf783d43b 100644 --- a/pkg/actions/addon/get_test.go +++ b/pkg/actions/addon/get_test.go @@ -34,7 +34,7 @@ var _ = Describe("Get", func() { }) Describe("Get", func() { - mockDescribeAddon := func(podIdentityAssociations ...string) { + mockDescribeAddon := func(podIdentityAssociationIDs ...string) { mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { Expect(args).To(HaveLen(2)) Expect(args[1]).To(BeAssignableToTypeOf(&awseks.DescribeAddonVersionsInput{})) @@ -71,7 +71,7 @@ var _ = Describe("Get", func() { AddonVersion: aws.String("v1.1.0-eksbuild.1"), ServiceAccountRoleArn: aws.String("foo"), Status: "created", - PodIdentityAssociations: podIdentityAssociations, + PodIdentityAssociations: podIdentityAssociationIDs, Health: &ekstypes.AddonHealth{ Issues: []ekstypes.AddonIssue{ { @@ -83,12 +83,23 @@ var _ = Describe("Get", func() { }, }, }, nil) + if len(podIdentityAssociationIDs) > 0 { + for _, piaID := range podIdentityAssociationIDs { + mockProvider.MockEKS().On("DescribePodIdentityAssociation", mock.Anything, &awseks.DescribePodIdentityAssociationInput{ + AssociationId: aws.String(piaID), + }).Return(&awseks.DescribePodIdentityAssociationOutput{ + Association: &ekstypes.PodIdentityAssociation{ + AssociationId: aws.String(piaID), + }, + }, nil).Once() + } + } } It("returns an addon", func() { mockDescribeAddon() summary, err := manager.Get(context.Background(), &api.Addon{ Name: "my-addon", - }, false) + }) Expect(err).NotTo(HaveOccurred()) Expect(summary).To(Equal(addon.Summary{ Name: "my-addon", @@ -119,11 +130,12 @@ var _ = Describe("Get", func() { RoleArn: aws.String("role-1"), ServiceAccount: aws.String("default"), Namespace: aws.String("default"), + AssociationId: aws.String("a-1"), }, }, nil) summary, err := manager.Get(context.Background(), &api.Addon{ Name: "my-addon", - }, true) + }) Expect(err).NotTo(HaveOccurred()) Expect(summary).To(Equal(addon.Summary{ Name: "my-addon", @@ -143,6 +155,7 @@ var _ = Describe("Get", func() { Namespace: "default", ServiceAccount: "default", RoleARN: "role-1", + AssociationID: "a-1", }, }, })) @@ -158,7 +171,7 @@ var _ = Describe("Get", func() { _, err := manager.Get(context.Background(), &api.Addon{ Name: "my-addon", - }, false) + }) Expect(err).To(MatchError(`failed to get addon "my-addon": foo`)) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) Expect(*describeAddonInput.AddonName).To(Equal("my-addon")) @@ -217,7 +230,7 @@ var _ = Describe("Get", func() { }, }, nil) - summary, err := manager.GetAll(context.Background(), false) + summary, err := manager.GetAll(context.Background()) Expect(err).NotTo(HaveOccurred()) Expect(summary).To(Equal([]addon.Summary{ { @@ -251,7 +264,7 @@ var _ = Describe("Get", func() { describeAddonInput = args[1].(*awseks.DescribeAddonInput) }).Return(nil, fmt.Errorf("foo")) - _, err := manager.GetAll(context.Background(), false) + _, err := manager.GetAll(context.Background()) Expect(err).To(MatchError(`failed to get addon "my-addon": foo`)) Expect(*describeAddonInput.ClusterName).To(Equal("my-cluster")) Expect(*describeAddonInput.AddonName).To(Equal("my-addon")) @@ -270,7 +283,7 @@ var _ = Describe("Get", func() { Addons: []string{"my-addon"}, }, fmt.Errorf("foo")) - _, err := manager.GetAll(context.Background(), false) + _, err := manager.GetAll(context.Background()) Expect(err).To(MatchError(`failed to list addons: foo`)) Expect(*listAddonsInput.ClusterName).To(Equal("my-cluster")) }) diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go index 64436da34f..80351993af 100644 --- a/pkg/actions/addon/podidentityassociation.go +++ b/pkg/actions/addon/podidentityassociation.go @@ -8,6 +8,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/kris-nova/logger" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/manager" @@ -49,7 +51,6 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent roleARN := pia.RoleARN switch len(output.Associations) { default: - // TODO: does the API return a not found error if no association exists? return nil, fmt.Errorf("expected to find exactly 1 pod identity association for %s; got %d", pia.NameString(), len(output.Associations)) case 0: // Create IAM resources. @@ -58,6 +59,16 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent if roleARN, err = p.IAMRoleCreator.Create(ctx, &pia, addonName); err != nil { return nil, err } + stack, err := p.getStack(ctx, manager.MakeAddonStackName(p.ClusterName, addonName), pia.ServiceAccountName) + if err != nil { + return nil, fmt.Errorf("getting old IRSA stack for addon %s: %w", addonName, err) + } + if stack != nil { + logger.Info("deleting old IRSA stack for addon %s", addonName) + if err := p.deleteStack(ctx, stack); err != nil { + return nil, fmt.Errorf("deleting old IRSA stack for addon %s: %w", addonName, err) + } + } } case 1: // Update IAM resources if required. @@ -68,26 +79,21 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent if err != nil { return nil, err } - stackName := podidentityassociation.MakeAddonPodIdentityStackName(p.ClusterName, addonName, pia.ServiceAccountName) - hasStack := true - if _, err := p.StackDeleter.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(stackName), - }); err != nil { - if !manager.IsStackDoesNotExistError(err) { - return nil, fmt.Errorf("describing IAM resources stack for pod identity association %s: %w", pia.NameString(), err) - } - hasStack = false + stack, err := p.getAddonStack(ctx, addonName, pia.ServiceAccountName) + if err != nil { + return nil, fmt.Errorf("getting IAM resources stack for addon %s with pod identity association %s: %w", addonName, pia.NameString(), err) } roleValidator := &podidentityassociation.RoleUpdateValidator{ StackDescriber: p.StackDeleter, } + hasStack := stack != nil if err := roleValidator.ValidateRoleUpdate(pia, *output.Association, hasStack); err != nil { return nil, err } if hasStack { // TODO: if no pod identity has changed, skip update. - newRoleARN, hasChanged, err := p.IAMRoleUpdater.Update(ctx, pia, stackName, *output.Association.AssociationId) + newRoleARN, hasChanged, err := p.IAMRoleUpdater.Update(ctx, pia, *stack.StackName, *output.Association.AssociationId) if err != nil { return nil, err } @@ -106,28 +112,56 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent return addonPodIdentityAssociations, nil } +func (p *PodIdentityAssociationUpdater) getAddonStack(ctx context.Context, addonName, serviceAccount string) (*manager.Stack, error) { + for _, stackName := range []string{podidentityassociation.MakeAddonPodIdentityStackName(p.ClusterName, addonName, serviceAccount), + manager.MakeAddonStackName(p.ClusterName, addonName)} { + stack, err := p.getStack(ctx, stackName, serviceAccount) + if err != nil { + return nil, err + } + if stack != nil { + return stack, nil + } + } + return nil, nil +} + +func (p *PodIdentityAssociationUpdater) getStack(ctx context.Context, stackName, serviceAccount string) (*manager.Stack, error) { + switch stack, err := p.StackDeleter.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(stackName), + }); { + case err == nil: + return stack, nil + case manager.IsStackDoesNotExistError(err): + return nil, nil + default: + return nil, fmt.Errorf("describing IAM resources stack for service account %s: %w", serviceAccount, err) + } +} + func (p *PodIdentityAssociationUpdater) DeleteRole(ctx context.Context, addonName, serviceAccountName string) (bool, error) { - stack, err := p.StackDeleter.DescribeStack(ctx, &manager.Stack{ - StackName: aws.String(podidentityassociation.MakeAddonPodIdentityStackName(p.ClusterName, addonName, serviceAccountName)), - }) + stack, err := p.getAddonStack(ctx, addonName, serviceAccountName) if err != nil { - if manager.IsStackDoesNotExistError(err) { - return false, nil - } - return false, fmt.Errorf("describing IAM resources stack for addon %s: %w", addonName, err) + return false, fmt.Errorf("getting IAM resources stack for addon %s with service account %s: %w", addonName, serviceAccountName, err) } + if err := p.deleteStack(ctx, stack); err != nil { + return false, err + } + return true, nil +} +func (p *PodIdentityAssociationUpdater) deleteStack(ctx context.Context, stack *manager.Stack) error { errCh := make(chan error) if err := p.StackDeleter.DeleteStackBySpecSync(ctx, stack, errCh); err != nil { - return false, fmt.Errorf("deleting stack %s: %w", *stack.StackName, err) + return fmt.Errorf("deleting stack %s: %w", *stack.StackName, err) } select { case err := <-errCh: if err != nil { - return false, fmt.Errorf("deleting stack %s: %w", *stack.StackName, err) + return fmt.Errorf("deleting stack %s: %w", *stack.StackName, err) } - return true, nil + return nil case <-ctx.Done(): - return false, fmt.Errorf("timed out waiting for deletion of stack %s: %w", *stack.StackName, ctx.Err()) + return fmt.Errorf("timed out waiting for deletion of stack %s: %w", *stack.StackName, ctx.Err()) } } diff --git a/pkg/actions/addon/podidentityassociation_test.go b/pkg/actions/addon/podidentityassociation_test.go index ae733bcf1f..cc18d7f579 100644 --- a/pkg/actions/addon/podidentityassociation_test.go +++ b/pkg/actions/addon/podidentityassociation_test.go @@ -11,6 +11,7 @@ import ( "github.com/aws/smithy-go" "github.com/aws/aws-sdk-go-v2/aws" + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" @@ -124,7 +125,7 @@ var _ = Describe("Update Pod Identity Association", func() { eks: provider.MockEKS(), }) } - addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations, "") + addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations, "main") if e.expectedErr != "" { Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) return @@ -153,7 +154,12 @@ var _ = Describe("Update Pod Identity Association", func() { m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "vpc-cni", - }, "").Return("role-1", nil) + }, "main").Return("role-1", nil) + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main"), + }).Return(nil, &smithy.OperationError{ + Err: errors.New("ValidationError"), + }).Once() }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ @@ -201,19 +207,26 @@ var _ = Describe("Update Pod Identity Association", func() { m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "vpc-cni", - }, "eksctl-test-addon--podidentityrole-vpc-cni", "a-1").Return("cni-role-2", true, nil).Once() + }, "eksctl-test-addon-main-podidentityrole-vpc-cni", "a-1").Return("cni-role-2", true, nil).Once() + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), + }).Return(&manager.Stack{ + StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), + }, nil).Twice() m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ - StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), - }).Return(&manager.Stack{}, nil) + StackName: aws.String("eksctl-test-addon-main"), + }).Return(nil, &smithy.OperationError{ + Err: errors.New("ValidationError"), + }).Twice() m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "aws-ebs-csi-driver", - }, "").Return("csi-role", nil).Once() + }, "main").Return("csi-role", nil).Once() m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "karpenter", ServiceAccountName: "karpenter", - }, "").Return("karpenter-role", nil).Once() + }, "main").Return("karpenter-role", nil).Once() }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { @@ -281,7 +294,7 @@ var _ = Describe("Update Pod Identity Association", func() { } { id := makeID(i) - stackName := fmt.Sprintf("eksctl-test-addon--podidentityrole-%s", updateInput.serviceAccount) + stackName := fmt.Sprintf("eksctl-test-addon-main-podidentityrole-%s", updateInput.serviceAccount) m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ Namespace: updateInput.namespace, ServiceAccountName: updateInput.serviceAccount, @@ -421,13 +434,19 @@ var _ = Describe("Update Pod Identity Association", func() { mockDescribePodIdentityAssociation(m.eks, "role-1", "role-2", "role-3") for _, serviceAccount := range []string{"vpc-cni", "aws-ebs-csi-driver"} { m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ - StackName: aws.String(fmt.Sprintf("eksctl-test-addon--podidentityrole-%s", serviceAccount)), + StackName: aws.String(fmt.Sprintf("eksctl-test-addon-main-podidentityrole-%s", serviceAccount)), + }).Return(nil, &smithy.OperationError{ + Err: fmt.Errorf("ValidationError"), + }).Once() + + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main"), }).Return(nil, &smithy.OperationError{ Err: fmt.Errorf("ValidationError"), }).Once() } m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ - StackName: aws.String("eksctl-test-addon--podidentityrole-karpenter"), + StackName: aws.String("eksctl-test-addon-main-podidentityrole-karpenter"), }).Return(&manager.Stack{}, nil).Once() }, expectedErr: "cannot change podIdentityAssociation.roleARN since the role was created by eksctl", @@ -450,7 +469,7 @@ var _ = Describe("Update Pod Identity Association", func() { }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ - StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), + StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), }).Return(&manager.Stack{}, nil).Once() }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ @@ -478,12 +497,130 @@ var _ = Describe("Update Pod Identity Association", func() { }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ - StackName: aws.String("eksctl-test-addon--podidentityrole-vpc-cni"), + StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), }).Return(nil, &smithy.OperationError{ Err: errors.New("ValidationError"), }) + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main"), + }).Return(nil, &smithy.OperationError{ + Err: errors.New("ValidationError"), + }).Once() }, expectedErr: "podIdentityAssociation.roleARN is required since the role was not created by eksctl", }), + + Entry("addon contains pod identities that do not exist and have a pre-existing roleARN", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + RoleARN: "role-1", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + RoleARN: "role-2", + }, + { + Namespace: "karpenter", + ServiceAccountName: "karpenter", + RoleARN: "role-3", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, false, defaultListPodIdentityInputs) + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("role-1"), + }, + { + ServiceAccount: aws.String("aws-ebs-csi-driver"), + RoleArn: aws.String("role-2"), + }, + { + ServiceAccount: aws.String("karpenter"), + RoleArn: aws.String("role-3"), + }, + }, + }), + + Entry("addon contains a pod identity with an IRSA stack", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + }, + mockCalls: func(m piaMocks) { + mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ + { + namespace: "kube-system", + serviceAccount: "vpc-cni", + }, + }) + mockDescribePodIdentityAssociation(m.eks, "cni-role") + + m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, "eksctl-test-addon-main", "a-1").Return("cni-role-2", true, nil).Once() + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), + }).Return(nil, &smithy.OperationError{ + Err: fmt.Errorf("ValidationError"), + }).Once() + + m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main"), + }).Return(&manager.Stack{ + StackName: aws.String("eksctl-test-addon-main"), + }, nil).Once() + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("cni-role-2"), + }, + }, + }), + + Entry("addon using IRSA is updated to use pod identity", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, + }, + mockCalls: func(m piaMocks) { + m.eks.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(clusterName), + Namespace: aws.String("kube-system"), + ServiceAccount: aws.String("vpc-cni"), + }).Return(&eks.ListPodIdentityAssociationsOutput{}, nil) + + m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + }, "main").Return("role-1", nil) + irsaStack := &manager.Stack{ + StackName: aws.String("eksctl-test-addon-main"), + } + m.stackDeleter.On("DescribeStack", mock.Anything, irsaStack).Return(irsaStack, nil).Once() + m.stackDeleter.EXPECT().DeleteStackBySpecSync(mock.Anything, irsaStack, mock.Anything).RunAndReturn(func(ctx context.Context, stack *cfntypes.Stack, errCh chan error) error { + close(errCh) + return nil + }).Once() + + }, + expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("vpc-cni"), + RoleArn: aws.String("role-1"), + }, + }, + }), ) }) diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 3f95e7cc2e..c457ac27b4 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -45,7 +45,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp logger.Debug("resolve conflicts set to %s", updateAddonInput.ResolveConflicts) - summary, err := a.Get(ctx, addon, true) + summary, err := a.Get(ctx, addon) if err != nil { return err } @@ -72,8 +72,10 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp var deleteServiceAccountIAMResources []string if len(summary.PodIdentityAssociations) > 0 { if addon.PodIdentityAssociations == nil { - return fmt.Errorf("addon %s has pod identity associations; to remove pod identity associations from an addon, "+ - "addon.podIdentityAssociations must be explicitly set to []", addon.Name) + return fmt.Errorf("addon %s has pod identity associations, to remove pod identity associations from an addon, "+ + "addon.podIdentityAssociations must be explicitly set to []; if the addon was migrated to use pod identity, "+ + "addon.podIdentityAssociations must be set to values obtained from `aws eks describe-pod-identity-association --cluster-name=%s --association-id=%s`", + addon.Name, a.clusterConfig.Metadata.Name, summary.PodIdentityAssociations[0].AssociationID) } for _, pia := range summary.PodIdentityAssociations { if !slices.ContainsFunc(*addon.PodIdentityAssociations, func(addonPodIdentity api.PodIdentityAssociation) bool { @@ -82,7 +84,9 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp deleteServiceAccountIAMResources = append(deleteServiceAccountIAMResources, pia.ServiceAccount) } } - updateAddonInput.PodIdentityAssociations = []ekstypes.AddonPodIdentityAssociations{} + if len(deleteServiceAccountIAMResources) == 0 { + updateAddonInput.PodIdentityAssociations = []ekstypes.AddonPodIdentityAssociations{} + } } if addon.HasPodIDsSet() { diff --git a/pkg/actions/podidentityassociation/mocks/StackDeleter.go b/pkg/actions/podidentityassociation/mocks/StackDeleter.go index 597ec6dc74..78b583f85d 100644 --- a/pkg/actions/podidentityassociation/mocks/StackDeleter.go +++ b/pkg/actions/podidentityassociation/mocks/StackDeleter.go @@ -17,6 +17,14 @@ type StackDeleter struct { mock.Mock } +type StackDeleter_Expecter struct { + mock *mock.Mock +} + +func (_m *StackDeleter) EXPECT() *StackDeleter_Expecter { + return &StackDeleter_Expecter{mock: &_m.Mock} +} + // DeleteStackBySpecSync provides a mock function with given fields: ctx, stack, errCh func (_m *StackDeleter) DeleteStackBySpecSync(ctx context.Context, stack *types.Stack, errCh chan error) error { ret := _m.Called(ctx, stack, errCh) @@ -35,6 +43,36 @@ func (_m *StackDeleter) DeleteStackBySpecSync(ctx context.Context, stack *types. return r0 } +// StackDeleter_DeleteStackBySpecSync_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteStackBySpecSync' +type StackDeleter_DeleteStackBySpecSync_Call struct { + *mock.Call +} + +// DeleteStackBySpecSync is a helper method to define mock.On call +// - ctx context.Context +// - stack *types.Stack +// - errCh chan error +func (_e *StackDeleter_Expecter) DeleteStackBySpecSync(ctx interface{}, stack interface{}, errCh interface{}) *StackDeleter_DeleteStackBySpecSync_Call { + return &StackDeleter_DeleteStackBySpecSync_Call{Call: _e.mock.On("DeleteStackBySpecSync", ctx, stack, errCh)} +} + +func (_c *StackDeleter_DeleteStackBySpecSync_Call) Run(run func(ctx context.Context, stack *types.Stack, errCh chan error)) *StackDeleter_DeleteStackBySpecSync_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.Stack), args[2].(chan error)) + }) + return _c +} + +func (_c *StackDeleter_DeleteStackBySpecSync_Call) Return(_a0 error) *StackDeleter_DeleteStackBySpecSync_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *StackDeleter_DeleteStackBySpecSync_Call) RunAndReturn(run func(context.Context, *types.Stack, chan error) error) *StackDeleter_DeleteStackBySpecSync_Call { + _c.Call.Return(run) + return _c +} + // DescribeStack provides a mock function with given fields: ctx, stack func (_m *StackDeleter) DescribeStack(ctx context.Context, stack *types.Stack) (*types.Stack, error) { ret := _m.Called(ctx, stack) @@ -65,6 +103,35 @@ func (_m *StackDeleter) DescribeStack(ctx context.Context, stack *types.Stack) ( return r0, r1 } +// StackDeleter_DescribeStack_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DescribeStack' +type StackDeleter_DescribeStack_Call struct { + *mock.Call +} + +// DescribeStack is a helper method to define mock.On call +// - ctx context.Context +// - stack *types.Stack +func (_e *StackDeleter_Expecter) DescribeStack(ctx interface{}, stack interface{}) *StackDeleter_DescribeStack_Call { + return &StackDeleter_DescribeStack_Call{Call: _e.mock.On("DescribeStack", ctx, stack)} +} + +func (_c *StackDeleter_DescribeStack_Call) Run(run func(ctx context.Context, stack *types.Stack)) *StackDeleter_DescribeStack_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*types.Stack)) + }) + return _c +} + +func (_c *StackDeleter_DescribeStack_Call) Return(_a0 *types.Stack, _a1 error) *StackDeleter_DescribeStack_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *StackDeleter_DescribeStack_Call) RunAndReturn(run func(context.Context, *types.Stack) (*types.Stack, error)) *StackDeleter_DescribeStack_Call { + _c.Call.Return(run) + return _c +} + // GetIAMServiceAccounts provides a mock function with given fields: ctx func (_m *StackDeleter) GetIAMServiceAccounts(ctx context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error) { ret := _m.Called(ctx) @@ -95,6 +162,34 @@ func (_m *StackDeleter) GetIAMServiceAccounts(ctx context.Context) ([]*v1alpha5. return r0, r1 } +// StackDeleter_GetIAMServiceAccounts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIAMServiceAccounts' +type StackDeleter_GetIAMServiceAccounts_Call struct { + *mock.Call +} + +// GetIAMServiceAccounts is a helper method to define mock.On call +// - ctx context.Context +func (_e *StackDeleter_Expecter) GetIAMServiceAccounts(ctx interface{}) *StackDeleter_GetIAMServiceAccounts_Call { + return &StackDeleter_GetIAMServiceAccounts_Call{Call: _e.mock.On("GetIAMServiceAccounts", ctx)} +} + +func (_c *StackDeleter_GetIAMServiceAccounts_Call) Run(run func(ctx context.Context)) *StackDeleter_GetIAMServiceAccounts_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *StackDeleter_GetIAMServiceAccounts_Call) Return(_a0 []*v1alpha5.ClusterIAMServiceAccount, _a1 error) *StackDeleter_GetIAMServiceAccounts_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *StackDeleter_GetIAMServiceAccounts_Call) RunAndReturn(run func(context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error)) *StackDeleter_GetIAMServiceAccounts_Call { + _c.Call.Return(run) + return _c +} + // ListPodIdentityStackNames provides a mock function with given fields: ctx func (_m *StackDeleter) ListPodIdentityStackNames(ctx context.Context) ([]string, error) { ret := _m.Called(ctx) @@ -125,6 +220,34 @@ func (_m *StackDeleter) ListPodIdentityStackNames(ctx context.Context) ([]string return r0, r1 } +// StackDeleter_ListPodIdentityStackNames_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPodIdentityStackNames' +type StackDeleter_ListPodIdentityStackNames_Call struct { + *mock.Call +} + +// ListPodIdentityStackNames is a helper method to define mock.On call +// - ctx context.Context +func (_e *StackDeleter_Expecter) ListPodIdentityStackNames(ctx interface{}) *StackDeleter_ListPodIdentityStackNames_Call { + return &StackDeleter_ListPodIdentityStackNames_Call{Call: _e.mock.On("ListPodIdentityStackNames", ctx)} +} + +func (_c *StackDeleter_ListPodIdentityStackNames_Call) Run(run func(ctx context.Context)) *StackDeleter_ListPodIdentityStackNames_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *StackDeleter_ListPodIdentityStackNames_Call) Return(_a0 []string, _a1 error) *StackDeleter_ListPodIdentityStackNames_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *StackDeleter_ListPodIdentityStackNames_Call) RunAndReturn(run func(context.Context) ([]string, error)) *StackDeleter_ListPodIdentityStackNames_Call { + _c.Call.Return(run) + return _c +} + // NewStackDeleter creates a new instance of StackDeleter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewStackDeleter(t interface { diff --git a/pkg/actions/podidentityassociation/updater.go b/pkg/actions/podidentityassociation/updater.go index 68bfd78804..967451a415 100644 --- a/pkg/actions/podidentityassociation/updater.go +++ b/pkg/actions/podidentityassociation/updater.go @@ -175,7 +175,7 @@ type RoleUpdateValidator struct { StackDescriber StackDescriber } -// ValidateRoleUpdate validates TODO and returns a boolean indicating whether a stack exists. +// ValidateRoleUpdate validates the role associated with pia. func (r *RoleUpdateValidator) ValidateRoleUpdate(pia api.PodIdentityAssociation, association ekstypes.PodIdentityAssociation, hasStack bool) error { if hasStack { if association.RoleArn != nil && pia.RoleARN != "" && pia.RoleARN != *association.RoleArn { diff --git a/pkg/apis/eksctl.io/v1alpha5/validation_test.go b/pkg/apis/eksctl.io/v1alpha5/validation_test.go index 77e1ac6f08..da274609f3 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation_test.go @@ -2473,6 +2473,20 @@ var _ = Describe("ClusterConfig validation", func() { }) }) }) + + DescribeTable("ToPodIdentityAssociationID", func(piaARN, expectedID, expectedErr string) { + piaID, err := api.ToPodIdentityAssociationID(piaARN) + if expectedErr != "" { + Expect(err).To(MatchError(ContainSubstring(expectedErr))) + return + } + Expect(err).NotTo(HaveOccurred()) + Expect(piaID).To(Equal(expectedID)) + }, + Entry("valid PIA ARN", "arn:aws:eks:us-west-2:000:podidentityassociation/cluster/a-d3dw7wfvxtoatujeg", "a-d3dw7wfvxtoatujeg", ""), + Entry("invalid PIA ARN format", "arn:aws:eks:us-west-2:000:podidentityassociation/a-d3dw7wfvxtoatujeg", "", "unexpected pod identity association ARN format"), + Entry("invalid PIA ARN", "a-d3dw7wfvxtoatujeg", "", "parsing ARN"), + ) }) func newInt(value int) *int { diff --git a/pkg/ctl/get/addon.go b/pkg/ctl/get/addon.go index d337005fdb..fc3890703b 100644 --- a/pkg/ctl/get/addon.go +++ b/pkg/ctl/get/addon.go @@ -86,12 +86,12 @@ func getAddon(cmd *cmdutils.Cmd, a *api.Addon, params *getCmdParams) error { var summaries []addon.Summary if a.Name == "" { - summaries, err = addonManager.GetAll(ctx, true) + summaries, err = addonManager.GetAll(ctx) if err != nil { return err } } else { - summary, err := addonManager.Get(ctx, a, true) + summary, err := addonManager.Get(ctx, a) if err != nil { return err } From d5b6778016c9e2142267450dd1d9474aabe328d8 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Wed, 22 May 2024 17:45:55 +0530 Subject: [PATCH 22/35] Add integration test for addon.podIdentityAssociations --- integration/tests/addons/addons_test.go | 157 +++++++++++++++++++++++- 1 file changed, 155 insertions(+), 2 deletions(-) diff --git a/integration/tests/addons/addons_test.go b/integration/tests/addons/addons_test.go index 32a52fbb87..6659b1e72e 100644 --- a/integration/tests/addons/addons_test.go +++ b/integration/tests/addons/addons_test.go @@ -7,15 +7,19 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "strconv" "strings" "testing" "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudformation" awseks "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/aws/smithy-go" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -36,14 +40,16 @@ import ( ) var ( - params *tests.Params - rawClient *kubewrapper.RawClient + params *tests.Params + rawClient *kubewrapper.RawClient + awsProvider api.ClusterProvider ) func init() { // Call testing.Init() prior to tests.NewParams(), as otherwise -test.* will not be recognised. See also: https://golang.org/doc/go1.13#testing testing.Init() params = tests.NewParams("addons") + params.Region = "ap-northeast-2" } func TestEKSAddons(t *testing.T) { @@ -532,6 +538,152 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { )) }) + Context("pod identity associations", func() { + It("should manage pod identity associations for addons", func() { + output, err := awsProvider.EKS().ListPodIdentityAssociations(context.Background(), &awseks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(params.ClusterName), + }) + Expect(err).NotTo(HaveOccurred()) + Expect(output.Associations).To(BeEmpty()) + clusterConfig := getInitialClusterConfig() + clusterConfig.Addons = []*api.Addon{ + { + Name: "vpc-cni", + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, + }, + } + + makeUpdateAddonCMD := func() runner.Cmd { + return params.EksctlUpdateCmd. + WithArgs("addon"). + WithArgs("--config-file", "-"). + WithoutArg("--region", params.Region). + WithStdin(clusterutils.Reader(clusterConfig)) + } + By("updating addon to use pod identity") + assertAddonHasPodIDs := func(addonName string, podIDsCount int) { + addon, err := awsProvider.EKS().DescribeAddon(context.Background(), &awseks.DescribeAddonInput{ + AddonName: aws.String(addonName), + ClusterName: aws.String(clusterConfig.Metadata.Name), + }) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + ExpectWithOffset(1, addon.Addon.PodIdentityAssociations).To(HaveLen(podIDsCount)) + } + assertAddonHasPodIDs(api.VPCCNIAddon, 0) + irsaStackName := fmt.Sprintf("eksctl-%s-addon-%s", clusterConfig.Metadata.Name, api.VPCCNIAddon) + cmd := makeUpdateAddonCMD() + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring("deleting old IRSA stack for addon vpc-cni")), + ContainElement(ContainSubstring("will delete stack %q", irsaStackName)), + )) + assertAddonHasPodIDs(api.VPCCNIAddon, 1) + + By("ensuring the IRSA stack is deleted") + _, err = awsProvider.CloudFormation().DescribeStacks(context.Background(), &cloudformation.DescribeStacksInput{ + StackName: aws.String(irsaStackName), + }) + var opErr *smithy.OperationError + Expect(errors.As(err, &opErr) && strings.Contains(opErr.Error(), "ValidationError")).To(BeTrue(), "expected stack to not exist, err: %v", err) + + By("ensuring that updating addon again works") + cmd = makeUpdateAddonCMD() + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + Not(ContainElement(ContainSubstring("deleting old IRSA stack for addon %s", api.VPCCNIAddon))), + )) + + By("failing to update addon with pod identity if the field is unset") + clusterConfig.Addons[0].PodIdentityAssociations = nil + cmd = makeUpdateAddonCMD() + session := cmd.Run() + Expect(session.ExitCode()).To(Equal(1)) + Expect(session.Buffer().Contents()).To(ContainSubstring("addon %s has pod identity associations,"+ + " to remove pod identity associations from an addon, addon.podIdentityAssociations must be explicitly set to []; "+ + "if the addon was migrated to use pod identity, addon.podIdentityAssociations must be set to values obtained from "+ + "`aws eks describe-pod-identity-association --cluster-name=%s", api.VPCCNIAddon, clusterConfig.Metadata.Name)) + + By(fmt.Sprintf("recreating %s using pod identity", api.AWSEBSCSIDriverAddon)) + assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 0) + cmd = params.EksctlDeleteCmd. + WithArgs( + "addon", + "--name", api.AWSEBSCSIDriverAddon, + "--region", params.Region, + "--wait", + "-v", "2", + ) + Expect(cmd).To(RunSuccessfully()) + clusterConfig.Addons = []*api.Addon{ + { + Name: api.VPCCNIAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, + }, + { + Name: api.AWSEBSCSIDriverAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "aws-ebs-csi-driver", + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"}, + }, + }, + }, + } + cmd = params.EksctlCreateCmd. + WithArgs( + "addon", + "--config-file", "-", + ). + WithoutArg("--region", params.Region). + WithStdin(clusterutils.Reader(clusterConfig)) + Expect(cmd).To(RunSuccessfullyWithOutputString( + ContainSubstring(`deploying stack "eksctl-%s-addon-%s-podidentityrole-ebs-csi-controller-sa"`, api.AWSEBSCSIDriverAddon, clusterConfig.Metadata.Name), + )) + assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 1) + + By("removing pod identity associations") + clusterConfig.Addons = []*api.Addon{ + { + Name: api.VPCCNIAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "vpc-cni", + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, + }, + { + Name: api.AWSEBSCSIDriverAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{}, + }, + } + cmd = makeUpdateAddonCMD() + Expect(cmd).To(RunSuccessfully()) + assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 0) + + cmd = params.EksctlGetCmd. + WithArgs("addon"). + WithArgs("--cluster", clusterConfig.Metadata.Name). + WithArgs("--region", params.Region) + Expect(cmd).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring("eksctl-%s-addon-vpc-cni-pod", clusterConfig.Metadata.Name)), + ContainElement(ContainSubstring("eksctl-%s-addon-aws-ebs-csi-driver-pod", clusterConfig.Metadata.Name)), + )) + }) + }) + Context("addons in a cluster with no nodes", func() { var clusterConfig *api.ClusterConfig @@ -657,6 +809,7 @@ func getRawClient(ctx context.Context, clusterName string) *kubewrapper.RawClien Expect(err).ShouldNot(HaveOccurred()) rawClient, err := ctl.NewRawClient(cfg) Expect(err).NotTo(HaveOccurred()) + awsProvider = ctl.AWSProvider return rawClient } From 8a36b288606f436df8faf5e1a8220289fb256d89 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Mon, 27 May 2024 10:44:31 +0300 Subject: [PATCH 23/35] add integration tests for creating and deleting addons && bugfixes around validations and error checking --- integration/tests/addons/addons_test.go | 380 ++++++++++-------- pkg/actions/addon/update.go | 9 +- .../podidentityassociation/addon_migrator.go | 2 +- pkg/actions/podidentityassociation/deleter.go | 1 + .../fakes/fake_stack_updater.go | 81 ++++ .../podidentityassociation/migrator.go | 50 +-- .../mocks/StackDeleter.go | 57 +++ pkg/actions/podidentityassociation/tasks.go | 43 +- pkg/cfn/builder/iam.go | 6 - pkg/cfn/template/api.go | 16 + pkg/ctl/update/addon.go | 22 + .../src/usage/pod-identity-associations.md | 31 +- 12 files changed, 468 insertions(+), 230 deletions(-) diff --git a/integration/tests/addons/addons_test.go b/integration/tests/addons/addons_test.go index 6659b1e72e..e7d3e76791 100644 --- a/integration/tests/addons/addons_test.go +++ b/integration/tests/addons/addons_test.go @@ -1,29 +1,25 @@ -//go:build integration -// +build integration - package addons import ( "bytes" "context" "encoding/json" - "errors" "fmt" + "slices" "strconv" "strings" "testing" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/cloudformation" + cfn "github.com/aws/aws-sdk-go-v2/service/cloudformation" + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" awseks "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/aws/smithy-go" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "k8s.io/utils/strings/slices" + k8sslices "k8s.io/utils/strings/slices" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -33,6 +29,7 @@ import ( . "github.com/weaveworks/eksctl/integration/runner" "github.com/weaveworks/eksctl/integration/tests" clusterutils "github.com/weaveworks/eksctl/integration/utilities/cluster" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/eks" kubewrapper "github.com/weaveworks/eksctl/pkg/kubernetes" @@ -49,7 +46,6 @@ func init() { // Call testing.Init() prior to tests.NewParams(), as otherwise -test.* will not be recognised. See also: https://golang.org/doc/go1.13#testing testing.Init() params = tests.NewParams("addons") - params.Region = "ap-northeast-2" } func TestEKSAddons(t *testing.T) { @@ -131,35 +127,6 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { return cmd }, "5m", "30s").Should(RunSuccessfullyWithOutputStringLines(ContainElement(ContainSubstring("ACTIVE")))) - By("successfully creating the aws-ebs-csi-driver addon via config file") - // setup config file - clusterConfig := getInitialClusterConfig() - clusterConfig.Addons = append(clusterConfig.Addons, &api.Addon{ - Name: api.AWSEBSCSIDriverAddon, - }) - data, err := json.Marshal(clusterConfig) - - Expect(err).NotTo(HaveOccurred()) - cmd = params.EksctlCreateCmd. - WithArgs( - "addon", - "--config-file", "-", - ). - WithoutArg("--region", params.Region). - WithStdin(bytes.NewReader(data)) - Expect(cmd).To(RunSuccessfully()) - - Eventually(func() runner.Cmd { - cmd := params.EksctlGetCmd. - WithArgs( - "addon", - "--name", api.AWSEBSCSIDriverAddon, - "--cluster", clusterName, - "--verbose", "2", - ) - return cmd - }, "10m", "30s").Should(RunSuccessfullyWithOutputStringLines(ContainElement(ContainSubstring("ACTIVE")))) - By("Deleting the kube-proxy addon") cmd = params.EksctlDeleteCmd. WithArgs( @@ -170,29 +137,16 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { ) Expect(cmd).To(RunSuccessfully()) - By("Deleting the aws-ebs-csi-driver addon") - cmd = params.EksctlDeleteCmd. - WithArgs( - "addon", - "--name", api.AWSEBSCSIDriverAddon, - "--cluster", clusterName, - "--verbose", "2", - ) - Expect(cmd).To(RunSuccessfully()) - - By("Deleting the vpc-cni addon with --preserve") + By("Deleting the vpc-cni addon") cmd = params.EksctlDeleteCmd. WithArgs( "addon", "--name", "vpc-cni", - "--preserve", "--cluster", clusterName, "--verbose", "2", ) Expect(cmd).To(RunSuccessfully()) - _, err = rawClient.ClientSet().AppsV1().DaemonSets("kube-system").Get(context.Background(), "aws-node", metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) }) It("should have full control over configMap when creating addons", func() { @@ -538,149 +492,233 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { )) }) - Context("pod identity associations", func() { - It("should manage pod identity associations for addons", func() { - output, err := awsProvider.EKS().ListPodIdentityAssociations(context.Background(), &awseks.ListPodIdentityAssociationsInput{ - ClusterName: aws.String(params.ClusterName), + Context("configure IAM permissions via pod identity associations or IRSA", Ordered, func() { + const ( + pollInterval = 10 //seconds + timeOutSeconds = 600 // 10 minutes + awsNodeSA = "aws-node" + ebsCSIControllerSA = "ebs-csi-controller-sa" + efsCSIControllerSA = "efs-csi-controller-sa" + ) + + clusterConfig := getInitialClusterConfig() + cfg := NewConfig(clusterConfig.Metadata.Region) + makeIRSAStackName := func(addonName string) string { + return fmt.Sprintf("eksctl-%s-addon-%s", clusterConfig.Metadata.Name, addonName) + } + makePodIDStackName := func(addonName, serviceAccountName string) string { + return podidentityassociation.MakeAddonPodIdentityStackName(clusterConfig.Metadata.Name, addonName, serviceAccountName) + } + makeCreateAddonCMD := func() runner.Cmd { + return params.EksctlCreateCmd. + WithArgs("addon"). + WithArgs("--config-file", "-"). + WithoutArg("--region", params.Region). + WithStdin(clusterutils.Reader(clusterConfig)) + } + makeUpdateAddonCMD := func(args ...string) runner.Cmd { + cmd := params.EksctlUpdateCmd. + WithArgs("addon"). + WithArgs("--config-file", "-"). + WithoutArg("--region", params.Region). + WithStdin(clusterutils.Reader(clusterConfig)) + if len(args) > 0 { + return cmd.WithArgs(args...) + } + return cmd + } + makeDeleteAddonCMD := func(addonName string, args ...string) runner.Cmd { + cmd := params.EksctlDeleteCmd.WithArgs( + "addon", + "--cluster", clusterConfig.Metadata.Name, + "--name", addonName, + ) + if len(args) > 0 { + return cmd.WithArgs(args...) + } + return cmd + } + assertStackExists := func(stackName string) { + Expect(cfg).To(HaveExistingStack(stackName)) + } + assertStackNotExists := func(stackName string) { + Expect(cfg).NotTo(HaveExistingStack(stackName)) + } + assertStackDeleted := func(stackName string) { + Eventually(cfg, timeOutSeconds, pollInterval).ShouldNot(HaveExistingStack(stackName)) + } + assertPodIDPresence := func(namespace, serviceAccountName string, expectPodIDExists bool) { + cmd := params.EksctlGetCmd.WithArgs( + "podidentityassociation", + "--cluster", clusterConfig.Metadata.Name, + "--namespace", namespace, + "--service-account-name", serviceAccountName, + ) + matcher := ContainElement("No podidentityassociations found") + if expectPodIDExists { + matcher = ContainElements( + ContainSubstring(namespace), + ContainSubstring(serviceAccountName), + ) + cmd = cmd.WithArgs("--output", "json") + } + Expect(cmd).To(RunSuccessfullyWithOutputStringLines(matcher)) + } + assertAddonHasPodIDs := func(addonName string, podIDsCount int) { + addon, err := awsProvider.EKS().DescribeAddon(context.Background(), &awseks.DescribeAddonInput{ + AddonName: aws.String(addonName), + ClusterName: aws.String(clusterConfig.Metadata.Name), }) - Expect(err).NotTo(HaveOccurred()) - Expect(output.Associations).To(BeEmpty()) - clusterConfig := getInitialClusterConfig() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + ExpectWithOffset(1, addon.Addon.PodIdentityAssociations).To(HaveLen(podIDsCount)) + } + + BeforeAll(func() { + clusterConfig.Addons = []*api.Addon{{Name: api.PodIdentityAgentAddon}} + Expect(makeCreateAddonCMD()).To(RunSuccessfully()) + }) + + It("should provide IAM permissions when creating addons", func() { + + By("creating pod identity associations for addons when explicitly set by user") clusterConfig.Addons = []*api.Addon{ { - Name: "vpc-cni", + Name: api.VPCCNIAddon, PodIdentityAssociations: &[]api.PodIdentityAssociation{ { Namespace: "kube-system", - ServiceAccountName: "vpc-cni", + ServiceAccountName: awsNodeSA, PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, }, }, }, } + Expect(makeCreateAddonCMD()).To(RunSuccessfully()) + assertAddonHasPodIDs(api.VPCCNIAddon, 1) + assertStackExists(makePodIDStackName(api.VPCCNIAddon, awsNodeSA)) + assertStackNotExists(makeIRSAStackName(api.VPCCNIAddon)) - makeUpdateAddonCMD := func() runner.Cmd { - return params.EksctlUpdateCmd. - WithArgs("addon"). - WithArgs("--config-file", "-"). - WithoutArg("--region", params.Region). - WithStdin(clusterutils.Reader(clusterConfig)) - } - By("updating addon to use pod identity") - assertAddonHasPodIDs := func(addonName string, podIDsCount int) { - addon, err := awsProvider.EKS().DescribeAddon(context.Background(), &awseks.DescribeAddonInput{ - AddonName: aws.String(addonName), - ClusterName: aws.String(clusterConfig.Metadata.Name), - }) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - ExpectWithOffset(1, addon.Addon.PodIdentityAssociations).To(HaveLen(podIDsCount)) + By("creating pod identity associations for addons when `autoCreate:true` and addon supports podIDs") + clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.Addons = []*api.Addon{{Name: api.AWSEBSCSIDriverAddon}} + Expect(makeCreateAddonCMD()).To(RunSuccessfully()) + assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 1) + assertStackExists(makePodIDStackName(api.AWSEBSCSIDriverAddon, ebsCSIControllerSA)) + assertStackNotExists(makeIRSAStackName(api.AWSEBSCSIDriverAddon)) + + By("falling back to IRSA when `autoCreate:true` but addon doesn't support podIDs") + clusterConfig.Addons = []*api.Addon{{Name: api.AWSEFSCSIDriverAddon}} + Expect(makeCreateAddonCMD()).To(RunSuccessfully()) + assertAddonHasPodIDs(api.AWSEFSCSIDriverAddon, 0) + assertStackNotExists(makePodIDStackName(api.AWSEFSCSIDriverAddon, efsCSIControllerSA)) + assertStackExists(makeIRSAStackName(api.AWSEFSCSIDriverAddon)) + }) + + It("should remove IAM permissions when deleting addons", func() { + + By("deleting pod identity associations and IAM role when deleting addon") + Expect(makeDeleteAddonCMD(api.VPCCNIAddon)).To(RunSuccessfully()) + assertPodIDPresence("kube-system", awsNodeSA, false) + assertStackDeleted(makePodIDStackName(api.VPCCNIAddon, awsNodeSA)) + + By("keeping pod identity associations and IAM role when deleting addon with preserve") + Expect(makeDeleteAddonCMD(api.AWSEBSCSIDriverAddon, "--preserve")).To(RunSuccessfully()) + assertPodIDPresence("kube-system", ebsCSIControllerSA, true) + assertStackExists(makePodIDStackName(api.AWSEBSCSIDriverAddon, ebsCSIControllerSA)) + _, err := rawClient.ClientSet().AppsV1().DaemonSets("kube-system").Get(context.Background(), ebsCSIControllerSA, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) + + By("cleaning up IAM role on subsequent deletion") + Expect(makeDeleteAddonCMD(api.AWSEBSCSIDriverAddon)).To(RunSuccessfully()) + assertStackDeleted(makePodIDStackName(api.AWSEBSCSIDriverAddon, ebsCSIControllerSA)) + // now manually cleanup the pod ID to not conflict with subsequent tests + Expect(params.EksctlDeleteCmd. + WithArgs( + "podidentityassociation", + "--cluster", clusterConfig.Metadata.Name, + "--namespace", "kube-system", + "--service-account-name", ebsCSIControllerSA, + )). + To(RunSuccessfully()) + assertPodIDPresence("kube-system", ebsCSIControllerSA, false) + + By("deleting IRSA when deleting addons") + Expect(makeDeleteAddonCMD(api.AWSEFSCSIDriverAddon)).To(RunSuccessfully()) + assertPodIDPresence("kube-system", efsCSIControllerSA, false) + assertStackDeleted(makeIRSAStackName(api.AWSEFSCSIDriverAddon)) + }) + + It("should update IAM permissions when updating or migrating addons", func() { + clusterConfig.IAM.AutoCreatePodIdentityAssociations = false + clusterConfig.Addons = []*api.Addon{ + {Name: api.VPCCNIAddon}, + {Name: api.AWSEBSCSIDriverAddon}, } + + By("creating addons with IRSA") + Expect(makeCreateAddonCMD()).To(RunSuccessfully()) assertAddonHasPodIDs(api.VPCCNIAddon, 0) - irsaStackName := fmt.Sprintf("eksctl-%s-addon-%s", clusterConfig.Metadata.Name, api.VPCCNIAddon) - cmd := makeUpdateAddonCMD() - Expect(cmd).To(RunSuccessfullyWithOutputStringLines( - ContainElement(ContainSubstring("deleting old IRSA stack for addon vpc-cni")), - ContainElement(ContainSubstring("will delete stack %q", irsaStackName)), - )) - assertAddonHasPodIDs(api.VPCCNIAddon, 1) + assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 0) + assertStackExists(makeIRSAStackName(api.VPCCNIAddon)) + assertStackExists(makeIRSAStackName(api.AWSEBSCSIDriverAddon)) + assertStackNotExists(makePodIDStackName(api.VPCCNIAddon, awsNodeSA)) + assertStackNotExists(makePodIDStackName(api.AWSEBSCSIDriverAddon, ebsCSIControllerSA)) - By("ensuring the IRSA stack is deleted") - _, err = awsProvider.CloudFormation().DescribeStacks(context.Background(), &cloudformation.DescribeStacksInput{ - StackName: aws.String(irsaStackName), - }) - var opErr *smithy.OperationError - Expect(errors.As(err, &opErr) && strings.Contains(opErr.Error(), "ValidationError")).To(BeTrue(), "expected stack to not exist, err: %v", err) + By("updating the addon to use pod identity") + clusterConfig.Addons[1].PodIdentityAssociations = &[]api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: ebsCSIControllerSA, + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"}, + }, + } + Expect(makeUpdateAddonCMD()).To(RunSuccessfullyWithOutputStringLines( + ContainElement(ContainSubstring("deleting old IRSA stack for addon %s", api.AWSEBSCSIDriverAddon)), + ContainElement(ContainSubstring("will delete stack %q", makeIRSAStackName(api.AWSEBSCSIDriverAddon))), + )) + assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 1) + assertStackNotExists(makeIRSAStackName(api.AWSEBSCSIDriverAddon)) By("ensuring that updating addon again works") - cmd = makeUpdateAddonCMD() - Expect(cmd).To(RunSuccessfullyWithOutputStringLines( - Not(ContainElement(ContainSubstring("deleting old IRSA stack for addon %s", api.VPCCNIAddon))), + Expect(makeUpdateAddonCMD()).To(RunSuccessfullyWithOutputStringLines( + Not(ContainElement(ContainSubstring("deleting old IRSA stack for addon %s", api.AWSEBSCSIDriverAddon))), )) By("failing to update addon with pod identity if the field is unset") - clusterConfig.Addons[0].PodIdentityAssociations = nil - cmd = makeUpdateAddonCMD() - session := cmd.Run() + clusterConfig.Addons[1].PodIdentityAssociations = nil + session := makeUpdateAddonCMD().Run() Expect(session.ExitCode()).To(Equal(1)) - Expect(session.Buffer().Contents()).To(ContainSubstring("addon %s has pod identity associations,"+ + Expect(string(session.Err.Contents())).To(ContainSubstring("addon %s has pod identity associations,"+ " to remove pod identity associations from an addon, addon.podIdentityAssociations must be explicitly set to []; "+ "if the addon was migrated to use pod identity, addon.podIdentityAssociations must be set to values obtained from "+ - "`aws eks describe-pod-identity-association --cluster-name=%s", api.VPCCNIAddon, clusterConfig.Metadata.Name)) + "`aws eks describe-pod-identity-association --cluster-name=%s", api.AWSEBSCSIDriverAddon, clusterConfig.Metadata.Name)) - By(fmt.Sprintf("recreating %s using pod identity", api.AWSEBSCSIDriverAddon)) + By("removing all pod identity associations owned by the addon") + clusterConfig.Addons[1].PodIdentityAssociations = &[]api.PodIdentityAssociation{} + Expect(makeUpdateAddonCMD()).To(RunSuccessfully()) assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 0) - cmd = params.EksctlDeleteCmd. - WithArgs( - "addon", - "--name", api.AWSEBSCSIDriverAddon, - "--region", params.Region, - "--wait", - "-v", "2", - ) - Expect(cmd).To(RunSuccessfully()) - clusterConfig.Addons = []*api.Addon{ - { - Name: api.VPCCNIAddon, - PodIdentityAssociations: &[]api.PodIdentityAssociation{ - { - Namespace: "kube-system", - ServiceAccountName: "vpc-cni", - PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, - }, - }, - }, - { - Name: api.AWSEBSCSIDriverAddon, - PodIdentityAssociations: &[]api.PodIdentityAssociation{ - { - Namespace: "kube-system", - ServiceAccountName: "aws-ebs-csi-driver", - PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/service-role/AmazonEBSCSIDriverPolicy"}, - }, - }, - }, - } - cmd = params.EksctlCreateCmd. - WithArgs( - "addon", - "--config-file", "-", - ). - WithoutArg("--region", params.Region). - WithStdin(clusterutils.Reader(clusterConfig)) - Expect(cmd).To(RunSuccessfullyWithOutputString( - ContainSubstring(`deploying stack "eksctl-%s-addon-%s-podidentityrole-ebs-csi-controller-sa"`, api.AWSEBSCSIDriverAddon, clusterConfig.Metadata.Name), - )) - assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 1) - By("removing pod identity associations") - clusterConfig.Addons = []*api.Addon{ - { - Name: api.VPCCNIAddon, - PodIdentityAssociations: &[]api.PodIdentityAssociation{ - { - Namespace: "kube-system", - ServiceAccountName: "vpc-cni", - PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, - }, - }, - }, - { - Name: api.AWSEBSCSIDriverAddon, - PodIdentityAssociations: &[]api.PodIdentityAssociation{}, - }, + By("migrating an addon to pod identity using the utils command") + Expect(params.EksctlUtilsCmd. + WithArgs( + "migrate-to-pod-identity", + "--cluster", clusterConfig.Metadata.Name, + "--approve", + )).To(RunSuccessfully()) + assertAddonHasPodIDs(api.VPCCNIAddon, 1) + // assert IRSA stack still exists, but tags reflect association with podID + stackHasTag := func(stack cfntypes.Stack, tag string) bool { + return slices.ContainsFunc(stack.Tags, func(t cfntypes.Tag) bool { + return *t.Key == tag + }) } - cmd = makeUpdateAddonCMD() - Expect(cmd).To(RunSuccessfully()) - assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 0) - - cmd = params.EksctlGetCmd. - WithArgs("addon"). - WithArgs("--cluster", clusterConfig.Metadata.Name). - WithArgs("--region", params.Region) - Expect(cmd).To(RunSuccessfullyWithOutputStringLines( - ContainElement(ContainSubstring("eksctl-%s-addon-vpc-cni-pod", clusterConfig.Metadata.Name)), - ContainElement(ContainSubstring("eksctl-%s-addon-aws-ebs-csi-driver-pod", clusterConfig.Metadata.Name)), - )) + describeStackOutput, err := awsProvider.CloudFormation().DescribeStacks(context.Background(), &cfn.DescribeStacksInput{ + StackName: aws.String(makeIRSAStackName(api.VPCCNIAddon)), + }) + Expect(err).NotTo(HaveOccurred()) + Expect(describeStackOutput.Stacks).To(HaveLen(1)) + Expect(stackHasTag(describeStackOutput.Stacks[0], api.IAMServiceAccountNameTag)).To(BeFalse()) + Expect(stackHasTag(describeStackOutput.Stacks[0], api.PodIdentityAssociationNameTag)).To(BeTrue()) }) }) @@ -836,8 +874,8 @@ func getInitialClusterConfig() *api.ClusterConfig { return clusterConfig } -func getConfigMap(clientset kubernetes.Interface, name string) *corev1.ConfigMap { - configMap, err := clientset.CoreV1().ConfigMaps("kube-system").Get(context.Background(), "coredns", metav1.GetOptions{}) +func getConfigMap(clientset kubernetes.Interface, addonName string) *corev1.ConfigMap { + configMap, err := clientset.CoreV1().ConfigMaps("kube-system").Get(context.Background(), addonName, metav1.GetOptions{}) Expect(err).NotTo(HaveOccurred()) return configMap } @@ -853,7 +891,7 @@ func getCacheValue(configMap *corev1.ConfigMap) string { Expect(ok).To(BeTrue()) coreFileValues := strings.Fields(strings.Replace(coreFile, "\n", " ", -1)) - return coreFileValues[slices.Index(coreFileValues, "cache")+1] + return coreFileValues[k8sslices.Index(coreFileValues, "cache")+1] } func updateCacheValue(configMap *corev1.ConfigMap, currentValue string, newValue string) { diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index c457ac27b4..0089f2b276 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -3,10 +3,9 @@ package addon import ( "context" "fmt" + "slices" "time" - "golang.org/x/exp/slices" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" @@ -84,7 +83,9 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp deleteServiceAccountIAMResources = append(deleteServiceAccountIAMResources, pia.ServiceAccount) } } - if len(deleteServiceAccountIAMResources) == 0 { + // to delete all pod IDs for the addon, explicitly set input.PodIdentityAssociations = [] + if len(*addon.PodIdentityAssociations) == 0 { + logger.Info("addon.podIdentityAssociations is explicitly set to []; all pod identity associations corresponding to addon %s will be deleted", addon.Name) updateAddonInput.PodIdentityAssociations = []ekstypes.AddonPodIdentityAssociations{} } } @@ -140,7 +141,7 @@ func (a *Manager) updateWithNewPolicies(ctx context.Context, addon *api.Addon) ( stackName := a.makeAddonName(addon.Name) stack, err := a.stackManager.DescribeStack(ctx, &manager.Stack{StackName: aws.String(stackName)}) if err != nil { - if manager.IsStackDoesNotExistError(err) { + if !manager.IsStackDoesNotExistError(err) { return "", fmt.Errorf("failed to get stack: %w", err) } } diff --git a/pkg/actions/podidentityassociation/addon_migrator.go b/pkg/actions/podidentityassociation/addon_migrator.go index ed2c273a4a..44be37d372 100644 --- a/pkg/actions/podidentityassociation/addon_migrator.go +++ b/pkg/actions/podidentityassociation/addon_migrator.go @@ -81,7 +81,7 @@ func (a *AddonMigrator) migrateAddon(ctx context.Context, addon *ekstypes.Addon, return nil, nil } - logger.Info("migrating addon %s with serviceAccountRoleARN %s to pod identity; OIDC provider trust relationship will also be removed", *addon.AddonName, *addon.ServiceAccountRoleArn) + logger.Info("will migrate addon %s with serviceAccountRoleARN %q to pod identity; OIDC provider trust relationship will also be removed", *addon.AddonName, *addon.ServiceAccountRoleArn) roleName, err := api.RoleNameFromARN(serviceAccountRoleARN) if err != nil { return nil, fmt.Errorf("parsing role ARN %s: %w", serviceAccountRoleARN, err) diff --git a/pkg/actions/podidentityassociation/deleter.go b/pkg/actions/podidentityassociation/deleter.go index 8abfca8087..6573b2efba 100644 --- a/pkg/actions/podidentityassociation/deleter.go +++ b/pkg/actions/podidentityassociation/deleter.go @@ -24,6 +24,7 @@ import ( type StackLister interface { ListPodIdentityStackNames(ctx context.Context) ([]string, error) DescribeStack(ctx context.Context, stack *manager.Stack) (*manager.Stack, error) + GetStackTemplate(ctx context.Context, stackName string) (string, error) GetIAMServiceAccounts(ctx context.Context) ([]*api.ClusterIAMServiceAccount, error) } diff --git a/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go b/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go index 1760dde011..fb913d3173 100644 --- a/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go +++ b/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go @@ -39,6 +39,20 @@ type FakeStackUpdater struct { result1 []*v1alpha5.ClusterIAMServiceAccount result2 error } + GetStackTemplateStub func(context.Context, string) (string, error) + getStackTemplateMutex sync.RWMutex + getStackTemplateArgsForCall []struct { + arg1 context.Context + arg2 string + } + getStackTemplateReturns struct { + result1 string + result2 error + } + getStackTemplateReturnsOnCall map[int]struct { + result1 string + result2 error + } ListPodIdentityStackNamesStub func(context.Context) ([]string, error) listPodIdentityStackNamesMutex sync.RWMutex listPodIdentityStackNamesArgsForCall []struct { @@ -197,6 +211,71 @@ func (fake *FakeStackUpdater) GetIAMServiceAccountsReturnsOnCall(i int, result1 }{result1, result2} } +func (fake *FakeStackUpdater) GetStackTemplate(arg1 context.Context, arg2 string) (string, error) { + fake.getStackTemplateMutex.Lock() + ret, specificReturn := fake.getStackTemplateReturnsOnCall[len(fake.getStackTemplateArgsForCall)] + fake.getStackTemplateArgsForCall = append(fake.getStackTemplateArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.GetStackTemplateStub + fakeReturns := fake.getStackTemplateReturns + fake.recordInvocation("GetStackTemplate", []interface{}{arg1, arg2}) + fake.getStackTemplateMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackUpdater) GetStackTemplateCallCount() int { + fake.getStackTemplateMutex.RLock() + defer fake.getStackTemplateMutex.RUnlock() + return len(fake.getStackTemplateArgsForCall) +} + +func (fake *FakeStackUpdater) GetStackTemplateCalls(stub func(context.Context, string) (string, error)) { + fake.getStackTemplateMutex.Lock() + defer fake.getStackTemplateMutex.Unlock() + fake.GetStackTemplateStub = stub +} + +func (fake *FakeStackUpdater) GetStackTemplateArgsForCall(i int) (context.Context, string) { + fake.getStackTemplateMutex.RLock() + defer fake.getStackTemplateMutex.RUnlock() + argsForCall := fake.getStackTemplateArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeStackUpdater) GetStackTemplateReturns(result1 string, result2 error) { + fake.getStackTemplateMutex.Lock() + defer fake.getStackTemplateMutex.Unlock() + fake.GetStackTemplateStub = nil + fake.getStackTemplateReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) GetStackTemplateReturnsOnCall(i int, result1 string, result2 error) { + fake.getStackTemplateMutex.Lock() + defer fake.getStackTemplateMutex.Unlock() + fake.GetStackTemplateStub = nil + if fake.getStackTemplateReturnsOnCall == nil { + fake.getStackTemplateReturnsOnCall = make(map[int]struct { + result1 string + result2 error + }) + } + fake.getStackTemplateReturnsOnCall[i] = struct { + result1 string + result2 error + }{result1, result2} +} + func (fake *FakeStackUpdater) ListPodIdentityStackNames(arg1 context.Context) ([]string, error) { fake.listPodIdentityStackNamesMutex.Lock() ret, specificReturn := fake.listPodIdentityStackNamesReturnsOnCall[len(fake.listPodIdentityStackNamesArgsForCall)] @@ -330,6 +409,8 @@ func (fake *FakeStackUpdater) Invocations() map[string][][]interface{} { defer fake.describeStackMutex.RUnlock() fake.getIAMServiceAccountsMutex.RLock() defer fake.getIAMServiceAccountsMutex.RUnlock() + fake.getStackTemplateMutex.RLock() + defer fake.getStackTemplateMutex.RUnlock() fake.listPodIdentityStackNamesMutex.RLock() defer fake.listPodIdentityStackNamesMutex.RUnlock() fake.mustUpdateStackMutex.RLock() diff --git a/pkg/actions/podidentityassociation/migrator.go b/pkg/actions/podidentityassociation/migrator.go index b762e89aca..b5608afe6d 100644 --- a/pkg/actions/podidentityassociation/migrator.go +++ b/pkg/actions/podidentityassociation/migrator.go @@ -96,14 +96,8 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity return fmt.Errorf("listing k8s service accounts: %w", err) } - updateTrustPolicyTasks := tasks.TaskTree{ - Parallel: true, - IsSubTask: true, - } - removeIRSAv1AnnotationTasks := tasks.TaskTree{ - Parallel: true, - IsSubTask: true, - } + updateTrustPolicyTasks := []tasks.Task{} + removeIRSAv1AnnotationTasks := []tasks.Task{} toBeCreated := []api.PodIdentityAssociation{} addonServiceAccountRoleMapper, err := CreateAddonServiceAccountRoleMapper(ctx, m.clusterName, m.eksAPI) @@ -117,9 +111,11 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity for _, sa := range serviceAccounts.Items { if roleARN, ok := sa.Annotations[api.AnnotationEKSRoleARN]; ok { if mappedAddon := addonServiceAccountRoleMapper.AddonForServiceAccountRole(roleARN); mappedAddon != nil { - logger.Info("found service account %s but it is associated with EKS addon %s", sa.Name, *mappedAddon.AddonName) + logger.Info("found IAM role for service account %s associated with EKS addon %s", sa.Name, *mappedAddon.AddonName) continue } + logger.Info("found IAM role for service account %s/%s", sa.Namespace, sa.Name) + // collect pod identity associations that need to be created toBeCreated = append(toBeCreated, api.PodIdentityAssociation{ ServiceAccountName: sa.Name, @@ -135,12 +131,11 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity // add updateTrustPolicyTasks if stackSummary, hasStack := resolver.GetStack(roleARN); hasStack { - updateTrustPolicyTasks.Append( + updateTrustPolicyTasks = append(updateTrustPolicyTasks, policyUpdater.UpdateTrustPolicyForOwnedRoleTask(ctx, roleName, "", stackSummary, options.RemoveOIDCProviderTrustRelationship), ) - } else { - updateTrustPolicyTasks.Append( + updateTrustPolicyTasks = append(updateTrustPolicyTasks, policyUpdater.UpdateTrustPolicyForUnownedRoleTask(ctx, roleName, options.RemoveOIDCProviderTrustRelationship), ) } @@ -154,7 +149,7 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity saCopy := &corev1.ServiceAccount{ ObjectMeta: sa.ObjectMeta, } - removeIRSAv1AnnotationTasks.Append(&tasks.GenericTask{ + removeIRSAv1AnnotationTasks = append(removeIRSAv1AnnotationTasks, &tasks.GenericTask{ Description: fmt.Sprintf("remove iamserviceaccount EKS role annotation for %q", saNameString), Doer: func() error { delete(saCopy.Annotations, api.AnnotationEKSRoleARN) @@ -181,27 +176,34 @@ func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentity if err != nil { return fmt.Errorf("error migrating addons to use pod identity: %w", err) } - if addonMigrationTasks.Len() == 0 && updateTrustPolicyTasks.Len() == 0 { + if addonMigrationTasks.Len() == 0 && len(toBeCreated) == 0 { logger.Info("no iamserviceaccounts or addons found to migrate to pod identity") return nil } - if updateTrustPolicyTasks.Len() > 0 { - taskTree.Append(&updateTrustPolicyTasks) - } + // add tasks to migrate addons if addonMigrationTasks.Len() > 0 { addonMigrationTasks.IsSubTask = true taskTree.Append(addonMigrationTasks) } - if removeIRSAv1AnnotationTasks.Len() > 0 { - taskTree.Append(&removeIRSAv1AnnotationTasks) - } - // add tasks to create pod identity associations + // add tasks to migrate iamserviceaccounts + iamserviceaccountMigrationTasks := &tasks.TaskTree{ + Parallel: true, + IsSubTask: true, + } createAssociationsTasks := NewCreator(m.clusterName, nil, m.eksAPI, m.clientSet).CreateTasks(ctx, toBeCreated, true) - if createAssociationsTasks.Len() > 0 { - createAssociationsTasks.IsSubTask = true - taskTree.Append(createAssociationsTasks) + for i := range toBeCreated { + subTasks := &tasks.TaskTree{IsSubTask: true} + subTasks.Append(updateTrustPolicyTasks[i]) + if len(removeIRSAv1AnnotationTasks) > 0 { + subTasks.Append(removeIRSAv1AnnotationTasks[i]) + } + subTasks.Append(createAssociationsTasks.Tasks[i]) + iamserviceaccountMigrationTasks.Append(subTasks) + } + if iamserviceaccountMigrationTasks.Len() > 0 { + taskTree.Append(iamserviceaccountMigrationTasks) } // add suggestive logs diff --git a/pkg/actions/podidentityassociation/mocks/StackDeleter.go b/pkg/actions/podidentityassociation/mocks/StackDeleter.go index 78b583f85d..8975a543ee 100644 --- a/pkg/actions/podidentityassociation/mocks/StackDeleter.go +++ b/pkg/actions/podidentityassociation/mocks/StackDeleter.go @@ -190,6 +190,63 @@ func (_c *StackDeleter_GetIAMServiceAccounts_Call) RunAndReturn(run func(context return _c } +// GetStackTemplate provides a mock function with given fields: ctx, stackName +func (_m *StackDeleter) GetStackTemplate(ctx context.Context, stackName string) (string, error) { + ret := _m.Called(ctx, stackName) + + if len(ret) == 0 { + panic("no return value specified for GetStackTemplate") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, stackName) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, stackName) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, stackName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// StackDeleter_GetStackTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetStackTemplate' +type StackDeleter_GetStackTemplate_Call struct { + *mock.Call +} + +// GetStackTemplate is a helper method to define mock.On call +// - ctx context.Context +// - stackName string +func (_e *StackDeleter_Expecter) GetStackTemplate(ctx interface{}, stackName interface{}) *StackDeleter_GetStackTemplate_Call { + return &StackDeleter_GetStackTemplate_Call{Call: _e.mock.On("GetStackTemplate", ctx, stackName)} +} + +func (_c *StackDeleter_GetStackTemplate_Call) Run(run func(ctx context.Context, stackName string)) *StackDeleter_GetStackTemplate_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *StackDeleter_GetStackTemplate_Call) Return(_a0 string, _a1 error) *StackDeleter_GetStackTemplate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *StackDeleter_GetStackTemplate_Call) RunAndReturn(run func(context.Context, string) (string, error)) *StackDeleter_GetStackTemplate_Call { + _c.Call.Return(run) + return _c +} + // ListPodIdentityStackNames provides a mock function with given fields: ctx func (_m *StackDeleter) ListPodIdentityStackNames(ctx context.Context) ([]string, error) { ret := _m.Called(ctx) diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index 68fe14b323..7f9783c2bd 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -18,11 +18,15 @@ import ( api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/awsapi" - "github.com/weaveworks/eksctl/pkg/cfn/builder" "github.com/weaveworks/eksctl/pkg/cfn/manager" + cft "github.com/weaveworks/eksctl/pkg/cfn/template" "github.com/weaveworks/eksctl/pkg/utils/tasks" ) +const ( + resourceTypeIAMRole = "AWS::IAM::Role" +) + type createPodIdentityAssociationTask struct { ctx context.Context info string @@ -78,15 +82,34 @@ func (t *trustPolicyUpdater) UpdateTrustPolicyForOwnedRoleTask(ctx context.Conte return fmt.Errorf("updating trust statements for role %s: %w", roleName, err) } - // build template for updating trust policy - rs := builder.NewIAMRoleResourceSetForPodIdentityWithTrustStatements(&api.PodIdentityAssociation{}, trustStatements) - if err := rs.AddAllResources(); err != nil { - return fmt.Errorf("adding resources to CloudFormation template: %w", err) + currentTemplate, err := t.stackUpdater.GetStackTemplate(ctx, stack.Name) + if err != nil { + return fmt.Errorf("fetching current template for stack %q", stack.Name) + } + + cfnTemplate := cft.NewTemplate() + if err := cfnTemplate.LoadJSON([]byte(currentTemplate)); err != nil { + return fmt.Errorf("unmarshalling current template for stack %q", stack.Name) } - template, err := rs.RenderJSON() + + for i, r := range cfnTemplate.Resources { + if r.Type != resourceTypeIAMRole { + continue + } + role, err := r.ToIAMRole() + if err != nil { + return fmt.Errorf("fetching properties for role %s: %w", roleName, err) + } + role.AssumeRolePolicyDocument["Statement"] = trustStatements + r.Properties = role + cfnTemplate.Resources[i] = r + } + + updatedTemplate, err := cfnTemplate.RenderJSON() if err != nil { - return fmt.Errorf("generating CloudFormation template: %w", err) + return fmt.Errorf("marshalling updated template for stack %q", stack.Name) } + logger.Debug("updated template for role %s: %v", string(updatedTemplate)) // update stack tags to reflect migration to IRSAv2 cfnTags := []cfntypes.Tag{} @@ -95,8 +118,8 @@ func (t *trustPolicyUpdater) UpdateTrustPolicyForOwnedRoleTask(ctx context.Conte continue } cfnTags = append(cfnTags, cfntypes.Tag{ - Key: &key, - Value: &value, + Key: aws.String(key), + Value: aws.String(value), }) } @@ -125,7 +148,7 @@ func (t *trustPolicyUpdater) UpdateTrustPolicyForOwnedRoleTask(ctx context.Conte }, ChangeSetName: fmt.Sprintf("eksctl-%s-update-%d", roleName, time.Now().Unix()), Description: fmt.Sprintf("updating IAM resources stack %q for role %q", stack.Name, roleName), - TemplateData: manager.TemplateBody(template), + TemplateData: manager.TemplateBody(updatedTemplate), Wait: true, }); err != nil { var noChangeErr *manager.NoChangeError diff --git a/pkg/cfn/builder/iam.go b/pkg/cfn/builder/iam.go index 422802c064..b534edfa46 100644 --- a/pkg/cfn/builder/iam.go +++ b/pkg/cfn/builder/iam.go @@ -217,12 +217,6 @@ func NewIAMRoleResourceSetForServiceAccount(spec *api.ClusterIAMServiceAccount, } } -func NewIAMRoleResourceSetForPodIdentityWithTrustStatements(spec *api.PodIdentityAssociation, trustStatements []api.IAMStatement) *IAMRoleResourceSet { - rs := NewIAMRoleResourceSetForPodIdentity(spec) - rs.trustStatements = trustStatements - return rs -} - func NewIAMRoleResourceSetForPodIdentity(spec *api.PodIdentityAssociation) *IAMRoleResourceSet { return &IAMRoleResourceSet{ template: cft.NewTemplate(), diff --git a/pkg/cfn/template/api.go b/pkg/cfn/template/api.go index efa1752439..e46568a51a 100644 --- a/pkg/cfn/template/api.go +++ b/pkg/cfn/template/api.go @@ -2,6 +2,7 @@ package template import ( "encoding/json" + "fmt" ) // Commonly-used constants @@ -31,6 +32,21 @@ type AnyResource struct { Properties interface{} } +func (r *AnyResource) ToIAMRole() (IAMRole, error) { + var role IAMRole + if r.Type != "AWS::IAM::Role" { + return role, fmt.Errorf("cannot convert resource of type %s to AWS::IAM::ROLE", r.Type) + } + bytes, err := json.Marshal(r.Properties) + if err != nil { + return role, err + } + if err := json.Unmarshal(bytes, &role); err != nil { + return role, err + } + return role, nil +} + type ( // Output represents a CloudFormation output definition Output struct { diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index 4b1def2e3a..509c221e66 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -13,6 +13,7 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/awsapi" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" ) @@ -73,6 +74,10 @@ func updateAddon(cmd *cmdutils.Cmd, force, wait bool) error { logger.Warning("no IAM OIDC provider associated with cluster, try 'eksctl utils associate-iam-oidc-provider --region=%s --cluster=%s'", cmd.ClusterConfig.Metadata.Region, cmd.ClusterConfig.Metadata.Name) } + if err := validatePodIdentityAgentAddon(ctx, clusterProvider.AWSProvider.EKS(), cmd.ClusterConfig); err != nil { + return err + } + stackManager := clusterProvider.NewStackManager(cmd.ClusterConfig) output, err := clusterProvider.AWSProvider.EKS().DescribeCluster(ctx, &awseks.DescribeClusterInput{ @@ -116,3 +121,20 @@ func updateAddon(cmd *cmdutils.Cmd, force, wait bool) error { return nil } + +func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg *api.ClusterConfig) error { + if isPodIdentityAgentInstalled, err := podidentityassociation.IsPodIdentityAgentInstalled(ctx, eksAPI, cfg.Metadata.Name); err != nil { + return fmt.Errorf("checking if %q addon is installed on the cluster: %w", api.PodIdentityAgentAddon, err) + } else if isPodIdentityAgentInstalled { + return nil + } + + for _, a := range cfg.Addons { + if a.HasPodIDsSet() { + suggestion := fmt.Sprintf("please enable it using `eksctl create addon --cluster=%s --name=%s`, or by adding it to the config file", cfg.Metadata.Name, api.PodIdentityAgentAddon) + return api.ErrPodIdentityAgentNotInstalled(suggestion) + } + } + + return nil +} diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md index 2c51d27eeb..76aee86867 100644 --- a/userdocs/src/usage/pod-identity-associations.md +++ b/userdocs/src/usage/pod-identity-associations.md @@ -355,33 +355,36 @@ Running the command without the `--approve` flag will only output a plan consist ```bash [ℹ] (plan) would migrate 2 iamserviceaccount(s) and 2 addon(s) to pod identity association(s) by executing the following tasks [ℹ] (plan) -3 sequential tasks: { install eks-pod-identity-agent addon, - ## tasks for migrating the iamserviceaccounts - 2 parallel sub-tasks: { - update trust policy for owned role "eksctl-my-cluster-addon-iamserv-Role1-beYhlhzpwQte", - update trust policy for unowned role "Unowned-Role1", - }, - 2 parallel sub-tasks: { - create pod identity association for service account "default/sa1", - create pod identity association for service account "default/sa2", - }, + +3 sequential tasks: { install eks-pod-identity-agent addon, ## tasks for migrating the addons 2 parallel sub-tasks: { 2 sequential sub-tasks: { - update trust policy for owned role "eksctl-my-cluster-addon-aws-ebs-csi-d-Role1-9BMT7CgeSNvX", + update trust policy for owned role "eksctl-my-cluster--Role1-DDuMLoeZ8weD", migrate addon aws-ebs-csi-driver to pod identity, }, 2 sequential sub-tasks: { - update trust policy for owned role "eksctl-my-cluster-addon-vpc-cni-Role1-ePPlktZv2kjo", + update trust policy for owned role "eksctl-my-cluster--Role1-xYiPFOVp1aeI", migrate addon vpc-cni to pod identity, }, - }, + }, + ## tasks for migrating the iamserviceaccounts + 2 parallel sub-tasks: { + 2 sequential sub-tasks: { + update trust policy for owned role "eksctl-my-cluster--Role1-QLXqHcq9O1AR", + create pod identity association for service account "default/sa1", + }, + 2 sequential sub-tasks: { + update trust policy for unowned role "Unowned-Role1", + create pod identity association for service account "default/sa2", + }, + } } [ℹ] all tasks were skipped [!] no changes were applied, run again with '--approve' to apply the changes ``` -The existing OIDC provider trust relationship is always being deleted from IAM Roles associated with EKS Add-ons. Additionally, to delete the existing OIDC provider trust relationship from IAM Roles associated with iamserviceaccounts, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. +The existing OIDC provider trust relationship is always being removed from IAM Roles associated with EKS Add-ons. Additionally, to remove the existing OIDC provider trust relationship from IAM Roles associated with iamserviceaccounts, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. ``` eksctl utils migrate-to-pod-identity --cluster my-cluster --approve --remove-oidc-provider-trust-relationship From 4bb1a7d0d899fb2fa478e9562c6cf48e8a1bdb8e Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Mon, 27 May 2024 10:45:13 +0300 Subject: [PATCH 24/35] update describe addon config command to return pod identity config --- pkg/ctl/utils/describe_addon_configuration.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/ctl/utils/describe_addon_configuration.go b/pkg/ctl/utils/describe_addon_configuration.go index 5bc13d8e44..af30dd52c0 100644 --- a/pkg/ctl/utils/describe_addon_configuration.go +++ b/pkg/ctl/utils/describe_addon_configuration.go @@ -2,10 +2,12 @@ package utils import ( "context" + "encoding/json" "fmt" "github.com/aws/aws-sdk-go-v2/aws" awseks "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/aws/aws-sdk-go-v2/service/eks/types" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -61,6 +63,21 @@ func describeAddonConfiguration(cmd *cmdutils.Cmd, addonName, addonVersion strin return fmt.Errorf("no configuration schema found for %s@%s", addonName, addonVersion) } - fmt.Println(*addonConfig.ConfigurationSchema) + var schema interface{} + if err := json.Unmarshal([]byte(*addonConfig.ConfigurationSchema), &schema); err != nil { + return fmt.Errorf("unmarshalling retrieved addon configuration schema: %w", err) + } + config, err := json.MarshalIndent(struct { + Schema any `json:"configurationSchema"` + PodIDConfig []types.AddonPodIdentityConfiguration `json:"podIdentityConfiguration"` + }{ + Schema: schema, + PodIDConfig: addonConfig.PodIdentityConfiguration, + }, "", "\t") + if err != nil { + return fmt.Errorf("marshalling retrieved addon configuration: %w", err) + } + fmt.Println(string(config)) + return nil } From c63f625352fc28a7d569fa3872b4cf2937a731b6 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Mon, 27 May 2024 13:02:26 +0300 Subject: [PATCH 25/35] add auto-create-pod-identity-associations CLI flag --- pkg/ctl/cmdutils/addon.go | 1 + pkg/ctl/create/addon.go | 1 + userdocs/src/usage/pod-identity-associations.md | 9 ++++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/ctl/cmdutils/addon.go b/pkg/ctl/cmdutils/addon.go index 58c36103b7..13b58ca28c 100644 --- a/pkg/ctl/cmdutils/addon.go +++ b/pkg/ctl/cmdutils/addon.go @@ -9,6 +9,7 @@ var addonFlagsIncompatibleWithConfigFile = []string{ "version", "service-account-role-arn", "attach-policy-arn", + "auto-create-pod-identity-associations", } func NewCreateOrUpgradeAddonLoader(cmd *Cmd) ClusterConfigLoader { diff --git a/pkg/ctl/create/addon.go b/pkg/ctl/create/addon.go index 013d5d15fc..415e9fb2b6 100644 --- a/pkg/ctl/create/addon.go +++ b/pkg/ctl/create/addon.go @@ -33,6 +33,7 @@ func createAddonCmd(cmd *cmdutils.Cmd) { fs.StringVar(&cmd.ClusterConfig.Addons[0].Name, "name", "", "Add-on name") fs.StringVar(&cmd.ClusterConfig.Addons[0].Version, "version", "", "Add-on version. Use `eksctl utils describe-addon-versions` to discover a version or set to \"latest\"") fs.StringVar(&cmd.ClusterConfig.Addons[0].ServiceAccountRoleARN, "service-account-role-arn", "", "Add-on serviceAccountRoleARN") + fs.BoolVar(&cmd.ClusterConfig.IAM.AutoCreatePodIdentityAssociations, "auto-create-pod-identity-associations", false, "create recommended pod identity associations for the addon(s), if supported") fs.BoolVar(&force, "force", false, "Force migrates an existing self-managed add-on to an EKS managed add-on") fs.BoolVar(&wait, "wait", false, "Wait for the addon creation to complete") diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md index 76aee86867..e705be1b84 100644 --- a/userdocs/src/usage/pod-identity-associations.md +++ b/userdocs/src/usage/pod-identity-associations.md @@ -199,7 +199,8 @@ For EKS Add-ons that support pod identities, `eksctl` offers the option to autom ```yaml iam: autoCreatePodIdentityAssociations: true -# bear in mind that if either pod identity or IRSA configuration is explicitly set in the config file, +# bear in mind that if either pod identity or IRSA configuration is explicitly set in the config file, +# or if the addon does not support pod identities, # iam.autoCreatePodIdentityAssociations won't have any effect. addons: - name: vpc-cni @@ -212,6 +213,12 @@ eksctl create addon -f config.yaml 2024-05-13 15:38:58 [ℹ] "iam.AutoCreatePodIdentityAssociations" is set to true; will lookup recommended pod identity configuration for "vpc-cni" addon ``` +Equivalently, the same can be done via CLI flags e.g. + +```bash +eksctl create addon --cluster my-cluster --name vpc-cni --auto-create-pod-identity-associations +``` + ### Updating addons with IAM permissions When updating an addon, specifying `addon.PodIdentityAssociations` will represent the single source of truth for the state that the addon shall have, after the update operation is completed. Behind the scenes, different types of operations are performed in order to achieve the desired state i.e. From 80ecc7bac293ce4307bb3743b70c10f42cd143bd Mon Sep 17 00:00:00 2001 From: tiberiugc Date: Mon, 27 May 2024 18:05:13 +0300 Subject: [PATCH 26/35] update unit tests --- .../podidentityassociation/migrator_test.go | 88 +++++++++++++++---- userdocs/src/usage/minimum-iam-policies.md | 1 + 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/pkg/actions/podidentityassociation/migrator_test.go b/pkg/actions/podidentityassociation/migrator_test.go index beeb26a261..bb54a7a095 100644 --- a/pkg/actions/podidentityassociation/migrator_test.go +++ b/pkg/actions/podidentityassociation/migrator_test.go @@ -66,25 +66,6 @@ var _ = Describe("Create", func() { genericErr = fmt.Errorf("ERR") ) - var policyDocument = aws.String(`{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Principal": { - "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/test" - }, - "Action": "sts:AssumeRoleWithWebIdentity", - "Condition": { - "StringEquals": { - "oidc.eks.eu-north-1.amazonaws.com/id/test:sub": "system:serviceaccount:default:service-account-1", - "oidc.eks.eu-north-1.amazonaws.com/id/test:aud": "sts.amazonaws.com" - } - } - } - ] - }`) - mockDescribeAddon := func(provider *mockprovider.MockProvider, err error) { mockProvider.MockEKS(). On("DescribeAddon", mock.Anything, mock.Anything). @@ -290,6 +271,9 @@ var _ = Describe("Create", func() { }, }, nil) + stackUpdater.GetStackTemplateReturnsOnCall(0, iamRoleStackTemplate(nsDefault, sa1), nil) + stackUpdater.GetStackTemplateReturnsOnCall(1, iamRoleStackTemplate(nsDefault, sa2), nil) + stackUpdater.MustUpdateStackStub = func(ctx context.Context, options manager.UpdateStackOptions) error { Expect(options.Stack).NotTo(BeNil()) Expect(options.Stack.Tags).To(ConsistOf([]cfntypes.Tag{ @@ -299,6 +283,9 @@ var _ = Describe("Create", func() { }, })) Expect(options.Stack.Capabilities).To(ConsistOf([]cfntypes.Capability{"CAPABILITY_IAM"})) + template := string(options.TemplateData.(manager.TemplateBody)) + Expect(template).To(ContainSubstring(api.EKSServicePrincipal)) + Expect(template).NotTo(ContainSubstring("oidc")) return nil } }, @@ -313,3 +300,66 @@ var _ = Describe("Create", func() { }), ) }) + +var policyDocument = aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/test" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "oidc.eks.eu-north-1.amazonaws.com/id/test:sub": "system:serviceaccount:default:service-account-1", + "oidc.eks.eu-north-1.amazonaws.com/id/test:aud": "sts.amazonaws.com" + } + } + } + ] +}`) + +var iamRoleStackTemplate = func(ns, sa string) string { + return fmt.Sprintf(`{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "IAM role for serviceaccount \"%s/%s\" [created and managed by eksctl]", + "Resources": { + "Role1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRoleWithWebIdentity" + ], + "Condition": { + "StringEquals": { + "oidc.eks.ap-northeast-2.amazonaws.com/id/BD00DB7DD37421596942D195F2B4F419:aud": "sts.amazonaws.com", + "oidc.eks.ap-northeast-2.amazonaws.com/id/BD00DB7DD37421596942D195F2B4F419:sub": "system:serviceaccount:backend-apps:s3-reader" + } + }, + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/BD00DB7DD37421596942D195F2B4F419" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess" + ] + } + } + }, + "Outputs": { + "Role1": { + "Value": { + "Fn::GetAtt": "Role1.Arn" + } + } + } + }`, ns, sa) +} diff --git a/userdocs/src/usage/minimum-iam-policies.md b/userdocs/src/usage/minimum-iam-policies.md index dfdc7c4c3d..1765165c3a 100644 --- a/userdocs/src/usage/minimum-iam-policies.md +++ b/userdocs/src/usage/minimum-iam-policies.md @@ -128,6 +128,7 @@ IamLimitedAccess "iam:DeleteRole", "iam:AttachRolePolicy", "iam:PutRolePolicy", + "iam:UpdateAssumeRolePolicy" "iam:AddRoleToInstanceProfile", "iam:ListInstanceProfilesForRole", "iam:PassRole", From befc6a96b72a4676b313058bccde960ed3c93f51 Mon Sep 17 00:00:00 2001 From: tiberiugc Date: Tue, 28 May 2024 08:58:05 +0300 Subject: [PATCH 27/35] update list of minimum IAM permissions --- userdocs/src/usage/minimum-iam-policies.md | 1 + 1 file changed, 1 insertion(+) diff --git a/userdocs/src/usage/minimum-iam-policies.md b/userdocs/src/usage/minimum-iam-policies.md index 1765165c3a..67b4ae7143 100644 --- a/userdocs/src/usage/minimum-iam-policies.md +++ b/userdocs/src/usage/minimum-iam-policies.md @@ -141,6 +141,7 @@ IamLimitedAccess "iam:TagOpenIDConnectProvider", "iam:ListAttachedRolePolicies", "iam:TagRole", + "iam:UntagRole", "iam:GetPolicy", "iam:CreatePolicy", "iam:DeletePolicy", From d970920d4c104e333b8e97a83ed080cbe8ba0ed0 Mon Sep 17 00:00:00 2001 From: tiberiugc Date: Tue, 28 May 2024 09:10:10 +0300 Subject: [PATCH 28/35] tech debt - unskip tests from PI suite --- .../pod_identity_associations_test.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/integration/tests/pod_identity_associations/pod_identity_associations_test.go b/integration/tests/pod_identity_associations/pod_identity_associations_test.go index 10a5bc63d9..6d95cca8b7 100644 --- a/integration/tests/pod_identity_associations/pod_identity_associations_test.go +++ b/integration/tests/pod_identity_associations/pod_identity_associations_test.go @@ -121,7 +121,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should create a cluster with iam service accounts", func() { - Skip("until integration test account is amended with the required permissions") cfg.IAM = &api.ClusterIAM{ WithOIDC: aws.Bool(true), ServiceAccounts: []*api.ClusterIAMServiceAccount{ @@ -158,7 +157,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should migrate to pod identity associations", func() { - Skip("until integration test account is amended with the required permissions") Expect(params.EksctlUtilsCmd. WithArgs( "migrate-to-pod-identity", @@ -169,7 +167,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should fetch all expected associations", func() { - Skip("until integration test account is amended with the required permissions") var output []podidentityassociation.Summary session := params.EksctlGetCmd. WithArgs( @@ -183,7 +180,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should not return any iam service accounts", func() { - Skip("until integration test account is amended with the required permissions") Expect(params.EksctlGetCmd. WithArgs( "iamserviceaccount", @@ -192,7 +188,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should fail to update an owned migrated role", func() { - Skip("until integration test account is amended with the required permissions") session := params.EksctlUpdateCmd. WithArgs( "podidentityassociation", @@ -206,7 +201,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should update an unowned migrated role", func() { - Skip("until integration test account is amended with the required permissions") Expect(params.EksctlUpdateCmd. WithArgs( "podidentityassociation", @@ -219,7 +213,6 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { }) It("should delete an owned migrated role", func() { - Skip("until integration test account is amended with the required permissions") Expect(params.EksctlDeleteCmd. WithArgs( "podidentityassociation", @@ -555,11 +548,9 @@ var _ = SynchronizedAfterSuite(func() {}, func() { return } - /* - Expect(params.EksctlDeleteCmd.WithArgs( - "cluster", clusterIRSAv1, - )).To(RunSuccessfully()) - */ + Expect(params.EksctlDeleteCmd.WithArgs( + "cluster", clusterIRSAv1, + )).To(RunSuccessfully()) Expect(params.EksctlDeleteCmd.WithArgs( "cluster", clusterIRSAv2, From 55004c15a1d59885f4df74412bc3731bf634dcb4 Mon Sep 17 00:00:00 2001 From: tiberiugc Date: Wed, 29 May 2024 18:53:47 +0300 Subject: [PATCH 29/35] fix addons integration test --- integration/tests/addons/addons_test.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/integration/tests/addons/addons_test.go b/integration/tests/addons/addons_test.go index e7d3e76791..d4812c0551 100644 --- a/integration/tests/addons/addons_test.go +++ b/integration/tests/addons/addons_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + package addons import ( @@ -137,16 +140,18 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { ) Expect(cmd).To(RunSuccessfully()) - By("Deleting the vpc-cni addon") + By("Deleting the vpc-cni addon with --preserve") cmd = params.EksctlDeleteCmd. WithArgs( "addon", "--name", "vpc-cni", + "--preserve", "--cluster", clusterName, "--verbose", "2", ) Expect(cmd).To(RunSuccessfully()) - + _, err := rawClient.ClientSet().AppsV1().DaemonSets("kube-system").Get(context.Background(), "aws-node", metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred()) }) It("should have full control over configMap when creating addons", func() { @@ -596,7 +601,6 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { Expect(makeCreateAddonCMD()).To(RunSuccessfully()) assertAddonHasPodIDs(api.VPCCNIAddon, 1) assertStackExists(makePodIDStackName(api.VPCCNIAddon, awsNodeSA)) - assertStackNotExists(makeIRSAStackName(api.VPCCNIAddon)) By("creating pod identity associations for addons when `autoCreate:true` and addon supports podIDs") clusterConfig.IAM.AutoCreatePodIdentityAssociations = true @@ -619,14 +623,13 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { By("deleting pod identity associations and IAM role when deleting addon") Expect(makeDeleteAddonCMD(api.VPCCNIAddon)).To(RunSuccessfully()) assertPodIDPresence("kube-system", awsNodeSA, false) + assertStackDeleted(makeIRSAStackName(api.VPCCNIAddon)) assertStackDeleted(makePodIDStackName(api.VPCCNIAddon, awsNodeSA)) By("keeping pod identity associations and IAM role when deleting addon with preserve") Expect(makeDeleteAddonCMD(api.AWSEBSCSIDriverAddon, "--preserve")).To(RunSuccessfully()) assertPodIDPresence("kube-system", ebsCSIControllerSA, true) assertStackExists(makePodIDStackName(api.AWSEBSCSIDriverAddon, ebsCSIControllerSA)) - _, err := rawClient.ClientSet().AppsV1().DaemonSets("kube-system").Get(context.Background(), ebsCSIControllerSA, metav1.GetOptions{}) - Expect(err).NotTo(HaveOccurred()) By("cleaning up IAM role on subsequent deletion") Expect(makeDeleteAddonCMD(api.AWSEBSCSIDriverAddon)).To(RunSuccessfully()) @@ -828,6 +831,7 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { var _ = AfterSuite(func() { cmd := params.EksctlDeleteCmd.WithArgs( "cluster", params.ClusterName, + "--disable-nodegroup-eviction", "--verbose", "2", ) Expect(cmd).To(RunSuccessfully()) From dda4a326cc36860b6f58d517c183971ece49ed80 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Thu, 30 May 2024 19:21:57 +0530 Subject: [PATCH 30/35] Allow updating addons with recommended IAM policies, disallow setting tags and wellKnownPolicies --- pkg/actions/addon/addon.go | 14 +- pkg/actions/addon/create.go | 2 +- .../addon/mocks/PodIdentityIAMUpdater.go | 23 ++- pkg/actions/addon/podidentityassociation.go | 31 ++- .../addon/podidentityassociation_test.go | 192 +++++++----------- pkg/actions/addon/update.go | 78 +++++-- pkg/actions/addon/update_test.go | 101 ++++++++- .../iam_role_creator.go | 1 + .../iam_role_updater.go | 4 +- pkg/apis/eksctl.io/v1alpha5/addon.go | 4 + .../eksctl.io/v1alpha5/assets/schema.json | 11 +- pkg/apis/eksctl.io/v1alpha5/iam.go | 5 +- pkg/apis/eksctl.io/v1alpha5/validation.go | 31 +++ .../eksctl.io/v1alpha5/validation_test.go | 61 ++++++ pkg/ctl/cmdutils/configfile.go | 2 +- 15 files changed, 385 insertions(+), 175 deletions(-) diff --git a/pkg/actions/addon/addon.go b/pkg/actions/addon/addon.go index 316b7c5147..51a3f0ef21 100644 --- a/pkg/actions/addon/addon.go +++ b/pkg/actions/addon/addon.go @@ -92,6 +92,15 @@ func (a *Manager) waitForAddonToBeActive(ctx context.Context, addon *api.Addon, return nil } +type versionNotFoundError struct { + addonName string + addonVersion string +} + +func (v *versionNotFoundError) Error() string { + return fmt.Sprintf("no version(s) found matching %q for %q", v.addonVersion, v.addonName) +} + func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon) (string, bool, error) { addonInfos, err := a.describeVersions(ctx, addon) if err != nil { @@ -123,7 +132,10 @@ func (a *Manager) getLatestMatchingVersion(ctx context.Context, addon *api.Addon } if len(versions) == 0 { - return "", false, fmt.Errorf("no version(s) found matching %q for %q", addonVersion, addon.Name) + return "", false, &versionNotFoundError{ + addonName: addon.Name, + addonVersion: addonVersion, + } } sort.SliceStable(versions, func(i, j int) bool { diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 5da3dd65e7..e1b48b5351 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -175,7 +175,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I createAddonInput.ServiceAccountRoleArn = &roleARN // if neither podIDs nor IRSA are set explicitly, then check if podIDs should be created automatically - case a.clusterConfig.IAM.AutoCreatePodIdentityAssociations && supportsPodIDs: + case (a.clusterConfig.IAM.AutoCreatePodIdentityAssociations || addon.CreateDefaultPodIdentityAssociations) && supportsPodIDs: logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for %q addon", addon.Name) if addon.CanonicalName() == api.VPCCNIAddon && a.clusterConfig.IPv6Enabled() { diff --git a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go index 66171a149a..01dd647a58 100644 --- a/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go +++ b/pkg/actions/addon/mocks/PodIdentityIAMUpdater.go @@ -5,9 +5,12 @@ package mocks import ( context "context" - types "github.com/aws/aws-sdk-go-v2/service/eks/types" + addon "github.com/weaveworks/eksctl/pkg/actions/addon" + mock "github.com/stretchr/testify/mock" + types "github.com/aws/aws-sdk-go-v2/service/eks/types" + v1alpha5 "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" ) @@ -44,9 +47,9 @@ func (_m *PodIdentityIAMUpdater) DeleteRole(ctx context.Context, addonName strin return r0, r1 } -// UpdateRole provides a mock function with given fields: ctx, podIdentityAssociations, addonName -func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []v1alpha5.PodIdentityAssociation, addonName string) ([]types.AddonPodIdentityAssociations, error) { - ret := _m.Called(ctx, podIdentityAssociations, addonName) +// UpdateRole provides a mock function with given fields: ctx, podIdentityAssociations, addonName, existingPodIdentityAssociations +func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []v1alpha5.PodIdentityAssociation, addonName string, existingPodIdentityAssociations []addon.PodIdentityAssociationSummary) ([]types.AddonPodIdentityAssociations, error) { + ret := _m.Called(ctx, podIdentityAssociations, addonName, existingPodIdentityAssociations) if len(ret) == 0 { panic("no return value specified for UpdateRole") @@ -54,19 +57,19 @@ func (_m *PodIdentityIAMUpdater) UpdateRole(ctx context.Context, podIdentityAsso var r0 []types.AddonPodIdentityAssociations var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation, string) ([]types.AddonPodIdentityAssociations, error)); ok { - return rf(ctx, podIdentityAssociations, addonName) + if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation, string, []addon.PodIdentityAssociationSummary) ([]types.AddonPodIdentityAssociations, error)); ok { + return rf(ctx, podIdentityAssociations, addonName, existingPodIdentityAssociations) } - if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation, string) []types.AddonPodIdentityAssociations); ok { - r0 = rf(ctx, podIdentityAssociations, addonName) + if rf, ok := ret.Get(0).(func(context.Context, []v1alpha5.PodIdentityAssociation, string, []addon.PodIdentityAssociationSummary) []types.AddonPodIdentityAssociations); ok { + r0 = rf(ctx, podIdentityAssociations, addonName, existingPodIdentityAssociations) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]types.AddonPodIdentityAssociations) } } - if rf, ok := ret.Get(1).(func(context.Context, []v1alpha5.PodIdentityAssociation, string) error); ok { - r1 = rf(ctx, podIdentityAssociations, addonName) + if rf, ok := ret.Get(1).(func(context.Context, []v1alpha5.PodIdentityAssociation, string, []addon.PodIdentityAssociationSummary) error); ok { + r1 = rf(ctx, podIdentityAssociations, addonName, existingPodIdentityAssociations) } else { r1 = ret.Error(1) } diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go index 80351993af..a4d8fcf240 100644 --- a/pkg/actions/addon/podidentityassociation.go +++ b/pkg/actions/addon/podidentityassociation.go @@ -3,6 +3,7 @@ package addon import ( "context" "fmt" + "slices" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/eks" @@ -15,16 +16,21 @@ import ( "github.com/weaveworks/eksctl/pkg/cfn/manager" ) +// EKSPodIdentityDescriber describes pod identities. type EKSPodIdentityDescriber interface { - ListPodIdentityAssociations(ctx context.Context, params *eks.ListPodIdentityAssociationsInput, optFns ...func(*eks.Options)) (*eks.ListPodIdentityAssociationsOutput, error) DescribePodIdentityAssociation(ctx context.Context, params *eks.DescribePodIdentityAssociationInput, optFns ...func(*eks.Options)) (*eks.DescribePodIdentityAssociationOutput, error) } +// IAMRoleCreator creates IAM resources for a pod identity association. type IAMRoleCreator interface { + // Create creates IAM resources for podIdentityAssociation and returns the IAM role ARN. Create(ctx context.Context, podIdentityAssociation *api.PodIdentityAssociation, addonName string) (roleARN string, err error) } +// IAMRoleUpdater updates IAM resources for a pod identity association. type IAMRoleUpdater interface { + // Update updates IAM resources for podIdentityAssociation and returns an IAM role ARN upon success. The boolean return value reports + // whether the IAM resources have changed or not. Update(ctx context.Context, podIdentityAssociation api.PodIdentityAssociation, stackName string, podIdentityAssociationID string) (string, bool, error) } @@ -37,22 +43,14 @@ type PodIdentityAssociationUpdater struct { StackDeleter podidentityassociation.StackDeleter } -func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string) ([]ekstypes.AddonPodIdentityAssociations, error) { +// UpdateRole creates or updates IAM roles for podIdentityAssociations. +func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string, existingPodIdentityAssociations []PodIdentityAssociationSummary) ([]ekstypes.AddonPodIdentityAssociations, error) { var addonPodIdentityAssociations []ekstypes.AddonPodIdentityAssociations for _, pia := range podIdentityAssociations { - output, err := p.EKSPodIdentityDescriber.ListPodIdentityAssociations(ctx, &eks.ListPodIdentityAssociationsInput{ - ClusterName: aws.String(p.ClusterName), - Namespace: aws.String(pia.Namespace), - ServiceAccount: aws.String(pia.ServiceAccountName), - }) - if err != nil { - return nil, fmt.Errorf("listing pod identity associations: %w", err) - } roleARN := pia.RoleARN - switch len(output.Associations) { - default: - return nil, fmt.Errorf("expected to find exactly 1 pod identity association for %s; got %d", pia.NameString(), len(output.Associations)) - case 0: + if idx := slices.IndexFunc(existingPodIdentityAssociations, func(existingPIA PodIdentityAssociationSummary) bool { + return pia.ServiceAccountName == existingPIA.ServiceAccount + }); idx == -1 { // Create IAM resources. if roleARN == "" { var err error @@ -70,11 +68,11 @@ func (p *PodIdentityAssociationUpdater) UpdateRole(ctx context.Context, podIdent } } } - case 1: + } else { // Update IAM resources if required. output, err := p.EKSPodIdentityDescriber.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ ClusterName: aws.String(p.ClusterName), - AssociationId: output.Associations[0].AssociationId, + AssociationId: aws.String(existingPodIdentityAssociations[idx].AssociationID), }) if err != nil { return nil, err @@ -139,6 +137,7 @@ func (p *PodIdentityAssociationUpdater) getStack(ctx context.Context, stackName, } } +// DeleteRole deletes the IAM resources for addonName and serviceAccountName. func (p *PodIdentityAssociationUpdater) DeleteRole(ctx context.Context, addonName, serviceAccountName string) (bool, error) { stack, err := p.getAddonStack(ctx, addonName, serviceAccountName) if err != nil { diff --git a/pkg/actions/addon/podidentityassociation_test.go b/pkg/actions/addon/podidentityassociation_test.go index cc18d7f579..0b899e53bb 100644 --- a/pkg/actions/addon/podidentityassociation_test.go +++ b/pkg/actions/addon/podidentityassociation_test.go @@ -34,8 +34,9 @@ var _ = Describe("Update Pod Identity Association", func() { eks *mocksv2.EKS } type updateEntry struct { - podIdentityAssociations []api.PodIdentityAssociation - mockCalls func(m piaMocks) + podIdentityAssociations []api.PodIdentityAssociation + mockCalls func(m piaMocks) + existingPodIdentityAssociations []addon.PodIdentityAssociationSummary expectedAddonPodIdentityAssociations []ekstypes.AddonPodIdentityAssociations @@ -47,45 +48,6 @@ var _ = Describe("Update Pod Identity Association", func() { makeID := func(i int) string { return fmt.Sprintf("a-%d", i+1) } - type listPodIdentityInput struct { - namespace string - serviceAccount string - } - defaultListPodIdentityInputs := []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "vpc-cni", - }, - { - namespace: "kube-system", - serviceAccount: "aws-ebs-csi-driver", - }, - { - namespace: "karpenter", - serviceAccount: "karpenter", - }, - } - mockListPodIdentityAssociations := func(eksAPI *mocksv2.EKS, hasAssociation bool, listInputs []listPodIdentityInput) { - for i, listInput := range listInputs { - var associations []ekstypes.PodIdentityAssociationSummary - if hasAssociation { - associations = []ekstypes.PodIdentityAssociationSummary{ - { - Namespace: aws.String(listInput.namespace), - ServiceAccount: aws.String(listInput.serviceAccount), - AssociationId: aws.String(makeID(i)), - }, - } - } - eksAPI.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ - ClusterName: aws.String(clusterName), - Namespace: aws.String(listInput.namespace), - ServiceAccount: aws.String(listInput.serviceAccount), - }).Return(&eks.ListPodIdentityAssociationsOutput{ - Associations: associations, - }, nil).Once() - } - } mockDescribePodIdentityAssociation := func(eksAPI *mocksv2.EKS, roleARNs ...string) { for i, roleARN := range roleARNs { @@ -125,17 +87,22 @@ var _ = Describe("Update Pod Identity Association", func() { eks: provider.MockEKS(), }) } - addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations, "main") + addonPodIdentityAssociations, err := piaUpdater.UpdateRole(context.Background(), e.podIdentityAssociations, "main", e.existingPodIdentityAssociations) if e.expectedErr != "" { Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) return } Expect(err).NotTo(HaveOccurred()) Expect(addonPodIdentityAssociations).To(Equal(e.expectedAddonPodIdentityAssociations)) - t := GinkgoT() - roleCreator.AssertExpectations(t) - roleUpdater.AssertExpectations(t) - provider.MockEKS().AssertExpectations(t) + for _, asserter := range []interface { + AssertExpectations(t mock.TestingT) bool + }{ + &roleCreator, + &roleUpdater, + provider.MockEKS(), + } { + asserter.AssertExpectations(GinkgoT()) + } }, Entry("addon contains pod identity that does not exist", updateEntry{ podIdentityAssociations: []api.PodIdentityAssociation{ @@ -145,12 +112,6 @@ var _ = Describe("Update Pod Identity Association", func() { }, }, mockCalls: func(m piaMocks) { - m.eks.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ - ClusterName: aws.String(clusterName), - Namespace: aws.String("kube-system"), - ServiceAccount: aws.String("vpc-cni"), - }).Return(&eks.ListPodIdentityAssociationsOutput{}, nil) - m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "vpc-cni", @@ -185,24 +146,15 @@ var _ = Describe("Update Pod Identity Association", func() { ServiceAccountName: "karpenter", }, }, + existingPodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "kube-system", + ServiceAccount: "vpc-cni", + AssociationID: "a-1", + }, + }, mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "vpc-cni", - }, - }) mockDescribePodIdentityAssociation(m.eks, "cni-role") - mockListPodIdentityAssociations(m.eks, false, []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "aws-ebs-csi-driver", - }, - { - namespace: "karpenter", - serviceAccount: "karpenter", - }, - }) m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ Namespace: "kube-system", @@ -259,8 +211,24 @@ var _ = Describe("Update Pod Identity Association", func() { ServiceAccountName: "karpenter", }, }, + existingPodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "kube-system", + ServiceAccount: "vpc-cni", + AssociationID: "a-1", + }, + { + Namespace: "kube-system", + ServiceAccount: "aws-ebs-csi-driver", + AssociationID: "a-2", + }, + { + Namespace: "karpenter", + ServiceAccount: "karpenter", + AssociationID: "a-3", + }, + }, mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, true, defaultListPodIdentityInputs) mockDescribePodIdentityAssociation(m.eks, "cni-role", "csi-role", "karpenter-role") for i, updateInput := range []struct { @@ -341,9 +309,6 @@ var _ = Describe("Update Pod Identity Association", func() { RoleARN: "role-3", }, }, - mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, false, defaultListPodIdentityInputs) - }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { ServiceAccount: aws.String("vpc-cni"), @@ -378,10 +343,6 @@ var _ = Describe("Update Pod Identity Association", func() { RoleARN: "role-3", }, }, - mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, false, defaultListPodIdentityInputs) - - }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { ServiceAccount: aws.String("vpc-cni"), @@ -416,21 +377,24 @@ var _ = Describe("Update Pod Identity Association", func() { RoleARN: "karpenter-role", }, }, + existingPodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "kube-system", + ServiceAccount: "vpc-cni", + AssociationID: "a-1", + }, + { + Namespace: "kube-system", + ServiceAccount: "aws-ebs-csi-driver", + AssociationID: "a-2", + }, + { + Namespace: "karpenter", + ServiceAccount: "karpenter", + AssociationID: "a-3", + }, + }, mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "vpc-cni", - }, - { - namespace: "kube-system", - serviceAccount: "aws-ebs-csi-driver", - }, - { - namespace: "karpenter", - serviceAccount: "karpenter", - }, - }) mockDescribePodIdentityAssociation(m.eks, "role-1", "role-2", "role-3") for _, serviceAccount := range []string{"vpc-cni", "aws-ebs-csi-driver"} { m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ @@ -460,13 +424,14 @@ var _ = Describe("Update Pod Identity Association", func() { RoleARN: "vpc-cni-role-2", }, }, + existingPodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "kube-system", + ServiceAccount: "vpc-cni", + AssociationID: "a-1", + }, + }, mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "vpc-cni", - }, - }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), @@ -488,13 +453,14 @@ var _ = Describe("Update Pod Identity Association", func() { ServiceAccountName: "vpc-cni", }, }, + existingPodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "kube-system", + ServiceAccount: "vpc-cni", + AssociationID: "a-1", + }, + }, mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "vpc-cni", - }, - }) mockDescribePodIdentityAssociation(m.eks, "vpc-cni-role") m.stackDeleter.On("DescribeStack", mock.Anything, &manager.Stack{ StackName: aws.String("eksctl-test-addon-main-podidentityrole-vpc-cni"), @@ -528,9 +494,6 @@ var _ = Describe("Update Pod Identity Association", func() { RoleARN: "role-3", }, }, - mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, false, defaultListPodIdentityInputs) - }, expectedAddonPodIdentityAssociations: []ekstypes.AddonPodIdentityAssociations{ { ServiceAccount: aws.String("vpc-cni"), @@ -554,13 +517,14 @@ var _ = Describe("Update Pod Identity Association", func() { ServiceAccountName: "vpc-cni", }, }, + existingPodIdentityAssociations: []addon.PodIdentityAssociationSummary{ + { + Namespace: "kube-system", + ServiceAccount: "vpc-cni", + AssociationID: "a-1", + }, + }, mockCalls: func(m piaMocks) { - mockListPodIdentityAssociations(m.eks, true, []listPodIdentityInput{ - { - namespace: "kube-system", - serviceAccount: "vpc-cni", - }, - }) mockDescribePodIdentityAssociation(m.eks, "cni-role") m.roleUpdater.On("Update", mock.Anything, api.PodIdentityAssociation{ @@ -595,12 +559,6 @@ var _ = Describe("Update Pod Identity Association", func() { }, }, mockCalls: func(m piaMocks) { - m.eks.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ - ClusterName: aws.String(clusterName), - Namespace: aws.String("kube-system"), - ServiceAccount: aws.String("vpc-cni"), - }).Return(&eks.ListPodIdentityAssociationsOutput{}, nil) - m.roleCreator.On("Create", mock.Anything, &api.PodIdentityAssociation{ Namespace: "kube-system", ServiceAccountName: "vpc-cni", diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 0089f2b276..973f1c6ac8 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -2,6 +2,7 @@ package addon import ( "context" + "errors" "fmt" "slices" "time" @@ -19,7 +20,7 @@ import ( // PodIdentityIAMUpdater creates or updates IAM resources for pod identity associations. type PodIdentityIAMUpdater interface { // UpdateRole creates or updates IAM resources for podIdentityAssociations. - UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string) ([]ekstypes.AddonPodIdentityAssociations, error) + UpdateRole(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation, addonName string, existingPodIdentityAssociations []PodIdentityAssociationSummary) ([]ekstypes.AddonPodIdentityAssociations, error) // DeleteRole deletes the IAM resources for the specified addon. DeleteRole(ctx context.Context, addonName, serviceAccountName string) (bool, error) } @@ -49,27 +50,34 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp return err } + var requiresIAMPermissions bool if addon.Version == "" { // preserve existing version // Might be redundant, does the API care? logger.Info("no new version provided, preserving existing version: %s", summary.Version) - + addon.Version = summary.Version + _, requiresIAMPermissions, err = a.getLatestMatchingVersion(ctx, addon) + if err != nil { + var notFoundErr *versionNotFoundError + if !errors.As(err, ¬FoundErr) { + return fmt.Errorf("failed to fetch addon version %s: %w", summary.Version, err) + } + } updateAddonInput.AddonVersion = &summary.Version } else { - version, _, err := a.getLatestMatchingVersion(ctx, addon) + var latestVersion string + latestVersion, requiresIAMPermissions, err = a.getLatestMatchingVersion(ctx, addon) if err != nil { return fmt.Errorf("failed to fetch addon version: %w", err) } - - if summary.Version != version { - logger.Info("new version provided %s", version) + if summary.Version != latestVersion { + logger.Info("new version provided %s", latestVersion) } - - updateAddonInput.AddonVersion = &version + updateAddonInput.AddonVersion = &latestVersion } var deleteServiceAccountIAMResources []string - if len(summary.PodIdentityAssociations) > 0 { + if len(summary.PodIdentityAssociations) > 0 && !addon.CreateDefaultPodIdentityAssociations { if addon.PodIdentityAssociations == nil { return fmt.Errorf("addon %s has pod identity associations, to remove pod identity associations from an addon, "+ "addon.podIdentityAssociations must be explicitly set to []; if the addon was migrated to use pod identity, "+ @@ -91,23 +99,51 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp } if addon.HasPodIDsSet() { - addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, *addon.PodIdentityAssociations, addon.Name) - if err != nil { - return fmt.Errorf("updating pod identity associations: %w", err) + if requiresIAMPermissions { + addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, *addon.PodIdentityAssociations, addon.Name, summary.PodIdentityAssociations) + if err != nil { + return fmt.Errorf("updating pod identity associations: %w", err) + } + updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations + } else { + logger.Warning(IAMPermissionsNotRequiredWarning(addon.Name)) } - updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations } else { - // check if we have been provided a different set of policies/role - if addon.ServiceAccountRoleARN != "" { - updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN - } else if addon.HasIRSAPoliciesSet() { - serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) + supportsPodIdentity := false + if addon.CreateDefaultPodIdentityAssociations && requiresIAMPermissions { + var pidConfigList []ekstypes.AddonPodIdentityConfiguration + pidConfigList, supportsPodIdentity, err = a.getRecommendedPoliciesForPodID(ctx, addon) if err != nil { return err } - updateAddonInput.ServiceAccountRoleArn = &serviceAccountRoleARN - } else if summary.IAMRole != "" { // Preserve current role. - updateAddonInput.ServiceAccountRoleArn = &summary.IAMRole + if supportsPodIdentity { + var podIdentityAssociations []api.PodIdentityAssociation + for _, pidConfig := range pidConfigList { + podIdentityAssociations = append(podIdentityAssociations, api.PodIdentityAssociation{ + ServiceAccountName: *pidConfig.ServiceAccount, + PermissionPolicyARNs: pidConfig.RecommendedManagedPolicies, + }) + } + addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, podIdentityAssociations, addon.Name, summary.PodIdentityAssociations) + if err != nil { + return err + } + updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations + } + } + if !supportsPodIdentity { + // check if we have been provided a different set of policies/role + if addon.ServiceAccountRoleARN != "" { + updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN + } else if addon.HasIRSAPoliciesSet() { + serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) + if err != nil { + return err + } + updateAddonInput.ServiceAccountRoleArn = &serviceAccountRoleARN + } else if summary.IAMRole != "" { // Preserve current role. + updateAddonInput.ServiceAccountRoleArn = &summary.IAMRole + } } } diff --git a/pkg/actions/addon/update_test.go b/pkg/actions/addon/update_test.go index cc46dafbde..9d6f6d32ff 100644 --- a/pkg/actions/addon/update_test.go +++ b/pkg/actions/addon/update_test.go @@ -37,6 +37,13 @@ var _ = Describe("Update", func() { waitTimeout = 5 * time.Minute ) + makeOIDCManager := func() *iamoidc.OpenIDConnectManager { + oidc, err := iamoidc.NewOpenIDConnectManager(nil, "456123987123", "https://oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E", "aws", nil) + Expect(err).NotTo(HaveOccurred()) + oidc.ProviderARN = "arn:aws:iam::456123987123:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E" + return oidc + } + BeforeEach(func() { var err error mockProvider = mockprovider.NewMockProvider() @@ -51,9 +58,7 @@ var _ = Describe("Update", func() { return nil } - oidc, err := iamoidc.NewOpenIDConnectManager(nil, "456123987123", "https://oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E", "aws", nil) - Expect(err).NotTo(HaveOccurred()) - oidc.ProviderARN = "arn:aws:iam::456123987123:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/A39A2842863C47208955D753DE205E6E" + oidc := makeOIDCManager() mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, mock.Anything).Run(func(args mock.Arguments) { Expect(args).To(HaveLen(2)) @@ -527,4 +532,94 @@ var _ = Describe("Update", func() { Expect(*updateAddonInput.AddonName).To(Equal("my-addon")) }) }) + + type addonPIAEntry struct { + addonVersion string + } + DescribeTable("updating pod identity associations", func(e addonPIAEntry) { + const clusterName = "my-cluster" + const addonVersion = "v1.7.5-eksbuild.1" + mockProvider := mockprovider.NewMockProvider() + + mockProvider.MockEKS().On("DescribeAddon", mock.Anything, &awseks.DescribeAddonInput{ + ClusterName: aws.String(clusterName), + AddonName: aws.String(api.VPCCNIAddon), + }).Return(&awseks.DescribeAddonOutput{ + Addon: &ekstypes.Addon{ + AddonName: aws.String(api.VPCCNIAddon), + ClusterName: aws.String(clusterName), + AddonVersion: aws.String(addonVersion), + }, + }, nil).Once() + mockProvider.MockEKS().On("DescribeAddonVersions", mock.Anything, &awseks.DescribeAddonVersionsInput{ + AddonName: aws.String("vpc-cni"), + KubernetesVersion: aws.String(api.LatestVersion), + }).Return(&awseks.DescribeAddonVersionsOutput{ + Addons: []ekstypes.AddonInfo{ + { + AddonName: aws.String("vpc-cni"), + AddonVersions: []ekstypes.AddonVersionInfo{ + { + AddonVersion: aws.String(addonVersion), + RequiresIamPermissions: true, + }, + }, + }, + }, + }, nil).Twice() + mockProvider.MockEKS().On("DescribeAddonConfiguration", mock.Anything, &awseks.DescribeAddonConfigurationInput{ + AddonName: aws.String(api.VPCCNIAddon), + AddonVersion: aws.String(addonVersion), + }).Return(&awseks.DescribeAddonConfigurationOutput{ + AddonName: aws.String(api.VPCCNIAddon), + AddonVersion: aws.String(addonVersion), + PodIdentityConfiguration: []ekstypes.AddonPodIdentityConfiguration{ + { + ServiceAccount: aws.String("aws-node"), + RecommendedManagedPolicies: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, + }, nil).Once() + addonPIAs := []ekstypes.AddonPodIdentityAssociations{ + { + ServiceAccount: aws.String("aws-node"), + RoleArn: aws.String("role-1"), + }, + } + mockProvider.MockEKS().On("UpdateAddon", mock.Anything, &awseks.UpdateAddonInput{ + AddonName: aws.String("vpc-cni"), + ClusterName: aws.String("my-cluster"), + AddonVersion: aws.String("v1.7.5-eksbuild.1"), + PodIdentityAssociations: addonPIAs, + }).Return(&awseks.UpdateAddonOutput{}, nil).Once() + + var podIdentityIAMUpdater mocks.PodIdentityIAMUpdater + podIdentityIAMUpdater.On("UpdateRole", mock.Anything, []api.PodIdentityAssociation{ + { + Namespace: "", + ServiceAccountName: "aws-node", + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, "vpc-cni", mock.Anything).Return(addonPIAs, nil).Once() + + addonManager, err := addon.New(&api.ClusterConfig{Metadata: &api.ClusterMeta{ + Version: api.LatestVersion, + Name: clusterName, + }}, mockProvider.EKS(), fakeStackManager, true, makeOIDCManager(), nil) + Expect(err).NotTo(HaveOccurred()) + + err = addonManager.Update(context.Background(), &api.Addon{ + Name: "vpc-cni", + CreateDefaultPodIdentityAssociations: true, + Version: e.addonVersion, + }, &podIdentityIAMUpdater, 0) + Expect(err).NotTo(HaveOccurred()) + mockProvider.MockEKS().AssertExpectations(GinkgoT()) + podIdentityIAMUpdater.AssertExpectations(GinkgoT()) + }, + Entry("addon with version", addonPIAEntry{ + addonVersion: "v1.7.5-eksbuild.1", + }), + Entry("addon without version", addonPIAEntry{}), + ) }) diff --git a/pkg/actions/podidentityassociation/iam_role_creator.go b/pkg/actions/podidentityassociation/iam_role_creator.go index 0cce033984..950a415d81 100644 --- a/pkg/actions/podidentityassociation/iam_role_creator.go +++ b/pkg/actions/podidentityassociation/iam_role_creator.go @@ -8,6 +8,7 @@ import ( "github.com/weaveworks/eksctl/pkg/cfn/builder" ) +// IAMRoleCreator creates IAM resources for a pod identity association. type IAMRoleCreator struct { ClusterName string StackCreator StackCreator diff --git a/pkg/actions/podidentityassociation/iam_role_updater.go b/pkg/actions/podidentityassociation/iam_role_updater.go index d1a56ec351..3d9b6f4a3b 100644 --- a/pkg/actions/podidentityassociation/iam_role_updater.go +++ b/pkg/actions/podidentityassociation/iam_role_updater.go @@ -23,7 +23,7 @@ type IAMRoleUpdater struct { StackUpdater StackUpdater } -// Update updates IAM resources for updateConfig and returns an IAM role ARN upon success. The boolean return value reports +// Update updates IAM resources for podIdentityAssociation and returns an IAM role ARN upon success. The boolean return value reports // whether the IAM resources have changed or not. func (u *IAMRoleUpdater) Update(ctx context.Context, podIdentityAssociation api.PodIdentityAssociation, stackName, podIdentityAssociationID string) (string, bool, error) { stack, err := u.StackUpdater.DescribeStack(ctx, &manager.Stack{ @@ -52,7 +52,7 @@ func (u *IAMRoleUpdater) Update(ctx context.Context, podIdentityAssociation api. }); err != nil { var noChangeErr *manager.NoChangeError if errors.As(err, &noChangeErr) { - logger.Info("IAM resources for %q are already up-to-date", podIdentityAssociationID) + logger.Info("IAM resources for %s (pod identity association ID: %s) are already up-to-date", podIdentityAssociation.NameString(), podIdentityAssociationID) return podIdentityAssociation.RoleARN, false, nil } return "", false, fmt.Errorf("updating IAM resources for pod identity association: %w", err) diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index 0e2ac15a9f..af04eaa754 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -38,6 +38,10 @@ type Addon struct { // PodIdentityAssociations holds a list of associations to be configured for the addon // +optional PodIdentityAssociations *[]PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` + // CreateDefaultPodIdentityAssociations uses the pod identity associations recommended by the EKS API. + // Defaults to false. + // +optional + CreateDefaultPodIdentityAssociations bool `json:"createDefaultPodIdentityAssociations,omitempty"` // ConfigurationValues defines the set of configuration properties for add-ons. // For now, all properties will be specified as a JSON string // and have to respect the schema from DescribeAddonConfiguration. diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index 09d2e75ac0..5cf1b0dcb8 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -175,6 +175,12 @@ "description": "defines the set of configuration properties for add-ons. For now, all properties will be specified as a JSON string and have to respect the schema from DescribeAddonConfiguration.", "x-intellij-html-description": "defines the set of configuration properties for add-ons. For now, all properties will be specified as a JSON string and have to respect the schema from DescribeAddonConfiguration." }, + "createDefaultPodIdentityAssociations": { + "type": "boolean", + "description": "uses the pod identity associations recommended by the EKS API. Defaults to false.", + "x-intellij-html-description": "uses the pod identity associations recommended by the EKS API. Defaults to false.", + "default": "false" + }, "name": { "type": "string" }, @@ -246,6 +252,7 @@ "tags", "resolveConflicts", "podIdentityAssociations", + "createDefaultPodIdentityAssociations", "configurationValues", "publishers", "types", @@ -530,8 +537,8 @@ "$ref": "#/definitions/PodIdentityAssociation" }, "type": "array", - "description": "pod identity associations to create in the cluster. See [Pod Identity Associations](TBD)", - "x-intellij-html-description": "pod identity associations to create in the cluster. See Pod Identity Associations" + "description": "pod identity associations to create in the cluster. See [Pod Identity Associations](/usage/pod-identity-associations)", + "x-intellij-html-description": "pod identity associations to create in the cluster. See Pod Identity Associations" }, "serviceAccounts": { "items": { diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index 8e27ef6ecf..9cc0f68a74 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -59,7 +59,7 @@ type ClusterIAM struct { AutoCreatePodIdentityAssociations bool `json:"autoCreatePodIdentityAssociations,omitempty"` // pod identity associations to create in the cluster. - // See [Pod Identity Associations](TBD) + // See [Pod Identity Associations](/usage/pod-identity-associations) // +optional PodIdentityAssociations []PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` @@ -203,6 +203,9 @@ type PodIdentityAssociation struct { } func (p PodIdentityAssociation) NameString() string { + if p.Namespace == "" { + return p.ServiceAccountName + } return p.Namespace + "/" + p.ServiceAccountName } diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index 3810b12493..22669b65de 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -222,6 +222,9 @@ func ValidateClusterConfig(cfg *ClusterConfig) error { if err := validateIAMIdentityMappings(cfg); err != nil { return err } + if err := validateAddonPodIdentityAssociations(cfg.Addons); err != nil { + return err + } if len(cfg.AccessConfig.AccessEntries) > 0 { switch cfg.AccessConfig.AuthenticationMode { @@ -1678,3 +1681,31 @@ func validateIAMIdentityMappings(clusterConfig *ClusterConfig) error { } return nil } + +func validateAddonPodIdentityAssociations(addons []*Addon) error { + for _, addon := range addons { + makeAddonErr := func(msg string) error { + return fmt.Errorf("%s (addon: %s)", msg, addon.Name) + } + if addon.PodIdentityAssociations != nil { + for _, pia := range *addon.PodIdentityAssociations { + if pia.WellKnownPolicies.HasPolicy() { + return makeAddonErr("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.createDefaultPodIdentityAssociations instead") + } + if pia.Tags != nil { + return makeAddonErr("tags is not supported for addon.podIdentityAssociations") + } + } + } + if addon.CreateDefaultPodIdentityAssociations { + if addon.HasPodIDsSet() { + return makeAddonErr("cannot specify both addon.createDefaultPodIdentityAssociations and addon.podIdentityAssociations") + } + if addon.ServiceAccountRoleARN != "" || addon.WellKnownPolicies.HasPolicy() || len(addon.AttachPolicy) > 0 || len(addon.AttachPolicyARNs) > 0 { + return makeAddonErr("cannot specify serviceAccountRoleARN, wellKnownPolicies, attachPolicy or attachPolicyARNs" + + " when createDefaultPodIdentityAssociations is set") + } + } + } + return nil +} diff --git a/pkg/apis/eksctl.io/v1alpha5/validation_test.go b/pkg/apis/eksctl.io/v1alpha5/validation_test.go index da274609f3..e640feb5c8 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation_test.go @@ -2487,6 +2487,67 @@ var _ = Describe("ClusterConfig validation", func() { Entry("invalid PIA ARN format", "arn:aws:eks:us-west-2:000:podidentityassociation/a-d3dw7wfvxtoatujeg", "", "unexpected pod identity association ARN format"), Entry("invalid PIA ARN", "a-d3dw7wfvxtoatujeg", "", "parsing ARN"), ) + + DescribeTable("addon pod identity association", func(addons []*api.Addon, expectedErr string) { + clusterConfig := api.NewClusterConfig() + clusterConfig.Addons = addons + err := api.ValidateClusterConfig(clusterConfig) + if expectedErr != "" { + Expect(err).To(MatchError(ContainSubstring(expectedErr))) + } else { + Expect(err).NotTo(HaveOccurred()) + } + }, + Entry("wellKnownPolicies specified", []*api.Addon{ + { + Name: api.VPCCNIAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + WellKnownPolicies: api.WellKnownPolicies{AutoScaler: true}, + }, + }, + }, + }, fmt.Sprintf("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.createDefaultPodIdentityAssociations instead (addon: %s)", api.VPCCNIAddon)), + Entry("tags specified", []*api.Addon{ + { + Name: api.VPCCNIAddon, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + Tags: map[string]string{}, + }, + }, + }, + }, fmt.Sprintf("tags is not supported for addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)), + Entry("pod identity associations specified with createDefaultPodIdentityAssociations", []*api.Addon{ + { + Name: api.VPCCNIAddon, + CreateDefaultPodIdentityAssociations: true, + PodIdentityAssociations: &[]api.PodIdentityAssociation{ + { + ServiceAccountName: "aws-node", + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, + }, + }, fmt.Sprintf("cannot specify both addon.createDefaultPodIdentityAssociations and addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)), + Entry("IRSA fields specified with createDefaultPodIdentityAssociations", []*api.Addon{ + { + Name: api.VPCCNIAddon, + ServiceAccountRoleARN: "role-1", + CreateDefaultPodIdentityAssociations: true, + }, + }, fmt.Sprintf("cannot specify serviceAccountRoleARN, wellKnownPolicies, attachPolicy or attachPolicyARNs"+ + " when createDefaultPodIdentityAssociations is set (addon: %s)", api.VPCCNIAddon)), + + Entry("IRSA fields specified", []*api.Addon{ + { + Name: api.VPCCNIAddon, + ServiceAccountRoleARN: "role-1", + }, + }, ""), + ) }) func newInt(value int) *int { diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 5b6316f876..e3e82597ba 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -317,7 +317,7 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. if cfg.IAM != nil && cfg.IAM.AutoCreatePodIdentityAssociations { return true } - if addon.PodIdentityAssociations != nil && len(*addon.PodIdentityAssociations) > 0 { + if addon.CreateDefaultPodIdentityAssociations || addon.HasPodIDsSet() { return true } } From 5a96c5e5b21dc9afd9bd6d3ce24efc8ca7cf0927 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Mon, 3 Jun 2024 16:59:21 +0530 Subject: [PATCH 31/35] Add more validation --- pkg/actions/addon/create.go | 11 +++- pkg/actions/addon/create_test.go | 2 +- pkg/actions/addon/podidentityassociation.go | 2 +- pkg/actions/addon/update.go | 60 ++++++++++----------- pkg/ctl/create/addon.go | 2 +- pkg/ctl/update/addon.go | 2 +- 6 files changed, 41 insertions(+), 38 deletions(-) diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index e1b48b5351..4930cf5de1 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -72,6 +72,15 @@ const ( awsNodeServiceAccount = "aws-node" ) +type unsupportedPodIdentityErr struct { + addonName string +} + +func (e *unsupportedPodIdentityErr) Error() string { + return fmt.Sprintf("%q addon does not support pod identity associations; use IRSA config"+ + " (`addon.serviceAccountRoleARN`, `addon.attachPolicyARNs`, `addon.attachPolicy` or `addon.wellKnownPolicies`) instead", e.addonName) +} + func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator IAMRoleCreator, waitTimeout time.Duration) error { // check if the addon is already present as an EKS managed addon // in a state different from CREATE_FAILED, and if so, don't re-create @@ -133,7 +142,7 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I // firstly, check if the user has specifically defined pod identity associations case addon.HasPodIDsSet(): if !supportsPodIDs { - return fmt.Errorf("%q addon does not support pod identity associations; use IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`) instead", addon.Name) + return &unsupportedPodIdentityErr{addonName: addon.Name} } logger.Info("pod identity associations are set for %q addon; will use these to configure required IAM permissions", addon.Name) for _, pia := range *addon.PodIdentityAssociations { diff --git a/pkg/actions/addon/create_test.go b/pkg/actions/addon/create_test.go index 6437cd76e0..50a5596604 100644 --- a/pkg/actions/addon/create_test.go +++ b/pkg/actions/addon/create_test.go @@ -650,7 +650,7 @@ var _ = Describe("Create", func() { mockDescribeAddonVersions(provider.MockEKS(), nil) mockDescribeAddonConfiguration(mockProvider.MockEKS(), []string{}, nil) }, - expectedErr: "\"my-addon\" addon does not support pod identity associations; use IRSA config (`addon.ServiceAccountRoleARN`, `addon.AttachPolicyARNs`, `addon.AttachPolicy` or `addon.WellKnownPolicies`) instead", + expectedErr: "\"my-addon\" addon does not support pod identity associations; use IRSA config (`addon.serviceAccountRoleARN`, `addon.attachPolicyARNs`, `addon.attachPolicy` or `addon.wellKnownPolicies`) instead", }), Entry("[RequiresIAMPermissions] podIDs set explicitly and supportsPodIDs", createAddonEntry{ diff --git a/pkg/actions/addon/podidentityassociation.go b/pkg/actions/addon/podidentityassociation.go index a4d8fcf240..bda85cf305 100644 --- a/pkg/actions/addon/podidentityassociation.go +++ b/pkg/actions/addon/podidentityassociation.go @@ -31,7 +31,7 @@ type IAMRoleCreator interface { type IAMRoleUpdater interface { // Update updates IAM resources for podIdentityAssociation and returns an IAM role ARN upon success. The boolean return value reports // whether the IAM resources have changed or not. - Update(ctx context.Context, podIdentityAssociation api.PodIdentityAssociation, stackName string, podIdentityAssociationID string) (string, bool, error) + Update(ctx context.Context, podIdentityAssociation api.PodIdentityAssociation, stackName, podIdentityAssociationID string) (string, bool, error) } // PodIdentityAssociationUpdater creates or updates IAM resources for pod identities associated with an addon. diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 973f1c6ac8..22aab7f590 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -98,52 +98,46 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp } } - if addon.HasPodIDsSet() { + if addon.HasPodIDsSet() || addon.CreateDefaultPodIdentityAssociations { if requiresIAMPermissions { - addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, *addon.PodIdentityAssociations, addon.Name, summary.PodIdentityAssociations) + pidConfigList, supportsPodIdentity, err := a.getRecommendedPoliciesForPodID(ctx, addon) if err != nil { - return fmt.Errorf("updating pod identity associations: %w", err) + return fmt.Errorf("getting recommended policies for addon %s", addon.Name) } - updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations - } else { - logger.Warning(IAMPermissionsNotRequiredWarning(addon.Name)) - } - } else { - supportsPodIdentity := false - if addon.CreateDefaultPodIdentityAssociations && requiresIAMPermissions { - var pidConfigList []ekstypes.AddonPodIdentityConfiguration - pidConfigList, supportsPodIdentity, err = a.getRecommendedPoliciesForPodID(ctx, addon) - if err != nil { - return err + if !supportsPodIdentity { + return &unsupportedPodIdentityErr{addonName: addon.Name} } - if supportsPodIdentity { - var podIdentityAssociations []api.PodIdentityAssociation + var podIdentityAssociations []api.PodIdentityAssociation + if addon.CreateDefaultPodIdentityAssociations { for _, pidConfig := range pidConfigList { podIdentityAssociations = append(podIdentityAssociations, api.PodIdentityAssociation{ ServiceAccountName: *pidConfig.ServiceAccount, PermissionPolicyARNs: pidConfig.RecommendedManagedPolicies, }) } - addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, podIdentityAssociations, addon.Name, summary.PodIdentityAssociations) - if err != nil { - return err - } - updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations + } else { + podIdentityAssociations = *addon.PodIdentityAssociations } + addonPodIdentityAssociations, err := podIdentityIAMUpdater.UpdateRole(ctx, podIdentityAssociations, addon.Name, summary.PodIdentityAssociations) + if err != nil { + return fmt.Errorf("updating pod identity associations: %w", err) + } + updateAddonInput.PodIdentityAssociations = addonPodIdentityAssociations + } else { + logger.Warning(IAMPermissionsNotRequiredWarning(addon.Name)) } - if !supportsPodIdentity { - // check if we have been provided a different set of policies/role - if addon.ServiceAccountRoleARN != "" { - updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN - } else if addon.HasIRSAPoliciesSet() { - serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) - if err != nil { - return err - } - updateAddonInput.ServiceAccountRoleArn = &serviceAccountRoleARN - } else if summary.IAMRole != "" { // Preserve current role. - updateAddonInput.ServiceAccountRoleArn = &summary.IAMRole + } else { + // check if we have been provided a different set of policies/role + if addon.ServiceAccountRoleARN != "" { + updateAddonInput.ServiceAccountRoleArn = &addon.ServiceAccountRoleARN + } else if addon.HasIRSAPoliciesSet() { + serviceAccountRoleARN, err := a.updateWithNewPolicies(ctx, addon) + if err != nil { + return err } + updateAddonInput.ServiceAccountRoleArn = &serviceAccountRoleARN + } else if summary.IAMRole != "" { // Preserve current role. + updateAddonInput.ServiceAccountRoleArn = &summary.IAMRole } } diff --git a/pkg/ctl/create/addon.go b/pkg/ctl/create/addon.go index 415e9fb2b6..8ee0c598e3 100644 --- a/pkg/ctl/create/addon.go +++ b/pkg/ctl/create/addon.go @@ -140,7 +140,7 @@ func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg * if a.CanonicalName() == api.PodIdentityAgentAddon { podIdentityAgentFoundInConfig = true } - if a.PodIdentityAssociations != nil && len(*a.PodIdentityAssociations) > 0 { + if a.HasPodIDsSet() || a.CreateDefaultPodIdentityAssociations { shallCreatePodIdentityAssociations = true } } diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index 509c221e66..180f77c029 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -130,7 +130,7 @@ func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg * } for _, a := range cfg.Addons { - if a.HasPodIDsSet() { + if a.HasPodIDsSet() || a.CreateDefaultPodIdentityAssociations { suggestion := fmt.Sprintf("please enable it using `eksctl create addon --cluster=%s --name=%s`, or by adding it to the config file", cfg.Metadata.Name, api.PodIdentityAgentAddon) return api.ErrPodIdentityAgentNotInstalled(suggestion) } From 6a7369444a142ac29dd247b80ff3394451b5718a Mon Sep 17 00:00:00 2001 From: cPu1 Date: Mon, 3 Jun 2024 22:05:24 +0530 Subject: [PATCH 32/35] Rename fields to addonsConfig.autoApplyPodIdentityAssociations and addon.useDefaultPodIdentityAssociations --- integration/tests/addons/addons_test.go | 8 +- integration/tests/dry_run/dry_run_test.go | 1 + pkg/actions/addon/create.go | 4 +- pkg/actions/addon/create_test.go | 22 +++--- pkg/actions/addon/update.go | 6 +- pkg/actions/addon/update_test.go | 6 +- pkg/apis/eksctl.io/v1alpha5/addon.go | 12 ++- .../eksctl.io/v1alpha5/assets/schema.json | 43 +++++++---- pkg/apis/eksctl.io/v1alpha5/iam.go | 6 -- .../v1alpha5/identity_provider_test.go | 3 +- pkg/apis/eksctl.io/v1alpha5/types.go | 4 + pkg/apis/eksctl.io/v1alpha5/validation.go | 8 +- .../eksctl.io/v1alpha5/validation_test.go | 20 ++--- .../v1alpha5/zz_generated.deepcopy.go | 17 +++++ pkg/ctl/cmdutils/addon.go | 2 +- pkg/ctl/cmdutils/configfile.go | 5 +- .../cmdutils/filter/nodegroup_filter_test.go | 1 + pkg/ctl/create/addon.go | 6 +- pkg/ctl/update/addon.go | 2 +- .../src/usage/pod-identity-associations.md | 76 +++++++++++-------- 20 files changed, 151 insertions(+), 101 deletions(-) diff --git a/integration/tests/addons/addons_test.go b/integration/tests/addons/addons_test.go index d4812c0551..8dfcf7d6ed 100644 --- a/integration/tests/addons/addons_test.go +++ b/integration/tests/addons/addons_test.go @@ -602,15 +602,15 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { assertAddonHasPodIDs(api.VPCCNIAddon, 1) assertStackExists(makePodIDStackName(api.VPCCNIAddon, awsNodeSA)) - By("creating pod identity associations for addons when `autoCreate:true` and addon supports podIDs") - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + By("creating pod identity associations for addons when `autoApplyPodIdentityAssociations: true` and addon supports podIDs") + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true clusterConfig.Addons = []*api.Addon{{Name: api.AWSEBSCSIDriverAddon}} Expect(makeCreateAddonCMD()).To(RunSuccessfully()) assertAddonHasPodIDs(api.AWSEBSCSIDriverAddon, 1) assertStackExists(makePodIDStackName(api.AWSEBSCSIDriverAddon, ebsCSIControllerSA)) assertStackNotExists(makeIRSAStackName(api.AWSEBSCSIDriverAddon)) - By("falling back to IRSA when `autoCreate:true` but addon doesn't support podIDs") + By("falling back to IRSA when `autoApplyPodIdentityAssociations: true` but addon doesn't support podIDs") clusterConfig.Addons = []*api.Addon{{Name: api.AWSEFSCSIDriverAddon}} Expect(makeCreateAddonCMD()).To(RunSuccessfully()) assertAddonHasPodIDs(api.AWSEFSCSIDriverAddon, 0) @@ -652,7 +652,7 @@ var _ = Describe("(Integration) [EKS Addons test]", func() { }) It("should update IAM permissions when updating or migrating addons", func() { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = false + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = false clusterConfig.Addons = []*api.Addon{ {Name: api.VPCCNIAddon}, {Name: api.AWSEBSCSIDriverAddon}, diff --git a/integration/tests/dry_run/dry_run_test.go b/integration/tests/dry_run/dry_run_test.go index c86e868379..794ee64e54 100644 --- a/integration/tests/dry_run/dry_run_test.go +++ b/integration/tests/dry_run/dry_run_test.go @@ -67,6 +67,7 @@ kubernetesNetworkConfig: ipFamily: IPv4 accessConfig: authenticationMode: API_AND_CONFIG_MAP +addonsConfig: {} nodeGroups: - amiFamily: AmazonLinux2 containerRuntime: containerd diff --git a/pkg/actions/addon/create.go b/pkg/actions/addon/create.go index 4930cf5de1..4496494fec 100644 --- a/pkg/actions/addon/create.go +++ b/pkg/actions/addon/create.go @@ -184,8 +184,8 @@ func (a *Manager) Create(ctx context.Context, addon *api.Addon, iamRoleCreator I createAddonInput.ServiceAccountRoleArn = &roleARN // if neither podIDs nor IRSA are set explicitly, then check if podIDs should be created automatically - case (a.clusterConfig.IAM.AutoCreatePodIdentityAssociations || addon.CreateDefaultPodIdentityAssociations) && supportsPodIDs: - logger.Info("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for %q addon", addon.Name) + case (a.clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations || addon.UseDefaultPodIdentityAssociations) && supportsPodIDs: + logger.Info("\"addonsConfig.autoApplyPodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for %q addon", addon.Name) if addon.CanonicalName() == api.VPCCNIAddon && a.clusterConfig.IPv6Enabled() { roleARN, err := iamRoleCreator.Create(ctx, &api.PodIdentityAssociation{ diff --git a/pkg/actions/addon/create_test.go b/pkg/actions/addon/create_test.go index 50a5596604..df2765f484 100644 --- a/pkg/actions/addon/create_test.go +++ b/pkg/actions/addon/create_test.go @@ -684,9 +684,9 @@ var _ = Describe("Create", func() { }, }), - Entry("[RequiresIAMPermissions] `autoCreatePodIdentityAssociations:true` and NOT supportsPodIDs", createAddonEntry{ + Entry("[RequiresIAMPermissions] `autoApplyPodIdentityAssociations: true` and NOT supportsPodIDs", createAddonEntry{ mockClusterConfig: func(clusterConfig *api.ClusterConfig) { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true }, mockEKS: func(provider *mockprovider.MockProvider) { mockDescribeAddon(mockProvider.MockEKS(), nil) @@ -706,9 +706,9 @@ var _ = Describe("Create", func() { }, }), - Entry("[RequiresIAMPermissions] `autoCreatePodIdentityAssociations:true` and supportsPodIDs", createAddonEntry{ + Entry("[RequiresIAMPermissions] `autoApplyPodIdentityAssociations: true` and supportsPodIDs", createAddonEntry{ mockClusterConfig: func(clusterConfig *api.ClusterConfig) { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true }, mockEKS: func(provider *mockprovider.MockProvider) { mockDescribeAddon(mockProvider.MockEKS(), nil) @@ -734,17 +734,17 @@ var _ = Describe("Create", func() { Expect(*input.PodIdentityAssociations[0].RoleArn).To(Equal("arn:aws:iam::111122223333:role/sa1")) }, validateCustomLoggerOutput: func(output string) { - Expect(output).To(ContainSubstring("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for \"my-addon\" addon")) + Expect(output).To(ContainSubstring("\"addonsConfig.autoApplyPodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for \"my-addon\" addon")) }, }), - Entry("[RequiresIAMPermissions] `autoCreatePodIdentityAssociations:true` and supportsPodIDs (vpc-cni && ipv6)", createAddonEntry{ + Entry("[RequiresIAMPermissions] `autoApplyPodIdentityAssociations: true` and supportsPodIDs (vpc-cni && ipv6)", createAddonEntry{ addon: api.Addon{ Name: api.VPCCNIAddon, }, mockK8s: true, mockClusterConfig: func(clusterConfig *api.ClusterConfig) { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true clusterConfig.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{ IPFamily: "IPv6", } @@ -773,7 +773,7 @@ var _ = Describe("Create", func() { Expect(*input.PodIdentityAssociations[0].RoleArn).To(Equal("arn:aws:iam::111122223333:role/aws-node")) }, validateCustomLoggerOutput: func(output string) { - Expect(output).To(ContainSubstring("\"iam.AutoCreatePodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for \"vpc-cni\" addon")) + Expect(output).To(ContainSubstring("\"addonsConfig.autoApplyPodIdentityAssociations\" is set to true; will lookup recommended pod identity configuration for \"vpc-cni\" addon")) }, }), @@ -791,7 +791,7 @@ var _ = Describe("Create", func() { }, }, mockClusterConfig: func(clusterConfig *api.ClusterConfig) { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true }, mockEKS: func(provider *mockprovider.MockProvider) { mockDescribeAddon(mockProvider.MockEKS(), nil) @@ -815,7 +815,7 @@ var _ = Describe("Create", func() { }, withOIDC: true, mockClusterConfig: func(clusterConfig *api.ClusterConfig) { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true }, mockEKS: func(provider *mockprovider.MockProvider) { mockDescribeAddon(mockProvider.MockEKS(), nil) @@ -841,7 +841,7 @@ var _ = Describe("Create", func() { }, withOIDC: true, mockClusterConfig: func(clusterConfig *api.ClusterConfig) { - clusterConfig.IAM.AutoCreatePodIdentityAssociations = true + clusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations = true }, mockEKS: func(provider *mockprovider.MockProvider) { mockDescribeAddon(mockProvider.MockEKS(), nil) diff --git a/pkg/actions/addon/update.go b/pkg/actions/addon/update.go index 22aab7f590..0caefcc79c 100644 --- a/pkg/actions/addon/update.go +++ b/pkg/actions/addon/update.go @@ -77,7 +77,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp } var deleteServiceAccountIAMResources []string - if len(summary.PodIdentityAssociations) > 0 && !addon.CreateDefaultPodIdentityAssociations { + if len(summary.PodIdentityAssociations) > 0 && !addon.UseDefaultPodIdentityAssociations { if addon.PodIdentityAssociations == nil { return fmt.Errorf("addon %s has pod identity associations, to remove pod identity associations from an addon, "+ "addon.podIdentityAssociations must be explicitly set to []; if the addon was migrated to use pod identity, "+ @@ -98,7 +98,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp } } - if addon.HasPodIDsSet() || addon.CreateDefaultPodIdentityAssociations { + if addon.HasPodIDsSet() || addon.UseDefaultPodIdentityAssociations { if requiresIAMPermissions { pidConfigList, supportsPodIdentity, err := a.getRecommendedPoliciesForPodID(ctx, addon) if err != nil { @@ -108,7 +108,7 @@ func (a *Manager) Update(ctx context.Context, addon *api.Addon, podIdentityIAMUp return &unsupportedPodIdentityErr{addonName: addon.Name} } var podIdentityAssociations []api.PodIdentityAssociation - if addon.CreateDefaultPodIdentityAssociations { + if addon.UseDefaultPodIdentityAssociations { for _, pidConfig := range pidConfigList { podIdentityAssociations = append(podIdentityAssociations, api.PodIdentityAssociation{ ServiceAccountName: *pidConfig.ServiceAccount, diff --git a/pkg/actions/addon/update_test.go b/pkg/actions/addon/update_test.go index 9d6f6d32ff..cda7a5603e 100644 --- a/pkg/actions/addon/update_test.go +++ b/pkg/actions/addon/update_test.go @@ -609,9 +609,9 @@ var _ = Describe("Update", func() { Expect(err).NotTo(HaveOccurred()) err = addonManager.Update(context.Background(), &api.Addon{ - Name: "vpc-cni", - CreateDefaultPodIdentityAssociations: true, - Version: e.addonVersion, + Name: "vpc-cni", + UseDefaultPodIdentityAssociations: true, + Version: e.addonVersion, }, &podIdentityIAMUpdater, 0) Expect(err).NotTo(HaveOccurred()) mockProvider.MockEKS().AssertExpectations(GinkgoT()) diff --git a/pkg/apis/eksctl.io/v1alpha5/addon.go b/pkg/apis/eksctl.io/v1alpha5/addon.go index af04eaa754..084aa6844f 100644 --- a/pkg/apis/eksctl.io/v1alpha5/addon.go +++ b/pkg/apis/eksctl.io/v1alpha5/addon.go @@ -38,10 +38,10 @@ type Addon struct { // PodIdentityAssociations holds a list of associations to be configured for the addon // +optional PodIdentityAssociations *[]PodIdentityAssociation `json:"podIdentityAssociations,omitempty"` - // CreateDefaultPodIdentityAssociations uses the pod identity associations recommended by the EKS API. + // UseDefaultPodIdentityAssociations uses the pod identity associations recommended by the EKS API. // Defaults to false. // +optional - CreateDefaultPodIdentityAssociations bool `json:"createDefaultPodIdentityAssociations,omitempty"` + UseDefaultPodIdentityAssociations bool `json:"useDefaultPodIdentityAssociations,omitempty"` // ConfigurationValues defines the set of configuration properties for add-ons. // For now, all properties will be specified as a JSON string // and have to respect the schema from DescribeAddonConfiguration. @@ -58,6 +58,14 @@ type Addon struct { Owners []string `json:"owners,omitempty"` } +// AddonsConfig holds the addons config. +type AddonsConfig struct { + // AutoApplyPodIdentityAssociations specifies whether to automatically apply pod identity associations + // for supported addons that require IAM permissions. + // +optional + AutoApplyPodIdentityAssociations bool `json:"autoApplyPodIdentityAssociations,omitempty"` +} + func (a Addon) CanonicalName() string { return strings.ToLower(a.Name) } diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index 5cf1b0dcb8..d74d622ca5 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -175,12 +175,6 @@ "description": "defines the set of configuration properties for add-ons. For now, all properties will be specified as a JSON string and have to respect the schema from DescribeAddonConfiguration.", "x-intellij-html-description": "defines the set of configuration properties for add-ons. For now, all properties will be specified as a JSON string and have to respect the schema from DescribeAddonConfiguration." }, - "createDefaultPodIdentityAssociations": { - "type": "boolean", - "description": "uses the pod identity associations recommended by the EKS API. Defaults to false.", - "x-intellij-html-description": "uses the pod identity associations recommended by the EKS API. Defaults to false.", - "default": "false" - }, "name": { "type": "string" }, @@ -232,6 +226,12 @@ }, "type": "array" }, + "useDefaultPodIdentityAssociations": { + "type": "boolean", + "description": "uses the pod identity associations recommended by the EKS API. Defaults to false.", + "x-intellij-html-description": "uses the pod identity associations recommended by the EKS API. Defaults to false.", + "default": "false" + }, "version": { "type": "string" }, @@ -252,7 +252,7 @@ "tags", "resolveConflicts", "podIdentityAssociations", - "createDefaultPodIdentityAssociations", + "useDefaultPodIdentityAssociations", "configurationValues", "publishers", "types", @@ -262,6 +262,22 @@ "description": "holds the EKS addon configuration", "x-intellij-html-description": "holds the EKS addon configuration" }, + "AddonsConfig": { + "properties": { + "autoApplyPodIdentityAssociations": { + "type": "boolean", + "description": "specifies whether to automatically apply pod identity associations for supported addons that require IAM permissions.", + "x-intellij-html-description": "specifies whether to automatically apply pod identity associations for supported addons that require IAM permissions.", + "default": "false" + } + }, + "preferredOrder": [ + "autoApplyPodIdentityAssociations" + ], + "additionalProperties": false, + "description": "holds the addons config.", + "x-intellij-html-description": "holds the addons config." + }, "CapacityReservation": { "properties": { "capacityReservationPreference": { @@ -362,6 +378,11 @@ }, "type": "array" }, + "addonsConfig": { + "$ref": "#/definitions/AddonsConfig", + "description": "specifies the configuration for addons.", + "x-intellij-html-description": "specifies the configuration for addons." + }, "apiVersion": { "type": "string", "enum": [ @@ -474,6 +495,7 @@ "accessConfig", "vpc", "addons", + "addonsConfig", "privateCluster", "nodeGroups", "managedNodeGroups", @@ -516,12 +538,6 @@ }, "ClusterIAM": { "properties": { - "autoCreatePodIdentityAssociations": { - "type": "boolean", - "description": "specifies whether or not to automatically create pod identity associations for supported addons that require IAM permissions", - "x-intellij-html-description": "specifies whether or not to automatically create pod identity associations for supported addons that require IAM permissions", - "default": "false" - }, "fargatePodExecutionRoleARN": { "type": "string", "description": "role used by pods to access AWS APIs. This role is added to the Kubernetes RBAC for authorization. See [Pod Execution Role](https://docs.aws.amazon.com/eks/latest/userguide/pod-execution-role.html)", @@ -575,7 +591,6 @@ "fargatePodExecutionRolePermissionsBoundary", "withOIDC", "serviceAccounts", - "autoCreatePodIdentityAssociations", "podIdentityAssociations", "vpcResourceControllerPolicy" ], diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index 9cc0f68a74..1a446dd5e5 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -52,12 +52,6 @@ type ClusterIAM struct { // See [IAM Service Accounts](/usage/iamserviceaccounts/#usage-with-config-files) // +optional ServiceAccounts []*ClusterIAMServiceAccount `json:"serviceAccounts,omitempty"` - - // AutoCreatePodIdentityAssociations specifies whether or not to automatically create pod identity associations - // for supported addons that require IAM permissions - // +optional - AutoCreatePodIdentityAssociations bool `json:"autoCreatePodIdentityAssociations,omitempty"` - // pod identity associations to create in the cluster. // See [Pod Identity Associations](/usage/pod-identity-associations) // +optional diff --git a/pkg/apis/eksctl.io/v1alpha5/identity_provider_test.go b/pkg/apis/eksctl.io/v1alpha5/identity_provider_test.go index 039a51c57c..1eaa1136ab 100644 --- a/pkg/apis/eksctl.io/v1alpha5/identity_provider_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/identity_provider_test.go @@ -11,7 +11,8 @@ import ( "sigs.k8s.io/yaml" ) -const identityProviders = `apiVersion: eksctl.io/v1alpha5 +const identityProviders = `addonsConfig: {} +apiVersion: eksctl.io/v1alpha5 identityProviders: - clientID: client issuerURL: example.com diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index a79a5a5614..fb42d53d50 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -908,6 +908,10 @@ type ClusterConfig struct { // +optional Addons []*Addon `json:"addons,omitempty"` + // AddonsConfig specifies the configuration for addons. + // +optional + AddonsConfig AddonsConfig `json:"addonsConfig,omitempty"` + // PrivateCluster allows configuring a fully-private cluster // in which no node has outbound internet access, and private access // to AWS services is enabled via VPC endpoints diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index 22669b65de..1ac66eab99 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -1690,20 +1690,20 @@ func validateAddonPodIdentityAssociations(addons []*Addon) error { if addon.PodIdentityAssociations != nil { for _, pia := range *addon.PodIdentityAssociations { if pia.WellKnownPolicies.HasPolicy() { - return makeAddonErr("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.createDefaultPodIdentityAssociations instead") + return makeAddonErr("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.useDefaultPodIdentityAssociations instead") } if pia.Tags != nil { return makeAddonErr("tags is not supported for addon.podIdentityAssociations") } } } - if addon.CreateDefaultPodIdentityAssociations { + if addon.UseDefaultPodIdentityAssociations { if addon.HasPodIDsSet() { - return makeAddonErr("cannot specify both addon.createDefaultPodIdentityAssociations and addon.podIdentityAssociations") + return makeAddonErr("cannot specify both addon.useDefaultPodIdentityAssociations and addon.podIdentityAssociations") } if addon.ServiceAccountRoleARN != "" || addon.WellKnownPolicies.HasPolicy() || len(addon.AttachPolicy) > 0 || len(addon.AttachPolicyARNs) > 0 { return makeAddonErr("cannot specify serviceAccountRoleARN, wellKnownPolicies, attachPolicy or attachPolicyARNs" + - " when createDefaultPodIdentityAssociations is set") + " when addon.useDefaultPodIdentityAssociations is set") } } } diff --git a/pkg/apis/eksctl.io/v1alpha5/validation_test.go b/pkg/apis/eksctl.io/v1alpha5/validation_test.go index e640feb5c8..a34a1880d2 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation_test.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation_test.go @@ -2508,7 +2508,7 @@ var _ = Describe("ClusterConfig validation", func() { }, }, }, - }, fmt.Sprintf("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.createDefaultPodIdentityAssociations instead (addon: %s)", api.VPCCNIAddon)), + }, fmt.Sprintf("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.useDefaultPodIdentityAssociations instead (addon: %s)", api.VPCCNIAddon)), Entry("tags specified", []*api.Addon{ { Name: api.VPCCNIAddon, @@ -2520,10 +2520,10 @@ var _ = Describe("ClusterConfig validation", func() { }, }, }, fmt.Sprintf("tags is not supported for addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)), - Entry("pod identity associations specified with createDefaultPodIdentityAssociations", []*api.Addon{ + Entry("pod identity associations specified with useDefaultPodIdentityAssociations", []*api.Addon{ { - Name: api.VPCCNIAddon, - CreateDefaultPodIdentityAssociations: true, + Name: api.VPCCNIAddon, + UseDefaultPodIdentityAssociations: true, PodIdentityAssociations: &[]api.PodIdentityAssociation{ { ServiceAccountName: "aws-node", @@ -2531,15 +2531,15 @@ var _ = Describe("ClusterConfig validation", func() { }, }, }, - }, fmt.Sprintf("cannot specify both addon.createDefaultPodIdentityAssociations and addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)), - Entry("IRSA fields specified with createDefaultPodIdentityAssociations", []*api.Addon{ + }, fmt.Sprintf("cannot specify both addon.useDefaultPodIdentityAssociations and addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)), + Entry("IRSA fields specified with useDefaultPodIdentityAssociations", []*api.Addon{ { - Name: api.VPCCNIAddon, - ServiceAccountRoleARN: "role-1", - CreateDefaultPodIdentityAssociations: true, + Name: api.VPCCNIAddon, + ServiceAccountRoleARN: "role-1", + UseDefaultPodIdentityAssociations: true, }, }, fmt.Sprintf("cannot specify serviceAccountRoleARN, wellKnownPolicies, attachPolicy or attachPolicyARNs"+ - " when createDefaultPodIdentityAssociations is set (addon: %s)", api.VPCCNIAddon)), + " when addon.useDefaultPodIdentityAssociations is set (addon: %s)", api.VPCCNIAddon)), Entry("IRSA fields specified", []*api.Addon{ { diff --git a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go index 590ce81cb2..2ba930f113 100644 --- a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go +++ b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go @@ -239,6 +239,22 @@ func (in *Addon) DeepCopy() *Addon { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AddonsConfig) DeepCopyInto(out *AddonsConfig) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AddonsConfig. +func (in *AddonsConfig) DeepCopy() *AddonsConfig { + if in == nil { + return nil + } + out := new(AddonsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CapacityReservation) DeepCopyInto(out *CapacityReservation) { *out = *in @@ -391,6 +407,7 @@ func (in *ClusterConfig) DeepCopyInto(out *ClusterConfig) { } } } + out.AddonsConfig = in.AddonsConfig if in.PrivateCluster != nil { in, out := &in.PrivateCluster, &out.PrivateCluster *out = new(PrivateCluster) diff --git a/pkg/ctl/cmdutils/addon.go b/pkg/ctl/cmdutils/addon.go index 13b58ca28c..085761abee 100644 --- a/pkg/ctl/cmdutils/addon.go +++ b/pkg/ctl/cmdutils/addon.go @@ -9,7 +9,7 @@ var addonFlagsIncompatibleWithConfigFile = []string{ "version", "service-account-role-arn", "attach-policy-arn", - "auto-create-pod-identity-associations", + "auto-apply-pod-identity-associations", } func NewCreateOrUpgradeAddonLoader(cmd *Cmd) ClusterConfigLoader { diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index e3e82597ba..29b6bfbd89 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -314,10 +314,7 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. return true } for _, addon := range clusterConfig.Addons { - if cfg.IAM != nil && cfg.IAM.AutoCreatePodIdentityAssociations { - return true - } - if addon.CreateDefaultPodIdentityAssociations || addon.HasPodIDsSet() { + if cfg.AddonsConfig.AutoApplyPodIdentityAssociations || addon.UseDefaultPodIdentityAssociations || addon.HasPodIDsSet() { return true } } diff --git a/pkg/ctl/cmdutils/filter/nodegroup_filter_test.go b/pkg/ctl/cmdutils/filter/nodegroup_filter_test.go index bb86886bdb..380c7afa84 100644 --- a/pkg/ctl/cmdutils/filter/nodegroup_filter_test.go +++ b/pkg/ctl/cmdutils/filter/nodegroup_filter_test.go @@ -365,6 +365,7 @@ const expected = ` "cloudWatch": { "clusterLogging": {} }, + "addonsConfig": {}, "privateCluster": { "enabled": false, "skipEndpointCreation": false diff --git a/pkg/ctl/create/addon.go b/pkg/ctl/create/addon.go index 8ee0c598e3..6cb1fb66b4 100644 --- a/pkg/ctl/create/addon.go +++ b/pkg/ctl/create/addon.go @@ -33,7 +33,7 @@ func createAddonCmd(cmd *cmdutils.Cmd) { fs.StringVar(&cmd.ClusterConfig.Addons[0].Name, "name", "", "Add-on name") fs.StringVar(&cmd.ClusterConfig.Addons[0].Version, "version", "", "Add-on version. Use `eksctl utils describe-addon-versions` to discover a version or set to \"latest\"") fs.StringVar(&cmd.ClusterConfig.Addons[0].ServiceAccountRoleARN, "service-account-role-arn", "", "Add-on serviceAccountRoleARN") - fs.BoolVar(&cmd.ClusterConfig.IAM.AutoCreatePodIdentityAssociations, "auto-create-pod-identity-associations", false, "create recommended pod identity associations for the addon(s), if supported") + fs.BoolVar(&cmd.ClusterConfig.AddonsConfig.AutoApplyPodIdentityAssociations, "auto-apply-pod-identity-associations", false, "apply recommended pod identity associations for the addon(s), if supported") fs.BoolVar(&force, "force", false, "Force migrates an existing self-managed add-on to an EKS managed add-on") fs.BoolVar(&wait, "wait", false, "Wait for the addon creation to complete") @@ -134,13 +134,13 @@ func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg * return err } - shallCreatePodIdentityAssociations := cfg.IAM.AutoCreatePodIdentityAssociations + shallCreatePodIdentityAssociations := cfg.AddonsConfig.AutoApplyPodIdentityAssociations podIdentityAgentFoundInConfig := false for _, a := range cfg.Addons { if a.CanonicalName() == api.PodIdentityAgentAddon { podIdentityAgentFoundInConfig = true } - if a.HasPodIDsSet() || a.CreateDefaultPodIdentityAssociations { + if a.HasPodIDsSet() || a.UseDefaultPodIdentityAssociations { shallCreatePodIdentityAssociations = true } } diff --git a/pkg/ctl/update/addon.go b/pkg/ctl/update/addon.go index 180f77c029..102566ca0c 100644 --- a/pkg/ctl/update/addon.go +++ b/pkg/ctl/update/addon.go @@ -130,7 +130,7 @@ func validatePodIdentityAgentAddon(ctx context.Context, eksAPI awsapi.EKS, cfg * } for _, a := range cfg.Addons { - if a.HasPodIDsSet() || a.CreateDefaultPodIdentityAssociations { + if a.HasPodIDsSet() || a.UseDefaultPodIdentityAssociations { suggestion := fmt.Sprintf("please enable it using `eksctl create addon --cluster=%s --name=%s`, or by adding it to the config file", cfg.Metadata.Name, api.PodIdentityAgentAddon) return api.ErrPodIdentityAgentNotInstalled(suggestion) } diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md index e705be1b84..6aa3274f74 100644 --- a/userdocs/src/usage/pod-identity-associations.md +++ b/userdocs/src/usage/pod-identity-associations.md @@ -2,7 +2,7 @@ ## Introduction -AWS EKS has introduced a new enhanced mechanism called Pod Identity Association for cluster administrators to configure Kubernetes applications to receive IAM permissions required to connect with AWS services outside of the cluster. Pod Identity Association leverages IRSA, however, it makes it configurable directly through EKS API, eliminating the need for using IAM API altogether. +AWS EKS has introduced a new enhanced mechanism called Pod Identity Association for cluster administrators to configure Kubernetes applications to receive IAM permissions required to connect with AWS services outside of the cluster. Pod Identity Association leverages IRSA, however, it makes it configurable directly through EKS API, eliminating the need for using IAM API altogether. As a result, IAM roles no longer need to reference an [OIDC provider](/usage/iamserviceaccounts/#how-it-works) and hence won't be tied to a single cluster anymore. This means, IAM roles can now be used across multiple EKS clusters without the need to update the role trust policy each time a new cluster is created. This in turn, eliminates the need for role duplication and simplifies the process of automating IRSA altogether. @@ -41,8 +41,8 @@ If instead you do not provide the ARN of an existing role to the create command, For manipulating pod identity associations, `eksctl` has added a new field under `iam.podIdentityAssociations`, e.g. ```yaml -iam: - podIdentityAssociations: +iam: + podIdentityAssociations: - namespace: #required serviceAccountName: #required createServiceAccount: true #optional, default is false @@ -162,12 +162,12 @@ eksctl delete podidentityassociation -f config.yaml OR (to delete a single association) pass the `--namespace` and `--service-account-name` via CLI flags: ``` -eksctl delete podidentityassociation --cluster my-cluster --namespace default --service-account-name s3-reader +eksctl delete podidentityassociation --cluster my-cluster --namespace default --service-account-name s3-reader ``` ## EKS Add-ons support for pod identity associations -EKS Add-ons also support receiving IAM permissions via EKS Pod Identity Associations. The config file exposes two fields that allow configuring these: `addon.podIdentityAssociations` and `iam.autoCreatePodIdentityAssociations`. You can either explicitly configure the desired pod identity associations, using the former, or have `eksctl` automatically resolve (and apply) the recommended pod identity configuration, using the latter. +EKS Add-ons also support receiving IAM permissions via EKS Pod Identity Associations. The config file exposes three fields that allow configuring these: `addon.podIdentityAssociations`, `addonsConfig.autoApplyPodIdentityAssociations` and `addon.useDefaultPodIdentityAssociations`. You can either explicitly configure the desired pod identity associations, using `addon.podIdentityAssociations`, or have `eksctl` automatically resolve (and apply) the recommended pod identity configuration, using either `addonsConfig.autoApplyPodIdentityAssociations` or `addon.useDefaultPodIdentityAssociations`. ???+ note Not all EKS Add-ons will support pod identity associations at launch. For this case, required IAM permissions shall continue to be provided using [IRSA settings](/usage/addons/#creating-addons-and-providing-iam-permissions-via-irsa) @@ -194,14 +194,14 @@ eksctl create addon -f config.yaml ???+ note Setting both pod identities and IRSA at the same time is not allowed, and will result in a validation error. -For EKS Add-ons that support pod identities, `eksctl` offers the option to automatically configure any recommended IAM permissions, on addon creation. This can be achieved by simply setting `iam.AutoCreatePodIdentityAssociations: true` in the config file. e.g. +For EKS Add-ons that support pod identities, `eksctl` offers the option to automatically configure any recommended IAM permissions, on addon creation. This can be achieved by simply setting `addonsConfig.autoApplyPodIdentityAssociations: true` in the config file. e.g. ```yaml -iam: - autoCreatePodIdentityAssociations: true +addonsConfig: + autoApplyPodIdentityAssociations: true # bear in mind that if either pod identity or IRSA configuration is explicitly set in the config file, # or if the addon does not support pod identities, -# iam.autoCreatePodIdentityAssociations won't have any effect. +# addonsConfig.autoApplyPodIdentityAssociations won't have any effect. addons: - name: vpc-cni ``` @@ -210,13 +210,25 @@ and run ```bash eksctl create addon -f config.yaml -2024-05-13 15:38:58 [ℹ] "iam.AutoCreatePodIdentityAssociations" is set to true; will lookup recommended pod identity configuration for "vpc-cni" addon +2024-05-13 15:38:58 [ℹ] "addonsConfig.autoApplyPodIdentityAssociations" is set to true; will lookup recommended pod identity configuration for "vpc-cni" addon ``` Equivalently, the same can be done via CLI flags e.g. ```bash -eksctl create addon --cluster my-cluster --name vpc-cni --auto-create-pod-identity-associations +eksctl create addon --cluster my-cluster --name vpc-cni --auto-apply-pod-identity-associations +``` + +To migrate an existing addon to use pod identity with the recommended IAM policies, use + +```yaml +addons: +- name: vpc-cni + useDefaultPodIdentityAssociations: true +``` + +```bash +$ eksctl update addon -f config.yaml ``` ### Updating addons with IAM permissions @@ -257,7 +269,7 @@ Now use the below configuration: addons: - name: adot podIdentityAssociations: - + # For the first association, the permissions policy of the role will be updated - serviceAccountName: adot-col-prom-metrics permissionPolicyARNs: @@ -268,7 +280,7 @@ addons: #- serviceAccountName: adot-col-otlp-ingest # permissionPolicyARNs: # - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess - + # The third association will be created, as it's been added to the config file - serviceAccountName: adot-col-container-logs permissionPolicyARNs: @@ -302,7 +314,7 @@ eksctl update addon -f config.yaml now check that pod identity config was updated correctly ```bash -eksctl get podidentityassociation --cluster my-cluster --output json +eksctl get podidentityassociation --cluster my-cluster --output json [ { ... @@ -316,7 +328,7 @@ eksctl get podidentityassociation --cluster my-cluster --output json "RoleARN": "arn:aws:iam::111122223333:role/eksctl-my-cluster-addon-adot-podident-Role1-1k1XhAdziGzX", "OwnerARN": "arn:aws:eks:us-west-2:111122223333:addon/my-cluster/adot/1ec7bb63-8c4e-ca0a-f947-310c4b55052e" } -] +] ``` @@ -325,8 +337,8 @@ To remove all pod identity associations from an addon, `addon.PodIdentityAssocia ```yaml addons: - name: vpc-cni - # omitting the `podIdentityAssociations` field from the config file, - # instead of explicitly setting it to [], will result in a validation error + # omitting the `podIdentityAssociations` field from the config file, + # instead of explicitly setting it to [], will result in a validation error podIdentityAssociations: [] ``` @@ -355,43 +367,43 @@ Behind the scenes, the command will apply the following steps: - identify all IAM Roles that are associated with EKS addons that support pod identity associations - update the IAM trust policy of all identified roles, with an additional trusted entity, pointing to the new EKS Service principal (and, optionally, remove exising OIDC provider trust relationship) - create pod identity associations for filtered roles associated with iamserviceaccounts -- update EKS addons with pod identities (EKS API will create the pod identities behind the scenes) +- update EKS addons with pod identities (EKS API will create the pod identities behind the scenes) -Running the command without the `--approve` flag will only output a plan consisting of a set of tasks reflecting the steps above, e.g. +Running the command without the `--approve` flag will only output a plan consisting of a set of tasks reflecting the steps above, e.g. ```bash [ℹ] (plan) would migrate 2 iamserviceaccount(s) and 2 addon(s) to pod identity association(s) by executing the following tasks -[ℹ] (plan) +[ℹ] (plan) -3 sequential tasks: { install eks-pod-identity-agent addon, +3 sequential tasks: { install eks-pod-identity-agent addon, ## tasks for migrating the addons - 2 parallel sub-tasks: { - 2 sequential sub-tasks: { + 2 parallel sub-tasks: { + 2 sequential sub-tasks: { update trust policy for owned role "eksctl-my-cluster--Role1-DDuMLoeZ8weD", migrate addon aws-ebs-csi-driver to pod identity, }, - 2 sequential sub-tasks: { + 2 sequential sub-tasks: { update trust policy for owned role "eksctl-my-cluster--Role1-xYiPFOVp1aeI", migrate addon vpc-cni to pod identity, }, - }, + }, ## tasks for migrating the iamserviceaccounts - 2 parallel sub-tasks: { - 2 sequential sub-tasks: { + 2 parallel sub-tasks: { + 2 sequential sub-tasks: { update trust policy for owned role "eksctl-my-cluster--Role1-QLXqHcq9O1AR", create pod identity association for service account "default/sa1", }, - 2 sequential sub-tasks: { + 2 sequential sub-tasks: { update trust policy for unowned role "Unowned-Role1", create pod identity association for service account "default/sa2", }, - } + } } [ℹ] all tasks were skipped [!] no changes were applied, run again with '--approve' to apply the changes ``` -The existing OIDC provider trust relationship is always being removed from IAM Roles associated with EKS Add-ons. Additionally, to remove the existing OIDC provider trust relationship from IAM Roles associated with iamserviceaccounts, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. +The existing OIDC provider trust relationship is always being removed from IAM Roles associated with EKS Add-ons. Additionally, to remove the existing OIDC provider trust relationship from IAM Roles associated with iamserviceaccounts, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. ``` eksctl utils migrate-to-pod-identity --cluster my-cluster --approve --remove-oidc-provider-trust-relationship @@ -401,8 +413,8 @@ eksctl utils migrate-to-pod-identity --cluster my-cluster --approve --remove-oid [Official AWS Blog Post on EKS Add-ons support for pod identities] //https://TBD -[Official AWS Userdocs for EKS Add-ons support for pod identities] //https://TBD +[Official AWS Userdocs for EKS Add-ons support for pod identities] //https://TBD [Official AWS Blog Post on Pod Identity Associations](https://aws.amazon.com/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/) -[Official AWS userdocs for Pod Identity Associations](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) \ No newline at end of file +[Official AWS userdocs for Pod Identity Associations](https://docs.aws.amazon.com/eks/latest/userguide/pod-identities.html) From bb87f300b12fc7f0457ab366bd991aa539fae1f8 Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 4 Jun 2024 00:37:37 +0530 Subject: [PATCH 33/35] Update AWS SDK --- go.mod | 8 +- go.sum | 16 +- pkg/awsapi/eks.go | 503 +++++++++++++++++++++++++++------------------- 3 files changed, 308 insertions(+), 219 deletions(-) diff --git a/go.mod b/go.mod index 3e44d4f9c7..a05f873131 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/aws/amazon-ec2-instance-selector/v2 v2.4.2-0.20230601180523-74e721cb8c1e github.com/aws/aws-sdk-go v1.51.16 - github.com/aws/aws-sdk-go-v2 v1.26.1 + github.com/aws/aws-sdk-go-v2 v1.27.1 github.com/aws/aws-sdk-go-v2/config v1.27.11 github.com/aws/aws-sdk-go-v2/credentials v1.17.11 github.com/aws/aws-sdk-go-v2/service/autoscaling v1.40.5 @@ -20,7 +20,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.35.1 github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.36.3 github.com/aws/aws-sdk-go-v2/service/ec2 v1.156.0 - github.com/aws/aws-sdk-go-v2/service/eks v1.42.1 + github.com/aws/aws-sdk-go-v2/service/eks v1.43.0 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.24.4 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.5 github.com/aws/aws-sdk-go-v2/service/iam v1.32.0 @@ -127,8 +127,8 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect diff --git a/go.sum b/go.sum index bf0a457bfe..21aaa45831 100644 --- a/go.sum +++ b/go.sum @@ -716,8 +716,8 @@ github.com/aws/amazon-ec2-instance-selector/v2 v2.4.2-0.20230601180523-74e721cb8 github.com/aws/aws-sdk-go v1.51.16 h1:vnWKK8KjbftEkuPX8bRj3WHsLy1uhotn0eXptpvrxJI= github.com/aws/aws-sdk-go v1.51.16/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v1.16.15/go.mod h1:SwiyXi/1zTUZ6KIAmLK5V5ll8SiURNUYOqTerZPaF9k= -github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= -github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2 v1.27.1 h1:xypCL2owhog46iFxBKKpBcw+bPTX/RJzwNj8uSilENw= +github.com/aws/aws-sdk-go-v2 v1.27.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg= github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA= @@ -727,11 +727,11 @@ github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.22/go.mod h1:/vNv5Al0bpiF8YdX2Ov6Xy05VTiXsql94yUqJMYaj0w= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8 h1:RnLB7p6aaFMRfyQkD6ckxR7myCC9SABIqSz4czYUUbU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.8/go.mod h1:XH7dQJd+56wEbP1I4e4Duo+QhSMxNArE8VP7NuUOTeM= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.16/go.mod h1:62dsXI0BqTIGomDl8Hpm33dv0OntGaVblri3ZRParVQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8 h1:jzApk2f58L9yW9q1GEab3BMMFWUkkiZhyrRUtbwUbKU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.8/go.mod h1:WqO+FftfO3tGePUtQxPXM6iODVfqMwsVMgTbG/ZXIdQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/autoscaling v1.40.5 h1:vhdJymxlWS2qftzLiuCjSswjXBRLGfzo/BEE9LDveBA= @@ -746,8 +746,8 @@ github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.36.3 h1:JNWpkjIm github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.36.3/go.mod h1:TiLZ2/+WAEyG2PnuAYj/un46UJ7qBf5BWWTAKgaHP8I= github.com/aws/aws-sdk-go-v2/service/ec2 v1.156.0 h1:TFK9GeUINErClL2+A+GLYhjiChVdaXCgIUiCsS/UQrE= github.com/aws/aws-sdk-go-v2/service/ec2 v1.156.0/go.mod h1:xejKuuRDjz6z5OqyeLsz01MlOqqW7CqpAB4PabNvpu8= -github.com/aws/aws-sdk-go-v2/service/eks v1.42.1 h1:q7MWjPP0uCmUvuGDFCvkbqRkqfH+Bq6di9RTd64S0YM= -github.com/aws/aws-sdk-go-v2/service/eks v1.42.1/go.mod h1:UhKBrO0Ezz8iIg02a6u4irGKBKh0gTz3fF8LNdD2vDI= +github.com/aws/aws-sdk-go-v2/service/eks v1.43.0 h1:TRgA51vdnrXiZpCab7pQT0bF52rX5idH0/fzrIVnQS0= +github.com/aws/aws-sdk-go-v2/service/eks v1.43.0/go.mod h1:875ZmajQCZ9N7HeR1DE25nTSaalkqGYzQa+BxLattlQ= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.24.4 h1:V5YvSMQwZklktzYeOOhYdptx7rP650XP3RnxwNu1UEQ= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.24.4/go.mod h1:aYygRYqRxmLGrxRxAisgNarwo4x8bcJG14rh4r57VqE= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.30.5 h1:/x2u/TOx+n17U+gz98TOw1HKJom0EOqrhL4SjrHr0cQ= diff --git a/pkg/awsapi/eks.go b/pkg/awsapi/eks.go index d60d836fbb..13d254e94b 100644 --- a/pkg/awsapi/eks.go +++ b/pkg/awsapi/eks.go @@ -18,72 +18,92 @@ type EKS interface { // functional options. Options() eks.Options // Associates an access policy and its scope to an access entry. For more - // information about associating access policies, see Associating and - // disassociating access policies to and from access entries (https://docs.aws.amazon.com/eks/latest/userguide/access-policies.html) - // in the Amazon EKS User Guide. + // information about associating access policies, see [Associating and disassociating access policies to and from access entries]in the Amazon EKS User Guide. + // + // [Associating and disassociating access policies to and from access entries]: https://docs.aws.amazon.com/eks/latest/userguide/access-policies.html AssociateAccessPolicy(ctx context.Context, params *AssociateAccessPolicyInput, optFns ...func(*Options)) (*AssociateAccessPolicyOutput, error) - // Associates an encryption configuration to an existing cluster. Use this API to - // enable encryption on existing clusters that don't already have encryption - // enabled. This allows you to implement a defense-in-depth security strategy - // without migrating applications to new Amazon EKS clusters. + // Associates an encryption configuration to an existing cluster. + // + // Use this API to enable encryption on existing clusters that don't already have + // encryption enabled. This allows you to implement a defense-in-depth security + // strategy without migrating applications to new Amazon EKS clusters. AssociateEncryptionConfig(ctx context.Context, params *AssociateEncryptionConfigInput, optFns ...func(*Options)) (*AssociateEncryptionConfigOutput, error) - // Associates an identity provider configuration to a cluster. If you want to - // authenticate identities using an identity provider, you can create an identity - // provider configuration and associate it to your cluster. After configuring - // authentication to your cluster you can create Kubernetes Role and ClusterRole - // objects, assign permissions to them, and then bind them to the identities using - // Kubernetes RoleBinding and ClusterRoleBinding objects. For more information see - // Using RBAC Authorization (https://kubernetes.io/docs/reference/access-authn-authz/rbac/) - // in the Kubernetes documentation. + // Associates an identity provider configuration to a cluster. + // + // If you want to authenticate identities using an identity provider, you can + // create an identity provider configuration and associate it to your cluster. + // After configuring authentication to your cluster you can create Kubernetes Role + // and ClusterRole objects, assign permissions to them, and then bind them to the + // identities using Kubernetes RoleBinding and ClusterRoleBinding objects. For + // more information see [Using RBAC Authorization]in the Kubernetes documentation. + // + // [Using RBAC Authorization]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ AssociateIdentityProviderConfig(ctx context.Context, params *AssociateIdentityProviderConfigInput, optFns ...func(*Options)) (*AssociateIdentityProviderConfigOutput, error) - // Creates an access entry. An access entry allows an IAM principal to access your - // cluster. Access entries can replace the need to maintain entries in the aws-auth - // ConfigMap for authentication. You have the following options for authorizing an - // IAM principal to access Kubernetes objects on your cluster: Kubernetes - // role-based access control (RBAC), Amazon EKS, or both. Kubernetes RBAC - // authorization requires you to create and manage Kubernetes Role , ClusterRole , - // RoleBinding , and ClusterRoleBinding objects, in addition to managing access - // entries. If you use Amazon EKS authorization exclusively, you don't need to - // create and manage Kubernetes Role , ClusterRole , RoleBinding , and - // ClusterRoleBinding objects. For more information about access entries, see - // Access entries (https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html) - // in the Amazon EKS User Guide. + // Creates an access entry. + // + // An access entry allows an IAM principal to access your cluster. Access entries + // can replace the need to maintain entries in the aws-auth ConfigMap for + // authentication. You have the following options for authorizing an IAM principal + // to access Kubernetes objects on your cluster: Kubernetes role-based access + // control (RBAC), Amazon EKS, or both. Kubernetes RBAC authorization requires you + // to create and manage Kubernetes Role , ClusterRole , RoleBinding , and + // ClusterRoleBinding objects, in addition to managing access entries. If you use + // Amazon EKS authorization exclusively, you don't need to create and manage + // Kubernetes Role , ClusterRole , RoleBinding , and ClusterRoleBinding objects. + // + // For more information about access entries, see [Access entries] in the Amazon EKS User Guide. + // + // [Access entries]: https://docs.aws.amazon.com/eks/latest/userguide/access-entries.html CreateAccessEntry(ctx context.Context, params *CreateAccessEntryInput, optFns ...func(*Options)) (*CreateAccessEntryOutput, error) - // Creates an Amazon EKS add-on. Amazon EKS add-ons help to automate the - // provisioning and lifecycle management of common operational software for Amazon - // EKS clusters. For more information, see Amazon EKS add-ons (https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html) - // in the Amazon EKS User Guide. + // Creates an Amazon EKS add-on. + // + // Amazon EKS add-ons help to automate the provisioning and lifecycle management + // of common operational software for Amazon EKS clusters. For more information, + // see [Amazon EKS add-ons]in the Amazon EKS User Guide. + // + // [Amazon EKS add-ons]: https://docs.aws.amazon.com/eks/latest/userguide/eks-add-ons.html CreateAddon(ctx context.Context, params *CreateAddonInput, optFns ...func(*Options)) (*CreateAddonOutput, error) - // Creates an Amazon EKS control plane. The Amazon EKS control plane consists of - // control plane instances that run the Kubernetes software, such as etcd and the - // API server. The control plane runs in an account managed by Amazon Web Services, - // and the Kubernetes API is exposed by the Amazon EKS API server endpoint. Each - // Amazon EKS cluster control plane is single tenant and unique. It runs on its own - // set of Amazon EC2 instances. The cluster control plane is provisioned across - // multiple Availability Zones and fronted by an Elastic Load Balancing Network - // Load Balancer. Amazon EKS also provisions elastic network interfaces in your VPC - // subnets to provide connectivity from the control plane instances to the nodes - // (for example, to support kubectl exec , logs , and proxy data flows). Amazon - // EKS nodes run in your Amazon Web Services account and connect to your cluster's - // control plane over the Kubernetes API server endpoint and a certificate file - // that is created for your cluster. You can use the endpointPublicAccess and - // endpointPrivateAccess parameters to enable or disable public and private access - // to your cluster's Kubernetes API server endpoint. By default, public access is - // enabled, and private access is disabled. For more information, see Amazon EKS - // Cluster Endpoint Access Control (https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html) - // in the Amazon EKS User Guide . You can use the logging parameter to enable or - // disable exporting the Kubernetes control plane logs for your cluster to - // CloudWatch Logs. By default, cluster control plane logs aren't exported to - // CloudWatch Logs. For more information, see Amazon EKS Cluster Control Plane Logs (https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html) - // in the Amazon EKS User Guide . CloudWatch Logs ingestion, archive storage, and - // data scanning rates apply to exported control plane logs. For more information, - // see CloudWatch Pricing (http://aws.amazon.com/cloudwatch/pricing/) . In most - // cases, it takes several minutes to create a cluster. After you create an Amazon - // EKS cluster, you must configure your Kubernetes tooling to communicate with the - // API server and launch nodes into your cluster. For more information, see - // Managing Cluster Authentication (https://docs.aws.amazon.com/eks/latest/userguide/managing-auth.html) - // and Launching Amazon EKS nodes (https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html) - // in the Amazon EKS User Guide. + // Creates an Amazon EKS control plane. + // + // The Amazon EKS control plane consists of control plane instances that run the + // Kubernetes software, such as etcd and the API server. The control plane runs in + // an account managed by Amazon Web Services, and the Kubernetes API is exposed by + // the Amazon EKS API server endpoint. Each Amazon EKS cluster control plane is + // single tenant and unique. It runs on its own set of Amazon EC2 instances. + // + // The cluster control plane is provisioned across multiple Availability Zones and + // fronted by an Elastic Load Balancing Network Load Balancer. Amazon EKS also + // provisions elastic network interfaces in your VPC subnets to provide + // connectivity from the control plane instances to the nodes (for example, to + // support kubectl exec , logs , and proxy data flows). + // + // Amazon EKS nodes run in your Amazon Web Services account and connect to your + // cluster's control plane over the Kubernetes API server endpoint and a + // certificate file that is created for your cluster. + // + // You can use the endpointPublicAccess and endpointPrivateAccess parameters to + // enable or disable public and private access to your cluster's Kubernetes API + // server endpoint. By default, public access is enabled, and private access is + // disabled. For more information, see [Amazon EKS Cluster Endpoint Access Control]in the Amazon EKS User Guide . + // + // You can use the logging parameter to enable or disable exporting the Kubernetes + // control plane logs for your cluster to CloudWatch Logs. By default, cluster + // control plane logs aren't exported to CloudWatch Logs. For more information, see + // [Amazon EKS Cluster Control Plane Logs]in the Amazon EKS User Guide . + // + // CloudWatch Logs ingestion, archive storage, and data scanning rates apply to + // exported control plane logs. For more information, see [CloudWatch Pricing]. + // + // In most cases, it takes several minutes to create a cluster. After you create + // an Amazon EKS cluster, you must configure your Kubernetes tooling to communicate + // with the API server and launch nodes into your cluster. For more information, + // see [Allowing users to access your cluster]and [Launching Amazon EKS nodes] in the Amazon EKS User Guide. + // + // [Allowing users to access your cluster]: https://docs.aws.amazon.com/eks/latest/userguide/cluster-auth.html + // [CloudWatch Pricing]: http://aws.amazon.com/cloudwatch/pricing/ + // [Amazon EKS Cluster Control Plane Logs]: https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html + // [Amazon EKS Cluster Endpoint Access Control]: https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html + // [Launching Amazon EKS nodes]: https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html CreateCluster(ctx context.Context, params *CreateClusterInput, optFns ...func(*Options)) (*CreateClusterOutput, error) // Creates an EKS Anywhere subscription. When a subscription is created, it is a // contract agreement for the length of the term specified in the request. Licenses @@ -92,72 +112,98 @@ type EKS interface { // Packages. CreateEksAnywhereSubscription(ctx context.Context, params *CreateEksAnywhereSubscriptionInput, optFns ...func(*Options)) (*CreateEksAnywhereSubscriptionOutput, error) // Creates an Fargate profile for your Amazon EKS cluster. You must have at least - // one Fargate profile in a cluster to be able to run pods on Fargate. The Fargate - // profile allows an administrator to declare which pods run on Fargate and specify - // which pods run on which Fargate profile. This declaration is done through the - // profile’s selectors. Each profile can have up to five selectors that contain a - // namespace and labels. A namespace is required for every selector. The label - // field consists of multiple optional key-value pairs. Pods that match the - // selectors are scheduled on Fargate. If a to-be-scheduled pod matches any of the - // selectors in the Fargate profile, then that pod is run on Fargate. When you - // create a Fargate profile, you must specify a pod execution role to use with the - // pods that are scheduled with the profile. This role is added to the cluster's - // Kubernetes Role Based Access Control (https://kubernetes.io/docs/reference/access-authn-authz/rbac/) - // (RBAC) for authorization so that the kubelet that is running on the Fargate - // infrastructure can register with your Amazon EKS cluster so that it can appear - // in your cluster as a node. The pod execution role also provides IAM permissions - // to the Fargate infrastructure to allow read access to Amazon ECR image - // repositories. For more information, see Pod Execution Role (https://docs.aws.amazon.com/eks/latest/userguide/pod-execution-role.html) - // in the Amazon EKS User Guide. Fargate profiles are immutable. However, you can - // create a new updated profile to replace an existing profile and then delete the - // original after the updated profile has finished creating. If any Fargate - // profiles in a cluster are in the DELETING status, you must wait for that - // Fargate profile to finish deleting before you can create any other profiles in - // that cluster. For more information, see Fargate profile (https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html) - // in the Amazon EKS User Guide. + // one Fargate profile in a cluster to be able to run pods on Fargate. + // + // The Fargate profile allows an administrator to declare which pods run on + // Fargate and specify which pods run on which Fargate profile. This declaration is + // done through the profile’s selectors. Each profile can have up to five selectors + // that contain a namespace and labels. A namespace is required for every selector. + // The label field consists of multiple optional key-value pairs. Pods that match + // the selectors are scheduled on Fargate. If a to-be-scheduled pod matches any of + // the selectors in the Fargate profile, then that pod is run on Fargate. + // + // When you create a Fargate profile, you must specify a pod execution role to use + // with the pods that are scheduled with the profile. This role is added to the + // cluster's Kubernetes [Role Based Access Control](RBAC) for authorization so that the kubelet that is + // running on the Fargate infrastructure can register with your Amazon EKS cluster + // so that it can appear in your cluster as a node. The pod execution role also + // provides IAM permissions to the Fargate infrastructure to allow read access to + // Amazon ECR image repositories. For more information, see [Pod Execution Role]in the Amazon EKS User + // Guide. + // + // Fargate profiles are immutable. However, you can create a new updated profile + // to replace an existing profile and then delete the original after the updated + // profile has finished creating. + // + // If any Fargate profiles in a cluster are in the DELETING status, you must wait + // for that Fargate profile to finish deleting before you can create any other + // profiles in that cluster. + // + // For more information, see [Fargate profile] in the Amazon EKS User Guide. + // + // [Role Based Access Control]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ + // [Fargate profile]: https://docs.aws.amazon.com/eks/latest/userguide/fargate-profile.html + // [Pod Execution Role]: https://docs.aws.amazon.com/eks/latest/userguide/pod-execution-role.html CreateFargateProfile(ctx context.Context, params *CreateFargateProfileInput, optFns ...func(*Options)) (*CreateFargateProfileOutput, error) - // Creates a managed node group for an Amazon EKS cluster. You can only create a - // node group for your cluster that is equal to the current Kubernetes version for - // the cluster. All node groups are created with the latest AMI release version for - // the respective minor Kubernetes version of the cluster, unless you deploy a - // custom AMI using a launch template. For more information about using launch - // templates, see Launch template support (https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html) - // . An Amazon EKS managed node group is an Amazon EC2 Auto Scaling group and + // Creates a managed node group for an Amazon EKS cluster. + // + // You can only create a node group for your cluster that is equal to the current + // Kubernetes version for the cluster. All node groups are created with the latest + // AMI release version for the respective minor Kubernetes version of the cluster, + // unless you deploy a custom AMI using a launch template. For more information + // about using launch templates, see [Customizing managed nodes with launch templates]. + // + // An Amazon EKS managed node group is an Amazon EC2 Auto Scaling group and // associated Amazon EC2 instances that are managed by Amazon Web Services for an - // Amazon EKS cluster. For more information, see Managed node groups (https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html) - // in the Amazon EKS User Guide. Windows AMI types are only supported for - // commercial Amazon Web Services Regions that support Windows on Amazon EKS. + // Amazon EKS cluster. For more information, see [Managed node groups]in the Amazon EKS User Guide. + // + // Windows AMI types are only supported for commercial Amazon Web Services Regions + // that support Windows on Amazon EKS. + // + // [Customizing managed nodes with launch templates]: https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html + // [Managed node groups]: https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html CreateNodegroup(ctx context.Context, params *CreateNodegroupInput, optFns ...func(*Options)) (*CreateNodegroupOutput, error) // Creates an EKS Pod Identity association between a service account in an Amazon // EKS cluster and an IAM role with EKS Pod Identity. Use EKS Pod Identity to give // temporary IAM credentials to pods and the credentials are rotated automatically. + // // Amazon EKS Pod Identity associations provide the ability to manage credentials // for your applications, similar to the way that Amazon EC2 instance profiles - // provide credentials to Amazon EC2 instances. If a pod uses a service account - // that has an association, Amazon EKS sets environment variables in the containers - // of the pod. The environment variables configure the Amazon Web Services SDKs, - // including the Command Line Interface, to use the EKS Pod Identity credentials. + // provide credentials to Amazon EC2 instances. + // + // If a pod uses a service account that has an association, Amazon EKS sets + // environment variables in the containers of the pod. The environment variables + // configure the Amazon Web Services SDKs, including the Command Line Interface, to + // use the EKS Pod Identity credentials. + // // Pod Identity is a simpler method than IAM roles for service accounts, as this // method doesn't use OIDC identity providers. Additionally, you can configure a // role for Pod Identity once, and reuse it across clusters. CreatePodIdentityAssociation(ctx context.Context, params *CreatePodIdentityAssociationInput, optFns ...func(*Options)) (*CreatePodIdentityAssociationOutput, error) - // Deletes an access entry. Deleting an access entry of a type other than Standard - // can cause your cluster to function improperly. If you delete an access entry in - // error, you can recreate it. + // Deletes an access entry. + // + // Deleting an access entry of a type other than Standard can cause your cluster + // to function improperly. If you delete an access entry in error, you can recreate + // it. DeleteAccessEntry(ctx context.Context, params *DeleteAccessEntryInput, optFns ...func(*Options)) (*DeleteAccessEntryOutput, error) - // Deletes an Amazon EKS add-on. When you remove an add-on, it's deleted from the - // cluster. You can always manually start an add-on on the cluster using the - // Kubernetes API. + // Deletes an Amazon EKS add-on. + // + // When you remove an add-on, it's deleted from the cluster. You can always + // manually start an add-on on the cluster using the Kubernetes API. DeleteAddon(ctx context.Context, params *DeleteAddonInput, optFns ...func(*Options)) (*DeleteAddonOutput, error) - // Deletes an Amazon EKS cluster control plane. If you have active services in - // your cluster that are associated with a load balancer, you must delete those - // services before deleting the cluster so that the load balancers are deleted - // properly. Otherwise, you can have orphaned resources in your VPC that prevent - // you from being able to delete the VPC. For more information, see Deleting a - // cluster (https://docs.aws.amazon.com/eks/latest/userguide/delete-cluster.html) - // in the Amazon EKS User Guide. If you have managed node groups or Fargate - // profiles attached to the cluster, you must delete them first. For more - // information, see DeleteNodgroup and DeleteFargateProfile . + // Deletes an Amazon EKS cluster control plane. + // + // If you have active services in your cluster that are associated with a load + // balancer, you must delete those services before deleting the cluster so that the + // load balancers are deleted properly. Otherwise, you can have orphaned resources + // in your VPC that prevent you from being able to delete the VPC. For more + // information, see [Deleting a cluster]in the Amazon EKS User Guide. + // + // If you have managed node groups or Fargate profiles attached to the cluster, + // you must delete them first. For more information, see DeleteNodgroup and + // DeleteFargateProfile . + // + // [Deleting a cluster]: https://docs.aws.amazon.com/eks/latest/userguide/delete-cluster.html DeleteCluster(ctx context.Context, params *DeleteClusterInput, optFns ...func(*Options)) (*DeleteClusterOutput, error) // Deletes an expired or inactive subscription. Deleting inactive subscriptions // removes them from the Amazon Web Services Management Console view and from @@ -165,25 +211,33 @@ type EKS interface { // of creation and are cancelled by creating a ticket in the Amazon Web Services // Support Center. DeleteEksAnywhereSubscription(ctx context.Context, params *DeleteEksAnywhereSubscriptionInput, optFns ...func(*Options)) (*DeleteEksAnywhereSubscriptionOutput, error) - // Deletes an Fargate profile. When you delete a Fargate profile, any Pod running - // on Fargate that was created with the profile is deleted. If the Pod matches - // another Fargate profile, then it is scheduled on Fargate with that profile. If - // it no longer matches any Fargate profiles, then it's not scheduled on Fargate - // and may remain in a pending state. Only one Fargate profile in a cluster can be - // in the DELETING status at a time. You must wait for a Fargate profile to finish - // deleting before you can delete any other profiles in that cluster. + // Deletes an Fargate profile. + // + // When you delete a Fargate profile, any Pod running on Fargate that was created + // with the profile is deleted. If the Pod matches another Fargate profile, then + // it is scheduled on Fargate with that profile. If it no longer matches any + // Fargate profiles, then it's not scheduled on Fargate and may remain in a pending + // state. + // + // Only one Fargate profile in a cluster can be in the DELETING status at a time. + // You must wait for a Fargate profile to finish deleting before you can delete any + // other profiles in that cluster. DeleteFargateProfile(ctx context.Context, params *DeleteFargateProfileInput, optFns ...func(*Options)) (*DeleteFargateProfileOutput, error) // Deletes a managed node group. DeleteNodegroup(ctx context.Context, params *DeleteNodegroupInput, optFns ...func(*Options)) (*DeleteNodegroupOutput, error) - // Deletes a EKS Pod Identity association. The temporary Amazon Web Services - // credentials from the previous IAM role session might still be valid until the - // session expiry. If you need to immediately revoke the temporary session - // credentials, then go to the role in the IAM console. + // Deletes a EKS Pod Identity association. + // + // The temporary Amazon Web Services credentials from the previous IAM role + // session might still be valid until the session expiry. If you need to + // immediately revoke the temporary session credentials, then go to the role in the + // IAM console. DeletePodIdentityAssociation(ctx context.Context, params *DeletePodIdentityAssociationInput, optFns ...func(*Options)) (*DeletePodIdentityAssociationOutput, error) // Deregisters a connected cluster to remove it from the Amazon EKS control plane. + // // A connected cluster is a Kubernetes cluster that you've connected to your - // control plane using the Amazon EKS Connector (https://docs.aws.amazon.com/eks/latest/userguide/eks-connector.html) - // . + // control plane using the [Amazon EKS Connector]. + // + // [Amazon EKS Connector]: https://docs.aws.amazon.com/eks/latest/userguide/eks-connector.html DeregisterCluster(ctx context.Context, params *DeregisterClusterInput, optFns ...func(*Options)) (*DeregisterClusterOutput, error) // Describes an access entry. DescribeAccessEntry(ctx context.Context, params *DescribeAccessEntryInput, optFns ...func(*Options)) (*DescribeAccessEntryOutput, error) @@ -191,16 +245,21 @@ type EKS interface { DescribeAddon(ctx context.Context, params *DescribeAddonInput, optFns ...func(*Options)) (*DescribeAddonOutput, error) // Returns configuration options. DescribeAddonConfiguration(ctx context.Context, params *DescribeAddonConfigurationInput, optFns ...func(*Options)) (*DescribeAddonConfigurationOutput, error) - // Describes the versions for an add-on. Information such as the Kubernetes - // versions that you can use the add-on with, the owner , publisher , and the type - // of the add-on are returned. + // Describes the versions for an add-on. + // + // Information such as the Kubernetes versions that you can use the add-on with, + // the owner , publisher , and the type of the add-on are returned. DescribeAddonVersions(ctx context.Context, params *DescribeAddonVersionsInput, optFns ...func(*Options)) (*DescribeAddonVersionsOutput, error) - // Describes an Amazon EKS cluster. The API server endpoint and certificate - // authority data returned by this operation are required for kubelet and kubectl - // to communicate with your Kubernetes API server. For more information, see - // Creating or updating a kubeconfig file for an Amazon EKS cluster (https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html) - // . The API server endpoint and certificate authority data aren't available until + // Describes an Amazon EKS cluster. + // + // The API server endpoint and certificate authority data returned by this + // operation are required for kubelet and kubectl to communicate with your + // Kubernetes API server. For more information, see [Creating or updating a kubeconfig file for an Amazon EKS cluster]kubeconfig . + // + // The API server endpoint and certificate authority data aren't available until // the cluster reaches the ACTIVE state. + // + // [Creating or updating a kubeconfig file for an Amazon EKS cluster]: https://docs.aws.amazon.com/eks/latest/userguide/create-kubeconfig.html DescribeCluster(ctx context.Context, params *DescribeClusterInput, optFns ...func(*Options)) (*DescribeClusterOutput, error) // Returns descriptive information about a subscription. DescribeEksAnywhereSubscription(ctx context.Context, params *DescribeEksAnywhereSubscriptionInput, optFns ...func(*Options)) (*DescribeEksAnywhereSubscriptionOutput, error) @@ -212,21 +271,25 @@ type EKS interface { DescribeInsight(ctx context.Context, params *DescribeInsightInput, optFns ...func(*Options)) (*DescribeInsightOutput, error) // Describes a managed node group. DescribeNodegroup(ctx context.Context, params *DescribeNodegroupInput, optFns ...func(*Options)) (*DescribeNodegroupOutput, error) - // Returns descriptive information about an EKS Pod Identity association. This - // action requires the ID of the association. You can get the ID from the response - // to the CreatePodIdentityAssocation for newly created associations. Or, you can - // list the IDs for associations with ListPodIdentityAssociations and filter the - // list by namespace or service account. + // Returns descriptive information about an EKS Pod Identity association. + // + // This action requires the ID of the association. You can get the ID from the + // response to the CreatePodIdentityAssocation for newly created associations. Or, + // you can list the IDs for associations with ListPodIdentityAssociations and + // filter the list by namespace or service account. DescribePodIdentityAssociation(ctx context.Context, params *DescribePodIdentityAssociationInput, optFns ...func(*Options)) (*DescribePodIdentityAssociationOutput, error) - // Describes an update to an Amazon EKS resource. When the status of the update is - // Succeeded , the update is complete. If an update fails, the status is Failed , - // and an error detail explains the reason for the failure. + // Describes an update to an Amazon EKS resource. + // + // When the status of the update is Succeeded , the update is complete. If an + // update fails, the status is Failed , and an error detail explains the reason for + // the failure. DescribeUpdate(ctx context.Context, params *DescribeUpdateInput, optFns ...func(*Options)) (*DescribeUpdateOutput, error) // Disassociates an access policy from an access entry. DisassociateAccessPolicy(ctx context.Context, params *DisassociateAccessPolicyInput, optFns ...func(*Options)) (*DisassociateAccessPolicyOutput, error) - // Disassociates an identity provider configuration from a cluster. If you - // disassociate an identity provider from your cluster, users included in the - // provider can no longer access the cluster. However, you can still access the + // Disassociates an identity provider configuration from a cluster. + // + // If you disassociate an identity provider from your cluster, users included in + // the provider can no longer access the cluster. However, you can still access the // cluster with IAM principals. DisassociateIdentityProviderConfig(ctx context.Context, params *DisassociateIdentityProviderConfigInput, optFns ...func(*Options)) (*DisassociateIdentityProviderConfigOutput, error) // Lists the access entries for your cluster. @@ -264,17 +327,23 @@ type EKS interface { // Lists the updates associated with an Amazon EKS resource in your Amazon Web // Services account, in the specified Amazon Web Services Region. ListUpdates(ctx context.Context, params *ListUpdatesInput, optFns ...func(*Options)) (*ListUpdatesOutput, error) - // Connects a Kubernetes cluster to the Amazon EKS control plane. Any Kubernetes - // cluster can be connected to the Amazon EKS control plane to view current - // information about the cluster and its nodes. Cluster connection requires two - // steps. First, send a RegisterClusterRequest to add it to the Amazon EKS control - // plane. Second, a Manifest (https://amazon-eks.s3.us-west-2.amazonaws.com/eks-connector/manifests/eks-connector/latest/eks-connector.yaml) - // containing the activationID and activationCode must be applied to the - // Kubernetes cluster through it's native provider to provide visibility. After the - // manifest is updated and applied, the connected cluster is visible to the Amazon - // EKS control plane. If the manifest isn't applied within three days, the - // connected cluster will no longer be visible and must be deregistered using + // Connects a Kubernetes cluster to the Amazon EKS control plane. + // + // Any Kubernetes cluster can be connected to the Amazon EKS control plane to view + // current information about the cluster and its nodes. + // + // Cluster connection requires two steps. First, send a RegisterClusterRequest to add it to the Amazon + // EKS control plane. + // + // Second, a [Manifest] containing the activationID and activationCode must be applied to + // the Kubernetes cluster through it's native provider to provide visibility. + // + // After the manifest is updated and applied, the connected cluster is visible to + // the Amazon EKS control plane. If the manifest isn't applied within three days, + // the connected cluster will no longer be visible and must be deregistered using // DeregisterCluster . + // + // [Manifest]: https://amazon-eks.s3.us-west-2.amazonaws.com/eks-connector/manifests/eks-connector/latest/eks-connector.yaml RegisterCluster(ctx context.Context, params *RegisterClusterInput, optFns ...func(*Options)) (*RegisterClusterOutput, error) // Associates the specified tags to an Amazon EKS resource with the specified // resourceArn . If existing tags on a resource are not specified in the request @@ -292,69 +361,89 @@ type EKS interface { UpdateAddon(ctx context.Context, params *UpdateAddonInput, optFns ...func(*Options)) (*UpdateAddonOutput, error) // Updates an Amazon EKS cluster configuration. Your cluster continues to function // during the update. The response output includes an update ID that you can use to - // track the status of your cluster update with DescribeUpdate "/>. You can use - // this API operation to enable or disable exporting the Kubernetes control plane - // logs for your cluster to CloudWatch Logs. By default, cluster control plane logs - // aren't exported to CloudWatch Logs. For more information, see Amazon EKS - // Cluster control plane logs (https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html) - // in the Amazon EKS User Guide . CloudWatch Logs ingestion, archive storage, and - // data scanning rates apply to exported control plane logs. For more information, - // see CloudWatch Pricing (http://aws.amazon.com/cloudwatch/pricing/) . You can - // also use this API operation to enable or disable public and private access to - // your cluster's Kubernetes API server endpoint. By default, public access is - // enabled, and private access is disabled. For more information, see Amazon EKS - // cluster endpoint access control (https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html) - // in the Amazon EKS User Guide . You can also use this API operation to choose - // different subnets and security groups for the cluster. You must specify at least - // two subnets that are in different Availability Zones. You can't change which VPC - // the subnets are from, the subnets must be in the same VPC as the subnets that - // the cluster was created with. For more information about the VPC requirements, - // see https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html (https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html) - // in the Amazon EKS User Guide . Cluster updates are asynchronous, and they should - // finish within a few minutes. During an update, the cluster status moves to - // UPDATING (this status transition is eventually consistent). When the update is - // complete (either Failed or Successful ), the cluster status moves to Active . + // track the status of your cluster update with DescribeUpdate "/>. + // + // You can use this API operation to enable or disable exporting the Kubernetes + // control plane logs for your cluster to CloudWatch Logs. By default, cluster + // control plane logs aren't exported to CloudWatch Logs. For more information, see + // [Amazon EKS Cluster control plane logs]in the Amazon EKS User Guide . + // + // CloudWatch Logs ingestion, archive storage, and data scanning rates apply to + // exported control plane logs. For more information, see [CloudWatch Pricing]. + // + // You can also use this API operation to enable or disable public and private + // access to your cluster's Kubernetes API server endpoint. By default, public + // access is enabled, and private access is disabled. For more information, see [Amazon EKS cluster endpoint access control]in + // the Amazon EKS User Guide . + // + // You can also use this API operation to choose different subnets and security + // groups for the cluster. You must specify at least two subnets that are in + // different Availability Zones. You can't change which VPC the subnets are from, + // the subnets must be in the same VPC as the subnets that the cluster was created + // with. For more information about the VPC requirements, see [https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html]in the Amazon EKS + // User Guide . + // + // Cluster updates are asynchronous, and they should finish within a few minutes. + // During an update, the cluster status moves to UPDATING (this status transition + // is eventually consistent). When the update is complete (either Failed or + // Successful ), the cluster status moves to Active . + // + // [Amazon EKS Cluster control plane logs]: https://docs.aws.amazon.com/eks/latest/userguide/control-plane-logs.html + // [CloudWatch Pricing]: http://aws.amazon.com/cloudwatch/pricing/ + // [https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html]: https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html + // [Amazon EKS cluster endpoint access control]: https://docs.aws.amazon.com/eks/latest/userguide/cluster-endpoint.html UpdateClusterConfig(ctx context.Context, params *UpdateClusterConfigInput, optFns ...func(*Options)) (*UpdateClusterConfigOutput, error) // Updates an Amazon EKS cluster to the specified Kubernetes version. Your cluster // continues to function during the update. The response output includes an update - // ID that you can use to track the status of your cluster update with the - // DescribeUpdate API operation. Cluster updates are asynchronous, and they should - // finish within a few minutes. During an update, the cluster status moves to - // UPDATING (this status transition is eventually consistent). When the update is - // complete (either Failed or Successful ), the cluster status moves to Active . If - // your cluster has managed node groups attached to it, all of your node groups’ - // Kubernetes versions must match the cluster’s Kubernetes version in order to - // update the cluster to a new Kubernetes version. + // ID that you can use to track the status of your cluster update with the DescribeUpdateAPI + // operation. + // + // Cluster updates are asynchronous, and they should finish within a few minutes. + // During an update, the cluster status moves to UPDATING (this status transition + // is eventually consistent). When the update is complete (either Failed or + // Successful ), the cluster status moves to Active . + // + // If your cluster has managed node groups attached to it, all of your node + // groups’ Kubernetes versions must match the cluster’s Kubernetes version in order + // to update the cluster to a new Kubernetes version. UpdateClusterVersion(ctx context.Context, params *UpdateClusterVersionInput, optFns ...func(*Options)) (*UpdateClusterVersionOutput, error) // Update an EKS Anywhere Subscription. Only auto renewal and tags can be updated // after subscription creation. UpdateEksAnywhereSubscription(ctx context.Context, params *UpdateEksAnywhereSubscriptionInput, optFns ...func(*Options)) (*UpdateEksAnywhereSubscriptionOutput, error) // Updates an Amazon EKS managed node group configuration. Your node group // continues to function during the update. The response output includes an update - // ID that you can use to track the status of your node group update with the - // DescribeUpdate API operation. Currently you can update the Kubernetes labels for - // a node group or the scaling configuration. + // ID that you can use to track the status of your node group update with the DescribeUpdateAPI + // operation. Currently you can update the Kubernetes labels for a node group or + // the scaling configuration. UpdateNodegroupConfig(ctx context.Context, params *UpdateNodegroupConfigInput, optFns ...func(*Options)) (*UpdateNodegroupConfigOutput, error) // Updates the Kubernetes version or AMI version of an Amazon EKS managed node - // group. You can update a node group using a launch template only if the node - // group was originally deployed with a launch template. If you need to update a - // custom AMI in a node group that was deployed with a launch template, then update - // your custom AMI, specify the new ID in a new version of the launch template, and - // then update the node group to the new version of the launch template. If you - // update without a launch template, then you can update to the latest available - // AMI version of a node group's current Kubernetes version by not specifying a - // Kubernetes version in the request. You can update to the latest AMI version of - // your cluster's current Kubernetes version by specifying your cluster's - // Kubernetes version in the request. For information about Linux versions, see - // Amazon EKS optimized Amazon Linux AMI versions (https://docs.aws.amazon.com/eks/latest/userguide/eks-linux-ami-versions.html) - // in the Amazon EKS User Guide. For information about Windows versions, see - // Amazon EKS optimized Windows AMI versions (https://docs.aws.amazon.com/eks/latest/userguide/eks-ami-versions-windows.html) - // in the Amazon EKS User Guide. You cannot roll back a node group to an earlier - // Kubernetes version or AMI version. When a node in a managed node group is - // terminated due to a scaling action or update, every Pod on that node is drained - // first. Amazon EKS attempts to drain the nodes gracefully and will fail if it is - // unable to do so. You can force the update if Amazon EKS is unable to drain the - // nodes as a result of a Pod disruption budget issue. + // group. + // + // You can update a node group using a launch template only if the node group was + // originally deployed with a launch template. If you need to update a custom AMI + // in a node group that was deployed with a launch template, then update your + // custom AMI, specify the new ID in a new version of the launch template, and then + // update the node group to the new version of the launch template. + // + // If you update without a launch template, then you can update to the latest + // available AMI version of a node group's current Kubernetes version by not + // specifying a Kubernetes version in the request. You can update to the latest AMI + // version of your cluster's current Kubernetes version by specifying your + // cluster's Kubernetes version in the request. For information about Linux + // versions, see [Amazon EKS optimized Amazon Linux AMI versions]in the Amazon EKS User Guide. For information about Windows + // versions, see [Amazon EKS optimized Windows AMI versions]in the Amazon EKS User Guide. + // + // You cannot roll back a node group to an earlier Kubernetes version or AMI + // version. + // + // When a node in a managed node group is terminated due to a scaling action or + // update, every Pod on that node is drained first. Amazon EKS attempts to drain + // the nodes gracefully and will fail if it is unable to do so. You can force the + // update if Amazon EKS is unable to drain the nodes as a result of a Pod + // disruption budget issue. + // + // [Amazon EKS optimized Amazon Linux AMI versions]: https://docs.aws.amazon.com/eks/latest/userguide/eks-linux-ami-versions.html + // [Amazon EKS optimized Windows AMI versions]: https://docs.aws.amazon.com/eks/latest/userguide/eks-ami-versions-windows.html UpdateNodegroupVersion(ctx context.Context, params *UpdateNodegroupVersionInput, optFns ...func(*Options)) (*UpdateNodegroupVersionOutput, error) // Updates a EKS Pod Identity association. Only the IAM role can be changed; an // association can't be moved between clusters, namespaces, or service accounts. If From e64db43bd455518d0712cff213df738a501d80ac Mon Sep 17 00:00:00 2001 From: tiberiugc Date: Tue, 4 Jun 2024 00:16:52 +0300 Subject: [PATCH 34/35] use service level endpoint resolver instead of global endpoint resolver which was deprecated --- pkg/eks/api.go | 11 +++------ pkg/eks/apiv2.go | 53 ------------------------------------------ pkg/eks/services_v2.go | 33 ++++++++++++++++++++++---- 3 files changed, 31 insertions(+), 66 deletions(-) diff --git a/pkg/eks/api.go b/pkg/eks/api.go index ebae086e71..9c8e8582e5 100644 --- a/pkg/eks/api.go +++ b/pkg/eks/api.go @@ -200,14 +200,9 @@ func newAWSProvider(spec *api.ProviderConfig, configurationLoader AWSConfigurati provider.asg = autoscaling.NewFromConfig(cfg) provider.cloudwatchlogs = cloudwatchlogs.NewFromConfig(cfg) - provider.cloudtrail = cloudtrail.NewFromConfig(cfg) - - if endpoint, ok := os.LookupEnv("AWS_CLOUDTRAIL_ENDPOINT"); ok { - logger.Debug("Setting CloudTrail endpoint to %s", endpoint) - provider.cloudtrail = cloudtrail.NewFromConfig(cfg, func(o *cloudtrail.Options) { - o.BaseEndpoint = &endpoint - }) - } + provider.cloudtrail = cloudtrail.NewFromConfig(cfg, func(o *cloudtrail.Options) { + o.BaseEndpoint = getBaseEndpoint(cloudtrail.ServiceID, "AWS_CLOUDTRAIL_ENDPOINT") + }) return provider, nil } diff --git a/pkg/eks/apiv2.go b/pkg/eks/apiv2.go index 47cca9cd8c..cd5f633f7c 100644 --- a/pkg/eks/apiv2.go +++ b/pkg/eks/apiv2.go @@ -3,21 +3,12 @@ package eks import ( "context" "fmt" - "os" "time" "github.com/aws/aws-sdk-go-v2/aws" middlewarev2 "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" - "github.com/aws/aws-sdk-go-v2/service/cloudformation" - "github.com/aws/aws-sdk-go-v2/service/cloudtrail" - "github.com/aws/aws-sdk-go-v2/service/ec2" - "github.com/aws/aws-sdk-go-v2/service/eks" - "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" - "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" - "github.com/aws/aws-sdk-go-v2/service/iam" - "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/aws/smithy-go/middleware" "github.com/gofrs/flock" "github.com/kris-nova/logger" @@ -55,10 +46,6 @@ func newV2Config(pc *api.ProviderConfig, credentialsCacheFilePath string, config } options = append(options, config.WithClientLogMode(clientLogMode)) - if endpointResolver := makeEndpointResolverFunc(); endpointResolver != nil { - options = append(options, config.WithEndpointResolverWithOptions(endpointResolver)) - } - if !pc.Profile.SourceIsEnvVar { options = append(options, config.WithSharedConfigProfile(pc.Profile.Name)) } @@ -99,43 +86,3 @@ func newV2Config(pc *api.ProviderConfig, credentialsCacheFilePath string, config } return cfg, nil } - -func makeEndpointResolverFunc() aws.EndpointResolverWithOptionsFunc { - serviceIDEnvMap := map[string]string{ - cloudformation.ServiceID: "AWS_CLOUDFORMATION_ENDPOINT", - eks.ServiceID: "AWS_EKS_ENDPOINT", - ec2.ServiceID: "AWS_EC2_ENDPOINT", - elasticloadbalancing.ServiceID: "AWS_ELB_ENDPOINT", - elasticloadbalancingv2.ServiceID: "AWS_ELBV2_ENDPOINT", - sts.ServiceID: "AWS_STS_ENDPOINT", - iam.ServiceID: "AWS_IAM_ENDPOINT", - cloudtrail.ServiceID: "AWS_CLOUDTRAIL_ENDPOINT", - } - - hasCustomEndpoint := false - for service, envName := range serviceIDEnvMap { - if endpoint, ok := os.LookupEnv(envName); ok { - logger.Debug( - "Setting %s endpoint to %s", service, endpoint) - hasCustomEndpoint = true - } - } - - if !hasCustomEndpoint { - return nil - } - - return func(service, region string, options ...interface{}) (aws.Endpoint, error) { - if envName, ok := serviceIDEnvMap[service]; ok { - if ok { - if endpoint, ok := os.LookupEnv(envName); ok { - return aws.Endpoint{ - URL: endpoint, - SigningRegion: region, - }, nil - } - } - } - return aws.Endpoint{}, &aws.EndpointNotFoundError{} - } -} diff --git a/pkg/eks/services_v2.go b/pkg/eks/services_v2.go index 22c24dafce..26154f6c1f 100644 --- a/pkg/eks/services_v2.go +++ b/pkg/eks/services_v2.go @@ -1,6 +1,7 @@ package eks import ( + "os" "sync" "github.com/aws/aws-sdk-go-v2/aws" @@ -14,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/outposts" "github.com/aws/aws-sdk-go-v2/service/ssm" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/awsapi" @@ -45,6 +47,7 @@ func (s *ServicesV2) STS() awsapi.STS { defer s.mu.Unlock() if s.sts == nil { s.sts = sts.NewFromConfig(s.config, func(o *sts.Options) { + o.BaseEndpoint = getBaseEndpoint(sts.ServiceID, "AWS_STS_ENDPOINT") // Disable retryer for STS // (see https://github.com/eksctl-io/eksctl/issues/705) o.Retryer = aws.NopRetryer{} @@ -75,6 +78,7 @@ func (s *ServicesV2) CloudFormation() awsapi.CloudFormation { defer s.mu.Unlock() if s.cloudformation == nil { s.cloudformation = cloudformation.NewFromConfig(s.config, func(o *cloudformation.Options) { + o.BaseEndpoint = getBaseEndpoint(cloudformation.ServiceID, "AWS_CLOUDFORMATION_ENDPOINT") // Use adaptive mode for retrying CloudFormation requests to mimic // the logic used for AWS SDK v1. o.Retryer = retry.NewAdaptiveMode(func(o *retry.AdaptiveModeOptions) { @@ -94,7 +98,9 @@ func (s *ServicesV2) ELB() awsapi.ELB { s.mu.Lock() defer s.mu.Unlock() if s.elasticloadbalancing == nil { - s.elasticloadbalancing = elasticloadbalancing.NewFromConfig(s.config) + s.elasticloadbalancing = elasticloadbalancing.NewFromConfig(s.config, func(o *elasticloadbalancing.Options) { + o.BaseEndpoint = getBaseEndpoint(elasticloadbalancing.ServiceID, "AWS_ELB_ENDPOINT") + }) } return s.elasticloadbalancing } @@ -104,7 +110,9 @@ func (s *ServicesV2) ELBV2() awsapi.ELBV2 { s.mu.Lock() defer s.mu.Unlock() if s.elasticloadbalancingV2 == nil { - s.elasticloadbalancingV2 = elasticloadbalancingv2.NewFromConfig(s.config) + s.elasticloadbalancingV2 = elasticloadbalancingv2.NewFromConfig(s.config, func(o *elasticloadbalancingv2.Options) { + o.BaseEndpoint = getBaseEndpoint(elasticloadbalancingv2.ServiceID, "AWS_ELBV2_ENDPOINT") + }) } return s.elasticloadbalancingV2 } @@ -124,7 +132,9 @@ func (s *ServicesV2) IAM() awsapi.IAM { s.mu.Lock() defer s.mu.Unlock() if s.iam == nil { - s.iam = iam.NewFromConfig(s.config) + s.iam = iam.NewFromConfig(s.config, func(o *iam.Options) { + o.BaseEndpoint = getBaseEndpoint(iam.ServiceID, "AWS_IAM_ENDPOINT") + }) } return s.iam } @@ -134,7 +144,9 @@ func (s *ServicesV2) EC2() awsapi.EC2 { s.mu.Lock() defer s.mu.Unlock() if s.ec2 == nil { - s.ec2 = ec2.NewFromConfig(s.config) + s.ec2 = ec2.NewFromConfig(s.config, func(o *ec2.Options) { + o.BaseEndpoint = getBaseEndpoint(ec2.ServiceID, "AWS_EC2_ENDPOINT") + }) } return s.ec2 } @@ -144,7 +156,9 @@ func (s *ServicesV2) EKS() awsapi.EKS { s.mu.Lock() defer s.mu.Unlock() if s.eks == nil { - s.eks = eks.NewFromConfig(s.config) + s.eks = eks.NewFromConfig(s.config, func(o *eks.Options) { + o.BaseEndpoint = getBaseEndpoint(eks.ServiceID, "AWS_EKS_ENDPOINT") + }) } return s.eks } @@ -166,3 +180,12 @@ func (s *ServicesV2) AWSConfig() aws.Config { func (s *ServicesV2) CredentialsProvider() aws.CredentialsProvider { return s.config.Credentials } + +func getBaseEndpoint(serviceID, endpoint string) *string { + if endpoint, ok := os.LookupEnv(endpoint); ok { + logger.Debug( + "Setting %s endpoint to %s", serviceID, endpoint) + return aws.String(endpoint) + } + return nil +} From b71d96e3be2615f2a0dcd47a05b1f5489cf49cbc Mon Sep 17 00:00:00 2001 From: cPu1 Date: Tue, 4 Jun 2024 02:54:15 +0530 Subject: [PATCH 35/35] Update link to docs --- userdocs/src/usage/pod-identity-associations.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md index 6aa3274f74..7a2ef8d597 100644 --- a/userdocs/src/usage/pod-identity-associations.md +++ b/userdocs/src/usage/pod-identity-associations.md @@ -411,9 +411,7 @@ eksctl utils migrate-to-pod-identity --cluster my-cluster --approve --remove-oid ## Further references -[Official AWS Blog Post on EKS Add-ons support for pod identities] //https://TBD - -[Official AWS Userdocs for EKS Add-ons support for pod identities] //https://TBD +[Official AWS Userdocs for EKS Add-ons support for pod identities](https://docs.aws.amazon.com/eks/latest/userguide/add-ons-iam.html) [Official AWS Blog Post on Pod Identity Associations](https://aws.amazon.com/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/)