Skip to content

Commit

Permalink
Generate CF template with Go
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkeloscar committed Jan 16, 2018
1 parent 65fd11b commit c801e68
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 93 deletions.
2 changes: 0 additions & 2 deletions aws/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ const (
DefaultStackTTL = 5 * time.Minute

nameTag = "Name"

certificateARNsTag = "ingress:certificate-arns"
)

var (
Expand Down
109 changes: 29 additions & 80 deletions aws/cf.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
yaml "gopkg.in/yaml.v2"
)

const (
deleteScheduled = "deleteScheduled"
deleteScheduled = "deleteScheduled"
certificateARNTagPrefix = "ingress:certificate-arn/"
)

// Stack is a simple wrapper around a CloudFormation Stack.
Expand All @@ -25,14 +25,18 @@ type Stack struct {
dnsName string
scheme string
targetGroupARN string
CertificateARNs []string
certificateARNs []string
tags map[string]string
}

func (s *Stack) Name() string {
return s.name
}

func (s *Stack) CertificateARNs() []string {
return s.certificateARNs
}

func (s *Stack) DNSName() string {
return s.dnsName
}
Expand Down Expand Up @@ -142,71 +146,8 @@ type healthCheck struct {
interval time.Duration
}

type Certificate struct {
CertificateArn string `yaml:"CertificateArn"`
}

// cfTemplate is an opauqe structure for unmarshaling a yaml cloudformation
// stack into in order to replace the list of certificates for the
// HTTPSListener.
type cfTemplate struct {
AWSTemplateFormatVersion string `yaml:"AWSTemplateFormatVersion"`
Description string `yaml:"Description"`
Parameters interface{} `yaml:"Parameters"`
Conditions interface{} `yaml:"Conditions"`
Resources struct {
HTTPListener interface{} `yaml:"HTTPListener"`
HTTPSListener struct {
Type string `yaml:"Type"`
Condition string `yaml:"Condition"`
Properties struct {
DefaultActions []struct {
Type string `yaml:"Type"`
TargetGroupArn string `yaml:"TargetGroupArn"`
} `yaml:"DefaultActions"`
LoadBalancerArn string `yaml:"LoadBalancerArn"`
Port int `yaml:"Port"`
Protocol string `yaml:"Protocol"`
Certificates []Certificate `yaml:"Certificates"`
}
}
LB interface{} `yaml:"LB"`
TG interface{} `yaml:"TG"`
} `yaml:"Resources"`
Outputs interface{} `yaml:"Outputs"`
}

// injectCertificates injects a list of certificates into the cloudformation
// template.
func injectCertificates(cfTmpl string, certs []string) (string, error) {
var template cfTemplate
err := yaml.Unmarshal([]byte(cfTmpl), &template)
if err != nil {
return "", err
}

certificates := make([]Certificate, len(certs))
for i, cert := range certs {
certificates[i] = Certificate{CertificateArn: cert}
}

template.Resources.HTTPSListener.Properties.Certificates = certificates

data, err := yaml.Marshal(&template)
if err != nil {
return "", err
}

return string(data), nil
}

func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (string, error) {
template := templateYAML
if spec.customTemplate != "" {
template = spec.customTemplate
}

template, err := injectCertificates(template, spec.certificateARNs)
template, err := generateTemplate(spec.certificateARNs)
if err != nil {
return "", err
}
Expand All @@ -219,7 +160,7 @@ func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
cfParam(parameterLoadBalancerSecurityGroupParameter, spec.securityGroupID),
cfParam(parameterLoadBalancerSubnetsParameter, strings.Join(spec.subnets, ",")),
cfParam(parameterTargetGroupVPCIDParameter, spec.vpcID),
cfParam(parameterListenerCertificatesParameter, strings.Join(spec.certificateARNs, ",")),
// cfParam(parameterListenerCertificatesParameter, strings.Join(spec.certificateARNs, ",")),
},
Tags: []*cloudformation.Tag{
cfTag(kubernetesCreatorTag, kubernetesCreatorValue),
Expand All @@ -228,9 +169,11 @@ func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
TemplateBody: aws.String(template),
TimeoutInMinutes: aws.Int64(int64(spec.timeoutInMinutes)),
}
if len(spec.certificateARNs) > 0 {
params.Tags = append(params.Tags, cfTag(certificateARNsTag, strings.Join(spec.certificateARNs, ",")))

for _, certARN := range spec.certificateARNs {
params.Tags = append(params.Tags, cfTag(certificateARNTagPrefix+certARN, "used"))
}

if spec.healthCheck != nil {
params.Parameters = append(params.Parameters,
cfParam(parameterTargetGroupHealthCheckPathParameter, spec.healthCheck.path),
Expand All @@ -247,12 +190,7 @@ func createStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
}

func updateStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (string, error) {
template := templateYAML
if spec.customTemplate != "" {
template = spec.customTemplate
}

template, err := injectCertificates(template, spec.certificateARNs)
template, err := generateTemplate(spec.certificateARNs)
if err != nil {
return "", err
}
Expand All @@ -264,17 +202,19 @@ func updateStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st
cfParam(parameterLoadBalancerSecurityGroupParameter, spec.securityGroupID),
cfParam(parameterLoadBalancerSubnetsParameter, strings.Join(spec.subnets, ",")),
cfParam(parameterTargetGroupVPCIDParameter, spec.vpcID),
cfParam(parameterListenerCertificatesParameter, strings.Join(spec.certificateARNs, ",")),
// cfParam(parameterListenerCertificatesParameter, strings.Join(spec.certificateARNs, ",")),
},
Tags: []*cloudformation.Tag{
cfTag(kubernetesCreatorTag, kubernetesCreatorValue),
cfTag(clusterIDTagPrefix+spec.clusterID, resourceLifecycleOwned),
},
TemplateBody: aws.String(template),
}
if len(spec.certificateARNs) > 0 {
params.Tags = append(params.Tags, cfTag(certificateARNsTag, strings.Join(spec.certificateARNs, ",")))

for _, certARN := range spec.certificateARNs {
params.Tags = append(params.Tags, cfTag(certificateARNTagPrefix+certARN, "used"))
}

if spec.healthCheck != nil {
params.Parameters = append(params.Parameters,
cfParam(parameterTargetGroupHealthCheckPathParameter, spec.healthCheck.path),
Expand Down Expand Up @@ -365,12 +305,21 @@ func getCFStackByName(svc cloudformationiface.CloudFormationAPI, stackName strin

func mapToManagedStack(stack *cloudformation.Stack) *Stack {
o, t := newStackOutput(stack.Outputs), convertCloudFormationTags(stack.Tags)

certificateARNs := make([]string, 0, len(t))
for key, _ := range t {
if strings.HasPrefix(key, certificateARNTagPrefix) {
arn := strings.TrimPrefix(key, certificateARNTagPrefix)
certificateARNs = append(certificateARNs, arn)
}
}

return &Stack{
name: aws.StringValue(stack.StackName),
dnsName: o.dnsName(),
scheme: o.scheme(),
targetGroupARN: o.targetGroupARN(),
CertificateARNs: strings.Split(t[certificateARNsTag], ","),
certificateARNs: certificateARNs,
tags: convertCloudFormationTags(stack.Tags),
}
}
Expand Down
137 changes: 137 additions & 0 deletions aws/cf_template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package aws

import (
"encoding/json"

"github.com/mweagle/go-cloudformation"
)

func generateTemplate(certs []string) (string, error) {
template := cloudformation.NewTemplate()
template.Description = "Load Balancer for Kubernetes Ingress"
template.Parameters = map[string]*cloudformation.Parameter{
"LoadBalancerSchemeParameter": &cloudformation.Parameter{
Type: "String",
Description: "The Load Balancer scheme - 'internal' or 'internet-facing'",
Default: "internet-facing",
},
"LoadBalancerSecurityGroupParameter": &cloudformation.Parameter{
Type: "List<AWS::EC2::SecurityGroup::Id>",
Description: "The security group ID for the Load Balancer",
},
"LoadBalancerSubnetsParameter": &cloudformation.Parameter{
Type: "List<AWS::EC2::Subnet::Id>",
Description: "The list of subnets IDs for the Load Balancer",
},
"TargetGroupHealthCheckPathParameter": &cloudformation.Parameter{
Type: "String",
Description: "The healthcheck path",
Default: "/kube-system/healthz",
},
"TargetGroupHealthCheckPortParameter": &cloudformation.Parameter{
Type: "Number",
Description: "The healthcheck port",
Default: "9999",
},
"TargetGroupHealthCheckIntervalParameter": &cloudformation.Parameter{
Type: "Number",
Description: "The healthcheck interval",
Default: "10",
},
"TargetGroupVPCIDParameter": &cloudformation.Parameter{
Type: "AWS::EC2::VPC::Id",
Description: "The VPCID for the TargetGroup",
},
// "ListenerCertificatesParameter": &cloudformation.Parameter{
// Type: "String",
// Description: "The HTTPS Listener certificate ARNs (IAM/ACM)",
// Default: "",
// },
}
// template.Conditions = map[string]interface{}{
// "": "",
// }
template.AddResource("HTTPListener", &cloudformation.ElasticLoadBalancingV2Listener{
DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
{
Type: cloudformation.String("forward"),
TargetGroupArn: cloudformation.Ref("TG").String(),
},
},
LoadBalancerArn: cloudformation.Ref("LB").String(),
Port: cloudformation.Integer(80),
Protocol: cloudformation.String("HTTP"),
})

if len(certs) > 0 {
defaultCertificateARN := cloudformation.ElasticLoadBalancingV2ListenerCertificatePropertyList{
{
CertificateArn: cloudformation.String(certs[0]),
},
}

template.AddResource("HTTPSListener", &cloudformation.ElasticLoadBalancingV2Listener{
DefaultActions: &cloudformation.ElasticLoadBalancingV2ListenerActionList{
{
Type: cloudformation.String("forward"),
TargetGroupArn: cloudformation.Ref("TG").String(),
},
},
Certificates: &defaultCertificateARN,
LoadBalancerArn: cloudformation.Ref("LB").String(),
Port: cloudformation.Integer(443),
Protocol: cloudformation.String("HTTPS"),
})

if len(certs) > 1 {
certificateARNs := make(cloudformation.ElasticLoadBalancingV2ListenerCertificateCertificateList, 0, len(certs)-1)
for _, cert := range certs[1:] {
c := cloudformation.ElasticLoadBalancingV2ListenerCertificateCertificate{
CertificateArn: cloudformation.String(cert),
}
certificateARNs = append(certificateARNs, c)
}

template.AddResource("HTTPSListenerCertificate", &cloudformation.ElasticLoadBalancingV2ListenerCertificate{
Certificates: &certificateARNs,
ListenerArn: cloudformation.Ref("HTTPSListener").String(),
})
}
}
template.AddResource("LB", &cloudformation.ElasticLoadBalancingV2LoadBalancer{
Scheme: cloudformation.Ref("LoadBalancerSchemeParameter").String(),
SecurityGroups: cloudformation.Ref("LoadBalancerSecurityGroupParameter").StringList(),
Subnets: cloudformation.Ref("LoadBalancerSubnetsParameter").StringList(),
Tags: &cloudformation.TagList{
{
Key: cloudformation.String("StackName"),
Value: cloudformation.Ref("AWS::StackName").String(),
},
},
})
template.AddResource("TG", &cloudformation.ElasticLoadBalancingV2TargetGroup{
HealthCheckIntervalSeconds: cloudformation.Ref("TargetGroupHealthCheckIntervalParameter").Integer(),
HealthCheckPath: cloudformation.Ref("TargetGroupHealthCheckPathParameter").String(),
Port: cloudformation.Ref("TargetGroupHealthCheckPortParameter").Integer(),
Protocol: cloudformation.String("HTTP"),
VPCID: cloudformation.Ref("TargetGroupVPCIDParameter").String(),
})

template.Outputs = map[string]*cloudformation.Output{
"LoadBalancerDNSName": &cloudformation.Output{
Description: "DNS name for the LoadBalancer",
Value: cloudformation.GetAtt("LB", "DNSName").String(),
},
"TargetGroupARN": &cloudformation.Output{
Description: "The ARN of the TargetGroup",
Value: cloudformation.Ref("TG").String(),
},
}

stackTemplate, err := json.MarshalIndent(template, "", " ")
if err != nil {
return "", err
}

return string(stackTemplate), nil
}
12 changes: 6 additions & 6 deletions aws/cf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ func TestFindingManagedStacks(t *testing.T) {
Tags: []*cloudformation.Tag{
cfTag(kubernetesCreatorTag, kubernetesCreatorValue),
cfTag(clusterIDTagPrefix+"test-cluster", resourceLifecycleOwned),
cfTag(certificateARNsTag, "cert-arn"),
cfTag(certificateARNTagPrefix+"cert-arn", "used"),
},
Outputs: []*cloudformation.Output{
{OutputKey: aws.String(outputLoadBalancerDNSName), OutputValue: aws.String("example-notready.com")},
Expand All @@ -201,7 +201,7 @@ func TestFindingManagedStacks(t *testing.T) {
Tags: []*cloudformation.Tag{
cfTag(kubernetesCreatorTag, kubernetesCreatorValue),
cfTag(clusterIDTagPrefix+"test-cluster", resourceLifecycleOwned),
cfTag(certificateARNsTag, "cert-arn"),
cfTag(certificateARNTagPrefix+"cert-arn", "used"),
},
Outputs: []*cloudformation.Output{
{OutputKey: aws.String(outputLoadBalancerDNSName), OutputValue: aws.String("example.com")},
Expand Down Expand Up @@ -241,12 +241,12 @@ func TestFindingManagedStacks(t *testing.T) {
{
name: "managed-stack",
dnsName: "example.com",
CertificateARNs: []string{"cert-arn"},
certificateARNs: []string{"cert-arn"},
targetGroupARN: "tg-arn",
tags: map[string]string{
kubernetesCreatorTag: kubernetesCreatorValue,
clusterIDTagPrefix + "test-cluster": resourceLifecycleOwned,
certificateARNsTag: "cert-arn",
kubernetesCreatorTag: kubernetesCreatorValue,
clusterIDTagPrefix + "test-cluster": resourceLifecycleOwned,
certificateARNTagPrefix + "cert-arn": "used",
},
},
},
Expand Down
Loading

0 comments on commit c801e68

Please sign in to comment.