Repository: aws-controllers-k8s/efs-controller
Pull request #41: fix: ensure Filesystem resource is synced when available
Author: login: michaelhtm
Labels: name: approved
name: lgtm
description: Indicates that a PR is ready to be merged.
Created: 2025-05-12T16:06:24Z | Merged: 2025-05-19T22:28:40Z

Issue [#2468](https://github.com/aws-controllers-k8s/community/issues/2468)

Description of changes:
removing the following portion from generator.yaml:
```yaml
      sdk_update_post_request:
        code: return desired, nil
```
This portion was returning `desired, nil`, right after an update call was made,
without checking if the update call returned an error, nor logging the metrics call.

Instead using 
```yaml
      sdk_update_pre_set_output:
        template_path: ...
```
Which checks for update errors, logs API Metrics, and ensures ko.Status is 
latest.Status (ensuring we return the updated Status to be patched to k8s)



By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Diff:
diff --git a/apis/v1alpha1/ack-generate-metadata.yaml b/apis/v1alpha1/ack-generate-metadata.yaml
index 8802574..b7bac11 100755
--- a/apis/v1alpha1/ack-generate-metadata.yaml
+++ b/apis/v1alpha1/ack-generate-metadata.yaml
@@ -7,7 +7,7 @@ api_directory_checksum: ebeb6f826282ad9134ca78efd74dc9125898fddf
 api_version: v1alpha1
 aws_sdk_go_version: v1.32.6
 generator_config_info:
-  file_checksum: 99053458e09d8c915086d82f2f76c55d6374efef
+  file_checksum: 8f51ccb0fdf00c0900f3cd0641fe851d5909686e
   original_file_name: generator.yaml
 last_modification:
   reason: API generation
diff --git a/apis/v1alpha1/generator.yaml b/apis/v1alpha1/generator.yaml
index 03214bf..f27a6ee 100644
--- a/apis/v1alpha1/generator.yaml
+++ b/apis/v1alpha1/generator.yaml
@@ -56,8 +56,8 @@ resources:
     hooks:
       delta_pre_compare:
         code: customPreCompare(delta, a, b)
-      sdk_update_post_request:
-        code: return desired, nil
+      sdk_update_pre_set_output:
+        template_path: hooks/file_system/sdk_update_pre_set_output.go.tpl
       sdk_create_post_build_request:
         template_path: hooks/file_system/sdk_create_post_build_request.go.tpl
       sdk_read_many_post_set_output:
diff --git a/generator.yaml b/generator.yaml
index 03214bf..f27a6ee 100644
--- a/generator.yaml
+++ b/generator.yaml
@@ -56,8 +56,8 @@ resources:
     hooks:
       delta_pre_compare:
         code: customPreCompare(delta, a, b)
-      sdk_update_post_request:
-        code: return desired, nil
+      sdk_update_pre_set_output:
+        template_path: hooks/file_system/sdk_update_pre_set_output.go.tpl
       sdk_create_post_build_request:
         template_path: hooks/file_system/sdk_create_post_build_request.go.tpl
       sdk_read_many_post_set_output:
diff --git a/pkg/resource/file_system/hooks.go b/pkg/resource/file_system/hooks.go
index 34b7adc..17b1ee0 100644
--- a/pkg/resource/file_system/hooks.go
+++ b/pkg/resource/file_system/hooks.go
@@ -56,7 +56,7 @@ func requeueWaitState(r *resource) *ackrequeue.RequeueNeededAfter {
 	return ackrequeue.NeededAfter(
 		fmt.Errorf("filesystem in '%s' state, requeuing until filesystem is '%s'",
 			status, svcapitypes.LifeCycleState_available),
-		time.Second*10,
+		time.Second*3,
 	)
 }
 
diff --git a/pkg/resource/file_system/sdk.go b/pkg/resource/file_system/sdk.go
index 915edae..b8a1c72 100644
--- a/pkg/resource/file_system/sdk.go
+++ b/pkg/resource/file_system/sdk.go
@@ -455,7 +455,11 @@ func (rm *resourceManager) sdkUpdate(
 	defer func() {
 		exit(err)
 	}()
-
+	res := desired.ko.DeepCopy()
+	// This step Will ensure that the latest Status
+	// is patched into k8s, and user will be able
+	// to see the latest efs filesystem Status
+	res.Status = latest.ko.Status
 	if delta.DifferentAt("Spec.Tags") {
 		err := syncTags(
 			ctx, rm.sdkapi, rm.metrics,
@@ -463,38 +467,39 @@ func (rm *resourceManager) sdkUpdate(
 			desired.ko.Spec.Tags, latest.ko.Spec.Tags,
 		)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.Policy") {
 		err := rm.syncPolicy(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.BackupPolicy") {
 		err := rm.syncBackupPolicy(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.LifecyclePolicies") {
 		err := rm.syncLifecyclePolicies(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.FileSystemProtection") {
 		err := rm.syncFilesystemProtection(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	// To trigger to normal update we need to make sure that at least
 	// one of the following fields are different.
 	if !delta.DifferentAt("Spec.ProvisionedThroughputInMiBps") && !delta.DifferentAt("Spec.ThroughputMode") {
-		return desired, nil
+		return &resource{res}, nil
 	}
+
 	input, err := rm.newUpdateRequestPayload(ctx, desired, delta)
 	if err != nil {
 		return nil, err
@@ -503,7 +508,6 @@ func (rm *resourceManager) sdkUpdate(
 	var resp *svcsdk.UpdateFileSystemOutput
 	_ = resp
 	resp, err = rm.sdkapi.UpdateFileSystem(ctx, input)
-	return desired, nil
 	rm.metrics.RecordAPICall("UPDATE", "UpdateFileSystem", err)
 	if err != nil {
 		return nil, err
@@ -511,6 +515,8 @@ func (rm *resourceManager) sdkUpdate(
 	// Merge in the information we read from the API call above to the copy of
 	// the original Kubernetes object we passed to the function
 	ko := desired.ko.DeepCopy()
+	ko.Status = latest.ko.Status
+	return &resource{ko}, nil
 
 	if resp.AvailabilityZoneId != nil {
 		ko.Status.AvailabilityZoneID = resp.AvailabilityZoneId
diff --git a/templates/hooks/file_system/sdk_update_pre_build_request.go.tpl b/templates/hooks/file_system/sdk_update_pre_build_request.go.tpl
index 5129e8a..a2910f3 100644
--- a/templates/hooks/file_system/sdk_update_pre_build_request.go.tpl
+++ b/templates/hooks/file_system/sdk_update_pre_build_request.go.tpl
@@ -1,40 +1,44 @@
-
+	res := desired.ko.DeepCopy()
+	// This step Will ensure that the latest Status
+	// is patched into k8s, and user will be able
+	// to see the latest efs filesystem Status
+	res.Status = latest.ko.Status
 	if delta.DifferentAt("Spec.Tags") {
 		err := syncTags(
-			ctx, rm.sdkapi, rm.metrics, 
-			string(*desired.ko.Status.ACKResourceMetadata.ARN), 
+			ctx, rm.sdkapi, rm.metrics,
+			string(*desired.ko.Status.ACKResourceMetadata.ARN),
 			desired.ko.Spec.Tags, latest.ko.Spec.Tags,
 		)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.Policy") {
 		err := rm.syncPolicy(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.BackupPolicy") {
 		err := rm.syncBackupPolicy(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.LifecyclePolicies") {
 		err := rm.syncLifecyclePolicies(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	if delta.DifferentAt("Spec.FileSystemProtection") {
 		err := rm.syncFilesystemProtection(ctx, desired)
 		if err != nil {
-			return nil, err
+			return &resource{res}, err
 		}
 	}
 	// To trigger to normal update we need to make sure that at least
 	// one of the following fields are different.
-    if !delta.DifferentAt("Spec.ProvisionedThroughputInMiBps") && !delta.DifferentAt("Spec.ThroughputMode") {
-        return desired, nil
-    }
\ No newline at end of file
+	if !delta.DifferentAt("Spec.ProvisionedThroughputInMiBps") && !delta.DifferentAt("Spec.ThroughputMode") {
+		return &resource{res}, nil
+	}
diff --git a/templates/hooks/file_system/sdk_update_pre_set_output.go.tpl b/templates/hooks/file_system/sdk_update_pre_set_output.go.tpl
new file mode 100644
index 0000000..79c51cc
--- /dev/null
+++ b/templates/hooks/file_system/sdk_update_pre_set_output.go.tpl
@@ -0,0 +1,2 @@
+	ko.Status = latest.ko.Status
+	return &resource{ko}, nil
\ No newline at end of file
diff --git a/test/e2e/tests/test_file_system.py b/test/e2e/tests/test_file_system.py
index 95a7e5e..0388860 100644
--- a/test/e2e/tests/test_file_system.py
+++ b/test/e2e/tests/test_file_system.py
@@ -90,11 +90,12 @@ def simple_file_system(efs_client):
 class TestFileSystem:
 
     def test_create_delete(self, efs_client, simple_file_system):
-        (_, _, file_system_id) = simple_file_system
+        (ref, _, file_system_id) = simple_file_system
         assert file_system_id is not None
 
         validator = EFSValidator(efs_client)
         assert validator.file_system_exists(file_system_id)
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
     
     def test_create_update_delete(self, efs_client, simple_file_system):
         (ref, _, file_system_id) = simple_file_system
@@ -116,6 +117,7 @@ def test_create_update_delete(self, efs_client, simple_file_system):
 
         fs = validator.get_file_system(file_system_id)
         assert fs is not None
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
         assert fs['FileSystems'][0]['ThroughputMode'] == "elastic"
         assert fs['FileSystems'][0]['FileSystemProtection']['ReplicationOverwriteProtection'] == "DISABLED"
 
@@ -138,6 +140,7 @@ def test_update_policies(self, efs_client, simple_file_system):
         time.sleep(CREATE_WAIT_AFTER_SECONDS)
 
         observedPolicy = validator.get_file_system_policy(file_system_id)
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
         assert json.loads(policy) == json.loads(observedPolicy)
 
     def test_update_backup_policy(self, efs_client, simple_file_system):
@@ -159,6 +162,7 @@ def test_update_backup_policy(self, efs_client, simple_file_system):
 
         bp = validator.get_backup_policy(file_system_id)
         assert bp is not None
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
         assert bp['Status'] == "DISABLED"
 
     def test_update_lifecycle_policies(self, efs_client, simple_file_system):
@@ -168,6 +172,7 @@ def test_update_lifecycle_policies(self, efs_client, simple_file_system):
         validator = EFSValidator(efs_client)
         assert validator.file_system_exists(file_system_id)
 
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
         updates = {
             "spec": {
                 "lifecyclePolicies": [
@@ -180,6 +185,7 @@ def test_update_lifecycle_policies(self, efs_client, simple_file_system):
 
         lfps = validator.get_file_system_lifecycle_policy(file_system_id)
         assert lfps is not None
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
         assert lfps[0]['TransitionToIA'] == "AFTER_30_DAYS"
     
     def test_update_tags(self, efs_client, simple_file_system):
@@ -188,6 +194,7 @@ def test_update_tags(self, efs_client, simple_file_system):
 
         validator = EFSValidator(efs_client)
         assert validator.file_system_exists(file_system_id)
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
 
         desired_tags = [{
             "key": "Name",
@@ -203,6 +210,7 @@ def test_update_tags(self, efs_client, simple_file_system):
 
         fs = validator.get_file_system(file_system_id)
         assert fs is not None
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=5)
         latest_tags = fs['FileSystems'][0]["Tags"]
 
         tags.assert_ack_system_tags(


Reviews:
user: login: a-hilaly
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T16:40:50Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T16:43:23Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T16:45:09Z
user: login: a-hilaly
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T18:08:42Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T04:18:54Z
user: login: a-hilaly
body: 👍
/lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-19T22:19:52Z

Review comments:
diff_hunk: @@ -0,0 +1,2 @@
+	ko.Status = latest.ko.Status
path: templates/hooks/file_system/sdk_update_pre_set_output.go.tpl
user: login: a-hilaly
body: Why do you need this? 
created_at: 2025-05-12T16:36:02Z
author_association: MEMBER
diff_hunk: @@ -224,6 +224,10 @@ func (rm *resourceManager) syncPolicy(ctx context.Context, r *resource) (err err
 	exit := rlog.Trace("rm.syncPolicy")
 	defer func() { exit(err) }()
 
+	if r.ko.Spec.Policy == nil {
path: pkg/resource/file_system/hooks.go
user: login: a-hilaly
body: Most controllers treat this as empty policy, why skip here?
created_at: 2025-05-12T16:37:00Z
author_association: MEMBER
diff_hunk: @@ -56,8 +56,8 @@ resources:
     hooks:
       delta_pre_compare:
         code: customPreCompare(delta, a, b)
-      sdk_update_post_request:
-        code: return desired, nil
path: apis/v1alpha1/generator.yaml
user: login: a-hilaly
body: I think you should add to the description something about this and why it doesn't work also you can state that I do not recall why I did it..
created_at: 2025-05-12T16:40:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,2 @@
+	ko.Status = latest.ko.Status
path: templates/hooks/file_system/sdk_update_pre_set_output.go.tpl
user: login: michaelhtm
body: So that the latest Status (LifecycleState, etc) get patched to k8s. The issue was mainly the fact that the status was never updated, and remained in `creating` state
created_at: 2025-05-12T16:43:23Z
author_association: MEMBER
diff_hunk: @@ -224,6 +224,10 @@ func (rm *resourceManager) syncPolicy(ctx context.Context, r *resource) (err err
 	exit := rlog.Trace("rm.syncPolicy")
 	defer func() { exit(err) }()
 
+	if r.ko.Spec.Policy == nil {
path: pkg/resource/file_system/hooks.go
user: login: michaelhtm
body: PutPolicy requires Policy. Maybe if nil, we should instead call [`DeleteFileSystemPolicy`](https://docs.aws.amazon.com/efs/latest/ug/API_DeleteFileSystemPolicy.html)?
created_at: 2025-05-12T16:45:09Z
author_association: MEMBER
diff_hunk: @@ -224,6 +224,10 @@ func (rm *resourceManager) syncPolicy(ctx context.Context, r *resource) (err err
 	exit := rlog.Trace("rm.syncPolicy")
 	defer func() { exit(err) }()
 
+	if r.ko.Spec.Policy == nil {
path: pkg/resource/file_system/hooks.go
user: login: a-hilaly
body: Yes!
created_at: 2025-05-12T18:08:42Z
author_association: MEMBER
diff_hunk: @@ -224,6 +224,10 @@ func (rm *resourceManager) syncPolicy(ctx context.Context, r *resource) (err err
 	exit := rlog.Trace("rm.syncPolicy")
 	defer func() { exit(err) }()
 
+	if r.ko.Spec.Policy == nil {
path: pkg/resource/file_system/hooks.go
user: login: michaelhtm
body: Added to separate [PR](https://github.com/aws-controllers-k8s/efs-controller/pull/45)
created_at: 2025-05-13T04:18:54Z
author_association: MEMBER

PR comments:
user: login: a-hilaly
created_at: 2025-05-12T17:45:48Z
body: @michaelhtm do you wanna add some e2e for this?
author_association: MEMBER
user: login: ack-prow[bot]
created_at: 2025-05-14T00:39:45Z
body: @michaelhtm: The following test **failed**, say `/retest` to rerun all failed tests or `/retest-required` to rerun all mandatory failed tests:

Test name | Commit | Details | Required | Rerun command
--- | --- | --- | --- | ---
efs-verify-attribution | d15e70e3af81f72f6b7056487be574c62ba7cd22 | [link](https://prow.ack.aws.dev/view/s3/ack-prow-logs/pr-logs/pull/aws-controllers-k8s_efs-controller/41/efs-verify-attribution/1922451677370126336) | false | `/test efs-verify-attribution`

[Full PR test history](https://prow.ack.aws.dev/pr-history?org=aws-controllers-k8s&repo=efs-controller&pr=41). [Your PR dashboard](https://prow.ack.aws.dev/pr?query=is:pr+state:open+author:michaelhtm).

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository. I understand the commands that are listed [here](https://go.k8s.io/bot-commands).
</details>
<!-- test report -->
author_association: NONE
user: login: ack-prow[bot]
created_at: 2025-05-19T22:19:57Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/aws-controllers-k8s/efs-controller/pull/41#pullrequestreview-2852104585" title="LGTM">a-hilaly</a>*, *<a href="https://github.com/aws-controllers-k8s/efs-controller/pull/41#" title="Author self-approved">michaelhtm</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=aws-controllers-k8s%2Fefs-controller).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/aws-controllers-k8s/efs-controller/blob/main/OWNERS)~~ [a-hilaly,michaelhtm]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE