Skip to content

Commit

Permalink
Add managed DNS record support for API endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
gottwald committed Jan 29, 2021
1 parent f140253 commit eb28316
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 6 deletions.
21 changes: 18 additions & 3 deletions api/v1alpha3/docluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,22 @@ 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 ControlPlaneDNSRecord is unset, the DO load-balancer IP
// of the KAS is used.
// +optional
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
// ControlPlaneDNSRecord is a managed DNS record that points to the
// load-balancer IP used for the ControlPlaneEndpoint.
// +optional
ControlPlaneDNSRecord *DOControlPlaneDNSRecord `json:"controlPlaneDNSRecord"`
}

// DOClusterStatus defines the observed state of DOCluster.
Expand All @@ -50,6 +57,14 @@ type DOClusterStatus struct {
Network DONetworkResource `json:"network,omitempty"`
}

type DOControlPlaneDNSRecord 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
}
4 changes: 4 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
62 changes: 62 additions & 0 deletions cloud/services/networking/domains.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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
}
18 changes: 17 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:
controlPlaneDNSRecord:
description: ControlPlaneDNSRecord is a managed DNS record 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 ControlPlaneDNSRecord is
unset, the DO load-balancer IP of the KAS is used.
properties:
host:
description: The hostname on which the API server is serving.
Expand Down
40 changes: 39 additions & 1 deletion controllers/docluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
"context"
"fmt"
"time"

"github.com/go-logr/logr"
Expand Down Expand Up @@ -141,8 +142,38 @@ func (r *DOClusterReconciler) reconcile(ctx context.Context, clusterScope *scope
}

r.Recorder.Eventf(docluster, corev1.EventTypeNormal, "LoadBalancerReady", "LoadBalancer got an IP Address - %s", loadbalancer.IP)

var cpEndpointHost = loadbalancer.IP
if docluster.Spec.ControlPlaneDNSRecord != nil {
clusterScope.Info("Verifying LB DNS Record")
// ensure DNS record is created and use it as control plane endpoint
recordSpec := docluster.Spec.ControlPlaneDNSRecord
cpEndpointHost = fmt.Sprintf("%s.%s", recordSpec.Name, recordSpec.Domain)
dRecord, err := networkingsvc.GetDomainRecord(
recordSpec.Domain,
recordSpec.Name,
"A",
)
if err != nil {
return reconcile.Result{}, errors.Wrapf(err, "failed verify DNS record for LB Name %s.%s",
recordSpec.Name, recordSpec.Domain)
}
if dRecord == nil || dRecord.Data != loadbalancer.IP {
clusterScope.Info("Ensuring LB DNS Record is in place")
if err := networkingsvc.UpsertDomainRecord(
recordSpec.Domain,
recordSpec.Name,
"A",
loadbalancer.IP,
); err != nil {
return reconcile.Result{}, errors.Wrap(err, "failed to reconcile LB DNS record")
}
}
r.Recorder.Eventf(docluster, corev1.EventTypeNormal, "DomainRecordReady", "DNS Record '%s.%s' with IP '%s'", recordSpec.Name, recordSpec.Domain, loadbalancer.IP)
}

clusterScope.SetControlPlaneEndpoint(clusterv1.APIEndpoint{
Host: loadbalancer.IP,
Host: cpEndpointHost,
Port: int32(apiServerLoadbalancer.Port),
})

Expand All @@ -158,6 +189,13 @@ func (r *DOClusterReconciler) reconcileDelete(ctx context.Context, clusterScope
networkingsvc := networking.NewService(ctx, clusterScope)
apiServerLoadbalancerRef := clusterScope.APIServerLoadbalancersRef()

if docluster.Spec.ControlPlaneDNSRecord != nil {
recordSpec := docluster.Spec.ControlPlaneDNSRecord
if err := networkingsvc.DeleteDomainRecord(recordSpec.Domain, recordSpec.Name, "A"); err != nil {
return reconcile.Result{}, err
}
}

loadbalancer, err := networkingsvc.GetLoadBalancer(apiServerLoadbalancerRef.ResourceID)
if err != nil {
return reconcile.Result{}, err
Expand Down

0 comments on commit eb28316

Please sign in to comment.