Repository: crossplane-contrib/provider-aws (go)

Issue:
IAM Role possible update-loop due to `RoleLastUsed`
### What happened?

Similar to this `iam.Role` issue https://github.com/crossplane-contrib/provider-aws/issues/1945, the **observed** parameter `RoleLastUsed` can lead to update-loops if AWS sets it. (only set on AWS SDK/CLI usage of role?)

from the provider-aws debug logs:

```
"Found observed difference in IAM role
  &types.Role{
   ... // 2 ignored and 5 identical fields
   MaxSessionDuration:  &3600,
   PermissionsBoundary: nil,
   RoleLastUsed: &types.RoleLastUsed{
-      LastUsedDate: &s\"0001-01-01 00:00:00 +0000 UTC\",
+      LastUsedDate: &s\"2023-12-06 14:30:40 +0000 UTC\",
       Region:       &\"eu-central-1\",
       ... // 1 ignored field
   },
   ...
 }"
```

EDIT: I will try to provide a PR.

### How can we reproduce it?
1. Create `iam.Role`
2. use Role so AWS sets a LastUsedDate/"Last activity"(== parameter display name in AWS Management Console) value
3. get update-loop

### What environment did it happen in?
Crossplane version: v1.14.3
provider-aws: v0.45.2 + master


Pull request description:
fix(iam/role): isUpToDate ignore roleLastUsed + add to status
### Description of your changes

for `iam.Role`:
- fix update loop by ignoring **observed** parameter `RoleLastUsed` in `isUpToDate()`
- add `createDate` and `roleLastUsed` to `status.atProvider` (API/CRD addition +  controller logic)
- update unit tests

Fixes #1968

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
manually + unit tests


Patch:
diff --git a/apis/iam/v1beta1/role_types.go b/apis/iam/v1beta1/role_types.go
index e9097dcf0..686fe4aa2 100644
--- a/apis/iam/v1beta1/role_types.go
+++ b/apis/iam/v1beta1/role_types.go
@@ -44,6 +44,29 @@ type Tag struct {
 	Value string `json:"value,omitempty"`
 }
 
+// Contains information about the last time that an IAM role was used. This
+// includes the date and time and the Region in which the role was last used.
+// Activity is only reported for the trailing 400 days. This period can be shorter
+// if your Region began supporting these features within the last year. The role
+// might have been used more than 400 days ago. For more information, see Regions
+// where data is tracked
+// (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period)
+// in the IAM User Guide. This data type is returned as a response element in the
+// GetRole and GetAccountAuthorizationDetails operations.
+type RoleLastUsed struct {
+
+	// The date and time, in ISO 8601 date-time format (http://www.iso.org/iso/iso8601)
+	// that the role was last used. This field is null if the role has not been used
+	// within the IAM tracking period. For more information about the tracking period,
+	// see Regions where data is tracked
+	// (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period)
+	// in the IAM User Guide.
+	LastUsedDate *metav1.Time `json:"lastUsedDate,omitempty"`
+
+	// The name of the Amazon Web Services Region in which the role was last used.
+	Region *string `json:"region,omitempty"`
+}
+
 // RoleParameters define the desired state of an AWS IAM Role.
 type RoleParameters struct {
 
@@ -98,6 +121,20 @@ type RoleExternalStatus struct {
 	// IDs, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html)
 	// in the Using IAM guide.
 	RoleID string `json:"roleID"`
+
+	// The date and time, in ISO 8601 date-time format
+	// (http://www.iso.org/iso/iso8601), when the role was created.
+	CreateDate *metav1.Time `json:"createDate,omitempty"`
+
+	// Contains information about the last time that an IAM role was used. This
+	// includes the date and time and the Region in which the role was last used.
+	// Activity is only reported for the trailing 400 days. This period can be shorter
+	// if your Region began supporting these features within the last year. The role
+	// might have been used more than 400 days ago. For more information, see Regions
+	// where data is tracked
+	// (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period)
+	// in the IAM User Guide.
+	RoleLastUsed *RoleLastUsed `json:"roleLastUsed,omitempty"`
 }
 
 // A RoleStatus represents the observed state of a Role.
diff --git a/apis/iam/v1beta1/zz_generated.deepcopy.go b/apis/iam/v1beta1/zz_generated.deepcopy.go
index c3edf5d84..1091f5bfb 100644
--- a/apis/iam/v1beta1/zz_generated.deepcopy.go
+++ b/apis/iam/v1beta1/zz_generated.deepcopy.go
@@ -866,6 +866,15 @@ func (in *Role) DeepCopyObject() runtime.Object {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RoleExternalStatus) DeepCopyInto(out *RoleExternalStatus) {
 	*out = *in
+	if in.CreateDate != nil {
+		in, out := &in.CreateDate, &out.CreateDate
+		*out = (*in).DeepCopy()
+	}
+	if in.RoleLastUsed != nil {
+		in, out := &in.RoleLastUsed, &out.RoleLastUsed
+		*out = new(RoleLastUsed)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleExternalStatus.
@@ -878,6 +887,30 @@ func (in *RoleExternalStatus) DeepCopy() *RoleExternalStatus {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *RoleLastUsed) DeepCopyInto(out *RoleLastUsed) {
+	*out = *in
+	if in.LastUsedDate != nil {
+		in, out := &in.LastUsedDate, &out.LastUsedDate
+		*out = (*in).DeepCopy()
+	}
+	if in.Region != nil {
+		in, out := &in.Region, &out.Region
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleLastUsed.
+func (in *RoleLastUsed) DeepCopy() *RoleLastUsed {
+	if in == nil {
+		return nil
+	}
+	out := new(RoleLastUsed)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *RoleList) DeepCopyInto(out *RoleList) {
 	*out = *in
@@ -1114,7 +1147,7 @@ func (in *RoleSpec) DeepCopy() *RoleSpec {
 func (in *RoleStatus) DeepCopyInto(out *RoleStatus) {
 	*out = *in
 	in.ResourceStatus.DeepCopyInto(&out.ResourceStatus)
-	out.AtProvider = in.AtProvider
+	in.AtProvider.DeepCopyInto(&out.AtProvider)
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleStatus.
diff --git a/package/crds/iam.aws.crossplane.io_roles.yaml b/package/crds/iam.aws.crossplane.io_roles.yaml
index 2ea4234f3..88aadf87b 100644
--- a/package/crds/iam.aws.crossplane.io_roles.yaml
+++ b/package/crds/iam.aws.crossplane.io_roles.yaml
@@ -299,12 +299,41 @@ spec:
                       in policies, see IAM Identifiers (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html)
                       in the IAM User Guide guide.
                     type: string
+                  createDate:
+                    description: The date and time, in ISO 8601 date-time format (http://www.iso.org/iso/iso8601),
+                      when the role was created.
+                    format: date-time
+                    type: string
                   roleID:
                     description: RoleID is the stable and unique string identifying
                       the role. For more information about IDs, see IAM Identifiers
                       (http://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html)
                       in the Using IAM guide.
                     type: string
+                  roleLastUsed:
+                    description: Contains information about the last time that an
+                      IAM role was used. This includes the date and time and the Region
+                      in which the role was last used. Activity is only reported for
+                      the trailing 400 days. This period can be shorter if your Region
+                      began supporting these features within the last year. The role
+                      might have been used more than 400 days ago. For more information,
+                      see Regions where data is tracked (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period)
+                      in the IAM User Guide.
+                    properties:
+                      lastUsedDate:
+                        description: The date and time, in ISO 8601 date-time format
+                          (http://www.iso.org/iso/iso8601) that the role was last
+                          used. This field is null if the role has not been used within
+                          the IAM tracking period. For more information about the
+                          tracking period, see Regions where data is tracked (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_access-advisor.html#access-advisor_tracking-period)
+                          in the IAM User Guide.
+                        format: date-time
+                        type: string
+                      region:
+                        description: The name of the Amazon Web Services Region in
+                          which the role was last used.
+                        type: string
+                    type: object
                 required:
                 - arn
                 - roleID
diff --git a/pkg/clients/iam/role.go b/pkg/clients/iam/role.go
index bcefc2e32..5c6f7e229 100644
--- a/pkg/clients/iam/role.go
+++ b/pkg/clients/iam/role.go
@@ -71,10 +71,20 @@ func GenerateCreateRoleInput(name string, p *v1beta1.RoleParameters) *iam.Create
 
 // GenerateRoleObservation is used to produce RoleExternalStatus from iamtypes.Role
 func GenerateRoleObservation(role iamtypes.Role) v1beta1.RoleExternalStatus {
-	return v1beta1.RoleExternalStatus{
-		ARN:    aws.ToString(role.Arn),
-		RoleID: aws.ToString(role.RoleId),
+	o := v1beta1.RoleExternalStatus{
+		ARN:        pointer.StringValue(role.Arn),
+		CreateDate: pointer.TimeToMetaTime(role.CreateDate),
+		RoleID:     pointer.StringValue(role.RoleId),
 	}
+
+	if role.RoleLastUsed != nil {
+		o.RoleLastUsed = &v1beta1.RoleLastUsed{
+			LastUsedDate: pointer.TimeToMetaTime(role.RoleLastUsed.LastUsedDate),
+			Region:       role.RoleLastUsed.Region,
+		}
+	}
+
+	return o
 }
 
 // GenerateRole assigns the in RoleParamters to role.
@@ -180,7 +190,7 @@ func IsRoleUpToDate(in v1beta1.RoleParameters, observed iamtypes.Role) (bool, st
 
 	diff := cmp.Diff(desired, &observed,
 		cmpopts.IgnoreInterfaces(struct{ resource.AttributeReferencer }{}),
-		cmpopts.IgnoreFields(observed, "AssumeRolePolicyDocument", "CreateDate", "PermissionsBoundary.PermissionsBoundaryType"),
+		cmpopts.IgnoreFields(observed, "AssumeRolePolicyDocument", "CreateDate", "PermissionsBoundary.PermissionsBoundaryType", "RoleLastUsed"),
 		cmpopts.IgnoreTypes(document.NoSerde{}), cmpopts.SortSlices(lessTag))
 	if diff == "" && policyUpToDate {
 		return true, diff, nil


Test patch:
diff --git a/pkg/clients/iam/role_test.go b/pkg/clients/iam/role_test.go
index 8b118f598..bca91c926 100644
--- a/pkg/clients/iam/role_test.go
+++ b/pkg/clients/iam/role_test.go
@@ -52,6 +52,7 @@ var (
 	tagValue           = "value"
 	permissionBoundary = "arn:aws:iam::111111111111:policy/permission-boundary"
 	createDate         = time.Now()
+	region             = "us-east-1"
 )
 
 func roleParams(m ...func(*v1beta1.RoleParameters)) *v1beta1.RoleParameters {
@@ -93,12 +94,22 @@ func role(m ...func(*iamtypes.Role)) *iamtypes.Role {
 func addRoleOutputFields(r *iamtypes.Role) {
 	r.Arn = pointer.ToOrNilIfZeroValue(roleARN)
 	r.RoleId = pointer.ToOrNilIfZeroValue(roleID)
+	r.CreateDate = &createDate
+	r.RoleLastUsed = &iamtypes.RoleLastUsed{
+		LastUsedDate: &createDate,
+		Region:       &region,
+	}
 }
 
 func roleObservation(m ...func(*v1beta1.RoleExternalStatus)) *v1beta1.RoleExternalStatus {
 	o := &v1beta1.RoleExternalStatus{
-		ARN:    roleARN,
-		RoleID: roleID,
+		ARN:        roleARN,
+		RoleID:     roleID,
+		CreateDate: pointer.TimeToMetaTime(&createDate),
+		RoleLastUsed: &v1beta1.RoleLastUsed{
+			LastUsedDate: pointer.TimeToMetaTime(&createDate),
+			Region:       &region,
+		},
 	}
 
 	for _, f := range m {
@@ -285,6 +296,10 @@ func TestIsRoleUpToDate(t *testing.T) {
 						PermissionsBoundaryArn:  &permissionBoundary,
 						PermissionsBoundaryType: "Policy",
 					},
+					RoleLastUsed: &iamtypes.RoleLastUsed{
+						LastUsedDate: &createDate,
+						Region:       pointer.ToOrNilIfZeroValue("us-east-1"),
+					},
 					Tags: []iamtypes.Tag{{
 						Key:   pointer.ToOrNilIfZeroValue("key1"),
 						Value: pointer.ToOrNilIfZeroValue("value1"),