-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 2b70a9d
Showing
8 changed files
with
736 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
_gitignore/ | ||
*~ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
MIT License | ||
|
||
Copyright (c) 2021 Jan Delgado | ||
Copyright (c) 2020 matthiasng | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# IONOS DNS API for `libdns` | ||
|
||
This package implements the libdns interfaces for the [IONOS DNS | ||
API (beta)](https://developer.hosting.ionos.de/docs/dns) | ||
|
||
## Authenticating | ||
|
||
To authenticate you need to supply a IONOS API Key, as described on | ||
https://developer.hosting.ionos.de/docs/getstarted | ||
|
||
## Example | ||
|
||
Here's a minimal example of how to get all DNS records for zone. | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"os" | ||
"time" | ||
|
||
"github.com/libdns/ionos" | ||
) | ||
|
||
func main() { | ||
token := os.Getenv("LIBDNS_IONOS_TOKEN") | ||
if token == "" { | ||
panic("LIBDNS_IONOS_TOKEN not set") | ||
} | ||
|
||
zone := os.Getenv("LIBDNS_IONOS_ZONE") | ||
if zone == "" { | ||
panic("LIBDNS_IONOS_ZONE not set") | ||
} | ||
|
||
p := &ionos.Provider{ | ||
AuthAPIToken: token, | ||
} | ||
|
||
records, err := p.GetRecords(context.TODO(), zone) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
out, _ := json.MarshalIndent(records, "", " ") | ||
fmt.Println(string(out)) | ||
} | ||
``` | ||
|
||
## Test | ||
|
||
The file `provisioner_test.go` contains an end-to-end test suite, using the | ||
original IONOS API service (i.e. no test doubles - be careful). To run the | ||
tests: | ||
|
||
```console | ||
$ export LIBDNS_IONOS_TEST_ZONE=mydomain.org | ||
$ export LIBDNS_IONOS_TEST_TOKEN=aaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb | ||
$ go test -v | ||
=== RUN Test_AppendRecords | ||
--- PASS: Test_AppendRecords (43.01s) | ||
=== RUN Test_DeleteRecords | ||
--- PASS: Test_DeleteRecords (23.91s) | ||
=== RUN Test_GetRecords | ||
--- PASS: Test_GetRecords (30.96s) | ||
=== RUN Test_SetRecords | ||
--- PASS: Test_SetRecords (51.39s) | ||
PASS | ||
ok github.com/libdns/ionos 149.277s | ||
``` | ||
|
||
The tests were taken from the [Hetzner libdns | ||
module](https://github.com/libdns/hetzner) and are not modified. | ||
|
||
## Author | ||
|
||
Original Work (C) Copyright 2020 by matthiasng (based on https://github.com/libdns/hetzner), | ||
this version (C) Copyright 2021 by Jan Delgado (github.com/jandelgado). | ||
|
||
License: MIT | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
// libdns client for IONOS DNS API | ||
package ionos | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/libdns/libdns" | ||
) | ||
|
||
const ( | ||
APIEndpoint = "https://api.hosting.ionos.com/dns/v1" | ||
) | ||
|
||
type getAllZonesResponse struct { | ||
Zones []zoneDescriptor | ||
} | ||
|
||
type zoneDescriptor struct { | ||
Name string `json:"name"` | ||
ID string `json:"id"` | ||
Type string `json:"type"` | ||
} | ||
|
||
type getZoneResponse struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Records []zoneRecord `json:"records"` | ||
} | ||
|
||
type zoneRecord struct { | ||
ID string `json:"id"` | ||
Name string `json:"name"` | ||
RootName string `json:"rootName"` | ||
Type string `json:"type"` | ||
Content string `json:"content"` | ||
ChangeDate string `json:"changeDate"` | ||
TTL int `json:"ttl"` | ||
Prio int `json:"prio"` | ||
Disabled bool `json:"disabled"` | ||
} | ||
|
||
type record struct { | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Content string `json:"content"` | ||
TTL int `json:"ttl"` | ||
Prio int `json:"prio"` | ||
Disabled bool `json:"disabled,omitempty"` // TODO default=true | ||
} | ||
|
||
func doRequest(token string, request *http.Request) ([]byte, error) { | ||
request.Header.Add("X-API-Key", token) | ||
request.Header.Add("Content-Type", "application/json") | ||
|
||
client := &http.Client{} // no timeout set because request is w/ context | ||
response, err := client.Do(request) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer response.Body.Close() | ||
|
||
if response.StatusCode < 200 || response.StatusCode >= 300 { | ||
return nil, fmt.Errorf("%s (%d)", http.StatusText(response.StatusCode), response.StatusCode) | ||
} | ||
return ioutil.ReadAll(response.Body) | ||
} | ||
|
||
// GET /v1/zones | ||
func getAllZones(ctx context.Context, token string) (getAllZonesResponse, error) { | ||
uri := fmt.Sprintf("%s/zones", APIEndpoint) | ||
req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) | ||
data, err := doRequest(token, req) | ||
|
||
if err != nil { | ||
return getAllZonesResponse{}, err | ||
} | ||
|
||
// parse top-level JSON array | ||
zones := make([]zoneDescriptor, 0) | ||
err = json.Unmarshal(data, &zones) | ||
return getAllZonesResponse{zones}, err | ||
} | ||
|
||
// findZoneDescriptor finds the zoneDescriptor for the named zoned in all zones | ||
func findZoneDescriptor(ctx context.Context, token string, zoneName string) (zoneDescriptor, error) { | ||
allZones, err := getAllZones(ctx, token) | ||
if err != nil { | ||
return zoneDescriptor{}, err | ||
} | ||
for _, zone := range allZones.Zones { | ||
if zone.Name == zoneName { | ||
return zone, nil | ||
} | ||
} | ||
return zoneDescriptor{}, fmt.Errorf("zone not found") | ||
} | ||
|
||
// getZone reads a zone by it's IONOS zoneID | ||
// /v1/zones/{zoneId} | ||
func getZone(ctx context.Context, token string, zoneID string) (getZoneResponse, error) { | ||
uri := fmt.Sprintf("%s/zones/%s", APIEndpoint, zoneID) | ||
req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) | ||
data, err := doRequest(token, req) | ||
var result getZoneResponse | ||
if err != nil { | ||
return result, err | ||
} | ||
|
||
err = json.Unmarshal(data, &result) | ||
return result, err | ||
} | ||
|
||
// findRecordInZone searches all records in the given zone for a record with | ||
// the given name and type and returns this record on success | ||
func findRecordInZone(ctx context.Context, token, zoneName, name, typ string) (zoneRecord, error) { | ||
zoneResp, err := getZoneByName(ctx, token, zoneName) | ||
if err != nil { | ||
return zoneRecord{}, err | ||
} | ||
|
||
for _, r := range zoneResp.Records { | ||
if r.Name == name && r.Type == typ { | ||
return r, nil | ||
} | ||
} | ||
return zoneRecord{}, fmt.Errorf("record not found") | ||
} | ||
|
||
// getZoneByName reads a zone by it's zone name, requiring 2 REST calls to | ||
// the IONOS API | ||
func getZoneByName(ctx context.Context, token, zoneName string) (getZoneResponse, error) { | ||
zoneDes, err := findZoneDescriptor(ctx, token, zoneName) | ||
if err != nil { | ||
return getZoneResponse{}, err | ||
} | ||
return getZone(ctx, token, zoneDes.ID) | ||
} | ||
|
||
// getAllRecords returns all records from the given zone | ||
func getAllRecords(ctx context.Context, token string, zoneName string) ([]libdns.Record, error) { | ||
zoneResp, err := getZoneByName(ctx, token, zoneName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
records := []libdns.Record{} | ||
for _, r := range zoneResp.Records { | ||
records = append(records, libdns.Record{ | ||
ID: r.ID, | ||
Type: r.Type, | ||
// libdns Name is partially qualified, relative to zone | ||
Name: libdns.RelativeName(r.Name, zoneResp.Name), | ||
Value: r.Content, | ||
TTL: time.Duration(r.TTL) * time.Second, | ||
}) | ||
} | ||
return records, nil | ||
} | ||
|
||
// createRecord creates a DNS record in the given zone | ||
// POST /v1/zones/{zoneId}/records | ||
func createRecord(ctx context.Context, token string, zoneName string, r libdns.Record) (libdns.Record, error) { | ||
zoneResp, err := getZoneByName(ctx, token, zoneName) | ||
if err != nil { | ||
return libdns.Record{}, err | ||
} | ||
|
||
reqData := []record{ | ||
{Type: r.Type, | ||
// IONOS: Name is fully qualified | ||
Name: libdns.AbsoluteName(r.Name, zoneName), | ||
Content: r.Value, | ||
TTL: int(r.TTL.Seconds()), | ||
}} | ||
|
||
reqBuffer, err := json.Marshal(reqData) | ||
if err != nil { | ||
return libdns.Record{}, err | ||
} | ||
|
||
uri := fmt.Sprintf("%s/zones/%s/records", APIEndpoint, zoneResp.ID) | ||
req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(reqBuffer)) | ||
_, err = doRequest(token, req) // no data is returned on success, just 201 | ||
|
||
if err != nil { | ||
return libdns.Record{}, err | ||
} | ||
|
||
// re-read the record so we get it's ID. IONOS API does not return the | ||
// ID currently in the response. | ||
createdRec, err := findRecordInZone(ctx, token, zoneName, libdns.AbsoluteName(r.Name, zoneName), r.Type) | ||
if err != nil { | ||
// Thats bad, the record was created, but we can not read it ?! | ||
// in this case we just return an empty ID | ||
log.Printf("ERROR could not find record: %+v", err) | ||
} | ||
|
||
return libdns.Record{ | ||
ID: createdRec.ID, | ||
Type: r.Type, | ||
// always return partially qualified name, relative to zone for libdns | ||
Name: libdns.RelativeName(unFQDN(r.Name), zoneName), | ||
Value: r.Value, | ||
TTL: r.TTL, | ||
}, nil | ||
} | ||
|
||
// DELETE /v1/zones/{zoneId}/records/{recordId} | ||
func deleteRecord(ctx context.Context, token, zoneName string, record libdns.Record) error { | ||
zoneResp, err := getZoneByName(ctx, token, zoneName) | ||
if err != nil { | ||
return err | ||
} | ||
req, err := http.NewRequestWithContext(ctx, "DELETE", | ||
fmt.Sprintf("%s/zones/%s/records/%s", APIEndpoint, zoneResp.ID, record.ID), nil) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = doRequest(token, req) | ||
return err | ||
} | ||
|
||
// /v1/zones/{zoneId}/records/{recordId} | ||
func updateRecord(ctx context.Context, token string, zone string, r libdns.Record) (libdns.Record, error) { | ||
zoneDes, err := getZoneByName(ctx, token, zone) | ||
if err != nil { | ||
return libdns.Record{}, err | ||
} | ||
|
||
reqData := record{ | ||
Type: r.Type, | ||
Name: libdns.AbsoluteName(r.Name, zone), | ||
Content: r.Value, | ||
TTL: int(r.TTL.Seconds()), | ||
} | ||
|
||
reqBuffer, err := json.Marshal(reqData) | ||
if err != nil { | ||
return libdns.Record{}, err | ||
} | ||
|
||
req, err := http.NewRequestWithContext(ctx, "PUT", | ||
fmt.Sprintf("%s/zones/%s/records/%s", APIEndpoint, zoneDes.ID, r.ID), | ||
bytes.NewBuffer(reqBuffer)) | ||
|
||
if err != nil { | ||
return libdns.Record{}, err | ||
} | ||
|
||
_, err = doRequest(token, req) | ||
|
||
return libdns.Record{ | ||
ID: r.ID, | ||
Type: r.Type, | ||
Name: r.Name, | ||
Value: r.Value, | ||
TTL: time.Duration(r.TTL) * time.Second, | ||
}, err | ||
} | ||
|
||
func createOrUpdateRecord(ctx context.Context, token string, zone string, r libdns.Record) (libdns.Record, error) { | ||
if r.ID == "" { | ||
return createRecord(ctx, token, zone, r) | ||
} | ||
return updateRecord(ctx, token, zone, r) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/libdns/ionos | ||
|
||
go 1.16 | ||
|
||
require github.com/libdns/libdns v0.2.1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= | ||
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= |
Oops, something went wrong.