mirror of
https://github.com/kubernetes-sigs/external-dns.git
synced 2025-08-06 17:46:57 +02:00
feat(ovh): major rewriting of the provider (#5143)
* feat: ovh: improve cache invalidation on errors + dry-run mode + relative CNAME handling + optimization Signed-off-by: Romain Beuque <556072+rbeuque74@users.noreply.github.com> * chore: add more tests Signed-off-by: Romain Beuque <556072+rbeuque74@users.noreply.github.com> * fix: align cache expiration with Default value * chore: address comments from review + updated documentation * chore: address comments from review --------- Signed-off-by: Romain Beuque <556072+rbeuque74@users.noreply.github.com>
This commit is contained in:
parent
823ea7e6f1
commit
ecd57c86f5
@ -57,7 +57,7 @@ ExternalDNS allows you to keep selected zones (via `--domain-filter`) synchroniz
|
|||||||
- [RFC2136](https://tools.ietf.org/html/rfc2136)
|
- [RFC2136](https://tools.ietf.org/html/rfc2136)
|
||||||
- [NS1](https://ns1.com/)
|
- [NS1](https://ns1.com/)
|
||||||
- [TransIP](https://www.transip.eu/domain-name/)
|
- [TransIP](https://www.transip.eu/domain-name/)
|
||||||
- [OVH](https://www.ovh.com)
|
- [OVHcloud](https://www.ovhcloud.com)
|
||||||
- [Scaleway](https://www.scaleway.com)
|
- [Scaleway](https://www.scaleway.com)
|
||||||
- [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
|
- [Akamai Edge DNS](https://learn.akamai.com/en-us/products/cloud_security/edge_dns.html)
|
||||||
- [GoDaddy](https://www.godaddy.com)
|
- [GoDaddy](https://www.godaddy.com)
|
||||||
@ -85,7 +85,7 @@ See PR #3063 for all the discussions about it.
|
|||||||
Known providers using webhooks:
|
Known providers using webhooks:
|
||||||
|
|
||||||
| Provider | Repo |
|
| Provider | Repo |
|
||||||
|-----------------------|----------------------------------------------------------------------|
|
| --------------------- | -------------------------------------------------------------------- |
|
||||||
| Abion | https://github.com/abiondevelopment/external-dns-webhook-abion |
|
| Abion | https://github.com/abiondevelopment/external-dns-webhook-abion |
|
||||||
| Adguard Home Provider | https://github.com/muhlba91/external-dns-provider-adguard |
|
| Adguard Home Provider | https://github.com/muhlba91/external-dns-provider-adguard |
|
||||||
| Anexia | https://github.com/ProbstenHias/external-dns-anexia-webhook |
|
| Anexia | https://github.com/ProbstenHias/external-dns-anexia-webhook |
|
||||||
@ -145,7 +145,7 @@ The following table clarifies the current status of the providers according to t
|
|||||||
| RFC2136 | Alpha | |
|
| RFC2136 | Alpha | |
|
||||||
| NS1 | Alpha | |
|
| NS1 | Alpha | |
|
||||||
| TransIP | Alpha | |
|
| TransIP | Alpha | |
|
||||||
| OVH | Alpha | |
|
| OVHcloud | Beta | @rbeuque74 |
|
||||||
| Scaleway DNS | Alpha | @Sh4d1 |
|
| Scaleway DNS | Alpha | @Sh4d1 |
|
||||||
| UltraDNS | Alpha | |
|
| UltraDNS | Alpha | |
|
||||||
| GoDaddy | Alpha | |
|
| GoDaddy | Alpha | |
|
||||||
@ -207,7 +207,7 @@ The following tutorials are provided:
|
|||||||
- [PowerDNS](docs/tutorials/pdns.md)
|
- [PowerDNS](docs/tutorials/pdns.md)
|
||||||
- [RFC2136](docs/tutorials/rfc2136.md)
|
- [RFC2136](docs/tutorials/rfc2136.md)
|
||||||
- [TransIP](docs/tutorials/transip.md)
|
- [TransIP](docs/tutorials/transip.md)
|
||||||
- [OVH](docs/tutorials/ovh.md)
|
- [OVHcloud](docs/tutorials/ovh.md)
|
||||||
- [Scaleway](docs/tutorials/scaleway.md)
|
- [Scaleway](docs/tutorials/scaleway.md)
|
||||||
- [UltraDNS](docs/tutorials/ultradns.md)
|
- [UltraDNS](docs/tutorials/ultradns.md)
|
||||||
- [GoDaddy](docs/tutorials/godaddy.md)
|
- [GoDaddy](docs/tutorials/godaddy.md)
|
||||||
|
@ -107,6 +107,7 @@
|
|||||||
| `--inmemory-zone=` | Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional) |
|
| `--inmemory-zone=` | Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional) |
|
||||||
| `--ovh-endpoint="ovh-eu"` | When using the OVH provider, specify the endpoint (default: ovh-eu) |
|
| `--ovh-endpoint="ovh-eu"` | When using the OVH provider, specify the endpoint (default: ovh-eu) |
|
||||||
| `--ovh-api-rate-limit=20` | When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20) |
|
| `--ovh-api-rate-limit=20` | When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20) |
|
||||||
|
| `--[no-]ovh-enable-cname-relative` | When using the OVH provider, specify if CNAME should be treated as relative on target without final dot (default: false) |
|
||||||
| `--pdns-server="http://localhost:8081"` | When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns) |
|
| `--pdns-server="http://localhost:8081"` | When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns) |
|
||||||
| `--pdns-server-id="localhost"` | When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost) |
|
| `--pdns-server-id="localhost"` | When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost) |
|
||||||
| `--pdns-api-key=""` | When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns) |
|
| `--pdns-api-key=""` | When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns) |
|
||||||
|
@ -1,30 +1,31 @@
|
|||||||
# OVHcloud
|
# OVHcloud
|
||||||
|
|
||||||
This tutorial describes how to setup ExternalDNS for use within a
|
This tutorial describes how to setup ExternalDNS for use within a
|
||||||
Kubernetes cluster using OVH DNS.
|
Kubernetes cluster using OVHcloud DNS.
|
||||||
|
|
||||||
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
|
Make sure to use **>=0.6** version of ExternalDNS for this tutorial.
|
||||||
|
|
||||||
## Creating a zone with OVH DNS
|
## Creating a zone with OVHcloud DNS
|
||||||
|
|
||||||
If you are new to OVH, we recommend you first read the following
|
If you are new to OVHcloud, we recommend you first read the following
|
||||||
instructions for creating a zone.
|
instructions for creating a zone.
|
||||||
|
|
||||||
[Creating a zone using the OVH manager](https://docs.ovh.com/gb/en/domains/create_a_dns_zone_for_a_domain_which_is_not_registered_at_ovh/)
|
[Creating a zone using the OVHcloud Manager](https://help.ovhcloud.com/csm/en-gb-dns-create-dns-zone?id=kb_article_view&sysparm_article=KB0051667/)
|
||||||
|
|
||||||
[Creating a zone using the OVH API](https://api.ovh.com/console/)
|
[Creating a zone using the OVHcloud API](https://api.ovh.com/console/)
|
||||||
|
|
||||||
## Creating OVH Credentials
|
## Creating OVHcloud Credentials
|
||||||
|
|
||||||
You first need to create an OVH application.
|
You first need to create an OVHcloud application: follow the
|
||||||
|
[OVHcloud documentation](https://help.ovhcloud.com/csm/en-gb-api-getting-started-ovhcloud-api?id=kb_article_view&sysparm_article=KB0042784#advanced-usage-pair-ovhcloud-apis-with-an-application)
|
||||||
Using the [OVH documentation](https://docs.ovh.com/gb/en/api/first-steps-with-ovh-api/#advanced-usage-pair-ovhcloud-apis-with-an-application_2) you will have your `Application key` and `Application secret`
|
you will have your `Application key` and `Application secret`
|
||||||
|
|
||||||
And you will need to generate your consumer key, here the permissions needed :
|
And you will need to generate your consumer key, here the permissions needed :
|
||||||
|
|
||||||
- GET on `/domain/zone`
|
- GET on `/domain/zone`
|
||||||
- GET on `/domain/zone/*/record`
|
- GET on `/domain/zone/*/record`
|
||||||
- GET on `/domain/zone/*/record/*`
|
- GET on `/domain/zone/*/record/*`
|
||||||
|
- PUT on `/domain/zone/*/record/*`
|
||||||
- POST on `/domain/zone/*/record`
|
- POST on `/domain/zone/*/record`
|
||||||
- DELETE on `/domain/zone/*/record/*`
|
- DELETE on `/domain/zone/*/record/*`
|
||||||
- GET on `/domain/zone/*/soa`
|
- GET on `/domain/zone/*/soa`
|
||||||
@ -51,6 +52,10 @@ curl -XPOST -H "X-Ovh-Application: <ApplicationKey>" -H "Content-type: applicati
|
|||||||
"method": "GET",
|
"method": "GET",
|
||||||
"path": "/domain/zone/*/record/*"
|
"path": "/domain/zone/*/record/*"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"method": "PUT",
|
||||||
|
"path": "/domain/zone/*/record/*"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"method": "POST",
|
"method": "POST",
|
||||||
"path": "/domain/zone/*/record"
|
"path": "/domain/zone/*/record"
|
||||||
@ -223,7 +228,7 @@ spec:
|
|||||||
|
|
||||||
**A note about annotations**
|
**A note about annotations**
|
||||||
|
|
||||||
Verify that the annotation on the service uses the same hostname as the OVH DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. 'www.example.com').
|
Verify that the annotation on the service uses the same hostname as the OVHcloud DNS zone created above. The annotation may also be a subdomain of the DNS zone (e.g. 'www.example.com').
|
||||||
|
|
||||||
The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.
|
The TTL annotation can be used to configure the TTL on DNS records managed by ExternalDNS and is optional. If this annotation is not set, the TTL on records managed by ExternalDNS will default to 10.
|
||||||
|
|
||||||
@ -235,11 +240,11 @@ ExternalDNS uses the hostname annotation to determine which services should be r
|
|||||||
kubectl create -f nginx.yaml
|
kubectl create -f nginx.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the OVH DNS records.
|
Depending on where you run your service, it may take some time for your cloud provider to create an external IP for the service. Once an external IP is assigned, ExternalDNS detects the new service IP address and synchronizes the OVHcloud DNS records.
|
||||||
|
|
||||||
## Verifying OVH DNS records
|
## Verifying OVHcloud DNS records
|
||||||
|
|
||||||
Use the OVH manager or API to verify that the A record for your domain shows the external IP address of the services.
|
Use the OVHcloud manager or API to verify that the A record for your domain shows the external IP address of the services.
|
||||||
|
|
||||||
## Cleanup
|
## Cleanup
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ func (f *Flags) addFlag(name, description string) {
|
|||||||
|
|
||||||
// It generates a markdown file
|
// It generates a markdown file
|
||||||
// with the supported flags and writes it to the 'docs/flags.md' file.
|
// with the supported flags and writes it to the 'docs/flags.md' file.
|
||||||
// to re-generate `docs/flags.md` execute 'go run internal/gen/main.go'
|
// to re-generate `docs/flags.md` execute 'go run internal/gen/docs/flags/main.go'
|
||||||
func main() {
|
func main() {
|
||||||
testPath, _ := os.Getwd()
|
testPath, _ := os.Getwd()
|
||||||
path := fmt.Sprintf("%s/docs/flags.md", testPath)
|
path := fmt.Sprintf("%s/docs/flags.md", testPath)
|
||||||
|
2
main.go
2
main.go
@ -267,7 +267,7 @@ func main() {
|
|||||||
case "digitalocean":
|
case "digitalocean":
|
||||||
p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
|
p, err = digitalocean.NewDigitalOceanProvider(ctx, domainFilter, cfg.DryRun, cfg.DigitalOceanAPIPageSize)
|
||||||
case "ovh":
|
case "ovh":
|
||||||
p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.DryRun)
|
p, err = ovh.NewOVHProvider(ctx, domainFilter, cfg.OVHEndpoint, cfg.OVHApiRateLimit, cfg.OVHEnableCNAMERelative, cfg.DryRun)
|
||||||
case "linode":
|
case "linode":
|
||||||
p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
|
p, err = linode.NewLinodeProvider(domainFilter, cfg.DryRun, externaldns.Version)
|
||||||
case "dnsimple":
|
case "dnsimple":
|
||||||
|
@ -131,6 +131,7 @@ type Config struct {
|
|||||||
InMemoryZones []string
|
InMemoryZones []string
|
||||||
OVHEndpoint string
|
OVHEndpoint string
|
||||||
OVHApiRateLimit int
|
OVHApiRateLimit int
|
||||||
|
OVHEnableCNAMERelative bool
|
||||||
PDNSServer string
|
PDNSServer string
|
||||||
PDNSServerID string
|
PDNSServerID string
|
||||||
PDNSAPIKey string `secure:"yes"`
|
PDNSAPIKey string `secure:"yes"`
|
||||||
@ -295,6 +296,7 @@ var defaultConfig = &Config{
|
|||||||
InMemoryZones: []string{},
|
InMemoryZones: []string{},
|
||||||
OVHEndpoint: "ovh-eu",
|
OVHEndpoint: "ovh-eu",
|
||||||
OVHApiRateLimit: 20,
|
OVHApiRateLimit: 20,
|
||||||
|
OVHEnableCNAMERelative: false,
|
||||||
PDNSServer: "http://localhost:8081",
|
PDNSServer: "http://localhost:8081",
|
||||||
PDNSServerID: "localhost",
|
PDNSServerID: "localhost",
|
||||||
PDNSAPIKey: "",
|
PDNSAPIKey: "",
|
||||||
@ -544,6 +546,7 @@ func App(cfg *Config) *kingpin.Application {
|
|||||||
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
|
app.Flag("inmemory-zone", "Provide a list of pre-configured zones for the inmemory provider; specify multiple times for multiple zones (optional)").Default("").StringsVar(&cfg.InMemoryZones)
|
||||||
app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint)
|
app.Flag("ovh-endpoint", "When using the OVH provider, specify the endpoint (default: ovh-eu)").Default(defaultConfig.OVHEndpoint).StringVar(&cfg.OVHEndpoint)
|
||||||
app.Flag("ovh-api-rate-limit", "When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20)").Default(strconv.Itoa(defaultConfig.OVHApiRateLimit)).IntVar(&cfg.OVHApiRateLimit)
|
app.Flag("ovh-api-rate-limit", "When using the OVH provider, specify the API request rate limit, X operations by seconds (default: 20)").Default(strconv.Itoa(defaultConfig.OVHApiRateLimit)).IntVar(&cfg.OVHApiRateLimit)
|
||||||
|
app.Flag("ovh-enable-cname-relative", "When using the OVH provider, specify if CNAME should be treated as relative on target without final dot (default: false)").Default(strconv.FormatBool(defaultConfig.OVHEnableCNAMERelative)).BoolVar(&cfg.OVHEnableCNAMERelative)
|
||||||
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
|
app.Flag("pdns-server", "When using the PowerDNS/PDNS provider, specify the URL to the pdns server (required when --provider=pdns)").Default(defaultConfig.PDNSServer).StringVar(&cfg.PDNSServer)
|
||||||
app.Flag("pdns-server-id", "When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost)").Default(defaultConfig.PDNSServerID).StringVar(&cfg.PDNSServerID)
|
app.Flag("pdns-server-id", "When using the PowerDNS/PDNS provider, specify the id of the server to retrieve. Should be `localhost` except when the server is behind a proxy (optional when --provider=pdns) (default: localhost)").Default(defaultConfig.PDNSServerID).StringVar(&cfg.PDNSServerID)
|
||||||
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
|
app.Flag("pdns-api-key", "When using the PowerDNS/PDNS provider, specify the API key to use to authorize requests (required when --provider=pdns)").Default(defaultConfig.PDNSAPIKey).StringVar(&cfg.PDNSAPIKey)
|
||||||
|
@ -20,6 +20,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -41,13 +44,12 @@ const (
|
|||||||
ovhDefaultTTL = 0
|
ovhDefaultTTL = 0
|
||||||
ovhCreate = iota
|
ovhCreate = iota
|
||||||
ovhDelete
|
ovhDelete
|
||||||
|
ovhUpdate
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
|
// ErrRecordToMutateNotFound when ApplyChange has to update/delete and didn't found the record in the existing zone (Change with no record ID)
|
||||||
ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
|
ErrRecordToMutateNotFound = errors.New("record to mutate not found in current zone")
|
||||||
// ErrNoDryRun No dry run support for the moment
|
|
||||||
ErrNoDryRun = errors.New("dry run not supported")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// OVHProvider is an implementation of Provider for OVH DNS.
|
// OVHProvider is an implementation of Provider for OVH DNS.
|
||||||
@ -59,7 +61,15 @@ type OVHProvider struct {
|
|||||||
apiRateLimiter ratelimit.Limiter
|
apiRateLimiter ratelimit.Limiter
|
||||||
|
|
||||||
domainFilter endpoint.DomainFilter
|
domainFilter endpoint.DomainFilter
|
||||||
DryRun bool
|
|
||||||
|
// DryRun enables dry-run mode
|
||||||
|
DryRun bool
|
||||||
|
|
||||||
|
// EnableCNAMERelativeTarget controls if CNAME target should be sent with relative format.
|
||||||
|
// Previous implementations of the OVHProvider always added a final dot as for absolut format.
|
||||||
|
// Default value is false, all CNAME are transformed into absolut format.
|
||||||
|
// Setting this to true will allow relative format to be sent to DNS zone.
|
||||||
|
EnableCNAMERelativeTarget bool
|
||||||
|
|
||||||
// UseCache controls if the OVHProvider will cache records in memory, and serve them
|
// UseCache controls if the OVHProvider will cache records in memory, and serve them
|
||||||
// without recontacting the OVHcloud API if the SOA of the domain zone hasn't changed.
|
// without recontacting the OVHcloud API if the SOA of the domain zone hasn't changed.
|
||||||
@ -67,16 +77,19 @@ type OVHProvider struct {
|
|||||||
// your refresh rate/number of records is too big, which might cause issue with the
|
// your refresh rate/number of records is too big, which might cause issue with the
|
||||||
// provider.
|
// provider.
|
||||||
// Default value: true
|
// Default value: true
|
||||||
UseCache bool
|
UseCache bool
|
||||||
|
lastRunRecords []ovhRecord
|
||||||
|
lastRunZones []string
|
||||||
|
|
||||||
cacheInstance *cache.Cache
|
cacheInstance *cache.Cache
|
||||||
dnsClient dnsClient
|
dnsClient dnsClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type ovhClient interface {
|
type ovhClient interface {
|
||||||
Post(string, interface{}, interface{}) error
|
PostWithContext(context.Context, string, any, any) error
|
||||||
Get(string, interface{}) error
|
PutWithContext(context.Context, string, any, any) error
|
||||||
Delete(string, interface{}) error
|
GetWithContext(context.Context, string, any) error
|
||||||
|
DeleteWithContext(context.Context, string, any) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type dnsClient interface {
|
type dnsClient interface {
|
||||||
@ -84,7 +97,11 @@ type dnsClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ovhRecordFields struct {
|
type ovhRecordFields struct {
|
||||||
|
ovhRecordFieldUpdate
|
||||||
FieldType string `json:"fieldType"`
|
FieldType string `json:"fieldType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ovhRecordFieldUpdate struct {
|
||||||
SubDomain string `json:"subDomain"`
|
SubDomain string `json:"subDomain"`
|
||||||
TTL int64 `json:"ttl"`
|
TTL int64 `json:"ttl"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
@ -96,86 +113,160 @@ type ovhRecord struct {
|
|||||||
Zone string `json:"zone"`
|
Zone string `json:"zone"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r ovhRecord) String() string {
|
||||||
|
return "record#" + strconv.Itoa(int(r.ID)) + ": " + r.FieldType + " | " + r.SubDomain + " => " + r.Target + " (" + strconv.Itoa(int(r.TTL)) + ")"
|
||||||
|
}
|
||||||
|
|
||||||
type ovhChange struct {
|
type ovhChange struct {
|
||||||
ovhRecord
|
ovhRecord
|
||||||
Action int
|
Action int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOVHProvider initializes a new OVH DNS based Provider.
|
// NewOVHProvider initializes a new OVH DNS based Provider.
|
||||||
func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, endpoint string, apiRateLimit int, dryRun bool) (*OVHProvider, error) {
|
func NewOVHProvider(ctx context.Context, domainFilter endpoint.DomainFilter, endpoint string, apiRateLimit int, enableCNAMERelative, dryRun bool) (*OVHProvider, error) {
|
||||||
client, err := ovh.NewEndpointClient(endpoint)
|
client, err := ovh.NewEndpointClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
client.UserAgent = externaldns.Version
|
client.UserAgent = "ExternalDNS/" + externaldns.Version
|
||||||
|
|
||||||
// TODO: Add Dry Run support
|
|
||||||
if dryRun {
|
|
||||||
return nil, ErrNoDryRun
|
|
||||||
}
|
|
||||||
return &OVHProvider{
|
return &OVHProvider{
|
||||||
client: client,
|
client: client,
|
||||||
domainFilter: domainFilter,
|
domainFilter: domainFilter,
|
||||||
apiRateLimiter: ratelimit.New(apiRateLimit),
|
apiRateLimiter: ratelimit.New(apiRateLimit),
|
||||||
DryRun: dryRun,
|
DryRun: dryRun,
|
||||||
cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration),
|
cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration),
|
||||||
dnsClient: new(dns.Client),
|
dnsClient: new(dns.Client),
|
||||||
UseCache: true,
|
UseCache: true,
|
||||||
|
EnableCNAMERelativeTarget: enableCNAMERelative,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Records returns the list of records in all relevant zones.
|
// Records returns the list of records in all relevant zones.
|
||||||
func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
func (p *OVHProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) {
|
||||||
_, records, err := p.zonesRecords(ctx)
|
zones, records, err := p.zonesRecords(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
p.lastRunRecords = records
|
||||||
|
p.lastRunZones = zones
|
||||||
endpoints := ovhGroupByNameAndType(records)
|
endpoints := ovhGroupByNameAndType(records)
|
||||||
log.Infof("OVH: %d endpoints have been found", len(endpoints))
|
log.Infof("OVH: %d endpoints have been found", len(endpoints))
|
||||||
return endpoints, nil
|
return endpoints, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyChanges applies a given set of changes in a given zone.
|
func planChangesByZoneName(zones []string, changes *plan.Changes) map[string]*plan.Changes {
|
||||||
func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) (err error) {
|
zoneNameIDMapper := provider.ZoneIDName{}
|
||||||
zones, records, err := p.zonesRecords(ctx)
|
for _, zone := range zones {
|
||||||
if err != nil {
|
zoneNameIDMapper.Add(zone, zone)
|
||||||
return provider.NewSoftError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
zonesChangeUniques := map[string]bool{}
|
output := map[string]*plan.Changes{}
|
||||||
|
for _, endpt := range changes.Delete {
|
||||||
// Always refresh zones even in case of errors.
|
_, zoneName := zoneNameIDMapper.FindZone(endpt.DNSName)
|
||||||
defer func() {
|
if _, ok := output[zoneName]; !ok {
|
||||||
log.Debugf("OVH: %d zones will be refreshed", len(zonesChangeUniques))
|
output[zoneName] = &plan.Changes{}
|
||||||
|
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
|
||||||
for zone := range zonesChangeUniques {
|
|
||||||
// This is necessary because the loop variable zone is reused in each iteration of the loop,
|
|
||||||
// and without this line, the goroutines launched by eg.Go would all reference the same zone variable.
|
|
||||||
zone := zone
|
|
||||||
eg.Go(func() error { return p.refresh(zone) })
|
|
||||||
}
|
}
|
||||||
|
output[zoneName].Delete = append(output[zoneName].Delete, endpt)
|
||||||
if e := eg.Wait(); e != nil && err == nil { // return the error only if there is no error during the changes
|
}
|
||||||
err = provider.NewSoftError(e)
|
for _, endpt := range changes.Create {
|
||||||
|
_, zoneName := zoneNameIDMapper.FindZone(endpt.DNSName)
|
||||||
|
if _, ok := output[zoneName]; !ok {
|
||||||
|
output[zoneName] = &plan.Changes{}
|
||||||
}
|
}
|
||||||
}()
|
output[zoneName].Create = append(output[zoneName].Create, endpt)
|
||||||
|
}
|
||||||
|
for _, endpt := range changes.UpdateOld {
|
||||||
|
_, zoneName := zoneNameIDMapper.FindZone(endpt.DNSName)
|
||||||
|
if _, ok := output[zoneName]; !ok {
|
||||||
|
output[zoneName] = &plan.Changes{}
|
||||||
|
}
|
||||||
|
output[zoneName].UpdateOld = append(output[zoneName].UpdateOld, endpt)
|
||||||
|
}
|
||||||
|
for _, endpt := range changes.UpdateNew {
|
||||||
|
_, zoneName := zoneNameIDMapper.FindZone(endpt.DNSName)
|
||||||
|
if _, ok := output[zoneName]; !ok {
|
||||||
|
output[zoneName] = &plan.Changes{}
|
||||||
|
}
|
||||||
|
output[zoneName].UpdateNew = append(output[zoneName].UpdateNew, endpt)
|
||||||
|
}
|
||||||
|
|
||||||
allChanges := make([]ovhChange, 0, countTargets(changes.Create, changes.UpdateNew, changes.UpdateOld, changes.Delete))
|
return output
|
||||||
allChanges = append(allChanges, newOvhChange(ovhCreate, changes.Create, zones, records)...)
|
}
|
||||||
allChanges = append(allChanges, newOvhChange(ovhCreate, changes.UpdateNew, zones, records)...)
|
|
||||||
allChanges = append(allChanges, newOvhChange(ovhDelete, changes.UpdateOld, zones, records)...)
|
|
||||||
allChanges = append(allChanges, newOvhChange(ovhDelete, changes.Delete, zones, records)...)
|
|
||||||
|
|
||||||
log.Infof("OVH: %d changes will be done", len(allChanges))
|
func (p OVHProvider) computeSingleZoneChanges(_ context.Context, zoneName string, existingRecords []ovhRecord, changes *plan.Changes) []ovhChange {
|
||||||
|
allChanges := []ovhChange{}
|
||||||
|
var computedChanges []ovhChange
|
||||||
|
|
||||||
eg, _ := errgroup.WithContext(ctx)
|
computedChanges, existingRecords = p.newOvhChangeCreateDelete(ovhCreate, changes.Create, zoneName, existingRecords)
|
||||||
|
allChanges = append(allChanges, computedChanges...)
|
||||||
|
computedChanges, existingRecords = p.newOvhChangeCreateDelete(ovhDelete, changes.Delete, zoneName, existingRecords)
|
||||||
|
allChanges = append(allChanges, computedChanges...)
|
||||||
|
|
||||||
|
computedChanges = p.newOvhChangeUpdate(changes.UpdateOld, changes.UpdateNew, zoneName, existingRecords)
|
||||||
|
allChanges = append(allChanges, computedChanges...)
|
||||||
|
|
||||||
|
return allChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *OVHProvider) handleSingleZoneUpdate(ctx context.Context, zoneName string, existingRecords []ovhRecord, changes *plan.Changes) error {
|
||||||
|
allChanges := p.computeSingleZoneChanges(ctx, zoneName, existingRecords, changes)
|
||||||
|
log.Infof("OVH: %q: %d changes will be done", zoneName, len(allChanges))
|
||||||
|
|
||||||
|
eg, ctxErrGroup := errgroup.WithContext(ctx)
|
||||||
for _, change := range allChanges {
|
for _, change := range allChanges {
|
||||||
change := change
|
change := change
|
||||||
zonesChangeUniques[change.Zone] = true
|
eg.Go(func() error {
|
||||||
eg.Go(func() error { return p.change(change) })
|
return p.change(ctxErrGroup, change)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := eg.Wait()
|
||||||
|
|
||||||
|
// do not refresh zone if errors: some records might haven't been processed yet, hence the zone will be in an inconsistent state
|
||||||
|
// if modification of the zone was in error, invalidating the cache to make sure next run will start freshly
|
||||||
|
if err == nil {
|
||||||
|
err = p.refresh(ctx, zoneName)
|
||||||
|
} else {
|
||||||
|
p.invalidateCache(zoneName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyChanges applies a given set of changes in a given zone.
|
||||||
|
func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) (err error) {
|
||||||
|
zones, records := p.lastRunZones, p.lastRunRecords
|
||||||
|
defer func() {
|
||||||
|
p.lastRunRecords = []ovhRecord{}
|
||||||
|
p.lastRunZones = []string{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if log.IsLevelEnabled(log.DebugLevel) {
|
||||||
|
for _, change := range changes.Create {
|
||||||
|
log.Debugf("OVH: changes CREATE dns:%q / targets:%v / type:%s", change.DNSName, change.Targets, change.RecordType)
|
||||||
|
}
|
||||||
|
for _, change := range changes.UpdateOld {
|
||||||
|
log.Debugf("OVH: changes UPDATEOLD dns:%q / targets:%v / type:%s", change.DNSName, change.Targets, change.RecordType)
|
||||||
|
}
|
||||||
|
for _, change := range changes.UpdateNew {
|
||||||
|
log.Debugf("OVH: changes UPDATENEW dns:%q / targets:%v / type:%s", change.DNSName, change.Targets, change.RecordType)
|
||||||
|
}
|
||||||
|
for _, change := range changes.Delete {
|
||||||
|
log.Debugf("OVH: changes DELETE dns:%q / targets:%v / type:%s", change.DNSName, change.Targets, change.RecordType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changesByZoneName := planChangesByZoneName(zones, changes)
|
||||||
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
|
for zoneName, changes := range changesByZoneName {
|
||||||
|
eg.Go(func() error {
|
||||||
|
return p.handleSingleZoneUpdate(ctx, zoneName, records, changes)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err := eg.Wait(); err != nil {
|
if err := eg.Wait(); err != nil {
|
||||||
return provider.NewSoftError(err)
|
return provider.NewSoftError(err)
|
||||||
}
|
}
|
||||||
@ -183,7 +274,7 @@ func (p *OVHProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OVHProvider) refresh(zone string) error {
|
func (p *OVHProvider) refresh(ctx context.Context, zone string) error {
|
||||||
log.Debugf("OVH: Refresh %s zone", zone)
|
log.Debugf("OVH: Refresh %s zone", zone)
|
||||||
|
|
||||||
// Zone has been altered so we invalidate the cache
|
// Zone has been altered so we invalidate the cache
|
||||||
@ -191,26 +282,50 @@ func (p *OVHProvider) refresh(zone string) error {
|
|||||||
p.invalidateCache(zone)
|
p.invalidateCache(zone)
|
||||||
|
|
||||||
p.apiRateLimiter.Take()
|
p.apiRateLimiter.Take()
|
||||||
if err := p.client.Post(fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil); err != nil {
|
if p.DryRun {
|
||||||
|
log.Infof("OVH: Dry-run: Would have refresh DNS zone %q", zone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := p.client.PostWithContext(ctx, fmt.Sprintf("/domain/zone/%s/refresh", url.PathEscape(zone)), nil, nil); err != nil {
|
||||||
return provider.NewSoftError(err)
|
return provider.NewSoftError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OVHProvider) change(change ovhChange) error {
|
func (p *OVHProvider) change(ctx context.Context, change ovhChange) error {
|
||||||
p.apiRateLimiter.Take()
|
p.apiRateLimiter.Take()
|
||||||
|
|
||||||
switch change.Action {
|
switch change.Action {
|
||||||
case ovhCreate:
|
case ovhCreate:
|
||||||
log.Debugf("OVH: Add an entry to %s", change.String())
|
log.Debugf("OVH: Add an entry to %s", change.String())
|
||||||
return p.client.Post(fmt.Sprintf("/domain/zone/%s/record", change.Zone), change.ovhRecordFields, nil)
|
if p.DryRun {
|
||||||
|
log.Infof("OVH: Dry-run: Would have created a DNS record for zone %s", change.Zone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.client.PostWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record", url.PathEscape(change.Zone)), change.ovhRecordFields, nil)
|
||||||
case ovhDelete:
|
case ovhDelete:
|
||||||
if change.ID == 0 {
|
if change.ID == 0 {
|
||||||
return ErrRecordToMutateNotFound
|
return ErrRecordToMutateNotFound
|
||||||
}
|
}
|
||||||
log.Debugf("OVH: Delete an entry to %s", change.String())
|
log.Debugf("OVH: Delete an entry to %s", change.String())
|
||||||
return p.client.Delete(fmt.Sprintf("/domain/zone/%s/record/%d", change.Zone, change.ID), nil)
|
if p.DryRun {
|
||||||
|
log.Infof("OVH: Dry-run: Would have deleted a DNS record for zone %s", change.Zone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.client.DeleteWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%d", url.PathEscape(change.Zone), change.ID), nil)
|
||||||
|
case ovhUpdate:
|
||||||
|
if change.ID == 0 {
|
||||||
|
return ErrRecordToMutateNotFound
|
||||||
|
}
|
||||||
|
log.Debugf("OVH: Update an entry to %s", change.String())
|
||||||
|
if p.DryRun {
|
||||||
|
log.Infof("OVH: Dry-run: Would have updated a DNS record for zone %s", change.Zone)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.client.PutWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%d", url.PathEscape(change.Zone), change.ID), change.ovhRecordFieldUpdate, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +335,7 @@ func (p *OVHProvider) invalidateCache(zone string) {
|
|||||||
|
|
||||||
func (p *OVHProvider) zonesRecords(ctx context.Context) ([]string, []ovhRecord, error) {
|
func (p *OVHProvider) zonesRecords(ctx context.Context) ([]string, []ovhRecord, error) {
|
||||||
var allRecords []ovhRecord
|
var allRecords []ovhRecord
|
||||||
zones, err := p.zones()
|
zones, err := p.zones(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, provider.NewSoftError(err)
|
return nil, nil, provider.NewSoftError(err)
|
||||||
}
|
}
|
||||||
@ -229,7 +344,7 @@ func (p *OVHProvider) zonesRecords(ctx context.Context) ([]string, []ovhRecord,
|
|||||||
eg, ctx := errgroup.WithContext(ctx)
|
eg, ctx := errgroup.WithContext(ctx)
|
||||||
for _, zone := range zones {
|
for _, zone := range zones {
|
||||||
zone := zone
|
zone := zone
|
||||||
eg.Go(func() error { return p.records(&ctx, &zone, chRecords) })
|
eg.Go(func() error { return p.records(ctx, &zone, chRecords) })
|
||||||
}
|
}
|
||||||
if err := eg.Wait(); err != nil {
|
if err := eg.Wait(); err != nil {
|
||||||
return nil, nil, provider.NewSoftError(err)
|
return nil, nil, provider.NewSoftError(err)
|
||||||
@ -241,12 +356,12 @@ func (p *OVHProvider) zonesRecords(ctx context.Context) ([]string, []ovhRecord,
|
|||||||
return zones, allRecords, nil
|
return zones, allRecords, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OVHProvider) zones() ([]string, error) {
|
func (p *OVHProvider) zones(ctx context.Context) ([]string, error) {
|
||||||
zones := []string{}
|
zones := []string{}
|
||||||
filteredZones := []string{}
|
filteredZones := []string{}
|
||||||
|
|
||||||
p.apiRateLimiter.Take()
|
p.apiRateLimiter.Take()
|
||||||
if err := p.client.Get("/domain/zone", &zones); err != nil {
|
if err := p.client.GetWithContext(ctx, "/domain/zone", &zones); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,22 +380,24 @@ type ovhSoa struct {
|
|||||||
records []ovhRecord
|
records []ovhRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OVHProvider) records(ctx *context.Context, zone *string, records chan<- []ovhRecord) error {
|
func (p *OVHProvider) records(ctx context.Context, zone *string, records chan<- []ovhRecord) error {
|
||||||
var recordsIds []uint64
|
var recordsIds []uint64
|
||||||
ovhRecords := make([]ovhRecord, len(recordsIds))
|
ovhRecords := make([]ovhRecord, len(recordsIds))
|
||||||
eg, _ := errgroup.WithContext(*ctx)
|
eg, ctxErrGroup := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
if p.UseCache {
|
if p.UseCache {
|
||||||
if cachedSoaItf, ok := p.cacheInstance.Get(*zone + "#soa"); ok {
|
if cachedSoaItf, ok := p.cacheInstance.Get(*zone + "#soa"); ok {
|
||||||
cachedSoa := cachedSoaItf.(ovhSoa)
|
cachedSoa := cachedSoaItf.(ovhSoa)
|
||||||
|
|
||||||
|
log.Debugf("OVH: zone %s: Checking SOA against %v", *zone, cachedSoa.Serial)
|
||||||
|
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetQuestion(dns.Fqdn(*zone), dns.TypeSOA)
|
m.SetQuestion(dns.Fqdn(*zone), dns.TypeSOA)
|
||||||
in, _, err := p.dnsClient.ExchangeContext(*ctx, m, strings.TrimSuffix(cachedSoa.Server, ".")+":53")
|
in, _, err := p.dnsClient.ExchangeContext(ctx, m, strings.TrimSuffix(cachedSoa.Server, ".")+":53")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if s, ok := in.Answer[0].(*dns.SOA); ok {
|
if s, ok := in.Answer[0].(*dns.SOA); ok {
|
||||||
// do something with t.Txt
|
|
||||||
if s.Serial == cachedSoa.Serial {
|
if s.Serial == cachedSoa.Serial {
|
||||||
|
log.Debugf("OVH: zone %s: SOA from cache is valid", *zone)
|
||||||
records <- cachedSoa.records
|
records <- cachedSoa.records
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -291,23 +408,23 @@ func (p *OVHProvider) records(ctx *context.Context, zone *string, records chan<-
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("OVH: Getting records for %s", *zone)
|
log.Debugf("OVH: Getting records for %s from API", *zone)
|
||||||
|
|
||||||
p.apiRateLimiter.Take()
|
p.apiRateLimiter.Take()
|
||||||
var soa ovhSoa
|
var soa ovhSoa
|
||||||
if p.UseCache {
|
if p.UseCache {
|
||||||
if err := p.client.Get("/domain/zone/"+*zone+"/soa", &soa); err != nil {
|
if err := p.client.GetWithContext(ctx, "/domain/zone/"+url.PathEscape(*zone)+"/soa", &soa); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record", *zone), &recordsIds); err != nil {
|
if err := p.client.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record", url.PathEscape(*zone)), &recordsIds); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
chRecords := make(chan ovhRecord, len(recordsIds))
|
chRecords := make(chan ovhRecord, len(recordsIds))
|
||||||
for _, id := range recordsIds {
|
for _, id := range recordsIds {
|
||||||
id := id
|
id := id
|
||||||
eg.Go(func() error { return p.record(zone, id, chRecords) })
|
eg.Go(func() error { return p.record(ctxErrGroup, zone, id, chRecords) })
|
||||||
}
|
}
|
||||||
if err := eg.Wait(); err != nil {
|
if err := eg.Wait(); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -319,20 +436,20 @@ func (p *OVHProvider) records(ctx *context.Context, zone *string, records chan<-
|
|||||||
|
|
||||||
if p.UseCache {
|
if p.UseCache {
|
||||||
soa.records = ovhRecords
|
soa.records = ovhRecords
|
||||||
_ = p.cacheInstance.Add(*zone+"#soa", soa, time.Hour)
|
_ = p.cacheInstance.Add(*zone+"#soa", soa, cache.DefaultExpiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
records <- ovhRecords
|
records <- ovhRecords
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *OVHProvider) record(zone *string, id uint64, records chan<- ovhRecord) error {
|
func (p *OVHProvider) record(ctx context.Context, zone *string, id uint64, records chan<- ovhRecord) error {
|
||||||
record := ovhRecord{}
|
record := ovhRecord{}
|
||||||
|
|
||||||
log.Debugf("OVH: Getting record %d for %s", id, *zone)
|
log.Debugf("OVH: Getting record %d for %s", id, *zone)
|
||||||
|
|
||||||
p.apiRateLimiter.Take()
|
p.apiRateLimiter.Take()
|
||||||
if err := p.client.Get(fmt.Sprintf("/domain/zone/%s/record/%d", *zone, id), &record); err != nil {
|
if err := p.client.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%d", url.PathEscape(*zone), id), &record); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if provider.SupportedRecordType(record.FieldType) {
|
if provider.SupportedRecordType(record.FieldType) {
|
||||||
@ -349,7 +466,7 @@ func ovhGroupByNameAndType(records []ovhRecord) []*endpoint.Endpoint {
|
|||||||
groups := map[string][]ovhRecord{}
|
groups := map[string][]ovhRecord{}
|
||||||
|
|
||||||
for _, r := range records {
|
for _, r := range records {
|
||||||
groupBy := r.Zone + r.SubDomain + r.FieldType
|
groupBy := r.Zone + "//" + r.SubDomain + "//" + r.FieldType
|
||||||
if _, ok := groups[groupBy]; !ok {
|
if _, ok := groups[groupBy]; !ok {
|
||||||
groups[groupBy] = []ovhRecord{}
|
groups[groupBy] = []ovhRecord{}
|
||||||
}
|
}
|
||||||
@ -375,74 +492,214 @@ func ovhGroupByNameAndType(records []ovhRecord) []*endpoint.Endpoint {
|
|||||||
return endpoints
|
return endpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOvhChange(action int, endpoints []*endpoint.Endpoint, zones []string, records []ovhRecord) []ovhChange {
|
func (p OVHProvider) newOvhChangeCreateDelete(action int, endpoints []*endpoint.Endpoint, zone string, existingRecords []ovhRecord) ([]ovhChange, []ovhRecord) {
|
||||||
// Copy the records because we need to mutate the list.
|
ovhChanges := []ovhChange{}
|
||||||
newRecords := make([]ovhRecord, len(records))
|
toDeleteIds := []int{}
|
||||||
copy(newRecords, records)
|
|
||||||
|
|
||||||
zoneNameIDMapper := provider.ZoneIDName{}
|
|
||||||
ovhChanges := make([]ovhChange, 0, countTargets(endpoints))
|
|
||||||
for _, zone := range zones {
|
|
||||||
zoneNameIDMapper.Add(zone, zone)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, e := range endpoints {
|
for _, e := range endpoints {
|
||||||
zone, _ := zoneNameIDMapper.FindZone(e.DNSName)
|
|
||||||
if zone == "" {
|
|
||||||
log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", e.DNSName)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, target := range e.Targets {
|
for _, target := range e.Targets {
|
||||||
if e.RecordType == endpoint.RecordTypeCNAME {
|
|
||||||
target = target + "."
|
|
||||||
}
|
|
||||||
change := ovhChange{
|
change := ovhChange{
|
||||||
Action: action,
|
Action: action,
|
||||||
ovhRecord: ovhRecord{
|
ovhRecord: ovhRecord{
|
||||||
Zone: zone,
|
Zone: zone,
|
||||||
ovhRecordFields: ovhRecordFields{
|
ovhRecordFields: ovhRecordFields{
|
||||||
FieldType: e.RecordType,
|
FieldType: e.RecordType,
|
||||||
SubDomain: strings.TrimSuffix(e.DNSName, "."+zone),
|
ovhRecordFieldUpdate: ovhRecordFieldUpdate{
|
||||||
TTL: ovhDefaultTTL,
|
SubDomain: convertDNSNameIntoSubDomain(e.DNSName, zone),
|
||||||
Target: target,
|
TTL: ovhDefaultTTL,
|
||||||
|
Target: target,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
p.formatCNAMETarget(&change)
|
||||||
if e.RecordTTL.IsConfigured() {
|
if e.RecordTTL.IsConfigured() {
|
||||||
change.TTL = int64(e.RecordTTL)
|
change.TTL = int64(e.RecordTTL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The Zone might have multiple records with the same target. In order to avoid applying the action to the
|
// The Zone might have multiple records with the same target. In order to avoid applying the action to the
|
||||||
// same OVH record, we remove a record from the list when a match is found.
|
// same OVH record, we remove a record from the list when a match is found.
|
||||||
for i := 0; i < len(newRecords); i++ {
|
if action == ovhDelete {
|
||||||
rec := newRecords[i]
|
for i, rec := range existingRecords {
|
||||||
if rec.Zone == change.Zone && rec.SubDomain == change.SubDomain && rec.FieldType == change.FieldType && rec.Target == change.Target {
|
if rec.Zone == change.Zone && rec.SubDomain == change.SubDomain && rec.FieldType == change.FieldType && rec.Target == change.Target && !slices.Contains(toDeleteIds, i) {
|
||||||
change.ID = rec.ID
|
change.ID = rec.ID
|
||||||
// Deleting this record from the list to avoid retargetting it later if a change with a similar target exists.
|
toDeleteIds = append(toDeleteIds, i)
|
||||||
newRecords = append(newRecords[:i], newRecords[i+1:]...)
|
break
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ovhChanges = append(ovhChanges, change)
|
ovhChanges = append(ovhChanges, change)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ovhChanges
|
if len(toDeleteIds) > 0 {
|
||||||
}
|
// Copy the records because we need to mutate the list.
|
||||||
|
existingRecords = slices.Clone(existingRecords)
|
||||||
func countTargets(allEndpoints ...[]*endpoint.Endpoint) int {
|
alreadyRemoved := 0
|
||||||
count := 0
|
for _, id := range toDeleteIds {
|
||||||
for _, endpoints := range allEndpoints {
|
existingRecords = slices.Delete(existingRecords, id-alreadyRemoved, id-alreadyRemoved+1)
|
||||||
for _, endpoint := range endpoints {
|
alreadyRemoved++
|
||||||
count += len(endpoint.Targets)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count
|
|
||||||
|
return ovhChanges, existingRecords
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertDNSNameIntoSubDomain(DNSName string, zoneName string) string {
|
||||||
|
if DNSName == zoneName {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSuffix(DNSName, "."+zoneName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p OVHProvider) newOvhChangeUpdate(endpointsOld []*endpoint.Endpoint, endpointsNew []*endpoint.Endpoint, zone string, existingRecords []ovhRecord) []ovhChange {
|
||||||
|
zoneNameIDMapper := provider.ZoneIDName{}
|
||||||
|
zoneNameIDMapper.Add(zone, zone)
|
||||||
|
|
||||||
|
oldEndpointByTypeAndName := map[string]*endpoint.Endpoint{}
|
||||||
|
newEndpointByTypeAndName := map[string]*endpoint.Endpoint{}
|
||||||
|
oldRecordsInZone := map[string][]ovhRecord{}
|
||||||
|
|
||||||
|
for _, e := range endpointsOld {
|
||||||
|
sub := convertDNSNameIntoSubDomain(e.DNSName, zone)
|
||||||
|
oldEndpointByTypeAndName[e.RecordType+"//"+sub] = e
|
||||||
|
}
|
||||||
|
for _, e := range endpointsNew {
|
||||||
|
sub := convertDNSNameIntoSubDomain(e.DNSName, zone)
|
||||||
|
newEndpointByTypeAndName[e.RecordType+"//"+sub] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
for id := range oldEndpointByTypeAndName {
|
||||||
|
for _, record := range existingRecords {
|
||||||
|
if id == record.FieldType+"//"+record.SubDomain {
|
||||||
|
oldRecordsInZone[id] = append(oldRecordsInZone[id], record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := []ovhChange{}
|
||||||
|
|
||||||
|
for id := range oldEndpointByTypeAndName {
|
||||||
|
oldRecords := slices.Clone(oldRecordsInZone[id])
|
||||||
|
endpointsNew := newEndpointByTypeAndName[id]
|
||||||
|
|
||||||
|
toInsertTarget := []string{}
|
||||||
|
|
||||||
|
for _, target := range endpointsNew.Targets {
|
||||||
|
var toDelete int = -1
|
||||||
|
|
||||||
|
for i, record := range oldRecords {
|
||||||
|
if target == record.Target {
|
||||||
|
toDelete = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if toDelete >= 0 {
|
||||||
|
oldRecords = slices.Delete(oldRecords, toDelete, toDelete+1)
|
||||||
|
} else {
|
||||||
|
toInsertTarget = append(toInsertTarget, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toInsertTargetToDelete := []int{}
|
||||||
|
for i, target := range toInsertTarget {
|
||||||
|
if len(oldRecords) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
record := oldRecords[0]
|
||||||
|
oldRecords = slices.Delete(oldRecords, 0, 1)
|
||||||
|
record.Target = target
|
||||||
|
|
||||||
|
if endpointsNew.RecordTTL.IsConfigured() {
|
||||||
|
record.TTL = int64(endpointsNew.RecordTTL)
|
||||||
|
} else {
|
||||||
|
record.TTL = ovhDefaultTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
change := ovhChange{
|
||||||
|
Action: ovhUpdate,
|
||||||
|
ovhRecord: record,
|
||||||
|
}
|
||||||
|
p.formatCNAMETarget(&change)
|
||||||
|
changes = append(changes, change)
|
||||||
|
toInsertTargetToDelete = append(toInsertTargetToDelete, i)
|
||||||
|
}
|
||||||
|
for _, i := range toInsertTargetToDelete {
|
||||||
|
toInsertTarget = slices.Delete(toInsertTarget, i, i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toInsertTarget) > 0 {
|
||||||
|
for _, target := range toInsertTarget {
|
||||||
|
recordTTL := int64(ovhDefaultTTL)
|
||||||
|
if endpointsNew.RecordTTL.IsConfigured() {
|
||||||
|
recordTTL = int64(endpointsNew.RecordTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
change := ovhChange{
|
||||||
|
Action: ovhCreate,
|
||||||
|
ovhRecord: ovhRecord{
|
||||||
|
Zone: zone,
|
||||||
|
ovhRecordFields: ovhRecordFields{
|
||||||
|
FieldType: endpointsNew.RecordType,
|
||||||
|
ovhRecordFieldUpdate: ovhRecordFieldUpdate{
|
||||||
|
SubDomain: convertDNSNameIntoSubDomain(endpointsNew.DNSName, zone),
|
||||||
|
TTL: recordTTL,
|
||||||
|
Target: target,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.formatCNAMETarget(&change)
|
||||||
|
changes = append(changes, change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oldRecords) > 0 {
|
||||||
|
for i := range oldRecords {
|
||||||
|
changes = append(changes, ovhChange{
|
||||||
|
Action: ovhDelete,
|
||||||
|
ovhRecord: oldRecords[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ovhChange) String() string {
|
func (c *ovhChange) String() string {
|
||||||
if c.ID != 0 {
|
var action string
|
||||||
return fmt.Sprintf("%s zone (ID : %d) : %s %d IN %s %s", c.Zone, c.ID, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
switch c.Action {
|
||||||
|
case ovhCreate:
|
||||||
|
action = "create"
|
||||||
|
case ovhUpdate:
|
||||||
|
action = "update"
|
||||||
|
case ovhDelete:
|
||||||
|
action = "delete"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s zone : %s %d IN %s %s", c.Zone, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
|
||||||
|
if c.ID != 0 {
|
||||||
|
return fmt.Sprintf("%s zone (ID : %d) action(%s) : %s %d IN %s %s", c.Zone, c.ID, action, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s zone action(%s) : %s %d IN %s %s", c.Zone, action, c.SubDomain, c.TTL, c.FieldType, c.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p OVHProvider) formatCNAMETarget(change *ovhChange) {
|
||||||
|
if change.FieldType != endpoint.RecordTypeCNAME {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.EnableCNAMERelativeTarget {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(change.Target, ".") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
change.Target += "."
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/maxatome/go-testdeep/td"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/ovh/go-ovh/ovh"
|
"github.com/ovh/go-ovh/ovh"
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
@ -38,21 +39,28 @@ type mockOvhClient struct {
|
|||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockOvhClient) Post(endpoint string, input interface{}, output interface{}) error {
|
func (c *mockOvhClient) PostWithContext(ctx context.Context, endpoint string, input interface{}, output interface{}) error {
|
||||||
stub := c.Called(endpoint, input)
|
stub := c.Called(endpoint, input)
|
||||||
data, _ := json.Marshal(stub.Get(0))
|
data, _ := json.Marshal(stub.Get(0))
|
||||||
json.Unmarshal(data, output)
|
json.Unmarshal(data, output)
|
||||||
return stub.Error(1)
|
return stub.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockOvhClient) Get(endpoint string, output interface{}) error {
|
func (c *mockOvhClient) PutWithContext(ctx context.Context, endpoint string, input interface{}, output interface{}) error {
|
||||||
|
stub := c.Called(endpoint, input)
|
||||||
|
data, _ := json.Marshal(stub.Get(0))
|
||||||
|
json.Unmarshal(data, output)
|
||||||
|
return stub.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockOvhClient) GetWithContext(ctx context.Context, endpoint string, output interface{}) error {
|
||||||
stub := c.Called(endpoint)
|
stub := c.Called(endpoint)
|
||||||
data, _ := json.Marshal(stub.Get(0))
|
data, _ := json.Marshal(stub.Get(0))
|
||||||
json.Unmarshal(data, output)
|
json.Unmarshal(data, output)
|
||||||
return stub.Error(1)
|
return stub.Error(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *mockOvhClient) Delete(endpoint string, output interface{}) error {
|
func (c *mockOvhClient) DeleteWithContext(ctx context.Context, endpoint string, output interface{}) error {
|
||||||
stub := c.Called(endpoint)
|
stub := c.Called(endpoint)
|
||||||
data, _ := json.Marshal(stub.Get(0))
|
data, _ := json.Marshal(stub.Get(0))
|
||||||
json.Unmarshal(data, output)
|
json.Unmarshal(data, output)
|
||||||
@ -84,16 +92,16 @@ func TestOvhZones(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Basic zones
|
// Basic zones
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.com", "example.net"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.com", "example.net"}, nil).Once()
|
||||||
domains, err := provider.zones()
|
domains, err := provider.zones(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.Contains(domains, "example.com")
|
assert.Contains(domains, "example.com")
|
||||||
assert.NotContains(domains, "example.net")
|
assert.NotContains(domains, "example.net")
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Error on getting zones
|
// Error on getting zones
|
||||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||||
domains, err = provider.zones()
|
domains, err = provider.zones(t.Context())
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Nil(domains)
|
assert.Nil(domains)
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
@ -106,21 +114,21 @@ func TestOvhZoneRecords(t *testing.T) {
|
|||||||
|
|
||||||
// Basic zones records
|
// Basic zones records
|
||||||
t.Log("Basic zones records")
|
t.Log("Basic zones records")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090901}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090901}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
zones, records, err := provider.zonesRecords(context.TODO())
|
zones, records, err := provider.zonesRecords(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
assert.ElementsMatch(zones, []string{"example.org"})
|
||||||
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
|
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}})
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Error on getting zones list
|
// Error on getting zones list
|
||||||
t.Log("Error on getting zones list")
|
t.Log("Error on getting zones list")
|
||||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Nil(zones)
|
assert.Nil(zones)
|
||||||
assert.Nil(records)
|
assert.Nil(records)
|
||||||
@ -129,9 +137,9 @@ func TestOvhZoneRecords(t *testing.T) {
|
|||||||
// Error on getting zone SOA
|
// Error on getting zone SOA
|
||||||
t.Log("Error on getting zone SOA")
|
t.Log("Error on getting zone SOA")
|
||||||
provider.cacheInstance = cache.New(cache.NoExpiration, cache.NoExpiration)
|
provider.cacheInstance = cache.New(cache.NoExpiration, cache.NoExpiration)
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(nil, ovh.ErrAPIDown).Once()
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Nil(zones)
|
assert.Nil(zones)
|
||||||
assert.Nil(records)
|
assert.Nil(records)
|
||||||
@ -139,10 +147,10 @@ func TestOvhZoneRecords(t *testing.T) {
|
|||||||
|
|
||||||
// Error on getting zone records
|
// Error on getting zone records
|
||||||
t.Log("Error on getting zone records")
|
t.Log("Error on getting zone records")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return(nil, ovh.ErrAPIDown).Once()
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Nil(zones)
|
assert.Nil(zones)
|
||||||
assert.Nil(records)
|
assert.Nil(records)
|
||||||
@ -150,11 +158,11 @@ func TestOvhZoneRecords(t *testing.T) {
|
|||||||
|
|
||||||
// Error on getting zone record detail
|
// Error on getting zone record detail
|
||||||
t.Log("Error on getting zone record detail")
|
t.Log("Error on getting zone record detail")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return([]uint64{42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/42").Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/42").Return(nil, ovh.ErrAPIDown).Once()
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Nil(zones)
|
assert.Nil(zones)
|
||||||
assert.Nil(records)
|
assert.Nil(records)
|
||||||
@ -169,16 +177,16 @@ func TestOvhZoneRecordsCache(t *testing.T) {
|
|||||||
|
|
||||||
// First call, cache miss
|
// First call, cache miss
|
||||||
t.Log("First call, cache miss")
|
t.Log("First call, cache miss")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090901}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090901}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
|
|
||||||
zones, records, err := provider.zonesRecords(context.TODO())
|
zones, records, err := provider.zonesRecords(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
assert.ElementsMatch(zones, []string{"example.org"})
|
||||||
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
|
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}})
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
dnsClient.AssertExpectations(t)
|
dnsClient.AssertExpectations(t)
|
||||||
|
|
||||||
@ -189,13 +197,13 @@ func TestOvhZoneRecordsCache(t *testing.T) {
|
|||||||
|
|
||||||
// second call, cache hit
|
// second call, cache hit
|
||||||
t.Log("second call, cache hit")
|
t.Log("second call, cache hit")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
||||||
Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090901}}}, nil)
|
Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090901}}}, nil)
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
assert.ElementsMatch(zones, []string{"example.org"})
|
||||||
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
|
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}})
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
dnsClient.AssertExpectations(t)
|
dnsClient.AssertExpectations(t)
|
||||||
|
|
||||||
@ -206,17 +214,17 @@ func TestOvhZoneRecordsCache(t *testing.T) {
|
|||||||
|
|
||||||
// third call, cache out of date
|
// third call, cache out of date
|
||||||
t.Log("third call, cache out of date")
|
t.Log("third call, cache out of date")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
||||||
Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090902}}}, nil)
|
Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090902}}}, nil)
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090902}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return([]uint64{24}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
|
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
assert.ElementsMatch(zones, []string{"example.org"})
|
||||||
assert.ElementsMatch(records, []ovhRecord{{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
|
assert.ElementsMatch(records, []ovhRecord{{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}})
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
dnsClient.AssertExpectations(t)
|
dnsClient.AssertExpectations(t)
|
||||||
|
|
||||||
@ -227,14 +235,14 @@ func TestOvhZoneRecordsCache(t *testing.T) {
|
|||||||
|
|
||||||
// fourth call, cache hit
|
// fourth call, cache hit
|
||||||
t.Log("fourth call, cache hit")
|
t.Log("fourth call, cache hit")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
||||||
Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090902}}}, nil)
|
Return(&dns.Msg{Answer: []dns.RR{&dns.SOA{Serial: 2022090902}}}, nil)
|
||||||
|
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
assert.ElementsMatch(zones, []string{"example.org"})
|
||||||
assert.ElementsMatch(records, []ovhRecord{{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
|
assert.ElementsMatch(records, []ovhRecord{{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}})
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
dnsClient.AssertExpectations(t)
|
dnsClient.AssertExpectations(t)
|
||||||
|
|
||||||
@ -245,18 +253,18 @@ func TestOvhZoneRecordsCache(t *testing.T) {
|
|||||||
|
|
||||||
// fifth call, dns issue
|
// fifth call, dns issue
|
||||||
t.Log("fourth call, cache hit")
|
t.Log("fourth call, cache hit")
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org"}, nil).Once()
|
||||||
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
dnsClient.On("ExchangeContext", mock.AnythingOfType("*context.cancelCtx"), mock.AnythingOfType("*dns.Msg"), "ns.example.org:53").
|
||||||
Return(&dns.Msg{Answer: []dns.RR{}}, errors.New("dns issue"))
|
Return(&dns.Msg{Answer: []dns.RR{}}, errors.New("dns issue"))
|
||||||
client.On("Get", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090903}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/soa").Return(ovhSoa{Server: "ns.example.org.", Serial: 2022090903}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
|
|
||||||
zones, records, err = provider.zonesRecords(context.TODO())
|
zones, records, err = provider.zonesRecords(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.ElementsMatch(zones, []string{"example.org"})
|
assert.ElementsMatch(zones, []string{"example.org"})
|
||||||
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "NS", TTL: 10, Target: "203.0.113.42"}}})
|
assert.ElementsMatch(records, []ovhRecord{{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, {ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "NS", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}})
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
dnsClient.AssertExpectations(t)
|
dnsClient.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
@ -267,14 +275,14 @@ func TestOvhRecords(t *testing.T) {
|
|||||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
|
||||||
// Basic zones records
|
// Basic zones records
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.org", "example.net"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.org", "example.net"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record").Return([]uint64{24, 42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/24").Return(ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{SubDomain: "www", FieldType: "CNAME", TTL: 10, Target: "example.org."}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.org/record/42").Return(ovhRecord{ID: 42, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "CNAME", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "www", TTL: 10, Target: "example.org."}}}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{24, 42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{24, 42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.net/record/24").Return(ovhRecord{ID: 24, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record/24").Return(ovhRecord{ID: 24, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.43"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.43"}}}, nil).Once()
|
||||||
endpoints, err := provider.Records(context.TODO())
|
endpoints, err := provider.Records(t.Context())
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
// Little fix for multi targets endpoint
|
// Little fix for multi targets endpoint
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
@ -288,25 +296,70 @@ func TestOvhRecords(t *testing.T) {
|
|||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Error getting zone
|
// Error getting zone
|
||||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
||||||
endpoints, err = provider.Records(context.TODO())
|
endpoints, err = provider.Records(t.Context())
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
assert.Nil(endpoints)
|
assert.Nil(endpoints)
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOvhComputeChanges(t *testing.T) {
|
||||||
|
existingRecords := []ovhRecord{
|
||||||
|
{
|
||||||
|
ID: 1,
|
||||||
|
Zone: "example.net",
|
||||||
|
ovhRecordFields: ovhRecordFields{
|
||||||
|
FieldType: "A",
|
||||||
|
ovhRecordFieldUpdate: ovhRecordFieldUpdate{
|
||||||
|
SubDomain: "",
|
||||||
|
Target: "203.0.113.42",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
changes := plan.Changes{
|
||||||
|
UpdateOld: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", Targets: []string{"203.0.113.42"}},
|
||||||
|
},
|
||||||
|
UpdateNew: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", Targets: []string{"203.0.113.43", "203.0.113.42"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := &OVHProvider{client: nil, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
ovhChanges := provider.computeSingleZoneChanges(t.Context(), "example.net", existingRecords, &changes)
|
||||||
|
td.Cmp(t, ovhChanges, []ovhChange{
|
||||||
|
{
|
||||||
|
Action: ovhCreate,
|
||||||
|
ovhRecord: ovhRecord{
|
||||||
|
Zone: "example.net",
|
||||||
|
ovhRecordFields: ovhRecordFields{
|
||||||
|
FieldType: "A",
|
||||||
|
ovhRecordFieldUpdate: ovhRecordFieldUpdate{
|
||||||
|
SubDomain: "",
|
||||||
|
Target: "203.0.113.43",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestOvhRefresh(t *testing.T) {
|
func TestOvhRefresh(t *testing.T) {
|
||||||
client := new(mockOvhClient)
|
client := new(mockOvhClient)
|
||||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
|
||||||
// Basic zone refresh
|
// Basic zone refresh
|
||||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
client.On("PostWithContext", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||||
provider.refresh("example.net")
|
provider.refresh(t.Context(), "example.net")
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOvhNewChange(t *testing.T) {
|
func TestOvhNewChange(t *testing.T) {
|
||||||
assert := assert.New(t)
|
provider := &OVHProvider{client: nil, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
|
||||||
endpoints := []*endpoint.Endpoint{
|
endpoints := []*endpoint.Endpoint{
|
||||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||||
@ -315,11 +368,11 @@ func TestOvhNewChange(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create change
|
// Create change
|
||||||
changes := newOvhChange(ovhCreate, endpoints, []string{"example.net"}, []ovhRecord{})
|
changes, _ := provider.newOvhChangeCreateDelete(ovhCreate, endpoints, "example.net", []ovhRecord{})
|
||||||
assert.ElementsMatch(changes, []ovhChange{
|
td.Cmp(t, changes, []ovhChange{
|
||||||
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}}},
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}},
|
||||||
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.43"}}},
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: ovhDefaultTTL, Target: "203.0.113.43"}}}},
|
||||||
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh2", FieldType: "CNAME", TTL: ovhDefaultTTL, Target: "ovh.example.net."}}},
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "CNAME", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh2", TTL: ovhDefaultTTL, Target: "ovh.example.net."}}}},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Delete change
|
// Delete change
|
||||||
@ -327,70 +380,196 @@ func TestOvhNewChange(t *testing.T) {
|
|||||||
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.42", "203.0.113.42", "203.0.113.42"}},
|
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.42", "203.0.113.42", "203.0.113.42"}},
|
||||||
}
|
}
|
||||||
records := []ovhRecord{
|
records := []ovhRecord{
|
||||||
{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", SubDomain: "ovh", Target: "203.0.113.42"}},
|
{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", Target: "203.0.113.42"}}},
|
||||||
{ID: 43, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", SubDomain: "ovh", Target: "203.0.113.42"}},
|
{ID: 43, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", Target: "203.0.113.42"}}},
|
||||||
{ID: 44, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", SubDomain: "ovh", Target: "203.0.113.42"}},
|
{ID: 44, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", Target: "203.0.113.42"}}},
|
||||||
}
|
}
|
||||||
changes = newOvhChange(ovhDelete, endpoints, []string{"example.net"}, records)
|
changes, _ = provider.newOvhChangeCreateDelete(ovhDelete, endpoints, "example.net", records)
|
||||||
assert.ElementsMatch(changes, []ovhChange{
|
td.Cmp(t, changes, []ovhChange{
|
||||||
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}},
|
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}}},
|
||||||
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 43, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}},
|
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 43, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}}},
|
||||||
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 44, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}},
|
{Action: ovhDelete, ovhRecord: ovhRecord{ID: 44, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: ovhDefaultTTL, Target: "203.0.113.42"}}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create change with CNAME relative
|
||||||
|
endpoints = []*endpoint.Endpoint{
|
||||||
|
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
|
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||||
|
{DNSName: "ovh2.example.net", RecordType: "CNAME", Targets: []string{"ovh"}},
|
||||||
|
{DNSName: "test.example.org"},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = &OVHProvider{client: nil, EnableCNAMERelativeTarget: true, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
changes, _ = provider.newOvhChangeCreateDelete(ovhCreate, endpoints, "example.net", []ovhRecord{})
|
||||||
|
td.Cmp(t, changes, []ovhChange{
|
||||||
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}},
|
||||||
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: ovhDefaultTTL, Target: "203.0.113.43"}}}},
|
||||||
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "CNAME", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh2", TTL: ovhDefaultTTL, Target: "ovh"}}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test with CNAME when target has already final dot
|
||||||
|
endpoints = []*endpoint.Endpoint{
|
||||||
|
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
|
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||||
|
{DNSName: "ovh2.example.net", RecordType: "CNAME", Targets: []string{"ovh.example.com."}},
|
||||||
|
{DNSName: "test.example.org"},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider = &OVHProvider{client: nil, EnableCNAMERelativeTarget: false, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
changes, _ = provider.newOvhChangeCreateDelete(ovhCreate, endpoints, "example.net", []ovhRecord{})
|
||||||
|
td.Cmp(t, changes, []ovhChange{
|
||||||
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}},
|
||||||
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: ovhDefaultTTL, Target: "203.0.113.43"}}}},
|
||||||
|
{Action: ovhCreate, ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "CNAME", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh2", TTL: ovhDefaultTTL, Target: "ovh.example.com."}}}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOvhApplyChanges(t *testing.T) {
|
func TestOvhApplyChanges(t *testing.T) {
|
||||||
assert := assert.New(t)
|
|
||||||
client := new(mockOvhClient)
|
client := new(mockOvhClient)
|
||||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
changes := plan.Changes{
|
changes := plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
},
|
},
|
||||||
Delete: []*endpoint.Endpoint{
|
Delete: []*endpoint.Endpoint{
|
||||||
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
|
||||||
client.On("Get", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh", FieldType: "A", TTL: 10, Target: "203.0.113.43"}}, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.43"}}}, nil).Once()
|
||||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, nil).Once()
|
client.On("PostWithContext", "/domain/zone/example.net/record", ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}).Return(nil, nil).Once()
|
||||||
client.On("Delete", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
client.On("DeleteWithContext", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
||||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
client.On("PostWithContext", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||||
|
|
||||||
|
_, err := provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
// Basic changes
|
// Basic changes
|
||||||
assert.NoError(provider.ApplyChanges(context.TODO(), &changes))
|
td.CmpNoError(t, provider.ApplyChanges(t.Context(), &changes))
|
||||||
client.AssertExpectations(t)
|
|
||||||
|
|
||||||
// Getting zones failed
|
|
||||||
client.On("Get", "/domain/zone").Return(nil, ovh.ErrAPIDown).Once()
|
|
||||||
assert.Error(provider.ApplyChanges(context.TODO(), &changes))
|
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Apply change failed
|
// Apply change failed
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
client = new(mockOvhClient)
|
||||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
|
provider.client = client
|
||||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
|
||||||
assert.Error(provider.ApplyChanges(context.TODO(), &plan.Changes{
|
client.On("PostWithContext", "/domain/zone/example.net/record", ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}).Return(nil, ovh.ErrAPIDown).Once()
|
||||||
|
|
||||||
|
_, err = provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
|
td.CmpError(t, provider.ApplyChanges(t.Context(), &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Refresh failed
|
// Refresh failed
|
||||||
client.On("Get", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
client = new(mockOvhClient)
|
||||||
client.On("Get", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
|
provider.client = client
|
||||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "", FieldType: "A", TTL: 10, Target: "203.0.113.42"}).Return(nil, nil).Once()
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
client.On("Post", "/domain/zone/example.net/refresh", nil).Return(nil, ovh.ErrAPIDown).Once()
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{}, nil).Once()
|
||||||
assert.Error(provider.ApplyChanges(context.TODO(), &plan.Changes{
|
client.On("PostWithContext", "/domain/zone/example.net/record", ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}).Return(nil, nil).Once()
|
||||||
|
client.On("PostWithContext", "/domain/zone/example.net/refresh", nil).Return(nil, ovh.ErrAPIDown).Once()
|
||||||
|
|
||||||
|
_, err = provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
|
td.CmpError(t, provider.ApplyChanges(t.Context(), &plan.Changes{
|
||||||
Create: []*endpoint.Endpoint{
|
Create: []*endpoint.Endpoint{
|
||||||
{DNSName: ".example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test Dry-Run
|
||||||
|
client = new(mockOvhClient)
|
||||||
|
provider = &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration), DryRun: true}
|
||||||
|
changes = plan.Changes{
|
||||||
|
Create: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
|
},
|
||||||
|
Delete: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "ovh.example.net", RecordType: "A", Targets: []string{"203.0.113.43"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.43"}}}, nil).Once()
|
||||||
|
|
||||||
|
_, err = provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
|
td.CmpNoError(t, provider.ApplyChanges(t.Context(), &changes))
|
||||||
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test Update
|
||||||
|
client = new(mockOvhClient)
|
||||||
|
provider = &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration), DryRun: false}
|
||||||
|
changes = plan.Changes{
|
||||||
|
UpdateOld: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
|
},
|
||||||
|
UpdateNew: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.43"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
|
client.On("PutWithContext", "/domain/zone/example.net/record/42", ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.43"}).Return(nil, nil).Once()
|
||||||
|
client.On("PostWithContext", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||||
|
|
||||||
|
_, err = provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
|
td.CmpNoError(t, provider.ApplyChanges(t.Context(), &changes))
|
||||||
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test Update DryRun
|
||||||
|
client = new(mockOvhClient)
|
||||||
|
provider = &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration), DryRun: true}
|
||||||
|
changes = plan.Changes{
|
||||||
|
UpdateOld: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42"}},
|
||||||
|
},
|
||||||
|
UpdateNew: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.43"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{42}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
|
|
||||||
|
_, err = provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
|
td.CmpNoError(t, provider.ApplyChanges(t.Context(), &changes))
|
||||||
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Test Update 2 records => 1 record
|
||||||
|
client = new(mockOvhClient)
|
||||||
|
provider = &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration), DryRun: false}
|
||||||
|
changes = plan.Changes{
|
||||||
|
UpdateOld: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.42", "203.0.113.43"}},
|
||||||
|
},
|
||||||
|
UpdateNew: []*endpoint.Endpoint{
|
||||||
|
{DNSName: "example.net", RecordType: "A", RecordTTL: 10, Targets: []string{"203.0.113.43"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
client.On("GetWithContext", "/domain/zone").Return([]string{"example.net"}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record").Return([]uint64{42, 43}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record/42").Return(ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.42"}}}, nil).Once()
|
||||||
|
client.On("GetWithContext", "/domain/zone/example.net/record/43").Return(ovhRecord{ID: 43, Zone: "example.net", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "", TTL: 10, Target: "203.0.113.43"}}}, nil).Once()
|
||||||
|
client.On("DeleteWithContext", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
||||||
|
client.On("PostWithContext", "/domain/zone/example.net/refresh", nil).Return(nil, nil).Once()
|
||||||
|
|
||||||
|
_, err = provider.Records(t.Context())
|
||||||
|
td.CmpNoError(t, err)
|
||||||
|
td.CmpNoError(t, provider.ApplyChanges(t.Context(), &changes))
|
||||||
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOvhChange(t *testing.T) {
|
func TestOvhChange(t *testing.T) {
|
||||||
@ -399,43 +578,44 @@ func TestOvhChange(t *testing.T) {
|
|||||||
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
provider := &OVHProvider{client: client, apiRateLimiter: ratelimit.New(10), cacheInstance: cache.New(cache.NoExpiration, cache.NoExpiration)}
|
||||||
|
|
||||||
// Record creation
|
// Record creation
|
||||||
client.On("Post", "/domain/zone/example.net/record", ovhRecordFields{SubDomain: "ovh"}).Return(nil, nil).Once()
|
client.On("PostWithContext", "/domain/zone/example.net/record", ovhRecordFields{ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh"}}).Return(nil, nil).Once()
|
||||||
assert.NoError(provider.change(ovhChange{
|
assert.NoError(provider.change(t.Context(), ovhChange{
|
||||||
Action: ovhCreate,
|
Action: ovhCreate,
|
||||||
ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
|
ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh"}}},
|
||||||
}))
|
}))
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Record deletion
|
// Record deletion
|
||||||
client.On("Delete", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
client.On("DeleteWithContext", "/domain/zone/example.net/record/42").Return(nil, nil).Once()
|
||||||
assert.NoError(provider.change(ovhChange{
|
assert.NoError(provider.change(t.Context(), ovhChange{
|
||||||
Action: ovhDelete,
|
Action: ovhDelete,
|
||||||
ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
|
ovhRecord: ovhRecord{ID: 42, Zone: "example.net", ovhRecordFields: ovhRecordFields{ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh"}}},
|
||||||
}))
|
}))
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
|
|
||||||
// Record deletion error
|
// Record deletion error
|
||||||
assert.Error(provider.change(ovhChange{
|
assert.Error(provider.change(t.Context(), ovhChange{
|
||||||
Action: ovhDelete,
|
Action: ovhDelete,
|
||||||
ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{SubDomain: "ovh"}},
|
ovhRecord: ovhRecord{Zone: "example.net", ovhRecordFields: ovhRecordFields{ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh"}}},
|
||||||
}))
|
}))
|
||||||
client.AssertExpectations(t)
|
client.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOvhCountTargets(t *testing.T) {
|
func TestOvhRecordString(t *testing.T) {
|
||||||
cases := []struct {
|
record := ovhRecord{ID: 24, Zone: "example.org", ovhRecordFields: ovhRecordFields{FieldType: "A", ovhRecordFieldUpdate: ovhRecordFieldUpdate{SubDomain: "ovh", TTL: 10, Target: "203.0.113.42"}}}
|
||||||
endpoints [][]*endpoint.Endpoint
|
|
||||||
count int
|
td.Cmp(t, record.String(), "record#24: A | ovh => 203.0.113.42 (10)")
|
||||||
}{
|
}
|
||||||
{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 1},
|
|
||||||
{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}, {DNSName: "ovh.example.net", Targets: endpoint.Targets{"target"}}}}, 2},
|
func TestNewOvhProvider(t *testing.T) {
|
||||||
{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target", "target"}}}}, 3},
|
var domainFilter endpoint.DomainFilter
|
||||||
{[][]*endpoint.Endpoint{{{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}, {{DNSName: "ovh.example.net", Targets: endpoint.Targets{"target", "target"}}}}, 4},
|
_, err := NewOVHProvider(t.Context(), domainFilter, "ovh-eu", 20, false, true)
|
||||||
}
|
td.CmpError(t, err)
|
||||||
for _, test := range cases {
|
|
||||||
count := countTargets(test.endpoints...)
|
t.Setenv("OVH_APPLICATION_KEY", "aaaaaa")
|
||||||
if count != test.count {
|
t.Setenv("OVH_APPLICATION_SECRET", "bbbbbb")
|
||||||
t.Errorf("Wrong targets counts (Should be %d, get %d)", test.count, count)
|
t.Setenv("OVH_CONSUMER_KEY", "cccccc")
|
||||||
}
|
|
||||||
}
|
_, err = NewOVHProvider(t.Context(), domainFilter, "ovh-eu", 20, false, true)
|
||||||
|
td.CmpNoError(t, err)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user