diff --git a/aws/adapter.go b/aws/adapter.go index bf26bba3..c9bed9e8 100644 --- a/aws/adapter.go +++ b/aws/adapter.go @@ -63,8 +63,6 @@ const ( DefaultStackTTL = 5 * time.Minute nameTag = "Name" - - certificateARNsTag = "ingress:certificate-arns" ) var ( diff --git a/aws/cf.go b/aws/cf.go index 1a2f26b0..930293be 100644 --- a/aws/cf.go +++ b/aws/cf.go @@ -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. @@ -25,7 +25,7 @@ type Stack struct { dnsName string scheme string targetGroupARN string - CertificateARNs []string + certificateARNs []string tags map[string]string } @@ -33,6 +33,10 @@ func (s *Stack) Name() string { return s.name } +func (s *Stack) CertificateARNs() []string { + return s.certificateARNs +} + func (s *Stack) DNSName() string { return s.dnsName } @@ -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 } @@ -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), @@ -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), @@ -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 } @@ -264,7 +202,7 @@ 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), @@ -272,9 +210,11 @@ func updateStack(svc cloudformationiface.CloudFormationAPI, spec *stackSpec) (st }, 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), @@ -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), } } diff --git a/aws/cf_template.go b/aws/cf_template.go new file mode 100644 index 00000000..81975213 --- /dev/null +++ b/aws/cf_template.go @@ -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", + Description: "The security group ID for the Load Balancer", + }, + "LoadBalancerSubnetsParameter": &cloudformation.Parameter{ + Type: "List", + 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 +} diff --git a/aws/cf_test.go b/aws/cf_test.go index 05e7ff04..24ec09bc 100644 --- a/aws/cf_test.go +++ b/aws/cf_test.go @@ -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")}, @@ -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")}, @@ -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", }, }, }, diff --git a/glide.lock b/glide.lock index d5b42526..1a1c11f3 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 1bde642ef645e303b10371bde9d4cfcef6a81344f98361cb23c5c28a0e9aef16 -updated: 2017-12-17T00:49:39.216937612+01:00 +hash: d5654c5e1559afc77695283e79645f2612400ac5a8f15e00a949393886a0a029 +updated: 2017-12-18T23:48:25.430534888+01:00 imports: - name: bitbucket.org/ww/goautoneg version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675 @@ -49,6 +49,12 @@ imports: - quantile - name: github.com/go-ini/ini version: e7fea39b01aea8d5671f6858f0532f56e8bff3a5 +- name: github.com/go-playground/locales + version: e4cbcb5d0652150d40ad0646651076b6bd2be4f6 + subpackages: + - currency +- name: github.com/go-playground/universal-translator + version: 71201497bace774495daed26a3874fd339e0b538 - name: github.com/golang/protobuf version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 subpackages: @@ -61,6 +67,8 @@ imports: version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a subpackages: - pbutil +- name: github.com/mweagle/go-cloudformation + version: d4d39d439140cd56c1580db1f56decfcf380c9f2 - name: github.com/pkg/errors version: 645ef00459ed84a119197bfb8d8205042c6df63d - name: github.com/prometheus/client_golang @@ -79,6 +87,8 @@ imports: - model - name: github.com/prometheus/procfs version: 454a56f35412459b5e684fd5ec0f9211b94f002a +- name: gopkg.in/go-playground/validator.v9 + version: b1f51f36f1c98cc97f777d6fc9d4b05eaa0cabb5 +testImports: - name: gopkg.in/yaml.v2 version: cd8b52f8269e0feb286dfeef29f8fe4d5b397e0b -testImports: [] diff --git a/glide.yaml b/glide.yaml index dc477b75..afa9c4cc 100644 --- a/glide.yaml +++ b/glide.yaml @@ -27,4 +27,4 @@ import: version: ~0.8.0 subpackages: - prometheus/promhttp -- package: gopkg.in/yaml.v2 +- package: github.com/mweagle/go-cloudformation diff --git a/worker.go b/worker.go index e015b6e1..7290c59a 100644 --- a/worker.go +++ b/worker.go @@ -141,7 +141,7 @@ func doWork(certsProvider certs.CertificatesProvider, awsAdapter *aws.Adapter, k func buildManagedModel(certsProvider certs.CertificatesProvider, ingresses []*kubernetes.Ingress, stacks []*aws.Stack) []*managedItem { sort.Slice(stacks, func(i, j int) bool { - return len(stacks[i].CertificateARNs) > len(stacks[j].CertificateARNs) + return len(stacks[i].CertificateARNs()) > len(stacks[j].CertificateARNs()) }) model := make([]*managedItem, 0, len(stacks)) for _, stack := range stacks {