Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add managed DNS record support for API endpoint #232

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -138,6 +142,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 @@
/*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can move this file to another directory like utils in the future. But leave it as is for now

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 2020 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