Repository: aws-controllers-k8s/opensearchservice-controller
Pull request #89: fix: Set missing fields during update cluster configuration
Author: login: michaelhtm
Labels: name: lgtm
description: Indicates that a PR is ready to be merged.
name: approved
Created: 2025-05-06T05:10:38Z | Merged: 2025-05-12T19:48:07Z

Issue [#2219](https://github.com/aws-controllers-k8s/community/issues/2219)

Description of changes:
Set missing fields during domain update operation

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 701471e..5519307 100755
--- a/apis/v1alpha1/ack-generate-metadata.yaml
+++ b/apis/v1alpha1/ack-generate-metadata.yaml
@@ -1,7 +1,7 @@
 ack_generate_info:
-  build_date: "2025-05-02T16:41:02Z"
+  build_date: "2025-05-06T05:27:03Z"
   build_hash: f8dc5330705b3752ce07dce0ac831161fd4cb14f
-  go_version: go1.24.2
+  go_version: go1.24.1
   version: v0.45.0
 api_directory_checksum: 13af9d3a7962f45a46b4bedaca965631b194a9a8
 api_version: v1alpha1
diff --git a/pkg/resource/domain/hooks.go b/pkg/resource/domain/hooks.go
index ea17cf2..c5014cd 100644
--- a/pkg/resource/domain/hooks.go
+++ b/pkg/resource/domain/hooks.go
@@ -16,8 +16,10 @@ package domain
 import (
 	"context"
 	"errors"
+	"fmt"
 
 	"github.com/aws-controllers-k8s/opensearchservice-controller/apis/v1alpha1"
+	svcapitypes "github.com/aws-controllers-k8s/opensearchservice-controller/apis/v1alpha1"
 	ackv1alpha1 "github.com/aws-controllers-k8s/runtime/apis/core/v1alpha1"
 	ackcompare "github.com/aws-controllers-k8s/runtime/pkg/compare"
 	ackcondition "github.com/aws-controllers-k8s/runtime/pkg/condition"
@@ -37,6 +39,27 @@ var (
 	)
 )
 
+func checkDomainStatus(resp *svcsdk.DescribeDomainOutput, ko *svcapitypes.Domain) {
+	if resp.DomainStatus.AutoTuneOptions != nil {
+		if ready, err := isAutoTuneOptionReady(string(resp.DomainStatus.AutoTuneOptions.State), resp.DomainStatus.AutoTuneOptions.ErrorMessage); err != nil {
+			reason := err.Error()
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		} else if !ready {
+			reason := fmt.Sprintf("waiting for AutotuneOptions to sync. Current state: %s", resp.DomainStatus.AutoTuneOptions.State)
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		}
+		ko.Spec.AutoTuneOptions.DesiredState = aws.String(string(resp.DomainStatus.AutoTuneOptions.State))
+	}
+
+	if domainProcessing(&resource{ko}) {
+		// Setting resource synced condition to false will trigger a requeue of
+		// the resource. No need to return a requeue error here.
+		ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, nil)
+	} else {
+		ackcondition.SetSynced(&resource{ko}, corev1.ConditionTrue, nil, nil)
+	}
+}
+
 // domainProcessing returns true if the supplied domain is in a state of
 // processing
 func domainProcessing(r *resource) bool {
@@ -46,21 +69,44 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(state string, errorMessage *string) (bool, error) {
+	switch svcsdktypes.AutoTuneState(state) {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if errorMessage != nil {
+			return false, fmt.Errorf("error: %s", *errorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
+	}
+}
+
 func (rm *resourceManager) customUpdateDomain(ctx context.Context, desired, latest *resource,
 	delta *ackcompare.Delta) (updated *resource, err error) {
 	rlog := ackrtlog.FromContext(ctx)
 	exit := rlog.Trace("rm.customUpdateDomain")
 	defer exit(err)
 
+	if latest.ko.Spec.AutoTuneOptions != nil &&
+		latest.ko.Spec.AutoTuneOptions.DesiredState != nil {
+		if ready, _ := isAutoTuneOptionReady(*latest.ko.Spec.AutoTuneOptions.DesiredState, nil); !ready {
+			return latest, ackrequeue.Needed(fmt.Errorf("autoTuneOption is updating"))
+		}
+	}
+
 	if domainProcessing(latest) {
 		msg := "Domain is currently processing configuration changes"
 		ackcondition.SetSynced(desired, corev1.ConditionFalse, &msg, nil)
-		return desired, requeueWaitWhileProcessing
+		return latest, requeueWaitWhileProcessing
 	}
 	if latest.ko.Status.UpgradeProcessing != nil && *latest.ko.Status.UpgradeProcessing {
 		msg := "Domain is currently upgrading software"
 		ackcondition.SetSynced(desired, corev1.ConditionFalse, &msg, nil)
-		return desired, requeueWaitWhileProcessing
+		return latest, requeueWaitWhileProcessing
 	}
 
 	if desired.ko.Spec.EngineVersion != nil && delta.DifferentAt("Spec.EngineVersion") {
@@ -185,6 +231,7 @@ func (rm *resourceManager) customUpdateDomain(ctx context.Context, desired, late
 		}
 		ko.Spec.AutoTuneOptions = &v1alpha1.AutoTuneOptionsInput{
 			DesiredState:         aws.String(string(resp.DomainConfig.AutoTuneOptions.Options.DesiredState)),
+			UseOffPeakWindow:     resp.DomainConfig.AutoTuneOptions.Options.UseOffPeakWindow,
 			MaintenanceSchedules: maintSchedules,
 		}
 	} else {
@@ -205,11 +252,15 @@ func (rm *resourceManager) customUpdateDomain(ctx context.Context, desired, late
 			}
 		}
 		ko.Spec.ClusterConfig = &v1alpha1.ClusterConfig{
-			ColdStorageOptions:     csOptions,
-			DedicatedMasterEnabled: resp.DomainConfig.ClusterConfig.Options.DedicatedMasterEnabled,
-			WarmEnabled:            resp.DomainConfig.ClusterConfig.Options.WarmEnabled,
-			ZoneAwarenessConfig:    zaConfig,
-			ZoneAwarenessEnabled:   resp.DomainConfig.ClusterConfig.Options.ZoneAwarenessEnabled,
+			ColdStorageOptions:        csOptions,
+			DedicatedMasterCount:      int64OrNil(resp.DomainConfig.ClusterConfig.Options.DedicatedMasterCount),
+			DedicatedMasterEnabled:    resp.DomainConfig.ClusterConfig.Options.DedicatedMasterEnabled,
+			InstanceCount:             int64OrNil(resp.DomainConfig.ClusterConfig.Options.InstanceCount),
+			WarmCount:                 int64OrNil(resp.DomainConfig.ClusterConfig.Options.WarmCount),
+			WarmEnabled:               resp.DomainConfig.ClusterConfig.Options.WarmEnabled,
+			ZoneAwarenessConfig:       zaConfig,
+			ZoneAwarenessEnabled:      resp.DomainConfig.ClusterConfig.Options.ZoneAwarenessEnabled,
+			MultiAZWithStandbyEnabled: resp.DomainConfig.ClusterConfig.Options.MultiAZWithStandbyEnabled,
 		}
 		if resp.DomainConfig.ClusterConfig.Options.DedicatedMasterCount != nil {
 			ko.Spec.ClusterConfig.DedicatedMasterCount = aws.Int64(int64(*resp.DomainConfig.ClusterConfig.Options.DedicatedMasterCount))
@@ -285,6 +336,11 @@ func (rm *resourceManager) customUpdateDomain(ctx context.Context, desired, late
 	} else {
 		ko.Spec.EngineVersion = nil
 	}
+	if resp.DomainConfig.IPAddressType != nil {
+		ko.Spec.IPAddressType = aws.String(string(resp.DomainConfig.IPAddressType.Options))
+	} else {
+		ko.Spec.IPAddressType = nil
+	}
 	if resp.DomainConfig.NodeToNodeEncryptionOptions != nil {
 		ko.Spec.NodeToNodeEncryptionOptions = &v1alpha1.NodeToNodeEncryptionOptions{
 			Enabled: resp.DomainConfig.NodeToNodeEncryptionOptions.Options.Enabled,
@@ -292,6 +348,41 @@ func (rm *resourceManager) customUpdateDomain(ctx context.Context, desired, late
 	} else {
 		ko.Spec.NodeToNodeEncryptionOptions = nil
 	}
+	if resp.DomainConfig.SoftwareUpdateOptions != nil {
+		ko.Spec.SoftwareUpdateOptions = &v1alpha1.SoftwareUpdateOptions{
+			AutoSoftwareUpdateEnabled: resp.DomainConfig.SoftwareUpdateOptions.Options.AutoSoftwareUpdateEnabled,
+		}
+	} else {
+		ko.Spec.SoftwareUpdateOptions = nil
+	}
+	if resp.DomainConfig.AIMLOptions != nil && resp.DomainConfig.AIMLOptions.Options != nil {
+		if resp.DomainConfig.AIMLOptions.Options.NaturalLanguageQueryGenerationOptions != nil {
+			ko.Spec.AIMLOptions = &v1alpha1.AIMLOptionsInput{
+				NATuralLanguageQueryGenerationOptions: &v1alpha1.NATuralLanguageQueryGenerationOptionsInput{
+					DesiredState: aws.String(string(resp.DomainConfig.AIMLOptions.Options.NaturalLanguageQueryGenerationOptions.DesiredState)),
+				},
+			}
+		}
+	} else {
+		ko.Spec.AIMLOptions = nil
+	}
+	if resp.DomainConfig.OffPeakWindowOptions != nil && resp.DomainConfig.OffPeakWindowOptions.Options != nil {
+		var offPeakWindow *v1alpha1.OffPeakWindow
+		if resp.DomainConfig.OffPeakWindowOptions.Options.OffPeakWindow != nil {
+			offPeakWindow = &v1alpha1.OffPeakWindow{
+				WindowStartTime: &v1alpha1.WindowStartTime{
+					Hours:   aws.Int64(resp.DomainConfig.OffPeakWindowOptions.Options.OffPeakWindow.WindowStartTime.Hours),
+					Minutes: aws.Int64(resp.DomainConfig.OffPeakWindowOptions.Options.OffPeakWindow.WindowStartTime.Minutes),
+				},
+			}
+		}
+		ko.Spec.OffPeakWindowOptions = &v1alpha1.OffPeakWindowOptions{
+			Enabled:       resp.DomainConfig.OffPeakWindowOptions.Options.Enabled,
+			OffPeakWindow: offPeakWindow,
+		}
+	} else {
+		ko.Spec.OffPeakWindowOptions = nil
+	}
 
 	rm.setStatusDefaults(ko)
 
@@ -401,6 +492,9 @@ func (rm *resourceManager) newCustomUpdateRequestPayload(
 		if desired.ko.Spec.AutoTuneOptions.DesiredState != nil {
 			f3.DesiredState = svcsdktypes.AutoTuneDesiredState(*desired.ko.Spec.AutoTuneOptions.DesiredState)
 		}
+		if desired.ko.Spec.AutoTuneOptions.UseOffPeakWindow != nil {
+			f3.UseOffPeakWindow = desired.ko.Spec.AutoTuneOptions.UseOffPeakWindow
+		}
 		if desired.ko.Spec.AutoTuneOptions.MaintenanceSchedules != nil {
 			f3f1 := []svcsdktypes.AutoTuneMaintenanceSchedule{}
 			for _, f3f1iter := range desired.ko.Spec.AutoTuneOptions.MaintenanceSchedules {
@@ -471,6 +565,9 @@ func (rm *resourceManager) newCustomUpdateRequestPayload(
 		if desired.ko.Spec.ClusterConfig.ZoneAwarenessEnabled != nil {
 			f4.ZoneAwarenessEnabled = desired.ko.Spec.ClusterConfig.ZoneAwarenessEnabled
 		}
+		if desired.ko.Spec.ClusterConfig.MultiAZWithStandbyEnabled != nil {
+			f4.MultiAZWithStandbyEnabled = desired.ko.Spec.ClusterConfig.MultiAZWithStandbyEnabled
+		}
 		res.ClusterConfig = f4
 	}
 
@@ -586,5 +683,59 @@ func (rm *resourceManager) newCustomUpdateRequestPayload(
 		res.VPCOptions = f14
 	}
 
+	if desired.ko.Spec.IPAddressType != nil && delta.DifferentAt("Spec.IPAddressType") {
+		res.IPAddressType = svcsdktypes.IPAddressType(*desired.ko.Spec.IPAddressType)
+	}
+
+	if desired.ko.Spec.SoftwareUpdateOptions != nil && delta.DifferentAt("Spec.SoftwareUpdateOptions") {
+		f15 := &svcsdktypes.SoftwareUpdateOptions{}
+		if desired.ko.Spec.SoftwareUpdateOptions.AutoSoftwareUpdateEnabled != nil {
+			f15.AutoSoftwareUpdateEnabled = desired.ko.Spec.SoftwareUpdateOptions.AutoSoftwareUpdateEnabled
+		}
+		res.SoftwareUpdateOptions = f15
+	}
+
+	if desired.ko.Spec.AIMLOptions != nil && delta.DifferentAt("Spec.AIMLOptions") {
+		f16 := &svcsdktypes.AIMLOptionsInput{}
+		if desired.ko.Spec.AIMLOptions.NATuralLanguageQueryGenerationOptions != nil {
+			f16f0 := &svcsdktypes.NaturalLanguageQueryGenerationOptionsInput{}
+			if desired.ko.Spec.AIMLOptions.NATuralLanguageQueryGenerationOptions.DesiredState != nil {
+				f16f0.DesiredState = svcsdktypes.NaturalLanguageQueryGenerationDesiredState(*desired.ko.Spec.AIMLOptions.NATuralLanguageQueryGenerationOptions.DesiredState)
+			}
+			f16.NaturalLanguageQueryGenerationOptions = f16f0
+		}
+		res.AIMLOptions = f16
+	}
+
+	if desired.ko.Spec.OffPeakWindowOptions != nil && delta.DifferentAt("Spec.OffPeakWindowOptions") {
+		f17 := &svcsdktypes.OffPeakWindowOptions{}
+		if desired.ko.Spec.OffPeakWindowOptions.Enabled != nil {
+			f17.Enabled = desired.ko.Spec.OffPeakWindowOptions.Enabled
+		}
+		if desired.ko.Spec.OffPeakWindowOptions.OffPeakWindow != nil {
+			f17f1 := &svcsdktypes.OffPeakWindow{}
+			if desired.ko.Spec.OffPeakWindowOptions.OffPeakWindow.WindowStartTime != nil {
+				f17f1f1 := &svcsdktypes.WindowStartTime{}
+				if desired.ko.Spec.OffPeakWindowOptions.OffPeakWindow.WindowStartTime.Hours != nil {
+					f17f1f1.Hours = *desired.ko.Spec.OffPeakWindowOptions.OffPeakWindow.WindowStartTime.Hours
+				}
+				if desired.ko.Spec.OffPeakWindowOptions.OffPeakWindow.WindowStartTime.Minutes != nil {
+					f17f1f1.Minutes = *desired.ko.Spec.OffPeakWindowOptions.OffPeakWindow.WindowStartTime.Minutes
+				}
+				f17f1.WindowStartTime = f17f1f1
+			}
+			f17.OffPeakWindow = f17f1
+		}
+		res.OffPeakWindowOptions = f17
+	}
+
 	return res, nil
 }
+
+func int64OrNil(num *int32) *int64 {
+	if num == nil {
+		return nil
+	}
+
+	return aws.Int64(int64(*num))
+}
diff --git a/pkg/resource/domain/sdk.go b/pkg/resource/domain/sdk.go
index d28962f..6324406 100644
--- a/pkg/resource/domain/sdk.go
+++ b/pkg/resource/domain/sdk.go
@@ -535,13 +535,7 @@ func (rm *resourceManager) sdkFind(
 	}
 
 	rm.setStatusDefaults(ko)
-	if domainProcessing(&resource{ko}) {
-		// Setting resource synced condition to false will trigger a requeue of
-		// the resource. No need to return a requeue error here.
-		ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, nil)
-	} else {
-		ackcondition.SetSynced(&resource{ko}, corev1.ConditionTrue, nil, nil)
-	}
+	checkDomainStatus(resp, ko)
 
 	return &resource{ko}, nil
 }
diff --git a/templates/hooks/domain/sdk_read_one_post_set_output.go.tpl b/templates/hooks/domain/sdk_read_one_post_set_output.go.tpl
index 9739e50..3cfe62c 100644
--- a/templates/hooks/domain/sdk_read_one_post_set_output.go.tpl
+++ b/templates/hooks/domain/sdk_read_one_post_set_output.go.tpl
@@ -1,8 +1 @@
-	if domainProcessing(&resource{ko}) {
-		// Setting resource synced condition to false will trigger a requeue of
-		// the resource. No need to return a requeue error here.
-		ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, nil)
-	} else {
-		ackcondition.SetSynced(&resource{ko}, corev1.ConditionTrue, nil, nil)
-	}
-
+	checkDomainStatus(resp, ko)
diff --git a/test/e2e/tests/test_domain.py b/test/e2e/tests/test_domain.py
index f8972fd..22d5396 100644
--- a/test/e2e/tests/test_domain.py
+++ b/test/e2e/tests/test_domain.py
@@ -87,7 +87,7 @@ def es_7_9_domain(os_client, resources: BootstrapResources):
     )
     k8s.create_custom_resource(ref, resource_data)
     k8s.wait_resource_consumed_by_controller(ref)
-    condition.assert_not_synced(ref)
+    assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "False", wait_periods=10)
 
     # An OpenSearch Domain gets its `DomainStatus.Created` field set to
     # `True` almost immediately, however the `DomainStatus.Processing` field
@@ -105,7 +105,7 @@ def es_7_9_domain(os_client, resources: BootstrapResources):
     logging.info(f"ES Domain {resource.name} creation succeeded and DomainStatus.Processing is now False")
 
     time.sleep(CHECK_STATUS_WAIT_SECONDS)
-    condition.assert_synced(ref)
+    assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=10)
 
     yield ref, resource
 
@@ -144,14 +144,14 @@ def es_2d3m_multi_az_no_vpc_7_9_domain(os_client, resources: BootstrapResources)
     )
     k8s.create_custom_resource(ref, resource_data)
     k8s.wait_resource_consumed_by_controller(ref)
-    condition.assert_not_synced(ref)
+    assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "False", wait_periods=10)
 
     domain.wait_until(ref.name, domain.processing_matches(False))
 
     logging.info(f"ES Domain {resource.name} creation succeeded and DomainStatus.Processing is now False")
 
     time.sleep(CHECK_STATUS_WAIT_SECONDS)
-    condition.assert_synced(ref)
+    assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=10)
 
     yield ref, resource
 
@@ -200,14 +200,14 @@ def es_2d3m_multi_az_vpc_2_subnet7_9_domain(os_client, resources: BootstrapResou
     )
     k8s.create_custom_resource(ref, resource_data)
     k8s.wait_resource_consumed_by_controller(ref)
-    condition.assert_not_synced(ref)
+    assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "False", wait_periods=10)
 
     domain.wait_until(ref.name, domain.processing_matches(False))
 
     logging.info(f"OpenSearch Domain {resource.name} creation succeeded and DomainStatus.Processing is now False")
 
     time.sleep(CHECK_STATUS_WAIT_SECONDS)
-    condition.assert_synced(ref)
+    assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=10)
 
     yield ref, resource
 
@@ -244,7 +244,26 @@ def test_create_delete_es_7_9(self, es_7_9_domain):
         # now we will modify the engine version to test upgrades
         # similar to creating a new domain, this takes a long time, often 20+ minutes
         updates = {
-            "spec": {"engineVersion": "Elasticsearch_7.10"},
+            "spec": {
+                "engineVersion": "Elasticsearch_7.10",
+                "autoTuneOptions": {
+                    "useOffPeakWindow": False
+                },
+                "clusterConfig": {
+                    "multiAZWithStandbyEnabled": False
+                },
+                "offPeakWindowOptions": {
+                    "offPeakWindow": {
+                        "windowStartTime": {
+                            "hours": 23,
+                            "minutes": 30
+                        }
+                    }
+                },
+                "softwareUpdateOptions": {
+                    "autoSoftwareUpdateEnabled": True
+                }
+            }
         }
         k8s.patch_custom_resource(ref, updates)
 
@@ -260,8 +279,35 @@ def test_create_delete_es_7_9(self, es_7_9_domain):
                 time.sleep(CHECK_STATUS_WAIT_SECONDS)
                 continue
             else:
-                assert latest['DomainStatus']['EngineVersion'] == "Elasticsearch_7.10"
                 break
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=10)
+        cr = k8s.get_resource(ref)
+
+        assert latest['DomainStatus']['SoftwareUpdateOptions']["AutoSoftwareUpdateEnabled"] is True
+
+        assert "engineVersion" in cr["spec"]
+        assert cr["spec"]["engineVersion"] == latest['DomainStatus']['EngineVersion'] 
+
+        assert "autoTuneOptions" in cr["spec"]
+        assert  "useOffPeakWindow" in cr["spec"]["autoTuneOptions"]
+        assert cr["spec"]["autoTuneOptions"]["useOffPeakWindow"] == False
+
+        assert "clusterConfig" in cr["spec"]
+        assert  "multiAZWithStandbyEnabled" in cr["spec"]["clusterConfig"]
+        assert cr["spec"]["clusterConfig"]["multiAZWithStandbyEnabled"] == False
+
+        assert "offPeakWindowOptions" in cr["spec"]
+        assert  "enabled" in cr["spec"]["offPeakWindowOptions"]
+        assert cr["spec"]["offPeakWindowOptions"]["enabled"] == latest['DomainStatus']['OffPeakWindowOptions']["Enabled"]
+
+        assert  "offPeakWindow" in cr["spec"]["offPeakWindowOptions"]
+        assert  "windowStartTime" in cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]
+        assert  "hours" in cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]
+        assert  "minutes" in cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]
+        latest_hours = latest['DomainStatus']['OffPeakWindowOptions']["OffPeakWindow"]["WindowStartTime"]["Hours"]
+        latest_minutes = latest['DomainStatus']['OffPeakWindowOptions']["OffPeakWindow"]["WindowStartTime"]["Minutes"]
+        assert  cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]["hours"] == latest_hours
+        assert  cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]["minutes"] == latest_minutes
 
     def test_create_delete_es_2d3m_multi_az_no_vpc_7_9(self, es_2d3m_multi_az_no_vpc_7_9_domain):
         ref, resource = es_2d3m_multi_az_no_vpc_7_9_domain


Reviews:
user: login: knottnt
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T18:32:00Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T20:08:25Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T20:10:52Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T20:15:47Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T20:17:47Z
user: login: knottnt
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-08T21:10:55Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T21:34:02Z
user: login: rushmash91
body: Nice @michaelhtm ! left a few comments
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:11:11Z
user: login: michaelhtm
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T21:14:29Z
user: login: rushmash91
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T21:17:34Z

Review comments:
diff_hunk: @@ -1,7 +1,7 @@
 ack_generate_info:
-  build_date: "2025-05-02T16:41:02Z"
+  build_date: "2025-05-06T05:27:03Z"
   build_hash: f8dc5330705b3752ce07dce0ac831161fd4cb14f
-  go_version: go1.24.2
+  go_version: go1.24.1
path: apis/v1alpha1/ack-generate-metadata.yaml
user: login: knottnt
body: Q: Does it matter much that an older version of Go was used?
created_at: 2025-05-08T17:57:55Z
author_association: NONE
diff_hunk: @@ -46,21 +51,68 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(autoTuneOption *svcsdktypes.AutoTuneOptionsOutput) (bool, error) {
+	switch autoTuneOption.State {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if autoTuneOption.ErrorMessage != nil {
+			return false, fmt.Errorf("error: %s", *autoTuneOption.ErrorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
path: pkg/resource/domain/hooks.go
user: login: knottnt
body: If this represents an unknown/unhandled state it could be helpful to log that the state wasn't recognized.
created_at: 2025-05-08T18:06:57Z
author_association: NONE
diff_hunk: @@ -28,13 +30,16 @@ import (
 	svcsdktypes "github.com/aws/aws-sdk-go-v2/service/opensearch/types"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	svcapitypes "github.com/aws-controllers-k8s/opensearchservice-controller/apis/v1alpha1"
 )
 
 var (
 	requeueWaitWhileProcessing = ackrequeue.NeededAfter(
 		errors.New("domain is currently processing changes, cannot be modified or deleted"),
 		ackrequeue.DefaultRequeueAfterDuration,
 	)
+	noAutoTuneInstances = []string{"t2", "t3"}
path: pkg/resource/domain/hooks.go
user: login: knottnt
body: Is there a way of programmatically checking this or an error code that the AWS API returns that indicates this? It seems like this hard-coded list could easily get out of sync with what's actually not supported in AWS.
created_at: 2025-05-08T18:15:28Z
author_association: NONE
diff_hunk: @@ -46,21 +51,68 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(autoTuneOption *svcsdktypes.AutoTuneOptionsOutput) (bool, error) {
+	switch autoTuneOption.State {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if autoTuneOption.ErrorMessage != nil {
+			return false, fmt.Errorf("error: %s", *autoTuneOption.ErrorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
+	}
+}
+
+func isAutoTuneOptionAvailable(autoTuneOption *svcapitypes.AutoTuneOptionsInput) bool {
+	if autoTuneOption == nil {
path: pkg/resource/domain/hooks.go
user: login: knottnt
body: Q: What is meant by "Available" here? It seems odd that something would be considered available when something is nil or disabled.
created_at: 2025-05-08T18:23:33Z
author_association: NONE
diff_hunk: @@ -1,7 +1,7 @@
 ack_generate_info:
-  build_date: "2025-05-02T16:41:02Z"
+  build_date: "2025-05-06T05:27:03Z"
   build_hash: f8dc5330705b3752ce07dce0ac831161fd4cb14f
-  go_version: go1.24.2
+  go_version: go1.24.1
path: apis/v1alpha1/ack-generate-metadata.yaml
user: login: michaelhtm
body: not necessarily. The release uses the [version in test-infra](https://github.com/aws-controllers-k8s/test-infra/blob/62b132c217bd912960e268962664c545a6fa824d/build_config.yaml#L1) to build the controller image

created_at: 2025-05-08T20:08:24Z
author_association: MEMBER
diff_hunk: @@ -46,21 +51,68 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(autoTuneOption *svcsdktypes.AutoTuneOptionsOutput) (bool, error) {
+	switch autoTuneOption.State {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if autoTuneOption.ErrorMessage != nil {
+			return false, fmt.Errorf("error: %s", *autoTuneOption.ErrorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
path: pkg/resource/domain/hooks.go
user: login: michaelhtm
body: it is a recognized state, (Enabling, Disabling, etc.). Just wanted to requeue regardless of the state as long as it's not ready
created_at: 2025-05-08T20:10:51Z
author_association: MEMBER
diff_hunk: @@ -28,13 +30,16 @@ import (
 	svcsdktypes "github.com/aws/aws-sdk-go-v2/service/opensearch/types"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	svcapitypes "github.com/aws-controllers-k8s/opensearchservice-controller/apis/v1alpha1"
 )
 
 var (
 	requeueWaitWhileProcessing = ackrequeue.NeededAfter(
 		errors.New("domain is currently processing changes, cannot be modified or deleted"),
 		ackrequeue.DefaultRequeueAfterDuration,
 	)
+	noAutoTuneInstances = []string{"t2", "t3"}
path: pkg/resource/domain/hooks.go
user: login: michaelhtm
body: You're right, it's not being used currently tho so i'll remove momentarily.
created_at: 2025-05-08T20:15:47Z
author_association: MEMBER
diff_hunk: @@ -46,21 +51,68 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(autoTuneOption *svcsdktypes.AutoTuneOptionsOutput) (bool, error) {
+	switch autoTuneOption.State {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if autoTuneOption.ErrorMessage != nil {
+			return false, fmt.Errorf("error: %s", *autoTuneOption.ErrorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
+	}
+}
+
+func isAutoTuneOptionAvailable(autoTuneOption *svcapitypes.AutoTuneOptionsInput) bool {
+	if autoTuneOption == nil {
path: pkg/resource/domain/hooks.go
user: login: michaelhtm
body: this function is just another form of `isAutoTuneOptionReady` which  accepts the `svcsdktypes.AutotuneOptionsOutput`.
created_at: 2025-05-08T20:17:47Z
author_association: MEMBER
diff_hunk: @@ -46,21 +51,68 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(autoTuneOption *svcsdktypes.AutoTuneOptionsOutput) (bool, error) {
+	switch autoTuneOption.State {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if autoTuneOption.ErrorMessage != nil {
+			return false, fmt.Errorf("error: %s", *autoTuneOption.ErrorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
+	}
+}
+
+func isAutoTuneOptionAvailable(autoTuneOption *svcapitypes.AutoTuneOptionsInput) bool {
+	if autoTuneOption == nil {
path: pkg/resource/domain/hooks.go
user: login: knottnt
body: Is this more of an overload of `isAutoTuneOptionReady` where the semantic meaning is the same or does it mean something different for AutoTune to be available vs ready?
created_at: 2025-05-08T21:10:55Z
author_association: NONE
diff_hunk: @@ -46,21 +51,68 @@ func domainProcessing(r *resource) bool {
 	return *r.ko.Status.Processing
 }
 
+func isAutoTuneOptionReady(autoTuneOption *svcsdktypes.AutoTuneOptionsOutput) (bool, error) {
+	switch autoTuneOption.State {
+	case svcsdktypes.AutoTuneStateEnabled, svcsdktypes.AutoTuneStateDisabled:
+		return true, nil
+
+	case svcsdktypes.AutoTuneStateError:
+		if autoTuneOption.ErrorMessage != nil {
+			return false, fmt.Errorf("error: %s", *autoTuneOption.ErrorMessage)
+		}
+		return false, fmt.Errorf("there is an error when updating AutoTuneOptions")
+
+	default:
+		return false, nil
+	}
+}
+
+func isAutoTuneOptionAvailable(autoTuneOption *svcapitypes.AutoTuneOptionsInput) bool {
+	if autoTuneOption == nil {
path: pkg/resource/domain/hooks.go
user: login: michaelhtm
body: There's no difference really. Just needed to use it with different structs. removing `isAutoTuneOptionAvailable` to avoid confusion.
created_at: 2025-05-08T21:34:02Z
author_association: MEMBER
diff_hunk: @@ -1,3 +1,14 @@
+	if resp.DomainStatus.AutoTuneOptions != nil {
+		if ready, err := isAutoTuneOptionReady(string(resp.DomainStatus.AutoTuneOptions.State), resp.DomainStatus.AutoTuneOptions.ErrorMessage); err != nil {
+			reason := err.Error()
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		} else if !ready {
+			reason := fmt.Sprintf("waiting for AutotuneOptions to sync. Current state: %s", resp.DomainStatus.AutoTuneOptions.State)
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		}
+		ko.Spec.AutoTuneOptions.DesiredState = aws.String(string(resp.DomainStatus.AutoTuneOptions.State))
+	}
+
path: templates/hooks/domain/sdk_read_one_post_set_output.go.tpl
user: login: rushmash91
body: can be moved to a `setAdditionalFields` in hooks
created_at: 2025-05-09T19:48:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -260,8 +279,37 @@ def test_create_delete_es_7_9(self, es_7_9_domain):
                 time.sleep(CHECK_STATUS_WAIT_SECONDS)
                 continue
             else:
-                assert latest['DomainStatus']['EngineVersion'] == "Elasticsearch_7.10"
                 break
+        assert k8s.wait_on_condition(ref, "ACK.ResourceSynced", "True", wait_periods=10)
+        cr = k8s.get_resource(ref)
+
+        assert latest['DomainStatus']['EngineVersion'] == "Elasticsearch_7.10"
+        assert latest['DomainStatus']['OffPeakWindowOptions']["Enabled"] is True
+        assert latest['DomainStatus']['OffPeakWindowOptions']["OffPeakWindow"]["WindowStartTime"]["Hours"] == 23
+        assert latest['DomainStatus']['OffPeakWindowOptions']["OffPeakWindow"]["WindowStartTime"]["Minutes"] == 30
+        assert latest['DomainStatus']['SoftwareUpdateOptions']["AutoSoftwareUpdateEnabled"] is True
+
+        assert "engineVersion" in cr["spec"]
+        assert cr["spec"]["engineVersion"] == "Elasticsearch_7.10"
+
+        assert "autoTuneOptions" in cr["spec"]
+        assert  "useOffPeakWindow" in cr["spec"]["autoTuneOptions"]
+        assert cr["spec"]["autoTuneOptions"]["useOffPeakWindow"] == False
+
+        assert "clusterConfig" in cr["spec"]
+        assert  "multiAZWithStandbyEnabled" in cr["spec"]["clusterConfig"]
+        assert cr["spec"]["clusterConfig"]["multiAZWithStandbyEnabled"] == False
+
+        assert "offPeakWindowOptions" in cr["spec"]
+        assert  "enabled" in cr["spec"]["offPeakWindowOptions"]
+        assert cr["spec"]["offPeakWindowOptions"]["enabled"] == True
+
+        assert  "offPeakWindow" in cr["spec"]["offPeakWindowOptions"]
+        assert  "windowStartTime" in cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]
+        assert  "hours" in cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]
+        assert  "minutes" in cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]
+        assert  cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]["hours"] == 23
+        assert  cr["spec"]["offPeakWindowOptions"]["offPeakWindow"]["windowStartTime"]["minutes"] == 30
 
path: test/e2e/tests/test_domain.py
user: login: rushmash91
body: We can compare latest and cr values directly instead of one by one with hardcoded values?
created_at: 2025-05-09T21:09:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,3 +1,14 @@
+	if resp.DomainStatus.AutoTuneOptions != nil {
+		if ready, err := isAutoTuneOptionReady(string(resp.DomainStatus.AutoTuneOptions.State), resp.DomainStatus.AutoTuneOptions.ErrorMessage); err != nil {
+			reason := err.Error()
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		} else if !ready {
+			reason := fmt.Sprintf("waiting for AutotuneOptions to sync. Current state: %s", resp.DomainStatus.AutoTuneOptions.State)
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		}
+		ko.Spec.AutoTuneOptions.DesiredState = aws.String(string(resp.DomainStatus.AutoTuneOptions.State))
+	}
+
path: templates/hooks/domain/sdk_read_one_post_set_output.go.tpl
user: login: michaelhtm
body: The portion is mainly checking for resource readiness..
created_at: 2025-05-09T21:14:28Z
author_association: MEMBER
diff_hunk: @@ -1,3 +1,14 @@
+	if resp.DomainStatus.AutoTuneOptions != nil {
+		if ready, err := isAutoTuneOptionReady(string(resp.DomainStatus.AutoTuneOptions.State), resp.DomainStatus.AutoTuneOptions.ErrorMessage); err != nil {
+			reason := err.Error()
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		} else if !ready {
+			reason := fmt.Sprintf("waiting for AutotuneOptions to sync. Current state: %s", resp.DomainStatus.AutoTuneOptions.State)
+			ackcondition.SetSynced(&resource{ko}, corev1.ConditionFalse, nil, &reason)
+		}
+		ko.Spec.AutoTuneOptions.DesiredState = aws.String(string(resp.DomainStatus.AutoTuneOptions.State))
+	}
+
path: templates/hooks/domain/sdk_read_one_post_set_output.go.tpl
user: login: rushmash91
body: `checkDomainStatus`
created_at: 2025-05-09T21:17:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: michaelhtm
created_at: 2025-05-07T08:03:41Z
body: /retest
author_association: MEMBER
user: login: michaelhtm
created_at: 2025-05-09T08:16:03Z
body: /test opensearchservice-kind-e2e
author_association: MEMBER
user: login: michaelhtm
created_at: 2025-05-09T08:16:27Z
body: /test opensearchservice-kind-e2e
author_association: MEMBER
user: login: ack-prow[bot]
created_at: 2025-05-09T23:01:02Z
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
--- | --- | --- | --- | ---
opensearchservice-verify-attribution | 0a1fb39021da6327bbf2b829d86d40fbc839b341 | [link](https://prow.ack.aws.dev/view/s3/ack-prow-logs/pr-logs/pull/aws-controllers-k8s_opensearchservice-controller/89/opensearchservice-verify-attribution/1920977282625703936) | false | `/test opensearchservice-verify-attribution`

[Full PR test history](https://prow.ack.aws.dev/pr-history?org=aws-controllers-k8s&repo=opensearchservice-controller&pr=89). [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: rushmash91
created_at: 2025-05-12T19:47:45Z
body: /lgtm
author_association: CONTRIBUTOR
user: login: ack-prow[bot]
created_at: 2025-05-12T19:47:51Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/aws-controllers-k8s/opensearchservice-controller/pull/89#" title="Author self-approved">michaelhtm</a>*, *<a href="https://github.com/aws-controllers-k8s/opensearchservice-controller/pull/89#issuecomment-2873796920" title="LGTM">rushmash91</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=aws-controllers-k8s%2Fopensearchservice-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/opensearchservice-controller/blob/main/OWNERS)~~ [michaelhtm,rushmash91]

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