Repository: go-acme/lego
Pull request #2181 (2024-05-09 12:53:34)

route53: adds option to not wait for changes
Related to #2180


Patch:
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index fda2a845..0e12e9c8 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -2306,6 +2306,7 @@ func displayDNSHelp(w io.Writer, name string) error {
 		ew.writeln(`	- "AWS_REGION":	Managed by the AWS client ('AWS_REGION_FILE' is not supported)`)
 		ew.writeln(`	- "AWS_SDK_LOAD_CONFIG":	Managed by the AWS client. Retrieve the region from the CLI config file ('AWS_SDK_LOAD_CONFIG_FILE' is not supported)`)
 		ew.writeln(`	- "AWS_SECRET_ACCESS_KEY":	Managed by the AWS client. Secret access key ('AWS_SECRET_ACCESS_KEY_FILE' is not supported, use 'AWS_SHARED_CREDENTIALS_FILE' instead)`)
+		ew.writeln(`	- "AWS_WAIT_FOR_RECORD_SETS_CHANGED":	Wait for changes to be INSYNC (it can be unstable)`)
 		ew.writeln()
 
 		ew.writeln(`Additional Configuration:`)
diff --git a/docs/content/dns/zz_gen_route53.md b/docs/content/dns/zz_gen_route53.md
index 166fa18e..62ef172a 100644
--- a/docs/content/dns/zz_gen_route53.md
+++ b/docs/content/dns/zz_gen_route53.md
@@ -48,6 +48,7 @@ lego --domains example.com --email your_example@email.com --dns route53 --accept
 | `AWS_REGION` | Managed by the AWS client (`AWS_REGION_FILE` is not supported) |
 | `AWS_SDK_LOAD_CONFIG` | Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported) |
 | `AWS_SECRET_ACCESS_KEY` | Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead) |
+| `AWS_WAIT_FOR_RECORD_SETS_CHANGED` | Wait for changes to be INSYNC (it can be unstable) |
 
 The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
 More information [here]({{< ref "dns#configuration-and-credentials" >}}).
diff --git a/providers/dns/route53/route53.go b/providers/dns/route53/route53.go
index dd8d4587..394aa506 100644
--- a/providers/dns/route53/route53.go
+++ b/providers/dns/route53/route53.go
@@ -34,6 +34,8 @@ const (
 	EnvAssumeRoleArn   = envNamespace + "ASSUME_ROLE_ARN"
 	EnvExternalID      = envNamespace + "EXTERNAL_ID"
 
+	EnvWaitForRecordSetsChanged = envNamespace + "WAIT_FOR_RECORD_SETS_CHANGED"
+
 	EnvTTL                = envNamespace + "TTL"
 	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
 	EnvPollingInterval    = envNamespace + "POLLING_INTERVAL"
@@ -53,6 +55,8 @@ type Config struct {
 	AssumeRoleArn string
 	ExternalID    string
 
+	WaitForRecordSetsChanged bool
+
 	TTL                int
 	PropagationTimeout time.Duration
 	PollingInterval    time.Duration
@@ -68,6 +72,8 @@ func NewDefaultConfig() *Config {
 		AssumeRoleArn: env.GetOrDefaultString(EnvAssumeRoleArn, ""),
 		ExternalID:    env.GetOrDefaultString(EnvExternalID, ""),
 
+		WaitForRecordSetsChanged: env.GetOrDefaultBool(EnvWaitForRecordSetsChanged, true),
+
 		TTL:                env.GetOrDefaultInt(EnvTTL, 10),
 		PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
 		PollingInterval:    env.GetOrDefaultSecond(EnvPollingInterval, 4*time.Second),
@@ -235,19 +241,22 @@ func (d *DNSProvider) changeRecord(ctx context.Context, action awstypes.ChangeAc
 
 	changeID := resp.ChangeInfo.Id
 
-	return wait.For("route53", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
-		reqParams := &route53.GetChangeInput{Id: changeID}
+	if d.config.WaitForRecordSetsChanged {
+		return wait.For("route53", d.config.PropagationTimeout, d.config.PollingInterval, func() (bool, error) {
+			resp, err := d.client.GetChange(ctx, &route53.GetChangeInput{Id: changeID})
+			if err != nil {
+				return false, fmt.Errorf("failed to query change status: %w", err)
+			}
 
-		resp, err := d.client.GetChange(ctx, reqParams)
-		if err != nil {
-			return false, fmt.Errorf("failed to query change status: %w", err)
-		}
+			if resp.ChangeInfo.Status == awstypes.ChangeStatusInsync {
+				return true, nil
+			}
 
-		if resp.ChangeInfo.Status == awstypes.ChangeStatusInsync {
-			return true, nil
-		}
-		return false, fmt.Errorf("unable to retrieve change: ID=%s", deref(changeID))
-	})
+			return false, fmt.Errorf("unable to retrieve change: ID=%s", deref(changeID))
+		})
+	}
+
+	return nil
 }
 
 func (d *DNSProvider) getExistingRecordSets(ctx context.Context, hostedZoneID, fqdn string) ([]awstypes.ResourceRecord, error) {
diff --git a/providers/dns/route53/route53.toml b/providers/dns/route53/route53.toml
index f16541e3..da8b489a 100644
--- a/providers/dns/route53/route53.toml
+++ b/providers/dns/route53/route53.toml
@@ -131,6 +131,7 @@ Replace `Z11111112222222333333` with your hosted zone ID and `example.com` with
     AWS_SDK_LOAD_CONFIG = "Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported)"
     AWS_ASSUME_ROLE_ARN = "Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN_FILE` is not supported)"
     AWS_EXTERNAL_ID = "Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported)"
+    AWS_WAIT_FOR_RECORD_SETS_CHANGED = "Wait for changes to be INSYNC (it can be unstable)"
   [Configuration.Additional]
     AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file."
     AWS_MAX_RETRIES = "The number of maximum returns the service will use to make an individual API request"


Test patch:
diff --git a/providers/dns/route53/route53_test.go b/providers/dns/route53/route53_test.go
index 1c8e5f5f..4301ca10 100644
--- a/providers/dns/route53/route53_test.go
+++ b/providers/dns/route53/route53_test.go
@@ -25,7 +25,8 @@ var envTest = tester.NewEnvTest(
 	EnvMaxRetries,
 	EnvTTL,
 	EnvPropagationTimeout,
-	EnvPollingInterval).
+	EnvPollingInterval,
+	EnvWaitForRecordSetsChanged).
 	WithDomain(envDomain).
 	WithLiveTestRequirements(EnvAccessKeyID, EnvSecretAccessKey, EnvRegion, envDomain)
 
@@ -119,20 +120,22 @@ func TestNewDefaultConfig(t *testing.T) {
 		{
 			desc: "default configuration",
 			expected: &Config{
-				MaxRetries:         5,
-				TTL:                10,
-				PropagationTimeout: 2 * time.Minute,
-				PollingInterval:    4 * time.Second,
+				MaxRetries:               5,
+				TTL:                      10,
+				PropagationTimeout:       2 * time.Minute,
+				PollingInterval:          4 * time.Second,
+				WaitForRecordSetsChanged: true,
 			},
 		},
 		{
-			desc: "",
+			desc: "set values",
 			envVars: map[string]string{
-				EnvMaxRetries:         "10",
-				EnvTTL:                "99",
-				EnvPropagationTimeout: "60",
-				EnvPollingInterval:    "60",
-				EnvHostedZoneID:       "abc123",
+				EnvMaxRetries:               "10",
+				EnvTTL:                      "99",
+				EnvPropagationTimeout:       "60",
+				EnvPollingInterval:          "60",
+				EnvHostedZoneID:             "abc123",
+				EnvWaitForRecordSetsChanged: "false",
 			},
 			expected: &Config{
 				MaxRetries:         10,