diff --git a/integration/scripts/ginkgo-parallel-procs.sh b/integration/scripts/ginkgo-parallel-procs.sh index ea861397d8..0cb94d214a 100755 --- a/integration/scripts/ginkgo-parallel-procs.sh +++ b/integration/scripts/ginkgo-parallel-procs.sh @@ -12,6 +12,9 @@ case $1 in "windows") echo "${PARALLEL_PROCS}3" ;; + "pod_identity_associations") + echo "${PARALLEL_PROCS}2" + ;; *) echo "" ;; diff --git a/integration/tests/pod_identity_associations/pod_identity_associations_test.go b/integration/tests/pod_identity_associations/pod_identity_associations_test.go index 504b496018..69d17ba2e7 100644 --- a/integration/tests/pod_identity_associations/pod_identity_associations_test.go +++ b/integration/tests/pod_identity_associations/pod_identity_associations_test.go @@ -8,16 +8,18 @@ import ( "context" "encoding/json" "fmt" + "strings" "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "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/matchers" . "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" @@ -26,6 +28,10 @@ import ( ) const ( + clusterIRSAv1 = "iam-service-accounts" + clusterIRSAv2 = "pod-identity-associations" + + nsDefault = "default" nsInitial = "initial" nsCLI = "cli" nsConfigFile = "config-file" @@ -49,17 +55,18 @@ var ( 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") + params = tests.NewParamsWithGivenClusterName("", "test") } func TestPodIdentityAssociations(t *testing.T) { testutils.RegisterAndRun(t) } -var _ = BeforeSuite(func() { +var _ = SynchronizedBeforeSuite(func() []byte { var err error ctl, err = eks.New(context.TODO(), &api.ProviderConfig{Region: params.Region}, nil) Expect(err).NotTo(HaveOccurred()) + roleOutput, err := ctl.AWSProvider.IAM().CreateRole(context.Background(), &iam.CreateRoleInput{ RoleName: aws.String(initialRole1), AssumeRolePolicyDocument: trustPolicy, @@ -73,17 +80,138 @@ var _ = BeforeSuite(func() { }) Expect(err).NotTo(HaveOccurred()) role2ARN = *roleOutput.Role.Arn + + return []byte(role1ARN + "," + role2ARN) +}, func(arns []byte) { + roleARNs := strings.Split(string(arns), ",") + role1ARN, role2ARN = roleARNs[0], roleARNs[1] + + var err error + ctl, err = eks.New(context.TODO(), &api.ProviderConfig{Region: params.Region}, nil) + Expect(err).NotTo(HaveOccurred()) }) -var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() { +var _ = Describe("(Integration) [PodIdentityAssociations Test]", func() { - Context("Cluster with pod identity associations", func() { + Context("Cluster with iam service accounts", Ordered, func() { var ( cfg *api.ClusterConfig ) BeforeAll(func() { - cfg = makeClusterConfig() + cfg = makeClusterConfig(clusterIRSAv1) + }) + + It("should create a cluster with iam service accounts", func() { + cfg.IAM = &api.ClusterIAM{ + WithOIDC: aws.Bool(true), + ServiceAccounts: []*api.ClusterIAMServiceAccount{ + { + ClusterIAMMeta: api.ClusterIAMMeta{ + Name: sa1, + }, + AttachPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"}, + }, + { + ClusterIAMMeta: api.ClusterIAMMeta{ + Name: sa2, + }, + AttachRoleARN: 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()) + + awsConfig := NewConfig(params.Region) + stackNamePrefix := fmt.Sprintf("eksctl-%s-addon-iamserviceaccount-", clusterIRSAv1) + Expect(awsConfig).To(HaveExistingStack(stackNamePrefix + "default-service-account-1")) + }) + + It("should migrate to pod identity associations", func() { + Expect(params.EksctlUtilsCmd. + WithArgs( + "migrate-to-pod-identity", + "--cluster", clusterIRSAv1, + "--remove-oidc-provider-trust-relationship", + "--approve", + )).To(RunSuccessfully()) + }) + + It("should fetch all expected associations", func() { + var output []podidentityassociation.Summary + session := params.EksctlGetCmd. + WithArgs( + "podidentityassociation", + "--cluster", clusterIRSAv1, + "--output", "json", + ).Run() + Expect(session.ExitCode()).To(Equal(0)) + Expect(json.Unmarshal(session.Out.Contents(), &output)).To(Succeed()) + Expect(output).To(HaveLen(3)) + }) + + It("should not return any iam service accounts", func() { + Expect(params.EksctlGetCmd. + WithArgs( + "iamserviceaccount", + "--cluster", clusterIRSAv1, + )).To(RunSuccessfullyWithOutputStringLines(ContainElement("No iamserviceaccounts found"))) + }) + + It("should fail to update an owned migrated role", func() { + session := params.EksctlUpdateCmd. + WithArgs( + "podidentityassociation", + "--cluster", clusterIRSAv1, + "--namespace", nsDefault, + "--service-account-name", sa1, + "--role-arn", role1ARN, + ).Run() + Expect(session.ExitCode()).To(Equal(1)) + Expect(session.Err.Contents()).To(ContainSubstring("cannot change podIdentityAssociation.roleARN since the role was created by eksctl")) + }) + + It("should update an unowned migrated role", func() { + Expect(params.EksctlUpdateCmd. + WithArgs( + "podidentityassociation", + "--cluster", clusterIRSAv1, + "--namespace", nsDefault, + "--service-account-name", sa2, + "--role-arn", role1ARN, + ), + ).To(RunSuccessfully()) + }) + + It("should delete an owned migrated role", func() { + Expect(params.EksctlDeleteCmd. + WithArgs( + "podidentityassociation", + "--cluster", clusterIRSAv1, + "--namespace", nsDefault, + "--service-account-name", sa1, + )).To(RunSuccessfully()) + }) + }) + + Context("Cluster with pod identity associations", Ordered, func() { + var ( + cfg *api.ClusterConfig + ) + + BeforeAll(func() { + cfg = makeClusterConfig(clusterIRSAv2) }) It("should create a cluster with pod identity associations", func() { @@ -124,7 +252,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() session := params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--output", "json", ).Run() Expect(session.ExitCode()).To(Equal(0)) @@ -137,7 +265,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlCreateCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsInitial, "--service-account-name", sa1, "--role-arn", role1ARN, @@ -149,7 +277,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlCreateCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsCLI, "--service-account-name", sa1, "--well-known-policies", "certManager", @@ -194,7 +322,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() session := params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--output", "json", ).Run() Expect(session.ExitCode()).To(Equal(0)) @@ -207,7 +335,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() session := params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsConfigFile, "--output", "json", ).Run() @@ -221,7 +349,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() session := params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsConfigFile, "--service-account-name", sa1, "--output", "json", @@ -234,22 +362,23 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Context("Updating pod identity associations", func() { It("should fail to update an association with role created by eksctl", func() { - Expect(params.EksctlUpdateCmd. + session := params.EksctlUpdateCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsCLI, "--service-account-name", sa1, "--role-arn", role1ARN, - ), - ).NotTo(RunSuccessfully()) + ).Run() + Expect(session.ExitCode()).To(Equal(1)) + Expect(session.Err.Contents()).To(ContainSubstring("cannot change podIdentityAssociation.roleARN since the role was created by eksctl")) }) It("should update an association via CLI", func() { Expect(params.EksctlUpdateCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsInitial, "--service-account-name", sa1, "--role-arn", role2ARN, @@ -289,7 +418,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() session := params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsInitial, "--output", "json", ).Run() @@ -307,7 +436,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlDeleteCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsInitial, "--service-account-name", sa1, ), @@ -343,7 +472,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsInitial, )).To(RunSuccessfullyWithOutputStringLines(ContainElement("No podidentityassociations found"))) }) @@ -353,7 +482,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() BeforeAll(func() { _, err := ctl.AWSProvider.EKS().CreatePodIdentityAssociation(context.Background(), &awseks.CreatePodIdentityAssociationInput{ - ClusterName: ¶ms.ClusterName, + ClusterName: aws.String(clusterIRSAv2), Namespace: aws.String(nsUnowned), ServiceAccount: aws.String(sa1), RoleArn: &role1ARN, @@ -365,7 +494,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsUnowned, "--service-account-name", sa1, "--output", "json", @@ -379,7 +508,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlDeleteCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsUnowned, "--service-account-name", sa1, )).To(RunSuccessfully()) @@ -387,7 +516,7 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() Expect(params.EksctlGetCmd. WithArgs( "podidentityassociation", - "--cluster", params.ClusterName, + "--cluster", clusterIRSAv2, "--namespace", nsUnowned, "--service-account-name", sa1, )).To(RunSuccessfullyWithOutputStringLines(ContainElement("No podidentityassociations found"))) @@ -396,10 +525,19 @@ var _ = Describe("(Integration) [PodIdentityAssociations Test]", Ordered, func() }) }) -var _ = AfterSuite(func() { +var _ = SynchronizedAfterSuite(func() {}, func() { if ctl == nil { return } + + Expect(params.EksctlDeleteCmd.WithArgs( + "cluster", clusterIRSAv1, + )).To(RunSuccessfully()) + + Expect(params.EksctlDeleteCmd.WithArgs( + "cluster", clusterIRSAv2, + )).To(RunSuccessfully()) + _, err := ctl.AWSProvider.IAM().DeleteRole(context.Background(), &iam.DeleteRoleInput{ RoleName: aws.String(initialRole1), }) @@ -409,14 +547,12 @@ var _ = AfterSuite(func() { RoleName: aws.String(initialRole2), }) Expect(err).NotTo(HaveOccurred()) - - params.DeleteClusters() }) var ( - makeClusterConfig = func() *api.ClusterConfig { + makeClusterConfig = func(clusterName string) *api.ClusterConfig { cfg := api.NewClusterConfig() - cfg.Metadata.Name = params.ClusterName + cfg.Metadata.Name = clusterName cfg.Metadata.Version = params.Version cfg.Metadata.Region = params.Region return cfg diff --git a/pkg/actions/podidentityassociation/creator.go b/pkg/actions/podidentityassociation/creator.go index 210cf3cf31..099ce11de2 100644 --- a/pkg/actions/podidentityassociation/creator.go +++ b/pkg/actions/podidentityassociation/creator.go @@ -6,20 +6,27 @@ import ( api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/awsapi" + "github.com/weaveworks/eksctl/pkg/cfn/builder" "github.com/weaveworks/eksctl/pkg/utils/tasks" ) +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate +//counterfeiter:generate -o fakes/fake_stack_creator.go . StackCreator +type StackCreator interface { + CreateStack(ctx context.Context, name string, stack builder.ResourceSetReader, tags, parameters map[string]string, errs chan error) error +} + type Creator struct { clusterName string - stackManager StackManager + stackCreator StackCreator eksAPI awsapi.EKS } -func NewCreator(clusterName string, stackManager StackManager, eksAPI awsapi.EKS) *Creator { +func NewCreator(clusterName string, stackCreator StackCreator, eksAPI awsapi.EKS) *Creator { return &Creator{ clusterName: clusterName, - stackManager: stackManager, + stackCreator: stackCreator, eksAPI: eksAPI, } } @@ -40,15 +47,15 @@ func (c *Creator) CreateTasks(ctx context.Context, podIdentityAssociations []api if pia.RoleARN == "" { piaCreationTasks.Append(&createIAMRoleTask{ ctx: ctx, - info: fmt.Sprintf("create IAM role for pod identity association for service account %s in namespace %s", pia.ServiceAccountName, pia.Namespace), + info: fmt.Sprintf("create IAM role for pod identity association for service account %q", pia.NameString()), clusterName: c.clusterName, podIdentityAssociation: &podIdentityAssociations[i], - stackManager: c.stackManager, + stackCreator: c.stackCreator, }) } piaCreationTasks.Append(&createPodIdentityAssociationTask{ ctx: ctx, - info: fmt.Sprintf("create pod identity association for service account %s in namespace %s", pia.ServiceAccountName, pia.Namespace), + info: fmt.Sprintf("create pod identity association for service account %q", pia.NameString()), clusterName: c.clusterName, podIdentityAssociation: &podIdentityAssociations[i], eksAPI: c.eksAPI, diff --git a/pkg/actions/podidentityassociation/creator_test.go b/pkg/actions/podidentityassociation/creator_test.go index 78368983ff..cca1452186 100644 --- a/pkg/actions/podidentityassociation/creator_test.go +++ b/pkg/actions/podidentityassociation/creator_test.go @@ -19,7 +19,7 @@ import ( type createPodIdentityAssociationEntry struct { toBeCreated []api.PodIdentityAssociation mockEKS func(provider *mockprovider.MockProvider) - mockCFN func(stackManager *fakes.FakeStackManager) + mockCFN func(stackCreator *fakes.FakeStackCreator) expectedCreateStackCalls int expectedErr string } @@ -27,7 +27,7 @@ type createPodIdentityAssociationEntry struct { var _ = Describe("Create", func() { var ( creator *podidentityassociation.Creator - fakeStackManager *fakes.FakeStackManager + fakeStackCreator *fakes.FakeStackCreator mockProvider *mockprovider.MockProvider clusterName = "test-cluster" @@ -39,9 +39,9 @@ var _ = Describe("Create", func() { ) DescribeTable("Create", func(e createPodIdentityAssociationEntry) { - fakeStackManager = new(fakes.FakeStackManager) + fakeStackCreator = new(fakes.FakeStackCreator) if e.mockCFN != nil { - e.mockCFN(fakeStackManager) + e.mockCFN(fakeStackCreator) } mockProvider = mockprovider.NewMockProvider() @@ -49,7 +49,7 @@ var _ = Describe("Create", func() { e.mockEKS(mockProvider) } - creator = podidentityassociation.NewCreator(clusterName, fakeStackManager, mockProvider.MockEKS()) + creator = podidentityassociation.NewCreator(clusterName, fakeStackCreator, mockProvider.MockEKS()) err := creator.CreatePodIdentityAssociations(context.Background(), e.toBeCreated) if e.expectedErr != "" { @@ -57,7 +57,7 @@ var _ = Describe("Create", func() { return } Expect(err).ToNot(HaveOccurred()) - Expect(fakeStackManager.CreateStackCallCount()).To(Equal(e.expectedCreateStackCalls)) + Expect(fakeStackCreator.CreateStackCallCount()).To(Equal(e.expectedCreateStackCalls)) }, Entry("returns an error if creating the IAM role fails", createPodIdentityAssociationEntry{ toBeCreated: []api.PodIdentityAssociation{ @@ -66,8 +66,8 @@ var _ = Describe("Create", func() { ServiceAccountName: serviceAccountName1, }, }, - mockCFN: func(stackManager *fakes.FakeStackManager) { - stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + mockCFN: func(stackCreator *fakes.FakeStackCreator) { + stackCreator.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { defer close(c) Expect(s).To(Equal(podidentityassociation.MakeStackName( clusterName, @@ -123,8 +123,8 @@ var _ = Describe("Create", func() { Return(&awseks.CreatePodIdentityAssociationOutput{}, nil). Twice() }, - mockCFN: func(stackManager *fakes.FakeStackManager) { - stackManager.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { + mockCFN: func(stackCreator *fakes.FakeStackCreator) { + stackCreator.CreateStackStub = func(ctx context.Context, s string, rsr builder.ResourceSetReader, m1, m2 map[string]string, c chan error) error { defer close(c) return nil } diff --git a/pkg/actions/podidentityassociation/deleter.go b/pkg/actions/podidentityassociation/deleter.go index 960455e8f5..7ac88f0d0a 100644 --- a/pkg/actions/podidentityassociation/deleter.go +++ b/pkg/actions/podidentityassociation/deleter.go @@ -7,8 +7,6 @@ import ( 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" @@ -21,8 +19,9 @@ import ( // A StackLister lists and describes CloudFormation stacks. type StackLister interface { - ListStackNames(ctx context.Context, regExp string) ([]string, error) + ListPodIdentityStackNames(ctx context.Context) ([]string, error) DescribeStack(ctx context.Context, stack *manager.Stack) (*manager.Stack, error) + GetIAMServiceAccounts(ctx context.Context) ([]*api.ClusterIAMServiceAccount, error) } // A StackDeleter lists and deletes CloudFormation stacks. @@ -60,6 +59,18 @@ type Identifier struct { ServiceAccountName string } +func (i Identifier) IDString() string { + return i.toString("/") +} + +func (i Identifier) NameString() string { + return i.toString("-") +} + +func (i Identifier) toString(delimiter string) string { + return i.Namespace + delimiter + i.ServiceAccountName +} + func NewDeleter(clusterName string, stackDeleter StackDeleter, apiDeleter APIDeleter) *Deleter { return &Deleter{ ClusterName: clusterName, @@ -78,7 +89,7 @@ func (d *Deleter) Delete(ctx context.Context, podIDs []Identifier) error { } func (d *Deleter) DeleteTasks(ctx context.Context, podIDs []Identifier) (*tasks.TaskTree, error) { - roleStackNames, err := d.StackDeleter.ListStackNames(ctx, fmt.Sprintf("^%s*", makeStackNamePrefix(d.ClusterName))) + roleStackNames, err := d.StackDeleter.ListPodIdentityStackNames(ctx) if err != nil { return nil, fmt.Errorf("error listing stack names for pod identity associations: %w", err) } @@ -106,7 +117,7 @@ func (d *Deleter) DeleteTasks(ctx context.Context, podIDs []Identifier) (*tasks. } func (d *Deleter) makeDeleteTask(ctx context.Context, p Identifier, roleStackNames []string) tasks.Task { - podIdentityAssociationID := makeID(p.Namespace, p.ServiceAccountName) + podIdentityAssociationID := p.IDString() return &tasks.GenericTask{ Description: fmt.Sprintf("delete pod identity association %q", podIdentityAssociationID), Doer: func() error { @@ -128,10 +139,10 @@ func (d *Deleter) deletePodIdentityAssociation(ctx context.Context, p Identifier 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 0: + logger.Warning("pod identity association %q not found", podIdentityAssociationID) case 1: if _, err := d.APIDeleter.DeletePodIdentityAssociation(ctx, &eks.DeletePodIdentityAssociationInput{ ClusterName: aws.String(d.ClusterName), @@ -141,8 +152,8 @@ func (d *Deleter) deletePodIdentityAssociation(ctx context.Context, p Identifier } } - stackName := MakeStackName(d.ClusterName, p.Namespace, p.ServiceAccountName) - if !slices.Contains(roleStackNames, stackName) { + stackName, hasStack := getIAMResourcesStack(roleStackNames, p) + if !hasStack { return nil } logger.Info("deleting IAM resources stack %q for pod identity association %q", stackName, podIdentityAssociationID) @@ -181,6 +192,11 @@ func ToIdentifiers(podIdentityAssociations []api.PodIdentityAssociation) []Ident return identifiers } -func makeID(namespace, serviceAccountName string) string { - return fmt.Sprintf("%s/%s", namespace, serviceAccountName) +func getIAMResourcesStack(stackNames []string, p Identifier) (string, bool) { + for _, name := range stackNames { + if strings.Contains(name, p.NameString()) { + return name, true + } + } + return "", false } diff --git a/pkg/actions/podidentityassociation/deleter_test.go b/pkg/actions/podidentityassociation/deleter_test.go index 03d8a7f017..5b2c7e2177 100644 --- a/pkg/actions/podidentityassociation/deleter_test.go +++ b/pkg/actions/podidentityassociation/deleter_test.go @@ -41,7 +41,7 @@ var _ = Describe("Pod Identity Deleter", func() { } } mockCalls := func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS, podID podidentityassociation.Identifier) { - stackName := makeStackName(podID) + stackName := makeIRSAv2StackName(podID) associationID := fmt.Sprintf("%x", sha1.Sum([]byte(stackName))) mockListPodIdentityAssociations(eksAPI, podID, []ekstypes.PodIdentityAssociationSummary{ { @@ -90,7 +90,7 @@ var _ = Describe("Pod Identity Deleter", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(1)) eksAPI.AssertExpectations(GinkgoT()) }, @@ -118,15 +118,32 @@ var _ = Describe("Pod Identity Deleter", func() { ServiceAccountName: "aws-node", }, } - mockListStackNames(stackManager, podIDs) + mockListStackNamesWithIRSAv1(stackManager, podIDs[:1], podIDs[1:]) for _, podID := range podIDs { mockCalls(stackManager, eksAPI, podID) } }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(2)) + + var names []string + for i := 0; i < stackManager.DescribeStackCallCount(); i++ { + _, stack := stackManager.DescribeStackArgsForCall(i) + names = append(names, *stack.StackName) + } + Expect(names).To(ConsistOf( + makeIRSAv1StackName(podidentityassociation.Identifier{ + Namespace: "default", + ServiceAccountName: "default", + }), + makeIRSAv2StackName(podidentityassociation.Identifier{ + Namespace: "kube-system", + ServiceAccountName: "aws-node", + }), + )) + eksAPI.AssertExpectations(GinkgoT()) }, }), @@ -176,7 +193,7 @@ var _ = Describe("Pod Identity Deleter", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(3)) Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(3)) eksAPI.AssertExpectations(GinkgoT()) @@ -197,11 +214,11 @@ var _ = Describe("Pod Identity Deleter", func() { } mockListStackNames(stackManager, []podidentityassociation.Identifier{podID}) mockListPodIdentityAssociations(eksAPI, podID, nil, nil) - mockStackManager(stackManager, makeStackName(podID)) + mockStackManager(stackManager, makeIRSAv2StackName(podID)) }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(1)) Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(1)) eksAPI.AssertExpectations(GinkgoT()) @@ -237,7 +254,7 @@ var _ = Describe("Pod Identity Deleter", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(0)) eksAPI.AssertExpectations(GinkgoT()) @@ -261,11 +278,11 @@ var _ = Describe("Pod Identity Deleter", func() { ServiceAccountName: "default", }, } - mockListStackNames(stackManager, podIDs) + mockListStackNamesWithIRSAv1(stackManager, podIDs[:1], podIDs[1:]) mockStackManager(stackManager, "") }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(3)) Expect(stackManager.DeleteStackBySpecSyncCallCount()).To(Equal(3)) @@ -275,15 +292,15 @@ var _ = Describe("Pod Identity Deleter", func() { names = append(names, *stack.StackName) } Expect(names).To(ConsistOf( - makeStackName(podidentityassociation.Identifier{ + makeIRSAv1StackName(podidentityassociation.Identifier{ Namespace: "default", ServiceAccountName: "default", }), - makeStackName(podidentityassociation.Identifier{ + makeIRSAv2StackName(podidentityassociation.Identifier{ Namespace: "kube-system", ServiceAccountName: "default", }), - makeStackName(podidentityassociation.Identifier{ + makeIRSAv2StackName(podidentityassociation.Identifier{ Namespace: "kube-system", ServiceAccountName: "aws-node", }), @@ -291,5 +308,4 @@ var _ = Describe("Pod Identity Deleter", func() { }, }), ) - }) diff --git a/pkg/actions/podidentityassociation/fakes/fake_stack_manager.go b/pkg/actions/podidentityassociation/fakes/fake_stack_creator.go similarity index 84% rename from pkg/actions/podidentityassociation/fakes/fake_stack_manager.go rename to pkg/actions/podidentityassociation/fakes/fake_stack_creator.go index 82c63bf321..6dafa76f80 100644 --- a/pkg/actions/podidentityassociation/fakes/fake_stack_manager.go +++ b/pkg/actions/podidentityassociation/fakes/fake_stack_creator.go @@ -9,7 +9,7 @@ import ( "github.com/weaveworks/eksctl/pkg/cfn/builder" ) -type FakeStackManager struct { +type FakeStackCreator struct { CreateStackStub func(context.Context, string, builder.ResourceSetReader, map[string]string, map[string]string, chan error) error createStackMutex sync.RWMutex createStackArgsForCall []struct { @@ -30,7 +30,7 @@ type FakeStackManager struct { invocationsMutex sync.RWMutex } -func (fake *FakeStackManager) CreateStack(arg1 context.Context, arg2 string, arg3 builder.ResourceSetReader, arg4 map[string]string, arg5 map[string]string, arg6 chan error) error { +func (fake *FakeStackCreator) CreateStack(arg1 context.Context, arg2 string, arg3 builder.ResourceSetReader, arg4 map[string]string, arg5 map[string]string, arg6 chan error) error { fake.createStackMutex.Lock() ret, specificReturn := fake.createStackReturnsOnCall[len(fake.createStackArgsForCall)] fake.createStackArgsForCall = append(fake.createStackArgsForCall, struct { @@ -54,26 +54,26 @@ func (fake *FakeStackManager) CreateStack(arg1 context.Context, arg2 string, arg return fakeReturns.result1 } -func (fake *FakeStackManager) CreateStackCallCount() int { +func (fake *FakeStackCreator) CreateStackCallCount() int { fake.createStackMutex.RLock() defer fake.createStackMutex.RUnlock() return len(fake.createStackArgsForCall) } -func (fake *FakeStackManager) CreateStackCalls(stub func(context.Context, string, builder.ResourceSetReader, map[string]string, map[string]string, chan error) error) { +func (fake *FakeStackCreator) CreateStackCalls(stub func(context.Context, string, builder.ResourceSetReader, map[string]string, map[string]string, chan error) error) { fake.createStackMutex.Lock() defer fake.createStackMutex.Unlock() fake.CreateStackStub = stub } -func (fake *FakeStackManager) CreateStackArgsForCall(i int) (context.Context, string, builder.ResourceSetReader, map[string]string, map[string]string, chan error) { +func (fake *FakeStackCreator) CreateStackArgsForCall(i int) (context.Context, string, builder.ResourceSetReader, map[string]string, map[string]string, chan error) { fake.createStackMutex.RLock() defer fake.createStackMutex.RUnlock() argsForCall := fake.createStackArgsForCall[i] return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6 } -func (fake *FakeStackManager) CreateStackReturns(result1 error) { +func (fake *FakeStackCreator) CreateStackReturns(result1 error) { fake.createStackMutex.Lock() defer fake.createStackMutex.Unlock() fake.CreateStackStub = nil @@ -82,7 +82,7 @@ func (fake *FakeStackManager) CreateStackReturns(result1 error) { }{result1} } -func (fake *FakeStackManager) CreateStackReturnsOnCall(i int, result1 error) { +func (fake *FakeStackCreator) CreateStackReturnsOnCall(i int, result1 error) { fake.createStackMutex.Lock() defer fake.createStackMutex.Unlock() fake.CreateStackStub = nil @@ -96,7 +96,7 @@ func (fake *FakeStackManager) CreateStackReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeStackManager) Invocations() map[string][][]interface{} { +func (fake *FakeStackCreator) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() fake.createStackMutex.RLock() @@ -108,7 +108,7 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { return copiedInvocations } -func (fake *FakeStackManager) recordInvocation(key string, args []interface{}) { +func (fake *FakeStackCreator) recordInvocation(key string, args []interface{}) { fake.invocationsMutex.Lock() defer fake.invocationsMutex.Unlock() if fake.invocations == nil { @@ -120,4 +120,4 @@ func (fake *FakeStackManager) recordInvocation(key string, args []interface{}) { fake.invocations[key] = append(fake.invocations[key], args) } -var _ podidentityassociation.StackManager = new(FakeStackManager) +var _ podidentityassociation.StackCreator = new(FakeStackCreator) diff --git a/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go b/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go new file mode 100644 index 0000000000..1760dde011 --- /dev/null +++ b/pkg/actions/podidentityassociation/fakes/fake_stack_updater.go @@ -0,0 +1,356 @@ +// Code generated by counterfeiter. DO NOT EDIT. +package fakes + +import ( + "context" + "sync" + + "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" +) + +type FakeStackUpdater struct { + DescribeStackStub func(context.Context, *types.Stack) (*types.Stack, error) + describeStackMutex sync.RWMutex + describeStackArgsForCall []struct { + arg1 context.Context + arg2 *types.Stack + } + describeStackReturns struct { + result1 *types.Stack + result2 error + } + describeStackReturnsOnCall map[int]struct { + result1 *types.Stack + result2 error + } + GetIAMServiceAccountsStub func(context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error) + getIAMServiceAccountsMutex sync.RWMutex + getIAMServiceAccountsArgsForCall []struct { + arg1 context.Context + } + getIAMServiceAccountsReturns struct { + result1 []*v1alpha5.ClusterIAMServiceAccount + result2 error + } + getIAMServiceAccountsReturnsOnCall map[int]struct { + result1 []*v1alpha5.ClusterIAMServiceAccount + result2 error + } + ListPodIdentityStackNamesStub func(context.Context) ([]string, error) + listPodIdentityStackNamesMutex sync.RWMutex + listPodIdentityStackNamesArgsForCall []struct { + arg1 context.Context + } + listPodIdentityStackNamesReturns struct { + result1 []string + result2 error + } + listPodIdentityStackNamesReturnsOnCall map[int]struct { + result1 []string + result2 error + } + 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 + } + invocations map[string][][]interface{} + invocationsMutex sync.RWMutex +} + +func (fake *FakeStackUpdater) DescribeStack(arg1 context.Context, arg2 *types.Stack) (*types.Stack, error) { + fake.describeStackMutex.Lock() + ret, specificReturn := fake.describeStackReturnsOnCall[len(fake.describeStackArgsForCall)] + fake.describeStackArgsForCall = append(fake.describeStackArgsForCall, struct { + arg1 context.Context + arg2 *types.Stack + }{arg1, arg2}) + stub := fake.DescribeStackStub + fakeReturns := fake.describeStackReturns + fake.recordInvocation("DescribeStack", []interface{}{arg1, arg2}) + fake.describeStackMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackUpdater) DescribeStackCallCount() int { + fake.describeStackMutex.RLock() + defer fake.describeStackMutex.RUnlock() + return len(fake.describeStackArgsForCall) +} + +func (fake *FakeStackUpdater) DescribeStackCalls(stub func(context.Context, *types.Stack) (*types.Stack, error)) { + fake.describeStackMutex.Lock() + defer fake.describeStackMutex.Unlock() + fake.DescribeStackStub = stub +} + +func (fake *FakeStackUpdater) DescribeStackArgsForCall(i int) (context.Context, *types.Stack) { + fake.describeStackMutex.RLock() + defer fake.describeStackMutex.RUnlock() + argsForCall := fake.describeStackArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeStackUpdater) DescribeStackReturns(result1 *types.Stack, result2 error) { + fake.describeStackMutex.Lock() + defer fake.describeStackMutex.Unlock() + fake.DescribeStackStub = nil + fake.describeStackReturns = struct { + result1 *types.Stack + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) DescribeStackReturnsOnCall(i int, result1 *types.Stack, result2 error) { + fake.describeStackMutex.Lock() + defer fake.describeStackMutex.Unlock() + fake.DescribeStackStub = nil + if fake.describeStackReturnsOnCall == nil { + fake.describeStackReturnsOnCall = make(map[int]struct { + result1 *types.Stack + result2 error + }) + } + fake.describeStackReturnsOnCall[i] = struct { + result1 *types.Stack + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) GetIAMServiceAccounts(arg1 context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error) { + fake.getIAMServiceAccountsMutex.Lock() + ret, specificReturn := fake.getIAMServiceAccountsReturnsOnCall[len(fake.getIAMServiceAccountsArgsForCall)] + fake.getIAMServiceAccountsArgsForCall = append(fake.getIAMServiceAccountsArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.GetIAMServiceAccountsStub + fakeReturns := fake.getIAMServiceAccountsReturns + fake.recordInvocation("GetIAMServiceAccounts", []interface{}{arg1}) + fake.getIAMServiceAccountsMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackUpdater) GetIAMServiceAccountsCallCount() int { + fake.getIAMServiceAccountsMutex.RLock() + defer fake.getIAMServiceAccountsMutex.RUnlock() + return len(fake.getIAMServiceAccountsArgsForCall) +} + +func (fake *FakeStackUpdater) GetIAMServiceAccountsCalls(stub func(context.Context) ([]*v1alpha5.ClusterIAMServiceAccount, error)) { + fake.getIAMServiceAccountsMutex.Lock() + defer fake.getIAMServiceAccountsMutex.Unlock() + fake.GetIAMServiceAccountsStub = stub +} + +func (fake *FakeStackUpdater) GetIAMServiceAccountsArgsForCall(i int) context.Context { + fake.getIAMServiceAccountsMutex.RLock() + defer fake.getIAMServiceAccountsMutex.RUnlock() + argsForCall := fake.getIAMServiceAccountsArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStackUpdater) GetIAMServiceAccountsReturns(result1 []*v1alpha5.ClusterIAMServiceAccount, result2 error) { + fake.getIAMServiceAccountsMutex.Lock() + defer fake.getIAMServiceAccountsMutex.Unlock() + fake.GetIAMServiceAccountsStub = nil + fake.getIAMServiceAccountsReturns = struct { + result1 []*v1alpha5.ClusterIAMServiceAccount + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) GetIAMServiceAccountsReturnsOnCall(i int, result1 []*v1alpha5.ClusterIAMServiceAccount, result2 error) { + fake.getIAMServiceAccountsMutex.Lock() + defer fake.getIAMServiceAccountsMutex.Unlock() + fake.GetIAMServiceAccountsStub = nil + if fake.getIAMServiceAccountsReturnsOnCall == nil { + fake.getIAMServiceAccountsReturnsOnCall = make(map[int]struct { + result1 []*v1alpha5.ClusterIAMServiceAccount + result2 error + }) + } + fake.getIAMServiceAccountsReturnsOnCall[i] = struct { + result1 []*v1alpha5.ClusterIAMServiceAccount + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) ListPodIdentityStackNames(arg1 context.Context) ([]string, error) { + fake.listPodIdentityStackNamesMutex.Lock() + ret, specificReturn := fake.listPodIdentityStackNamesReturnsOnCall[len(fake.listPodIdentityStackNamesArgsForCall)] + fake.listPodIdentityStackNamesArgsForCall = append(fake.listPodIdentityStackNamesArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.ListPodIdentityStackNamesStub + fakeReturns := fake.listPodIdentityStackNamesReturns + fake.recordInvocation("ListPodIdentityStackNames", []interface{}{arg1}) + fake.listPodIdentityStackNamesMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackUpdater) ListPodIdentityStackNamesCallCount() int { + fake.listPodIdentityStackNamesMutex.RLock() + defer fake.listPodIdentityStackNamesMutex.RUnlock() + return len(fake.listPodIdentityStackNamesArgsForCall) +} + +func (fake *FakeStackUpdater) ListPodIdentityStackNamesCalls(stub func(context.Context) ([]string, error)) { + fake.listPodIdentityStackNamesMutex.Lock() + defer fake.listPodIdentityStackNamesMutex.Unlock() + fake.ListPodIdentityStackNamesStub = stub +} + +func (fake *FakeStackUpdater) ListPodIdentityStackNamesArgsForCall(i int) context.Context { + fake.listPodIdentityStackNamesMutex.RLock() + defer fake.listPodIdentityStackNamesMutex.RUnlock() + argsForCall := fake.listPodIdentityStackNamesArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStackUpdater) ListPodIdentityStackNamesReturns(result1 []string, result2 error) { + fake.listPodIdentityStackNamesMutex.Lock() + defer fake.listPodIdentityStackNamesMutex.Unlock() + fake.ListPodIdentityStackNamesStub = nil + fake.listPodIdentityStackNamesReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) ListPodIdentityStackNamesReturnsOnCall(i int, result1 []string, result2 error) { + fake.listPodIdentityStackNamesMutex.Lock() + defer fake.listPodIdentityStackNamesMutex.Unlock() + fake.ListPodIdentityStackNamesStub = nil + if fake.listPodIdentityStackNamesReturnsOnCall == nil { + fake.listPodIdentityStackNamesReturnsOnCall = make(map[int]struct { + result1 []string + result2 error + }) + } + fake.listPodIdentityStackNamesReturnsOnCall[i] = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeStackUpdater) 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 *FakeStackUpdater) MustUpdateStackCallCount() int { + fake.mustUpdateStackMutex.RLock() + defer fake.mustUpdateStackMutex.RUnlock() + return len(fake.mustUpdateStackArgsForCall) +} + +func (fake *FakeStackUpdater) MustUpdateStackCalls(stub func(context.Context, manager.UpdateStackOptions) error) { + fake.mustUpdateStackMutex.Lock() + defer fake.mustUpdateStackMutex.Unlock() + fake.MustUpdateStackStub = stub +} + +func (fake *FakeStackUpdater) 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 *FakeStackUpdater) MustUpdateStackReturns(result1 error) { + fake.mustUpdateStackMutex.Lock() + defer fake.mustUpdateStackMutex.Unlock() + fake.MustUpdateStackStub = nil + fake.mustUpdateStackReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeStackUpdater) 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 *FakeStackUpdater) Invocations() map[string][][]interface{} { + fake.invocationsMutex.RLock() + defer fake.invocationsMutex.RUnlock() + fake.describeStackMutex.RLock() + defer fake.describeStackMutex.RUnlock() + fake.getIAMServiceAccountsMutex.RLock() + defer fake.getIAMServiceAccountsMutex.RUnlock() + fake.listPodIdentityStackNamesMutex.RLock() + defer fake.listPodIdentityStackNamesMutex.RUnlock() + fake.mustUpdateStackMutex.RLock() + defer fake.mustUpdateStackMutex.RUnlock() + copiedInvocations := map[string][][]interface{}{} + for key, value := range fake.invocations { + copiedInvocations[key] = value + } + return copiedInvocations +} + +func (fake *FakeStackUpdater) recordInvocation(key string, args []interface{}) { + fake.invocationsMutex.Lock() + defer fake.invocationsMutex.Unlock() + if fake.invocations == nil { + fake.invocations = map[string][][]interface{}{} + } + if fake.invocations[key] == nil { + fake.invocations[key] = [][]interface{}{} + } + fake.invocations[key] = append(fake.invocations[key], args) +} + +var _ podidentityassociation.StackUpdater = new(FakeStackUpdater) diff --git a/pkg/actions/podidentityassociation/migrator.go b/pkg/actions/podidentityassociation/migrator.go new file mode 100644 index 0000000000..6144eced9f --- /dev/null +++ b/pkg/actions/podidentityassociation/migrator.go @@ -0,0 +1,245 @@ +package podidentityassociation + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/aws/aws-sdk-go-v2/aws" + awseks "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + "github.com/kris-nova/logger" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/awsapi" + "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" + "github.com/weaveworks/eksctl/pkg/kubernetes" + "github.com/weaveworks/eksctl/pkg/utils/tasks" +) + +type AddonCreator interface { + Create(ctx context.Context, addon *api.Addon, waitTimeout time.Duration) error +} + +type PodIdentityMigrationOptions struct { + RemoveOIDCProviderTrustRelationship bool + // SkipAgentInstallation bool + Approve bool + Timeout time.Duration +} + +type Migrator struct { + clusterName string + eksAPI awsapi.EKS + iamAPI awsapi.IAM + stackUpdater StackUpdater + clientSet kubernetes.Interface + addonCreator AddonCreator +} + +func NewMigrator( + clusterName string, + eksAPI awsapi.EKS, + iamAPI awsapi.IAM, + stackUpdater StackUpdater, + clientSet kubernetes.Interface, + addonCreator AddonCreator, +) *Migrator { + return &Migrator{ + clusterName: clusterName, + eksAPI: eksAPI, + iamAPI: iamAPI, + stackUpdater: stackUpdater, + clientSet: clientSet, + addonCreator: addonCreator, + } +} + +func (m *Migrator) MigrateToPodIdentity(ctx context.Context, options PodIdentityMigrationOptions) error { + taskTree := tasks.TaskTree{ + Parallel: false, + PlanMode: !options.Approve, + } + + // add task to install the pod identity agent addon + isInstalled, err := IsPodIdentityAgentInstalled(ctx, m.eksAPI, m.clusterName) + if err != nil { + return err + } + if !isInstalled { + taskTree.Append(&tasks.GenericTask{ + Description: fmt.Sprintf("install %s addon", api.PodIdentityAgentAddon), + Doer: func() error { + return m.addonCreator.Create(ctx, &api.Addon{Name: api.PodIdentityAgentAddon}, options.Timeout) + }, + }) + } + + /* + add tasks to: + update trust policies for IRSAv1 roles + AND + remove IRSAv1 annotation from service accounts + */ + resolver := IRSAv1StackNameResolver{} + if err := resolver.Populate(func() ([]*api.ClusterIAMServiceAccount, error) { + return m.stackUpdater.GetIAMServiceAccounts(ctx) + }); err != nil { + return err + } + + serviceAccounts, err := m.clientSet.CoreV1().ServiceAccounts("").List(ctx, metav1.ListOptions{}) + if err != nil { + return fmt.Errorf("listing k8s service accounts: %w", err) + } + + updateTrustPolicyTasks := tasks.TaskTree{ + Parallel: true, + IsSubTask: true, + } + removeIRSAv1AnnotationTasks := tasks.TaskTree{ + Parallel: true, + IsSubTask: true, + } + toBeCreated := []api.PodIdentityAssociation{} + for _, sa := range serviceAccounts.Items { + if roleARN, ok := sa.Annotations[api.AnnotationEKSRoleARN]; ok { + // collect pod identity associations that need to be created + toBeCreated = append(toBeCreated, api.PodIdentityAssociation{ + ServiceAccountName: sa.Name, + Namespace: sa.Namespace, + RoleARN: roleARN, + }) + + // infer role name to use in IAM API inputs + roleName, err := getNameFromARN(roleARN) + if err != nil { + return err + } + + // add updateTrustPolicyTasks + if stackSummary, hasStack := resolver.GetStack(roleARN); hasStack { + updateTrustPolicyTasks.Append(&updateTrustPolicyForOwnedRole{ + ctx: ctx, + info: fmt.Sprintf("update trust policy for owned role %q", roleName), + roleName: roleName, + stack: stackSummary, + removeOIDCProviderTrustRelationship: options.RemoveOIDCProviderTrustRelationship, + iamAPI: m.iamAPI, + stackUpdater: m.stackUpdater, + }) + + } else { + updateTrustPolicyTasks.Append(&updateTrustPolicyForUnownedRole{ + ctx: ctx, + info: fmt.Sprintf("update trust policy for unowned role %q", roleName), + roleName: roleName, + removeOIDCProviderTrustRelationship: options.RemoveOIDCProviderTrustRelationship, + iamAPI: m.iamAPI, + }) + } + + // add removeIRSAv1AnnotationTasks + if !options.RemoveOIDCProviderTrustRelationship { + continue + } + + saNameString := sa.Namespace + "/" + sa.Name + saCopy := &corev1.ServiceAccount{ + ObjectMeta: sa.ObjectMeta, + } + removeIRSAv1AnnotationTasks.Append(&tasks.GenericTask{ + Description: fmt.Sprintf("remove iamserviceaccount EKS role annotation for %q", saNameString), + Doer: func() error { + delete(saCopy.Annotations, api.AnnotationEKSRoleARN) + _, err := m.clientSet.CoreV1().ServiceAccounts(saCopy.Namespace).Update(ctx, saCopy, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("updating serviceaccount %q: %w", saNameString, err) + } + logger.Info("removed iamserviceaccount annotation with key %q for %q", api.AnnotationEKSRoleARN, saNameString) + return nil + }, + }) + } + } + if updateTrustPolicyTasks.Len() == 0 { + logger.Info("no iamserviceacconts found, there is no need to migrate to pod identity") + return nil + } + taskTree.Append(&updateTrustPolicyTasks) + if removeIRSAv1AnnotationTasks.Len() > 0 { + taskTree.Append(&removeIRSAv1AnnotationTasks) + } + + // add tasks to create pod identity associations + createAssociationsTasks := NewCreator(m.clusterName, nil, m.eksAPI).CreateTasks(ctx, toBeCreated) + if createAssociationsTasks.Len() > 0 { + createAssociationsTasks.IsSubTask = true + taskTree.Append(createAssociationsTasks) + } + + // add suggestive logs + cmdutils.LogIntendedAction(taskTree.PlanMode, "migrate %d iamserviceaccount(s) to pod identity association(s) by executing the following tasks", len(toBeCreated)) + defer cmdutils.LogPlanModeWarning(taskTree.PlanMode) + + return runAllTasks(&taskTree) +} + +func IsPodIdentityAgentInstalled(ctx context.Context, eksAPI awsapi.EKS, clusterName string) (bool, error) { + if _, err := eksAPI.DescribeAddon(ctx, &awseks.DescribeAddonInput{ + AddonName: aws.String(api.PodIdentityAgentAddon), + ClusterName: &clusterName, + }); err != nil { + var notFoundErr *ekstypes.ResourceNotFoundException + if errors.As(err, ¬FoundErr) { + return false, nil + } + return false, fmt.Errorf("calling %q: %w", fmt.Sprintf("EKS::DescribeAddon::%s", api.PodIdentityAgentAddon), err) + } + return true, nil +} + +func getNameFromARN(roleARN string) (string, error) { + parts := strings.Split(roleARN, "/") + if len(parts) != 2 { + return "", fmt.Errorf("cannot parse role name from roleARN: %s", roleARN) + } + return parts[1], nil +} + +type IRSAv1StackNameResolver map[string]IRSAv1StackSummary + +type IRSAv1StackSummary struct { + Name string + Tags map[string]string + Capabilities []string +} + +func (r *IRSAv1StackNameResolver) Populate( + getIAMServiceAccounts func() ([]*api.ClusterIAMServiceAccount, error), +) error { + serviceAccounts, err := getIAMServiceAccounts() + if err != nil { + return fmt.Errorf("getting iamserviceaccount role stacks: %w", err) + } + for _, sa := range serviceAccounts { + (*r)[*sa.Status.RoleARN] = IRSAv1StackSummary{ + Name: *sa.Status.StackName, + Tags: sa.Status.Tags, + Capabilities: sa.Status.Capabilities, + } + } + return nil +} + +func (r *IRSAv1StackNameResolver) GetStack(roleARN string) (IRSAv1StackSummary, bool) { + if stack, ok := (*r)[roleARN]; ok { + return stack, true + } + return IRSAv1StackSummary{}, false +} diff --git a/pkg/actions/podidentityassociation/migrator_test.go b/pkg/actions/podidentityassociation/migrator_test.go new file mode 100644 index 0000000000..aca95f588b --- /dev/null +++ b/pkg/actions/podidentityassociation/migrator_test.go @@ -0,0 +1,303 @@ +package podidentityassociation_test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + + "github.com/kris-nova/logger" + "github.com/stretchr/testify/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "k8s.io/client-go/kubernetes/fake" + + "github.com/aws/aws-sdk-go-v2/aws" + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + awseks "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + awsiam "github.com/aws/aws-sdk-go-v2/service/iam" + iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/weaveworks/eksctl/pkg/actions/addon" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation/fakes" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" +) + +type migrateToPodIdentityAssociationEntry struct { + mockEKS func(provider *mockprovider.MockProvider) + mockCFN func(stackUpdater *fakes.FakeStackUpdater) + mockK8s func(clientSet *fake.Clientset) + validateCustomLoggerOutput func(output string) + options podidentityassociation.PodIdentityMigrationOptions + expectedErr string +} + +var _ = Describe("Create", func() { + var ( + migrator *podidentityassociation.Migrator + mockProvider *mockprovider.MockProvider + fakeStackUpdater *fakes.FakeStackUpdater + fakeClientset *fake.Clientset + + clusterName = "test-cluster" + nsDefault = "default" + sa1 = "service-account-1" + sa2 = "service-account-2" + + roleARN1 = "arn:aws:iam::111122223333:role/test-role-1" + roleARN2 = "arn:aws:iam::111122223333:role/test-role-2" + genericErr = fmt.Errorf("ERR") + ) + + var policyDocument = aws.String(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-west-2.amazonaws.com/id/test" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "oidc.eks.eu-north-1.amazonaws.com/id/test:sub": "system:serviceaccount:default:service-account-1", + "oidc.eks.eu-north-1.amazonaws.com/id/test:aud": "sts.amazonaws.com" + } + } + } + ] + }`) + + mockDescribeAddon := func(provider *mockprovider.MockProvider, err error) { + mockProvider.MockEKS(). + On("DescribeAddon", mock.Anything, mock.Anything). + Return(nil, err). + Once() + } + + createFakeServiceAccount := func(clientSet *fake.Clientset, namespace, serviceAccountName, roleARN string) { + objMeta := metav1.ObjectMeta{ + Namespace: namespace, + Name: serviceAccountName, + } + if roleARN != "" { + objMeta.Annotations = make(map[string]string) + objMeta.Annotations[api.AnnotationEKSRoleARN] = roleARN + } + _, err := clientSet.CoreV1().ServiceAccounts(namespace).Create(context.Background(), + &corev1.ServiceAccount{ObjectMeta: objMeta}, metav1.CreateOptions{}) + Expect(err).NotTo(HaveOccurred()) + } + + DescribeTable("Create", func(e migrateToPodIdentityAssociationEntry) { + fakeStackUpdater = new(fakes.FakeStackUpdater) + if e.mockCFN != nil { + e.mockCFN(fakeStackUpdater) + } + + mockProvider = mockprovider.NewMockProvider() + if e.mockEKS != nil { + e.mockEKS(mockProvider) + } + + fakeClientset = fake.NewSimpleClientset() + if e.mockK8s != nil { + e.mockK8s(fakeClientset) + } + + output := &bytes.Buffer{} + if e.validateCustomLoggerOutput != nil { + defer func() { + logger.Writer = os.Stdout + }() + logger.Writer = output + } + + addonCreator, err := addon.New(api.NewClusterConfig(), mockProvider.MockEKS(), nil, false, nil, nil) + Expect(err).NotTo(HaveOccurred()) + + migrator = podidentityassociation.NewMigrator( + clusterName, + mockProvider.MockEKS(), + mockProvider.MockIAM(), + fakeStackUpdater, + fakeClientset, + addonCreator, + ) + + err = migrator.MigrateToPodIdentity(context.Background(), e.options) + if e.expectedErr != "" { + Expect(err).To(MatchError(ContainSubstring(e.expectedErr))) + return + } + Expect(err).ToNot(HaveOccurred()) + + if e.validateCustomLoggerOutput != nil { + e.validateCustomLoggerOutput(output.String()) + } + }, + Entry("[API errors] describing pod identity agent addon fails", migrateToPodIdentityAssociationEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider, genericErr) + }, + expectedErr: fmt.Sprintf("calling %q", fmt.Sprintf("EKS::DescribeAddon::%s", api.PodIdentityAgentAddon)), + }), + + Entry("[API errors] fetching iamserviceaccounts fails", migrateToPodIdentityAssociationEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider, nil) + }, + mockCFN: func(stackUpdater *fakes.FakeStackUpdater) { + stackUpdater.GetIAMServiceAccountsReturns(nil, genericErr) + }, + expectedErr: "getting iamserviceaccount role stacks", + }), + + Entry("[taskTree] contains a task to create pod identity agent addon if not already installed", migrateToPodIdentityAssociationEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider, &ekstypes.ResourceNotFoundException{ + Message: aws.String(genericErr.Error()), + }) + }, + mockCFN: func(stackUpdater *fakes.FakeStackUpdater) { + stackUpdater.GetIAMServiceAccountsReturns([]*api.ClusterIAMServiceAccount{}, nil) + }, + mockK8s: func(clientSet *fake.Clientset) { + createFakeServiceAccount(clientSet, nsDefault, sa1, roleARN1) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring(fmt.Sprintf("install %s addon", api.PodIdentityAgentAddon))) + }, + }), + + Entry("[taskTree] contains tasks to remove IRSAv1 EKS Role annotation if remove trust option is specified", migrateToPodIdentityAssociationEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider, nil) + }, + mockCFN: func(stackUpdater *fakes.FakeStackUpdater) { + stackUpdater.GetIAMServiceAccountsReturns([]*api.ClusterIAMServiceAccount{}, nil) + }, + mockK8s: func(clientSet *fake.Clientset) { + createFakeServiceAccount(clientSet, nsDefault, sa1, roleARN1) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("remove iamserviceaccount EKS role annotation for \"default/service-account-1\"")) + }, + options: podidentityassociation.PodIdentityMigrationOptions{ + RemoveOIDCProviderTrustRelationship: true, + }, + }), + + Entry("[taskTree] contains all other expected tasks", migrateToPodIdentityAssociationEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider, nil) + }, + mockCFN: func(stackUpdater *fakes.FakeStackUpdater) { + stackUpdater.GetIAMServiceAccountsReturns([]*api.ClusterIAMServiceAccount{ + { + Status: &api.ClusterIAMServiceAccountStatus{ + RoleARN: aws.String(roleARN1), + StackName: aws.String(makeIRSAv2StackName(podidentityassociation.Identifier{ + Namespace: nsDefault, + ServiceAccountName: sa1, + })), + }, + }, + }, nil) + }, + mockK8s: func(clientSet *fake.Clientset) { + createFakeServiceAccount(clientSet, nsDefault, sa1, roleARN1) + createFakeServiceAccount(clientSet, nsDefault, sa2, roleARN2) + }, + validateCustomLoggerOutput: func(output string) { + Expect(output).To(ContainSubstring("update trust policy for owned role \"test-role-1\"")) + Expect(output).To(ContainSubstring("update trust policy for unowned role \"test-role-2\"")) + Expect(output).To(ContainSubstring("create pod identity association for service account \"default/service-account-1\"")) + Expect(output).To(ContainSubstring("create pod identity association for service account \"default/service-account-2\"")) + }, + }), + + Entry("completes all tasks successfully", migrateToPodIdentityAssociationEntry{ + mockEKS: func(provider *mockprovider.MockProvider) { + mockDescribeAddon(provider, nil) + + mockProvider.MockEKS(). + On("CreatePodIdentityAssociation", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awseks.CreatePodIdentityAssociationInput{})) + }). + Return(nil, nil). + Twice() + + mockProvider.MockIAM(). + On("GetRole", mock.Anything, mock.Anything). + Return(&awsiam.GetRoleOutput{ + Role: &iamtypes.Role{ + AssumeRolePolicyDocument: policyDocument, + }, + }, nil). + Twice() + + mockProvider.MockIAM(). + On("UpdateAssumeRolePolicy", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + Expect(args).To(HaveLen(2)) + Expect(args[1]).To(BeAssignableToTypeOf(&awsiam.UpdateAssumeRolePolicyInput{})) + input := args[1].(*awsiam.UpdateAssumeRolePolicyInput) + + var trustPolicy api.IAMPolicyDocument + Expect(json.Unmarshal([]byte(*input.PolicyDocument), &trustPolicy)).NotTo(HaveOccurred()) + Expect(trustPolicy.Statements).To(HaveLen(1)) + value, exists := trustPolicy.Statements[0].Principal["Service"] + Expect(exists).To(BeTrue()) + Expect(value).To(ConsistOf([]string{api.EKSServicePrincipal})) + }). + Return(nil, nil). + Once() + }, + mockCFN: func(stackUpdater *fakes.FakeStackUpdater) { + stackUpdater.GetIAMServiceAccountsReturns([]*api.ClusterIAMServiceAccount{ + { + Status: &api.ClusterIAMServiceAccountStatus{ + RoleARN: aws.String(roleARN1), + StackName: aws.String(makeIRSAv1StackName(podidentityassociation.Identifier{ + Namespace: nsDefault, + ServiceAccountName: sa1, + })), + Capabilities: []string{"CAPABILITY_IAM"}, + }, + }, + }, nil) + + stackUpdater.MustUpdateStackStub = func(ctx context.Context, options manager.UpdateStackOptions) error { + Expect(options.Stack).NotTo(BeNil()) + Expect(options.Stack.Tags).To(ConsistOf([]cfntypes.Tag{ + { + Key: aws.String(api.PodIdentityAssociationNameTag), + Value: aws.String(nsDefault + "/" + sa1), + }, + })) + Expect(options.Stack.Capabilities).To(ConsistOf([]cfntypes.Capability{"CAPABILITY_IAM"})) + return nil + } + }, + mockK8s: func(clientSet *fake.Clientset) { + createFakeServiceAccount(clientSet, nsDefault, sa1, roleARN1) + createFakeServiceAccount(clientSet, nsDefault, sa2, roleARN2) + }, + options: podidentityassociation.PodIdentityMigrationOptions{ + RemoveOIDCProviderTrustRelationship: true, + Approve: true, + }, + }), + ) +}) diff --git a/pkg/actions/podidentityassociation/tasks.go b/pkg/actions/podidentityassociation/tasks.go index e694225940..a130bea456 100644 --- a/pkg/actions/podidentityassociation/tasks.go +++ b/pkg/actions/podidentityassociation/tasks.go @@ -2,30 +2,31 @@ package podidentityassociation import ( "context" + "encoding/json" "fmt" + "net/url" "strings" + "time" + "github.com/aws/aws-sdk-go-v2/aws" + cfntypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" awseks "github.com/aws/aws-sdk-go-v2/service/eks" + awsiam "github.com/aws/aws-sdk-go-v2/service/iam" "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/awsapi" "github.com/weaveworks/eksctl/pkg/cfn/builder" + "github.com/weaveworks/eksctl/pkg/cfn/manager" "github.com/weaveworks/eksctl/pkg/utils/tasks" ) -//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate -//counterfeiter:generate -o fakes/fake_stack_manager.go . StackManager -type StackManager interface { - CreateStack(ctx context.Context, name string, stack builder.ResourceSetReader, tags, parameters map[string]string, errs chan error) error -} - type createIAMRoleTask struct { ctx context.Context info string clusterName string podIdentityAssociation *api.PodIdentityAssociation - stackManager StackManager + stackCreator StackCreator } func (t *createIAMRoleTask) Describe() string { @@ -37,12 +38,16 @@ func (t *createIAMRoleTask) Do(errorCh chan error) error { if err := rs.AddAllResources(); err != nil { return err } - if err := t.stackManager.CreateStack(t.ctx, - MakeStackName( - t.clusterName, - t.podIdentityAssociation.Namespace, - t.podIdentityAssociation.ServiceAccountName), - rs, nil, nil, errorCh); err != nil { + if t.podIdentityAssociation.Tags == nil { + t.podIdentityAssociation.Tags = make(map[string]string) + } + t.podIdentityAssociation.Tags[api.PodIdentityAssociationNameTag] = Identifier{ + Namespace: t.podIdentityAssociation.Namespace, + ServiceAccountName: t.podIdentityAssociation.ServiceAccountName, + }.IDString() + + stackName := MakeStackName(t.clusterName, t.podIdentityAssociation.Namespace, t.podIdentityAssociation.ServiceAccountName) + if err := t.stackCreator.CreateStack(t.ctx, stackName, rs, t.podIdentityAssociation.Tags, nil, errorCh); err != nil { return fmt.Errorf("creating IAM role for pod identity association for service account %s in namespace %s: %w", t.podIdentityAssociation.ServiceAccountName, t.podIdentityAssociation.Namespace, err) } @@ -72,20 +77,182 @@ func (t *createPodIdentityAssociationTask) Do(errorCh chan error) error { Tags: t.podIdentityAssociation.Tags, }); err != nil { return fmt.Errorf( - "creating pod identity association for service account %s in namespace %s: %w", + "creating pod identity association for service account %q in namespace %q: %w", t.podIdentityAssociation.ServiceAccountName, t.podIdentityAssociation.Namespace, err) } + logger.Info(fmt.Sprintf("created pod identity association for service account %q in namespace %q", + t.podIdentityAssociation.ServiceAccountName, t.podIdentityAssociation.Namespace)) + return nil +} + +type updateTrustPolicyForOwnedRole struct { + ctx context.Context + info string + roleName string + stack IRSAv1StackSummary + removeOIDCProviderTrustRelationship bool + iamAPI awsapi.IAM + stackUpdater StackUpdater +} + +func (t *updateTrustPolicyForOwnedRole) Describe() string { + return t.info +} + +func (t *updateTrustPolicyForOwnedRole) Do(errorCh chan error) error { + defer close(errorCh) + + trustStatements, err := updateTrustStatements(t.removeOIDCProviderTrustRelationship, func() (*awsiam.GetRoleOutput, error) { + return t.iamAPI.GetRole(t.ctx, &awsiam.GetRoleInput{RoleName: &t.roleName}) + }) + if err != nil { + return fmt.Errorf("updating trust statements for role %s: %w", t.roleName, err) + } + + // build template for updating trust policy + rs := builder.NewIAMRoleResourceSetForPodIdentityWithTrustStatements(&api.PodIdentityAssociation{}, trustStatements) + if err := rs.AddAllResources(); err != nil { + return fmt.Errorf("adding resources to CloudFormation template: %w", err) + } + template, err := rs.RenderJSON() + if err != nil { + return fmt.Errorf("generating CloudFormation template: %w", err) + } + + // update stack tags to reflect migration to IRSAv2 + cfnTags := []cfntypes.Tag{} + for key, value := range t.stack.Tags { + if key == api.IAMServiceAccountNameTag && t.removeOIDCProviderTrustRelationship { + continue + } + cfnTags = append(cfnTags, cfntypes.Tag{ + Key: &key, + Value: &value, + }) + } + getIAMServiceAccountName := func() string { + return strings.Replace(strings.Split(t.stack.Name, "-iamserviceaccount-")[1], "-", "/", 1) + } + cfnTags = append(cfnTags, cfntypes.Tag{ + Key: aws.String(api.PodIdentityAssociationNameTag), + Value: aws.String(getIAMServiceAccountName()), + }) + + // propagate capabilities + cfnCapabilities := []cfntypes.Capability{} + for _, c := range t.stack.Capabilities { + cfnCapabilities = append(cfnCapabilities, cfntypes.Capability(c)) + } + + if err := t.stackUpdater.MustUpdateStack(t.ctx, manager.UpdateStackOptions{ + Stack: &cfntypes.Stack{ + StackName: &t.stack.Name, + Tags: cfnTags, + Capabilities: cfnCapabilities, + }, + ChangeSetName: fmt.Sprintf("eksctl-%s-update-%d", t.roleName, time.Now().Unix()), + Description: fmt.Sprintf("updating IAM resources stack %q for role %q", t.stack.Name, t.roleName), + TemplateData: manager.TemplateBody(template), + Wait: true, + }); err != nil { + if _, ok := err.(*manager.NoChangeError); ok { + logger.Info("IAM resources for role %q are already up-to-date", t.roleName) + return nil + } + return fmt.Errorf("updating IAM resources for role %q: %w", t.roleName, err) + } + logger.Info("updated IAM resources stack %q for role %q", t.stack.Name, t.roleName) + + return nil +} + +type updateTrustPolicyForUnownedRole struct { + ctx context.Context + info string + roleName string + iamAPI awsapi.IAM + removeOIDCProviderTrustRelationship bool +} + +func (t *updateTrustPolicyForUnownedRole) Describe() string { + return t.info +} + +func (t *updateTrustPolicyForUnownedRole) Do(errorCh chan error) error { + defer close(errorCh) + + trustStatements, err := updateTrustStatements(t.removeOIDCProviderTrustRelationship, func() (*awsiam.GetRoleOutput, error) { + return t.iamAPI.GetRole(t.ctx, &awsiam.GetRoleInput{RoleName: &t.roleName}) + }) + if err != nil { + return fmt.Errorf("updating trust statements for role %s: %w", t.roleName, err) + } + + documentString, err := json.Marshal(api.IAMPolicyDocument{ + Version: "2012-10-17", + Statements: trustStatements, + }) + if err != nil { + return fmt.Errorf("marshalling trust policy document: %w", err) + } + + if _, err := t.iamAPI.UpdateAssumeRolePolicy(t.ctx, &awsiam.UpdateAssumeRolePolicyInput{ + RoleName: &t.roleName, + PolicyDocument: aws.String(string(documentString)), + }); err != nil { + return fmt.Errorf("updating trust policy for role %s: %w", t.roleName, err) + } + logger.Info(fmt.Sprintf("updated trust policy for role %s", t.roleName)) return nil } -func makeStackNamePrefix(clusterName string) string { - return fmt.Sprintf("eksctl-%s-podidentityrole-ns-", clusterName) +func updateTrustStatements( + removeOIDCProviderTrustRelationship bool, + getRole func() (*awsiam.GetRoleOutput, error), +) ([]api.IAMStatement, error) { + var trustStatements []api.IAMStatement + var trustPolicy api.IAMPolicyDocument + + output, err := getRole() + if err != nil { + return trustStatements, err + } + documentJSONString, err := url.PathUnescape(*output.Role.AssumeRolePolicyDocument) + if err != nil { + return trustStatements, err + } + if err := json.Unmarshal([]byte(documentJSONString), &trustPolicy); err != nil { + return trustStatements, err + } + + shouldRemoveStatement := func(s api.IAMStatement) bool { + value, ok := s.Principal["Federated"] + if ok && len(value) == 1 && + strings.Contains(value[0], "oidc-provider") && + removeOIDCProviderTrustRelationship { + return true + } + return false + } + + // remove OIDC provider trust relationship if instructed so + for _, s := range trustPolicy.Statements { + if shouldRemoveStatement(s) { + continue + } + trustStatements = append(trustStatements, s) + } + + // add trust relationship with new EKS Service Principal + trustStatements = append(trustStatements, api.EKSServicePrincipalTrustStatement) + + return trustStatements, nil } // MakeStackName creates a stack name for the specified access entry. func MakeStackName(clusterName, namespace, serviceAccountName string) string { - return fmt.Sprintf("%s%s-sa-%s", makeStackNamePrefix(clusterName), namespace, serviceAccountName) + return fmt.Sprintf("eksctl-%s-podidentityrole-%s-%s", clusterName, namespace, serviceAccountName) } func runAllTasks(taskTree *tasks.TaskTree) error { @@ -97,6 +264,12 @@ func runAllTasks(taskTree *tasks.TaskTree) error { } return fmt.Errorf(strings.Join(allErrs, "\n")) } - logger.Info("successfully finished all tasks") + completedAction := func() string { + if taskTree.PlanMode { + return "skipped" + } + return "completed successfully" + } + logger.Info("all tasks were %s", completedAction()) return nil } diff --git a/pkg/actions/podidentityassociation/updater.go b/pkg/actions/podidentityassociation/updater.go index 7d373d7aae..ddd7c48702 100644 --- a/pkg/actions/podidentityassociation/updater.go +++ b/pkg/actions/podidentityassociation/updater.go @@ -34,6 +34,9 @@ type Updater struct { } // A StackUpdater updates CloudFormation stacks. +// +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate +//counterfeiter:generate -o fakes/fake_stack_updater.go . StackUpdater type StackUpdater interface { StackLister // MustUpdateStack updates the CloudFormation stack. @@ -56,7 +59,7 @@ type updateConfig struct { // 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)) + roleStackNames, err := u.StackUpdater.ListPodIdentityStackNames(ctx) if err != nil { return fmt.Errorf("error listing stack names for pod identity associations: %w", err) } @@ -64,7 +67,10 @@ func (u *Updater) Update(ctx context.Context, podIdentityAssociations []api.PodI Parallel: true, } for _, p := range podIdentityAssociations { - podIdentityAssociationID := makeID(p.Namespace, p.ServiceAccountName) + podIdentityAssociationID := Identifier{ + Namespace: p.Namespace, + ServiceAccountName: p.ServiceAccountName, + }.IDString() updateErr := func(err error) error { return fmt.Errorf("error updating pod identity association %q: %w", podIdentityAssociationID, err) } @@ -82,7 +88,6 @@ func (u *Updater) Update(ctx context.Context, podIdentityAssociations []api.PodI }, }) } - logger.Info(taskTree.Describe()) return runAllTasks(taskTree) } @@ -162,10 +167,10 @@ func (u *Updater) makeUpdate(ctx context.Context, p api.PodIdentityAssociation, 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 0: + return nil, errors.New(notFoundErrMsg) case 1: describeOutput, err := u.APIUpdater.DescribePodIdentityAssociation(ctx, &eks.DescribePodIdentityAssociationInput{ ClusterName: aws.String(u.ClusterName), @@ -174,9 +179,11 @@ func (u *Updater) makeUpdate(ctx context.Context, p api.PodIdentityAssociation, if err != nil { return nil, fmt.Errorf("error describing pod identity association: %w", err) } - stackName := MakeStackName(u.ClusterName, p.Namespace, p.ServiceAccountName) - hasIAMResourcesStack := slices.Contains(roleStackNames, stackName) - if hasIAMResourcesStack { + stackName, hasStack := getIAMResourcesStack(roleStackNames, Identifier{ + Namespace: p.Namespace, + ServiceAccountName: p.ServiceAccountName, + }) + if hasStack { if describeOutput.Association.RoleArn != nil && p.RoleARN != "" && p.RoleARN != *describeOutput.Association.RoleArn { return nil, errors.New("cannot change podIdentityAssociation.roleARN since the role was created by eksctl") } @@ -196,7 +203,7 @@ func (u *Updater) makeUpdate(ctx context.Context, p api.PodIdentityAssociation, return &updateConfig{ podIdentityAssociation: p, associationID: *describeOutput.Association.AssociationId, - hasIAMResourcesStack: hasIAMResourcesStack, + hasIAMResourcesStack: hasStack, stackName: stackName, }, nil } diff --git a/pkg/actions/podidentityassociation/updater_test.go b/pkg/actions/podidentityassociation/updater_test.go index cb73a0e160..7411f74881 100644 --- a/pkg/actions/podidentityassociation/updater_test.go +++ b/pkg/actions/podidentityassociation/updater_test.go @@ -46,10 +46,14 @@ var _ = Describe("Pod Identity Update", func() { updateRoleARN string describeStackOutputs []cfntypes.Output describeStackCapabilities []cfntypes.Capability + makeStackName func(podidentityassociation.Identifier) string } mockCalls := func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS, o mockOptions) { - stackName := makeStackName(o.podIdentifier) + stackName := makeIRSAv2StackName(o.podIdentifier) + if o.makeStackName != nil { + stackName = o.makeStackName(o.podIdentifier) + } associationID := fmt.Sprintf("%x", sha1.Sum([]byte(stackName))) mockListPodIdentityAssociations(eksAPI, o.podIdentifier, []ekstypes.PodIdentityAssociationSummary{ { @@ -112,7 +116,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) eksAPI.AssertExpectations(GinkgoT()) @@ -140,7 +144,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) eksAPI.AssertExpectations(GinkgoT()) @@ -169,7 +173,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) eksAPI.AssertExpectations(GinkgoT()) @@ -198,7 +202,7 @@ var _ = Describe("Pod Identity Update", func() { ServiceAccountName: "aws-node", }, } - mockListStackNames(stackManager, podIdentifiers) + mockListStackNamesWithIRSAv1(stackManager, podIdentifiers[:1], podIdentifiers[1:]) describeStackOutputs := []cfntypes.Output{ { OutputKey: aws.String(outputs.IAMServiceAccountRoleName), @@ -210,6 +214,7 @@ var _ = Describe("Pod Identity Update", func() { podIdentifier: podIdentifiers[0], updateRoleARN: "arn:aws:iam::1234567:role/Role", describeStackOutputs: describeStackOutputs, + makeStackName: makeIRSAv1StackName, }, { podIdentifier: podIdentifiers[1], @@ -224,7 +229,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(4)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(2)) eksAPI.AssertExpectations(GinkgoT()) @@ -271,7 +276,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(2)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(2)) eksAPI.AssertExpectations(GinkgoT()) @@ -301,7 +306,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(0)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) eksAPI.AssertExpectations(GinkgoT()) @@ -332,7 +337,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(1)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(0)) eksAPI.AssertExpectations(GinkgoT()) @@ -364,7 +369,7 @@ var _ = Describe("Pod Identity Update", func() { ServiceAccountName: "aws-node", }, } - mockListStackNames(stackManager, podIdentifiers) + mockListStackNamesWithIRSAv1(stackManager, podIdentifiers[:1], podIdentifiers[1:]) describeStackOutputs := []cfntypes.Output{ { OutputKey: aws.String(outputs.IAMServiceAccountRoleName), @@ -376,6 +381,7 @@ var _ = Describe("Pod Identity Update", func() { podIdentifier: podIdentifiers[0], describeStackOutputs: describeStackOutputs, updateRoleARN: "arn:aws:iam::1234567:role/Role", + makeStackName: makeIRSAv1StackName, }, { podIdentifier: podIdentifiers[1], @@ -390,7 +396,7 @@ var _ = Describe("Pod Identity Update", func() { }, expectedCalls: func(stackManager *managerfakes.FakeStackManager, eksAPI *mocksv2.EKS) { - Expect(stackManager.ListStackNamesCallCount()).To(Equal(1)) + Expect(stackManager.ListPodIdentityStackNamesCallCount()).To(Equal(1)) Expect(stackManager.DescribeStackCallCount()).To(Equal(4)) Expect(stackManager.MustUpdateStackCallCount()).To(Equal(2)) eksAPI.AssertExpectations(GinkgoT()) @@ -409,14 +415,29 @@ func mockListPodIdentityAssociations(eksAPI *mocksv2.EKS, podID podidentityassoc }, err) } -func makeStackName(podID podidentityassociation.Identifier) string { - return fmt.Sprintf("eksctl-%s-podidentityrole-ns-%s-sa-%s", clusterName, podID.Namespace, podID.ServiceAccountName) +func makeIRSAv1StackName(podID podidentityassociation.Identifier) string { + return fmt.Sprintf("eksctl-%s-addon-iamserviceaccount-%s-%s", clusterName, podID.Namespace, podID.ServiceAccountName) +} + +func makeIRSAv2StackName(podID podidentityassociation.Identifier) string { + return podidentityassociation.MakeStackName(clusterName, podID.Namespace, podID.ServiceAccountName) +} + +func mockListStackNames(stackManager *managerfakes.FakeStackManager, podIDs []podidentityassociation.Identifier) { + mockListStackNamesWithIRSAv1(stackManager, []podidentityassociation.Identifier{}, podIDs) } -func mockListStackNames(stackManager *managerfakes.FakeStackManager, podIdentifiers []podidentityassociation.Identifier) { +func mockListStackNamesWithIRSAv1( + stackManager *managerfakes.FakeStackManager, + irsaV1podIdentifiers []podidentityassociation.Identifier, + irsaV2podIdentifiers []podidentityassociation.Identifier, +) { var stackNames []string - for _, id := range podIdentifiers { - stackNames = append(stackNames, makeStackName(id)) + for _, id := range irsaV1podIdentifiers { + stackNames = append(stackNames, makeIRSAv1StackName(id)) + } + for _, id := range irsaV2podIdentifiers { + stackNames = append(stackNames, makeIRSAv2StackName(id)) } - stackManager.ListStackNamesReturns(stackNames, nil) + stackManager.ListPodIdentityStackNamesReturns(stackNames, nil) } diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index 6d282f647b..e12b1df17a 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -540,12 +540,31 @@ }, "ClusterIAMServiceAccountStatus": { "properties": { + "capabilities": { + "items": { + "type": "string" + }, + "type": "array" + }, "roleARN": { "type": "string" + }, + "stackName": { + "type": "string" + }, + "tags": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "default": "{}" } }, "preferredOrder": [ - "roleARN" + "roleARN", + "stackName", + "tags", + "capabilities" ], "additionalProperties": false, "description": "holds status of the IAM service account", diff --git a/pkg/apis/eksctl.io/v1alpha5/iam.go b/pkg/apis/eksctl.io/v1alpha5/iam.go index af81ab3382..8fbe60cfa6 100644 --- a/pkg/apis/eksctl.io/v1alpha5/iam.go +++ b/pkg/apis/eksctl.io/v1alpha5/iam.go @@ -1,6 +1,7 @@ package v1alpha5 import ( + "encoding/json" "fmt" "strings" @@ -13,6 +14,17 @@ const ( EKSServicePrincipal = "pods.eks.amazonaws.com" ) +var EKSServicePrincipalTrustStatement = IAMStatement{ + Effect: "Allow", + Action: []string{ + "sts:AssumeRole", + "sts:TagSession", + }, + Principal: map[string]CustomStringSlice{ + "Service": []string{EKSServicePrincipal}, + }, +} + // ClusterIAM holds all IAM attributes of a cluster type ClusterIAM struct { // +optional @@ -119,6 +131,12 @@ type ClusterIAMServiceAccount struct { type ClusterIAMServiceAccountStatus struct { // +optional RoleARN *string `json:"roleARN,omitempty"` + // +optional + StackName *string `json:"stackName,omitempty"` + // +optional + Tags map[string]string `json:"tags,omitempty"` + // +optional + Capabilities []string `json:"capabilities,omitempty"` } // NameString returns common name string @@ -175,3 +193,88 @@ type PodIdentityAssociation struct { // +optional Tags map[string]string `json:"tags,omitempty"` } + +func (p PodIdentityAssociation) NameString() string { + return p.Namespace + "/" + p.ServiceAccountName +} + +// Internal type +// IAMPolicyDocument represents an IAM assume role policy document +type IAMPolicyDocument struct { + Version string `json:"Version"` + ID string `json:"Id,omitempty"` + Statements []IAMStatement `json:"Statement"` +} + +// Internal type +// IAMStatement represents an IAM policy document statement +type IAMStatement struct { + Sid string `json:"Sid,omitempty"` // statement ID, service specific + Effect string `json:"Effect"` // Allow or Deny + Principal map[string]CustomStringSlice `json:"Principal,omitempty"` // principal that is allowed or denied + NotPrincipal map[string]CustomStringSlice `json:"NotPrincipal,omitempty"` // exception to a list of principals + Action CustomStringSlice `json:"Action"` // allowed or denied action + NotAction CustomStringSlice `json:"NotAction,omitempty"` // matches everything except + Resource CustomStringSlice `json:"Resource,omitempty"` // object or objects that the statement covers + NotResource CustomStringSlice `json:"NotResource,omitempty"` // matches everything except + Condition json.RawMessage `json:"Condition,omitempty"` // conditions for when a policy is in effect +} + +func (s *IAMStatement) ToMapOfInterfaces() map[string]interface{} { + mapOfInterfaces := map[string]interface{}{ + "Effect": s.Effect, + "Action": s.Action, + } + if s.Sid != "" { + mapOfInterfaces["Sid"] = s.Sid + } + if s.Principal != nil { + mapOfInterfaces["Principal"] = s.Principal + } + if s.NotPrincipal != nil { + mapOfInterfaces["NotPrincipal"] = s.NotPrincipal + } + if s.NotAction != nil { + mapOfInterfaces["NotAction"] = s.NotAction + } + if s.Resource != nil { + mapOfInterfaces["Resource"] = s.Resource + } + if s.NotResource != nil { + mapOfInterfaces["NotResource"] = s.NotResource + } + if s.Condition != nil { + mapOfInterfaces["Condition"] = s.Condition + } + return mapOfInterfaces +} + +// AWS allows string or []string as value, we convert everything to []string to avoid casting +type CustomStringSlice []string + +func (c *CustomStringSlice) UnmarshalJSON(b []byte) error { + + var raw interface{} + err := json.Unmarshal(b, &raw) + if err != nil { + return err + } + + var p []string + // value can be string or []string, convert everything to []string + switch v := raw.(type) { + case string: + p = []string{v} + case []interface{}: + var items []string + for _, item := range v { + items = append(items, fmt.Sprintf("%v", item)) + } + p = items + default: + return fmt.Errorf("invalid %s value element: allowed is only string or []string", c) + } + + *c = p + return nil +} diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index 33a2d85d47..85b63c032f 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -286,6 +286,9 @@ const ( // IAMServiceAccountNameTag defines the tag of the IAM service account name IAMServiceAccountNameTag = "alpha.eksctl.io/iamserviceaccount-name" + // PodIdentityAssociationNameTag defines the tag of Pod Identity Association name + PodIdentityAssociationNameTag = "alpha.eksctl.io/podidentityassociation-name" + // AddonNameTag defines the tag of the IAM service account name AddonNameTag = "alpha.eksctl.io/addon-name" diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index 8c2f193290..97d9b2d956 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -49,7 +49,7 @@ var ( "for more details") ErrPodIdentityAgentNotInstalled = func(suggestion string) error { - return fmt.Errorf("the `%s` addon must be installed to create pod identity associations; %s", PodIdentityAgentAddon, suggestion) + return fmt.Errorf("the %q addon must be installed to create pod identity associations; %s", PodIdentityAgentAddon, suggestion) } ) diff --git a/pkg/cfn/builder/iam.go b/pkg/cfn/builder/iam.go index 3b87555ddb..387c526927 100644 --- a/pkg/cfn/builder/iam.go +++ b/pkg/cfn/builder/iam.go @@ -14,7 +14,6 @@ import ( api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" "github.com/weaveworks/eksctl/pkg/cfn/outputs" - "github.com/weaveworks/eksctl/pkg/cfn/template" cft "github.com/weaveworks/eksctl/pkg/cfn/template" iamoidc "github.com/weaveworks/eksctl/pkg/iam/oidc" ) @@ -223,6 +222,12 @@ func NewIAMRoleResourceSetForServiceAccount(spec *api.ClusterIAMServiceAccount, } } +func NewIAMRoleResourceSetForPodIdentityWithTrustStatements(spec *api.PodIdentityAssociation, trustStatements []api.IAMStatement) *IAMRoleResourceSet { + rs := NewIAMRoleResourceSetForPodIdentity(spec) + rs.trustStatements = trustStatements + return rs +} + func NewIAMRoleResourceSetForPodIdentity(spec *api.PodIdentityAssociation) *IAMRoleResourceSet { return &IAMRoleResourceSet{ template: cft.NewTemplate(), @@ -253,6 +258,7 @@ type IAMRoleResourceSet struct { wellKnownPolicies api.WellKnownPolicies attachPolicyARNs []string attachPolicy api.InlineDocument + trustStatements []api.IAMStatement roleNameCollector func(string) error OutputRole string serviceAccount string @@ -349,8 +355,11 @@ func (rs *IAMRoleResourceSet) AddAllResources() error { } func (rs *IAMRoleResourceSet) makeAssumeRolePolicyDocument() cft.MapOfInterfaces { + if len(rs.trustStatements) > 0 { + return cft.MakePolicyDocument(toMapOfInterfaces(rs.trustStatements)...) + } if rs.oidc == nil { - return template.MakeAssumeRolePolicyDocumentForPodIdentity() + return cft.MakeAssumeRolePolicyDocumentForPodIdentity() } if rs.serviceAccount != "" && rs.namespace != "" { logger.Debug("service account location provided: %s/%s, adding sub condition", api.AWSNodeMeta.Namespace, api.AWSNodeMeta.Name) @@ -359,6 +368,14 @@ func (rs *IAMRoleResourceSet) makeAssumeRolePolicyDocument() cft.MapOfInterfaces return rs.oidc.MakeAssumeRolePolicyDocument() } +func toMapOfInterfaces(old []api.IAMStatement) []cft.MapOfInterfaces { + new := []cft.MapOfInterfaces{} + for _, s := range old { + new = append(new, s.ToMapOfInterfaces()) + } + return new +} + // RenderJSON will render iamserviceaccount stack as JSON func (rs *IAMRoleResourceSet) RenderJSON() ([]byte, error) { return rs.template.RenderJSON() diff --git a/pkg/cfn/manager/api.go b/pkg/cfn/manager/api.go index 1288284aea..5e68c3fa16 100644 --- a/pkg/cfn/manager/api.go +++ b/pkg/cfn/manager/api.go @@ -32,7 +32,7 @@ const ( resourceTypeAutoScalingGroup = "auto-scaling-group" outputsRootPath = "Outputs" mappingsRootPath = "Mappings" - ourStackRegexFmt = "^(eksctl|EKS)-%s-((cluster|nodegroup-.+|addon-.+|fargate|karpenter)|(VPC|ServiceRole|ControlPlane|DefaultNodeGroup))$" + ourStackRegexFmt = "^(eksctl|EKS)-%s-((cluster|nodegroup-.+|addon-.+|podidentityrole-.+|fargate|karpenter)|(VPC|ServiceRole|ControlPlane|DefaultNodeGroup))$" clusterStackRegex = "eksctl-.*-cluster" ) @@ -333,7 +333,7 @@ func (c *StackCollection) updateStack(ctx context.Context, options UpdateStackOp logger.Info(options.Description) if options.Stack == nil { i := &Stack{StackName: &options.StackName} - // Read existing tags + // Read existing tags and capabilities s, err := c.DescribeStack(ctx, i) if err != nil { return err diff --git a/pkg/cfn/manager/fakes/fake_stack_manager.go b/pkg/cfn/manager/fakes/fake_stack_manager.go index a292e441bc..e7f21c5037 100644 --- a/pkg/cfn/manager/fakes/fake_stack_manager.go +++ b/pkg/cfn/manager/fakes/fake_stack_manager.go @@ -515,6 +515,19 @@ type FakeStackManager struct { result1 []manager.NodeGroupStack result2 error } + ListPodIdentityStackNamesStub func(context.Context) ([]string, error) + listPodIdentityStackNamesMutex sync.RWMutex + listPodIdentityStackNamesArgsForCall []struct { + arg1 context.Context + } + listPodIdentityStackNamesReturns struct { + result1 []string + result2 error + } + listPodIdentityStackNamesReturnsOnCall map[int]struct { + result1 []string + result2 error + } ListStackNamesStub func(context.Context, string) ([]string, error) listStackNamesMutex sync.RWMutex listStackNamesArgsForCall []struct { @@ -3205,6 +3218,70 @@ func (fake *FakeStackManager) ListNodeGroupStacksWithStatusesReturnsOnCall(i int }{result1, result2} } +func (fake *FakeStackManager) ListPodIdentityStackNames(arg1 context.Context) ([]string, error) { + fake.listPodIdentityStackNamesMutex.Lock() + ret, specificReturn := fake.listPodIdentityStackNamesReturnsOnCall[len(fake.listPodIdentityStackNamesArgsForCall)] + fake.listPodIdentityStackNamesArgsForCall = append(fake.listPodIdentityStackNamesArgsForCall, struct { + arg1 context.Context + }{arg1}) + stub := fake.ListPodIdentityStackNamesStub + fakeReturns := fake.listPodIdentityStackNamesReturns + fake.recordInvocation("ListPodIdentityStackNames", []interface{}{arg1}) + fake.listPodIdentityStackNamesMutex.Unlock() + if stub != nil { + return stub(arg1) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeStackManager) ListPodIdentityStackNamesCallCount() int { + fake.listPodIdentityStackNamesMutex.RLock() + defer fake.listPodIdentityStackNamesMutex.RUnlock() + return len(fake.listPodIdentityStackNamesArgsForCall) +} + +func (fake *FakeStackManager) ListPodIdentityStackNamesCalls(stub func(context.Context) ([]string, error)) { + fake.listPodIdentityStackNamesMutex.Lock() + defer fake.listPodIdentityStackNamesMutex.Unlock() + fake.ListPodIdentityStackNamesStub = stub +} + +func (fake *FakeStackManager) ListPodIdentityStackNamesArgsForCall(i int) context.Context { + fake.listPodIdentityStackNamesMutex.RLock() + defer fake.listPodIdentityStackNamesMutex.RUnlock() + argsForCall := fake.listPodIdentityStackNamesArgsForCall[i] + return argsForCall.arg1 +} + +func (fake *FakeStackManager) ListPodIdentityStackNamesReturns(result1 []string, result2 error) { + fake.listPodIdentityStackNamesMutex.Lock() + defer fake.listPodIdentityStackNamesMutex.Unlock() + fake.ListPodIdentityStackNamesStub = nil + fake.listPodIdentityStackNamesReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeStackManager) ListPodIdentityStackNamesReturnsOnCall(i int, result1 []string, result2 error) { + fake.listPodIdentityStackNamesMutex.Lock() + defer fake.listPodIdentityStackNamesMutex.Unlock() + fake.ListPodIdentityStackNamesStub = nil + if fake.listPodIdentityStackNamesReturnsOnCall == nil { + fake.listPodIdentityStackNamesReturnsOnCall = make(map[int]struct { + result1 []string + result2 error + }) + } + fake.listPodIdentityStackNamesReturnsOnCall[i] = struct { + result1 []string + result2 error + }{result1, result2} +} + func (fake *FakeStackManager) ListStackNames(arg1 context.Context, arg2 string) ([]string, error) { fake.listStackNamesMutex.Lock() ret, specificReturn := fake.listStackNamesReturnsOnCall[len(fake.listStackNamesArgsForCall)] @@ -4738,6 +4815,8 @@ func (fake *FakeStackManager) Invocations() map[string][][]interface{} { defer fake.listNodeGroupStacksMutex.RUnlock() fake.listNodeGroupStacksWithStatusesMutex.RLock() defer fake.listNodeGroupStacksWithStatusesMutex.RUnlock() + fake.listPodIdentityStackNamesMutex.RLock() + defer fake.listPodIdentityStackNamesMutex.RUnlock() fake.listStackNamesMutex.RLock() defer fake.listStackNamesMutex.RUnlock() fake.listStacksMutex.RLock() diff --git a/pkg/cfn/manager/iam.go b/pkg/cfn/manager/iam.go index 85574b70ea..c688348434 100644 --- a/pkg/cfn/manager/iam.go +++ b/pkg/cfn/manager/iam.go @@ -88,7 +88,17 @@ func (c *StackCollection) GetIAMServiceAccounts(ctx context.Context) ([]*api.Clu } serviceAccount := &api.ClusterIAMServiceAccount{ ClusterIAMMeta: *meta, - Status: &api.ClusterIAMServiceAccountStatus{}, + Status: &api.ClusterIAMServiceAccountStatus{ + StackName: s.StackName, + }, + } + for _, t := range s.Tags { + serviceAccount.Status.Tags = make(map[string]string) + serviceAccount.Status.Tags[*t.Key] = *t.Value + } + for _, c := range s.Capabilities { + serviceAccount.Status.Capabilities = make([]string, 0) + serviceAccount.Status.Capabilities = append(serviceAccount.Status.Capabilities, string(c)) } // TODO: we need to make it easier to fetch full definition of the object, diff --git a/pkg/cfn/manager/interface.go b/pkg/cfn/manager/interface.go index b8fbe2f1b8..1231f67152 100644 --- a/pkg/cfn/manager/interface.go +++ b/pkg/cfn/manager/interface.go @@ -76,6 +76,7 @@ type StackManager interface { ListIAMServiceAccountStacks(ctx context.Context) ([]string, error) ListNodeGroupStacks(ctx context.Context) ([]*Stack, error) ListNodeGroupStacksWithStatuses(ctx context.Context) ([]NodeGroupStack, error) + ListPodIdentityStackNames(ctx context.Context) ([]string, error) 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) diff --git a/pkg/cfn/manager/pod_identity_association.go b/pkg/cfn/manager/pod_identity_association.go new file mode 100644 index 0000000000..dde693a3ab --- /dev/null +++ b/pkg/cfn/manager/pod_identity_association.go @@ -0,0 +1,30 @@ +package manager + +import ( + "context" + "fmt" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +func (c *StackCollection) ListPodIdentityStackNames(ctx context.Context) ([]string, error) { + names := []string{} + stacks, err := c.ListStacks(ctx) + if err != nil { + return names, fmt.Errorf("listing stacks: %w", err) + } + + for _, s := range stacks { + isPodIdentityStack := false + for _, tag := range s.Tags { + if *tag.Key == api.PodIdentityAssociationNameTag { + isPodIdentityStack = true + } + } + if isPodIdentityStack { + names = append(names, *s.StackName) + } + } + + return names, nil +} diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go index 74ed21e080..2c69fb3168 100644 --- a/pkg/ctl/cmdutils/configfile.go +++ b/pkg/ctl/cmdutils/configfile.go @@ -309,7 +309,7 @@ func NewCreateClusterLoader(cmd *Cmd, ngFilter *filter.NodeGroupFilter, ng *api. addonNames = append(addonNames, addon.Name) } if !slices.Contains(addonNames, api.PodIdentityAgentAddon) { - suggestion := fmt.Sprintf("please add `%s` addon to the config file", api.PodIdentityAgentAddon) + suggestion := fmt.Sprintf("please add %q addon to the config file", api.PodIdentityAgentAddon) return api.ErrPodIdentityAgentNotInstalled(suggestion) } if err := validatePodIdentityAssociationsForConfig(clusterConfig, true); err != nil { diff --git a/pkg/ctl/create/pod_identity_association.go b/pkg/ctl/create/pod_identity_association.go index d3cd64ed73..2bc678c2e5 100644 --- a/pkg/ctl/create/pod_identity_association.go +++ b/pkg/ctl/create/pod_identity_association.go @@ -4,11 +4,6 @@ import ( "context" "fmt" - "github.com/aws/aws-sdk-go-v2/service/eks" - ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" - "github.com/aws/aws-sdk-go/aws" - - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -50,14 +45,12 @@ func doCreatePodIdentityAssociation(cmd *cmdutils.Cmd) error { return err } - if _, err := ctl.AWSProvider.EKS().DescribeAddon(ctx, &eks.DescribeAddonInput{ - AddonName: aws.String(api.PodIdentityAgentAddon), - ClusterName: &cfg.Metadata.Name, - }); err != nil { - var notFoundErr *ekstypes.ResourceNotFoundException - if !errors.As(err, ¬FoundErr) { - return fmt.Errorf("error calling `EKS::DescribeAddon::%s`: %v", api.PodIdentityAgentAddon, err) - } + isInstalled, err := podidentityassociation.IsPodIdentityAgentInstalled(ctx, ctl.AWSProvider.EKS(), cfg.Metadata.Name) + if err != nil { + return err + } + + if !isInstalled { suggestion := fmt.Sprintf("please enable it using `eksctl create addon --cluster=%s --name=%s`", cmd.ClusterConfig.Metadata.Name, api.PodIdentityAgentAddon) return api.ErrPodIdentityAgentNotInstalled(suggestion) } diff --git a/pkg/ctl/utils/migrate_to_pod_identity.go b/pkg/ctl/utils/migrate_to_pod_identity.go new file mode 100644 index 0000000000..d704f83499 --- /dev/null +++ b/pkg/ctl/utils/migrate_to_pod_identity.go @@ -0,0 +1,92 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/kris-nova/logger" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/weaveworks/eksctl/pkg/actions/addon" + "github.com/weaveworks/eksctl/pkg/actions/podidentityassociation" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" +) + +func migrateToPodIdentityCmd(cmd *cmdutils.Cmd) { + cfg := api.NewClusterConfig() + cmd.ClusterConfig = cfg + + cmd.SetDescription("migrate-to-pod-identity", "Updates the authentication mode for a cluster", "") + + var options podidentityassociation.PodIdentityMigrationOptions + cmd.FlagSetGroup.InFlagSet("Authentication mode", func(fs *pflag.FlagSet) { + fs.BoolVar(&options.RemoveOIDCProviderTrustRelationship, "remove-oidc-provider-trust-relationship", false, "Remove existing IRSAv1 OIDC provided entities") + fs.BoolVar(&options.Approve, "approve", false, "Apply the changes") + + // fs.BoolVar(&options.SkipAgentInstallation, "skip-agent-installation", false, "Skip installing pod-identity-agent addon") + // cmdutils.AddIAMServiceAccountFilterFlags(fs, &cmd.Include, &cmd.Exclude) + }) + + cmd.FlagSetGroup.InFlagSet("General", func(fs *pflag.FlagSet) { + cmdutils.AddClusterFlag(fs, cmd.ClusterConfig.Metadata) + cmdutils.AddRegionFlag(fs, &cmd.ProviderConfig) + cmdutils.AddTimeoutFlag(fs, &options.Timeout) + }) + + cmd.CobraCommand.RunE = func(_ *cobra.Command, args []string) error { + cmd.NameArg = cmdutils.GetNameArg(args) + return doMigrateToPodIdentity(cmd, options) + } +} + +func doMigrateToPodIdentity(cmd *cmdutils.Cmd, options podidentityassociation.PodIdentityMigrationOptions) error { + cfg := cmd.ClusterConfig + if cfg.Metadata.Name == "" { + return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) + } + + ctx := context.Background() + ctl, err := cmd.NewProviderForExistingCluster(ctx) + if err != nil { + return err + } + + if ok, err := ctl.CanOperate(cfg); !ok { + return err + } + + oidc, err := ctl.NewOpenIDConnectManager(ctx, cfg) + if err != nil { + return err + } + + providerExists, err := oidc.CheckProviderExists(ctx) + if err != nil { + return err + } + + if !providerExists { + logger.Warning("no IAM OIDC provider associated with cluster, hence no iamserviceaccounts to be migrated") + return nil + } + + clientSet, err := ctl.NewStdClientSet(cfg) + if err != nil { + return err + } + + addonCreator, err := addon.New(cfg, ctl.AWSProvider.EKS(), nil, false, nil, nil) + if err != nil { + return fmt.Errorf("initializing addon creator %w", err) + } + + return podidentityassociation.NewMigrator( + cfg.Metadata.Name, + ctl.AWSProvider.EKS(), + ctl.AWSProvider.IAM(), + ctl.NewStackManager(cfg), + clientSet, + addonCreator, + ).MigrateToPodIdentity(ctx, options) +} diff --git a/pkg/ctl/utils/utils.go b/pkg/ctl/utils/utils.go index 764af2c896..08c5ed9115 100644 --- a/pkg/ctl/utils/utils.go +++ b/pkg/ctl/utils/utils.go @@ -27,6 +27,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddResourceCmd(flagGrouping, verbCmd, nodeGroupHealthCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, describeAddonVersionsCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, describeAddonConfigurationCmd) + cmdutils.AddResourceCmd(flagGrouping, verbCmd, migrateToPodIdentityCmd) return verbCmd } diff --git a/userdocs/src/usage/pod-identity-associations.md b/userdocs/src/usage/pod-identity-associations.md index e0ccd99423..aebc7e5260 100644 --- a/userdocs/src/usage/pod-identity-associations.md +++ b/userdocs/src/usage/pod-identity-associations.md @@ -164,6 +164,47 @@ OR (to delete a single association) pass the `--namespace` and `--service-accoun eksctl delete podidentityassociation --cluster my-cluster --namespace default --service-account-name s3-reader ``` +## Migrating existing iamserviceaccounts to pod identity associations + +`eksctl` has introduced a new utils command for migrating existing IAM Roles for service accounts to pod identity associations, i.e. + +``` +eksctl utils migrate-to-pod-identity --cluster my-cluster --approve +``` + +Behind the scenes, the command will apply the following steps: + +- install the `eks-pod-identity-agent` addon if not already active on the cluster +- identify all IAM Roles that are associated with K8s service accounts +- update the IAM trust policy of all roles, with an additional trusted entity, pointing to the new EKS Service principal (and, optionally, remove exising OIDC provider trust relationship) +- create pod identity associations between all identified roles and the respective service accounts + +Running the command without the `--approve` flag will only output a plan consisting of a set of tasks reflecting the steps above, e.g. + +```bash +[ℹ] (plan) would migrate 2 iamserviceaccount(s) to pod identity association(s) by executing the following tasks +[ℹ] (plan) +3 sequential tasks: { install eks-pod-identity-agent addon, + 2 parallel sub-tasks: { + update trust policy for owned role "eksctl-my-cluster-addon-iamserv-Role1-beYhlhzpwQte", + update trust policy for unowned role "Unowned-Role1", + }, + 2 parallel sub-tasks: { + create pod identity association for service account "default/sa1", + create pod identity association for service account "default/sa2", + } +} +[ℹ] all tasks were skipped +[!] no changes were applied, run again with '--approve' to apply the changes +``` + +Additionally, to delete the existing OIDC provider trust relationship from all IAM Roles, run the command with `--remove-oidc-provider-trust-relationship` flag, e.g. + +``` +eksctl utils migrate-to-pod-identity --cluster my-cluster --approve --remove-oidc-provider-trust-relationship +``` + + ## Further references [Official AWS Blog Post](https://aws.amazon.com/blogs/aws/amazon-eks-pod-identity-simplifies-iam-permissions-for-applications-on-amazon-eks-clusters/)