Repository: crossplane/provider-aws (go)

Issue:
Missing crossplane default tags from iam.Policy
<!--
Thank you for helping to improve Crossplane!

Please be sure to search for open issues before raising a new one. We use issues
for bug reports and feature requests. Please find us at https://slack.crossplane.io
for questions, support, and discussion.
-->

### What problem are you facing?
<!--
Please tell us a little about your use case - it's okay if it's hypothetical!
Leading with this context helps frame the feature request so we can ensure we
implement it sensibly.
--->
iam.Policy is not being automatically tagged with the default crossplane tags (crossplane-kind, crossplane-name, crossplane-providerconfig)

### How could Crossplane help solve your problem?
<!--
Let us know how you think Crossplane could help with your use case. 
-->
iam.Policy should be automatically tagged with the default crossplane tags

Pull request description:
add default Crossplane tags to iam.Policy
Signed-off-by: Cecilia Bernardi <cbernardi@expediagroup.com>

<!--
Thank you for helping to improve Crossplane!

Please read through https://git.io/fj2m9 if this is your first time opening a
Crossplane pull request. Find us in https://slack.crossplane.io/messages/dev if
you need any help contributing.
-->

### Description of your changes

<!--
Briefly describe what this pull request does. Be sure to direct your reviewers'
attention to anything that needs special consideration.

We love pull requests that resolve an open Crossplane issue. If yours does, you
can uncomment the below line to indicate which issue your PR fixes, for example
"Fixes #500":

-->
Fixes #1206

I have:

- [x] Read and followed Crossplane's [contribution process].
- [x] Run `make reviewable test` to ensure this PR is ready for review.

### How has this code been tested

<!--
Before reviewers can be confident in the correctness of this pull request, it
needs to tested and shown to be correct. Briefly describe the testing that has
already been done or which is planned for this change.
-->

- Unit Tests
- Manual tests on a kind cluster configured with an AWS account:
    - Create resource scenario: a new iam.policy crossplane resource was created and the default tags were added in the kubernetes cluster; the policy was successfully created in the configured AWS account with the default tags
    - Add custom tags scenario: custom tags were added to the crossplane resources, and the AWS policy was successfully tagged with the new tags
    - Modify custom tags scenario: custom tags were modified on the kubernetes resource, and the modification was successfully reconciled with the AWS resource (could observe tags values being modified)
    - Remove custom tags scenario: custom tags were removed from the kubernetes resource, and after the reconciliation, the tags were removed also from the AWS resource
    - Modify / Remove default tags: default tags were removed and changed, and they were immediately restored. No modification was observed on the AWS resource

[contribution process]: https://git.io/fj2m9


Patch:
diff --git a/pkg/clients/iam/fake/policy.go b/pkg/clients/iam/fake/policy.go
index dcb973f5a..bc1110a0b 100644
--- a/pkg/clients/iam/fake/policy.go
+++ b/pkg/clients/iam/fake/policy.go
@@ -29,8 +29,16 @@ import (
 var _ clientset.PolicyClient = (*MockPolicyClient)(nil)
 var _ clientset.STSClient = (*MockSTSClient)(nil)
 
+// MockPolicyInput holds the input structures for future inspections
+type MockPolicyInput struct {
+	CreatePolicyInput *iam.CreatePolicyInput
+	TagPolicyInput    *iam.TagPolicyInput
+	UntagPolicyInput  *iam.UntagPolicyInput
+}
+
 // MockPolicyClient is a type that implements all the methods for PolicyClient interface
 type MockPolicyClient struct {
+	MockPolicyInput         MockPolicyInput
 	MockGetPolicy           func(ctx context.Context, input *iam.GetPolicyInput, opts []func(*iam.Options)) (*iam.GetPolicyOutput, error)
 	MockCreatePolicy        func(ctx context.Context, input *iam.CreatePolicyInput, opts []func(*iam.Options)) (*iam.CreatePolicyOutput, error)
 	MockDeletePolicy        func(ctx context.Context, input *iam.DeletePolicyInput, opts []func(*iam.Options)) (*iam.DeletePolicyOutput, error)
@@ -59,6 +67,7 @@ func (m *MockPolicyClient) GetPolicy(ctx context.Context, input *iam.GetPolicyIn
 
 // CreatePolicy mocks CreatePolicy method
 func (m *MockPolicyClient) CreatePolicy(ctx context.Context, input *iam.CreatePolicyInput, opts ...func(*iam.Options)) (*iam.CreatePolicyOutput, error) {
+	m.MockPolicyInput.CreatePolicyInput = input
 	return m.MockCreatePolicy(ctx, input, opts)
 }
 
@@ -89,10 +98,12 @@ func (m *MockPolicyClient) DeletePolicyVersion(ctx context.Context, input *iam.D
 
 // TagPolicy mocks TagPolicy method
 func (m *MockPolicyClient) TagPolicy(ctx context.Context, input *iam.TagPolicyInput, opts ...func(*iam.Options)) (*iam.TagPolicyOutput, error) {
+	m.MockPolicyInput.TagPolicyInput = input
 	return m.MockTagPolicy(ctx, input, opts)
 }
 
 // UntagPolicy mocks UnTagPolicy method
 func (m *MockPolicyClient) UntagPolicy(ctx context.Context, input *iam.UntagPolicyInput, opts ...func(*iam.Options)) (*iam.UntagPolicyOutput, error) {
+	m.MockPolicyInput.UntagPolicyInput = input
 	return m.MockUntagPolicy(ctx, input, opts)
 }
diff --git a/pkg/controller/iam/policy/controller.go b/pkg/controller/iam/policy/controller.go
index 5e91678fb..c3e6f4049 100644
--- a/pkg/controller/iam/policy/controller.go
+++ b/pkg/controller/iam/policy/controller.go
@@ -43,14 +43,17 @@ import (
 const (
 	errUnexpectedObject = "The managed resource is not a Policy resource"
 
-	errGet           = "failed to get IAM Policy"
-	errCreate        = "failed to create the IAM Policy"
-	errDelete        = "failed to delete the IAM Policy"
-	errUpdate        = "failed to update the IAM Policy"
-	errExternalName  = "failed to update the IAM Policy external-name"
-	errEmptyPolicy   = "empty IAM Policy received from IAM API"
-	errPolicyVersion = "No version for policy received from IAM API"
-	errUpToDate      = "cannot check if policy is up to date"
+	errGet              = "failed to get IAM Policy"
+	errCreate           = "failed to create the IAM Policy"
+	errDelete           = "failed to delete the IAM Policy"
+	errUpdate           = "failed to update the IAM Policy"
+	errExternalName     = "failed to update the IAM Policy external-name"
+	errEmptyPolicy      = "empty IAM Policy received from IAM API"
+	errPolicyVersion    = "No version for policy received from IAM API"
+	errUpToDate         = "cannot check if policy is up to date"
+	errKubeUpdateFailed = "cannot late initialize IAM Policy"
+	errTag              = "cannot tag policy"
+	errUntag            = "cannot untag policy"
 )
 
 // SetupPolicy adds a controller that reconciles IAM Policy.
@@ -64,7 +67,7 @@ func SetupPolicy(mgr ctrl.Manager, o controller.Options) error {
 		Complete(managed.NewReconciler(mgr,
 			resource.ManagedKind(v1beta1.PolicyGroupVersionKind),
 			managed.WithExternalConnecter(&connector{kube: mgr.GetClient(), newClientFn: iam.NewPolicyClient, newSTSClientFn: iam.NewSTSClient}),
-			managed.WithInitializers(),
+			managed.WithInitializers(&tagger{kube: mgr.GetClient()}),
 			managed.WithConnectionPublishers(),
 			managed.WithPollInterval(o.PollInterval),
 			managed.WithLogger(o.Logger.WithValues("controller", name)),
@@ -224,18 +227,13 @@ func (e *external) Update(ctx context.Context, mgd resource.Managed) (managed.Ex
 		return managed.ExternalUpdate{}, awsclient.Wrap(resource.Ignore(iam.IsErrorNotFound, err), errGet)
 	}
 
-	crTagMap := make(map[string]string, len(cr.Spec.ForProvider.Tags))
-	for _, v := range cr.Spec.ForProvider.Tags {
-		crTagMap[v.Key] = v.Value
-	}
-
-	add, remove, _ := iam.DiffIAMTags(crTagMap, observed.Policy.Tags)
+	add, remove, _ := iam.DiffIAMTagsWithUpdates(cr.Spec.ForProvider.Tags, observed.Policy.Tags)
 	if len(add) != 0 {
 		if _, err := e.client.TagPolicy(ctx, &awsiam.TagPolicyInput{
 			PolicyArn: aws.String(meta.GetExternalName(cr)),
 			Tags:      add,
 		}); err != nil {
-			return managed.ExternalUpdate{}, awsclient.Wrap(err, "cannot tag policy")
+			return managed.ExternalUpdate{}, awsclient.Wrap(err, errTag)
 		}
 	}
 
@@ -244,7 +242,7 @@ func (e *external) Update(ctx context.Context, mgd resource.Managed) (managed.Ex
 			PolicyArn: aws.String(meta.GetExternalName(cr)),
 			TagKeys:   remove,
 		}); err != nil {
-			return managed.ExternalUpdate{}, awsclient.Wrap(err, "cannot untag policy")
+			return managed.ExternalUpdate{}, awsclient.Wrap(err, errUntag)
 		}
 	}
 
@@ -371,3 +369,36 @@ func (e *external) getPolicyArnByNameAndPath(ctx context.Context, policyName str
 
 	return aws.String(policyArn.String()), nil
 }
+
+type tagger struct {
+	kube client.Client
+}
+
+func (t *tagger) Initialize(ctx context.Context, mgd resource.Managed) error {
+	cr, ok := mgd.(*v1beta1.Policy)
+	if !ok {
+		return errors.New(errUnexpectedObject)
+	}
+	added := false
+	defaultTags := resource.GetExternalTags(mgd)
+
+	for i, t := range cr.Spec.ForProvider.Tags {
+		v, ok := defaultTags[t.Key]
+		if ok {
+			if v != t.Value {
+				cr.Spec.ForProvider.Tags[i].Value = v
+				added = true
+			}
+			delete(defaultTags, t.Key)
+		}
+	}
+
+	for k, v := range defaultTags {
+		cr.Spec.ForProvider.Tags = append(cr.Spec.ForProvider.Tags, v1beta1.Tag{Key: k, Value: v})
+		added = true
+	}
+	if !added {
+		return nil
+	}
+	return errors.Wrap(t.kube.Update(ctx, cr), errKubeUpdateFailed)
+}


Test patch:
diff --git a/pkg/controller/iam/policy/controller_test.go b/pkg/controller/iam/policy/controller_test.go
index 0cb832dfc..542189b3d 100644
--- a/pkg/controller/iam/policy/controller_test.go
+++ b/pkg/controller/iam/policy/controller_test.go
@@ -18,25 +18,29 @@ package policy
 
 import (
 	"context"
+
 	"testing"
 
-	"github.com/crossplane/provider-aws/apis/iam/v1beta1"
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+
+	"github.com/aws/smithy-go/middleware"
 
 	awsiam "github.com/aws/aws-sdk-go-v2/service/iam"
 	awsiamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
 	"github.com/aws/aws-sdk-go-v2/service/sts"
-	"github.com/aws/smithy-go/middleware"
-	"github.com/google/go-cmp/cmp"
+	"github.com/aws/aws-sdk-go/aws"
+
 	"github.com/pkg/errors"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 
+	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	"github.com/crossplane/crossplane-runtime/pkg/meta"
 	"github.com/crossplane/crossplane-runtime/pkg/reconciler/managed"
 	"github.com/crossplane/crossplane-runtime/pkg/resource"
-
-	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
 	"github.com/crossplane/crossplane-runtime/pkg/test"
 
+	"github.com/crossplane/provider-aws/apis/iam/v1beta1"
 	awsclient "github.com/crossplane/provider-aws/pkg/clients"
 	"github.com/crossplane/provider-aws/pkg/clients/iam"
 	"github.com/crossplane/provider-aws/pkg/clients/iam/fake"
@@ -67,11 +71,46 @@ var (
 		UserId:         awsclient.String("AIDASAMPLEUSERID"),
 		ResultMetadata: middleware.Metadata{},
 	}
+
+	tagComparer = cmp.Comparer(func(expected, actual awsiamtypes.Tag) bool {
+		return cmp.Equal(expected.Key, actual.Key) &&
+			cmp.Equal(expected.Value, actual.Value)
+	})
+
+	createInputComparer = cmp.Comparer(func(expected, actual *awsiam.CreatePolicyInput) bool {
+		return cmp.Equal(expected.PolicyName, actual.PolicyName) &&
+			cmp.Equal(expected.Path, actual.Path) &&
+			cmp.Equal(expected.Description, actual.Description) &&
+			cmp.Equal(expected.PolicyDocument, actual.PolicyDocument) &&
+			cmp.Equal(expected.Tags, actual.Tags, tagComparer, sortIAMTags)
+	})
+
+	tagInputComparer = cmp.Comparer(func(expected, actual *awsiam.TagPolicyInput) bool {
+		return cmp.Equal(expected.PolicyArn, actual.PolicyArn) &&
+			cmp.Equal(expected.Tags, actual.Tags, tagComparer, sortIAMTags)
+	})
+
+	untagInputComparer = cmp.Comparer(func(expected, actual *awsiam.UntagPolicyInput) bool {
+		return cmp.Equal(expected.PolicyArn, actual.PolicyArn) &&
+			cmp.Equal(expected.TagKeys, actual.TagKeys, sortStrings)
+	})
+
+	sortIAMTags = cmpopts.SortSlices(func(a, b awsiamtypes.Tag) bool {
+		return *a.Key > *b.Key
+	})
+
+	sortTags = cmpopts.SortSlices(func(a, b v1beta1.Tag) bool {
+		return a.Key > b.Key
+	})
+
+	sortStrings = cmpopts.SortSlices(func(x, y string) bool {
+		return x < y
+	})
 )
 
 type args struct {
 	kube client.Client
-	iam  iam.PolicyClient
+	iam  *fake.MockPolicyClient
 	sts  iam.STSClient
 	cr   resource.Managed
 }
@@ -98,6 +137,24 @@ func withPath(path string) policyModifier {
 	}
 }
 
+func withTags(tagMaps ...map[string]string) policyModifier {
+	var tagList []v1beta1.Tag
+	for _, tagMap := range tagMaps {
+		for k, v := range tagMap {
+			tagList = append(tagList, v1beta1.Tag{Key: k, Value: v})
+		}
+	}
+	return func(r *v1beta1.Policy) {
+		r.Spec.ForProvider.Tags = tagList
+	}
+}
+
+func withGroupVersionKind() policyModifier {
+	return func(iamRole *v1beta1.Policy) {
+		iamRole.TypeMeta.SetGroupVersionKind(v1beta1.PolicyGroupVersionKind)
+	}
+}
+
 func policy(m ...policyModifier) *v1beta1.Policy {
 	cr := &v1beta1.Policy{}
 	cr.Spec.ForProvider.Name = name
@@ -292,6 +349,49 @@ func TestObserve(t *testing.T) {
 				},
 			},
 		},
+		"DifferentTags": {
+			args: args{
+				iam: &fake.MockPolicyClient{
+					MockGetPolicy: func(ctx context.Context, input *awsiam.GetPolicyInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyOutput, error) {
+						return &awsiam.GetPolicyOutput{
+							Policy: &awsiamtypes.Policy{
+								Tags: []awsiamtypes.Tag{
+									{Key: aws.String("key1"), Value: aws.String("value1")},
+								},
+							},
+						}, nil
+					},
+					MockGetPolicyVersion: func(ctx context.Context, input *awsiam.GetPolicyVersionInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyVersionOutput, error) {
+						return &awsiam.GetPolicyVersionOutput{
+							PolicyVersion: &awsiamtypes.PolicyVersion{
+								Document: &document,
+							},
+						}, nil
+					},
+				},
+				cr: policy(withSpec(v1beta1.PolicyParameters{
+					Document: document,
+					Name:     name,
+					Tags: []v1beta1.Tag{
+						{Key: "key2", Value: "value2"},
+					},
+				}), withExternalName(policyArn)),
+			},
+			want: want{
+				cr: policy(withSpec(v1beta1.PolicyParameters{
+					Document: document,
+					Name:     name,
+					Tags: []v1beta1.Tag{
+						{Key: "key2", Value: "value2"},
+					},
+				}), withExternalName(policyArn),
+					withConditions(xpv1.Available())),
+				result: managed.ExternalObservation{
+					ResourceExists:   true,
+					ResourceUpToDate: false,
+				},
+			},
+		},
 	}
 
 	for name, tc := range cases {
@@ -318,6 +418,7 @@ func TestCreate(t *testing.T) {
 		cr     resource.Managed
 		result managed.ExternalCreation
 		err    error
+		input  *awsiam.CreatePolicyInput
 	}
 
 	cases := map[string]struct {
@@ -339,18 +440,40 @@ func TestCreate(t *testing.T) {
 					},
 				},
 				cr: policy(withSpec(v1beta1.PolicyParameters{
-					Document: document,
-					Name:     name,
+					Name:        name,
+					Document:    document,
+					Description: aws.String("description"),
+					Path:        aws.String("path"),
+					Tags: []v1beta1.Tag{
+						{Key: "key1", Value: "value1"},
+						{Key: "key2", Value: "value2"},
+					},
 				})),
 			},
 			want: want{
 				cr: policy(
 					withSpec(v1beta1.PolicyParameters{
-						Document: document,
-						Name:     name,
+						Name:        name,
+						Document:    document,
+						Description: aws.String("description"),
+						Path:        aws.String("path"),
+						Tags: []v1beta1.Tag{
+							{Key: "key2", Value: "value2"},
+							{Key: "key1", Value: "value1"},
+						},
 					}),
 					withExternalName(policyArn)),
 				result: managed.ExternalCreation{},
+				input: &awsiam.CreatePolicyInput{
+					PolicyName:     aws.String(name),
+					Description:    aws.String("description"),
+					Path:           aws.String("path"),
+					PolicyDocument: aws.String(document),
+					Tags: []awsiamtypes.Tag{
+						{Key: aws.String("key1"), Value: aws.String("value1")},
+						{Key: aws.String("key2"), Value: aws.String("value2")},
+					},
+				},
 			},
 		},
 		"InValidInput": {
@@ -386,12 +509,18 @@ func TestCreate(t *testing.T) {
 			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
 				t.Errorf("r: -want, +got:\n%s", diff)
 			}
-			if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions()); diff != "" {
+			if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions(), sortTags); diff != "" {
 				t.Errorf("r: -want, +got:\n%s", diff)
 			}
 			if diff := cmp.Diff(tc.want.result, o); diff != "" {
 				t.Errorf("r: -want, +got:\n%s", diff)
 			}
+			if tc.want.input != nil {
+				actual := tc.args.iam.MockPolicyInput.CreatePolicyInput
+				if diff := cmp.Diff(tc.want.input, actual, createInputComparer); diff != "" {
+					t.Errorf("r: -want, +got:\n%s", diff)
+				}
+			}
 		})
 	}
 }
@@ -495,6 +624,230 @@ func TestUpdate(t *testing.T) {
 	}
 }
 
+func TestUpdate_Tags(t *testing.T) {
+
+	type want struct {
+		cr         resource.Managed
+		result     managed.ExternalUpdate
+		err        error
+		tagInput   *awsiam.TagPolicyInput
+		untagInput *awsiam.UntagPolicyInput
+	}
+
+	listPolicyVersions := func(ctx context.Context, input *awsiam.ListPolicyVersionsInput, opts []func(*awsiam.Options)) (*awsiam.ListPolicyVersionsOutput, error) {
+		return &awsiam.ListPolicyVersionsOutput{}, nil
+	}
+
+	deletePolicyVersions := func(ctx context.Context, input *awsiam.DeletePolicyVersionInput, opts []func(*awsiam.Options)) (*awsiam.DeletePolicyVersionOutput, error) {
+		return &awsiam.DeletePolicyVersionOutput{}, nil
+	}
+
+	createPolicyVersion := func(ctx context.Context, input *awsiam.CreatePolicyVersionInput, opts []func(*awsiam.Options)) (*awsiam.CreatePolicyVersionOutput, error) {
+		return &awsiam.CreatePolicyVersionOutput{}, nil
+	}
+
+	cases := map[string]struct {
+		args
+		want
+	}{
+		"AddTagsError": {
+			args: args{
+				iam: &fake.MockPolicyClient{
+					MockGetPolicy: func(ctx context.Context, input *awsiam.GetPolicyInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyOutput, error) {
+						return &awsiam.GetPolicyOutput{
+							Policy: &awsiamtypes.Policy{},
+						}, nil
+					},
+					MockTagPolicy: func(ctx context.Context, input *awsiam.TagPolicyInput, opts []func(*awsiam.Options)) (*awsiam.TagPolicyOutput, error) {
+						return nil, errBoom
+					},
+				},
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key": "value",
+					})),
+			},
+			want: want{
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key": "value",
+					})),
+				err: awsclient.Wrap(errBoom, errTag),
+			},
+		},
+		"AddTagsSuccess": {
+			args: args{
+				iam: &fake.MockPolicyClient{
+					MockGetPolicy: func(ctx context.Context, input *awsiam.GetPolicyInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyOutput, error) {
+						return &awsiam.GetPolicyOutput{
+							Policy: &awsiamtypes.Policy{},
+						}, nil
+					},
+					MockTagPolicy: func(ctx context.Context, input *awsiam.TagPolicyInput, opts []func(*awsiam.Options)) (*awsiam.TagPolicyOutput, error) {
+						return nil, nil
+					},
+				},
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key": "value",
+					})),
+			},
+			want: want{
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key": "value",
+					})),
+				tagInput: &awsiam.TagPolicyInput{
+					PolicyArn: aws.String(policyArn),
+					Tags: []awsiamtypes.Tag{
+						{Key: aws.String("key"), Value: aws.String("value")},
+					},
+				},
+			},
+		},
+		"UpdateTagsSuccess": {
+			args: args{
+				iam: &fake.MockPolicyClient{
+					MockGetPolicy: func(ctx context.Context, input *awsiam.GetPolicyInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyOutput, error) {
+						return &awsiam.GetPolicyOutput{
+							Policy: &awsiamtypes.Policy{
+								Tags: []awsiamtypes.Tag{
+									{Key: aws.String("key1"), Value: aws.String("value1")},
+									{Key: aws.String("key2"), Value: aws.String("value2")},
+								},
+							},
+						}, nil
+					},
+					MockTagPolicy: func(ctx context.Context, input *awsiam.TagPolicyInput, opts []func(*awsiam.Options)) (*awsiam.TagPolicyOutput, error) {
+						return nil, nil
+					},
+				},
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key1": "value1",
+						"key2": "value1",
+					})),
+			},
+			want: want{
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key2": "value1",
+						"key1": "value1",
+					})),
+				tagInput: &awsiam.TagPolicyInput{
+					PolicyArn: aws.String(policyArn),
+					Tags: []awsiamtypes.Tag{
+						{Key: aws.String("key2"), Value: aws.String("value1")},
+					},
+				},
+			},
+		},
+		"RemoveTagsError": {
+			args: args{
+				iam: &fake.MockPolicyClient{
+					MockGetPolicy: func(ctx context.Context, input *awsiam.GetPolicyInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyOutput, error) {
+						return &awsiam.GetPolicyOutput{
+							Policy: &awsiamtypes.Policy{
+								Tags: []awsiamtypes.Tag{
+									{Key: aws.String("key1"), Value: aws.String("value1")},
+									{Key: aws.String("key2"), Value: aws.String("value2")},
+								},
+							},
+						}, nil
+					},
+					MockUntagPolicy: func(ctx context.Context, input *awsiam.UntagPolicyInput, opts []func(*awsiam.Options)) (*awsiam.UntagPolicyOutput, error) {
+						return nil, errBoom
+					},
+				},
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key2": "value2",
+					})),
+			},
+			want: want{
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key2": "value2",
+					})),
+				err: awsclient.Wrap(errBoom, errUntag),
+			},
+		},
+		"RemoveTagsSuccess": {
+			args: args{
+				iam: &fake.MockPolicyClient{
+					MockGetPolicy: func(ctx context.Context, input *awsiam.GetPolicyInput, opts []func(*awsiam.Options)) (*awsiam.GetPolicyOutput, error) {
+						return &awsiam.GetPolicyOutput{
+							Policy: &awsiamtypes.Policy{
+								Tags: []awsiamtypes.Tag{
+									{Key: aws.String("key1"), Value: aws.String("value1")},
+									{Key: aws.String("key2"), Value: aws.String("value2")},
+								},
+							},
+						}, nil
+					},
+					MockUntagPolicy: func(ctx context.Context, input *awsiam.UntagPolicyInput, opts []func(*awsiam.Options)) (*awsiam.UntagPolicyOutput, error) {
+						return nil, nil
+					},
+				},
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key2": "value2",
+					})),
+			},
+			want: want{
+				cr: policy(
+					withExternalName(policyArn),
+					withTags(map[string]string{
+						"key2": "value2",
+					})),
+				untagInput: &awsiam.UntagPolicyInput{
+					PolicyArn: aws.String(policyArn),
+					TagKeys:   []string{"key1"},
+				},
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			tc.iam.MockListPolicyVersions = listPolicyVersions
+			tc.iam.MockDeletePolicyVersion = deletePolicyVersions
+			tc.iam.MockCreatePolicyVersion = createPolicyVersion
+
+			e := &external{client: tc.iam}
+			o, err := e.Update(context.Background(), tc.args.cr)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("r: -want, +got:\n%s", diff)
+			}
+			if diff := cmp.Diff(tc.want.cr, tc.args.cr, test.EquateConditions(), sortTags); diff != "" {
+				t.Errorf("r: -want, +got:\n%s", diff)
+			}
+			if diff := cmp.Diff(tc.want.result, o); diff != "" {
+				t.Errorf("r: -want, +got:\n%s", diff)
+			}
+			if tc.want.tagInput != nil {
+				if diff := cmp.Diff(tc.want.tagInput, tc.iam.MockPolicyInput.TagPolicyInput, tagInputComparer); diff != "" {
+					t.Errorf("r: -want, +got:\n%s", diff)
+				}
+			}
+			if tc.want.untagInput != nil {
+				if diff := cmp.Diff(tc.want.untagInput, tc.iam.MockPolicyInput.UntagPolicyInput, untagInputComparer); diff != "" {
+					t.Errorf("r: -want, +got:\n%s", diff)
+				}
+			}
+		})
+	}
+}
 func TestDelete(t *testing.T) {
 
 	type want struct {
@@ -595,3 +948,79 @@ func TestDelete(t *testing.T) {
 		})
 	}
 }
+
+func TestInitialize(t *testing.T) {
+	type args struct {
+		cr   resource.Managed
+		kube client.Client
+	}
+	type want struct {
+		cr  *v1beta1.Policy
+		err error
+	}
+
+	cases := map[string]struct {
+		args
+		want
+	}{
+		"Unexpected": {
+			args: args{
+				cr:   unexpectedItem,
+				kube: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
+			},
+			want: want{
+				err: errors.New(errUnexpectedObject),
+			},
+		},
+		"Successful": {
+			args: args{
+				cr:   policy(withTags(map[string]string{"foo": "bar"})),
+				kube: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
+			},
+			want: want{
+				cr: policy(withTags(resource.GetExternalTags(policy()), map[string]string{"foo": "bar"})),
+			},
+		},
+		"DefaultTags": {
+			args: args{
+				cr:   policy(withTags(map[string]string{"foo": "bar"}), withGroupVersionKind()),
+				kube: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
+			},
+			want: want{
+				cr: policy(withTags(resource.GetExternalTags(policy(withGroupVersionKind())), map[string]string{"foo": "bar"}), withGroupVersionKind()),
+			},
+		},
+		"UpdateDefaultTags": {
+			args: args{
+				cr:   policy(withTags(map[string]string{resource.ExternalResourceTagKeyKind: "bar"}), withGroupVersionKind()),
+				kube: &test.MockClient{MockUpdate: test.NewMockUpdateFn(nil)},
+			},
+			want: want{
+				cr: policy(withTags(resource.GetExternalTags(policy(withGroupVersionKind()))), withGroupVersionKind()),
+			},
+		},
+		"UpdateFailed": {
+			args: args{
+				cr:   policy(),
+				kube: &test.MockClient{MockUpdate: test.NewMockUpdateFn(errBoom)},
+			},
+			want: want{
+				err: errors.Wrap(errBoom, errKubeUpdateFailed),
+			},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			e := &tagger{kube: tc.kube}
+			err := e.Initialize(context.Background(), tc.args.cr)
+
+			if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
+				t.Errorf("r: -want, +got:\n%s", diff)
+			}
+			if diff := cmp.Diff(tc.want.cr, tc.args.cr, cmpopts.SortSlices(func(a, b v1beta1.Tag) bool { return a.Key > b.Key })); err == nil && diff != "" {
+				t.Errorf("r: -want, +got:\n%s", diff)
+			}
+		})
+	}
+}