Skip to content

Commit

Permalink
Add managed DNS record support for API endpoint (#232)
Browse files Browse the repository at this point in the history
* Add managed DNS record support for API endpoint

* Add DNS propagation check

* Address code review comments

* Disallow @ and * DNS names

* Fix copyright header
  • Loading branch information
gottwald authored Feb 17, 2021
1 parent deb5e64 commit d66583b
Show file tree
Hide file tree
Showing 12 changed files with 494 additions and 6 deletions.
25 changes: 22 additions & 3 deletions api/v1alpha3/docluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,46 @@ const (

// DOClusterSpec defines the desired state of DOCluster.
type DOClusterSpec struct {
// The DigitalOcean Region the cluster lives in.
// It must be one of available region on DigitalOcean. See https://developers.digitalocean.com/documentation/v2/#list-all-regions
// The DigitalOcean Region the cluster lives in. It must be one of available
// region on DigitalOcean. See
// https://developers.digitalocean.com/documentation/v2/#list-all-regions
Region string `json:"region"`
// Network configurations
// +optional
Network DONetwork `json:"network,omitempty"`
// ControlPlaneEndpoint represents the endpoint used to communicate with the control plane.
// ControlPlaneEndpoint represents the endpoint used to communicate with the
// control plane. If ControlPlaneDNS is unset, the DO load-balancer IP
// of the Kubernetes API Server is used.
// +optional
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
// ControlPlaneDNS is a managed DNS name that points to the load-balancer
// IP used for the ControlPlaneEndpoint.
// +optional
ControlPlaneDNS *DOControlPlaneDNS `json:"controlPlaneDNS"`
}

// DOClusterStatus defines the observed state of DOCluster.
type DOClusterStatus struct {
// Ready denotes that the cluster (infrastructure) is ready.
// +optional
Ready bool `json:"ready"`
// ControlPlaneDNSRecordReady denotes that the DNS record is ready and
// propagated to the DO DNS servers.
// +optional
ControlPlaneDNSRecordReady bool `json:"controlPlaneDNSRecordReady,omitempty"`
// Network encapsulates all things related to DigitalOcean network.
// +optional
Network DONetworkResource `json:"network,omitempty"`
}

type DOControlPlaneDNS struct {
// Domain is the DO domain that this record should live in.
// It must be pre-existing in your DO account.
Domain string `json:"domain"`
// Name is the DNS short name of the record (non-FQDN)
Name string `json:"name"`
}

// +kubebuilder:object:root=true
// +kubebuilder:resource:path=doclusters,scope=Namespaced,categories=cluster-api
// +kubebuilder:storageversion
Expand Down
22 changes: 21 additions & 1 deletion api/v1alpha3/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cloud/scope/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ type DOClients struct {
Images godo.ImagesService
Keys godo.KeysService
LoadBalancers godo.LoadBalancersService
Domains godo.DomainsService
}
9 changes: 9 additions & 0 deletions cloud/scope/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) {
params.DOClients.LoadBalancers = session.LoadBalancers
}

if params.DOClients.Domains == nil {
params.DOClients.Domains = session.Domains
}

helper, err := patch.NewHelper(params.DOCluster, params.Client)
if err != nil {
return nil, errors.Wrap(err, "failed to init patch helper")
Expand Down Expand Up @@ -142,6 +146,11 @@ func (s *ClusterScope) SetReady() {
s.DOCluster.Status.Ready = true
}

// SetControlPlaneDNSRecordReady sets the DOCluster ControlPlaneDNSRecordReady Status.
func (s *ClusterScope) SetControlPlaneDNSRecordReady(ready bool) {
s.DOCluster.Status.ControlPlaneDNSRecordReady = ready
}

// SetControlPlaneEndpoint sets the DOCluster status APIEndpoints.
func (s *ClusterScope) SetControlPlaneEndpoint(apiEndpoint clusterv1.APIEndpoint) {
s.DOCluster.Spec.ControlPlaneEndpoint = apiEndpoint
Expand Down
61 changes: 61 additions & 0 deletions cloud/services/networking/dns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
Copyright 2021 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.
*/

package networking

import (
"github.com/miekg/dns"
"github.com/pkg/errors"
)

type DNSQuerier interface {
Query(servers []string, msg *dns.Msg) (*dns.Msg, error)
LocalQuery(msg *dns.Msg) (*dns.Msg, error)
}

type DNSResolver struct {
config *dns.ClientConfig
client *dns.Client
}

func NewDNSResolver() (*DNSResolver, error) {
dnsConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, errors.Wrap(err, "unable to get DNS config")
}
sq := &DNSResolver{
config: dnsConfig,
client: new(dns.Client),
}
return sq, nil
}

func (dr *DNSResolver) Query(servers []string, msg *dns.Msg) (*dns.Msg, error) {
for _, server := range servers {
r, _, err := dr.client.Exchange(msg, server+":"+dr.config.Port)
if err != nil {
return nil, err
}
if r == nil || r.Rcode == dns.RcodeNameError || r.Rcode == dns.RcodeSuccess {
return r, err
}
}
return nil, errors.New("No name server to answer the question")
}

func (dr *DNSResolver) LocalQuery(msg *dns.Msg) (*dns.Msg, error) {
return dr.Query(dr.config.Servers, msg)
}
78 changes: 78 additions & 0 deletions cloud/services/networking/domains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Copyright 2021 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.
*/

package networking

import (
"fmt"
"net/http"

"github.com/digitalocean/godo"
)

// GetDomainRecord retrieves a single domain record from DO.
func (s *Service) GetDomainRecord(domain, name, rType string) (*godo.DomainRecord, error) {
fqdn := fmt.Sprintf("%s.%s", name, domain)
records, resp, err := s.scope.Domains.RecordsByTypeAndName(s.ctx, domain, rType, fqdn, nil)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
return nil, nil
}
return nil, err
}
switch len(records) {
case 0:
return nil, nil
case 1:
return &records[0], nil
default:
return nil, fmt.Errorf("multiple DNS records (%d) found for '%s.%s' type %s",
len(records), name, domain, rType)
}
}

// UpsertDomainRecord creates or updates a DO domain record.
func (s *Service) UpsertDomainRecord(domain, name, rType, data string) error {
record, err := s.GetDomainRecord(domain, name, rType)
if err != nil {
return fmt.Errorf("unable to get current DNS record from API: %s", err)
}
recordReq := &godo.DomainRecordEditRequest{
Type: rType,
Name: name,
Data: data,
TTL: 30,
}
if record == nil {
_, _, err = s.scope.Domains.CreateRecord(s.ctx, domain, recordReq)
} else {
_, _, err = s.scope.Domains.EditRecord(s.ctx, domain, record.ID, recordReq)
}
return err
}

// DeleteDomainRecord removes a DO domain record.
func (s *Service) DeleteDomainRecord(domain, name, rType string) error {
record, err := s.GetDomainRecord(domain, name, rType)
if err != nil {
return fmt.Errorf("unable to get current DNS record from API: %s", err)
}
if record == nil {
return nil
}
_, err = s.scope.Domains.DeleteRecord(s.ctx, domain, record.ID)
return err
}
22 changes: 21 additions & 1 deletion config/crd/bases/infrastructure.cluster.x-k8s.io_doclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,9 +183,25 @@ spec:
spec:
description: DOClusterSpec defines the desired state of DOCluster.
properties:
controlPlaneDNS:
description: ControlPlaneDNS is a managed DNS name that points to
the load-balancer IP used for the ControlPlaneEndpoint.
properties:
domain:
description: Domain is the DO domain that this record should live
in. It must be pre-existing in your DO account.
type: string
name:
description: Name is the DNS short name of the record (non-FQDN)
type: string
required:
- domain
- name
type: object
controlPlaneEndpoint:
description: ControlPlaneEndpoint represents the endpoint used to
communicate with the control plane.
communicate with the control plane. If ControlPlaneDNS is unset,
the DO load-balancer IP of the Kubernetes API Server is used.
properties:
host:
description: The hostname on which the API server is serving.
Expand Down Expand Up @@ -275,6 +291,10 @@ spec:
status:
description: DOClusterStatus defines the observed state of DOCluster.
properties:
controlPlaneDNSRecordReady:
description: ControlPlaneDNSRecordReady denotes that the DNS record
is ready and propagated to the DO DNS servers.
type: boolean
network:
description: Network encapsulates all things related to DigitalOcean
network.
Expand Down
Loading

0 comments on commit d66583b

Please sign in to comment.