google.go 8.59 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

17
package provider
18 19 20 21

import (
	"strings"

22 23
	log "github.com/Sirupsen/logrus"

24
	"golang.org/x/net/context"
25
	"golang.org/x/oauth2/google"
26

27
	"google.golang.org/api/dns/v1"
28
	googleapi "google.golang.org/api/googleapi"
29

30
	"github.com/kubernetes-incubator/external-dns/endpoint"
31 32 33
	"github.com/kubernetes-incubator/external-dns/plan"
)

34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
type managedZonesCreateCallInterface interface {
	Do(opts ...googleapi.CallOption) (*dns.ManagedZone, error)
}

type managedZonesDeleteCallInterface interface {
	Do(opts ...googleapi.CallOption) error
}

type managedZonesListCallInterface interface {
	Pages(ctx context.Context, f func(*dns.ManagedZonesListResponse) error) error
}

type managedZonesServiceInterface interface {
	Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface
	Delete(project string, managedZone string) managedZonesDeleteCallInterface
	List(project string) managedZonesListCallInterface
}

type resourceRecordSetsListCallInterface interface {
	Pages(ctx context.Context, f func(*dns.ResourceRecordSetsListResponse) error) error
}

type resourceRecordSetsClientInterface interface {
	List(project string, managedZone string) resourceRecordSetsListCallInterface
}

type changesCreateCallInterface interface {
	Do(opts ...googleapi.CallOption) (*dns.Change, error)
}

type changesServiceInterface interface {
	Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface
}

type resourceRecordSetsService struct {
69
	service *dns.ResourceRecordSetsService
70 71 72
}

func (r resourceRecordSetsService) List(project string, managedZone string) resourceRecordSetsListCallInterface {
73
	return r.service.List(project, managedZone)
74 75 76
}

type managedZonesService struct {
77
	service *dns.ManagedZonesService
78 79 80
}

func (m managedZonesService) Create(project string, managedzone *dns.ManagedZone) managedZonesCreateCallInterface {
81
	return m.service.Create(project, managedzone)
82 83 84
}

func (m managedZonesService) Delete(project string, managedZone string) managedZonesDeleteCallInterface {
85
	return m.service.Delete(project, managedZone)
86 87 88
}

func (m managedZonesService) List(project string) managedZonesListCallInterface {
89
	return m.service.List(project)
90 91 92
}

type changesService struct {
93
	service *dns.ChangesService
94 95 96
}

func (c changesService) Create(project string, managedZone string, change *dns.Change) changesCreateCallInterface {
97
	return c.service.Create(project, managedZone, change)
98 99
}

100
// googleProvider is an implementation of Provider for Google CloudDNS.
101
type googleProvider struct {
102
	// The Google project to work in
103
	project string
104
	// Enabled dry-run will print any modifying actions rather than execute them.
105
	dryRun bool
106
	// A client for managing resource record sets
107
	resourceRecordSetsClient resourceRecordSetsClientInterface
108
	// A client for managing hosted zones
109
	managedZonesClient managedZonesServiceInterface
110
	// A client for managing change sets
111 112 113
	changesClient changesServiceInterface
}

114 115
// NewGoogleProvider initializes a new Google CloudDNS based Provider.
func NewGoogleProvider(project string, dryRun bool) (Provider, error) {
116 117 118 119 120 121 122 123 124 125
	gcloud, err := google.DefaultClient(context.TODO(), dns.NdevClouddnsReadwriteScope)
	if err != nil {
		return nil, err
	}

	dnsClient, err := dns.New(gcloud)
	if err != nil {
		return nil, err
	}

126
	provider := &googleProvider{
127 128 129 130 131
		project: project,
		dryRun:  dryRun,
		resourceRecordSetsClient: resourceRecordSetsService{dnsClient.ResourceRecordSets},
		managedZonesClient:       managedZonesService{dnsClient.ManagedZones},
		changesClient:            changesService{dnsClient.Changes},
132 133 134
	}

	return provider, nil
135 136 137
}

// Zones returns the list of hosted zones.
138
func (p *googleProvider) Zones() (zones []*dns.ManagedZone, _ error) {
139 140 141 142 143 144
	f := func(resp *dns.ManagedZonesListResponse) error {
		// each page is processed sequentially, no need for a mutex here.
		zones = append(zones, resp.ManagedZones...)
		return nil
	}

145
	err := p.managedZonesClient.List(p.project).Pages(context.TODO(), f)
146 147 148 149
	if err != nil {
		return nil, err
	}

150
	return zones, nil
151 152 153
}

// CreateZone creates a hosted zone given a name.
154
func (p *googleProvider) CreateZone(name, domain string) error {
155 156 157
	zone := &dns.ManagedZone{
		Name:        name,
		DnsName:     domain,
158
		Description: "Automatically managed zone by kubernetes.io/external-dns",
159 160
	}

161
	_, err := p.managedZonesClient.Create(p.project, zone).Do()
162 163 164 165 166 167 168 169
	if err != nil {
		return err
	}

	return nil
}

// DeleteZone deletes a hosted zone given a name.
170 171
func (p *googleProvider) DeleteZone(name string) error {
	err := p.managedZonesClient.Delete(p.project, name).Do()
172 173 174 175 176 177 178 179 180
	if err != nil {
		if !isNotFound(err) {
			return err
		}
	}

	return nil
}

181
// Records returns the list of A records in a given hosted zone.
182
func (p *googleProvider) Records(zone string) (endpoints []endpoint.Endpoint, _ error) {
183 184 185 186 187 188 189 190 191 192 193 194 195
	f := func(resp *dns.ResourceRecordSetsListResponse) error {
		for _, r := range resp.Rrsets {
			if r.Type != "A" {
				continue
			}

			for _, rr := range r.Rrdatas {
				// each page is processed sequentially, no need for a mutex here.
				endpoints = append(endpoints, endpoint.Endpoint{
					DNSName: r.Name,
					Target:  rr,
				})
			}
196 197
		}

198 199 200
		return nil
	}

201
	err := p.resourceRecordSetsClient.List(p.project, zone).Pages(context.TODO(), f)
202 203
	if err != nil {
		return nil, err
204 205 206
	}

	return endpoints, nil
207 208
}

209
// CreateRecords creates a given set of DNS records in the given hosted zone.
210
func (p *googleProvider) CreateRecords(zone string, endpoints []endpoint.Endpoint) error {
211
	change := &dns.Change{}
212

213
	change.Additions = append(change.Additions, newRecords(endpoints)...)
214

215 216
	return p.submitChange(zone, change)
}
217

218
// UpdateRecords updates a given set of old records to a new set of records in a given hosted zone.
219
func (p *googleProvider) UpdateRecords(zone string, records, oldRecords []endpoint.Endpoint) error {
220
	change := &dns.Change{}
221

222 223
	change.Additions = append(change.Additions, newRecords(records)...)
	change.Deletions = append(change.Deletions, newRecords(oldRecords)...)
224

225
	return p.submitChange(zone, change)
226 227
}

228
// DeleteRecords deletes a given set of DNS records in a given zone.
229
func (p *googleProvider) DeleteRecords(zone string, endpoints []endpoint.Endpoint) error {
230
	change := &dns.Change{}
231

232
	change.Deletions = append(change.Deletions, newRecords(endpoints)...)
233

234 235
	return p.submitChange(zone, change)
}
236

237
// ApplyChanges applies a given set of changes in a given zone.
238
func (p *googleProvider) ApplyChanges(zone string, changes *plan.Changes) error {
239
	change := &dns.Change{}
240

241
	change.Additions = append(change.Additions, newRecords(changes.Create)...)
242

243 244
	change.Additions = append(change.Additions, newRecords(changes.UpdateNew)...)
	change.Deletions = append(change.Deletions, newRecords(changes.UpdateOld)...)
245

246 247 248
	change.Deletions = append(change.Deletions, newRecords(changes.Delete)...)

	return p.submitChange(zone, change)
249 250
}

251
// submitChange takes a zone and a Change and sends it to Google.
252 253
func (p *googleProvider) submitChange(zone string, change *dns.Change) error {
	if p.dryRun {
254 255 256
		for _, del := range change.Deletions {
			log.Infof("Del records: %s %s %s", del.Name, del.Type, del.Rrdatas)
		}
257 258 259
		for _, add := range change.Additions {
			log.Infof("Add records: %s %s %s", add.Name, add.Type, add.Rrdatas)
		}
260 261

		return nil
262 263
	}

264
	if len(change.Additions) == 0 && len(change.Deletions) == 0 {
265 266 267
		return nil
	}

268
	_, err := p.changesClient.Create(p.project, zone, change).Do()
269 270 271 272 273 274 275 276 277
	if err != nil {
		if !isNotFound(err) {
			return err
		}
	}

	return nil
}

278 279 280
// newRecords returns a collection of RecordSets based on the given endpoints.
func newRecords(endpoints []endpoint.Endpoint) []*dns.ResourceRecordSet {
	records := make([]*dns.ResourceRecordSet, len(endpoints))
281

282 283
	for i, endpoint := range endpoints {
		records[i] = newRecord(endpoint)
284 285
	}

286 287
	return records
}
288

289 290 291 292 293 294 295 296
// newRecord returns a RecordSet based on the given endpoint.
func newRecord(endpoint endpoint.Endpoint) *dns.ResourceRecordSet {
	return &dns.ResourceRecordSet{
		Name:    endpoint.DNSName,
		Rrdatas: []string{endpoint.Target},
		Ttl:     300,
		Type:    "A",
	}
297 298
}

299 300 301 302
// isNotFound returns true if a given error is due to a resource not being found.
func isNotFound(err error) bool {
	return strings.Contains(err.Error(), "notFound")
}