Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/scaleway.com #899

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
- OVH
- Porkbun
- Route53
- Scaleway
- Selfhost.de
- Servercow.de
- Spdyn
Expand Down Expand Up @@ -256,6 +257,7 @@ Check the documentation for your DNS provider:
- [OpenDNS](docs/opendns.md)
- [OVH](docs/ovh.md)
- [Porkbun](docs/porkbun.md)
- [Scaleway](docs/scaleway.md)
- [Selfhost.de](docs/selfhost.de.md)
- [Servercow.de](docs/servercow.md)
- [Spdyn](docs/spdyn.md)
Expand Down
37 changes: 37 additions & 0 deletions docs/scaleway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Example.com

## Configuration

If something is unclear in the documentation below, please refer to the [scaleway API documentation](https://www.scaleway.com/en/developers/api/domains-and-dns/#path-records-update-records-within-a-dns-zone).

### Example

```json
{
"settings": [
{
"provider": "scaleway",
"domain": "munchkin-academia.eu",
"secret_key": "<SECRET_KEY>",
"ip_version": "ipv4",
"ipv6_suffix": "",
"ttl": 450
}
]
}
```

### Compulsory parameters

- `"domain"` is the domain to update. It can be `example.com` (root domain), `sub.example.com` (subdomain of `example.com`) or `*.example.com` for the wildcard. This field is used to extract the `dns-zone`, `id_fields.name`, and `records.name`, and used to make the scaleway API call. For example. if your domain is `example.com`, and you set as `"domain`" `sub.example.com`, then the API call will be made with `dns-zone = example.com`, `id_fields.name = sub`, and `records.name = sub`.
- `"secret_key"`

### Optional parameters

- `"ip_version"` can be `"ipv4"` or `"ipv6"`. It defaults to `"ipv4"`.
- `"ipv6_suffix"` is the suffix to append to the IPv6 address. It defaults to `""`.
- `"ttl"` is the TTL of the DNS record to update. It defaults to `3600`.

## Domain setup

If you need more information about how to configure your domain, you can check the [scaleway official documentation](https://www.scaleway.com/en/docs/network/domains-and-dns/).
2 changes: 2 additions & 0 deletions internal/provider/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
OVH models.Provider = "ovh"
Porkbun models.Provider = "porkbun"
Route53 models.Provider = "route53"
Scaleway models.Provider = "scaleway"
SelfhostDe models.Provider = "selfhost.de"
Servercow models.Provider = "servercow"
Spdyn models.Provider = "spdyn"
Expand Down Expand Up @@ -103,6 +104,7 @@ func ProviderChoices() []models.Provider {
OVH,
Porkbun,
Route53,
Scaleway,
SelfhostDe,
Spdyn,
Strato,
Expand Down
4 changes: 4 additions & 0 deletions internal/provider/headers/headers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func SetXFilter(request *http.Request, value string) {
request.Header.Set("X-Filter", value)
}

func SetXAuthToken(request *http.Request, value string) {
request.Header.Set("X-Auth-Token", value)
}

func SetXAuthUsername(request *http.Request, value string) {
request.Header.Set("X-Auth-Username", value)
}
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/providers/ovh"
"github.com/qdm12/ddns-updater/internal/provider/providers/porkbun"
"github.com/qdm12/ddns-updater/internal/provider/providers/route53"
"github.com/qdm12/ddns-updater/internal/provider/providers/scaleway"
"github.com/qdm12/ddns-updater/internal/provider/providers/selfhostde"
"github.com/qdm12/ddns-updater/internal/provider/providers/servercow"
"github.com/qdm12/ddns-updater/internal/provider/providers/spdyn"
Expand Down Expand Up @@ -174,6 +175,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
return porkbun.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Route53:
return route53.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Scaleway:
return scaleway.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.SelfhostDe:
return selfhostde.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Servercow:
Expand Down
195 changes: 195 additions & 0 deletions internal/provider/providers/scaleway/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package scaleway

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/netip"
"net/url"

"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/provider/constants"
"github.com/qdm12/ddns-updater/internal/provider/errors"
"github.com/qdm12/ddns-updater/internal/provider/headers"
"github.com/qdm12/ddns-updater/internal/provider/utils"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)

type Provider struct {
domain string
owner string
ipVersion ipversion.IPVersion
ipv6Suffix netip.Prefix
secretKey string
ttl uint16
}

func New(data json.RawMessage, domain, owner string,
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
provider *Provider, err error,
) {
var providerSpecificSettings struct {
SecretKey string `json:"secret_key"`
TTL uint16 `json:"ttl"`
}
err = json.Unmarshal(data, &providerSpecificSettings)
if err != nil {
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
}

if providerSpecificSettings.TTL == 0 {
providerSpecificSettings.TTL = 3600
}

err = validateSettings(domain,
providerSpecificSettings.SecretKey)
if err != nil {
return nil, fmt.Errorf("validating provider specific settings: %w", err)
}

return &Provider{
domain: domain,
owner: owner,
ipVersion: ipVersion,
ipv6Suffix: ipv6Suffix,
secretKey: providerSpecificSettings.SecretKey,
ttl: providerSpecificSettings.TTL,
}, nil
}

func validateSettings(domain, secretKey string) (err error) {
err = utils.CheckDomain(domain)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
}

if secretKey == "" {
return fmt.Errorf("%w", errors.ErrSecretKeyNotSet)
}

return nil
}

func (p *Provider) String() string {
return utils.ToString(p.domain, p.owner, constants.Dyn, p.ipVersion)
}

func (p *Provider) Domain() string {
return p.domain
}

func (p *Provider) Owner() string {
return p.owner
}

func (p *Provider) IPVersion() ipversion.IPVersion {
return p.ipVersion
}

func (p *Provider) IPv6Suffix() netip.Prefix {
return p.ipv6Suffix
}

func (p *Provider) Proxied() bool {
return false
}

func (p *Provider) BuildDomainName() string {
return utils.BuildDomainName(p.owner, p.domain)
}

func (p *Provider) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
Owner: p.Owner(),
Provider: "<a href=\"https://www.scaleway.com/\">Scaleway</a>",
IPVersion: p.ipVersion.String(),
}
}

// Update updates the DNS record for the domain using Scaleway's API.
// See https://www.scaleway.com/en/developers/api/domains-and-dns/#path-records-update-records-within-a-dns-zone
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "api.scaleway.com",
Path: fmt.Sprintf("/domain/v2beta1/dns-zones/%s/records", p.domain),
}

fieldType := "A"
if ip.Is6() {
fieldType = "AAAA"
}
type recordJSON struct {
Data string `json:"data"`
Name string `json:"name"`
TTL uint16 `json:"ttl"`
}
type changeJSON struct {
Set struct {
IDFields struct {
Name string `json:"name"`
Type string `json:"type"`
} `json:"id_fields"`
Records []recordJSON `json:"records"`
} `json:"set"`
}
var change changeJSON
change.Set.IDFields.Name = p.owner
change.Set.IDFields.Type = fieldType
change.Set.Records = []recordJSON{{
Data: ip.String(),
Name: p.owner,
TTL: p.ttl,
}}
requestBody := struct {
Changes []changeJSON `json:"changes"`
}{
Changes: []changeJSON{change},
}

buffer := bytes.NewBuffer(nil)
encoder := json.NewEncoder(buffer)
err = encoder.Encode(requestBody)
if err != nil {
return netip.Addr{}, fmt.Errorf("json encoding request body: %w", err)
}

request, err := http.NewRequestWithContext(ctx, http.MethodPatch, u.String(), buffer)
if err != nil {
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
}
headers.SetContentType(request, "application/json")
headers.SetAccept(request, "application/json")
headers.SetXAuthToken(request, p.secretKey)
headers.SetUserAgent(request)

response, err := client.Do(request)
if err != nil {
return netip.Addr{}, fmt.Errorf("doing http request: %w", err)
}
defer response.Body.Close()

s, err := utils.ReadAndCleanBody(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
}

if response.StatusCode != http.StatusOK {
var errorResponse struct {
Message string `json:"message"`
Type string `json:"type"`
}
if jsonErr := json.Unmarshal([]byte(s), &errorResponse); jsonErr == nil {
if errorResponse.Type == "denied_authentication" {
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrAuth, errorResponse.Message)
}
}
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
}

return ip, nil
}