sigs.k8s.io/external-dns@v0.14.1/provider/ns1/ns1.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package ns1 18 19 import ( 20 "context" 21 "crypto/tls" 22 "fmt" 23 "net/http" 24 "os" 25 "strings" 26 27 log "github.com/sirupsen/logrus" 28 api "gopkg.in/ns1/ns1-go.v2/rest" 29 "gopkg.in/ns1/ns1-go.v2/rest/model/dns" 30 31 "sigs.k8s.io/external-dns/endpoint" 32 "sigs.k8s.io/external-dns/plan" 33 "sigs.k8s.io/external-dns/provider" 34 ) 35 36 const ( 37 // ns1Create is a ChangeAction enum value 38 ns1Create = "CREATE" 39 // ns1Delete is a ChangeAction enum value 40 ns1Delete = "DELETE" 41 // ns1Update is a ChangeAction enum value 42 ns1Update = "UPDATE" 43 // ns1DefaultTTL is the default ttl for ttls that are not set 44 ns1DefaultTTL = 10 45 ) 46 47 // NS1DomainClient is a subset of the NS1 API the the provider uses, to ease testing 48 type NS1DomainClient interface { 49 CreateRecord(r *dns.Record) (*http.Response, error) 50 DeleteRecord(zone string, domain string, t string) (*http.Response, error) 51 UpdateRecord(r *dns.Record) (*http.Response, error) 52 GetZone(zone string) (*dns.Zone, *http.Response, error) 53 ListZones() ([]*dns.Zone, *http.Response, error) 54 } 55 56 // NS1DomainService wraps the API and fulfills the NS1DomainClient interface 57 type NS1DomainService struct { 58 service *api.Client 59 } 60 61 // CreateRecord wraps the Create method of the API's Record service 62 func (n NS1DomainService) CreateRecord(r *dns.Record) (*http.Response, error) { 63 return n.service.Records.Create(r) 64 } 65 66 // DeleteRecord wraps the Delete method of the API's Record service 67 func (n NS1DomainService) DeleteRecord(zone string, domain string, t string) (*http.Response, error) { 68 return n.service.Records.Delete(zone, domain, t) 69 } 70 71 // UpdateRecord wraps the Update method of the API's Record service 72 func (n NS1DomainService) UpdateRecord(r *dns.Record) (*http.Response, error) { 73 return n.service.Records.Update(r) 74 } 75 76 // GetZone wraps the Get method of the API's Zones service 77 func (n NS1DomainService) GetZone(zone string) (*dns.Zone, *http.Response, error) { 78 return n.service.Zones.Get(zone, true) 79 } 80 81 // ListZones wraps the List method of the API's Zones service 82 func (n NS1DomainService) ListZones() ([]*dns.Zone, *http.Response, error) { 83 return n.service.Zones.List() 84 } 85 86 // NS1Config passes cli args to the NS1Provider 87 type NS1Config struct { 88 DomainFilter endpoint.DomainFilter 89 ZoneIDFilter provider.ZoneIDFilter 90 NS1Endpoint string 91 NS1IgnoreSSL bool 92 DryRun bool 93 MinTTLSeconds int 94 } 95 96 // NS1Provider is the NS1 provider 97 type NS1Provider struct { 98 provider.BaseProvider 99 client NS1DomainClient 100 domainFilter endpoint.DomainFilter 101 zoneIDFilter provider.ZoneIDFilter 102 dryRun bool 103 minTTLSeconds int 104 } 105 106 // NewNS1Provider creates a new NS1 Provider 107 func NewNS1Provider(config NS1Config) (*NS1Provider, error) { 108 return newNS1ProviderWithHTTPClient(config, http.DefaultClient) 109 } 110 111 func newNS1ProviderWithHTTPClient(config NS1Config, client *http.Client) (*NS1Provider, error) { 112 token, ok := os.LookupEnv("NS1_APIKEY") 113 if !ok { 114 return nil, fmt.Errorf("NS1_APIKEY environment variable is not set") 115 } 116 clientArgs := []func(*api.Client){api.SetAPIKey(token)} 117 if config.NS1Endpoint != "" { 118 log.Infof("ns1-endpoint flag is set, targeting endpoint at %s", config.NS1Endpoint) 119 clientArgs = append(clientArgs, api.SetEndpoint(config.NS1Endpoint)) 120 } 121 122 if config.NS1IgnoreSSL { 123 log.Info("ns1-ignoressl flag is True, skipping SSL verification") 124 defaultTransport := http.DefaultTransport.(*http.Transport) 125 tr := &http.Transport{ 126 Proxy: defaultTransport.Proxy, 127 DialContext: defaultTransport.DialContext, 128 MaxIdleConns: defaultTransport.MaxIdleConns, 129 IdleConnTimeout: defaultTransport.IdleConnTimeout, 130 ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout, 131 TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout, 132 TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 133 } 134 client.Transport = tr 135 } 136 137 apiClient := api.NewClient(client, clientArgs...) 138 139 provider := &NS1Provider{ 140 client: NS1DomainService{apiClient}, 141 domainFilter: config.DomainFilter, 142 zoneIDFilter: config.ZoneIDFilter, 143 minTTLSeconds: config.MinTTLSeconds, 144 } 145 return provider, nil 146 } 147 148 // Records returns the endpoints this provider knows about 149 func (p *NS1Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { 150 zones, err := p.zonesFiltered() 151 if err != nil { 152 return nil, err 153 } 154 155 var endpoints []*endpoint.Endpoint 156 157 for _, zone := range zones { 158 // TODO handle Header Codes 159 zoneData, _, err := p.client.GetZone(zone.String()) 160 if err != nil { 161 return nil, err 162 } 163 164 for _, record := range zoneData.Records { 165 if provider.SupportedRecordType(record.Type) { 166 endpoints = append(endpoints, endpoint.NewEndpointWithTTL( 167 record.Domain, 168 record.Type, 169 endpoint.TTL(record.TTL), 170 record.ShortAns..., 171 ), 172 ) 173 } 174 } 175 } 176 177 return endpoints, nil 178 } 179 180 // ns1BuildRecord returns a dns.Record for a change set 181 func (p *NS1Provider) ns1BuildRecord(zoneName string, change *ns1Change) *dns.Record { 182 record := dns.NewRecord(zoneName, change.Endpoint.DNSName, change.Endpoint.RecordType, map[string]string{}, []string{}) 183 for _, v := range change.Endpoint.Targets { 184 record.AddAnswer(dns.NewAnswer(strings.Split(v, " "))) 185 } 186 // set default ttl, but respect minTTLSeconds 187 ttl := ns1DefaultTTL 188 if p.minTTLSeconds > ttl { 189 ttl = p.minTTLSeconds 190 } 191 if change.Endpoint.RecordTTL.IsConfigured() { 192 ttl = int(change.Endpoint.RecordTTL) 193 } 194 record.TTL = ttl 195 196 return record 197 } 198 199 // ns1SubmitChanges takes an array of changes and sends them to NS1 200 func (p *NS1Provider) ns1SubmitChanges(changes []*ns1Change) error { 201 // return early if there is nothing to change 202 if len(changes) == 0 { 203 return nil 204 } 205 206 zones, err := p.zonesFiltered() 207 if err != nil { 208 return err 209 } 210 211 // separate into per-zone change sets to be passed to the API. 212 changesByZone := ns1ChangesByZone(zones, changes) 213 for zoneName, changes := range changesByZone { 214 for _, change := range changes { 215 record := p.ns1BuildRecord(zoneName, change) 216 logFields := log.Fields{ 217 "record": record.Domain, 218 "type": record.Type, 219 "ttl": record.TTL, 220 "action": change.Action, 221 "zone": zoneName, 222 } 223 224 log.WithFields(logFields).Info("Changing record.") 225 226 if p.dryRun { 227 continue 228 } 229 230 switch change.Action { 231 case ns1Create: 232 _, err := p.client.CreateRecord(record) 233 if err != nil { 234 return err 235 } 236 case ns1Delete: 237 _, err := p.client.DeleteRecord(zoneName, record.Domain, record.Type) 238 if err != nil { 239 return err 240 } 241 case ns1Update: 242 _, err := p.client.UpdateRecord(record) 243 if err != nil { 244 return err 245 } 246 } 247 } 248 } 249 return nil 250 } 251 252 // Zones returns the list of hosted zones. 253 func (p *NS1Provider) zonesFiltered() ([]*dns.Zone, error) { 254 // TODO handle Header Codes 255 zones, _, err := p.client.ListZones() 256 if err != nil { 257 return nil, err 258 } 259 260 toReturn := []*dns.Zone{} 261 262 for _, z := range zones { 263 if p.domainFilter.Match(z.Zone) && p.zoneIDFilter.Match(z.ID) { 264 toReturn = append(toReturn, z) 265 log.Debugf("Matched %s", z.Zone) 266 } else { 267 log.Debugf("Filtered %s", z.Zone) 268 } 269 } 270 271 return toReturn, nil 272 } 273 274 // ns1Change differentiates between ChangeActions 275 type ns1Change struct { 276 Action string 277 Endpoint *endpoint.Endpoint 278 } 279 280 // ApplyChanges applies a given set of changes in a given zone. 281 func (p *NS1Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { 282 combinedChanges := make([]*ns1Change, 0, len(changes.Create)+len(changes.UpdateNew)+len(changes.Delete)) 283 284 combinedChanges = append(combinedChanges, newNS1Changes(ns1Create, changes.Create)...) 285 combinedChanges = append(combinedChanges, newNS1Changes(ns1Update, changes.UpdateNew)...) 286 combinedChanges = append(combinedChanges, newNS1Changes(ns1Delete, changes.Delete)...) 287 288 return p.ns1SubmitChanges(combinedChanges) 289 } 290 291 // newNS1Changes returns a collection of Changes based on the given records and action. 292 func newNS1Changes(action string, endpoints []*endpoint.Endpoint) []*ns1Change { 293 changes := make([]*ns1Change, 0, len(endpoints)) 294 295 for _, endpoint := range endpoints { 296 changes = append(changes, &ns1Change{ 297 Action: action, 298 Endpoint: endpoint, 299 }, 300 ) 301 } 302 303 return changes 304 } 305 306 // ns1ChangesByZone separates a multi-zone change into a single change per zone. 307 func ns1ChangesByZone(zones []*dns.Zone, changeSets []*ns1Change) map[string][]*ns1Change { 308 changes := make(map[string][]*ns1Change) 309 zoneNameIDMapper := provider.ZoneIDName{} 310 for _, z := range zones { 311 zoneNameIDMapper.Add(z.Zone, z.Zone) 312 changes[z.Zone] = []*ns1Change{} 313 } 314 315 for _, c := range changeSets { 316 zone, _ := zoneNameIDMapper.FindZone(c.Endpoint.DNSName) 317 if zone == "" { 318 log.Debugf("Skipping record %s because no hosted zone matching record DNS Name was detected", c.Endpoint.DNSName) 319 continue 320 } 321 changes[zone] = append(changes[zone], c) 322 } 323 324 return changes 325 }