From 7a2ab9eb7b6000b13d0b31ac93d150d24e158223 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Tue, 13 Feb 2024 02:29:43 +0100 Subject: [PATCH] Add DNS provider for Abion --- providers/dns/abion/abion.go | 206 +++++++++++++ providers/dns/abion/abion.toml | 22 ++ providers/dns/abion/abion_test.go | 115 ++++++++ providers/dns/abion/internal/client.go | 168 +++++++++++ providers/dns/abion/internal/client_test.go | 272 ++++++++++++++++++ .../dns/abion/internal/fixtures/error.json | 9 + .../dns/abion/internal/fixtures/update.json | 45 +++ .../dns/abion/internal/fixtures/zone.json | 45 +++ .../dns/abion/internal/fixtures/zones.json | 22 ++ providers/dns/abion/internal/types.go | 72 +++++ 10 files changed, 976 insertions(+) create mode 100644 providers/dns/abion/abion.go create mode 100644 providers/dns/abion/abion.toml create mode 100644 providers/dns/abion/abion_test.go create mode 100644 providers/dns/abion/internal/client.go create mode 100644 providers/dns/abion/internal/client_test.go create mode 100644 providers/dns/abion/internal/fixtures/error.json create mode 100644 providers/dns/abion/internal/fixtures/update.json create mode 100644 providers/dns/abion/internal/fixtures/zone.json create mode 100644 providers/dns/abion/internal/fixtures/zones.json create mode 100644 providers/dns/abion/internal/types.go diff --git a/providers/dns/abion/abion.go b/providers/dns/abion/abion.go new file mode 100644 index 0000000000..982115068c --- /dev/null +++ b/providers/dns/abion/abion.go @@ -0,0 +1,206 @@ +// Package abion implements a DNS provider for solving the DNS-01 challenge using Abion. +package abion + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/abion/internal" +) + +// Environment variables names. +const ( + envNamespace = "ABION_" + + EnvAPIKey = envNamespace + "API_KEY" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPClient *http.Client +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPClient: &http.Client{ + Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second), + }, + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + client *internal.Client +} + +// NewDNSProvider returns a DNSProvider instance configured for Abion. +// Credentials must be passed in the environment variable: ABION_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("abion: %w", err) + } + + config := NewDefaultConfig() + config.APIKey = values[EnvAPIKey] + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for Abion. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("abion: the configuration of the DNS provider is nil") + } + + if config.APIKey == "" { + return nil, errors.New("abion: credentials missing") + } + + client := internal.NewClient(config.APIKey) + + if config.HTTPClient != nil { + client.HTTPClient = config.HTTPClient + } + + return &DNSProvider{ + config: config, + client: client, + }, nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, _, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("abion: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("abion: %w", err) + } + + zones, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("abion: get zone %w", err) + } + + var data []internal.Record + if sub, ok := zones.Data.Attributes.Records[subDomain]; ok { + if records, exist := sub["TXT"]; exist { + data = append(data, records...) + } + } + + data = append(data, internal.Record{ + TTL: d.config.TTL, + Data: info.Value, + Comments: "lego", + }) + + patch := internal.ZoneRequest{ + Data: internal.Zone{ + Type: "zone", + ID: dns01.UnFqdn(authZone), + Attributes: internal.Attributes{ + Records: map[string]map[string][]internal.Record{ + subDomain: {"TXT": data}, + }, + }, + }, + } + + _, err = d.client.UpdateZone(ctx, dns01.UnFqdn(authZone), patch) + if err != nil { + return fmt.Errorf("abion: update zone %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + ctx := context.Background() + info := dns01.GetChallengeInfo(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN) + if err != nil { + return fmt.Errorf("abion: could not find zone for domain %q: %w", domain, err) + } + + subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone) + if err != nil { + return fmt.Errorf("abion: %w", err) + } + + zones, err := d.client.GetZone(ctx, dns01.UnFqdn(authZone)) + if err != nil { + return fmt.Errorf("abion: get zone %w", err) + } + + var data []internal.Record + if sub, ok := zones.Data.Attributes.Records[subDomain]; ok { + if records, exist := sub["TXT"]; exist { + for _, record := range records { + if record.Data != info.Value { + data = append(data, record) + } + } + } + } + + payload := map[string][]internal.Record{} + if len(data) == 0 { + payload["TXT"] = nil + } else { + payload["TXT"] = data + } + + patch := internal.ZoneRequest{ + Data: internal.Zone{ + Type: "zone", + ID: dns01.UnFqdn(authZone), + Attributes: internal.Attributes{ + Records: map[string]map[string][]internal.Record{ + subDomain: payload, + }, + }, + }, + } + + _, err = d.client.UpdateZone(ctx, dns01.UnFqdn(authZone), patch) + if err != nil { + return fmt.Errorf("abion: update zone %w", err) + } + + return nil +} diff --git a/providers/dns/abion/abion.toml b/providers/dns/abion/abion.toml new file mode 100644 index 0000000000..f8b0b81331 --- /dev/null +++ b/providers/dns/abion/abion.toml @@ -0,0 +1,22 @@ +Name = "Abion" +Description = '''''' +URL = "https://abion.com" +Code = "abion" +Since = "v4.20.0" + +Example = ''' +ABION_API_KEY="xxxxxxxxxxxx" \ +lego --email you@example.com --dns abion --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + ABION_API_KEY = "API key" + [Configuration.Additional] + ABION_POLLING_INTERVAL = "Time between DNS propagation check" + ABION_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + ABION_TTL = "The TTL of the TXT record used for the DNS challenge" + ABION_HTTP_TIMEOUT = "API request timeout" + +[Links] + API = "https://demo.abion.com/pmapi-doc/openapi-ui/index.html" diff --git a/providers/dns/abion/abion_test.go b/providers/dns/abion/abion_test.go new file mode 100644 index 0000000000..2c8aa7fa50 --- /dev/null +++ b/providers/dns/abion/abion_test.go @@ -0,0 +1,115 @@ +package abion + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvAPIKey).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvAPIKey: "123", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvAPIKey: "", + }, + expected: "abion: some credentials information are missing: ABION_API_KEY", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + apiKey string + ttl int + expected string + }{ + { + desc: "success", + apiKey: "123", + }, + { + desc: "missing credentials", + expected: "abion: credentials missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.APIKey = test.apiKey + config.TTL = test.ttl + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/abion/internal/client.go b/providers/dns/abion/internal/client.go new file mode 100644 index 0000000000..5ac0b2e837 --- /dev/null +++ b/providers/dns/abion/internal/client.go @@ -0,0 +1,168 @@ +package internal + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "github.com/go-acme/lego/v4/providers/dns/internal/errutils" + querystring "github.com/google/go-querystring/query" +) + +// defaultBaseURL represents the API endpoint to call. +const defaultBaseURL = "https://api.abion.com" + +const apiKeyHeader = "X-API-KEY" + +// Client the Abion API client. +type Client struct { + apiKey string + baseURL *url.URL + HTTPClient *http.Client +} + +// NewClient Creates a new Client. +func NewClient(apiKey string) *Client { + baseURL, _ := url.Parse(defaultBaseURL) + + return &Client{ + apiKey: apiKey, + baseURL: baseURL, + HTTPClient: &http.Client{Timeout: 5 * time.Second}, + } +} + +// GetZones Lists all the zones your session can access. +func (c *Client) GetZones(ctx context.Context, page *Pagination) (*APIResponse[[]Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones") + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, err + } + + if page != nil { + v, errQ := querystring.Values(page) + if errQ != nil { + return nil, errQ + } + + req.URL.RawQuery = v.Encode() + } + + results := &APIResponse[[]Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not get zones: %w", err) + } + + return results, nil +} + +// GetZone Returns the full information on a single zone. +func (c *Client) GetZone(ctx context.Context, name string) (*APIResponse[*Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones", name) + + req, err := newJSONRequest(ctx, http.MethodGet, endpoint, http.NoBody) + if err != nil { + return nil, err + } + + results := &APIResponse[*Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not get zone %s: %w", name, err) + } + + return results, nil +} + +// UpdateZone Updates a zone by patching it according to JSON Merge Patch format (RFC 7396). +func (c *Client) UpdateZone(ctx context.Context, name string, patch ZoneRequest) (*APIResponse[*Zone], error) { + endpoint := c.baseURL.JoinPath("v1", "zones", name) + + req, err := newJSONRequest(ctx, http.MethodPatch, endpoint, patch) + if err != nil { + return nil, err + } + + results := &APIResponse[*Zone]{} + + if err := c.do(req, results); err != nil { + return nil, fmt.Errorf("could not update zone %s: %w", name, err) + } + + return results, nil +} + +func (c *Client) do(req *http.Request, result any) error { + req.Header.Set(apiKeyHeader, c.apiKey) + + resp, err := c.HTTPClient.Do(req) + if err != nil { + return errutils.NewHTTPDoError(req, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return parseError(req, resp) + } + + if result == nil { + return nil + } + + raw, err := io.ReadAll(resp.Body) + if err != nil { + return errutils.NewReadResponseError(req, resp.StatusCode, err) + } + + err = json.Unmarshal(raw, result) + if err != nil { + return errutils.NewUnmarshalError(req, resp.StatusCode, raw, err) + } + + return nil +} + +func newJSONRequest(ctx context.Context, method string, endpoint *url.URL, payload any) (*http.Request, error) { + buf := new(bytes.Buffer) + + if payload != nil { + err := json.NewEncoder(buf).Encode(payload) + if err != nil { + return nil, fmt.Errorf("failed to create request JSON body: %w", err) + } + } + + req, err := http.NewRequestWithContext(ctx, method, endpoint.String(), buf) + if err != nil { + return nil, fmt.Errorf("unable to create request: %w", err) + } + + req.Header.Set("Accept", "application/json") + + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + return req, nil +} + +func parseError(req *http.Request, resp *http.Response) error { + raw, _ := io.ReadAll(resp.Body) + + zResp := &APIResponse[any]{} + err := json.Unmarshal(raw, zResp) + if err != nil { + return errutils.NewUnexpectedStatusCodeError(req, resp.StatusCode, raw) + } + + return zResp.Error +} diff --git a/providers/dns/abion/internal/client_test.go b/providers/dns/abion/internal/client_test.go new file mode 100644 index 0000000000..b81c7e9d85 --- /dev/null +++ b/providers/dns/abion/internal/client_test.go @@ -0,0 +1,272 @@ +package internal + +import ( + "context" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTest(t *testing.T, method, pattern string, status int, file string) *Client { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + t.Cleanup(server.Close) + + mux.HandleFunc(pattern, func(rw http.ResponseWriter, req *http.Request) { + if req.Method != method { + http.Error(rw, fmt.Sprintf("unsupported method: %s", req.Method), http.StatusBadRequest) + return + } + + auth := req.Header.Get(apiKeyHeader) + if auth != "secret" { + http.Error(rw, fmt.Sprintf("invalid API key: %s", auth), http.StatusUnauthorized) + return + } + + if file == "" { + rw.WriteHeader(status) + return + } + + open, err := os.Open(filepath.Join("fixtures", file)) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { _ = open.Close() }() + + rw.WriteHeader(status) + _, err = io.Copy(rw, open) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + }) + + client := NewClient("secret") + client.HTTPClient = server.Client() + client.baseURL, _ = url.Parse(server.URL) + + return client +} + +func TestUpdateZone(t *testing.T) { + domain := "example.com" + + client := setupTest(t, http.MethodPatch, "/v1/zones/"+domain, http.StatusOK, "update.json") + + patch := ZoneRequest{ + Data: Zone{ + Type: "zone", + ID: domain, + Attributes: Attributes{ + Records: map[string]map[string][]Record{ + "_acme-challenge.test": { + "TXT": []Record{ + {Data: "test"}, + {Data: "test1"}, + {Data: "test2"}, + }, + }, + }, + }, + }, + } + + zone, err := client.UpdateZone(context.Background(), domain, patch) + require.NoError(t, err) + + expected := &APIResponse[*Zone]{ + Meta: &Metadata{ + InvocationID: "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + }, + Data: &Zone{ + Type: "zone", + ID: "dipcon.com", + Attributes: Attributes{ + OrganisationID: "10154", + OrganisationDescription: "My Company AB", + DNSTypeDescription: "Anycast", + Slave: false, + Pending: false, + Deleted: false, + Settings: &Settings{ + MName: "dns01.dipcon.com.", + Refresh: 3600, + Expire: 604800, + TTL: 600, + }, + Records: map[string]map[string][]Record{ + "@": { + "NS": { + { + TTL: 3600, + Data: "193.14.90.194", + Comments: "this is a comment", + }, + }, + }, + }, + Redirects: map[string][]Redirect{ + "": { + { + Path: "/x/y", + Destination: "https://abion.com/?ref=dipcon", + Status: 301, + Slugs: true, + Certificate: true, + }, + }, + }, + }, + }, + } + + assert.Equal(t, expected, zone) +} + +func TestUpdateZone_error(t *testing.T) { + domain := "example.com" + + client := setupTest(t, http.MethodPatch, "/v1/zones/"+domain, http.StatusUnauthorized, "error.json") + + patch := ZoneRequest{ + Data: Zone{ + Type: "zone", + ID: domain, + Attributes: Attributes{ + Records: map[string]map[string][]Record{ + "_acme-challenge.test": { + "TXT": []Record{ + {Data: "test"}, + {Data: "test1"}, + {Data: "test2"}, + }, + }, + }, + }, + }, + } + + _, err := client.UpdateZone(context.Background(), domain, patch) + require.EqualError(t, err, "could not update zone example.com: api error: status=401, message=Authentication Error") +} + +func TestGetZones(t *testing.T) { + client := setupTest(t, http.MethodGet, "/v1/zones/", http.StatusOK, "zones.json") + + zones, err := client.GetZones(context.Background(), nil) + require.NoError(t, err) + + expected := &APIResponse[[]Zone]{ + Meta: &Metadata{ + InvocationID: "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + Pagination: &Pagination{ + Offset: 0, + Limit: 1, + Total: 1, + }, + }, + Data: []Zone{ + { + Type: "zone", + ID: "dipcon.com", + Attributes: Attributes{ + OrganisationID: "10154", + OrganisationDescription: "My Company AB", + DNSTypeDescription: "Anycast", + Slave: true, + Pending: true, + Deleted: true, + }, + }, + }, + } + + assert.Equal(t, expected, zones) +} + +func TestGetZones_error(t *testing.T) { + client := setupTest(t, http.MethodGet, "/v1/zones/", http.StatusUnauthorized, "error.json") + + _, err := client.GetZones(context.Background(), nil) + require.EqualError(t, err, "could not get zones: api error: status=401, message=Authentication Error") +} + +func TestGetZone(t *testing.T) { + domain := "example.com" + + client := setupTest(t, http.MethodGet, "/v1/zones/"+domain, http.StatusOK, "zone.json") + + zones, err := client.GetZone(context.Background(), domain) + require.NoError(t, err) + + expected := &APIResponse[*Zone]{ + Meta: &Metadata{ + InvocationID: "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + }, + Data: &Zone{ + Type: "zone", + ID: "dipcon.com", + Attributes: Attributes{ + OrganisationID: "10154", + OrganisationDescription: "My Company AB", + DNSTypeDescription: "Anycast", + Slave: false, + Pending: false, + Deleted: false, + Settings: &Settings{ + MName: "dns01.dipcon.com.", + Refresh: 3600, + Expire: 604800, + TTL: 600, + }, + Records: map[string]map[string][]Record{ + "@": { + "NS": { + { + TTL: 3600, + Data: "193.14.90.194", + Comments: "this is a comment", + }, + }, + }, + }, + Redirects: map[string][]Redirect{ + "": { + { + Path: "/x/y", + Destination: "https://abion.com/?ref=dipcon", + Status: 301, + Slugs: true, + Certificate: true, + }, + }, + }, + }, + }, + } + + assert.Equal(t, expected, zones) +} + +func TestGetZone_error(t *testing.T) { + domain := "example.com" + + client := setupTest(t, http.MethodGet, "/v1/zones/"+domain, http.StatusUnauthorized, "error.json") + + _, err := client.GetZone(context.Background(), domain) + require.EqualError(t, err, "could not get zone example.com: api error: status=401, message=Authentication Error") +} diff --git a/providers/dns/abion/internal/fixtures/error.json b/providers/dns/abion/internal/fixtures/error.json new file mode 100644 index 0000000000..9877fdb8c3 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/error.json @@ -0,0 +1,9 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147" + }, + "error": { + "status": 401, + "message": "Authentication Error" + } +} diff --git a/providers/dns/abion/internal/fixtures/update.json b/providers/dns/abion/internal/fixtures/update.json new file mode 100644 index 0000000000..a26defd639 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/update.json @@ -0,0 +1,45 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147" + }, + "data": { + "type": "zone", + "id": "dipcon.com", + "attributes": { + "organisationId": "10154", + "organisationDescription": "My Company AB", + "dnsTypeDescription": "Anycast", + "slave": false, + "pending": false, + "deleted": false, + "settings": { + "mname": "dns01.dipcon.com.", + "refresh": 3600, + "expire": 604800, + "ttl": 600 + }, + "records": { + "@": { + "NS": [ + { + "ttl": 3600, + "rdata": "193.14.90.194", + "comments": "this is a comment" + } + ] + } + }, + "redirects": { + "": [ + { + "path": "/x/y", + "destination": "https://abion.com/?ref=dipcon", + "status": 301, + "slugs": true, + "certificate": true + } + ] + } + } + } +} diff --git a/providers/dns/abion/internal/fixtures/zone.json b/providers/dns/abion/internal/fixtures/zone.json new file mode 100644 index 0000000000..a26defd639 --- /dev/null +++ b/providers/dns/abion/internal/fixtures/zone.json @@ -0,0 +1,45 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147" + }, + "data": { + "type": "zone", + "id": "dipcon.com", + "attributes": { + "organisationId": "10154", + "organisationDescription": "My Company AB", + "dnsTypeDescription": "Anycast", + "slave": false, + "pending": false, + "deleted": false, + "settings": { + "mname": "dns01.dipcon.com.", + "refresh": 3600, + "expire": 604800, + "ttl": 600 + }, + "records": { + "@": { + "NS": [ + { + "ttl": 3600, + "rdata": "193.14.90.194", + "comments": "this is a comment" + } + ] + } + }, + "redirects": { + "": [ + { + "path": "/x/y", + "destination": "https://abion.com/?ref=dipcon", + "status": 301, + "slugs": true, + "certificate": true + } + ] + } + } + } +} diff --git a/providers/dns/abion/internal/fixtures/zones.json b/providers/dns/abion/internal/fixtures/zones.json new file mode 100644 index 0000000000..3fa444dd9a --- /dev/null +++ b/providers/dns/abion/internal/fixtures/zones.json @@ -0,0 +1,22 @@ +{ + "meta": { + "invocationId": "95cdcc21-b9c3-4b21-8bd1-b05c34c56147", + "offset": 0, + "limit": 1, + "total": 1 + }, + "data": [ + { + "type": "zone", + "id": "dipcon.com", + "attributes": { + "organisationId": "10154", + "organisationDescription": "My Company AB", + "dnsTypeDescription": "Anycast", + "slave": true, + "pending": true, + "deleted": true + } + } + ] +} diff --git a/providers/dns/abion/internal/types.go b/providers/dns/abion/internal/types.go new file mode 100644 index 0000000000..e19b468c2e --- /dev/null +++ b/providers/dns/abion/internal/types.go @@ -0,0 +1,72 @@ +package internal + +import "fmt" + +type ZoneRequest struct { + Data Zone `json:"data,omitempty"` +} + +type Pagination struct { + Offset int `json:"offset,omitempty" url:"offset"` + Limit int `json:"limit,omitempty" url:"limit"` + Total int `json:"total,omitempty" url:"total"` +} + +type APIResponse[T any] struct { + Meta *Metadata `json:"meta,omitempty"` + Data T `json:"data,omitempty"` + Error *Error `json:"error,omitempty"` +} + +type Metadata struct { + InvocationID string `json:"invocationId,omitempty"` + *Pagination +} + +type Zone struct { + Type string `json:"type,omitempty"` + ID string `json:"id,omitempty"` + Attributes Attributes `json:"attributes,omitempty"` +} + +type Attributes struct { + OrganisationID string `json:"organisationId,omitempty"` + OrganisationDescription string `json:"organisationDescription,omitempty"` + DNSTypeDescription string `json:"dnsTypeDescription,omitempty"` + Slave bool `json:"slave,omitempty"` + Pending bool `json:"pending,omitempty"` + Deleted bool `json:"deleted,omitempty"` + Settings *Settings `json:"settings,omitempty"` + Records map[string]map[string][]Record `json:"records,omitempty"` + Redirects map[string][]Redirect `json:"redirects,omitempty"` +} + +type Settings struct { + MName string `json:"mname,omitempty"` + Refresh int `json:"refresh,omitempty"` + Expire int `json:"expire,omitempty"` + TTL int `json:"ttl,omitempty"` +} + +type Record struct { + TTL int `json:"ttl,omitempty"` + Data string `json:"rdata,omitempty"` + Comments string `json:"comments,omitempty"` +} + +type Redirect struct { + Path string `json:"path"` + Destination string `json:"destination"` + Status int `json:"status"` + Slugs bool `json:"slugs"` + Certificate bool `json:"certificate"` +} + +type Error struct { + Status int `json:"status"` + Message string `json:"message"` +} + +func (e *Error) Error() string { + return fmt.Sprintf("api error: status=%d, message=%s", e.Status, e.Message) +}