Repository: go-acme/lego (language: go)

Issue:
acme-dns: add CNAME provisioned flag for HTTP storage
With the new acme-dns HTTP storage feature, it is possible to complete the challenge if the HTTP storage server will also create the CNAME records.  Currently, the acme-dns provider will always return an error indicating that the CNAME must be created.

This adds an environment configuration to tell the acme-dns provider to presume the CNAME record has been added to the DNS zone.  This allows the challenge to complete successfully when the CNAME record is created when a new DNS challenge is registered with acme-dns.  This is mainly useful when using the HTTP storage backend if the server will also create the CNAME record.

Related to (#2393)


Discussion / hints:
I think you can implement that inside the HTTP storage server.

Everything depends on your implementation of the `Fetch`.
I did, but the the acme-dns provider always returns `ErrCNAMERequired`.  This breaks the flow for many services using lego as they don't understand the context of the error for each provider, but only that the provider returned an error.

The use case I tested was with `traefik` + `acme-dns` with the HTTP storage server.  It required a restart of traefik to have it retry the DNS challenge because the `ErrCNAMERequired` error.  It doesn't understand all of the errors for each provider, just that the provider you are using returned an error.

This was a way to tell lego that the CNAME has been created and it should not return an error.
If the `Fetch` always returns something, the `register` will never be called.
:thinking: I understand your context, the `Fetch` implementation is a solution but if you want to use the `register` I think a better approach is to delegate this behavior to the `Save` implementation.
I need to think about that.
The Fetch doesn’t contain all of the information required.  It will only contain the domain attempting to be verified as a GET path parameter.  To create the CNAME, we need the acme-dns data returned from registration.

Since the Put and Save are both in the `register` function, it would not make a difference since that function will always return `ErrCNAMERequired`.

I like that the HTTP client doesn’t implement Save because then we would need to track state across requests.  I think Put is sufficient.
I have a proposal, I will open a PR in a few seconds

Fix patch:
diff --git a/providers/dns/acmedns/acmedns.go b/providers/dns/acmedns/acmedns.go
index 8aabaa14c9..9dd63d0a2d 100644
--- a/providers/dns/acmedns/acmedns.go
+++ b/providers/dns/acmedns/acmedns.go
@@ -178,16 +178,26 @@ func (d *DNSProvider) register(ctx context.Context, domain, fqdn string) error {
 		return err
 	}
 
+	var cnameCreated bool
+
 	// Store the new account in the storage and call save to persist the data.
 	err = d.storage.Put(ctx, domain, newAcct)
 	if err != nil {
-		return err
+		cnameCreated = errors.Is(err, internal.ErrCNAMEAlreadyCreated)
+		if !cnameCreated {
+			return err
+		}
 	}
+
 	err = d.storage.Save(ctx)
 	if err != nil {
 		return err
 	}
 
+	if cnameCreated {
+		return nil
+	}
+
 	// Stop issuance by returning an error.
 	// The user needs to perform a manual one-time CNAME setup in their DNS zone
 	// to complete the setup of the new account we created.
diff --git a/providers/dns/acmedns/internal/http_storage.go b/providers/dns/acmedns/internal/http_storage.go
index 1a1e8e2eee..7a535eb20b 100644
--- a/providers/dns/acmedns/internal/http_storage.go
+++ b/providers/dns/acmedns/internal/http_storage.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
 	"net/http"
@@ -17,6 +18,8 @@ import (
 
 var _ goacmedns.Storage = (*HTTPStorage)(nil)
 
+var ErrCNAMEAlreadyCreated = errors.New("the CNAME has already been created")
+
 // HTTPStorage is an implementation of [acmedns.Storage] over HTTP.
 type HTTPStorage struct {
 	client  *http.Client
@@ -98,6 +101,11 @@ func (s *HTTPStorage) do(req *http.Request, result any) error {
 	}
 
 	if result == nil {
+		// Hack related to `Put`.
+		if resp.StatusCode == http.StatusCreated {
+			return ErrCNAMEAlreadyCreated
+		}
+
 		return nil
 	}
 
diff --git a/providers/dns/acmedns/internal/readme.md b/providers/dns/acmedns/internal/readme.md
index bccdc5388b..b667d3d23c 100644
--- a/providers/dns/acmedns/internal/readme.md
+++ b/providers/dns/acmedns/internal/readme.md
@@ -61,7 +61,9 @@ Endpoint: `POST <BaseURL>/<domain>`
 
 ### Response
 
-Response status code 200.
+Response status code:
+- 200: the process will be stopped to allow the user to create the CNAME.
+- 201: the process will continue without error (the CNAME should be created by the server)
 
 No expected body.
 


Test patch:
diff --git a/providers/dns/acmedns/internal/http_storage_test.go b/providers/dns/acmedns/internal/http_storage_test.go
index a628f9a6d2..7f93677229 100644
--- a/providers/dns/acmedns/internal/http_storage_test.go
+++ b/providers/dns/acmedns/internal/http_storage_test.go
@@ -137,3 +137,18 @@ func TestHTTPStorage_Put_error(t *testing.T) {
 	err := storage.Put(context.Background(), "example.com", account)
 	require.Error(t, err)
 }
+
+func TestHTTPStorage_Put_CNAME_created(t *testing.T) {
+	storage := setupTest(t, "POST /example.com", "", http.StatusCreated)
+
+	account := goacmedns.Account{
+		FullDomain: "foo.example.com",
+		SubDomain:  "foo",
+		Username:   "user",
+		Password:   "secret",
+		ServerURL:  "https://example.com",
+	}
+
+	err := storage.Put(context.Background(), "example.com", account)
+	require.ErrorIs(t, err, ErrCNAMEAlreadyCreated)
+}