mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2025-12-09 05:36:27 +08:00
Merge 739c01bad9 into 986bc4c5f4
This commit is contained in:
commit
5287f96581
10 changed files with 640 additions and 0 deletions
1
OWNERS
1
OWNERS
|
|
@ -30,6 +30,7 @@ providers/hetznerv2 @das7pad
|
||||||
providers/hexonet @KaiSchwarz-cnic
|
providers/hexonet @KaiSchwarz-cnic
|
||||||
providers/hostingde @juliusrickert
|
providers/hostingde @juliusrickert
|
||||||
providers/huaweicloud @huihuimoe
|
providers/huaweicloud @huihuimoe
|
||||||
|
providers/infomaniak @jbelien
|
||||||
providers/internetbs @pragmaton
|
providers/internetbs @pragmaton
|
||||||
providers/inwx @patschi
|
providers/inwx @patschi
|
||||||
providers/joker @atrull
|
providers/joker @atrull
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ Currently supported DNS providers:
|
||||||
- hosting.de
|
- hosting.de
|
||||||
- Huawei Cloud DNS
|
- Huawei Cloud DNS
|
||||||
- Hurricane Electric DNS
|
- Hurricane Electric DNS
|
||||||
|
- Infomaniak
|
||||||
- INWX
|
- INWX
|
||||||
- Joker
|
- Joker
|
||||||
- Linode
|
- Linode
|
||||||
|
|
|
||||||
|
|
@ -145,6 +145,7 @@
|
||||||
* [hosting.de](provider/hostingde.md)
|
* [hosting.de](provider/hostingde.md)
|
||||||
* [Huawei Cloud DNS](provider/huaweicloud.md)
|
* [Huawei Cloud DNS](provider/huaweicloud.md)
|
||||||
* [Hurricane Electric DNS](provider/hedns.md)
|
* [Hurricane Electric DNS](provider/hedns.md)
|
||||||
|
* [Infomaniak](provider/infomaniak.md)
|
||||||
* [Internet.bs](provider/internetbs.md)
|
* [Internet.bs](provider/internetbs.md)
|
||||||
* [INWX](provider/inwx.md)
|
* [INWX](provider/inwx.md)
|
||||||
* [Joker](provider/joker.md)
|
* [Joker](provider/joker.md)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ Jump to a table:
|
||||||
| [`HEXONET`](hexonet.md) | ❌ | ✅ | ✅ |
|
| [`HEXONET`](hexonet.md) | ❌ | ✅ | ✅ |
|
||||||
| [`HOSTINGDE`](hostingde.md) | ❌ | ✅ | ✅ |
|
| [`HOSTINGDE`](hostingde.md) | ❌ | ✅ | ✅ |
|
||||||
| [`HUAWEICLOUD`](huaweicloud.md) | ❌ | ✅ | ❌ |
|
| [`HUAWEICLOUD`](huaweicloud.md) | ❌ | ✅ | ❌ |
|
||||||
|
| [`INFOMANIAK`](infomaniak.md) | ❌ | ✅ | ❌ |
|
||||||
| [`INTERNETBS`](internetbs.md) | ❌ | ❌ | ✅ |
|
| [`INTERNETBS`](internetbs.md) | ❌ | ❌ | ✅ |
|
||||||
| [`INWX`](inwx.md) | ❌ | ✅ | ✅ |
|
| [`INWX`](inwx.md) | ❌ | ✅ | ✅ |
|
||||||
| [`JOKER`](joker.md) | ❌ | ✅ | ❌ |
|
| [`JOKER`](joker.md) | ❌ | ✅ | ❌ |
|
||||||
|
|
@ -120,6 +121,7 @@ Jump to a table:
|
||||||
| [`HEXONET`](hexonet.md) | ❔ | ✅ | ✅ | ❔ |
|
| [`HEXONET`](hexonet.md) | ❔ | ✅ | ✅ | ❔ |
|
||||||
| [`HOSTINGDE`](hostingde.md) | ❔ | ✅ | ✅ | ✅ |
|
| [`HOSTINGDE`](hostingde.md) | ❔ | ✅ | ✅ | ✅ |
|
||||||
| [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ✅ | ✅ | ✅ |
|
| [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ✅ | ✅ | ✅ |
|
||||||
|
| [`INFOMANIAK`](infomaniak.md) | ❔ | ❔ | ❌ | ✅ |
|
||||||
| [`INTERNETBS`](internetbs.md) | ❔ | ❔ | ❌ | ❔ |
|
| [`INTERNETBS`](internetbs.md) | ❔ | ❔ | ❌ | ❔ |
|
||||||
| [`INWX`](inwx.md) | ✅ | ✅ | ✅ | ✅ |
|
| [`INWX`](inwx.md) | ✅ | ✅ | ✅ | ✅ |
|
||||||
| [`JOKER`](joker.md) | ❌ | ❌ | ✅ | ✅ |
|
| [`JOKER`](joker.md) | ❌ | ❌ | ✅ | ✅ |
|
||||||
|
|
@ -180,6 +182,7 @@ Jump to a table:
|
||||||
| [`HEXONET`](hexonet.md) | ❌ | ❔ | ❔ | ✅ | ❔ |
|
| [`HEXONET`](hexonet.md) | ❌ | ❔ | ❔ | ✅ | ❔ |
|
||||||
| [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ❌ | ✅ | ✅ |
|
| [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ❌ | ✅ | ✅ |
|
||||||
| [`HUAWEICLOUD`](huaweicloud.md) | ❌ | ❔ | ❌ | ❌ | ❌ |
|
| [`HUAWEICLOUD`](huaweicloud.md) | ❌ | ❔ | ❌ | ❌ | ❌ |
|
||||||
|
| [`INFOMANIAK`](infomaniak.md) | ❌ | ✅ | ❌ | ❌ | ❌ |
|
||||||
| [`INWX`](inwx.md) | ✅ | ❔ | ❔ | ✅ | ❔ |
|
| [`INWX`](inwx.md) | ✅ | ❔ | ❔ | ✅ | ❔ |
|
||||||
| [`JOKER`](joker.md) | ❌ | ❔ | ❌ | ❌ | ❌ |
|
| [`JOKER`](joker.md) | ❌ | ❔ | ❌ | ❌ | ❌ |
|
||||||
| [`LINODE`](linode.md) | ❔ | ❔ | ❌ | ❔ | ❔ |
|
| [`LINODE`](linode.md) | ❔ | ❔ | ❌ | ❔ | ❔ |
|
||||||
|
|
@ -237,6 +240,7 @@ Jump to a table:
|
||||||
| [`HEXONET`](hexonet.md) | ❔ | ❔ | ✅ | ❔ |
|
| [`HEXONET`](hexonet.md) | ❔ | ❔ | ✅ | ❔ |
|
||||||
| [`HOSTINGDE`](hostingde.md) | ❔ | ❌ | ✅ | ❔ |
|
| [`HOSTINGDE`](hostingde.md) | ❔ | ❌ | ✅ | ❔ |
|
||||||
| [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ❌ | ✅ | ❌ |
|
| [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ❌ | ✅ | ❌ |
|
||||||
|
| [`INFOMANIAK`](infomaniak.md) | ❌ | ❌ | ✅ | ❌ |
|
||||||
| [`INWX`](inwx.md) | ❔ | ✅ | ✅ | ✅ |
|
| [`INWX`](inwx.md) | ❔ | ✅ | ✅ | ✅ |
|
||||||
| [`JOKER`](joker.md) | ❔ | ✅ | ✅ | ❌ |
|
| [`JOKER`](joker.md) | ❔ | ✅ | ✅ | ❌ |
|
||||||
| [`LOOPIA`](loopia.md) | ❌ | ✅ | ✅ | ❌ |
|
| [`LOOPIA`](loopia.md) | ❌ | ✅ | ✅ | ❌ |
|
||||||
|
|
@ -293,6 +297,7 @@ Jump to a table:
|
||||||
| [`HEXONET`](hexonet.md) | ✅ | ❔ | ❔ | ❔ | ✅ |
|
| [`HEXONET`](hexonet.md) | ✅ | ❔ | ❔ | ❔ | ✅ |
|
||||||
| [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ❔ | ✅ | ✅ |
|
| [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ❔ | ✅ | ✅ |
|
||||||
| [`HUAWEICLOUD`](huaweicloud.md) | ✅ | ❌ | ❔ | ❌ | ❌ |
|
| [`HUAWEICLOUD`](huaweicloud.md) | ✅ | ❌ | ❔ | ❌ | ❌ |
|
||||||
|
| [`INFOMANIAK`](infomaniak.md) | ✅ | ❌ | ❌ | ✅ | ✅ |
|
||||||
| [`INWX`](inwx.md) | ✅ | ✅ | ❔ | ✅ | ✅ |
|
| [`INWX`](inwx.md) | ✅ | ✅ | ❔ | ✅ | ✅ |
|
||||||
| [`JOKER`](joker.md) | ✅ | ❌ | ❔ | ❌ | ❌ |
|
| [`JOKER`](joker.md) | ✅ | ❌ | ❔ | ❌ | ❌ |
|
||||||
| [`LINODE`](linode.md) | ✅ | ❔ | ❔ | ❔ | ❔ |
|
| [`LINODE`](linode.md) | ✅ | ❔ | ❔ | ❔ | ❔ |
|
||||||
|
|
@ -339,6 +344,7 @@ Jump to a table:
|
||||||
| [`HETZNER_V2`](hetzner_v2.md) | ❌ | ❔ | ✅ |
|
| [`HETZNER_V2`](hetzner_v2.md) | ❌ | ❔ | ✅ |
|
||||||
| [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ✅ |
|
| [`HOSTINGDE`](hostingde.md) | ✅ | ❔ | ✅ |
|
||||||
| [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ❔ | ❌ |
|
| [`HUAWEICLOUD`](huaweicloud.md) | ❔ | ❔ | ❌ |
|
||||||
|
| [`INFOMANIAK`](infomaniak.md) | ❌ | ❌ | ✅ |
|
||||||
| [`INWX`](inwx.md) | ✅ | ❔ | ❔ |
|
| [`INWX`](inwx.md) | ✅ | ❔ | ❔ |
|
||||||
| [`JOKER`](joker.md) | ❔ | ❌ | ❌ |
|
| [`JOKER`](joker.md) | ❔ | ❌ | ❌ |
|
||||||
| [`LOOPIA`](loopia.md) | ❌ | ❌ | ❌ |
|
| [`LOOPIA`](loopia.md) | ❌ | ❌ | ❌ |
|
||||||
|
|
@ -421,6 +427,7 @@ Providers in this category and their maintainers are:
|
||||||
|[`HEXONET`](hexonet.md)|@KaiSchwarz-cnic|
|
|[`HEXONET`](hexonet.md)|@KaiSchwarz-cnic|
|
||||||
|[`HOSTINGDE`](hostingde.md)|@membero|
|
|[`HOSTINGDE`](hostingde.md)|@membero|
|
||||||
|[`HUAWEICLOUD`](huaweicloud.md)|@huihuimoe|
|
|[`HUAWEICLOUD`](huaweicloud.md)|@huihuimoe|
|
||||||
|
|[`INFOMANIAK`](infomaniak.md)|@jbelien|
|
||||||
|[`INTERNETBS`](internetbs.md)|@pragmaton|
|
|[`INTERNETBS`](internetbs.md)|@pragmaton|
|
||||||
|[`INWX`](inwx.md)|@patschi|
|
|[`INWX`](inwx.md)|@patschi|
|
||||||
|[`LINODE`](linode.md)|@koesie10|
|
|[`LINODE`](linode.md)|@koesie10|
|
||||||
|
|
|
||||||
38
documentation/provider/infomaniak.md
Normal file
38
documentation/provider/infomaniak.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
This is the provider for [Infomaniak](https://www.infomaniak.com/).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
To use this provider, add an entry to `creds.json` with `TYPE` set to `INFOMANIAK` along with a Infomaniak account personal access token.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
{% code title="creds.json" %}
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"infomaniak": {
|
||||||
|
"TYPE": "INFOMANIAK",
|
||||||
|
"token": "your-infomaniak-account-access-token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
{% endcode %}
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
This provider does not recognize any special metadata fields unique to Infomaniak.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
An example configuration:
|
||||||
|
|
||||||
|
{% code title="dnsconfig.js" %}
|
||||||
|
```javascript
|
||||||
|
var REG_NONE = NewRegistrar("none");
|
||||||
|
var DSP_INFOMANIAK = NewDnsProvider("infomaniak");
|
||||||
|
|
||||||
|
D("example.com", REG_NONE, DnsProvider(DSP_INFOMANIAK),
|
||||||
|
A("test", "1.2.3.4"),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
{% endcode %}
|
||||||
|
|
||||||
|
## Activation
|
||||||
|
DNSControl depends on a Infomaniak account personal access token.
|
||||||
|
|
@ -208,6 +208,11 @@
|
||||||
"TYPE": "HUAWEICLOUD",
|
"TYPE": "HUAWEICLOUD",
|
||||||
"domain": "$HUAWEICLOUD_DOMAIN"
|
"domain": "$HUAWEICLOUD_DOMAIN"
|
||||||
},
|
},
|
||||||
|
"INFOMANIAK": {
|
||||||
|
"TYPE": "INFOMANIAK",
|
||||||
|
"domain": "$INFOMANIAK_DOMAIN",
|
||||||
|
"token": "$INFOMANIAK_TOKEN"
|
||||||
|
},
|
||||||
"INWX": {
|
"INWX": {
|
||||||
"TYPE": "INWX",
|
"TYPE": "INWX",
|
||||||
"domain": "$INWX_DOMAIN",
|
"domain": "$INWX_DOMAIN",
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import (
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/hexonet"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/hexonet"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/hostingde"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/hostingde"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/huaweicloud"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/huaweicloud"
|
||||||
|
_ "github.com/StackExchange/dnscontrol/v4/providers/infomaniak"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/internetbs"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/internetbs"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/inwx"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/inwx"
|
||||||
_ "github.com/StackExchange/dnscontrol/v4/providers/joker"
|
_ "github.com/StackExchange/dnscontrol/v4/providers/joker"
|
||||||
|
|
|
||||||
227
providers/infomaniak/api.go
Normal file
227
providers/infomaniak/api.go
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
package infomaniak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const baseURL = "https://api.infomaniak.com/2"
|
||||||
|
|
||||||
|
type dnssecRecord struct {
|
||||||
|
IsEnabled bool `json:"is_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorRecord struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsZoneResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Data dnsZone `json:"data,omitempty"`
|
||||||
|
Error errorRecord `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsRecordsResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Data []dnsRecord `json:"data,omitempty"`
|
||||||
|
Error errorRecord `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsRecordResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Data dnsRecord `json:"data,omitempty"`
|
||||||
|
Error errorRecord `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type boolResponse struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
Data bool `json:"data,omitempty"`
|
||||||
|
Error errorRecord `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
type dnsZone struct {
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
FQDN string `json:"fqdn,omitempty"`
|
||||||
|
DNSSEC dnssecRecord `json:"dnssec,omitempty"`
|
||||||
|
Nameservers []string `json:"nameservers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsRecord struct {
|
||||||
|
ID int64 `json:"id,omitempty"`
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
TTL int64 `json:"ttl,omitempty"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsRecordCreate struct {
|
||||||
|
Source string `json:"source,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
TTL int64 `json:"ttl,omitempty"`
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type dnsRecordUpdate struct {
|
||||||
|
Target string `json:"target,omitempty"`
|
||||||
|
TTL int64 `json:"ttl,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get zone information
|
||||||
|
// See https://developer.infomaniak.com/docs/api/get/2/zones/%7Bzone%7D
|
||||||
|
func (p *infomaniakProvider) getDNSZone(zone string) (*dnsZone, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/zones/%s", baseURL, zone)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+p.apiToken)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
response := &dnsZoneResponse{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all dns record for a given zone
|
||||||
|
// See https://developer.infomaniak.com/docs/api/get/2/zones/%7Bzone%7D/records
|
||||||
|
func (p *infomaniakProvider) getDNSRecords(zone string) ([]dnsRecord, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/zones/%s/records", baseURL, zone)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+p.apiToken)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
response := &dnsRecordsResponse{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a dns record
|
||||||
|
// See https://developer.infomaniak.com/docs/api/delete/2/zones/%7Bzone%7D/records/%7Brecord%7D
|
||||||
|
func (p *infomaniakProvider) deleteDNSRecord(zone string, recordID string) error {
|
||||||
|
reqURL := fmt.Sprintf("%s/zones/%s/records/%s", baseURL, zone, recordID)
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodDelete, reqURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+p.apiToken)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
response := &boolResponse{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Result == "error" {
|
||||||
|
return fmt.Errorf("failed to delete record %s in zone %s: %s", recordID, zone, response.Error.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a dns record in a given zone
|
||||||
|
// See https://developer.infomaniak.com/docs/api/post/2/zones/%7Bzone%7D/records
|
||||||
|
func (p *infomaniakProvider) createDNSRecord(zone string, rec *dnsRecordCreate) (*dnsRecord, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/zones/%s/records", baseURL, zone)
|
||||||
|
|
||||||
|
data, err := json.Marshal(rec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+p.apiToken)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
response := &dnsRecordResponse{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Result == "error" {
|
||||||
|
return nil, fmt.Errorf("failed to create %s record in zone %s: %s", rec.Type, zone, response.Error.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update a dns record in a given zone
|
||||||
|
// See https://developer.infomaniak.com/docs/api/put/2/zones/%7Bzone%7D/records/%7Brecord%7D
|
||||||
|
func (p *infomaniakProvider) updateDNSRecord(zone string, recordID string, rec *dnsRecordUpdate) (*dnsRecord, error) {
|
||||||
|
reqURL := fmt.Sprintf("%s/zones/%s/records/%s", baseURL, zone, recordID)
|
||||||
|
|
||||||
|
data, err := json.Marshal(rec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPut, reqURL, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Add("Authorization", "Bearer "+p.apiToken)
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
res, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
response := &dnsRecordResponse{}
|
||||||
|
err = json.NewDecoder(res.Body).Decode(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Result == "error" {
|
||||||
|
return nil, fmt.Errorf("failed to update record %s in zone %s: %s", recordID, zone, response.Error.Description)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response.Data, nil
|
||||||
|
}
|
||||||
15
providers/infomaniak/auditrecords.go
Normal file
15
providers/infomaniak/auditrecords.go
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
package infomaniak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/pkg/rejectif"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuditRecords returns a list of errors corresponding to the records
|
||||||
|
// that aren't supported by this provider. If all records are
|
||||||
|
// supported, an empty list is returned.
|
||||||
|
func AuditRecords(records []*models.RecordConfig) []error {
|
||||||
|
a := rejectif.Auditor{}
|
||||||
|
|
||||||
|
return a.Audit(records)
|
||||||
|
}
|
||||||
344
providers/infomaniak/infomaniakProvider.go
Normal file
344
providers/infomaniak/infomaniakProvider.go
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
package infomaniak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/models"
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
||||||
|
"github.com/StackExchange/dnscontrol/v4/providers"
|
||||||
|
"github.com/miekg/dns/dnsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// infomaniakProvider is the handle for operations.
|
||||||
|
type infomaniakProvider struct {
|
||||||
|
apiToken string // the account access token
|
||||||
|
}
|
||||||
|
|
||||||
|
var features = providers.DocumentationNotes{
|
||||||
|
// The default for unlisted capabilities is 'Cannot'.
|
||||||
|
// See providers/capabilities.go for the entire list of capabilities.
|
||||||
|
providers.CanUseCAA: providers.Can(),
|
||||||
|
providers.CanUseDNAME: providers.Can(),
|
||||||
|
providers.CanUseDS: providers.Can(),
|
||||||
|
providers.CanUseSSHFP: providers.Can(),
|
||||||
|
providers.CanUseTLSA: providers.Can(),
|
||||||
|
providers.CanUseSRV: providers.Can(),
|
||||||
|
// providers.DocCreateDomains: providers.Can(),
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInfomaniak(m map[string]string, message json.RawMessage) (providers.DNSServiceProvider, error) {
|
||||||
|
api := &infomaniakProvider{}
|
||||||
|
api.apiToken = m["token"]
|
||||||
|
if api.apiToken == "" {
|
||||||
|
return nil, errors.New("missing Infomaniak personal access token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return api, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
const providerName = "INFOMANIAK"
|
||||||
|
const providerMaintainer = "@jbelien"
|
||||||
|
fns := providers.DspFuncs{
|
||||||
|
Initializer: newInfomaniak,
|
||||||
|
RecordAuditor: AuditRecords,
|
||||||
|
}
|
||||||
|
providers.RegisterDomainServiceProviderType(providerName, fns, features)
|
||||||
|
providers.RegisterMaintainer(providerName, providerMaintainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *infomaniakProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
|
||||||
|
zone, err := p.getDNSZone(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return models.ToNameservers(zone.Nameservers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addTrailingDot adds a trailing dot if it's missing.
|
||||||
|
func addTrailingDot(target string) string {
|
||||||
|
if target == "" || target == "." {
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(target, ".") {
|
||||||
|
return target + "."
|
||||||
|
}
|
||||||
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
// toRecordConfig converts a DNS record from Infomaniak API to RecordConfig.
|
||||||
|
func toRecordConfig(domain string, r dnsRecord) (*models.RecordConfig, error) {
|
||||||
|
rc := &models.RecordConfig{
|
||||||
|
TTL: uint32(r.TTL),
|
||||||
|
Original: r,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the source/label - Infomaniak uses empty string or "." for apex
|
||||||
|
label := r.Source
|
||||||
|
if label == "" || label == "." {
|
||||||
|
label = "@"
|
||||||
|
}
|
||||||
|
rc.SetLabel(label, domain)
|
||||||
|
|
||||||
|
// Parse the target based on record type
|
||||||
|
rtype := r.Type
|
||||||
|
target := r.Target
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch rtype {
|
||||||
|
case "A", "AAAA":
|
||||||
|
rc.Type = rtype
|
||||||
|
err = rc.SetTarget(target)
|
||||||
|
|
||||||
|
case "CNAME", "NS", "DNAME":
|
||||||
|
rc.Type = rtype
|
||||||
|
// Add trailing dot and use AddOrigin to properly qualify the target
|
||||||
|
err = rc.SetTarget(dnsutil.AddOrigin(addTrailingDot(target), domain))
|
||||||
|
|
||||||
|
case "MX":
|
||||||
|
// Infomaniak returns MX as "priority target" (e.g., "5 mta-gw.infomaniak.ch")
|
||||||
|
rc.Type = rtype
|
||||||
|
err = rc.SetTargetMXString(addTrailingDot(target))
|
||||||
|
|
||||||
|
case "TXT":
|
||||||
|
rc.Type = rtype
|
||||||
|
// Infomaniak API returns TXT values wrapped in quotes, strip them
|
||||||
|
if len(target) >= 2 && strings.HasPrefix(target, "\"") && strings.HasSuffix(target, "\"") {
|
||||||
|
target = target[1 : len(target)-1]
|
||||||
|
}
|
||||||
|
err = rc.SetTargetTXT(target)
|
||||||
|
|
||||||
|
case "SRV":
|
||||||
|
// Infomaniak returns SRV as "priority weight port target"
|
||||||
|
rc.Type = rtype
|
||||||
|
err = rc.SetTargetSRVString(addTrailingDot(target))
|
||||||
|
|
||||||
|
case "CAA":
|
||||||
|
// Infomaniak returns CAA as "flags tag value" (e.g., "0 issue letsencrypt.org")
|
||||||
|
rc.Type = rtype
|
||||||
|
err = rc.SetTargetCAAString(target)
|
||||||
|
|
||||||
|
case "DS":
|
||||||
|
// Infomaniak returns DS as "keytag algorithm digesttype digest"
|
||||||
|
// Note: Infomaniak may split long digest data with spaces, so we need to rejoin them
|
||||||
|
rc.Type = rtype
|
||||||
|
parts := strings.Fields(target)
|
||||||
|
if len(parts) >= 4 {
|
||||||
|
// Rejoin all parts after the first 3 (keytag, algorithm, digesttype) as the digest
|
||||||
|
digest := strings.Join(parts[3:], "")
|
||||||
|
target = fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], digest)
|
||||||
|
}
|
||||||
|
err = rc.SetTargetDSString(target)
|
||||||
|
|
||||||
|
case "SSHFP":
|
||||||
|
// Infomaniak returns SSHFP as "algorithm fingerprint_type fingerprint"
|
||||||
|
// Note: Infomaniak may split long fingerprint data with spaces, so we need to rejoin them
|
||||||
|
rc.Type = rtype
|
||||||
|
parts := strings.Fields(target)
|
||||||
|
if len(parts) >= 3 {
|
||||||
|
// Rejoin all parts after the first 2 (algorithm, fingerprint_type) as the fingerprint
|
||||||
|
fingerprint := strings.Join(parts[2:], "")
|
||||||
|
target = fmt.Sprintf("%s %s %s", parts[0], parts[1], fingerprint)
|
||||||
|
}
|
||||||
|
err = rc.SetTargetSSHFPString(target)
|
||||||
|
|
||||||
|
case "TLSA":
|
||||||
|
// Infomaniak returns TLSA as "usage selector matching_type certificate"
|
||||||
|
// Note: Infomaniak may split long certificate data with spaces, so we need to rejoin them
|
||||||
|
rc.Type = rtype
|
||||||
|
parts := strings.Fields(target)
|
||||||
|
if len(parts) >= 4 {
|
||||||
|
// Rejoin all parts after the first 3 (usage, selector, matching_type) as the certificate
|
||||||
|
certificate := strings.Join(parts[3:], "")
|
||||||
|
target = fmt.Sprintf("%s %s %s %s", parts[0], parts[1], parts[2], certificate)
|
||||||
|
}
|
||||||
|
err = rc.SetTargetTLSAString(target)
|
||||||
|
|
||||||
|
default:
|
||||||
|
rc.Type = rtype
|
||||||
|
err = rc.SetTarget(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unparsable record type=%q target=%q received from Infomaniak: %w", rtype, target, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromRecordConfig converts a RecordConfig to the API format for creation.
|
||||||
|
func fromRecordConfig(rc *models.RecordConfig) *dnsRecordCreate {
|
||||||
|
// Get the label - Infomaniak uses empty string for apex
|
||||||
|
label := rc.GetLabel()
|
||||||
|
if label == "@" {
|
||||||
|
label = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the target in the format expected by Infomaniak API
|
||||||
|
var target string
|
||||||
|
switch rc.Type {
|
||||||
|
case "A", "AAAA":
|
||||||
|
target = rc.GetTargetField()
|
||||||
|
|
||||||
|
case "CNAME", "NS", "DNAME":
|
||||||
|
// Remove trailing dot for the API
|
||||||
|
target = strings.TrimSuffix(rc.GetTargetField(), ".")
|
||||||
|
|
||||||
|
case "MX":
|
||||||
|
// Format: "priority target" (without trailing dot)
|
||||||
|
target = fmt.Sprintf("%d %s", rc.MxPreference, strings.TrimSuffix(rc.GetTargetField(), "."))
|
||||||
|
|
||||||
|
case "TXT":
|
||||||
|
target = rc.GetTargetField()
|
||||||
|
|
||||||
|
case "SRV":
|
||||||
|
// Format: "priority weight port target" (without trailing dot)
|
||||||
|
target = fmt.Sprintf("%d %d %d %s", rc.SrvPriority, rc.SrvWeight, rc.SrvPort, strings.TrimSuffix(rc.GetTargetField(), "."))
|
||||||
|
|
||||||
|
case "CAA":
|
||||||
|
// Format: "flags tag value"
|
||||||
|
target = fmt.Sprintf("%d %s %s", rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
|
||||||
|
|
||||||
|
case "DS":
|
||||||
|
// Format: "keytag algorithm digesttype digest"
|
||||||
|
target = fmt.Sprintf("%d %d %d %s", rc.DsKeyTag, rc.DsAlgorithm, rc.DsDigestType, rc.DsDigest)
|
||||||
|
|
||||||
|
case "SSHFP":
|
||||||
|
// Format: "algorithm fingerprint_type fingerprint"
|
||||||
|
target = fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
|
||||||
|
|
||||||
|
case "TLSA":
|
||||||
|
// Format: "usage selector matching_type certificate"
|
||||||
|
target = fmt.Sprintf("%d %d %d %s", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.GetTargetField())
|
||||||
|
|
||||||
|
default:
|
||||||
|
target = rc.GetTargetField()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dnsRecordCreate{
|
||||||
|
Source: label,
|
||||||
|
Type: rc.Type,
|
||||||
|
TTL: int64(rc.TTL),
|
||||||
|
Target: target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toRecordUpdate converts a RecordConfig to the API format for updating.
|
||||||
|
func toRecordUpdate(rc *models.RecordConfig) *dnsRecordUpdate {
|
||||||
|
// Get the target in the format expected by Infomaniak API
|
||||||
|
var target string
|
||||||
|
switch rc.Type {
|
||||||
|
case "A", "AAAA":
|
||||||
|
target = rc.GetTargetField()
|
||||||
|
|
||||||
|
case "CNAME", "NS", "DNAME":
|
||||||
|
// Remove trailing dot for the API
|
||||||
|
target = strings.TrimSuffix(rc.GetTargetField(), ".")
|
||||||
|
|
||||||
|
case "MX":
|
||||||
|
// Format: "priority target" (without trailing dot)
|
||||||
|
target = fmt.Sprintf("%d %s", rc.MxPreference, strings.TrimSuffix(rc.GetTargetField(), "."))
|
||||||
|
|
||||||
|
case "TXT":
|
||||||
|
target = rc.GetTargetField()
|
||||||
|
|
||||||
|
case "SRV":
|
||||||
|
// Format: "priority weight port target" (without trailing dot)
|
||||||
|
target = fmt.Sprintf("%d %d %d %s", rc.SrvPriority, rc.SrvWeight, rc.SrvPort, strings.TrimSuffix(rc.GetTargetField(), "."))
|
||||||
|
|
||||||
|
case "CAA":
|
||||||
|
// Format: "flags tag value"
|
||||||
|
target = fmt.Sprintf("%d %s %s", rc.CaaFlag, rc.CaaTag, rc.GetTargetField())
|
||||||
|
|
||||||
|
case "DS":
|
||||||
|
// Format: "keytag algorithm digesttype digest"
|
||||||
|
target = fmt.Sprintf("%d %d %d %s", rc.DsKeyTag, rc.DsAlgorithm, rc.DsDigestType, rc.DsDigest)
|
||||||
|
|
||||||
|
case "SSHFP":
|
||||||
|
// Format: "algorithm fingerprint_type fingerprint"
|
||||||
|
target = fmt.Sprintf("%d %d %s", rc.SshfpAlgorithm, rc.SshfpFingerprint, rc.GetTargetField())
|
||||||
|
|
||||||
|
case "TLSA":
|
||||||
|
// Format: "usage selector matching_type certificate"
|
||||||
|
target = fmt.Sprintf("%d %d %d %s", rc.TlsaUsage, rc.TlsaSelector, rc.TlsaMatchingType, rc.GetTargetField())
|
||||||
|
|
||||||
|
default:
|
||||||
|
target = rc.GetTargetField()
|
||||||
|
}
|
||||||
|
|
||||||
|
return &dnsRecordUpdate{
|
||||||
|
TTL: int64(rc.TTL),
|
||||||
|
Target: target,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *infomaniakProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
||||||
|
records, err := p.getDNSRecords(domain)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanRecords := make(models.Records, 0, len(records))
|
||||||
|
|
||||||
|
for _, r := range records {
|
||||||
|
recConfig, err := toRecordConfig(domain, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cleanRecords = append(cleanRecords, recConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanRecords, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *infomaniakProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
|
||||||
|
var corrections []*models.Correction
|
||||||
|
domain := dc.Name
|
||||||
|
|
||||||
|
changes, actualChangeCount, err := diff2.ByRecord(existingRecords, dc, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, change := range changes {
|
||||||
|
switch change.Type {
|
||||||
|
case diff2.REPORT:
|
||||||
|
corrections = append(corrections, &models.Correction{Msg: change.MsgsJoined})
|
||||||
|
case diff2.CHANGE:
|
||||||
|
oldRec := change.Old[0].Original.(dnsRecord)
|
||||||
|
newRec := change.New[0]
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: change.MsgsJoined,
|
||||||
|
F: func() error {
|
||||||
|
_, err := p.updateDNSRecord(domain, fmt.Sprintf("%v", oldRec.ID), toRecordUpdate(newRec))
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case diff2.CREATE:
|
||||||
|
rec := change.New[0]
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: change.MsgsJoined,
|
||||||
|
F: func() error {
|
||||||
|
_, err := p.createDNSRecord(domain, fromRecordConfig(rec))
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
case diff2.DELETE:
|
||||||
|
rec := change.Old[0].Original.(dnsRecord)
|
||||||
|
corrections = append(corrections, &models.Correction{
|
||||||
|
Msg: change.MsgsJoined,
|
||||||
|
F: func() error {
|
||||||
|
return p.deleteDNSRecord(domain, fmt.Sprintf("%v", rec.ID))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return corrections, actualChangeCount, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue