1
0
mirror of https://github.com/StackExchange/dnscontrol.git synced 2024-05-11 05:55:12 +00:00

NEW PROVIDER: Oracle Cloud (#1021)

* feat: add Oracle provider

* fix ALIAS and NS tests

* return... else if -> return... if

* fix assignment

* remove extraneous blank lines

Co-authored-by: Tom Limoncelli <tlimoncelli@stackoverflow.com>
This commit is contained in:
Nick Gregory
2021-01-24 15:35:12 -05:00
committed by GitHub
parent 4a337ec8d8
commit 945ffb7e80
10 changed files with 423 additions and 1 deletions

1
OWNERS
View File

@@ -21,6 +21,7 @@ providers/namecheap @captncraig
# providers/namedotcom
providers/netcup @kordianbruck
providers/ns1 @captncraig
providers/oracle @kallsyms
# providers/route53
# providers/softlayer
providers/vultr @pgaskin

View File

@@ -42,6 +42,7 @@ Currently supported DNS providers:
- OVH
- OctoDNS
- OpenSRS
- Oracle Cloud
- PowerDNS
- SoftLayer
- Vultr

43
docs/_providers/oracle.md Normal file
View File

@@ -0,0 +1,43 @@
---
name: Oracle Cloud
title: Oracle Cloud Provider
layout: default
jsId: ORACLE
---
# Oracle Cloud Provider
## Configuration
Create an API key through the Oracle Cloud portal, and provide the user OCID, tenancy OCID, key fingerprint, region, and the contents of the private key.
The OCID of the compartment DNS resources should be put in can also optionally be provided.
{% highlight json %}
{
"oracle": {
"user_ocid": "$ORACLE_USER_OCID",
"tenancy_ocid": "$ORACLE_TENANCY_OCID",
"fingerprint": "$ORACLE_FINGERPRINT",
"region": "$ORACLE_REGION",
"private_key": "$ORACLE_PRIVATE_KEY",
"compartment": "$ORACLE_COMPARTMENT"
},
}
{% endhighlight %}
## Metadata
This provider does not recognize any special metadata fields unique to Oracle Cloud.
## Usage
Example Javascript:
{% highlight js %}
var REG_NONE = NewRegistrar('none', 'NONE')
var ORACLE = NewDnsProvider("oracle", "ORACLE");
D("example.tld", REG_NONE, DnsProvider(ORACLE),
NAMESERVER_TTL(86400),
A("test","1.2.3.4")
);
{% endhighlight %}

View File

@@ -91,6 +91,7 @@ Maintainers of contributed providers:
* `NS1` @captncraig
* `OCTODNS` @TomOnTime
* `OPENSRS` @pierre-emmanuelJ
* `ORACLE` @kallsyms
* `OVH` @masterzen
* `POWERDNS` @jpbede
* `SOFTLAYER`@jamielennox

1
go.mod
View File

@@ -47,6 +47,7 @@ require (
github.com/mjibson/esc v0.2.0
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04
github.com/nrdcg/goinwx v0.8.1
github.com/oracle/oci-go-sdk/v32 v32.0.0
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014
github.com/philhug/opensrs-go v0.0.0-20171126225031-9dfa7433020d
github.com/pierrec/lz4 v2.6.0+incompatible // indirect

4
go.sum
View File

@@ -369,6 +369,10 @@ github.com/nrdcg/goinwx v0.8.1 h1:20EQ/JaGFnSKwiDH2JzjIpicffl3cPk6imJBDqVBVtU=
github.com/nrdcg/goinwx v0.8.1/go.mod h1:tILVc10gieBp/5PMvbcYeXM6pVQ+c9jxDZnpaR1UW7c=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/oracle/oci-go-sdk v1.8.0 h1:4SO45bKV0I3/Mn1os3ANDZmV0eSE5z5CLdSUIkxtyzs=
github.com/oracle/oci-go-sdk v24.3.0+incompatible h1:x4mcfb4agelf1O4/1/auGlZ1lr97jXRSSN5MxTgG/zU=
github.com/oracle/oci-go-sdk/v32 v32.0.0 h1:SSbzrQO3WRcPJEZ8+b3SFPYsPtkFM96clqrp03lrwbU=
github.com/oracle/oci-go-sdk/v32 v32.0.0/go.mod h1:aZc4jC59IuNP3cr5y1nj555QvwojMX2nMJaBiozuuEs=
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014 h1:37VE5TYj2m/FLA9SNr4z0+A0JefvTmR60Zwf8XSEV7c=
github.com/ovh/go-ovh v0.0.0-20181109152953-ba5adb4cf014/go.mod h1:joRatxRJaZBsY3JAOEMcoOp05CnZzsx4scTxi95DHyQ=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=

View File

@@ -677,7 +677,7 @@ func makeTests(t *testing.T) []*TestGroup {
// Netcup: NS records not currently supported.
tc("NS for subdomain", ns("xyz", "ns2.foo.com.")),
tc("Dual NS for subdomain", ns("xyz", "ns2.foo.com."), ns("xyz", "ns1.foo.com.")),
tc("NS Record pointing to @", ns("foo", "**current-domain**")),
tc("NS Record pointing to @", a("@", "1.2.3.4"), ns("foo", "**current-domain**")),
),
testgroup("IGNORE_NAME function",

View File

@@ -126,6 +126,15 @@
"directory": "config",
"domain": "example.com"
},
"ORACLE": {
"user_ocid": "$ORACLE_USER_OCID",
"tenancy_ocid": "$ORACLE_TENANCY_OCID",
"fingerprint": "$ORACLE_FINGERPRINT",
"region": "$ORACLE_REGION",
"private_key": "$ORACLE_PRIVATE_KEY",
"compartment": "$ORACLE_COMPARTMENT",
"domain": "$ORACLE_DOMAIN"
},
"OVH": {
"app-key": "$OVH_APP_KEY",
"app-secret-key": "$OVH_APP_SECRET_KEY",

View File

@@ -30,6 +30,7 @@ import (
_ "github.com/StackExchange/dnscontrol/v3/providers/ns1"
_ "github.com/StackExchange/dnscontrol/v3/providers/octodns"
_ "github.com/StackExchange/dnscontrol/v3/providers/opensrs"
_ "github.com/StackExchange/dnscontrol/v3/providers/oracle"
_ "github.com/StackExchange/dnscontrol/v3/providers/ovh"
_ "github.com/StackExchange/dnscontrol/v3/providers/powerdns"
_ "github.com/StackExchange/dnscontrol/v3/providers/route53"

View File

@@ -0,0 +1,361 @@
package oracle
import (
"context"
"encoding/json"
"strings"
"time"
"github.com/oracle/oci-go-sdk/v32/dns"
"github.com/oracle/oci-go-sdk/v32/common"
"github.com/oracle/oci-go-sdk/v32/example/helpers"
"github.com/StackExchange/dnscontrol/v3/models"
"github.com/StackExchange/dnscontrol/v3/pkg/diff"
"github.com/StackExchange/dnscontrol/v3/pkg/printer"
"github.com/StackExchange/dnscontrol/v3/providers"
)
var features = providers.DocumentationNotes{
providers.DocCreateDomains: providers.Can(),
providers.DocDualHost: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(),
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Can(),
providers.CanUseCAA: providers.Can(),
providers.CanUseDS: providers.Cannot(), // should be supported, but getting 500s in tests
providers.CanUseNAPTR: providers.Can(),
providers.CanUsePTR: providers.Can(),
providers.CanUseSRV: providers.Can(),
providers.CanUseSSHFP: providers.Can(),
providers.CanUseTLSA: providers.Can(),
providers.CanUseTXTMulti: providers.Can(),
}
func init() {
providers.RegisterDomainServiceProviderType("ORACLE", New, features)
}
type oracleProvider struct {
client dns.DnsClient
compartment string
}
// New creates a new provider for Oracle Cloud DNS
func New(settings map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
client, err := dns.NewDnsClientWithConfigurationProvider(common.NewRawConfigurationProvider(
settings["tenancy_ocid"],
settings["user_ocid"],
settings["region"],
settings["fingerprint"],
settings["private_key"],
nil,
))
if err != nil {
return nil, err
}
return &oracleProvider{
client: client,
compartment: settings["compartment"],
}, nil
}
// ListZones lists the zones on this account.
func (o *oracleProvider) ListZones() ([]string, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
listResp, err := o.client.ListZones(ctx, dns.ListZonesRequest{
CompartmentId: &o.compartment,
})
if err != nil {
return nil, err
}
zones := make([]string, len(listResp.Items))
for i, zone := range listResp.Items {
zones[i] = *zone.Name
}
return zones, nil
}
// EnsureDomainExists creates the domain if it does not exist.
func (o *oracleProvider) EnsureDomainExists(domain string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
getResp, err := o.client.GetZone(ctx, dns.GetZoneRequest{
ZoneNameOrId: &domain,
CompartmentId: &o.compartment,
})
if err == nil {
return nil
}
if err != nil && getResp.RawResponse.StatusCode != 404 {
return err
}
_, err = o.client.CreateZone(ctx, dns.CreateZoneRequest{
CreateZoneDetails: dns.CreateZoneDetails{
CompartmentId: &o.compartment,
Name: &domain,
ZoneType: dns.CreateZoneDetailsZoneTypePrimary,
},
})
if err != nil {
return err
}
// poll until the zone is ready
pollUntilAvailable := func(r common.OCIOperationResponse) bool {
if converted, ok := r.Response.(dns.GetZoneResponse); ok {
return converted.LifecycleState != dns.ZoneLifecycleStateActive
}
return true
}
_, err = o.client.GetZone(ctx, dns.GetZoneRequest{
ZoneNameOrId: &domain,
CompartmentId: &o.compartment,
RequestMetadata: helpers.GetRequestMetadataWithCustomizedRetryPolicy(pollUntilAvailable),
})
return err
}
func (o *oracleProvider) GetNameservers(domain string) ([]*models.Nameserver, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
getResp, err := o.client.GetZone(ctx, dns.GetZoneRequest{
ZoneNameOrId: &domain,
CompartmentId: &o.compartment,
})
if err != nil {
return nil, err
}
nss := make([]string, len(getResp.Zone.Nameservers))
for i, ns := range getResp.Zone.Nameservers {
nss[i] = *ns.Hostname
}
return models.ToNameservers(nss)
}
func (o *oracleProvider) GetZoneRecords(domain string) (models.Records, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
records := models.Records{}
request := dns.GetZoneRecordsRequest{
ZoneNameOrId: &domain,
CompartmentId: &o.compartment,
}
for {
getResp, err := o.client.GetZoneRecords(ctx, request)
if err != nil {
return nil, err
}
for _, record := range getResp.Items {
// Hide SOAs
if *record.Rtype == "SOA" {
continue
}
rc := &models.RecordConfig{
Type: *record.Rtype,
TTL: uint32(*record.Ttl),
Original: record,
}
rc.SetLabelFromFQDN(*record.Domain, domain)
switch rc.Type {
case "ALIAS":
err = rc.SetTarget(*record.Rdata)
default:
err = rc.PopulateFromString(*record.Rtype, *record.Rdata, domain)
}
if err != nil {
return nil, err
}
records = append(records, rc)
}
if getResp.OpcNextPage == nil {
break
}
request.Page = getResp.OpcNextPage
}
return records, nil
}
func (o *oracleProvider) GetDomainCorrections(dc *models.DomainConfig) ([]*models.Correction, error) {
dc, err := dc.Copy()
if err != nil {
return nil, err
}
err = dc.Punycode()
if err != nil {
return nil, err
}
domain := dc.Name
existingRecords, err := o.GetZoneRecords(domain)
if err != nil {
return nil, err
}
// Normalize
models.PostProcessRecords(existingRecords)
filteredNewRecords := models.Records{}
// Ensure we don't emit changes for attempted modification of built-in apex NSs
for _, rec := range dc.Records {
if rec.Type != "NS" {
filteredNewRecords = append(filteredNewRecords, rec)
continue
}
recNS := rec.GetTargetField()
if rec.GetLabel() == "@" && strings.HasSuffix(recNS, "dns.oraclecloud.com.") {
printer.Warnf("Oracle Cloud does not allow changes to built-in apex NS records. Ignoring change to %s...\n", recNS)
continue
}
if rec.TTL != 86400 {
printer.Warnf("Oracle Cloud forces TTL=86400 for NS records. Ignoring configured TTL of %d for %s\n", rec.TTL, recNS)
rec.TTL = 86400
}
filteredNewRecords = append(filteredNewRecords, rec)
}
differ := diff.New(dc)
_, create, dels, modify, err := differ.IncrementalDiff(existingRecords)
if err != nil {
return nil, err
}
/*
Oracle's API doesn't have a way to update an existing record.
You can either update an existing RRSet, Domain (FQDN), or Zone in which you have to supply
the entire desired state, or you can patch specifying ADD/REMOVE actions.
Oracle's API is also increadibly slow, so updating individual RRSets is unbearably slow
for any size zone.
*/
corrections := []*models.Correction{}
if len(create) > 0 {
createRecords := models.Records{}
desc := ""
for _, d := range create {
createRecords = append(createRecords, d.Desired)
desc += d.String() + "\n"
}
desc = desc[:len(desc)-1]
corrections = append(corrections, &models.Correction{
Msg: desc,
F: func() error {
return o.patch(createRecords, nil, domain)
},
})
}
if len(dels) > 0 {
deleteRecords := models.Records{}
desc := ""
for _, d := range dels {
deleteRecords = append(deleteRecords, d.Existing)
desc += d.String() + "\n"
}
desc = desc[:len(desc)-1]
corrections = append(corrections, &models.Correction{
Msg: desc,
F: func() error {
return o.patch(nil, deleteRecords, domain)
},
})
}
if len(modify) > 0 {
createRecords := models.Records{}
deleteRecords := models.Records{}
desc := ""
for _, d := range modify {
createRecords = append(createRecords, d.Desired)
deleteRecords = append(deleteRecords, d.Existing)
desc += d.String() + "\n"
}
desc = desc[:len(desc)-1]
corrections = append(corrections, &models.Correction{
Msg: desc,
F: func() error {
return o.patch(createRecords, deleteRecords, domain)
},
})
}
return corrections, nil
}
func (o *oracleProvider) patch(createRecords, deleteRecords models.Records, domain string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
patchReq := dns.PatchZoneRecordsRequest{
ZoneNameOrId: &domain,
CompartmentId: &o.compartment,
}
ops := make([]dns.RecordOperation, 0, len(createRecords)+len(deleteRecords))
for _, rec := range deleteRecords {
ops = append(ops, convertToRecordOperation(rec, dns.RecordOperationOperationRemove))
}
for _, rec := range createRecords {
ops = append(ops, convertToRecordOperation(rec, dns.RecordOperationOperationAdd))
}
for batchStart := 0; batchStart < len(ops); batchStart += 100 {
batchEnd := batchStart + 100
if batchEnd > len(ops) {
batchEnd = len(ops)
}
patchReq.Items = ops[batchStart:batchEnd]
_, err := o.client.PatchZoneRecords(ctx, patchReq)
if err != nil {
return err
}
}
return nil
}
func convertToRecordOperation(rec *models.RecordConfig, op dns.RecordOperationOperationEnum) dns.RecordOperation {
fqdn := rec.GetLabelFQDN()
rtype := rec.Type
rdata := rec.GetTargetCombined()
ttl := int(rec.TTL)
return dns.RecordOperation{
Domain: &fqdn,
Rtype: &rtype,
Rdata: &rdata,
Ttl: &ttl,
Operation: op,
}
}