Skip to content
This repository has been archived by the owner on Sep 4, 2021. It is now read-only.

kube-aws: add option to create a record for externalDNSName automatic… #389

Merged
merged 1 commit into from
Apr 12, 2016
Merged
Show file tree
Hide file tree
Changes from all 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
59 changes: 59 additions & 0 deletions multi-node/aws/pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/route53"

"github.com/coreos/coreos-kubernetes/multi-node/aws/pkg/config"
)
Expand Down Expand Up @@ -131,6 +132,11 @@ func (c *Cluster) validateExistingVPCState(ec2Svc ec2Service) error {
}

func (c *Cluster) Create(stackBody string) error {
r53Svc := route53.New(c.session)
if err := c.validateDNSConfig(r53Svc); err != nil {
return err
}

ec2Svc := ec2.New(c.session)

if err := c.validateKeyPair(ec2Svc); err != nil {
Expand Down Expand Up @@ -265,6 +271,59 @@ func (c *Cluster) validateKeyPair(ec2Svc ec2Service) error {
}
return err
}
return nil
}

type r53Service interface {
ListHostedZonesByName(*route53.ListHostedZonesByNameInput) (*route53.ListHostedZonesByNameOutput, error)
ListResourceRecordSets(*route53.ListResourceRecordSetsInput) (*route53.ListResourceRecordSetsOutput, error)
}

func (c *Cluster) validateDNSConfig(r53 r53Service) error {
if !c.CreateRecordSet {
return nil
}

if c.RecordSetTTL < 1 {
return fmt.Errorf("TTL must be at least 1 second")
}

if c.HostedZone == "" {
return fmt.Errorf("hostName cannot be blank when createRecordSet is true")
}

zonesResp, err := r53.ListHostedZonesByName(&route53.ListHostedZonesByNameInput{
DNSName: aws.String(c.HostedZone),
})
if err != nil {
return fmt.Errorf("Error validating HostedZone: %s", err)
}

zones := zonesResp.HostedZones
if len(zones) == 0 || (*zones[0].Name != c.HostedZone) {
return fmt.Errorf(
"HostedZone %s does not exist. You'll need to create it manually",
c.HostedZone,
)
}

recordSetsResp, err := r53.ListResourceRecordSets(
&route53.ListResourceRecordSetsInput{
HostedZoneId: zones[0].Id,
},
)

if len(recordSetsResp.ResourceRecordSets) > 0 {
for _, recordSet := range recordSetsResp.ResourceRecordSets {
if *recordSet.Name == c.ExternalDNSName {
return fmt.Errorf(
"RecordSet for \"%s\" already exists in Hosted Zone \"%s.\"",
c.ExternalDNSName,
c.HostedZone,
)
}
}
}

return nil
}
89 changes: 89 additions & 0 deletions multi-node/aws/pkg/cluster/cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/route53"
"github.com/coreos/coreos-kubernetes/multi-node/aws/pkg/config"
)

Expand Down Expand Up @@ -203,3 +204,91 @@ func TestValidateKeyPair(t *testing.T) {
t.Errorf("failed to catch invalid key \"%s\"", c.KeyName)
}
}

type Zone struct {
Id string
DNS string
}

type dummyR53Service struct {
HostedZones []Zone
ResourceRecordSets map[string]string
}

func (r53 dummyR53Service) ListHostedZonesByName(input *route53.ListHostedZonesByNameInput) (*route53.ListHostedZonesByNameOutput, error) {
output := &route53.ListHostedZonesByNameOutput{}
for _, zone := range r53.HostedZones {
if zone.DNS == *input.DNSName {
output.HostedZones = append(output.HostedZones, &route53.HostedZone{
Name: aws.String(zone.DNS),
Id: aws.String(zone.Id),
})
}
}
return output, nil
}

func (r53 dummyR53Service) ListResourceRecordSets(input *route53.ListResourceRecordSetsInput) (*route53.ListResourceRecordSetsOutput, error) {
output := &route53.ListResourceRecordSetsOutput{}
if name, ok := r53.ResourceRecordSets[*input.HostedZoneId]; ok {
output.ResourceRecordSets = []*route53.ResourceRecordSet{
&route53.ResourceRecordSet{
Name: aws.String(name),
},
}
}
return output, nil
}

func TestValidateDNSConfig(t *testing.T) {
dnsConfig := `
createRecordSet: true
recordSetTTL: 60
hostedZone: staging.core-os.net
`

configBody := minimalConfigYaml + dnsConfig
clusterConfig, err := config.ClusterFromBytes([]byte(configBody))
if err != nil {
t.Errorf("could not get valid cluster config: %v", err)
}
c := &Cluster{Cluster: *clusterConfig}

r53 := dummyR53Service{
HostedZones: []Zone{
Zone{
Id: "staging_id",
DNS: "staging.core-os.net",
},
},
ResourceRecordSets: map[string]string{
"staging_id": "existing-record.staging.core-os.net",
},
}

if err := c.validateDNSConfig(r53); err != nil {
t.Errorf("returned error for valid config: %v", err)
}

c.RecordSetTTL = 0
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to reject invalid TTL")
}

c.RecordSetTTL = 300
c.HostedZone = ""
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to reject empty HostName")
}

c.HostedZone = "non-existant-zone"
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to catch non-existent hosted zone")
}

c.HostedZone = "staging.core-os.net"
c.ExternalDNSName = "existing-record.staging.core-os.net"
if err := c.validateDNSConfig(r53); err == nil {
t.Errorf("failed to catch already existing ExternalDNSName")
}
}
21 changes: 21 additions & 0 deletions multi-node/aws/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net"
"strings"
"text/template"
"unicode/utf8"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
Expand Down Expand Up @@ -43,6 +44,7 @@ func newDefaultCluster() *Cluster {
WorkerCount: 1,
WorkerInstanceType: "m3.medium",
WorkerRootVolumeSize: 30,
CreateRecordSet: false,
}
}

Expand All @@ -57,6 +59,13 @@ func ClusterFromFile(filename string) (*Cluster, error) {
return nil, fmt.Errorf("file %s: %v", filename, err)
}

// HostedZone needs to end with a '.'
c.HostedZone = withTrailingDot(c.HostedZone)

// ExternalDNSName doesn't require '.' to be created,
// but adding it here makes validations easier
c.ExternalDNSName = withTrailingDot(c.ExternalDNSName)

return c, nil
}

Expand Down Expand Up @@ -96,6 +105,10 @@ type Cluster struct {
K8sVer string `yaml:"kubernetesVersion"`
HyperkubeImageRepo string `yaml:"hyperkubeImageRepo"`
KMSKeyARN string `yaml:"kmsKeyArn"`

CreateRecordSet bool `yaml:"createRecordSet"`
RecordSetTTL int `yaml:"recordSetTTL"`
HostedZone string `yaml:"hostedZone"`
}

const (
Expand Down Expand Up @@ -476,3 +489,11 @@ func incrementIP(netIP net.IP) net.IP {
func cidrOverlap(a, b *net.IPNet) bool {
return a.Contains(b.IP) || b.Contains(a.IP)
}

func withTrailingDot(s string) string {
lastRune, _ := utf8.DecodeLastRuneInString(s)
if lastRune != rune('.') {
return s + "."
}
return s
}
16 changes: 14 additions & 2 deletions multi-node/aws/pkg/config/templates/cluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@
clusterName: {{.ClusterName}}

# DNS name routable to the Kubernetes controller nodes
# from worker nodes and external clients. The deployer
# is responsible for making this name routable
# from worker nodes and external clients. Configure the options
# below if you'd like kube-aws to create a Route53 record sets/hosted zones
# for you. Otherwise the deployer is responsible for making this name routable
externalDNSName: {{.ExternalDNSName}}

# Set to true if you want kube-aws to create a Route53 A Record for you.
#createRecordSet: false

# TTL in seconds for the Route53 RecordSet created if createRecordSet is set to true.
#recordSetTTL: 300

# The name of the hosted zone to add the externalDNSName to,
# E.g: "google.com". This needs to already exist, kube-aws will not create
# it for you.
#hostedZone: ""

# Name of the SSH keypair already loaded into the AWS
# account being used to deploy this cluster.
keyName: {{.KeyName}}
Expand Down
22 changes: 17 additions & 5 deletions multi-node/aws/pkg/config/templates/stack-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@
},
"Type": "AWS::EC2::EIP"
},
{{ if .CreateRecordSet }}
"ExternalDNS": {
"Type": "AWS::Route53::RecordSet",
"Properties": {
"HostedZoneName": "{{.HostedZone}}",
"Name": "{{.ExternalDNSName}}",
"TTL": {{.RecordSetTTL}},
"ResourceRecords": [{ "Ref": "EIPController"}],
"Type": "A"
}
},
{{ end }}
"IAMInstanceProfileController": {
"Properties": {
"Path": "/",
Expand Down Expand Up @@ -198,11 +210,11 @@
"Effect": "Allow",
"Resource": "*"
},
{
"Action" : "kms:Decrypt",
"Effect" : "Allow",
"Resource" : "{{.KMSKeyARN}}"
}
{
"Action" : "kms:Decrypt",
"Effect" : "Allow",
"Resource" : "{{.KMSKeyARN}}"
}
],
"Version": "2012-10-17"
},
Expand Down