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 1 commit
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
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
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
}
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