Skip to content

Commit d66583b

Browse files
authored
Add managed DNS record support for API endpoint (#232)
* Add managed DNS record support for API endpoint * Add DNS propagation check * Address code review comments * Disallow @ and * DNS names * Fix copyright header
1 parent deb5e64 commit d66583b

File tree

12 files changed

+494
-6
lines changed

12 files changed

+494
-6
lines changed

api/v1alpha3/docluster_types.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,27 +29,46 @@ const (
2929

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

4350
// DOClusterStatus defines the observed state of DOCluster.
4451
type DOClusterStatus struct {
4552
// Ready denotes that the cluster (infrastructure) is ready.
4653
// +optional
4754
Ready bool `json:"ready"`
55+
// ControlPlaneDNSRecordReady denotes that the DNS record is ready and
56+
// propagated to the DO DNS servers.
57+
// +optional
58+
ControlPlaneDNSRecordReady bool `json:"controlPlaneDNSRecordReady,omitempty"`
4859
// Network encapsulates all things related to DigitalOcean network.
4960
// +optional
5061
Network DONetworkResource `json:"network,omitempty"`
5162
}
5263

64+
type DOControlPlaneDNS struct {
65+
// Domain is the DO domain that this record should live in.
66+
// It must be pre-existing in your DO account.
67+
Domain string `json:"domain"`
68+
// Name is the DNS short name of the record (non-FQDN)
69+
Name string `json:"name"`
70+
}
71+
5372
// +kubebuilder:object:root=true
5473
// +kubebuilder:resource:path=doclusters,scope=Namespaced,categories=cluster-api
5574
// +kubebuilder:storageversion

api/v1alpha3/zz_generated.deepcopy.go

Lines changed: 21 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cloud/scope/clients.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ type DOClients struct {
2626
Images godo.ImagesService
2727
Keys godo.KeysService
2828
LoadBalancers godo.LoadBalancersService
29+
Domains godo.DomainsService
2930
}

cloud/scope/cluster.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ func NewClusterScope(params ClusterScopeParams) (*ClusterScope, error) {
8282
params.DOClients.LoadBalancers = session.LoadBalancers
8383
}
8484

85+
if params.DOClients.Domains == nil {
86+
params.DOClients.Domains = session.Domains
87+
}
88+
8589
helper, err := patch.NewHelper(params.DOCluster, params.Client)
8690
if err != nil {
8791
return nil, errors.Wrap(err, "failed to init patch helper")
@@ -142,6 +146,11 @@ func (s *ClusterScope) SetReady() {
142146
s.DOCluster.Status.Ready = true
143147
}
144148

149+
// SetControlPlaneDNSRecordReady sets the DOCluster ControlPlaneDNSRecordReady Status.
150+
func (s *ClusterScope) SetControlPlaneDNSRecordReady(ready bool) {
151+
s.DOCluster.Status.ControlPlaneDNSRecordReady = ready
152+
}
153+
145154
// SetControlPlaneEndpoint sets the DOCluster status APIEndpoints.
146155
func (s *ClusterScope) SetControlPlaneEndpoint(apiEndpoint clusterv1.APIEndpoint) {
147156
s.DOCluster.Spec.ControlPlaneEndpoint = apiEndpoint

cloud/services/networking/dns.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Copyright 2021 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 networking
18+
19+
import (
20+
"github.com/miekg/dns"
21+
"github.com/pkg/errors"
22+
)
23+
24+
type DNSQuerier interface {
25+
Query(servers []string, msg *dns.Msg) (*dns.Msg, error)
26+
LocalQuery(msg *dns.Msg) (*dns.Msg, error)
27+
}
28+
29+
type DNSResolver struct {
30+
config *dns.ClientConfig
31+
client *dns.Client
32+
}
33+
34+
func NewDNSResolver() (*DNSResolver, error) {
35+
dnsConfig, err := dns.ClientConfigFromFile("/etc/resolv.conf")
36+
if err != nil {
37+
return nil, errors.Wrap(err, "unable to get DNS config")
38+
}
39+
sq := &DNSResolver{
40+
config: dnsConfig,
41+
client: new(dns.Client),
42+
}
43+
return sq, nil
44+
}
45+
46+
func (dr *DNSResolver) Query(servers []string, msg *dns.Msg) (*dns.Msg, error) {
47+
for _, server := range servers {
48+
r, _, err := dr.client.Exchange(msg, server+":"+dr.config.Port)
49+
if err != nil {
50+
return nil, err
51+
}
52+
if r == nil || r.Rcode == dns.RcodeNameError || r.Rcode == dns.RcodeSuccess {
53+
return r, err
54+
}
55+
}
56+
return nil, errors.New("No name server to answer the question")
57+
}
58+
59+
func (dr *DNSResolver) LocalQuery(msg *dns.Msg) (*dns.Msg, error) {
60+
return dr.Query(dr.config.Servers, msg)
61+
}

cloud/services/networking/domains.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2021 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 networking
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
23+
"github.com/digitalocean/godo"
24+
)
25+
26+
// GetDomainRecord retrieves a single domain record from DO.
27+
func (s *Service) GetDomainRecord(domain, name, rType string) (*godo.DomainRecord, error) {
28+
fqdn := fmt.Sprintf("%s.%s", name, domain)
29+
records, resp, err := s.scope.Domains.RecordsByTypeAndName(s.ctx, domain, rType, fqdn, nil)
30+
if err != nil {
31+
if resp != nil && resp.StatusCode == http.StatusNotFound {
32+
return nil, nil
33+
}
34+
return nil, err
35+
}
36+
switch len(records) {
37+
case 0:
38+
return nil, nil
39+
case 1:
40+
return &records[0], nil
41+
default:
42+
return nil, fmt.Errorf("multiple DNS records (%d) found for '%s.%s' type %s",
43+
len(records), name, domain, rType)
44+
}
45+
}
46+
47+
// UpsertDomainRecord creates or updates a DO domain record.
48+
func (s *Service) UpsertDomainRecord(domain, name, rType, data string) error {
49+
record, err := s.GetDomainRecord(domain, name, rType)
50+
if err != nil {
51+
return fmt.Errorf("unable to get current DNS record from API: %s", err)
52+
}
53+
recordReq := &godo.DomainRecordEditRequest{
54+
Type: rType,
55+
Name: name,
56+
Data: data,
57+
TTL: 30,
58+
}
59+
if record == nil {
60+
_, _, err = s.scope.Domains.CreateRecord(s.ctx, domain, recordReq)
61+
} else {
62+
_, _, err = s.scope.Domains.EditRecord(s.ctx, domain, record.ID, recordReq)
63+
}
64+
return err
65+
}
66+
67+
// DeleteDomainRecord removes a DO domain record.
68+
func (s *Service) DeleteDomainRecord(domain, name, rType string) error {
69+
record, err := s.GetDomainRecord(domain, name, rType)
70+
if err != nil {
71+
return fmt.Errorf("unable to get current DNS record from API: %s", err)
72+
}
73+
if record == nil {
74+
return nil
75+
}
76+
_, err = s.scope.Domains.DeleteRecord(s.ctx, domain, record.ID)
77+
return err
78+
}

config/crd/bases/infrastructure.cluster.x-k8s.io_doclusters.yaml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,9 +183,25 @@ spec:
183183
spec:
184184
description: DOClusterSpec defines the desired state of DOCluster.
185185
properties:
186+
controlPlaneDNS:
187+
description: ControlPlaneDNS is a managed DNS name that points to
188+
the load-balancer IP used for the ControlPlaneEndpoint.
189+
properties:
190+
domain:
191+
description: Domain is the DO domain that this record should live
192+
in. It must be pre-existing in your DO account.
193+
type: string
194+
name:
195+
description: Name is the DNS short name of the record (non-FQDN)
196+
type: string
197+
required:
198+
- domain
199+
- name
200+
type: object
186201
controlPlaneEndpoint:
187202
description: ControlPlaneEndpoint represents the endpoint used to
188-
communicate with the control plane.
203+
communicate with the control plane. If ControlPlaneDNS is unset,
204+
the DO load-balancer IP of the Kubernetes API Server is used.
189205
properties:
190206
host:
191207
description: The hostname on which the API server is serving.
@@ -275,6 +291,10 @@ spec:
275291
status:
276292
description: DOClusterStatus defines the observed state of DOCluster.
277293
properties:
294+
controlPlaneDNSRecordReady:
295+
description: ControlPlaneDNSRecordReady denotes that the DNS record
296+
is ready and propagated to the DO DNS servers.
297+
type: boolean
278298
network:
279299
description: Network encapsulates all things related to DigitalOcean
280300
network.

0 commit comments

Comments
 (0)