From c35c754eff616443f4179b2419f8e0a0da26cff5 Mon Sep 17 00:00:00 2001 From: justinsb Date: Sun, 28 Jan 2024 13:31:01 -0500 Subject: [PATCH 1/2] Refactor: Split out NLB Listener into its own task This allows us to use more of our task machinery, including dependency analysis. The intent is that we'll be able to support multiple LoadBalancers and TargetGroups. --- pkg/model/awsmodel/api_loadbalancer.go | 64 +++-- pkg/model/awsmodel/bastion.go | 14 +- pkg/model/names.go | 6 + .../cloudup/awstasks/network_load_balancer.go | 195 +------------ .../awstasks/networkloadbalancerlistener.go | 261 ++++++++++++++++++ .../networkloadbalancerlistener_fitask.go | 52 ++++ 6 files changed, 379 insertions(+), 213 deletions(-) create mode 100644 upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener.go create mode 100644 upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener_fitask.go diff --git a/pkg/model/awsmodel/api_loadbalancer.go b/pkg/model/awsmodel/api_loadbalancer.go index 178f5be2dcb02..e537167553d4c 100644 --- a/pkg/model/awsmodel/api_loadbalancer.go +++ b/pkg/model/awsmodel/api_loadbalancer.go @@ -134,31 +134,49 @@ func (b *APILoadBalancerBuilder) Build(c *fi.CloudupModelBuilderContext) error { listeners := map[string]*awstasks.ClassicLoadBalancerListener{ "443": {InstancePort: 443}, } + var nlbListeners []*awstasks.NetworkLoadBalancerListener + + if lbSpec.SSLCertificate == "" { + listener443 := &awstasks.NetworkLoadBalancerListener{ + Name: fi.PtrTo(b.NLBListenerName("api", 443)), + Lifecycle: b.Lifecycle, + NetworkLoadBalancer: b.LinkToNLB("api"), + Port: 443, + TargetGroup: b.LinkToTargetGroup("tcp"), + } + nlbListeners = append(nlbListeners, listener443) + } else { + listener8443 := &awstasks.NetworkLoadBalancerListener{ + Name: fi.PtrTo(b.NLBListenerName("api", 8443)), + Lifecycle: b.Lifecycle, + NetworkLoadBalancer: b.LinkToNLB("api"), + Port: 8443, + TargetGroup: b.LinkToTargetGroup("tcp"), + } + nlbListeners = append(nlbListeners, listener8443) - nlbListeners := []*awstasks.NetworkLoadBalancerListener{ - { - Port: 443, - TargetGroupName: b.NLBTargetGroupName("tcp"), - }, - } - if b.Cluster.UsesNoneDNS() { - nlbListeners = append(nlbListeners, &awstasks.NetworkLoadBalancerListener{ - Port: wellknownports.KopsControllerPort, - TargetGroupName: b.NLBTargetGroupName("kops-controller"), - }) - } - - if lbSpec.SSLCertificate != "" { listeners["443"].SSLCertificateID = lbSpec.SSLCertificate - nlbListeners[0].Port = 8443 - - nlbListener := &awstasks.NetworkLoadBalancerListener{ - Port: 443, - TargetGroupName: b.NLBTargetGroupName("tls"), - SSLCertificateID: lbSpec.SSLCertificate, + listener443 := &awstasks.NetworkLoadBalancerListener{ + Name: fi.PtrTo(b.NLBListenerName("api", 443)), + Lifecycle: b.Lifecycle, + NetworkLoadBalancer: b.LinkToNLB("api"), + Port: 443, + TargetGroup: b.LinkToTargetGroup("tls"), + SSLCertificateID: lbSpec.SSLCertificate, } if lbSpec.SSLPolicy != nil { - nlbListener.SSLPolicy = *lbSpec.SSLPolicy + listener443.SSLPolicy = *lbSpec.SSLPolicy + } + nlbListeners = append(nlbListeners, listener443) + } + + if b.Cluster.UsesNoneDNS() { + nlbListener := &awstasks.NetworkLoadBalancerListener{ + Name: fi.PtrTo(b.NLBListenerName("api", wellknownports.KopsControllerPort)), + Lifecycle: b.Lifecycle, + NetworkLoadBalancer: b.LinkToNLB("api"), + Port: wellknownports.KopsControllerPort, + TargetGroup: b.LinkToTargetGroup("kops-controller"), } nlbListeners = append(nlbListeners, nlbListener) } @@ -184,7 +202,6 @@ func (b *APILoadBalancerBuilder) Build(c *fi.CloudupModelBuilderContext) error { b.LinkToELBSecurityGroup("api"), }, SubnetMappings: nlbSubnetMappings, - Listeners: nlbListeners, TargetGroups: make([]*awstasks.TargetGroup, 0), Tags: tags, @@ -359,6 +376,9 @@ func (b *APILoadBalancerBuilder) Build(c *fi.CloudupModelBuilderContext) error { nlb.TargetGroups = append(nlb.TargetGroups, secondaryTG) } sort.Stable(awstasks.OrderTargetGroupsByName(nlb.TargetGroups)) + for _, nlbListener := range nlbListeners { + c.AddTask(nlbListener) + } c.AddTask(nlb) } diff --git a/pkg/model/awsmodel/bastion.go b/pkg/model/awsmodel/bastion.go index 8c2f6c15c20f2..08c3ce67aa5a0 100644 --- a/pkg/model/awsmodel/bastion.go +++ b/pkg/model/awsmodel/bastion.go @@ -328,12 +328,15 @@ func (b *BastionModelBuilder) Build(c *fi.CloudupModelBuilderContext) error { // Override the returned name to be the expected ELB name tags["Name"] = "bastion." + b.ClusterName() - nlbListeners := []*awstasks.NetworkLoadBalancerListener{ - { - Port: 22, - TargetGroupName: b.NLBTargetGroupName("bastion"), - }, + nlbListener := &awstasks.NetworkLoadBalancerListener{ + Name: fi.PtrTo(b.NLBListenerName("bastion", 22)), + Lifecycle: b.Lifecycle, + NetworkLoadBalancer: b.LinkToNLB("bastion"), + Port: 22, + TargetGroup: b.LinkToTargetGroup("bastion"), } + c.AddTask(nlbListener) + nlb = &awstasks.NetworkLoadBalancer{ Name: fi.PtrTo(b.NLBName("bastion")), Lifecycle: b.Lifecycle, @@ -344,7 +347,6 @@ func (b *BastionModelBuilder) Build(c *fi.CloudupModelBuilderContext) error { SecurityGroups: []*awstasks.SecurityGroup{ b.LinkToELBSecurityGroup("bastion"), }, - Listeners: nlbListeners, TargetGroups: make([]*awstasks.TargetGroup, 0), Tags: tags, diff --git a/pkg/model/names.go b/pkg/model/names.go index 56aaaa35884be..2754e5246157d 100644 --- a/pkg/model/names.go +++ b/pkg/model/names.go @@ -19,6 +19,7 @@ package model import ( "fmt" "regexp" + "strconv" "strings" "k8s.io/klog/v2" @@ -115,6 +116,11 @@ func (b *KopsModelContext) LinkToNLB(prefix string) *awstasks.NetworkLoadBalance return &awstasks.NetworkLoadBalancer{Name: &name} } +func (b *KopsModelContext) NLBListenerName(loadBalancerPrefix string, port int) string { + name := b.NLBName(loadBalancerPrefix) + return name + "-" + strconv.Itoa(port) +} + func (b *KopsModelContext) LinkToTargetGroup(prefix string) *awstasks.TargetGroup { name := b.NLBTargetGroupName(prefix) return &awstasks.TargetGroup{Name: &name} diff --git a/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go b/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go index fa47858d0f78e..a099fae825c76 100644 --- a/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go +++ b/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go @@ -58,8 +58,6 @@ type NetworkLoadBalancer struct { SubnetMappings []*SubnetMapping SecurityGroups []*SecurityGroup - Listeners []*NetworkLoadBalancerListener - Scheme *string CrossZoneLoadBalancing *bool @@ -80,6 +78,9 @@ type NetworkLoadBalancer struct { // waitForLoadBalancerReady controls whether we wait for the load balancer to be ready before completing the "Render" operation. waitForLoadBalancerReady bool + + // After this is found/created, we store the ARN + loadBalancerArn string } func (e *NetworkLoadBalancer) SetWaitForLoadBalancerReady(v bool) { @@ -94,66 +95,6 @@ func (e *NetworkLoadBalancer) CompareWithID() *string { return e.Name } -type NetworkLoadBalancerListener struct { - Port int - TargetGroupName string - SSLCertificateID string - SSLPolicy string -} - -func (e *NetworkLoadBalancerListener) mapToAWS(targetGroups []*TargetGroup, loadBalancerArn string) (*elbv2.CreateListenerInput, error) { - var tgARN string - for _, tg := range targetGroups { - if fi.ValueOf(tg.Name) == e.TargetGroupName { - tgARN = fi.ValueOf(tg.ARN) - } - } - if tgARN == "" { - return nil, fmt.Errorf("target group not found for NLB listener %+v", e) - } - - l := &elbv2.CreateListenerInput{ - DefaultActions: []*elbv2.Action{ - { - TargetGroupArn: aws.String(tgARN), - Type: aws.String(elbv2.ActionTypeEnumForward), - }, - }, - LoadBalancerArn: aws.String(loadBalancerArn), - Port: aws.Int64(int64(e.Port)), - } - - if e.SSLCertificateID != "" { - l.Certificates = []*elbv2.Certificate{} - l.Certificates = append(l.Certificates, &elbv2.Certificate{ - CertificateArn: aws.String(e.SSLCertificateID), - }) - l.Protocol = aws.String(elbv2.ProtocolEnumTls) - if e.SSLPolicy != "" { - l.SslPolicy = aws.String(e.SSLPolicy) - } - } else { - l.Protocol = aws.String(elbv2.ProtocolEnumTcp) - } - - return l, nil -} - -var _ fi.CloudupHasDependencies = &NetworkLoadBalancerListener{} - -func (e *NetworkLoadBalancerListener) GetDependencies(tasks map[string]fi.CloudupTask) []fi.CloudupTask { - return nil -} - -// OrderListenersByPort implements sort.Interface for []OrderListenersByPort, based on port number -type OrderListenersByPort []*NetworkLoadBalancerListener - -func (a OrderListenersByPort) Len() int { return len(a) } -func (a OrderListenersByPort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a OrderListenersByPort) Less(i, j int) bool { - return a[i].Port < a[j].Port -} - // The load balancer name 'api.renamenlbcluster.k8s.local' can only contain characters that are alphanumeric characters and hyphens(-)\n\tstatus code: 400, func findNetworkLoadBalancerByLoadBalancerName(cloud awsup.AWSCloud, loadBalancerName string) (*elbv2.LoadBalancer, error) { request := &elbv2.DescribeLoadBalancersInput{ @@ -324,18 +265,8 @@ func (e *NetworkLoadBalancer) Find(c *fi.CloudupContext) (*NetworkLoadBalancer, return nil, fmt.Errorf("error querying for NLB listeners :%v", err) } - actual.Listeners = []*NetworkLoadBalancerListener{} actual.TargetGroups = []*TargetGroup{} for _, l := range response.Listeners { - actualListener := &NetworkLoadBalancerListener{} - actualListener.Port = int(aws.Int64Value(l.Port)) - if len(l.Certificates) != 0 { - actualListener.SSLCertificateID = aws.StringValue(l.Certificates[0].CertificateArn) // What if there is more then one certificate, can we just grab the default certificate? we don't set it as default, we only set the one. - if l.SslPolicy != nil { - actualListener.SSLPolicy = aws.StringValue(l.SslPolicy) - } - } - // This will need to be rearranged when we recognized multiple listeners and target groups per NLB if len(l.DefaultActions) > 0 { targetGroupARN := l.DefaultActions[0].TargetGroupArn @@ -356,11 +287,8 @@ func (e *NetworkLoadBalancer) Find(c *fi.CloudupContext) (*NetworkLoadBalancer, if len(descResp.TargetGroups) != 1 { return nil, fmt.Errorf("unexpected DescribeTargetGroups response: %v", descResp) } - - actualListener.TargetGroupName = aws.StringValue(descResp.TargetGroups[0].TargetGroupName) } } - actual.Listeners = append(actual.Listeners, actualListener) } sort.Stable(OrderTargetGroupsByName(actual.TargetGroups)) @@ -443,6 +371,9 @@ func (e *NetworkLoadBalancer) Find(c *fi.CloudupContext) (*NetworkLoadBalancer, actual.WellKnownServices = e.WellKnownServices actual.Lifecycle = e.Lifecycle + // Store for other tasks + e.loadBalancerArn = aws.StringValue(lb.LoadBalancerArn) + klog.V(4).Infof("Found NLB %+v", actual) return actual, nil @@ -496,7 +427,6 @@ func (e *NetworkLoadBalancer) Run(c *fi.CloudupContext) error { func (e *NetworkLoadBalancer) Normalize(c *fi.CloudupContext) error { // We need to sort our arrays consistently, so we don't get spurious changes sort.Stable(OrderSubnetMappingsByName(e.SubnetMappings)) - sort.Stable(OrderListenersByPort(e.Listeners)) sort.Stable(OrderTargetGroupsByName(e.TargetGroups)) e.IpAddressType = fi.PtrTo("dualstack") @@ -569,10 +499,6 @@ func (_ *NetworkLoadBalancer) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Ne var loadBalancerName string var loadBalancerArn string - if len(e.Listeners) != len(e.TargetGroups) { - return fmt.Errorf("nlb listeners and target groups do not match: %v listeners vs %v target groups", len(e.Listeners), len(e.TargetGroups)) - } - if a == nil { if e.LoadBalancerName == nil { return fi.RequiredField("LoadBalancerName") @@ -619,7 +545,8 @@ func (_ *NetworkLoadBalancer) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Ne e.DNSName = lb.DNSName e.HostedZoneId = lb.CanonicalHostedZoneId e.VPC = &VPC{ID: lb.VpcId} - loadBalancerArn = fi.ValueOf(lb.LoadBalancerArn) + loadBalancerArn = aws.StringValue(lb.LoadBalancerArn) + e.loadBalancerArn = loadBalancerArn } if e.waitForLoadBalancerReady { @@ -634,20 +561,6 @@ func (_ *NetworkLoadBalancer) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Ne } } - { - for _, listener := range e.Listeners { - createListenerInput, err := listener.mapToAWS(e.TargetGroups, loadBalancerArn) - if err != nil { - return err - } - - klog.V(2).Infof("Creating Listener for NLB with port %v", listener.Port) - _, err = t.Cloud.ELBV2().CreateListener(createListenerInput) - if err != nil { - return fmt.Errorf("error creating listener for NLB: %v", err) - } - } - } } else { loadBalancerName = fi.ValueOf(a.LoadBalancerName) @@ -720,43 +633,6 @@ func (_ *NetworkLoadBalancer) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *Ne } } - if changes.Listeners != nil { - - if lb != nil { - - request := &elbv2.DescribeListenersInput{ - LoadBalancerArn: lb.LoadBalancerArn, - } - response, err := t.Cloud.ELBV2().DescribeListeners(request) - if err != nil { - return fmt.Errorf("error querying for NLB listeners :%v", err) - } - - for _, l := range response.Listeners { - // delete the listener before recreating it - _, err := t.Cloud.ELBV2().DeleteListener(&elbv2.DeleteListenerInput{ - ListenerArn: l.ListenerArn, - }) - if err != nil { - return fmt.Errorf("error deleting load balancer listener with arn = : %v : %v", l.ListenerArn, err) - } - } - } - - for _, listener := range changes.Listeners { - - awsListener, err := listener.mapToAWS(e.TargetGroups, loadBalancerArn) - if err != nil { - return err - } - - klog.V(2).Infof("Creating Listener for NLB with port %v", listener.Port) - _, err = t.Cloud.ELBV2().CreateListener(awsListener) - if err != nil { - return fmt.Errorf("error creating NLB listener: %v", err) - } - } - } if err := t.AddELBV2Tags(loadBalancerArn, e.Tags); err != nil { return err } @@ -792,20 +668,6 @@ type terraformNetworkLoadBalancerSubnetMapping struct { PrivateIPv4Address *string `cty:"private_ipv4_address"` } -type terraformNetworkLoadBalancerListener struct { - LoadBalancer *terraformWriter.Literal `cty:"load_balancer_arn"` - Port int64 `cty:"port"` - Protocol string `cty:"protocol"` - CertificateARN *string `cty:"certificate_arn"` - SSLPolicy *string `cty:"ssl_policy"` - DefaultAction []terraformNetworkLoadBalancerListenerAction `cty:"default_action"` -} - -type terraformNetworkLoadBalancerListenerAction struct { - Type string `cty:"type"` - TargetGroupARN *terraformWriter.Literal `cty:"target_group_arn"` -} - func (_ *NetworkLoadBalancer) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *NetworkLoadBalancer) error { nlbTF := &terraformNetworkLoadBalancer{ Name: *e.LoadBalancerName, @@ -839,48 +701,11 @@ func (_ *NetworkLoadBalancer) RenderTerraform(t *terraform.TerraformTarget, a, e } } - err := t.RenderResource("aws_lb", *e.Name, nlbTF) + err := t.RenderResource("aws_lb", e.TerraformName(), nlbTF) if err != nil { return err } - for _, listener := range e.Listeners { - var listenerTG *TargetGroup - for _, tg := range e.TargetGroups { - if aws.StringValue(tg.Name) == listener.TargetGroupName { - listenerTG = tg - break - } - } - if listenerTG == nil { - return fmt.Errorf("target group not found for NLB listener %+v", e) - } - listenerTF := &terraformNetworkLoadBalancerListener{ - LoadBalancer: e.TerraformLink(), - Port: int64(listener.Port), - DefaultAction: []terraformNetworkLoadBalancerListenerAction{ - { - Type: elbv2.ActionTypeEnumForward, - TargetGroupARN: listenerTG.TerraformLink(), - }, - }, - } - if listener.SSLCertificateID != "" { - listenerTF.CertificateARN = &listener.SSLCertificateID - listenerTF.Protocol = elbv2.ProtocolEnumTls - if listener.SSLPolicy != "" { - listenerTF.SSLPolicy = &listener.SSLPolicy - } - } else { - listenerTF.Protocol = elbv2.ProtocolEnumTcp - } - - err = t.RenderResource("aws_lb_listener", fmt.Sprintf("%v-%v", e.TerraformName(), listener.Port), listenerTF) - if err != nil { - return err - } - } - return nil } @@ -894,7 +719,7 @@ func (e *NetworkLoadBalancer) TerraformLink(params ...string) *terraformWriter.L if len(params) > 0 { prop = params[0] } - return terraformWriter.LiteralProperty("aws_lb", *e.Name, prop) + return terraformWriter.LiteralProperty("aws_lb", e.TerraformName(), prop) } // FindDeletions schedules deletion of the corresponding legacy classic load balancer when it no longer has targets. diff --git a/upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener.go b/upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener.go new file mode 100644 index 0000000000000..4fea0a218effe --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener.go @@ -0,0 +1,261 @@ +/* +Copyright 2019 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 awstasks + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "k8s.io/klog/v2" + "k8s.io/kops/upup/pkg/fi" + "k8s.io/kops/upup/pkg/fi/cloudup/awsup" + "k8s.io/kops/upup/pkg/fi/cloudup/terraform" + "k8s.io/kops/upup/pkg/fi/cloudup/terraformWriter" +) + +// +kops:fitask +type NetworkLoadBalancerListener struct { + Name *string + Lifecycle fi.Lifecycle + + NetworkLoadBalancer *NetworkLoadBalancer + + Port int + TargetGroup *TargetGroup + SSLCertificateID string + SSLPolicy string + + listenerArn string +} + +var _ fi.CompareWithID = &NetworkLoadBalancerListener{} +var _ fi.CloudupTaskNormalize = &NetworkLoadBalancerListener{} + +func (e *NetworkLoadBalancerListener) CompareWithID() *string { + return e.Name +} + +func (e *NetworkLoadBalancerListener) Find(c *fi.CloudupContext) (*NetworkLoadBalancerListener, error) { + ctx := c.Context() + + cloud := c.T.Cloud.(awsup.AWSCloud) + + if e.NetworkLoadBalancer == nil { + return nil, fi.RequiredField("NetworkLoadBalancer") + } + + loadBalancerArn := e.NetworkLoadBalancer.loadBalancerArn + if loadBalancerArn == "" { + return nil, nil + } + + var l *elbv2.Listener + { + request := &elbv2.DescribeListenersInput{ + LoadBalancerArn: &loadBalancerArn, + } + // TODO: Move to lbInfo? + var allListeners []*elbv2.Listener + if err := cloud.ELBV2().DescribeListenersPagesWithContext(ctx, request, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { + allListeners = append(allListeners, page.Listeners...) + return true + }); err != nil { + return nil, fmt.Errorf("error querying for NLB listeners :%v", err) + } + + var matches []*elbv2.Listener + for _, listener := range allListeners { + if aws.Int64Value(listener.Port) == int64(e.Port) { + matches = append(matches, listener) + } + } + if len(matches) == 0 { + return nil, nil + } + if len(matches) > 1 { + return nil, fmt.Errorf("found multiple listeners matching %+v", e) + } + l = matches[0] + } + + actual := &NetworkLoadBalancerListener{} + actual.listenerArn = aws.StringValue(l.ListenerArn) + + actual.Port = int(aws.Int64Value(l.Port)) + if len(l.Certificates) != 0 { + actual.SSLCertificateID = aws.StringValue(l.Certificates[0].CertificateArn) // What if there is more then one certificate, can we just grab the default certificate? we don't set it as default, we only set the one. + if l.SslPolicy != nil { + actual.SSLPolicy = aws.StringValue(l.SslPolicy) + } + } + + // This will need to be rearranged when we recognized multiple listeners and target groups per NLB + if len(l.DefaultActions) > 0 { + targetGroupARN := l.DefaultActions[0].TargetGroupArn + if targetGroupARN != nil { + actual.TargetGroup = &TargetGroup{ + ARN: targetGroupARN, + } + } + } + + _ = actual.Normalize(c) + actual.Lifecycle = e.Lifecycle + + // Avoid spurious changes + actual.Name = e.Name + actual.NetworkLoadBalancer = e.NetworkLoadBalancer + + klog.V(4).Infof("Found NLB listener %+v", actual) + + return actual, nil +} + +func (e *NetworkLoadBalancerListener) Run(c *fi.CloudupContext) error { + return fi.CloudupDefaultDeltaRunMethod(e, c) +} + +func (e *NetworkLoadBalancerListener) Normalize(c *fi.CloudupContext) error { + return nil +} + +func (*NetworkLoadBalancerListener) CheckChanges(a, e, changes *NetworkLoadBalancerListener) error { + return nil +} + +func (*NetworkLoadBalancerListener) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *NetworkLoadBalancerListener) error { + ctx := context.TODO() + + if e.NetworkLoadBalancer == nil { + return fi.RequiredField("NetworkLoadBalancer") + } + loadBalancerArn := e.NetworkLoadBalancer.loadBalancerArn + if loadBalancerArn == "" { + return fmt.Errorf("load balancer not yet created (arn not set)") + } + + if a != nil { + // TODO: Can we do better here? + klog.Warningf("deleting ELB listener %q for required changes (%+v)", a.listenerArn, changes) + + // delete the listener before recreating it + _, err := t.Cloud.ELBV2().DeleteListenerWithContext(ctx, &elbv2.DeleteListenerInput{ + ListenerArn: &a.listenerArn, + }) + if err != nil { + return fmt.Errorf("error deleting load balancer listener with arn=%q: %w", e.listenerArn, err) + } + a = nil + } + + if a == nil { + if e.TargetGroup == nil { + return fi.RequiredField("TargetGroup") + } + targetGroupARN := fi.ValueOf(e.TargetGroup.ARN) + if targetGroupARN == "" { + return fmt.Errorf("target group not yet created (arn not set)") + } + request := &elbv2.CreateListenerInput{ + DefaultActions: []*elbv2.Action{ + { + TargetGroupArn: aws.String(targetGroupARN), + Type: aws.String(elbv2.ActionTypeEnumForward), + }, + }, + LoadBalancerArn: aws.String(loadBalancerArn), + Port: aws.Int64(int64(e.Port)), + } + + if e.SSLCertificateID != "" { + request.Certificates = []*elbv2.Certificate{} + request.Certificates = append(request.Certificates, &elbv2.Certificate{ + CertificateArn: aws.String(e.SSLCertificateID), + }) + request.Protocol = aws.String(elbv2.ProtocolEnumTls) + if e.SSLPolicy != "" { + request.SslPolicy = aws.String(e.SSLPolicy) + } + } else { + request.Protocol = aws.String(elbv2.ProtocolEnumTcp) + } + + klog.V(2).Infof("Creating Listener for NLB with port %v", e.Port) + _, err := t.Cloud.ELBV2().CreateListenerWithContext(ctx, request) + if err != nil { + return fmt.Errorf("creating listener for NLB on port %v: %w", e.Port, err) + } + } + + // TODO: Tags on the listener? + + return nil +} + +type terraformNetworkLoadBalancerListener struct { + LoadBalancer *terraformWriter.Literal `cty:"load_balancer_arn"` + Port int64 `cty:"port"` + Protocol string `cty:"protocol"` + CertificateARN *string `cty:"certificate_arn"` + SSLPolicy *string `cty:"ssl_policy"` + DefaultAction []terraformNetworkLoadBalancerListenerAction `cty:"default_action"` +} + +type terraformNetworkLoadBalancerListenerAction struct { + Type string `cty:"type"` + TargetGroupARN *terraformWriter.Literal `cty:"target_group_arn"` +} + +func (_ *NetworkLoadBalancerListener) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *NetworkLoadBalancerListener) error { + if e.TargetGroup == nil { + return fi.RequiredField("TargetGroup") + } + + listenerTF := &terraformNetworkLoadBalancerListener{ + LoadBalancer: e.NetworkLoadBalancer.TerraformLink(), + Port: int64(e.Port), + DefaultAction: []terraformNetworkLoadBalancerListenerAction{ + { + Type: elbv2.ActionTypeEnumForward, + TargetGroupARN: e.TargetGroup.TerraformLink(), + }, + }, + } + if e.SSLCertificateID != "" { + listenerTF.CertificateARN = &e.SSLCertificateID + listenerTF.Protocol = elbv2.ProtocolEnumTls + if e.SSLPolicy != "" { + listenerTF.SSLPolicy = &e.SSLPolicy + } + } else { + listenerTF.Protocol = elbv2.ProtocolEnumTcp + } + + err := t.RenderResource("aws_lb_listener", e.TerraformName(), listenerTF) + if err != nil { + return err + } + + return nil +} + +func (e *NetworkLoadBalancerListener) TerraformName() string { + tfName := fmt.Sprintf("%v-%v", e.NetworkLoadBalancer.TerraformName(), e.Port) + return tfName +} diff --git a/upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener_fitask.go b/upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener_fitask.go new file mode 100644 index 0000000000000..07175a20ccf22 --- /dev/null +++ b/upup/pkg/fi/cloudup/awstasks/networkloadbalancerlistener_fitask.go @@ -0,0 +1,52 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 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. +*/ + +// Code generated by fitask. DO NOT EDIT. + +package awstasks + +import ( + "k8s.io/kops/upup/pkg/fi" +) + +// NetworkLoadBalancerListener + +var _ fi.HasLifecycle = &NetworkLoadBalancerListener{} + +// GetLifecycle returns the Lifecycle of the object, implementing fi.HasLifecycle +func (o *NetworkLoadBalancerListener) GetLifecycle() fi.Lifecycle { + return o.Lifecycle +} + +// SetLifecycle sets the Lifecycle of the object, implementing fi.SetLifecycle +func (o *NetworkLoadBalancerListener) SetLifecycle(lifecycle fi.Lifecycle) { + o.Lifecycle = lifecycle +} + +var _ fi.HasName = &NetworkLoadBalancerListener{} + +// GetName returns the Name of the object, implementing fi.HasName +func (o *NetworkLoadBalancerListener) GetName() *string { + return o.Name +} + +// String is the stringer function for the task, producing readable output using fi.TaskAsString +func (o *NetworkLoadBalancerListener) String() string { + return fi.CloudupTaskAsString(o) +} From c9b9a47b942e455f489a43278a70ff7bce0b6ffe Mon Sep 17 00:00:00 2001 From: justinsb Date: Sun, 4 Feb 2024 15:05:55 -0500 Subject: [PATCH 2/2] cloudmock: Implement WithContext methods for ELBv2 Also switch methods that were not passing a context. --- cloudmock/aws/mockelbv2/listeners.go | 22 ++++++++++--------- .../cloudup/awstasks/network_load_balancer.go | 12 ++++++---- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/cloudmock/aws/mockelbv2/listeners.go b/cloudmock/aws/mockelbv2/listeners.go index b8937fae8d7ef..89fbfc7303c13 100644 --- a/cloudmock/aws/mockelbv2/listeners.go +++ b/cloudmock/aws/mockelbv2/listeners.go @@ -21,39 +21,41 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/elbv2" "k8s.io/klog/v2" ) -func (m *MockELBV2) DescribeListeners(request *elbv2.DescribeListenersInput) (*elbv2.DescribeListenersOutput, error) { +func (m *MockELBV2) DescribeListenersPagesWithContext(ctx aws.Context, request *elbv2.DescribeListenersInput, callback func(*elbv2.DescribeListenersOutput, bool) bool, options ...request.Option) error { m.mutex.Lock() defer m.mutex.Unlock() - klog.Infof("DescribeListeners v2 %v", request) + klog.Infof("DescribeListenersPagesWithContext v2 %v", request) - resp := &elbv2.DescribeListenersOutput{ + page := &elbv2.DescribeListenersOutput{ Listeners: make([]*elbv2.Listener, 0), } for _, l := range m.Listeners { listener := l.description if aws.StringValue(request.LoadBalancerArn) == aws.StringValue(listener.LoadBalancerArn) { - resp.Listeners = append(resp.Listeners, &listener) + page.Listeners = append(page.Listeners, &listener) } else { for _, reqARN := range request.ListenerArns { if aws.StringValue(reqARN) == aws.StringValue(listener.ListenerArn) { - resp.Listeners = append(resp.Listeners, &listener) + page.Listeners = append(page.Listeners, &listener) } } } } - return resp, nil + callback(page, true) + return nil } -func (m *MockELBV2) CreateListener(request *elbv2.CreateListenerInput) (*elbv2.CreateListenerOutput, error) { +func (m *MockELBV2) CreateListenerWithContext(ctx aws.Context, request *elbv2.CreateListenerInput, opts ...request.Option) (*elbv2.CreateListenerOutput, error) { m.mutex.Lock() defer m.mutex.Unlock() - klog.Infof("CreateListener v2 %v", request) + klog.Infof("CreateListenerWithContext v2 %v", request) l := elbv2.Listener{ DefaultActions: request.DefaultActions, @@ -96,11 +98,11 @@ func (m *MockELBV2) CreateListener(request *elbv2.CreateListenerInput) (*elbv2.C return &elbv2.CreateListenerOutput{Listeners: []*elbv2.Listener{&l}}, nil } -func (m *MockELBV2) DeleteListener(request *elbv2.DeleteListenerInput) (*elbv2.DeleteListenerOutput, error) { +func (m *MockELBV2) DeleteListenerWithContext(ctx aws.Context, request *elbv2.DeleteListenerInput, opts ...request.Option) (*elbv2.DeleteListenerOutput, error) { m.mutex.Lock() defer m.mutex.Unlock() - klog.Infof("DeleteListener v2 %v", request) + klog.Infof("DeleteListenerWithContext v2 %v", request) lARN := aws.StringValue(request.ListenerArn) if _, ok := m.Listeners[lARN]; !ok { diff --git a/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go b/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go index a099fae825c76..2fc866db73af4 100644 --- a/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go +++ b/upup/pkg/fi/cloudup/awstasks/network_load_balancer.go @@ -196,6 +196,7 @@ func (e *NetworkLoadBalancer) getHostedZoneId() *string { } func (e *NetworkLoadBalancer) Find(c *fi.CloudupContext) (*NetworkLoadBalancer, error) { + ctx := c.Context() cloud := c.T.Cloud.(awsup.AWSCloud) lb, err := cloud.FindELBV2ByNameTag(e.Tags["Name"]) @@ -260,13 +261,16 @@ func (e *NetworkLoadBalancer) Find(c *fi.CloudupContext) (*NetworkLoadBalancer, request := &elbv2.DescribeListenersInput{ LoadBalancerArn: loadBalancerArn, } - response, err := cloud.ELBV2().DescribeListeners(request) - if err != nil { - return nil, fmt.Errorf("error querying for NLB listeners :%v", err) + var listeners []*elbv2.Listener + if err := cloud.ELBV2().DescribeListenersPagesWithContext(ctx, request, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { + listeners = append(listeners, page.Listeners...) + return true + }); err != nil { + return nil, fmt.Errorf("listing NLB listeners: %w", err) } actual.TargetGroups = []*TargetGroup{} - for _, l := range response.Listeners { + for _, l := range listeners { // This will need to be rearranged when we recognized multiple listeners and target groups per NLB if len(l.DefaultActions) > 0 { targetGroupARN := l.DefaultActions[0].TargetGroupArn