Skip to content

Commit

Permalink
[feat] Add support for using Akamai EDGE DNS as DNS Provider for API …
Browse files Browse the repository at this point in the history
…Server Loadbalancing (#419)

* Add support for using Akamai EDGE DNS as DNS Provider for API Server Loadbalancing
  • Loading branch information
amold1 committed Jul 23, 2024
1 parent 2af3109 commit 64ff32a
Show file tree
Hide file tree
Showing 24 changed files with 948 additions and 172 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/e2e-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ on:
- linodemachine
- linodeobj
- linodevpc
- linodeplacementgroup
# - linodeplacementgroup
- all
e2e-flags:
type: string
Expand Down
6 changes: 6 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ for resource in manager_yaml:
if resource["metadata"]["name"] == "capl-manager-credentials":
resource["stringData"]["apiToken"] = os.getenv("LINODE_TOKEN")
resource["stringData"]["dnsToken"] = os.getenv("LINODE_DNS_TOKEN")
if resource["metadata"]["name"] == "capl-akamai-edgerc-secret":
resource["stringData"]["AKAMAI_HOST"] = os.getenv("AKAMAI_HOST")
resource["stringData"]["AKAMAI_CLIENT_TOKEN"] = os.getenv("AKAMAI_CLIENT_TOKEN")
resource["stringData"]["AKAMAI_CLIENT_SECRET"] = os.getenv("AKAMAI_CLIENT_SECRET")
resource["stringData"]["AKAMAI_ACCESS_TOKEN"] = os.getenv("AKAMAI_ACCESS_TOKEN")
if (
resource["kind"] == "CustomResourceDefinition"
and resource["spec"]["group"] == "infrastructure.cluster.x-k8s.io"
Expand Down Expand Up @@ -172,6 +177,7 @@ k8s_resource(
"capl-manager-rolebinding:clusterrolebinding",
"capl-proxy-rolebinding:clusterrolebinding",
"capl-manager-credentials:secret",
"capl-akamai-edgerc-secret:secret",
"capl-serving-cert:certificate",
"capl-selfsigned-issuer:issuer",
"capl-validating-webhook-configuration:validatingwebhookconfiguration",
Expand Down
3 changes: 2 additions & 1 deletion api/v1alpha2/linodecluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,9 @@ type NetworkSpec struct {
// DNSProvider is provider who manages the domain
// Ignored if the LoadBalancerType is set to anything other than dns
// If not set, defaults linode dns
// +kubebuilder:validation:Enum=linode;akamai
// +optional
DNSProvider int `json:"dnsProvider,omitempty"`
DNSProvider string `json:"dnsProvider,omitempty"`
// DNSRootDomain is the root domain used to create a DNS entry for the control-plane endpoint
// Ignored if the LoadBalancerType is set to anything other than dns
// +optional
Expand Down
12 changes: 12 additions & 0 deletions clients/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package clients
import (
"context"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/dns"
"github.com/linode/linodego"
"sigs.k8s.io/controller-runtime/pkg/client"
)
Expand All @@ -19,6 +20,17 @@ type LinodeClient interface {
LinodePlacementGroupClient
}

type AkamClient interface {
AkamEdgeDNSClient
}

type AkamEdgeDNSClient interface {
GetRecord(context.Context, string, string, string) (*dns.RecordBody, error)
CreateRecord(context.Context, *dns.RecordBody, string, ...bool) error
UpdateRecord(context.Context, *dns.RecordBody, string, ...bool) error
DeleteRecord(context.Context, *dns.RecordBody, string, ...bool) error
}

// LinodeInstanceClient defines the methods that interact with Linode's Instance service.
type LinodeInstanceClient interface {
GetInstanceIPAddresses(ctx context.Context, linodeID int) (*linodego.InstanceIPAddressResponse, error)
Expand Down
22 changes: 22 additions & 0 deletions cloud/scope/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"time"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/dns"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/edgegrid"
"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/session"
"github.com/linode/linodego"
"golang.org/x/oauth2"
corev1 "k8s.io/api/core/v1"
Expand All @@ -23,6 +27,9 @@ import (
const (
// defaultClientTimeout is the default timeout for a client Linode API call
defaultClientTimeout = time.Second * 10

// MaxBodySize is the max payload size for Akamai edge dns client requests
maxBody = 131072
)

type Option struct {
Expand Down Expand Up @@ -63,6 +70,21 @@ func CreateLinodeClient(apiKey string, timeout time.Duration, opts ...Option) (L
), nil
}

func setUpEdgeDNSInterface() (dnsInterface dns.DNS, err error) {
edgeRCConfig := edgegrid.Config{
Host: os.Getenv("AKAMAI_HOST"),
AccessToken: os.Getenv("AKAMAI_ACCESS_TOKEN"),
ClientToken: os.Getenv("AKAMAI_CLIENT_TOKEN"),
ClientSecret: os.Getenv("AKAMAI_CLIENT_SECRET"),
MaxBody: maxBody,
}
sess, err := session.New(session.WithSigner(&edgeRCConfig))
if err != nil {
return nil, err
}
return dns.Client(sess), nil
}

func getCredentialDataFromRef(ctx context.Context, crClient K8sClient, credentialsRef corev1.SecretReference, defaultNamespace, key string) ([]byte, error) {
credSecret, err := getCredentials(ctx, crClient, credentialsRef, defaultNamespace)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions cloud/scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type MachineScope struct {
Machine *clusterv1.Machine
LinodeClient LinodeClient
LinodeDomainsClient LinodeClient
AkamaiDomainsClient AkamClient
LinodeCluster *infrav1alpha2.LinodeCluster
LinodeMachine *infrav1alpha1.LinodeMachine
}
Expand Down Expand Up @@ -106,6 +107,11 @@ func NewMachineScope(ctx context.Context, apiKey, dnsKey string, params MachineS
return nil, fmt.Errorf("failed to create linode client: %w", err)
}

akamDomainsClient, err := setUpEdgeDNSInterface()
if err != nil {
return nil, fmt.Errorf("failed to create akamai dns client: %w", err)
}

helper, err := patch.NewHelper(params.LinodeMachine, params.Client)
if err != nil {
return nil, fmt.Errorf("failed to init patch helper: %w", err)
Expand All @@ -118,6 +124,7 @@ func NewMachineScope(ctx context.Context, apiKey, dnsKey string, params MachineS
Machine: params.Machine,
LinodeClient: linodeClient,
LinodeDomainsClient: linodeDomainsClient,
AkamaiDomainsClient: akamDomainsClient,
LinodeCluster: params.LinodeCluster,
LinodeMachine: params.LinodeMachine,
}, nil
Expand Down
130 changes: 99 additions & 31 deletions cloud/services/domains.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import (
"encoding/json"
"fmt"
"net/netip"
"strings"
"sync"

"github.com/akamai/AkamaiOPEN-edgegrid-golang/v8/pkg/dns"
"github.com/linode/linodego"
"golang.org/x/exp/slices"
"sigs.k8s.io/cluster-api/api/v1beta1"
kutil "sigs.k8s.io/cluster-api/util"

Expand All @@ -27,7 +30,7 @@ type DNSOptions struct {
DNSTTLSec int
}

// EnsureDNSEntries ensures the domainrecord is created, updated, or deleted based on operation passed
// EnsureDNSEntries ensures the domainrecord on Linode Cloud Manager is created, updated, or deleted based on operation passed
func EnsureDNSEntries(ctx context.Context, mscope *scope.MachineScope, operation string) error {
// Check if instance is a control plane node
if !kutil.IsControlPlaneMachine(mscope.Machine) {
Expand All @@ -41,6 +44,15 @@ func EnsureDNSEntries(ctx context.Context, mscope *scope.MachineScope, operation
return err
}

if mscope.LinodeCluster.Spec.Network.DNSProvider == "akamai" {
return EnsureAkamaiDNSEntries(ctx, mscope, operation, dnsEntries)
}

return EnsureLinodeDNSEntries(ctx, mscope, operation, dnsEntries)
}

// EnsureLinodeDNSEntries ensures the domainrecord on Linode Cloud Manager is created, updated, or deleted based on operation passed
func EnsureLinodeDNSEntries(ctx context.Context, mscope *scope.MachineScope, operation string, dnsEntries []DNSOptions) error {
// Get domainID from domain name
domainID, err := GetDomainID(ctx, mscope)
if err != nil {
Expand All @@ -62,6 +74,70 @@ func EnsureDNSEntries(ctx context.Context, mscope *scope.MachineScope, operation
return nil
}

// EnsureAkamaiDNSEntries ensures the domainrecord on Akamai EDGE DNS is created, updated, or deleted based on operation passed
func EnsureAkamaiDNSEntries(ctx context.Context, mscope *scope.MachineScope, operation string, dnsEntries []DNSOptions) error {
linodeCluster := mscope.LinodeCluster
linodeClusterNetworkSpec := linodeCluster.Spec.Network
rootDomain := linodeClusterNetworkSpec.DNSRootDomain
fqdn := linodeCluster.Name + "-" + linodeClusterNetworkSpec.DNSUniqueIdentifier + "." + rootDomain
akaDNSClient := mscope.AkamaiDomainsClient

for _, dnsEntry := range dnsEntries {
recordBody, err := akaDNSClient.GetRecord(ctx, rootDomain, fqdn, string(dnsEntry.DNSRecordType))
if err != nil {
if !strings.Contains(err.Error(), "Not Found") {
return err
}
if operation == "create" {
if err := akaDNSClient.CreateRecord(
ctx,
&dns.RecordBody{
Name: fqdn,
RecordType: string(dnsEntry.DNSRecordType),
TTL: dnsEntry.DNSTTLSec,
Target: []string{dnsEntry.Target},
}, rootDomain); err != nil {
return err
}
}
continue
}
if operation == "delete" {
switch {
case len(recordBody.Target) > 1:
recordBody.Target = removeElement(
recordBody.Target,
strings.Replace(dnsEntry.Target, "::", ":0:0:", 8), //nolint:mnd // 8 for 8 octest
)
if err := akaDNSClient.UpdateRecord(ctx, recordBody, rootDomain); err != nil {
return err
}
continue
default:
if err := akaDNSClient.DeleteRecord(ctx, recordBody, rootDomain); err != nil {
return err
}
}
} else {
recordBody.Target = append(recordBody.Target, dnsEntry.Target)
if err := akaDNSClient.UpdateRecord(ctx, recordBody, rootDomain); err != nil {
return err
}
}
}
return nil
}

func removeElement(stringList []string, elemToRemove string) []string {
for index, element := range stringList {
if element == elemToRemove {
stringList = slices.Delete(stringList, index, index+1)
continue
}
}
return stringList
}

// getDNSEntriesToEnsure return DNS entries to create/delete
func (d *DNSEntries) getDNSEntriesToEnsure(mscope *scope.MachineScope) ([]DNSOptions, error) {
d.mux.Lock()
Expand Down Expand Up @@ -127,7 +203,16 @@ func CreateUpdateDomainRecord(ctx context.Context, mscope *scope.MachineScope, d

// If record doesnt exist, create it
if len(domainRecords) == 0 {
if err := CreateDomainRecord(ctx, mscope, domainID, dnsEntry); err != nil {
if _, err := mscope.LinodeDomainsClient.CreateDomainRecord(
ctx,
domainID,
linodego.DomainRecordCreateOptions{
Type: dnsEntry.DNSRecordType,
Name: dnsEntry.Hostname,
Target: dnsEntry.Target,
TTLSec: dnsEntry.DNSTTLSec,
},
); err != nil {
return err
}
return nil
Expand All @@ -143,7 +228,18 @@ func CreateUpdateDomainRecord(ctx context.Context, mscope *scope.MachineScope, d
return fmt.Errorf("the domain record is not owned by this entity. wont update")
}
}
if err := UpdateDomainRecord(ctx, mscope, domainID, domainRecords[0].ID, dnsEntry); err != nil {

if _, err := mscope.LinodeDomainsClient.UpdateDomainRecord(
ctx,
domainID,
domainRecords[0].ID,
linodego.DomainRecordUpdateOptions{
Type: dnsEntry.DNSRecordType,
Name: dnsEntry.Hostname,
Target: dnsEntry.Target,
TTLSec: dnsEntry.DNSTTLSec,
},
); err != nil {
return err
}
return nil
Expand Down Expand Up @@ -184,34 +280,6 @@ func DeleteDomainRecord(ctx context.Context, mscope *scope.MachineScope, domainI
return nil
}

func CreateDomainRecord(ctx context.Context, mscope *scope.MachineScope, domainID int, dnsEntries DNSOptions) error {
recordReq := linodego.DomainRecordCreateOptions{
Type: dnsEntries.DNSRecordType,
Name: dnsEntries.Hostname,
Target: dnsEntries.Target,
TTLSec: dnsEntries.DNSTTLSec,
}

if _, err := mscope.LinodeDomainsClient.CreateDomainRecord(ctx, domainID, recordReq); err != nil {
return err
}
return nil
}

func UpdateDomainRecord(ctx context.Context, mscope *scope.MachineScope, domainID, domainRecordID int, dnsEntries DNSOptions) error {
recordReq := linodego.DomainRecordUpdateOptions{
Type: dnsEntries.DNSRecordType,
Name: dnsEntries.Hostname,
Target: dnsEntries.Target,
TTLSec: dnsEntries.DNSTTLSec,
}

if _, err := mscope.LinodeDomainsClient.UpdateDomainRecord(ctx, domainID, domainRecordID, recordReq); err != nil {
return err
}
return nil
}

func IsDomainRecordOwner(ctx context.Context, mscope *scope.MachineScope, hostname string, domainID int) (bool, error) {
// Check if domain record exists
filter, err := json.Marshal(map[string]interface{}{"name": hostname, "target": mscope.LinodeMachine.Name, "type": linodego.RecordTypeTXT})
Expand Down
Loading

0 comments on commit 64ff32a

Please sign in to comment.