From 0fc489606292a9912f6adc4dabd200d1d5db73ab Mon Sep 17 00:00:00 2001 From: Chetan Patwal Date: Mon, 27 Nov 2023 13:41:27 +0000 Subject: [PATCH 1/3] Implement updating and deleting pod identity associations (#7315) * Implement `update podidentityassociation` * Implement `delete podidentityassociation` * change create task order * add integration test * delete IAM role stacks on cluster deletion * move validations from cluster config level to command config level * avoid duplicating stack deletion tasks due to passing name by reference * fix integration tests * add eksctl prefix to integrationt test IAM roles * make IAM roles trust the production EKS Service Principal --------- Co-authored-by: Tibi <110664232+TiberiuGC@users.noreply.github.com> --- .../pod_identity_associations_test.go | 455 ++++++++++++++++++ pkg/actions/addon/tasks.go | 2 +- pkg/actions/cluster/owned.go | 9 +- pkg/actions/podidentityassociation/creator.go | 17 +- pkg/actions/podidentityassociation/deleter.go | 186 +++++++ .../podidentityassociation/deleter_test.go | 295 ++++++++++++ pkg/actions/podidentityassociation/tasks.go | 25 +- pkg/actions/podidentityassociation/updater.go | 203 ++++++++ .../podidentityassociation/updater_test.go | 422 ++++++++++++++++ pkg/apis/eksctl.io/v1alpha5/iam.go | 1 + pkg/apis/eksctl.io/v1alpha5/validation.go | 34 -- pkg/cfn/builder/iam.go | 3 +- pkg/cfn/manager/api.go | 44 +- pkg/cfn/manager/api_test.go | 4 +- pkg/cfn/manager/delete_tasks.go | 14 + pkg/cfn/manager/fakes/fake_stack_manager.go | 191 +++++++- pkg/cfn/manager/interface.go | 4 +- pkg/cfn/manager/waiters.go | 11 +- pkg/cfn/template/iam_helpers.go | 7 +- pkg/ctl/cmdutils/configfile.go | 3 + pkg/ctl/cmdutils/pod_identity_association.go | 135 +++++- pkg/ctl/create/cluster.go | 11 + .../create/pod_identity_association_test.go | 4 +- pkg/ctl/delete/delete.go | 1 + pkg/ctl/delete/pod_identity_association.go | 78 +++ pkg/ctl/update/pod_identity_association.go | 81 ++++ pkg/ctl/update/update.go | 1 + pkg/eks/tasks.go | 13 - pkg/utils/names/names.go | 9 - 29 files changed, 2130 insertions(+), 133 deletions(-) create mode 100644 integration/tests/pod_identity_associations/pod_identity_associations_test.go create mode 100644 pkg/actions/podidentityassociation/deleter.go create mode 100644 pkg/actions/podidentityassociation/deleter_test.go create mode 100644 pkg/actions/podidentityassociation/updater.go create mode 100644 pkg/actions/podidentityassociation/updater_test.go create mode 100644 pkg/ctl/delete/pod_identity_association.go create mode 100644 pkg/ctl/update/pod_identity_association.go diff --git a/integration/tests/pod_identity_associations/pod_identity_associations_test.go b/integration/tests/pod_identity_associations/pod_identity_associations_test.go new file mode 100644 index 0000000000..7d230e473f --- /dev/null +++ b/integration/tests/pod_identity_associations/pod_identity_associations_test.go @@ -0,0 +1,455 @@ +//go:build integration +// +build integration + +package podidentityassociations + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "testing" + + "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/iam" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "github.com/weaveworks/eksctl/integration/runner" + + "github.com/weaveworks/eksctl/integration/tests" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/eks" + "github.com/weaveworks/eksctl/pkg/testutils" +) + +const ( + nsInitial = "initial" + nsCLI = "cli" + nsConfigFile = "config-file" + nsUnowned = "unowned" + + sa1 = "service-account-1" + sa2 = "service-account-2" + sa3 = "service-account-3" + + rolePrefix = "eksctl-" + initialRole1 = rolePrefix + "pod-identity-role-1" + initialRole2 = rolePrefix + "pod-identity-role-2" +) + +var ( + params *tests.Params + ctl *eks.ClusterProvider + role1ARN, role2ARN string + err error +) + +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.NewParamsWithGivenClusterName("pod-identity-associations", "test") + ctl, err = eks.New(context.TODO(), &api.ProviderConfig{Region: params.Region}, nil) + if err != nil { + panic(err) + } +} + +func TestPodIdentityAssociations(t *testing.T) { + testutils.RegisterAndRun(t) +} + +var _ = BeforeSuite(func() { + roleOutput, err := ctl.AWSProvider.IAM().CreateRole(context.Background(), &iam.CreateRoleInput{ + RoleName: aws.String(initialRole1), + AssumeRolePolicyDocument: trustPolicy, + }) + Expect(err).NotTo(HaveOccurred()) + role1ARN = *roleOutput.Role.Arn + + roleOutput, err = ctl.AWSProvider.IAM().CreateRole(context.Background(), &iam.CreateRoleInput{ + RoleName: aws.String(initialRole2), + AssumeRolePolicyDocument: trustPolicy, + }) + Expect(err).NotTo(HaveOccurred()) + role2ARN = *roleOutput.Role.Arn +}) + +var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() { + + Context("Cluster with pod identity associations", func() { + var ( + cfg *api.ClusterConfig + ) + + BeforeAll(func() { + cfg = makeClusterConfig() + }) + + It("should create a cluster with pod identity associations", func() { + cfg.Addons = []*api.Addon{{Name: api.PodIdentityAgentAddon}} + cfg.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{ + { + Namespace: nsInitial, + ServiceAccountName: sa1, + RoleARN: role1ARN, + }, + { + Namespace: nsInitial, + ServiceAccountName: sa2, + RoleARN: role1ARN, + }, + { + Namespace: nsInitial, + ServiceAccountName: sa3, + RoleARN: role1ARN, + }, + } + + data, err := json.Marshal(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(params.EksctlCreateCmd. + WithArgs( + "cluster", + "--config-file", "-", + "--verbose", "4", + ). + WithoutArg("--region", params.Region). + WithStdin(bytes.NewReader(data))).To(RunSuccessfully()) + }) + + It("should fetch all expected associations", func() { + var output []podidentityassociation.Summary + session := params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--output", "json", + ).Run() + Expect(session.ExitCode()).To(Equal(0)) + Expect(json.Unmarshal(session.Out.Contents(), &output)).To(Succeed()) + Expect(output).To(HaveLen(3)) + }) + + Context("Create new pod identity associations", func() { + It("should fail to create a new association for the same namespace & service account", func() { + Expect(params.EksctlCreateCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsInitial, + "--service-account-name", sa1, + "--role-arn", role1ARN, + ), + ).NotTo(RunSuccessfully()) + }) + + It("should create a new association via CLI", func() { + Expect(params.EksctlCreateCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsCLI, + "--service-account-name", sa1, + "--well-known-policies", "certManager", + ), + ).To(RunSuccessfully()) + }) + + It("should create (multiple) associations via config file", func() { + cfg.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{ + { + Namespace: nsConfigFile, + ServiceAccountName: sa1, + WellKnownPolicies: api.WellKnownPolicies{ + AutoScaler: true, + ExternalDNS: true, + }, + }, + { + Namespace: nsConfigFile, + ServiceAccountName: sa2, + PermissionPolicy: permissionPolicy, + }, + } + + data, err := json.Marshal(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(params.EksctlCreateCmd. + WithArgs( + "podidentityassociation", + "--config-file", "-", + ). + WithoutArg("--region", params.Region). + WithStdin(bytes.NewReader(data)), + ).To(RunSuccessfully()) + }) + }) + + Context("Fetching pod identity associations", func() { + It("should fetch all associations for a cluster", func() { + var output []podidentityassociation.Summary + session := params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--output", "json", + ).Run() + Expect(session.ExitCode()).To(Equal(0)) + Expect(json.Unmarshal(session.Out.Contents(), &output)).To(Succeed()) + Expect(output).To(HaveLen(6)) + }) + + It("should fetch all associations for a namespace", func() { + var output []podidentityassociation.Summary + session := params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsConfigFile, + "--output", "json", + ).Run() + Expect(session.ExitCode()).To(Equal(0)) + Expect(json.Unmarshal(session.Out.Contents(), &output)).To(Succeed()) + Expect(output).To(HaveLen(2)) + }) + + It("should fetch a single association defined by namespace & service account", func() { + var output []podidentityassociation.Summary + session := params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsConfigFile, + "--service-account-name", sa1, + "--output", "json", + ).Run() + Expect(session.ExitCode()).To(Equal(0)) + Expect(json.Unmarshal(session.Out.Contents(), &output)).To(Succeed()) + Expect(output).To(HaveLen(1)) + }) + }) + + Context("Updating pod identity associations", func() { + It("should fail to update an association with role created by eksctl", func() { + Expect(params.EksctlUpdateCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsCLI, + "--service-account-name", sa1, + "--role-arn", role1ARN, + ), + ).NotTo(RunSuccessfully()) + }) + + It("should update an association via CLI", func() { + Expect(params.EksctlUpdateCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsInitial, + "--service-account-name", sa1, + "--role-arn", role2ARN, + ), + ).To(RunSuccessfully()) + }) + + It("should update (multiple) associations via config file", func() { + cfg.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{ + { + Namespace: nsInitial, + ServiceAccountName: sa2, + RoleARN: role2ARN, + }, + { + Namespace: nsInitial, + ServiceAccountName: sa3, + RoleARN: role2ARN, + }, + } + + data, err := json.Marshal(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(params.EksctlUpdateCmd. + WithArgs( + "podidentityassociation", + "--config-file", "-", + ). + WithoutArg("--region", params.Region). + WithStdin(bytes.NewReader(data)), + ).To(RunSuccessfully()) + }) + + It("should check all associations were updated successfully", func() { + var output []podidentityassociation.Summary + session := params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsInitial, + "--output", "json", + ).Run() + Expect(session.ExitCode()).To(Equal(0)) + Expect(json.Unmarshal(session.Out.Contents(), &output)).To(Succeed()) + Expect(output).To(HaveLen(3)) + Expect(output[0].RoleARN).To(Equal(role2ARN)) + Expect(output[1].RoleARN).To(Equal(role2ARN)) + Expect(output[2].RoleARN).To(Equal(role2ARN)) + }) + }) + + Context("Deleting pod identity associations", func() { + It("should delete an association via CLI", func() { + Expect(params.EksctlDeleteCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsInitial, + "--service-account-name", sa1, + ), + ).To(RunSuccessfully()) + }) + + It("should delete (multiple) associations via config file", func() { + cfg.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{ + { + Namespace: nsInitial, + ServiceAccountName: sa2, + }, + { + Namespace: nsInitial, + ServiceAccountName: sa3, + }, + } + + data, err := json.Marshal(cfg) + Expect(err).NotTo(HaveOccurred()) + + Expect(params.EksctlDeleteCmd. + WithArgs( + "podidentityassociation", + "--config-file", "-", + ). + WithoutArg("--region", params.Region). + WithStdin(bytes.NewReader(data)), + ).To(RunSuccessfully()) + }) + + It("should check that all associations were deleted successfully", func() { + Expect(params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsInitial, + )).To(RunSuccessfullyWithOutputStringLines(ContainElement("No podidentityassociations found"))) + }) + }) + + Context("Unowned pod identity association", func() { + BeforeAll(func() { + _, err := ctl.AWSProvider.EKS().CreatePodIdentityAssociation(context.Background(), + &awseks.CreatePodIdentityAssociationInput{ + ClusterName: ¶ms.ClusterName, + Namespace: aws.String(nsUnowned), + ServiceAccount: aws.String(sa1), + RoleArn: &role1ARN, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should fetch an unowned association", func() { + Expect(params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsUnowned, + "--service-account-name", sa1, + "--output", "json", + )).To(RunSuccessfullyWithOutputStringLines(ContainElements( + ContainSubstring(nsUnowned), + ContainSubstring(sa1), + ))) + }) + + It("should delete an unowned association", func() { + Expect(params.EksctlDeleteCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsUnowned, + "--service-account-name", sa1, + )).To(RunSuccessfully()) + + Expect(params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", params.ClusterName, + "--namespace", nsUnowned, + "--service-account-name", sa1, + )).To(RunSuccessfullyWithOutputStringLines(ContainElement("No podidentityassociations found"))) + }) + }) + }) +}) + +var _ = AfterSuite(func() { + _, err = ctl.AWSProvider.IAM().DeleteRole(context.Background(), &iam.DeleteRoleInput{ + RoleName: aws.String(initialRole1), + }) + Expect(err).NotTo(HaveOccurred()) + + _, err = ctl.AWSProvider.IAM().DeleteRole(context.Background(), &iam.DeleteRoleInput{ + RoleName: aws.String(initialRole2), + }) + Expect(err).NotTo(HaveOccurred()) + + params.DeleteClusters() +}) + +var ( + makeClusterConfig = func() *api.ClusterConfig { + cfg := api.NewClusterConfig() + cfg.Metadata.Name = params.ClusterName + cfg.Metadata.Version = params.Version + cfg.Metadata.Region = params.Region + return cfg + } + + trustPolicy = aws.String(fmt.Sprintf(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "%s" + ] + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ] + } + ] + }`, api.EKSServicePrincipal)) + + permissionPolicy = api.InlineDocument{ + "Version": "2012-10-17", + "Statement": []map[string]interface{}{ + { + "Effect": "Allow", + "Action": []string{ + "autoscaling:DescribeAutoScalingGroups", + "autoscaling:DescribeAutoScalingInstances", + }, + "Resource": "*", + }, + }, + } +) diff --git a/pkg/actions/addon/tasks.go b/pkg/actions/addon/tasks.go index f0f618b48f..0c5142eb18 100644 --- a/pkg/actions/addon/tasks.go +++ b/pkg/actions/addon/tasks.go @@ -18,7 +18,7 @@ func CreateAddonTasks(ctx context.Context, cfg *api.ClusterConfig, clusterProvid var preAddons []*api.Addon var postAddons []*api.Addon for _, addon := range cfg.Addons { - if strings.ToLower(addon.Name) == "vpc-cni" { + if strings.EqualFold(addon.Name, api.VPCCNIAddon) { preAddons = append(preAddons, addon) } else { postAddons = append(postAddons, addon) diff --git a/pkg/actions/cluster/owned.go b/pkg/actions/cluster/owned.go index 6e996c8640..8c0a4afa98 100644 --- a/pkg/actions/cluster/owned.go +++ b/pkg/actions/cluster/owned.go @@ -11,12 +11,14 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/nodegroup" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" "github.com/weaveworks/eksctl/pkg/eks" iamoidc "github.com/weaveworks/eksctl/pkg/iam/oidc" "github.com/weaveworks/eksctl/pkg/kubernetes" + "github.com/weaveworks/eksctl/pkg/utils/tasks" "github.com/weaveworks/eksctl/pkg/vpc" ) @@ -121,7 +123,12 @@ func (c *OwnedCluster) Delete(ctx context.Context, _, podEvictionWaitPeriod time return c.ctl.NewOpenIDConnectManager(ctx, c.cfg) } newTasksToDeleteAddonIAM := addon.NewRemover(c.stackManager).DeleteAddonIAMTasks - tasks, err := c.stackManager.NewTasksToDeleteClusterWithNodeGroups(ctx, c.clusterStack, allStacks, clusterOperable, newOIDCManager, newTasksToDeleteAddonIAM, c.ctl.Status.ClusterInfo.Cluster, kubernetes.NewCachedClientSet(clientSet), wait, force, func(errs chan error, _ string) error { + newTasksToDeletePodIdentityRoles := func() (*tasks.TaskTree, error) { + return podidentityassociation.NewDeleter(c.cfg.Metadata.Name, c.stackManager, c.ctl.AWSProvider.EKS()). + DeleteTasks(ctx, []podidentityassociation.Identifier{}) + } + + tasks, err := c.stackManager.NewTasksToDeleteClusterWithNodeGroups(ctx, c.clusterStack, allStacks, clusterOperable, newOIDCManager, newTasksToDeleteAddonIAM, newTasksToDeletePodIdentityRoles, c.ctl.Status.ClusterInfo.Cluster, kubernetes.NewCachedClientSet(clientSet), wait, force, func(errs chan error, _ string) error { logger.Info("trying to cleanup dangling network interfaces") stack, err := c.stackManager.DescribeClusterStack(ctx) if err != nil { diff --git a/pkg/actions/podidentityassociation/creator.go b/pkg/actions/podidentityassociation/creator.go index 47ef4c2ea8..210cf3cf31 100644 --- a/pkg/actions/podidentityassociation/creator.go +++ b/pkg/actions/podidentityassociation/creator.go @@ -3,9 +3,6 @@ package podidentityassociation import ( "context" "fmt" - "strings" - - "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/awsapi" @@ -28,19 +25,7 @@ func NewCreator(clusterName string, stackManager StackManager, eksAPI awsapi.EKS } func (c *Creator) CreatePodIdentityAssociations(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) error { - taskTree := c.CreateTasks(ctx, podIdentityAssociations) - logger.Info(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")) - } - - logger.Info("successfully created all pod identity associations") - return nil + return runAllTasks(c.CreateTasks(ctx, podIdentityAssociations)) } func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) *tasks.TaskTree { diff --git a/pkg/actions/podidentityassociation/deleter.go b/pkg/actions/podidentityassociation/deleter.go new file mode 100644 index 0000000000..960455e8f5 --- /dev/null +++ b/pkg/actions/podidentityassociation/deleter.go @@ -0,0 +1,186 @@ +package podidentityassociation + +import ( + "context" + "fmt" + "strings" + + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + + "golang.org/x/exp/slices" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + + "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" +) + +// A StackLister lists and describes CloudFormation stacks. +type StackLister interface { + ListStackNames(ctx context.Context, regExp string) ([]string, error) + DescribeStack(ctx context.Context, stack *manager.Stack) (*manager.Stack, error) +} + +// A StackDeleter lists and deletes CloudFormation stacks. +type StackDeleter interface { + StackLister + DeleteStackBySpecSync(ctx context.Context, stack *cfntypes.Stack, errCh chan error) error +} + +// APILister lists pod identity associations using the EKS API. +type APILister interface { + ListPodIdentityAssociations(ctx context.Context, params *eks.ListPodIdentityAssociationsInput, optFns ...func(*eks.Options)) (*eks.ListPodIdentityAssociationsOutput, error) +} + +// APIDeleter lists and deletes pod identity associations using the EKS API. +type APIDeleter interface { + APILister + DeletePodIdentityAssociation(ctx context.Context, params *eks.DeletePodIdentityAssociationInput, optFns ...func(*eks.Options)) (*eks.DeletePodIdentityAssociationOutput, error) +} + +// A Deleter deletes pod identity associations. +type Deleter struct { + // ClusterName is the cluster name. + ClusterName string + // StackDeleter is used to delete stacks. + StackDeleter StackDeleter + // APIDeleter deletes pod identity associations using the EKS API. + APIDeleter APIDeleter +} + +// Identifier represents a pod identity association. +type Identifier struct { + // Namespace is the namespace the service account belongs to. + Namespace string + // ServiceAccountName is the name of the Kubernetes ServiceAccount. + ServiceAccountName string +} + +func NewDeleter(clusterName string, stackDeleter StackDeleter, apiDeleter APIDeleter) *Deleter { + return &Deleter{ + ClusterName: clusterName, + StackDeleter: stackDeleter, + APIDeleter: apiDeleter, + } +} + +// Delete deletes the specified podIdentityAssociations. +func (d *Deleter) Delete(ctx context.Context, podIDs []Identifier) error { + tasks, err := d.DeleteTasks(ctx, podIDs) + if err != nil { + return err + } + return runAllTasks(tasks) +} + +func (d *Deleter) DeleteTasks(ctx context.Context, podIDs []Identifier) (*tasks.TaskTree, error) { + roleStackNames, err := d.StackDeleter.ListStackNames(ctx, fmt.Sprintf("^%s*", makeStackNamePrefix(d.ClusterName))) + if err != nil { + return nil, fmt.Errorf("error listing stack names for pod identity associations: %w", err) + } + taskTree := &tasks.TaskTree{Parallel: true} + + // this is true during cluster deletion, when no association identifier is given as user input, + // instead we will delete all pod-identity-role stacks for the cluster + if len(podIDs) == 0 { + for _, stackName := range roleStackNames { + name := strings.Clone(stackName) + taskTree.Append(&tasks.GenericTask{ + Description: fmt.Sprintf("deleting IAM resources stack %q", stackName), + Doer: func() error { + return d.deleteRoleStack(ctx, name) + }, + }) + } + return taskTree, nil + } + + for _, p := range podIDs { + taskTree.Append(d.makeDeleteTask(ctx, p, roleStackNames)) + } + return taskTree, nil +} + +func (d *Deleter) makeDeleteTask(ctx context.Context, p Identifier, roleStackNames []string) tasks.Task { + podIdentityAssociationID := makeID(p.Namespace, p.ServiceAccountName) + 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) + } + switch len(output.Associations) { + case 0: + logger.Warning("pod identity association %q not found", podIdentityAssociationID) + default: + return fmt.Errorf("expected to find only 1 pod identity association for %q; got %d", podIdentityAssociationID, len(output.Associations)) + 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) + } + } + + stackName := MakeStackName(d.ClusterName, p.Namespace, p.ServiceAccountName) + if !slices.Contains(roleStackNames, stackName) { + return nil + } + logger.Info("deleting IAM resources stack %q for pod identity association %q", stackName, podIdentityAssociationID) + return d.deleteRoleStack(ctx, stackName) +} + +func (d *Deleter) deleteRoleStack(ctx context.Context, stackName string) error { + stack, err := d.StackDeleter.DescribeStack(ctx, &manager.Stack{ + StackName: aws.String(stackName), + }) + if err != nil { + return fmt.Errorf("describing stack %q: %w", stackName, err) + } + + deleteStackCh := make(chan error) + if err := d.StackDeleter.DeleteStackBySpecSync(ctx, stack, deleteStackCh); err != nil { + return fmt.Errorf("deleting stack %q for IAM role: %w", stackName, err) + } + select { + case err := <-deleteStackCh: + return err + case <-ctx.Done(): + return fmt.Errorf("timed out waiting for deletion of pod identity association: %w", ctx.Err()) + } +} + +// ToIdentifiers maps a list of PodIdentityAssociations to a list of Identifiers. +func ToIdentifiers(podIdentityAssociations []api.PodIdentityAssociation) []Identifier { + identifiers := make([]Identifier, len(podIdentityAssociations)) + for i, p := range podIdentityAssociations { + identifiers[i] = Identifier{ + Namespace: p.Namespace, + ServiceAccountName: p.ServiceAccountName, + } + } + return identifiers +} + +func makeID(namespace, serviceAccountName string) string { + return fmt.Sprintf("%s/%s", namespace, serviceAccountName) +} diff --git a/pkg/actions/podidentityassociation/deleter_test.go b/pkg/actions/podidentityassociation/deleter_test.go new file mode 100644 index 0000000000..03d8a7f017 --- /dev/null +++ b/pkg/actions/podidentityassociation/deleter_test.go @@ -0,0 +1,295 @@ +package podidentityassociation_test + +import ( + "context" + "crypto/sha1" + "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/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" + 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("Pod Identity Deleter", func() { + type deleteEntry struct { + podIdentityAssociations []api.PodIdentityAssociation + mockCalls func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) + + expectedCalls func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) + expectedErr string + } + + mockStackManager := func(stackManager *managerfakes.FakeStackManager, stackName string) { + stackManager.DescribeStackReturns(&cfntypes.Stack{ + StackName: aws.String(stackName), + }, nil) + stackManager.DeleteStackBySpecSyncStub = func(_ context.Context, _ *cfntypes.Stack, errCh chan error) error { + close(errCh) + return nil + } + } + mockCalls := func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS, podID podidentityassociation.Identifier) { + stackName := makeStackName(podID) + associationID := fmt.Sprintf("%x", sha1.Sum([]byte(stackName))) + mockListPodIdentityAssociations(eksAPI, podID, []ekstypes.PodIdentityAssociationSummary{ + { + AssociationId: aws.String(associationID), + }, + }, nil) + eksAPI.On("DeletePodIdentityAssociation", mock.Anything, &eks.DeletePodIdentityAssociationInput{ + ClusterName: aws.String(clusterName), + AssociationId: aws.String(associationID), + }).Return(&eks.DeletePodIdentityAssociationOutput{}, nil) + mockStackManager(stackManager, stackName) + } + + DescribeTable("delete pod identity association", func(e deleteEntry) { + provider := mockprovider.NewMockProvider() + var stackManager managerfakes.FakeStackManager + e.mockCalls(&stackManager, provider.MockEKS()) + deleter := podidentityassociation.Deleter{ + ClusterName: clusterName, + StackDeleter: &stackManager, + APIDeleter: provider.EKS(), + } + err := deleter.Delete(context.Background(), podidentityassociation.ToIdentifiers(e.podIdentityAssociations)) + + if e.expectedErr != "" { + Expect(err).To(MatchError(e.expectedErr)) + } else { + Expect(err).NotTo(HaveOccurred()) + } + e.expectedCalls(&stackManager, provider.MockEKS()) + }, + Entry("one pod identity association exists", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podID := podidentityassociation.Identifier{ + Namespace: "default", + ServiceAccountName: "default", + } + mockListStackNames(stackManager, []podidentityassociation.Identifier{podID}) + mockCalls(stackManager, eksAPI, podID) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(1)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("multiple pod identity associations exist", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIDs := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + } + mockListStackNames(stackManager, podIDs) + for _, podID := range podIDs { + mockCalls(stackManager, eksAPI, podID) + } + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(2)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("some pod identity associations do not exist", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + { + Namespace: "kube-system", + ServiceAccountName: "kube-proxy", + }, + { + Namespace: "kube-system", + ServiceAccountName: "coredns", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIDs := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + { + Namespace: "kube-system", + ServiceAccountName: "coredns", + }, + } + mockListStackNames(stackManager, podIDs) + for _, podID := range podIDs { + mockCalls(stackManager, eksAPI, podID) + } + mockListPodIdentityAssociations(eksAPI, podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "kube-proxy", + }, nil, nil) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(3)) + Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(3)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("pod identity association resource does not exist but IAM resources exist", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podID := podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "aws-node", + } + mockListStackNames(stackManager, []podidentityassociation.Identifier{podID}) + mockListPodIdentityAssociations(eksAPI, podID, nil, nil) + mockStackManager(stackManager, makeStackName(podID)) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(1)) + Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(1)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("no pod identity associations exist", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIDs := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + } + mockListStackNames(stackManager, nil) + for _, podID := range podIDs { + mockListPodIdentityAssociations(eksAPI, podID, nil, nil) + } + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) + Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(0)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("delete IAM resources on cluster deletion", deleteEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{}, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIDs := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + { + Namespace: "kube-system", + ServiceAccountName: "default", + }, + } + mockListStackNames(stackManager, podIDs) + mockStackManager(stackManager, "") + }, + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(3)) + Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(3)) + + var names []string + for i := 0; i < stackManager.DescribeStackCallCount(); i++ { + _, stack := stackManager.DescribeStackArgsForCall(i) + names = append(names, *stack.StackName) + } + Expect(names).To(ConsistOf( + makeStackName(podidentityassociation.Identifier{ + Namespace: "default", + ServiceAccountName: "default", + }), + makeStackName(podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "default", + }), + makeStackName(podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }), + )) + }, + }), + ) + +}) diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index aa872f0d87..e694225940 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -3,12 +3,15 @@ package podidentityassociation import ( "context" "fmt" + "strings" awseks "github.com/aws/aws-sdk-go-v2/service/eks" + "github.com/kris-nova/logger" 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/utils/tasks" ) //go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate @@ -68,14 +71,32 @@ func (t *createPodIdentityAssociationTask) Do(errorCh chan error) error { ServiceAccount: &t.podIdentityAssociation.ServiceAccountName, Tags: t.podIdentityAssociation.Tags, }); err != nil { - return fmt.Errorf("creating pod identity association for service account %s in namespace %s: %w", + return fmt.Errorf( + "creating pod identity association for service account %s in namespace %s: %w", t.podIdentityAssociation.ServiceAccountName, t.podIdentityAssociation.Namespace, err) } return nil } +func makeStackNamePrefix(clusterName string) string { + return fmt.Sprintf("eksctl-%s-podidentityrole-ns-", clusterName) +} + // MakeStackName creates a stack name for the specified access entry. func MakeStackName(clusterName, namespace, serviceAccountName string) string { - return fmt.Sprintf("eksctl-%s-podidentityrole-ns-%s-sa-%s", clusterName, namespace, serviceAccountName) + return fmt.Sprintf("%s%s-sa-%s", makeStackNamePrefix(clusterName), namespace, serviceAccountName) +} + +func runAllTasks(taskTree *tasks.TaskTree) error { + logger.Info(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")) + } + logger.Info("successfully finished all tasks") + return nil } diff --git a/pkg/actions/podidentityassociation/updater.go b/pkg/actions/podidentityassociation/updater.go new file mode 100644 index 0000000000..7d373d7aae --- /dev/null +++ b/pkg/actions/podidentityassociation/updater.go @@ -0,0 +1,203 @@ +package podidentityassociation + +import ( + "context" + "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" + + 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" +) + +// An Updater updates pod identity associations. +type Updater struct { + // ClusterName is the cluster name. + ClusterName string + // StackUpdater updates stacks. + StackUpdater StackUpdater + // APIDeleter updates pod identity associations using the EKS API. + APIUpdater APIUpdater +} + +// A StackUpdater updates CloudFormation stacks. +type StackUpdater interface { + StackLister + // MustUpdateStack updates the CloudFormation stack. + MustUpdateStack(ctx context.Context, options manager.UpdateStackOptions) error +} + +// APIUpdater updates pod identity associations using the EKS API. +type APIUpdater interface { + APILister + DescribePodIdentityAssociation(ctx context.Context, params *eks.DescribePodIdentityAssociationInput, optFns ...func(*eks.Options)) (*eks.DescribePodIdentityAssociationOutput, error) + 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 +} + +// Update updates the specified pod identity associations. +func (u *Updater) Update(ctx context.Context, podIdentityAssociations []api.PodIdentityAssociation) error { + roleStackNames, err := u.StackUpdater.ListStackNames(ctx, makeStackNamePrefix(u.ClusterName)) + if err != nil { + return fmt.Errorf("error listing stack names for pod identity associations: %w", err) + } + taskTree := &tasks.TaskTree{ + Parallel: true, + } + for _, p := range podIdentityAssociations { + podIdentityAssociationID := makeID(p.Namespace, p.ServiceAccountName) + updateErr := func(err error) error { + return fmt.Errorf("error updating pod identity association %q: %w", podIdentityAssociationID, err) + } + updateConfig, err := u.makeUpdate(ctx, p, roleStackNames) + if err != nil { + return updateErr(err) + } + taskTree.Append(&tasks.GenericTask{ + Description: fmt.Sprintf("update pod identity association %s", podIdentityAssociationID), + Doer: func() error { + if err := u.update(ctx, updateConfig, podIdentityAssociationID); err != nil { + return updateErr(err) + } + return nil + }, + }) + } + logger.Info(taskTree.Describe()) + 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) + 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 u.updatePodIdentityAssociation(ctx, updateConfig, podIdentityAssociationID) +} + +func (u *Updater) updatePodIdentityAssociation(ctx context.Context, updateConfig *updateConfig, podIdentityAssociationID string) error { + roleARN := updateConfig.podIdentityAssociation.RoleARN + if _, err := u.APIUpdater.UpdatePodIdentityAssociation(ctx, &eks.UpdatePodIdentityAssociationInput{ + 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) + } + 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) { + 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), + }) + if err != nil { + if apierrors.IsNotFoundError(err) { + return nil, fmt.Errorf("%s: %w", notFoundErrMsg, err) + } + return nil, fmt.Errorf("error listing pod identity associations: %w", err) + } + switch len(output.Associations) { + case 0: + return nil, errors.New(notFoundErrMsg) + default: + return nil, fmt.Errorf("expected to find only 1 pod identity association; got %d", len(output.Associations)) + case 1: + describeOutput, err := u.APIUpdater.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ + ClusterName: aws.String(u.ClusterName), + AssociationId: output.Associations[0].AssociationId, + }) + if err != nil { + return nil, fmt.Errorf("error describing pod identity association: %w", err) + } + stackName := MakeStackName(u.ClusterName, p.Namespace, p.ServiceAccountName) + hasIAMResourcesStack := slices.Contains(roleStackNames, stackName) + if hasIAMResourcesStack { + 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 &updateConfig{ + podIdentityAssociation: p, + associationID: *describeOutput.Association.AssociationId, + hasIAMResourcesStack: hasIAMResourcesStack, + stackName: stackName, + }, nil + } +} diff --git a/pkg/actions/podidentityassociation/updater_test.go b/pkg/actions/podidentityassociation/updater_test.go new file mode 100644 index 0000000000..cb73a0e160 --- /dev/null +++ b/pkg/actions/podidentityassociation/updater_test.go @@ -0,0 +1,422 @@ +package podidentityassociation_test + +import ( + "context" + "crypto/sha1" + "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/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" + 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/cfn/outputs" + "github.com/weaveworks/eksctl/pkg/eks/mocksv2" + "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" +) + +var _ = Describe("Pod Identity Update", func() { + type updateEntry struct { + podIdentityAssociations []api.PodIdentityAssociation + mockCalls func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) + + expectedCalls func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) + expectedErr string + } + + mockStackManager := func(stackManager *managerfakes.FakeStackManager, stackName string, outputs []cfntypes.Output, capabilities []cfntypes.Capability) { + stackManager.DescribeStackReturns(&cfntypes.Stack{ + StackName: aws.String(stackName), + Outputs: outputs, + Capabilities: capabilities, + }, nil) + } + + type mockOptions struct { + podIdentifier podidentityassociation.Identifier + updateRoleARN string + describeStackOutputs []cfntypes.Output + describeStackCapabilities []cfntypes.Capability + } + + mockCalls := func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS, o mockOptions) { + stackName := makeStackName(o.podIdentifier) + associationID := fmt.Sprintf("%x", sha1.Sum([]byte(stackName))) + mockListPodIdentityAssociations(eksAPI, o.podIdentifier, []ekstypes.PodIdentityAssociationSummary{ + { + AssociationId: aws.String(associationID), + }, + }, nil) + eksAPI.On("DescribePodIdentityAssociation", mock.Anything, &eks.DescribePodIdentityAssociationInput{ + AssociationId: aws.String(associationID), + ClusterName: aws.String(clusterName), + }).Return(&eks.DescribePodIdentityAssociationOutput{ + Association: &ekstypes.PodIdentityAssociation{ + AssociationId: aws.String(associationID), + RoleArn: aws.String("arn:aws:iam::1234567:role/Role"), + }, + }, nil) + if o.updateRoleARN != "" { + eksAPI.On("UpdatePodIdentityAssociation", mock.Anything, &eks.UpdatePodIdentityAssociationInput{ + AssociationId: aws.String(associationID), + ClusterName: aws.String(clusterName), + RoleArn: aws.String(o.updateRoleARN), + }).Return(&eks.UpdatePodIdentityAssociationOutput{}, nil) + } + mockStackManager(stackManager, stackName, o.describeStackOutputs, o.describeStackCapabilities) + } + + DescribeTable("update pod identity associations", func(e updateEntry) { + provider := mockprovider.NewMockProvider() + var stackManager managerfakes.FakeStackManager + + e.mockCalls(&stackManager, provider.MockEKS()) + updater := podidentityassociation.Updater{ + ClusterName: clusterName, + StackUpdater: &stackManager, + APIUpdater: provider.EKS(), + } + err := updater.Update(context.Background(), e.podIdentityAssociations) + if e.expectedErr != "" { + Expect(err).To(MatchError(e.expectedErr)) + } else { + Expect(err).NotTo(HaveOccurred()) + } + e.expectedCalls(&stackManager, provider.MockEKS()) + }, + Entry("pod identity association does not exist", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podID := podidentityassociation.Identifier{ + Namespace: "default", + ServiceAccountName: "default", + } + mockListStackNames(stackManager, nil) + mockListPodIdentityAssociations(eksAPI, podID, nil, &ekstypes.NotFoundException{ + Message: aws.String("not found"), + }) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) + eksAPI.AssertExpectations(GinkgoT()) + }, + expectedErr: `error updating pod identity association "default/default": pod identity association does not exist: NotFoundException: not found`, + }), + + Entry("role ARN specified when the IAM resources were created by eksctl", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + RoleARN: "arn:aws:iam::00000000:role/new-role", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + id := podidentityassociation.Identifier{ + Namespace: "default", + ServiceAccountName: "default", + } + mockListStackNames(stackManager, []podidentityassociation.Identifier{id}) + mockCalls(stackManager, eksAPI, mockOptions{ + podIdentifier: id, + }) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) + eksAPI.AssertExpectations(GinkgoT()) + }, + + expectedErr: `error updating pod identity association "default/default": cannot change podIdentityAssociation.roleARN since the role was created by eksctl`, + }), + + Entry("role ARN specified when the IAM resources were not created by eksctl", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + RoleARN: "arn:aws:iam::00000000:role/new-role", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + mockListStackNames(stackManager, nil) + mockCalls(stackManager, eksAPI, mockOptions{ + podIdentifier: podidentityassociation.Identifier{ + Namespace: "default", + ServiceAccountName: "default", + }, + updateRoleARN: "arn:aws:iam::00000000:role/new-role", + }) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("pod identity association has changes", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIdentifiers := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + } + mockListStackNames(stackManager, podIdentifiers) + describeStackOutputs := []cfntypes.Output{ + { + OutputKey: aws.String(outputs.IAMServiceAccountRoleName), + OutputValue: aws.String("arn:aws:iam::1234567:role/Role"), + }, + } + for _, options := range []mockOptions{ + { + podIdentifier: podIdentifiers[0], + updateRoleARN: "arn:aws:iam::1234567:role/Role", + describeStackOutputs: describeStackOutputs, + }, + { + podIdentifier: podIdentifiers[1], + updateRoleARN: "arn:aws:iam::1234567:role/Role", + describeStackOutputs: describeStackOutputs, + }, + } { + mockCalls(stackManager, eksAPI, options) + } + + stackManager.MustUpdateStackReturns(nil) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(4)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(2)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("pod identity association has no changes", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIdentifiers := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + } + mockListStackNames(stackManager, podIdentifiers) + for _, options := range []mockOptions{ + { + podIdentifier: podIdentifiers[0], + }, + { + podIdentifier: podIdentifiers[1], + }, + } { + mockCalls(stackManager, eksAPI, options) + } + + stackManager.MustUpdateStackReturns(&manager.NoChangeError{ + Msg: "no changes found", + }) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(2)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(2)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + + Entry("fields that cannot be updated specified when the IAM resources were not created by eksctl", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + RoleARN: "arn:aws:iam::00000000:role/new-role", + WellKnownPolicies: api.WellKnownPolicies{ + AutoScaler: true, + }, + PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"}, + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + mockListStackNames(stackManager, nil) + mockCalls(stackManager, eksAPI, mockOptions{ + podIdentifier: podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + }) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).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/aws-node": only namespace, serviceAccountName and roleARN can be specified if the role was not created by eksctl`, + }), + + Entry("roleName specified when the pod identity association was not created with a roleName", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + RoleName: "default-role", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIdentifier := podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "aws-node", + } + mockListStackNames(stackManager, []podidentityassociation.Identifier{podIdentifier}) + + mockCalls(stackManager, eksAPI, mockOptions{ + podIdentifier: podIdentifier, + describeStackCapabilities: []cfntypes.Capability{cfntypes.CapabilityCapabilityIam}, + }) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(1)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) + eksAPI.AssertExpectations(GinkgoT()) + }, + + expectedErr: `error updating pod identity association "kube-system/aws-node": cannot update role name if the pod identity association was not created with a role name`, + }), + + Entry("roleName specified when the pod identity association was created with a roleName", updateEntry{ + podIdentityAssociations: []api.PodIdentityAssociation{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + RoleName: "default-role", + }, + }, + mockCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + podIdentifiers := []podidentityassociation.Identifier{ + { + Namespace: "default", + ServiceAccountName: "default", + }, + { + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }, + } + mockListStackNames(stackManager, podIdentifiers) + describeStackOutputs := []cfntypes.Output{ + { + OutputKey: aws.String(outputs.IAMServiceAccountRoleName), + OutputValue: aws.String("arn:aws:iam::1234567:role/Role"), + }, + } + for _, options := range []mockOptions{ + { + podIdentifier: podIdentifiers[0], + describeStackOutputs: describeStackOutputs, + updateRoleARN: "arn:aws:iam::1234567:role/Role", + }, + { + podIdentifier: podIdentifiers[1], + updateRoleARN: "arn:aws:iam::1234567:role/Role", + describeStackOutputs: describeStackOutputs, + describeStackCapabilities: []cfntypes.Capability{cfntypes.CapabilityCapabilityIam, cfntypes.CapabilityCapabilityNamedIam}, + }, + } { + mockCalls(stackManager, eksAPI, options) + } + stackManager.MustUpdateStackReturns(nil) + }, + + expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { + Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.DescribeStackCallCount()).To(Equal(4)) + Expect(stackManager.MustUpdateStackCallCount()).To(Equal(2)) + eksAPI.AssertExpectations(GinkgoT()) + }, + }), + ) +}) + +func mockListPodIdentityAssociations(eksAPI *mocksv2.EKS, podID podidentityassociation.Identifier, output []ekstypes.PodIdentityAssociationSummary, err error) { + eksAPI.On("ListPodIdentityAssociations", mock.Anything, &eks.ListPodIdentityAssociationsInput{ + ClusterName: aws.String(clusterName), + Namespace: aws.String(podID.Namespace), + ServiceAccount: aws.String(podID.ServiceAccountName), + }).Return(&eks.ListPodIdentityAssociationsOutput{ + Associations: output, + }, err) +} + +func makeStackName(podID podidentityassociation.Identifier) string { + return fmt.Sprintf("eksctl-%s-podidentityrole-ns-%s-sa-%s", clusterName, podID.Namespace, podID.ServiceAccountName) +} + +func mockListStackNames(stackManager *managerfakes.FakeStackManager, podIdentifiers []podidentityassociation.Identifier) { + var stackNames []string + for _, id := range podIdentifiers { + stackNames = append(stackNames, makeStackName(id)) + } + stackManager.ListStackNamesReturns(stackNames, nil) +} diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index 12a7aefc92..af81ab3382 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -10,6 +10,7 @@ import ( // Commonly-used constants const ( AnnotationEKSRoleARN = "eks.amazonaws.com/role-arn" + EKSServicePrincipal = "pods.eks.amazonaws.com" ) // ClusterIAM holds all IAM attributes of a cluster diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index d0f071d2b2..8c2f193290 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -207,10 +207,6 @@ func ValidateClusterConfig(cfg *ClusterConfig) error { return fmt.Errorf("failed to validate Karpenter config: %w", err) } - if err := validatePodIdentityAssociations(cfg); err != nil { - return fmt.Errorf("failed to validate pod identity associations: %w", err) - } - return nil } @@ -286,36 +282,6 @@ func validateCloudWatchLogging(clusterConfig *ClusterConfig) error { return nil } -func validatePodIdentityAssociations(cfg *ClusterConfig) error { - for i, pia := range cfg.IAM.PodIdentityAssociations { - path := fmt.Sprintf("podIdentityAssociations[%d]", i) - if pia.Namespace == "" { - return fmt.Errorf("%s.namespace must be set", path) - } - if pia.ServiceAccountName == "" { - return fmt.Errorf("%s.serviceAccountName must be set", path) - } - if pia.RoleARN == "" && - len(pia.PermissionPolicy) == 0 && - len(pia.PermissionPolicyARNs) == 0 && - !pia.WellKnownPolicies.HasPolicy() { - 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 != "" { - if len(pia.PermissionPolicy) > 0 { - return fmt.Errorf("%[1]s.permissionPolicy cannot be specified when %[1]s.roleARN is set", path) - } - if len(pia.PermissionPolicyARNs) > 0 { - return fmt.Errorf("%[1]s.permissionPolicyARNs cannot be specified when %[1]s.roleARN is set", path) - } - if pia.WellKnownPolicies.HasPolicy() { - return fmt.Errorf("%[1]s.wellKnownPolicies cannot be specified when %[1]s.roleARN is set", path) - } - } - } - return nil -} - // ValidateVPCConfig validates the vpc setting if it is defined. func (c *ClusterConfig) ValidateVPCConfig() error { if c.VPC == nil { diff --git a/pkg/cfn/builder/iam.go b/pkg/cfn/builder/iam.go index c701dcecd8..3b87555ddb 100644 --- a/pkg/cfn/builder/iam.go +++ b/pkg/cfn/builder/iam.go @@ -8,7 +8,6 @@ import ( "github.com/kris-nova/logger" "github.com/weaveworks/eksctl/pkg/iam" - "github.com/weaveworks/eksctl/pkg/utils/names" gfniam "github.com/weaveworks/goformation/v4/cloudformation/iam" gfnt "github.com/weaveworks/goformation/v4/cloudformation/types" @@ -232,7 +231,7 @@ func NewIAMRoleResourceSetForPodIdentity(spec *api.PodIdentityAssociation) *IAMR serviceAccount: spec.ServiceAccountName, namespace: spec.Namespace, wellKnownPolicies: spec.WellKnownPolicies, - roleName: names.ForIAMRole(spec.RoleName), + roleName: spec.RoleName, permissionsBoundary: spec.PermissionsBoundaryARN, description: fmt.Sprintf( "IAM role for pod identity association %s", diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index 3533e9d2d9..1288284aea 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -326,6 +326,10 @@ func (c *StackCollection) checkASGTagsNumber(ngName, asgName string, propagatedT // UpdateStack will update a CloudFormation stack by creating and executing a ChangeSet func (c *StackCollection) UpdateStack(ctx context.Context, options UpdateStackOptions) error { + return c.updateStack(ctx, options, true) +} + +func (c *StackCollection) updateStack(ctx context.Context, options UpdateStackOptions, ignoreNoChangeError bool) error { logger.Info(options.Description) if options.Stack == nil { i := &Stack{StackName: &options.StackName} @@ -350,8 +354,11 @@ func (c *StackCollection) UpdateStack(ctx context.Context, options UpdateStackOp return err } if err := c.doWaitUntilChangeSetIsCreated(ctx, options.Stack, options.ChangeSetName); err != nil { - if _, ok := err.(*noChangeError); ok { - return nil + if _, ok := err.(*NoChangeError); ok { + if ignoreNoChangeError { + return nil + } + return err } return err } @@ -370,6 +377,11 @@ func (c *StackCollection) UpdateStack(ctx context.Context, options UpdateStackOp return nil } +// MustUpdateStack is like UpdateStack but returns a NoChangeError if there are no changes to execute. +func (c *StackCollection) MustUpdateStack(ctx context.Context, options UpdateStackOptions) error { + return c.updateStack(ctx, options, false) +} + // DescribeStack describes a cloudformation stack. func (c *StackCollection) DescribeStack(ctx context.Context, i *Stack) (*Stack, error) { input := &cloudformation.DescribeStacksInput{ @@ -468,19 +480,16 @@ func (c *StackCollection) ListStacksMatching(ctx context.Context, nameRegex stri return stacks, nil } -// ListClusterStackNames gets all stack names matching regex -func (c *StackCollection) ListClusterStackNames(ctx context.Context) ([]string, error) { - var stacks []string - re, err := regexp.Compile(clusterStackRegex) +// ListStackNames lists all stack names matching regExp. +func (c *StackCollection) ListStackNames(ctx context.Context, regExp string) ([]string, error) { + re, err := regexp.Compile(regExp) if err != nil { - return nil, errors.Wrap(err, "cannot list stacks") + return nil, fmt.Errorf("unexpected error compiling RegExp: %w", err) } - input := &cloudformation.ListStacksInput{ + paginator := cloudformation.NewListStacksPaginator(c.cloudformationAPI, &cloudformation.ListStacksInput{ StackStatusFilter: defaultStackStatusFilter(), - } - - paginator := cloudformation.NewListStacksPaginator(c.cloudformationAPI, input) - + }) + var stackNames []string for paginator.HasMorePages() { out, err := paginator.NextPage(ctx) if err != nil { @@ -489,12 +498,17 @@ func (c *StackCollection) ListClusterStackNames(ctx context.Context) ([]string, for _, s := range out.StackSummaries { if re.MatchString(*s.StackName) { - stacks = append(stacks, *s.StackName) + stackNames = append(stackNames, *s.StackName) } } } - return stacks, nil + return stackNames, nil +} + +// ListClusterStackNames gets all stack names matching regex +func (c *StackCollection) ListClusterStackNames(ctx context.Context) ([]string, error) { + return c.ListStackNames(ctx, clusterStackRegex) } // ListStacksWithStatuses gets all of CloudFormation stacks @@ -583,7 +597,7 @@ func matchesCluster(clusterName string, tags []types.Tag) bool { // DeleteStackBySpecSync sends a request to delete the stack, and waits until status is DELETE_COMPLETE; // any errors will be written to errs channel, assume completion when nil is written, do not expect -// more then one error value on the channel, it's closed immediately after it is written to +// more than one error value on the channel, it's closed immediately after it is written to func (c *StackCollection) DeleteStackBySpecSync(ctx context.Context, s *Stack, errs chan error) error { i, err := c.DeleteStackBySpec(ctx, s) if err != nil { diff --git a/pkg/cfn/manager/api_test.go b/pkg/cfn/manager/api_test.go index 98dda1783b..cf650271a3 100644 --- a/pkg/cfn/manager/api_test.go +++ b/pkg/cfn/manager/api_test.go @@ -135,7 +135,7 @@ var _ = Describe("StackCollection", func() { // 1) DescribeStacks // 2) CreateChangeSet // 3) DescribeChangeSetRequest (FAILED to abort early) - // 4) DescribeChangeSet (StatusReason contains "The submitted information didn't contain changes" to exit with noChangeError) + // 4) DescribeChangeSet (StatusReason contains "The submitted information didn't contain changes" to exit with NoChangeError) stackName := "eksctl-stack" changeSetName := "eksctl-changeset" @@ -168,7 +168,7 @@ var _ = Describe("StackCollection", func() { // Order of AWS SDK invocation // 1) DescribeStacks // 2) CreateChangeSet - // 3) DescribeChangeSet (StatusReason contains "The submitted information didn't contain changes" to exit with noChangeError) + // 3) DescribeChangeSet (StatusReason contains "The submitted information didn't contain changes" to exit with NoChangeError) stackName := "eksctl-stack" changeSetName := "eksctl-changeset" diff --git a/pkg/cfn/manager/delete_tasks.go b/pkg/cfn/manager/delete_tasks.go index 303e53f86a..90ab8b739c 100644 --- a/pkg/cfn/manager/delete_tasks.go +++ b/pkg/cfn/manager/delete_tasks.go @@ -29,6 +29,9 @@ type NewOIDCManager func() (*iamoidc.OpenIDConnectManager, error) // NewTasksToDeleteAddonIAM temporary type, to be removed after moving NewTasksToDeleteClusterWithNodeGroups to actions package type NewTasksToDeleteAddonIAM func(ctx context.Context, wait bool) (*tasks.TaskTree, error) +// NewTasksToDeletePodIdentityRoles temporary type, to be removed after moving NewTasksToDeleteClusterWithNodeGroups to actions package +type NewTasksToDeletePodIdentityRole func() (*tasks.TaskTree, error) + // NewTasksToDeleteClusterWithNodeGroups defines tasks required to delete the given cluster along with all of its resources func (c *StackCollection) NewTasksToDeleteClusterWithNodeGroups( ctx context.Context, @@ -37,6 +40,7 @@ func (c *StackCollection) NewTasksToDeleteClusterWithNodeGroups( clusterOperable bool, newOIDCManager NewOIDCManager, newTasksToDeleteAddonIAM NewTasksToDeleteAddonIAM, + newTasksToDeletePodIdentityRole NewTasksToDeletePodIdentityRole, cluster *ekstypes.Cluster, clientSetGetter kubernetes.ClientSetGetter, wait, force bool, @@ -75,6 +79,16 @@ func (c *StackCollection) NewTasksToDeleteClusterWithNodeGroups( taskTree.Append(deleteAddonIAMTasks) } + deletePodIdentityRoleTasks, err := newTasksToDeletePodIdentityRole() + if err != nil { + return nil, err + } + + if deletePodIdentityRoleTasks.Len() > 0 { + deletePodIdentityRoleTasks.IsSubTask = true + taskTree.Append(deletePodIdentityRoleTasks) + } + if clusterStack == nil { return nil, &StackNotFoundErr{ClusterName: c.spec.Metadata.Name} } diff --git a/pkg/cfn/manager/fakes/fake_stack_manager.go b/pkg/cfn/manager/fakes/fake_stack_manager.go index 4c95b38e23..a292e441bc 100644 --- a/pkg/cfn/manager/fakes/fake_stack_manager.go +++ b/pkg/cfn/manager/fakes/fake_stack_manager.go @@ -515,6 +515,20 @@ type FakeStackManager struct { result1 []manager.NodeGroupStack result2 error } + ListStackNamesStub func(context.Context, string) ([]string, error) + listStackNamesMutex sync.RWMutex + listStackNamesArgsForCall []struct { + arg1 context.Context + arg2 string + } + listStackNamesReturns struct { + result1 []string + result2 error + } + listStackNamesReturnsOnCall map[int]struct { + result1 []string + result2 error + } ListStacksStub func(context.Context) ([]*types.Stack, error) listStacksMutex sync.RWMutex listStacksArgsForCall []struct { @@ -592,6 +606,18 @@ type FakeStackManager struct { makeClusterStackNameReturnsOnCall map[int]struct { result1 string } + MustUpdateStackStub func(context.Context, manager.UpdateStackOptions) error + mustUpdateStackMutex sync.RWMutex + mustUpdateStackArgsForCall []struct { + arg1 context.Context + arg2 manager.UpdateStackOptions + } + mustUpdateStackReturns struct { + result1 error + } + mustUpdateStackReturnsOnCall map[int]struct { + result1 error + } NewManagedNodeGroupTaskStub func(context.Context, []*v1alpha5.ManagedNodeGroup, bool, vpc.Importer) *tasks.TaskTree newManagedNodeGroupTaskMutex sync.RWMutex newManagedNodeGroupTaskArgsForCall []struct { @@ -648,7 +674,7 @@ type FakeStackManager struct { newTasksToCreateIAMServiceAccountsReturnsOnCall map[int]struct { result1 *tasks.TaskTree } - NewTasksToDeleteClusterWithNodeGroupsStub func(context.Context, *types.Stack, []manager.NodeGroupStack, bool, manager.NewOIDCManager, manager.NewTasksToDeleteAddonIAM, *typesc.Cluster, kubernetes.ClientSetGetter, bool, bool, func(chan error, string) error) (*tasks.TaskTree, error) + NewTasksToDeleteClusterWithNodeGroupsStub func(context.Context, *types.Stack, []manager.NodeGroupStack, bool, manager.NewOIDCManager, manager.NewTasksToDeleteAddonIAM, manager.NewTasksToDeletePodIdentityRole, *typesc.Cluster, kubernetes.ClientSetGetter, bool, bool, func(chan error, string) error) (*tasks.TaskTree, error) newTasksToDeleteClusterWithNodeGroupsMutex sync.RWMutex newTasksToDeleteClusterWithNodeGroupsArgsForCall []struct { arg1 context.Context @@ -657,11 +683,12 @@ type FakeStackManager struct { arg4 bool arg5 manager.NewOIDCManager arg6 manager.NewTasksToDeleteAddonIAM - arg7 *typesc.Cluster - arg8 kubernetes.ClientSetGetter - arg9 bool + arg7 manager.NewTasksToDeletePodIdentityRole + arg8 *typesc.Cluster + arg9 kubernetes.ClientSetGetter arg10 bool - arg11 func(chan error, string) error + arg11 bool + arg12 func(chan error, string) error } newTasksToDeleteClusterWithNodeGroupsReturns struct { result1 *tasks.TaskTree @@ -3178,6 +3205,71 @@ func (fake *FakeStackManager) ListNodeGroupStacksWithStatusesReturnsOnCall(i int }{result1, result2} } +func (fake *FakeStackManager) ListStackNames(arg1 context.Context, arg2 string) ([]string, error) { + fake.listStackNamesMutex.Lock() + ret, specificReturn := fake.listStackNamesReturnsOnCall[len(fake.listStackNamesArgsForCall)] + fake.listStackNamesArgsForCall = append(fake.listStackNamesArgsForCall, struct { + arg1 context.Context + arg2 string + }{arg1, arg2}) + stub := fake.ListStackNamesStub + fakeReturns := fake.listStackNamesReturns + fake.recordInvocation("ListStackNames", []interface{}{arg1, arg2}) + fake.listStackNamesMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackManager) ListStackNamesCallCount() int { + fake.listStackNamesMutex.RLock() + defer fake.listStackNamesMutex.RUnlock() + return len(fake.listStackNamesArgsForCall) +} + +func (fake *FakeStackManager) ListStackNamesCalls(stub func(context.Context, string) ([]string, error)) { + fake.listStackNamesMutex.Lock() + defer fake.listStackNamesMutex.Unlock() + fake.ListStackNamesStub = stub +} + +func (fake *FakeStackManager) ListStackNamesArgsForCall(i int) (context.Context, string) { + fake.listStackNamesMutex.RLock() + defer fake.listStackNamesMutex.RUnlock() + argsForCall := fake.listStackNamesArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeStackManager) ListStackNamesReturns(result1 []string, result2 error) { + fake.listStackNamesMutex.Lock() + defer fake.listStackNamesMutex.Unlock() + fake.ListStackNamesStub = nil + fake.listStackNamesReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeStackManager) ListStackNamesReturnsOnCall(i int, result1 []string, result2 error) { + fake.listStackNamesMutex.Lock() + defer fake.listStackNamesMutex.Unlock() + fake.ListStackNamesStub = nil + if fake.listStackNamesReturnsOnCall == nil { + fake.listStackNamesReturnsOnCall = make(map[int]struct { + result1 []string + result2 error + }) + } + fake.listStackNamesReturnsOnCall[i] = struct { + result1 []string + result2 error + }{result1, result2} +} + func (fake *FakeStackManager) ListStacks(arg1 context.Context) ([]*types.Stack, error) { fake.listStacksMutex.Lock() ret, specificReturn := fake.listStacksReturnsOnCall[len(fake.listStacksArgsForCall)] @@ -3552,6 +3644,68 @@ func (fake *FakeStackManager) MakeClusterStackNameReturnsOnCall(i int, result1 s }{result1} } +func (fake *FakeStackManager) MustUpdateStack(arg1 context.Context, arg2 manager.UpdateStackOptions) error { + fake.mustUpdateStackMutex.Lock() + ret, specificReturn := fake.mustUpdateStackReturnsOnCall[len(fake.mustUpdateStackArgsForCall)] + fake.mustUpdateStackArgsForCall = append(fake.mustUpdateStackArgsForCall, struct { + arg1 context.Context + arg2 manager.UpdateStackOptions + }{arg1, arg2}) + stub := fake.MustUpdateStackStub + fakeReturns := fake.mustUpdateStackReturns + fake.recordInvocation("MustUpdateStack", []interface{}{arg1, arg2}) + fake.mustUpdateStackMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeStackManager) MustUpdateStackCallCount() int { + fake.mustUpdateStackMutex.RLock() + defer fake.mustUpdateStackMutex.RUnlock() + return len(fake.mustUpdateStackArgsForCall) +} + +func (fake *FakeStackManager) MustUpdateStackCalls(stub func(context.Context, manager.UpdateStackOptions) error) { + fake.mustUpdateStackMutex.Lock() + defer fake.mustUpdateStackMutex.Unlock() + fake.MustUpdateStackStub = stub +} + +func (fake *FakeStackManager) MustUpdateStackArgsForCall(i int) (context.Context, manager.UpdateStackOptions) { + fake.mustUpdateStackMutex.RLock() + defer fake.mustUpdateStackMutex.RUnlock() + argsForCall := fake.mustUpdateStackArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeStackManager) MustUpdateStackReturns(result1 error) { + fake.mustUpdateStackMutex.Lock() + defer fake.mustUpdateStackMutex.Unlock() + fake.MustUpdateStackStub = nil + fake.mustUpdateStackReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeStackManager) MustUpdateStackReturnsOnCall(i int, result1 error) { + fake.mustUpdateStackMutex.Lock() + defer fake.mustUpdateStackMutex.Unlock() + fake.MustUpdateStackStub = nil + if fake.mustUpdateStackReturnsOnCall == nil { + fake.mustUpdateStackReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.mustUpdateStackReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeStackManager) NewManagedNodeGroupTask(arg1 context.Context, arg2 []*v1alpha5.ManagedNodeGroup, arg3 bool, arg4 vpc.Importer) *tasks.TaskTree { var arg2Copy []*v1alpha5.ManagedNodeGroup if arg2 != nil { @@ -3828,7 +3982,7 @@ func (fake *FakeStackManager) NewTasksToCreateIAMServiceAccountsReturnsOnCall(i }{result1} } -func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroups(arg1 context.Context, arg2 *types.Stack, arg3 []manager.NodeGroupStack, arg4 bool, arg5 manager.NewOIDCManager, arg6 manager.NewTasksToDeleteAddonIAM, arg7 *typesc.Cluster, arg8 kubernetes.ClientSetGetter, arg9 bool, arg10 bool, arg11 func(chan error, string) error) (*tasks.TaskTree, error) { +func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroups(arg1 context.Context, arg2 *types.Stack, arg3 []manager.NodeGroupStack, arg4 bool, arg5 manager.NewOIDCManager, arg6 manager.NewTasksToDeleteAddonIAM, arg7 manager.NewTasksToDeletePodIdentityRole, arg8 *typesc.Cluster, arg9 kubernetes.ClientSetGetter, arg10 bool, arg11 bool, arg12 func(chan error, string) error) (*tasks.TaskTree, error) { var arg3Copy []manager.NodeGroupStack if arg3 != nil { arg3Copy = make([]manager.NodeGroupStack, len(arg3)) @@ -3843,18 +3997,19 @@ func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroups(arg1 context arg4 bool arg5 manager.NewOIDCManager arg6 manager.NewTasksToDeleteAddonIAM - arg7 *typesc.Cluster - arg8 kubernetes.ClientSetGetter - arg9 bool + arg7 manager.NewTasksToDeletePodIdentityRole + arg8 *typesc.Cluster + arg9 kubernetes.ClientSetGetter arg10 bool - arg11 func(chan error, string) error - }{arg1, arg2, arg3Copy, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11}) + arg11 bool + arg12 func(chan error, string) error + }{arg1, arg2, arg3Copy, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12}) stub := fake.NewTasksToDeleteClusterWithNodeGroupsStub fakeReturns := fake.newTasksToDeleteClusterWithNodeGroupsReturns - fake.recordInvocation("NewTasksToDeleteClusterWithNodeGroups", []interface{}{arg1, arg2, arg3Copy, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11}) + fake.recordInvocation("NewTasksToDeleteClusterWithNodeGroups", []interface{}{arg1, arg2, arg3Copy, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12}) fake.newTasksToDeleteClusterWithNodeGroupsMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) + return stub(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12) } if specificReturn { return ret.result1, ret.result2 @@ -3868,17 +4023,17 @@ func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroupsCallCount() i return len(fake.newTasksToDeleteClusterWithNodeGroupsArgsForCall) } -func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroupsCalls(stub func(context.Context, *types.Stack, []manager.NodeGroupStack, bool, manager.NewOIDCManager, manager.NewTasksToDeleteAddonIAM, *typesc.Cluster, kubernetes.ClientSetGetter, bool, bool, func(chan error, string) error) (*tasks.TaskTree, error)) { +func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroupsCalls(stub func(context.Context, *types.Stack, []manager.NodeGroupStack, bool, manager.NewOIDCManager, manager.NewTasksToDeleteAddonIAM, manager.NewTasksToDeletePodIdentityRole, *typesc.Cluster, kubernetes.ClientSetGetter, bool, bool, func(chan error, string) error) (*tasks.TaskTree, error)) { fake.newTasksToDeleteClusterWithNodeGroupsMutex.Lock() defer fake.newTasksToDeleteClusterWithNodeGroupsMutex.Unlock() fake.NewTasksToDeleteClusterWithNodeGroupsStub = stub } -func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroupsArgsForCall(i int) (context.Context, *types.Stack, []manager.NodeGroupStack, bool, manager.NewOIDCManager, manager.NewTasksToDeleteAddonIAM, *typesc.Cluster, kubernetes.ClientSetGetter, bool, bool, func(chan error, string) error) { +func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroupsArgsForCall(i int) (context.Context, *types.Stack, []manager.NodeGroupStack, bool, manager.NewOIDCManager, manager.NewTasksToDeleteAddonIAM, manager.NewTasksToDeletePodIdentityRole, *typesc.Cluster, kubernetes.ClientSetGetter, bool, bool, func(chan error, string) error) { fake.newTasksToDeleteClusterWithNodeGroupsMutex.RLock() defer fake.newTasksToDeleteClusterWithNodeGroupsMutex.RUnlock() argsForCall := fake.newTasksToDeleteClusterWithNodeGroupsArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6, argsForCall.arg7, argsForCall.arg8, argsForCall.arg9, argsForCall.arg10, argsForCall.arg11 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6, argsForCall.arg7, argsForCall.arg8, argsForCall.arg9, argsForCall.arg10, argsForCall.arg11, argsForCall.arg12 } func (fake *FakeStackManager) NewTasksToDeleteClusterWithNodeGroupsReturns(result1 *tasks.TaskTree, result2 error) { @@ -4583,6 +4738,8 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.listNodeGroupStacksMutex.RUnlock() fake.listNodeGroupStacksWithStatusesMutex.RLock() defer fake.listNodeGroupStacksWithStatusesMutex.RUnlock() + fake.listStackNamesMutex.RLock() + defer fake.listStackNamesMutex.RUnlock() fake.listStacksMutex.RLock() defer fake.listStacksMutex.RUnlock() fake.listStacksMatchingMutex.RLock() @@ -4595,6 +4752,8 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.makeChangeSetNameMutex.RUnlock() fake.makeClusterStackNameMutex.RLock() defer fake.makeClusterStackNameMutex.RUnlock() + fake.mustUpdateStackMutex.RLock() + defer fake.mustUpdateStackMutex.RUnlock() fake.newManagedNodeGroupTaskMutex.RLock() defer fake.newManagedNodeGroupTaskMutex.RUnlock() fake.newTaskToDeleteUnownedNodeGroupMutex.RLock() diff --git a/pkg/cfn/manager/interface.go b/pkg/cfn/manager/interface.go index 3dc4ca91b5..b8fbe2f1b8 100644 --- a/pkg/cfn/manager/interface.go +++ b/pkg/cfn/manager/interface.go @@ -79,6 +79,7 @@ type StackManager interface { ListStacks(ctx context.Context) ([]*Stack, error) ListStacksWithStatuses(ctx context.Context, statusFilters ...cfntypes.StackStatus) ([]*Stack, error) ListStacksMatching(ctx context.Context, nameRegex string, statusFilters ...cfntypes.StackStatus) ([]*Stack, error) + ListStackNames(ctx context.Context, regExp string) ([]string, error) LookupCloudTrailEvents(ctx context.Context, i *Stack) ([]cttypes.Event, error) MakeChangeSetName(action string) string MakeClusterStackName() string @@ -86,7 +87,7 @@ type StackManager interface { NewTaskToDeleteUnownedNodeGroup(ctx context.Context, clusterName, nodegroup string, eksAPI awsapi.EKS, waitCondition *DeleteWaitCondition) tasks.Task NewTasksToCreateClusterWithNodeGroups(ctx context.Context, nodeGroups []*v1alpha5.NodeGroup, managedNodeGroups []*v1alpha5.ManagedNodeGroup, postClusterCreationTasks ...tasks.Task) *tasks.TaskTree NewTasksToCreateIAMServiceAccounts(serviceAccounts []*v1alpha5.ClusterIAMServiceAccount, oidc *iamoidc.OpenIDConnectManager, clientSetGetter kubernetes.ClientSetGetter) *tasks.TaskTree - NewTasksToDeleteClusterWithNodeGroups(ctx context.Context, clusterStack *Stack, nodeGroupStacks []NodeGroupStack, clusterOperable bool, newOIDCManager NewOIDCManager, newTasksToDeleteAddonIAM NewTasksToDeleteAddonIAM, cluster *ekstypes.Cluster, clientSetGetter kubernetes.ClientSetGetter, wait, force bool, cleanup func(chan error, string) error) (*tasks.TaskTree, error) + NewTasksToDeleteClusterWithNodeGroups(ctx context.Context, clusterStack *Stack, nodeGroupStacks []NodeGroupStack, clusterOperable bool, newOIDCManager NewOIDCManager, newTasksToDeleteAddonIAM NewTasksToDeleteAddonIAM, newTasksToDeletePodIdentityRole NewTasksToDeletePodIdentityRole, cluster *ekstypes.Cluster, clientSetGetter kubernetes.ClientSetGetter, wait, force bool, cleanup func(chan error, string) error) (*tasks.TaskTree, error) NewTasksToDeleteIAMServiceAccounts(ctx context.Context, serviceAccounts []string, clientSetGetter kubernetes.ClientSetGetter, wait bool) (*tasks.TaskTree, error) NewTasksToDeleteNodeGroups(stacks []NodeGroupStack, shouldDelete func(_ string) bool, wait bool, cleanup func(chan error, string) error) (*tasks.TaskTree, error) NewTasksToDeleteOIDCProviderWithIAMServiceAccounts(ctx context.Context, newOIDCManager NewOIDCManager, cluster *ekstypes.Cluster, clientSetGetter kubernetes.ClientSetGetter, force bool) (*tasks.TaskTree, error) @@ -96,4 +97,5 @@ type StackManager interface { StackStatusIsNotTransitional(s *Stack) bool UpdateNodeGroupStack(ctx context.Context, nodeGroupName, template string, wait bool) error UpdateStack(ctx context.Context, options UpdateStackOptions) error + MustUpdateStack(ctx context.Context, options UpdateStackOptions) error } diff --git a/pkg/cfn/manager/waiters.go b/pkg/cfn/manager/waiters.go index 25e43dab8c..e6c9220e3e 100644 --- a/pkg/cfn/manager/waiters.go +++ b/pkg/cfn/manager/waiters.go @@ -50,12 +50,13 @@ func (c *StackCollection) troubleshootStackFailureCause(ctx context.Context, i * } } -type noChangeError struct { - msg string +// NoChangeError represents an error for when a CloudFormation changeset contains no changes. +type NoChangeError struct { + Msg string } -func (e *noChangeError) Error() string { - return e.msg +func (e *NoChangeError) Error() string { + return e.Msg } // DoWaitUntilStackIsCreated blocks until the given stack's @@ -141,7 +142,7 @@ func (c *StackCollection) doWaitUntilChangeSetIsCreated(ctx context.Context, i * logger.Info("waiting for CloudFormation changeset %q for stack %q", changesetName, *i.StackName) if out.StatusReason != nil && strings.Contains(*out.StatusReason, "The submitted information didn't contain changes") { logger.Info("nothing to update") - return false, &noChangeError{*out.StatusReason} + return false, &NoChangeError{*out.StatusReason} } return defaultRetryer(ctx, in, out, err) } diff --git a/pkg/cfn/template/iam_helpers.go b/pkg/cfn/template/iam_helpers.go index 220cdd76bb..1d5070edf5 100644 --- a/pkg/cfn/template/iam_helpers.go +++ b/pkg/cfn/template/iam_helpers.go @@ -1,6 +1,9 @@ package template -import gfn "github.com/weaveworks/goformation/v4/cloudformation/types" +import ( + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + gfn "github.com/weaveworks/goformation/v4/cloudformation/types" +) // AttachPolicy attaches the specified policy document func (t *Template) AttachPolicy(name string, refRole *Value, policyDoc MapOfInterfaces) { @@ -63,7 +66,7 @@ func MakeAssumeRolePolicyDocumentForPodIdentity() MapOfInterfaces { "sts:TagSession", }, "Principal": map[string]string{ - "Service": "beta.pods.eks.aws.internal", + "Service": api.EKSServicePrincipal, }, }) } diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 2343346cfb..74ed21e080 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -312,6 +312,9 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. suggestion := fmt.Sprintf("please add `%s` addon to the config file", api.PodIdentityAgentAddon) return api.ErrPodIdentityAgentNotInstalled(suggestion) } + if err := validatePodIdentityAssociationsForConfig(clusterConfig, true); err != nil { + return err + } } return validateDryRun() diff --git a/pkg/ctl/cmdutils/pod_identity_association.go b/pkg/ctl/cmdutils/pod_identity_association.go index b9b2478407..7f374b9f54 100644 --- a/pkg/ctl/cmdutils/pod_identity_association.go +++ b/pkg/ctl/cmdutils/pod_identity_association.go @@ -1,6 +1,7 @@ package cmdutils import ( + "errors" "fmt" "k8s.io/apimachinery/pkg/util/sets" @@ -28,22 +29,17 @@ func NewCreatePodIdentityAssociationLoader(cmd *Cmd, podIdentityAssociation *api l.flagsIncompatibleWithConfigFile = sets.NewString(podIdentityAssociationFlagsIncompatibleWithConfigFile...) l.validateWithConfigFile = func() error { - if len(cmd.ClusterConfig.IAM.PodIdentityAssociations) == 0 { - return fmt.Errorf("at least one pod identity association is required") - } - return nil + return validatePodIdentityAssociationsForConfig(l.ClusterConfig, true) } l.validateWithoutConfigFile = func() error { - if l.ClusterConfig.Metadata.Name == "" { - return ErrMustBeSet(ClusterNameFlag(cmd)) - } - if podIdentityAssociation.Namespace == "" { - return ErrMustBeSet("--namespace") - } - if podIdentityAssociation.ServiceAccountName == "" { - return ErrMustBeSet("--service-account-name") + if err := validatePodIdentityAssociation(l, PodIdentityAssociationOptions{ + Namespace: podIdentityAssociation.Namespace, + ServiceAccountName: podIdentityAssociation.ServiceAccountName, + }); err != nil { + return err } + if podIdentityAssociation.RoleARN == "" && len(podIdentityAssociation.PermissionPolicyARNs) == 0 && !podIdentityAssociation.WellKnownPolicies.HasPolicy() { @@ -57,6 +53,7 @@ func NewCreatePodIdentityAssociationLoader(cmd *Cmd, podIdentityAssociation *api return fmt.Errorf("--well-known-policies cannot be specified when --role-arn is set") } } + l.Cmd.ClusterConfig.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{*podIdentityAssociation} return nil } @@ -67,6 +64,8 @@ func NewCreatePodIdentityAssociationLoader(cmd *Cmd, podIdentityAssociation *api func NewGetPodIdentityAssociationLoader(cmd *Cmd, pia *api.PodIdentityAssociation) ClusterConfigLoader { l := newCommonClusterConfigLoader(cmd) + l.flagsIncompatibleWithConfigFile = sets.NewString("cluster") + l.validateWithoutConfigFile = func() error { if cmd.ClusterConfig.Metadata.Name == "" { return ErrMustBeSet(ClusterNameFlag(cmd)) @@ -76,5 +75,117 @@ func NewGetPodIdentityAssociationLoader(cmd *Cmd, pia *api.PodIdentityAssociatio } return nil } + + l.validateWithConfigFile = func() error { + if cmd.ClusterConfig.Metadata.Name == "" { + return ErrMustBeSet(ClusterNameFlag(cmd)) + } + return nil + } + + return l +} + +// PodIdentityAssociationOptions holds the options for deleting a pod identity association. +type PodIdentityAssociationOptions struct { + // Namespace is the namespace the service account belongs to. + Namespace string + // ServiceAccountName is the name of the Kubernetes ServiceAccount. + ServiceAccountName string +} + +func validatePodIdentityAssociation(l *commonClusterConfigLoader, options PodIdentityAssociationOptions) error { + if l.ClusterConfig.Metadata.Name == "" { + return ErrMustBeSet(ClusterNameFlag(l.Cmd)) + } + if options.Namespace == "" { + return errors.New("--namespace is required") + } + if options.ServiceAccountName == "" { + return errors.New("--service-account-name is required") + } + return nil +} + +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") + } + + for i, pia := range clusterConfig.IAM.PodIdentityAssociations { + path := fmt.Sprintf("podIdentityAssociations[%d]", i) + if pia.Namespace == "" { + return fmt.Errorf("%s.namespace must be set", path) + } + if pia.ServiceAccountName == "" { + return fmt.Errorf("%s.serviceAccountName must be set", path) + } + + if !isCreate { + continue + } + + if pia.RoleARN == "" && + len(pia.PermissionPolicy) == 0 && + len(pia.PermissionPolicyARNs) == 0 && + !pia.WellKnownPolicies.HasPolicy() { + 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 != "" { + if len(pia.PermissionPolicy) > 0 { + return fmt.Errorf("%[1]s.permissionPolicy cannot be specified when %[1]s.roleARN is set", path) + } + if len(pia.PermissionPolicyARNs) > 0 { + return fmt.Errorf("%[1]s.permissionPolicyARNs cannot be specified when %[1]s.roleARN is set", path) + } + if pia.WellKnownPolicies.HasPolicy() { + return fmt.Errorf("%[1]s.wellKnownPolicies cannot be specified when %[1]s.roleARN is set", path) + } + } + } + + return nil +} + +// NewDeletePodIdentityAssociationLoader will load config or use flags for `eksctl delete podidentityassociation`. +func NewDeletePodIdentityAssociationLoader(cmd *Cmd, options PodIdentityAssociationOptions) ClusterConfigLoader { + l := newCommonClusterConfigLoader(cmd) + l.flagsIncompatibleWithConfigFile.Insert("namespace", "service-account-name") + + l.validateWithoutConfigFile = func() error { + return validatePodIdentityAssociation(l, options) + } + + l.validateWithConfigFile = func() error { + return validatePodIdentityAssociationsForConfig(l.ClusterConfig, false) + } + return l +} + +// UpdatePodIdentityAssociationOptions holds the options for updating a pod identity association. +type UpdatePodIdentityAssociationOptions struct { + PodIdentityAssociationOptions + // RoleARN is the IAM role ARN to be associated with the pod. + RoleARN string +} + +// NewUpdatePodIdentityAssociationLoader will load config or use flags for `eksctl update podidentityassociation`. +func NewUpdatePodIdentityAssociationLoader(cmd *Cmd, options UpdatePodIdentityAssociationOptions) ClusterConfigLoader { + l := newCommonClusterConfigLoader(cmd) + l.flagsIncompatibleWithConfigFile.Insert("namespace", "service-account-name", "role-arn") + + l.validateWithoutConfigFile = func() error { + if err := validatePodIdentityAssociation(l, options.PodIdentityAssociationOptions); err != nil { + return err + } + if options.RoleARN == "" { + return errors.New("--role-arn is required") + } + return nil + } + + l.validateWithConfigFile = func() error { + return validatePodIdentityAssociationsForConfig(l.ClusterConfig, false) + } return l } diff --git a/pkg/ctl/create/cluster.go b/pkg/ctl/create/cluster.go index a7d4720920..fa961d9d49 100644 --- a/pkg/ctl/create/cluster.go +++ b/pkg/ctl/create/cluster.go @@ -21,6 +21,7 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/addon" "github.com/weaveworks/eksctl/pkg/actions/flux" "github.com/weaveworks/eksctl/pkg/actions/karpenter" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/authconfigmap" "github.com/weaveworks/eksctl/pkg/cfn/manager" @@ -435,6 +436,16 @@ func doCreateCluster(cmd *cmdutils.Cmd, ngFilter *filter.NodeGroupFilter, params } } + if len(cfg.IAM.PodIdentityAssociations) > 0 { + if err := podidentityassociation.NewCreator( + cfg.Metadata.Name, + stackManager, + ctl.AWSProvider.EKS(), + ).CreatePodIdentityAssociations(ctx, cfg.IAM.PodIdentityAssociations); err != nil { + return err + } + } + // After we have the cluster config and all the nodes are done, we install Karpenter if necessary. if cfg.Karpenter != nil { config := kubeconfig.NewForKubectl(cfg, eks.GetUsername(ctl.Status.IAMRoleARN), params.AuthenticatorRoleARN, ctl.AWSProvider.Profile().Name) diff --git a/pkg/ctl/create/pod_identity_association_test.go b/pkg/ctl/create/pod_identity_association_test.go index 69c400db37..9c0e4d0d5f 100644 --- a/pkg/ctl/create/pod_identity_association_test.go +++ b/pkg/ctl/create/pod_identity_association_test.go @@ -31,11 +31,11 @@ var _ = Describe("create pod identity association", func() { }), Entry("missing required flag --namespace", createPodIdentityAssociationEntry{ args: []string{"--cluster", "test-cluster"}, - expectedErr: "--namespace must be set", + expectedErr: "--namespace is required", }), Entry("missing required flag --service-account-name", createPodIdentityAssociationEntry{ args: []string{"--cluster", "test-cluster", "--namespace", "test-namespace"}, - expectedErr: "--service-account-name must be set", + expectedErr: "--service-account-name is required", }), Entry("setting --cluster and --config-file at the same time", createPodIdentityAssociationEntry{ args: []string{"--cluster", "test-cluster", "--config-file", configFile}, diff --git a/pkg/ctl/delete/delete.go b/pkg/ctl/delete/delete.go index 1ad2351a1f..d55a38701a 100644 --- a/pkg/ctl/delete/delete.go +++ b/pkg/ctl/delete/delete.go @@ -16,6 +16,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddResourceCmd(flagGrouping, verbCmd, deleteIAMIdentityMappingCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, deleteFargateProfile) cmdutils.AddResourceCmd(flagGrouping, verbCmd, deleteAddonCmd) + cmdutils.AddResourceCmd(flagGrouping, verbCmd, deletePodIdentityAssociation) return verbCmd } diff --git a/pkg/ctl/delete/pod_identity_association.go b/pkg/ctl/delete/pod_identity_association.go new file mode 100644 index 0000000000..ec9389a71b --- /dev/null +++ b/pkg/ctl/delete/pod_identity_association.go @@ -0,0 +1,78 @@ +package delete + +import ( + "context" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" +) + +func deletePodIdentityAssociation(cmd *cmdutils.Cmd) { + cfg := api.NewClusterConfig() + cmd.ClusterConfig = cfg + + var options cmdutils.PodIdentityAssociationOptions + + cmd.SetDescription("podidentityassociation", "Delete pod identity associations", "") + + cmd.CobraCommand.RunE = func(_ *cobra.Command, args []string) error { + return doDeletePodIdentityAssociation(cmd, options) + } + + cmd.FlagSetGroup.InFlagSet("General", func(fs *pflag.FlagSet) { + cmdutils.AddClusterFlagWithDeprecated(fs, cfg.Metadata) + cmdutils.AddRegionFlag(fs, &cmd.ProviderConfig) + cmdutils.AddConfigFileFlag(fs, &cmd.ClusterConfigFile) + cmdutils.AddTimeoutFlag(fs, &cmd.ProviderConfig.WaitTimeout) + }) + + 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") + + }) + + cmdutils.AddCommonFlagsForAWS(cmd, &cmd.ProviderConfig, false) +} + +func doDeletePodIdentityAssociation(cmd *cmdutils.Cmd, options cmdutils.PodIdentityAssociationOptions) error { + if err := cmdutils.NewDeletePodIdentityAssociationLoader(cmd, options).Load(); err != nil { + return err + } + + cfg := cmd.ClusterConfig + ctx := context.Background() + ctl, err := cmd.NewProviderForExistingCluster(ctx) + if err != nil { + return err + } + + if cfg.Metadata.Name == "" { + return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) + } + + if ok, err := ctl.CanOperate(cfg); !ok { + return err + } + + if cmd.ClusterConfigFile == "" { + cmd.ClusterConfig.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{ + { + Namespace: options.Namespace, + ServiceAccountName: options.ServiceAccountName, + }, + } + } + + deleter := &podidentityassociation.Deleter{ + ClusterName: cfg.Metadata.Name, + StackDeleter: ctl.NewStackManager(cfg), + APIDeleter: ctl.AWSProvider.EKS(), + } + + return deleter.Delete(ctx, podidentityassociation.ToIdentifiers(cfg.IAM.PodIdentityAssociations)) +} diff --git a/pkg/ctl/update/pod_identity_association.go b/pkg/ctl/update/pod_identity_association.go new file mode 100644 index 0000000000..e93eaeed4d --- /dev/null +++ b/pkg/ctl/update/pod_identity_association.go @@ -0,0 +1,81 @@ +package update + +import ( + "context" + + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" +) + +func updatePodIdentityAssociation(cmd *cmdutils.Cmd) { + cfg := api.NewClusterConfig() + cmd.ClusterConfig = cfg + + var options cmdutils.UpdatePodIdentityAssociationOptions + + cmd.SetDescription("podidentityassociation", "Update pod identity associations", "") + + cmd.CobraCommand.RunE = func(_ *cobra.Command, args []string) error { + return doUpdatePodIdentityAssociation(cmd, options) + } + + cmd.FlagSetGroup.InFlagSet("General", func(fs *pflag.FlagSet) { + cmdutils.AddClusterFlagWithDeprecated(fs, cfg.Metadata) + cmdutils.AddRegionFlag(fs, &cmd.ProviderConfig) + cmdutils.AddConfigFileFlag(fs, &cmd.ClusterConfigFile) + cmdutils.AddTimeoutFlag(fs, &cmd.ProviderConfig.WaitTimeout) + }) + + 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") + + }) + + cmdutils.AddCommonFlagsForAWS(cmd, &cmd.ProviderConfig, false) +} + +func doUpdatePodIdentityAssociation(cmd *cmdutils.Cmd, options cmdutils.UpdatePodIdentityAssociationOptions) error { + if err := cmdutils.NewUpdatePodIdentityAssociationLoader(cmd, options).Load(); err != nil { + return err + } + + cfg := cmd.ClusterConfig + ctx := context.Background() + ctl, err := cmd.NewProviderForExistingCluster(ctx) + if err != nil { + return err + } + + if cfg.Metadata.Name == "" { + return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) + } + + if ok, err := ctl.CanOperate(cfg); !ok { + return err + } + + if cmd.ClusterConfigFile == "" { + cmd.ClusterConfig.IAM.PodIdentityAssociations = []api.PodIdentityAssociation{ + { + Namespace: options.Namespace, + ServiceAccountName: options.ServiceAccountName, + RoleARN: options.RoleARN, + }, + } + } + + stackManager := ctl.NewStackManager(cfg) + updater := &podidentityassociation.Updater{ + ClusterName: cfg.Metadata.Name, + APIUpdater: ctl.AWSProvider.EKS(), + StackUpdater: stackManager, + } + return updater.Update(ctx, cfg.IAM.PodIdentityAssociations) +} diff --git a/pkg/ctl/update/update.go b/pkg/ctl/update/update.go index 6ebf27142f..68126cbbc6 100644 --- a/pkg/ctl/update/update.go +++ b/pkg/ctl/update/update.go @@ -14,6 +14,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddResourceCmd(flagGrouping, verbCmd, updateAddonCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, updateIAMServiceAccountCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, updateNodeGroupCmd) + cmdutils.AddResourceCmd(flagGrouping, verbCmd, updatePodIdentityAssociation) return verbCmd } diff --git a/pkg/eks/tasks.go b/pkg/eks/tasks.go index 4075049e56..f10b57dc06 100644 --- a/pkg/eks/tasks.go +++ b/pkg/eks/tasks.go @@ -11,7 +11,6 @@ import ( "github.com/weaveworks/eksctl/pkg/actions/iamidentitymapping" "github.com/weaveworks/eksctl/pkg/actions/identityproviders" - "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" "github.com/weaveworks/eksctl/pkg/windows" @@ -314,18 +313,6 @@ func (c *ClusterProvider) CreateExtraClusterConfigTasks(ctx context.Context, cfg }) } - if len(cfg.IAM.PodIdentityAssociations) > 0 { - piaTasks := tasks.TaskTree{ - Parallel: true, - } - piaTasks.Append(podidentityassociation.NewCreator( - cfg.Metadata.Name, - c.NewStackManager(cfg), - c.AWSProvider.EKS(), - ).CreateTasks(ctx, cfg.IAM.PodIdentityAssociations)) - newTasks.Append(&piaTasks) - } - if cfg.HasWindowsNodeGroup() { newTasks.Append(&WindowsIPAMTask{ Info: "enable Windows IP address management", diff --git a/pkg/utils/names/names.go b/pkg/utils/names/names.go index ed2d20cf8c..c57a807879 100644 --- a/pkg/utils/names/names.go +++ b/pkg/utils/names/names.go @@ -48,15 +48,6 @@ func ForFargateProfile(name string) string { return fmt.Sprintf("fp-%s", RandomName(length, chars)) } -// ForIAMRole returns the provided name if non-empty, or else generates -// a random name matching: pod-identity-role-[abcdef0123456789]{8} -func ForIAMRole(name string) string { - if name != "" { - return name - } - return fmt.Sprintf("pod-identity-role-%s", RandomName(8, "abcdef0123456789")) -} - // useNameOrGenerate picks one of the provided strings or generates a // new one using the provided generate function func useNameOrGenerate(a, b string, generate func() string) string { From a4a288e583520d7f4141b860002727857367f682 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Mon, 27 Nov 2023 16:11:53 +0200 Subject: [PATCH 2/3] Add documentation for pod identity associations feature (#7313) * Add documentation for pod identity associations * add more details to how pod identity agent addon works * remove unnecessary empty lines * refactor as per PR suggestions * add references to official AWS docs * generate docs * rephrase intro Co-authored-by: Himangini * rephrase news section Co-authored-by: Himangini * add newline between docs links --------- Co-authored-by: Himangini --- examples/39-pod-identity-association.yaml | 52 ++++++ userdocs/mkdocs.yml | 1 + userdocs/src/getting-started.md | 2 + .../src/usage/pod-identity-associations.md | 171 ++++++++++++++++++ userdocs/theme/home.html | 1 + 5 files changed, 227 insertions(+) create mode 100644 examples/39-pod-identity-association.yaml create mode 100644 userdocs/src/usage/pod-identity-associations.md diff --git a/examples/39-pod-identity-association.yaml b/examples/39-pod-identity-association.yaml new file mode 100644 index 0000000000..65bbceca3d --- /dev/null +++ b/examples/39-pod-identity-association.yaml @@ -0,0 +1,52 @@ +# An example config for creating pod identity associations. +--- +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig + +metadata: + name: cluster-39 + region: us-west-2 + +managedNodeGroups: + - name: mng1 + +addons: + - name: eks-pod-identity-agent # required for `iam.podIdentityAssociations` + tags: + team: eks + +iam: + podIdentityAssociations: + # roleARN is given, eksctl will only create the pod identity association + - namespace: default + serviceAccountName: s3-reader + roleARN: arn:aws:iam::111122223333:role/role-1 + + # roleARN is not given, eksctl will first create an IAM role with given roleName using: + # permissionPolicyARNs, wellKnownPolicies and permissionsBoundaryARN + - namespace: dev + serviceAccountName: app-cache-access + roleName: pod-identity-role-app-cache + permissionPolicyARNs: ["arn:aws:iam::111122223333:policy/permission-policy-1", "arn:aws:iam::111122223333:policy/permission-policy-2"] + wellKnownPolicies: + autoScaler: true + externalDNS: true + permissionsBoundaryARN: arn:aws:iam::111122223333:policy/permission-boundary + + # roleARN is not given, eksctl will first create an IAM role with automatically generated roleName, + # using the permissionPolicy inline document + - namespace: dev + serviceAccountName: nginx + permissionPolicy: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "autoscaling:DescribeAutoScalingGroups" + - "autoscaling:DescribeAutoScalingInstances" + - "autoscaling:DescribeLaunchConfigurations" + - "autoscaling:DescribeTags" + - "autoscaling:SetDesiredCapacity" + - "autoscaling:TerminateInstanceInAutoScalingGroup" + - "ec2:DescribeLaunchTemplateVersions" + Resource: '*' \ No newline at end of file diff --git a/userdocs/mkdocs.yml b/userdocs/mkdocs.yml index 6ce84d4a44..5578b255a7 100644 --- a/userdocs/mkdocs.yml +++ b/userdocs/mkdocs.yml @@ -187,6 +187,7 @@ nav: - usage/iam-policies.md - usage/iam-identity-mappings.md - usage/iamserviceaccounts.md + - usage/pod-identity-associations.md - usage/dry-run.md - usage/schema.md - usage/eksctl-anywhere.md diff --git a/userdocs/src/getting-started.md b/userdocs/src/getting-started.md index fefb363dfa..b6eb765939 100644 --- a/userdocs/src/getting-started.md +++ b/userdocs/src/getting-started.md @@ -1,6 +1,8 @@ # Getting started !!! tip "New for 2023" + `eksctl` now supports configuring fine-grained permissions to EKS running apps via [EKS Pod Identity Associations](/usage/pod-identity-associations) + `eksctl` now supports [updating the subnets and security groups](/usage/cluster-subnets-security-groups) associated with the EKS control plane. `eksctl` now supports creating fully private clusters on [AWS Outposts](/usage/outposts). diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md new file mode 100644 index 0000000000..e0ccd99423 --- /dev/null +++ b/userdocs/src/usage/pod-identity-associations.md @@ -0,0 +1,171 @@ +# EKS Pod Identity Associations + +## 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. + +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. + +``` +eksctl create addon --cluster my-cluster --name eks-pod-identity-agent +``` + +Additionally, if using a pre-existing IAM role when creating a pod identity association, you must configure the role to trust the newly introduced EKS service principal (`pods.eks.amazonaws.com`). An example IAM trust policy can be found below: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "pods.eks.amazonaws.com" + }, + "Action": [ + "sts:AssumeRole", + "sts:TagSession" + ] + } + ] +} +``` + +If instead you do not provide the ARN of an existing role to the create command, `eksctl` will create one behind the scenes and configure the above trust policy. + +## Creating Pod Identity Associations + +For manipulating pod identity associations, `eksctl` has added a new field under `iam.podIdentityAssociations`, e.g. + +```yaml +iam: + podIdentityAssociations: + - namespace: #required + serviceAccountName: #required + roleARN: #required if none of permissionPolicyARNs, permissionPolicy and wellKnownPolicies is specified. Also, cannot be used together with any of the three other referenced fields. + roleName: #optional, generated automatically if not provided, ignored if roleARN is provided + permissionPolicy: {} #optional + permissionPolicyARNs: [] #optional + wellKnownPolicies: {} #optional + permissionsBoundaryARN: #optional + tags: {} #optional +``` + +For a complete example, refer to [pod-identity-associations.yaml](https://github.com/eksctl-io/eksctl/blob/main/examples/38-cluster-subnets-sgs.yaml). + +???+ note + Apart from `permissionPolicy` which is used as an inline policy document, all other fields have a CLI flag counterpart. + +Creating pod identity associations can be achieved in the following ways. During cluster creation, by specifying the desired pod identity associations as part of the config file and running: + +``` +eksctl create cluster -f config.yaml +``` + +Post cluster creation, using either a config file e.g. + +``` +eksctl create podidentityassociation -f config.yaml +``` + +OR using CLI flags e.g. + +```bash +eksctl create podidentityassociation \ + --cluster my-cluster \ + --namespace default \ + --service-account-name s3-reader \ + --permission-policy-arns="arn:aws:iam::111122223333:policy/permission-policy-1, arn:aws:iam::111122223333:policy/permission-policy-2" \ + --well-known-policies="autoScaler,externalDNS" \ + --permissions-boundary-arn arn:aws:iam::111122223333:policy/permissions-boundary +``` + +???+ note + Only a single IAM role can be associated with a service account at a time. Therefore, trying to create a second pod identity association for the same service account will result in an error. + +## Fetching Pod Identity Associations + +To retrieve all pod identity associations for a certain cluster, run one of the following commands: + +``` +eksctl get podidentityassociation -f config.yaml +``` + +OR + +``` +eksctl get podidentityassociation --cluster my-cluster +``` + +Additionally, to retrieve only the pod identity associations within a given namespace, use the `--namespace` flag, e.g. + +``` +eksctl get podidentityassociation --cluster my-cluster --namespace default +``` + +Finally, to retrieve a single association, corresponding to a certain K8s service account, also include the `--service-account-name` to the command above, i.e. + +``` +eksctl get podidentityassociation --cluster my-cluster --namespace default --service-account-name s3-reader +``` + +## Updating Pod Identity Associations + +To update the IAM role of one or more pod identity associations, either pass the new `roleARN(s)` to the config file e.g. + +```yaml +iam: + podIdentityAssociations: + - namespace: default + serviceAccountName: s3-reader + roleARN: new-role-arn-1 + - namespace: dev + serviceAccountName: app-cache-access + roleARN: new-role-arn-2 +``` + +and run: + +``` +eksctl update podidentityassociation -f config.yaml +``` + +OR (to update a single association) pass the new `--role-arn` via CLI flags: + +``` +eksctl update podidentityassociation --cluster my-cluster --namespace default --service-account-name s3-reader --role-arn new-role-arn +``` + +## Deleting Pod Identity Associations + +To delete one or more pod identity associations, either pass `namespace(s)` and `serviceAccountName(s)` to the config file e.g. + +```yaml +iam: + podIdentityAssociations: + - namespace: default + serviceAccountName: s3-reader + - namespace: dev + serviceAccountName: app-cache-access +``` + +and run: + +``` +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 +``` + +## 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 userdocs](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 c7e990f079..3c8790cdfd 100644 --- a/userdocs/theme/home.html +++ b/userdocs/theme/home.html @@ -541,6 +541,7 @@

eksctl create cluster

Usage Outposts

New for {{ build_date_utc.strftime('%Y') }}

+

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

Creating fully private clusters on AWS Outposts.

Supported Regions - Zurich (eu-central-2), Spain (eu-south-2), From 935f4aa26c5be83642368e9e4fc2948e042dff57 Mon Sep 17 00:00:00 2001 From: Tibi <110664232+TiberiuGC@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:46:44 +0200 Subject: [PATCH 3/3] update bottlerocket settings reference --- pkg/apis/eksctl.io/v1alpha5/assets/schema.json | 4 ++-- pkg/apis/eksctl.io/v1alpha5/types.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index 9af02753c7..6d282f647b 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -1776,8 +1776,8 @@ }, "settings": { "$ref": "#/definitions/InlineDocument", - "description": "contains any [bottlerocket settings](https://github.com/bottlerocket-os/bottlerocket/#description-of-settings)", - "x-intellij-html-description": "contains any bottlerocket settings" + "description": "contains any [bottlerocket settings](https://bottlerocket.dev/en/os/latest/#/api/settings/)", + "x-intellij-html-description": "contains any bottlerocket settings" } }, "preferredOrder": [ diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index c80e1bb576..33a2d85d47 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -1444,7 +1444,7 @@ type ( // +optional EnableAdminContainer *bool `json:"enableAdminContainer,omitempty"` // Settings contains any [bottlerocket - // settings](https://github.com/bottlerocket-os/bottlerocket/#description-of-settings) + // settings](https://bottlerocket.dev/en/os/latest/#/api/settings/) // +optional Settings *InlineDocument `json:"settings,omitempty"` }