From bf0e89cdd92fdef4f0f0e9fab15d7cc9205a7454 Mon Sep 17 00:00:00 2001 From: Nikita Shashkov <75970449+Tearix@users.noreply.github.com> Date: Wed, 17 Sep 2025 15:39:50 +0300 Subject: [PATCH] otc: adds option to use private zone (#2649) Co-authored-by: Fernandez Ludovic --- cmd/zz_gen_cmd_dnshelp.go | 3 +- docs/content/dns/zz_gen_otc.md | 3 +- providers/dns/otc/internal/client.go | 9 +++-- providers/dns/otc/internal/client_test.go | 19 ++++++++- providers/dns/otc/otc.go | 21 ++++++---- providers/dns/otc/otc.toml | 3 +- providers/dns/otc/otc_test.go | 48 ++++++++++++++++++++--- 7 files changed, 85 insertions(+), 21 deletions(-) diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 55199386e..5b0da36d4 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -2572,7 +2572,6 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Credentials:`) ew.writeln(` - "OTC_DOMAIN_NAME": Domain name`) - ew.writeln(` - "OTC_IDENTITY_ENDPOINT": Identity endpoint URL`) ew.writeln(` - "OTC_PASSWORD": Password`) ew.writeln(` - "OTC_PROJECT_NAME": Project name`) ew.writeln(` - "OTC_USER_NAME": User name`) @@ -2580,7 +2579,9 @@ func displayDNSHelp(w io.Writer, name string) error { ew.writeln(`Additional Configuration:`) ew.writeln(` - "OTC_HTTP_TIMEOUT": API request timeout in seconds (Default: 10)`) + ew.writeln(` - "OTC_IDENTITY_ENDPOINT": Identity endpoint URL`) ew.writeln(` - "OTC_POLLING_INTERVAL": Time between DNS propagation check in seconds (Default: 2)`) + ew.writeln(` - "OTC_PRIVATE_ZONE": Set to true to use private zones only (default: use public zones only)`) ew.writeln(` - "OTC_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation in seconds (Default: 60)`) ew.writeln(` - "OTC_SEQUENCE_INTERVAL": Time between sequential requests in seconds (Default: 60)`) ew.writeln(` - "OTC_TTL": The TTL of the TXT record used for the DNS challenge in seconds (Default: 300)`) diff --git a/docs/content/dns/zz_gen_otc.md b/docs/content/dns/zz_gen_otc.md index fe92f3001..e85850829 100644 --- a/docs/content/dns/zz_gen_otc.md +++ b/docs/content/dns/zz_gen_otc.md @@ -35,7 +35,6 @@ _Please contribute by adding a CLI example._ | Environment Variable Name | Description | |-----------------------|-------------| | `OTC_DOMAIN_NAME` | Domain name | -| `OTC_IDENTITY_ENDPOINT` | Identity endpoint URL | | `OTC_PASSWORD` | Password | | `OTC_PROJECT_NAME` | Project name | | `OTC_USER_NAME` | User name | @@ -49,7 +48,9 @@ More information [here]({{% ref "dns#configuration-and-credentials" %}}). | Environment Variable Name | Description | |--------------------------------|-------------| | `OTC_HTTP_TIMEOUT` | API request timeout in seconds (Default: 10) | +| `OTC_IDENTITY_ENDPOINT` | Identity endpoint URL | | `OTC_POLLING_INTERVAL` | Time between DNS propagation check in seconds (Default: 2) | +| `OTC_PRIVATE_ZONE` | Set to true to use private zones only (default: use public zones only) | | `OTC_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation in seconds (Default: 60) | | `OTC_SEQUENCE_INTERVAL` | Time between sequential requests in seconds (Default: 60) | | `OTC_TTL` | The TTL of the TXT record used for the DNS challenge in seconds (Default: 300) | diff --git a/providers/dns/otc/internal/client.go b/providers/dns/otc/internal/client.go index e3e225314..0bb4dd323 100644 --- a/providers/dns/otc/internal/client.go +++ b/providers/dns/otc/internal/client.go @@ -42,8 +42,8 @@ func NewClient(username, password, domainName, projectName string) *Client { } } -func (c *Client) GetZoneID(ctx context.Context, zone string) (string, error) { - zonesResp, err := c.getZones(ctx, zone) +func (c *Client) GetZoneID(ctx context.Context, zone string, privateZone bool) (string, error) { + zonesResp, err := c.getZones(ctx, zone, privateZone) if err != nil { return "", err } @@ -62,13 +62,16 @@ func (c *Client) GetZoneID(ctx context.Context, zone string) (string, error) { } // https://docs.otc.t-systems.com/domain-name-service/api-ref/apis/public_zone_management/querying_public_zones.html -func (c *Client) getZones(ctx context.Context, zone string) (*ZonesResponse, error) { +func (c *Client) getZones(ctx context.Context, zone string, privateZone bool) (*ZonesResponse, error) { c.muBaseURL.Lock() endpoint := c.baseURL.JoinPath("zones") c.muBaseURL.Unlock() query := endpoint.Query() query.Set("name", zone) + if privateZone { + query.Set("type", "private") + } endpoint.RawQuery = query.Encode() req, err := newJSONRequest(ctx, http.MethodGet, endpoint, nil) diff --git a/providers/dns/otc/internal/client_test.go b/providers/dns/otc/internal/client_test.go index a69cd1d45..74b5bb3af 100644 --- a/providers/dns/otc/internal/client_test.go +++ b/providers/dns/otc/internal/client_test.go @@ -33,7 +33,22 @@ func TestClient_GetZoneID(t *testing.T) { With("name", "example.com.")). Build(t) - zoneID, err := client.GetZoneID(context.Background(), "example.com.") + zoneID, err := client.GetZoneID(context.Background(), "example.com.", false) + require.NoError(t, err) + + assert.Equal(t, "123123", zoneID) +} + +func TestClient_GetZoneID_private(t *testing.T) { + client := mockBuilder(). + Route("GET /zones", + servermock.ResponseFromFixture("zones_GET.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "example.com."). + With("type", "private")). + Build(t) + + zoneID, err := client.GetZoneID(context.Background(), "example.com.", true) require.NoError(t, err) assert.Equal(t, "123123", zoneID) @@ -47,7 +62,7 @@ func TestClient_GetZoneID_error(t *testing.T) { With("name", "example.com.")). Build(t) - _, err := client.GetZoneID(context.Background(), "example.com.") + _, err := client.GetZoneID(context.Background(), "example.com.", false) require.EqualError(t, err, "zone example.com. not found") } diff --git a/providers/dns/otc/otc.go b/providers/dns/otc/otc.go index 3569e6343..a6374f822 100644 --- a/providers/dns/otc/otc.go +++ b/providers/dns/otc/otc.go @@ -23,6 +23,7 @@ const ( EnvPassword = envNamespace + "PASSWORD" EnvProjectName = envNamespace + "PROJECT_NAME" EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT" + EnvPrivateZone = envNamespace + "PRIVATE_ZONE" EnvTTL = envNamespace + "TTL" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" @@ -40,11 +41,13 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil) // Config is used to configure the creation of the DNSProvider. type Config struct { - IdentityEndpoint string - DomainName string - ProjectName string - UserName string - Password string + DomainName string + ProjectName string + UserName string + Password string + IdentityEndpoint string + PrivateZone bool + PropagationTimeout time.Duration PollingInterval time.Duration SequenceInterval time.Duration @@ -65,10 +68,12 @@ func NewDefaultConfig() *Config { tr.DisableKeepAlives = true return &Config{ + PrivateZone: env.GetOrDefaultBool(EnvPrivateZone, false), + IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint), + TTL: env.GetOrDefaultInt(EnvTTL, minTTL), PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), - IdentityEndpoint: env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint), SequenceInterval: env.GetOrDefaultSecond(EnvSequenceInterval, dns01.DefaultPropagationTimeout), HTTPClient: &http.Client{ Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), @@ -144,7 +149,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { return fmt.Errorf("otc: %w", err) } - zoneID, err := d.client.GetZoneID(ctx, authZone) + zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone) if err != nil { return fmt.Errorf("otc: unable to get zone: %w", err) } @@ -181,7 +186,7 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { return fmt.Errorf("otc: %w", err) } - zoneID, err := d.client.GetZoneID(ctx, authZone) + zoneID, err := d.client.GetZoneID(ctx, authZone, d.config.PrivateZone) if err != nil { return fmt.Errorf("otc: %w", err) } diff --git a/providers/dns/otc/otc.toml b/providers/dns/otc/otc.toml index cb1910d26..4f2f4ac0e 100644 --- a/providers/dns/otc/otc.toml +++ b/providers/dns/otc/otc.toml @@ -12,8 +12,9 @@ Example = '''''' OTC_PASSWORD = "Password" OTC_PROJECT_NAME = "Project name" OTC_DOMAIN_NAME = "Domain name" - OTC_IDENTITY_ENDPOINT = "Identity endpoint URL" [Configuration.Additional] + OTC_IDENTITY_ENDPOINT = "Identity endpoint URL" + OTC_PRIVATE_ZONE = "Set to true to use private zones only (default: use public zones only)" OTC_POLLING_INTERVAL = "Time between DNS propagation check in seconds (Default: 2)" OTC_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation in seconds (Default: 60)" OTC_SEQUENCE_INTERVAL = "Time between sequential requests in seconds (Default: 60)" diff --git a/providers/dns/otc/otc_test.go b/providers/dns/otc/otc_test.go index b5cb3a9d4..0b59e02ac 100644 --- a/providers/dns/otc/otc_test.go +++ b/providers/dns/otc/otc_test.go @@ -18,6 +18,7 @@ var envTest = tester.NewEnvTest( EnvDomainName, EnvUserName, EnvPassword, + EnvPrivateZone, EnvProjectName, EnvIdentityEndpoint). WithDomain(envDomain) @@ -213,7 +214,7 @@ func TestLiveCleanUp(t *testing.T) { } func TestDNSProvider_Present(t *testing.T) { - provider := mockBuilder(). + provider := mockBuilder(false). Route("GET /v2/zones", servermock.ResponseFromInternal("zones_GET.json"), servermock.CheckQueryParameter().Strict(). @@ -227,8 +228,24 @@ func TestDNSProvider_Present(t *testing.T) { require.NoError(t, err) } +func TestDNSProvider_Present_private(t *testing.T) { + provider := mockBuilder(true). + Route("GET /v2/zones", + servermock.ResponseFromInternal("zones_GET.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "example.com."). + With("type", "private")). + Route("POST /v2/zones/123123/recordsets", + servermock.Noop(), + servermock.CheckRequestJSONBodyFromInternal("zones-recordsets_POST-request.json")). + Build(t) + + err := provider.Present("example.com", "", "123d==") + require.NoError(t, err) +} + func TestDNSProvider_Present_emptyZone(t *testing.T) { - provider := mockBuilder(). + provider := mockBuilder(false). Route("GET /v2/zones", servermock.ResponseFromInternal("zones_GET_empty.json"), servermock.CheckQueryParameter().Strict(). @@ -240,7 +257,7 @@ func TestDNSProvider_Present_emptyZone(t *testing.T) { } func TestDNSProvider_Cleanup(t *testing.T) { - provider := mockBuilder(). + provider := mockBuilder(false). Route("GET /v2/zones", servermock.ResponseFromInternal("zones_GET.json"), servermock.CheckQueryParameter().Strict(). @@ -258,8 +275,28 @@ func TestDNSProvider_Cleanup(t *testing.T) { require.NoError(t, err) } +func TestDNSProvider_Cleanup_private(t *testing.T) { + provider := mockBuilder(true). + Route("GET /v2/zones", + servermock.ResponseFromInternal("zones_GET.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "example.com."). + With("type", "private")). + Route("GET /v2/zones/123123/recordsets", + servermock.ResponseFromInternal("zones-recordsets_GET.json"), + servermock.CheckQueryParameter().Strict(). + With("name", "_acme-challenge.example.com."). + With("type", "TXT")). + Route("DELETE /v2/zones/123123/recordsets/321321", + servermock.ResponseFromInternal("zones-recordsets_DELETE.json")). + Build(t) + + err := provider.CleanUp("example.com", "", "123d==") + require.NoError(t, err) +} + func TestDNSProvider_Cleanup_emptyRecordset(t *testing.T) { - provider := mockBuilder(). + provider := mockBuilder(false). Route("GET /v2/zones", servermock.ResponseFromInternal("zones_GET.json"), servermock.CheckQueryParameter().Strict(). @@ -275,7 +312,7 @@ func TestDNSProvider_Cleanup_emptyRecordset(t *testing.T) { require.EqualError(t, err, "otc: unable to get record _acme-challenge.example.com. for zone example.com: record not found") } -func mockBuilder() *servermock.Builder[*DNSProvider] { +func mockBuilder(private bool) *servermock.Builder[*DNSProvider] { return servermock.NewBuilder( func(server *httptest.Server) (*DNSProvider, error) { config := NewDefaultConfig() @@ -285,6 +322,7 @@ func mockBuilder() *servermock.Builder[*DNSProvider] { config.DomainName = "example.com" config.ProjectName = "test" config.IdentityEndpoint = fmt.Sprintf("%s/v3/auth/token", server.URL) + config.PrivateZone = private return NewDNSProviderConfig(config) },