Skip to content

Commit 736328b

Browse files
committed
Initial commit
0 parents  commit 736328b

File tree

7 files changed

+729
-0
lines changed

7 files changed

+729
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2022 gjung56
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# OVH DNS for `libdns`
2+
3+
[![godoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/libdns/ovh)
4+
5+
6+
This package implements the libdns interfaces for the [OVH DNS API](https://docs.ovh.com/gb/en/api/first-steps-with-ovh-api/) using the [OVH API GO SDK](https://github.com/ovh/go-ovh)
7+
8+
## Authenticating
9+
10+
To authenticate you need to create [script credentials](https://github.com/ovh/go-ovh#supported-apis) in your region account and specify these API rights :
11+
12+
For multiple domains :
13+
14+
```
15+
GET /domain/zone/*/record
16+
POST /domain/zone/*/record
17+
GET /domain/zone/*/record/*
18+
PUT /domain/zone/*/record/*
19+
DELETE /domain/zone/*/record/*
20+
GET /domain/zone/*/soa
21+
POST /domain/zone/*/refresh
22+
```
23+
24+
For a single domain or delegation :
25+
26+
```
27+
GET /domain/zone/yourdomain.com/record
28+
POST /domain/zone/yourdomain.com/record
29+
GET /domain/zone/yourdomain.com/record/*
30+
PUT /domain/zone/yourdomain.com/record/*
31+
DELETE /domain/zone/yourdomain.com/record/*
32+
GET /domain/zone/yourdomain.com/soa
33+
POST /domain/zone/yourdomain.com/refresh
34+
```
35+
36+
## Example
37+
38+
Here's a minimal example of how to get all DNS records for zone. See also: [provider_test.go](https://github.com/libdns/ovh/blob/main/provider_test.go)
39+
40+
```go
41+
package main
42+
43+
import (
44+
"context"
45+
"fmt"
46+
"os"
47+
"time"
48+
49+
"github.com/libdns/ovh"
50+
)
51+
52+
func main() {
53+
endPoint := os.Getenv("LIBDNS_OVH_TEST_ENDPOINT")
54+
if endPoint == "" {
55+
fmt.Printf("LIBDNS_OVH_TEST_ENDPOINT not set\n")
56+
return
57+
}
58+
59+
applicationKey := os.Getenv("LIBDNS_OVH_TEST_APPLICATION_KEY")
60+
if applicationKey == "" {
61+
fmt.Printf("LIBDNS_OVH_TEST_APPLICATION_KEY not set\n")
62+
return
63+
}
64+
65+
applicationSecret := os.Getenv("LIBDNS_OVH_TEST_APPLICATION_SECRET")
66+
if applicationSecret == "" {
67+
fmt.Printf("LIBDNS_OVH_TEST_APPLICATION_SECRET not set\n")
68+
return
69+
}
70+
71+
consumerKey := os.Getenv("LIBDNS_OVH_TEST_CONSUMER_KEY")
72+
if consumerKey == "" {
73+
fmt.Printf("LIBDNS_OVH_TEST_CONSUMER_KEY not set\n")
74+
return
75+
}
76+
77+
zone := os.Getenv("LIBDNS_OVH_TEST_ZONE")
78+
if zone == "" {
79+
fmt.Printf("LIBDNS_OVH_TEST_ZONE not set\n")
80+
return
81+
}
82+
83+
p := &ovh.Provider{
84+
Endpoint: endPoint,
85+
ApplicationKey: applicationKey,
86+
ApplicationSecret: applicationSecret,
87+
ConsumerKey: consumerKey,
88+
}
89+
90+
records, err := p.GetRecords(context.TODO(), zone)
91+
if err != nil {
92+
fmt.Printf("Error: %s", err.Error())
93+
return
94+
}
95+
96+
fmt.Println(records)
97+
}
98+
99+
```

client.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
package ovh
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
"strings"
8+
"sync"
9+
"time"
10+
11+
"github.com/libdns/libdns"
12+
"github.com/ovh/go-ovh/ovh"
13+
)
14+
15+
// Client is an abstraction of OvhClient
16+
type Client struct {
17+
ovhClient *ovh.Client
18+
mutex sync.Mutex
19+
}
20+
21+
// Ovh zone record implementation
22+
type OvhDomainZoneRecord struct {
23+
ID int64 `json:"id,omitempty"`
24+
Zone string `json:"zone,omitempty"`
25+
SubDomain string `json:"subDomain"`
26+
FieldType string `json:"fieldType,omitempty"`
27+
Target string `json:"target"`
28+
TTL int64 `json:"ttl"`
29+
}
30+
31+
// Ovh zone soa implementation
32+
type OvhDomainZoneSOA struct {
33+
Server string `json:"server"`
34+
Email string `json:"email"`
35+
Serial int64 `json:"serial"`
36+
Refresh int64 `json:"refresh"`
37+
NxDomainTTL int64 `json:"nxDomainTtl"`
38+
Expire int64 `json:"expire"`
39+
TTL int64 `json:"ttl"`
40+
}
41+
42+
// setupClient invokes authentication and store client to the provider instance.
43+
func (p *Provider) setupClient() error {
44+
if p.client.ovhClient == nil {
45+
client, err := ovh.NewClient(p.Endpoint, p.ApplicationKey, p.ApplicationSecret, p.ConsumerKey)
46+
47+
if err != nil {
48+
return err
49+
}
50+
51+
p.client.ovhClient = client
52+
}
53+
54+
return nil
55+
}
56+
57+
// getRecords gets all records in specified zone on Ovh DNS.
58+
// TTL as 0 for any record correspond to the default TTL for the zone
59+
func (p *Provider) getRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
60+
p.client.mutex.Lock()
61+
defer p.client.mutex.Unlock()
62+
63+
if err := p.setupClient(); err != nil {
64+
return nil, err
65+
}
66+
67+
var dzSOA OvhDomainZoneSOA
68+
if err := p.client.ovhClient.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/soa", zone), &dzSOA); err != nil {
69+
return nil, err
70+
}
71+
72+
var idRecords []int64
73+
if err := p.client.ovhClient.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record", zone), &idRecords); err != nil {
74+
return nil, err
75+
}
76+
77+
var records []libdns.Record
78+
for _, idr := range idRecords {
79+
var dzr OvhDomainZoneRecord
80+
if err := p.client.ovhClient.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%d", zone, idr), &dzr); err != nil {
81+
return nil, err
82+
}
83+
84+
if dzr.TTL == 0 {
85+
dzr.TTL = dzSOA.TTL
86+
}
87+
88+
record := libdns.Record{
89+
ID: strconv.FormatInt(dzr.ID, 10),
90+
Type: dzr.FieldType,
91+
Name: dzr.SubDomain,
92+
Value: strings.TrimRight(strings.TrimLeft(dzr.Target, "\""), "\""),
93+
TTL: time.Duration(dzr.TTL) * time.Second,
94+
}
95+
records = append(records, record)
96+
}
97+
98+
return records, nil
99+
}
100+
101+
// createRecord creates a new record in the specified zone.
102+
func (p *Provider) createRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
103+
p.client.mutex.Lock()
104+
defer p.client.mutex.Unlock()
105+
106+
if err := p.setupClient(); err != nil {
107+
return libdns.Record{}, err
108+
}
109+
110+
var nzr OvhDomainZoneRecord
111+
if err := p.client.ovhClient.PostWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record", zone), &OvhDomainZoneRecord{FieldType: record.Type, SubDomain: normalizeRecordName(record.Name, zone), Target: record.Value, TTL: int64(record.TTL.Seconds())}, &nzr); err != nil {
112+
return libdns.Record{}, err
113+
}
114+
115+
createdRecord := libdns.Record{
116+
ID: strconv.FormatInt(nzr.ID, 10),
117+
Type: nzr.FieldType,
118+
Name: nzr.SubDomain,
119+
Value: strings.TrimRight(strings.TrimLeft(nzr.Target, "\""), "\""),
120+
TTL: time.Duration(nzr.TTL) * time.Second,
121+
}
122+
123+
return createdRecord, nil
124+
}
125+
126+
// createOrUpdateRecord creates or updates a record, either by updating existing record or creating new one.
127+
func (p *Provider) createOrUpdateRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
128+
if len(record.ID) == 0 {
129+
return p.createRecord(ctx, zone, record)
130+
}
131+
132+
return p.updateRecord(ctx, zone, record)
133+
}
134+
135+
// updateRecord updates a record
136+
func (p *Provider) updateRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
137+
p.client.mutex.Lock()
138+
defer p.client.mutex.Unlock()
139+
140+
if err := p.setupClient(); err != nil {
141+
return libdns.Record{}, err
142+
}
143+
144+
var nzr OvhDomainZoneRecord
145+
if err := p.client.ovhClient.PutWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%s", zone, record.ID), &OvhDomainZoneRecord{SubDomain: record.Name, Target: record.Value, TTL: int64(record.TTL.Seconds())}, &nzr); err != nil {
146+
return libdns.Record{}, err
147+
}
148+
149+
var uzr OvhDomainZoneRecord
150+
if err := p.client.ovhClient.GetWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%s", zone, record.ID), &uzr); err != nil {
151+
return libdns.Record{}, err
152+
}
153+
154+
updatedRecord := libdns.Record{
155+
ID: strconv.FormatInt(uzr.ID, 10),
156+
Type: uzr.FieldType,
157+
Name: uzr.SubDomain,
158+
Value: strings.TrimRight(strings.TrimLeft(uzr.Target, "\""), "\""),
159+
TTL: time.Duration(uzr.TTL) * time.Second,
160+
}
161+
162+
return updatedRecord, nil
163+
}
164+
165+
// deleteRecord deletes an existing record.
166+
// Regardless of the value of the record, if the name and type match, the record will be deleted.
167+
func (p *Provider) deleteRecord(ctx context.Context, zone string, record libdns.Record) (libdns.Record, error) {
168+
p.client.mutex.Lock()
169+
defer p.client.mutex.Unlock()
170+
171+
if err := p.setupClient(); err != nil {
172+
return libdns.Record{}, err
173+
}
174+
175+
if err := p.client.ovhClient.DeleteWithContext(ctx, fmt.Sprintf("/domain/zone/%s/record/%s", zone, record.ID), nil); err != nil {
176+
return libdns.Record{}, err
177+
}
178+
179+
return record, nil
180+
}
181+
182+
// refresh trigger a reload of the DNS zone.
183+
// It must be called after appending, setting or deleting any record
184+
func (p *Provider) refresh(ctx context.Context, zone string) (error) {
185+
p.client.mutex.Lock()
186+
defer p.client.mutex.Unlock()
187+
188+
if err := p.setupClient(); err != nil {
189+
return err
190+
}
191+
192+
if err := p.client.ovhClient.PostWithContext(ctx, fmt.Sprintf("/domain/zone/%s/refresh", zone), nil, nil); err != nil {
193+
return err
194+
}
195+
196+
return nil
197+
}
198+
199+
// unFQDN trims any trailing "." from fqdn. OVH's API does not use FQDNs.
200+
func unFQDN(fqdn string) string {
201+
return strings.TrimSuffix(fqdn, ".")
202+
}
203+
204+
// normalizeRecordName remove absolute record name
205+
func normalizeRecordName(recordName string, zone string) string {
206+
normalized := unFQDN(recordName)
207+
normalized = strings.TrimSuffix(normalized, unFQDN(zone))
208+
return unFQDN(normalized)
209+
}
210+

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/libdns/ovh
2+
3+
go 1.14
4+
5+
require (
6+
github.com/libdns/libdns v0.2.1
7+
github.com/ovh/go-ovh v1.1.0
8+
)

go.sum

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
2+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
3+
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
4+
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
5+
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
6+
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
7+
github.com/ovh/go-ovh v1.1.0 h1:bHXZmw8nTgZin4Nv7JuaLs0KG5x54EQR7migYTd1zrk=
8+
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
9+
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
10+
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
11+
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
12+
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
13+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
14+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
15+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
16+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
17+
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
18+
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
19+
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

0 commit comments

Comments
 (0)