mirror of https://github.com/kubernetes/kops.git
901 lines
27 KiB
Go
901 lines
27 KiB
Go
/*
|
|
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 (
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/service/elb"
|
|
"github.com/aws/aws-sdk-go/service/elbv2"
|
|
"github.com/aws/aws-sdk-go/service/route53"
|
|
"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"
|
|
)
|
|
|
|
// NetworkLoadBalancer manages an NLB. We find the existing NLB using the Name tag.
|
|
var _ DNSTarget = &NetworkLoadBalancer{}
|
|
|
|
// +kops:fitask
|
|
type NetworkLoadBalancer struct {
|
|
// We use the Name tag to find the existing NLB, because we are (more or less) unrestricted when
|
|
// it comes to tag values, but the LoadBalancerName is length limited
|
|
Name *string
|
|
Lifecycle fi.Lifecycle
|
|
|
|
// LoadBalancerName is the name in NLB, possibly different from our name
|
|
// (NLB is restricted as to names, so we have limited choices!)
|
|
// We use the Name tag to find the existing NLB.
|
|
LoadBalancerName *string
|
|
CLBName *string
|
|
|
|
DNSName *string
|
|
HostedZoneId *string
|
|
|
|
SubnetMappings []*SubnetMapping
|
|
|
|
Listeners []*NetworkLoadBalancerListener
|
|
|
|
Scheme *string
|
|
|
|
CrossZoneLoadBalancing *bool
|
|
|
|
IpAddressType *string
|
|
|
|
Tags map[string]string
|
|
ForAPIServer bool
|
|
|
|
Type *string
|
|
|
|
VPC *VPC
|
|
TargetGroups []*TargetGroup
|
|
AccessLog *NetworkLoadBalancerAccessLog
|
|
}
|
|
|
|
var _ fi.CompareWithID = &NetworkLoadBalancer{}
|
|
var _ fi.CloudupTaskNormalize = &NetworkLoadBalancer{}
|
|
var _ fi.CloudupProducesDeletions = &NetworkLoadBalancer{}
|
|
|
|
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{
|
|
Names: []*string{&loadBalancerName},
|
|
}
|
|
found, err := describeNetworkLoadBalancers(cloud, request, func(lb *elbv2.LoadBalancer) bool {
|
|
// TODO: Filter by cluster?
|
|
|
|
if aws.StringValue(lb.LoadBalancerName) == loadBalancerName {
|
|
return true
|
|
}
|
|
|
|
klog.Warningf("Got NLB with unexpected name: %q", aws.StringValue(lb.LoadBalancerName))
|
|
return false
|
|
})
|
|
if err != nil {
|
|
if awsError, ok := err.(awserr.Error); ok {
|
|
if awsError.Code() == "LoadBalancerNotFound" {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("error listing NLBs: %v", err)
|
|
}
|
|
|
|
if len(found) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(found) != 1 {
|
|
return nil, fmt.Errorf("Found multiple NLBs with name %q", loadBalancerName)
|
|
}
|
|
|
|
return found[0], nil
|
|
}
|
|
|
|
func findNetworkLoadBalancerByAlias(cloud awsup.AWSCloud, alias *route53.AliasTarget) (*elbv2.LoadBalancer, error) {
|
|
// TODO: Any way to avoid listing all NLBs?
|
|
request := &elbv2.DescribeLoadBalancersInput{}
|
|
|
|
dnsName := aws.StringValue(alias.DNSName)
|
|
matchDnsName := strings.TrimSuffix(dnsName, ".")
|
|
if matchDnsName == "" {
|
|
return nil, fmt.Errorf("DNSName not set on AliasTarget")
|
|
}
|
|
|
|
matchHostedZoneId := aws.StringValue(alias.HostedZoneId)
|
|
|
|
found, err := describeNetworkLoadBalancers(cloud, request, func(lb *elbv2.LoadBalancer) bool {
|
|
// TODO: Filter by cluster?
|
|
|
|
if matchHostedZoneId != aws.StringValue(lb.CanonicalHostedZoneId) {
|
|
return false
|
|
}
|
|
|
|
lbDnsName := aws.StringValue(lb.DNSName)
|
|
lbDnsName = strings.TrimSuffix(lbDnsName, ".")
|
|
return lbDnsName == matchDnsName || "dualstack."+lbDnsName == matchDnsName
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing NLBs: %v", err)
|
|
}
|
|
|
|
if len(found) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(found) != 1 {
|
|
return nil, fmt.Errorf("Found multiple NLBs with DNSName %q", dnsName)
|
|
}
|
|
|
|
return found[0], nil
|
|
}
|
|
|
|
func describeNetworkLoadBalancers(cloud awsup.AWSCloud, request *elbv2.DescribeLoadBalancersInput, filter func(*elbv2.LoadBalancer) bool) ([]*elbv2.LoadBalancer, error) {
|
|
var found []*elbv2.LoadBalancer
|
|
err := cloud.ELBV2().DescribeLoadBalancersPages(request, func(p *elbv2.DescribeLoadBalancersOutput, lastPage bool) (shouldContinue bool) {
|
|
for _, lb := range p.LoadBalancers {
|
|
if filter(lb) {
|
|
found = append(found, lb)
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing NLBs: %v", err)
|
|
}
|
|
|
|
return found, nil
|
|
}
|
|
|
|
func (e *NetworkLoadBalancer) getDNSName() *string {
|
|
return e.DNSName
|
|
}
|
|
|
|
func (e *NetworkLoadBalancer) getHostedZoneId() *string {
|
|
return e.HostedZoneId
|
|
}
|
|
|
|
func (e *NetworkLoadBalancer) Find(c *fi.CloudupContext) (*NetworkLoadBalancer, error) {
|
|
cloud := c.T.Cloud.(awsup.AWSCloud)
|
|
|
|
lb, err := cloud.FindELBV2ByNameTag(e.Tags["Name"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lb == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
loadBalancerArn := lb.LoadBalancerArn
|
|
|
|
actual := &NetworkLoadBalancer{}
|
|
actual.Name = e.Name
|
|
actual.CLBName = e.CLBName
|
|
actual.LoadBalancerName = lb.LoadBalancerName
|
|
actual.DNSName = lb.DNSName
|
|
actual.HostedZoneId = lb.CanonicalHostedZoneId // CanonicalHostedZoneNameID
|
|
actual.Scheme = lb.Scheme
|
|
actual.VPC = &VPC{ID: lb.VpcId}
|
|
actual.Type = lb.Type
|
|
actual.IpAddressType = lb.IpAddressType
|
|
|
|
tagMap, err := cloud.DescribeELBV2Tags([]string{*loadBalancerArn})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
actual.Tags = make(map[string]string)
|
|
for _, tag := range tagMap[*loadBalancerArn] {
|
|
if strings.HasPrefix(aws.StringValue(tag.Key), "aws:cloudformation:") {
|
|
continue
|
|
}
|
|
actual.Tags[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value)
|
|
}
|
|
|
|
for _, az := range lb.AvailabilityZones {
|
|
sm := &SubnetMapping{
|
|
Subnet: &Subnet{ID: az.SubnetId},
|
|
}
|
|
for _, a := range az.LoadBalancerAddresses {
|
|
if a.PrivateIPv4Address != nil {
|
|
if sm.PrivateIPv4Address != nil {
|
|
return nil, fmt.Errorf("NLB has more then one PrivateIPv4Address, which is unexpected. This is a bug in kOps, please open a GitHub issue.")
|
|
}
|
|
sm.PrivateIPv4Address = a.PrivateIPv4Address
|
|
}
|
|
if a.AllocationId != nil {
|
|
if sm.AllocationID != nil {
|
|
return nil, fmt.Errorf("NLB has more then one AllocationID per subnet, which is unexpected. This is a bug in kOps, please open a GitHub issue.")
|
|
}
|
|
sm.AllocationID = a.AllocationId
|
|
}
|
|
}
|
|
actual.SubnetMappings = append(actual.SubnetMappings, sm)
|
|
}
|
|
|
|
{
|
|
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)
|
|
}
|
|
|
|
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
|
|
if targetGroupARN != nil {
|
|
targetGroupName, err := awsup.GetTargetGroupNameFromARN(fi.ValueOf(targetGroupARN))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
actual.TargetGroups = append(actual.TargetGroups, &TargetGroup{ARN: targetGroupARN, Name: fi.PtrTo(targetGroupName)})
|
|
|
|
cloud := c.T.Cloud.(awsup.AWSCloud)
|
|
descResp, err := cloud.ELBV2().DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{
|
|
TargetGroupArns: []*string{targetGroupARN},
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error querying for NLB listener target groups: %v", err)
|
|
}
|
|
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))
|
|
|
|
}
|
|
|
|
{
|
|
lbAttributes, err := findNetworkLoadBalancerAttributes(cloud, aws.StringValue(loadBalancerArn))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
klog.V(4).Infof("NLB Load Balancer attributes: %+v", lbAttributes)
|
|
|
|
for _, attribute := range lbAttributes {
|
|
if attribute.Value == nil {
|
|
continue
|
|
}
|
|
switch key, value := attribute.Key, attribute.Value; *key {
|
|
case "load_balancing.cross_zone.enabled":
|
|
b, err := strconv.ParseBool(*value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
actual.CrossZoneLoadBalancing = fi.PtrTo(b)
|
|
case "access_logs.s3.enabled":
|
|
b, err := strconv.ParseBool(*value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if actual.AccessLog == nil {
|
|
actual.AccessLog = &NetworkLoadBalancerAccessLog{}
|
|
}
|
|
actual.AccessLog.Enabled = fi.PtrTo(b)
|
|
case "access_logs.s3.bucket":
|
|
if actual.AccessLog == nil {
|
|
actual.AccessLog = &NetworkLoadBalancerAccessLog{}
|
|
}
|
|
if fi.ValueOf(value) != "" {
|
|
actual.AccessLog.S3BucketName = value
|
|
}
|
|
case "access_logs.s3.prefix":
|
|
if actual.AccessLog == nil {
|
|
actual.AccessLog = &NetworkLoadBalancerAccessLog{}
|
|
}
|
|
if fi.ValueOf(value) != "" {
|
|
actual.AccessLog.S3BucketPrefix = value
|
|
}
|
|
default:
|
|
klog.V(2).Infof("unsupported key -- ignoring, %v.\n", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Avoid spurious mismatches
|
|
if subnetMappingSlicesEqualIgnoreOrder(actual.SubnetMappings, e.SubnetMappings) {
|
|
actual.SubnetMappings = e.SubnetMappings
|
|
}
|
|
if e.DNSName == nil {
|
|
e.DNSName = actual.DNSName
|
|
}
|
|
if e.HostedZoneId == nil {
|
|
e.HostedZoneId = actual.HostedZoneId
|
|
}
|
|
if e.LoadBalancerName == nil {
|
|
e.LoadBalancerName = actual.LoadBalancerName
|
|
}
|
|
|
|
// An existing internal NLB can't be updated to dualstack.
|
|
if fi.ValueOf(actual.Scheme) == elbv2.LoadBalancerSchemeEnumInternal && fi.ValueOf(actual.IpAddressType) == elbv2.IpAddressTypeIpv4 {
|
|
e.IpAddressType = actual.IpAddressType
|
|
}
|
|
|
|
// We allow for the LoadBalancerName to be wrong:
|
|
// 1. We don't want to force a rename of the NLB, because that is a destructive operation
|
|
if fi.ValueOf(e.LoadBalancerName) != fi.ValueOf(actual.LoadBalancerName) {
|
|
klog.V(2).Infof("Reusing existing load balancer with name: %q", aws.StringValue(actual.LoadBalancerName))
|
|
e.LoadBalancerName = actual.LoadBalancerName
|
|
}
|
|
|
|
_ = actual.Normalize(c)
|
|
actual.ForAPIServer = e.ForAPIServer
|
|
actual.Lifecycle = e.Lifecycle
|
|
|
|
klog.V(4).Infof("Found NLB %+v", actual)
|
|
|
|
return actual, nil
|
|
}
|
|
|
|
var _ fi.HasAddress = &NetworkLoadBalancer{}
|
|
|
|
func (e *NetworkLoadBalancer) IsForAPIServer() bool {
|
|
return e.ForAPIServer
|
|
}
|
|
|
|
func (e *NetworkLoadBalancer) FindAddresses(context *fi.CloudupContext) ([]string, error) {
|
|
var addresses []string
|
|
|
|
cloud := context.T.Cloud.(awsup.AWSCloud)
|
|
cluster := context.T.Cluster
|
|
|
|
{
|
|
lb, err := cloud.FindELBV2ByNameTag(e.Tags["Name"])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find load balancer matching %q: %w", e.Tags["Name"], err)
|
|
}
|
|
if lb != nil && fi.ValueOf(lb.DNSName) != "" {
|
|
addresses = append(addresses, fi.ValueOf(lb.DNSName))
|
|
}
|
|
}
|
|
|
|
if cluster.UsesNoneDNS() {
|
|
nis, err := cloud.FindELBV2NetworkInterfacesByName(fi.ValueOf(e.VPC.ID), fi.ValueOf(e.LoadBalancerName))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to find network interfaces matching %q: %w", fi.ValueOf(e.LoadBalancerName), err)
|
|
}
|
|
for _, ni := range nis {
|
|
if fi.ValueOf(ni.PrivateIpAddress) != "" {
|
|
addresses = append(addresses, fi.ValueOf(ni.PrivateIpAddress))
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Strings(addresses)
|
|
|
|
return addresses, nil
|
|
}
|
|
|
|
func (e *NetworkLoadBalancer) Run(c *fi.CloudupContext) error {
|
|
return fi.CloudupDefaultDeltaRunMethod(e, c)
|
|
}
|
|
|
|
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))
|
|
return nil
|
|
}
|
|
|
|
func (*NetworkLoadBalancer) CheckChanges(a, e, changes *NetworkLoadBalancer) error {
|
|
if a == nil {
|
|
if fi.ValueOf(e.Name) == "" {
|
|
return fi.RequiredField("Name")
|
|
}
|
|
if len(e.SubnetMappings) == 0 {
|
|
return fi.RequiredField("SubnetMappings")
|
|
}
|
|
|
|
if e.CrossZoneLoadBalancing != nil {
|
|
if e.CrossZoneLoadBalancing == nil {
|
|
return fi.RequiredField("CrossZoneLoadBalancing")
|
|
}
|
|
}
|
|
|
|
if e.AccessLog != nil {
|
|
if e.AccessLog.Enabled == nil {
|
|
return fi.RequiredField("Accesslog.Enabled")
|
|
}
|
|
if *e.AccessLog.Enabled {
|
|
if e.AccessLog.S3BucketName == nil {
|
|
return fi.RequiredField("Accesslog.S3Bucket")
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if len(changes.SubnetMappings) > 0 {
|
|
expectedSubnets := make(map[string]*string)
|
|
for _, s := range e.SubnetMappings {
|
|
if s.AllocationID != nil {
|
|
expectedSubnets[*s.Subnet.ID] = s.AllocationID
|
|
} else if s.PrivateIPv4Address != nil {
|
|
expectedSubnets[*s.Subnet.ID] = s.PrivateIPv4Address
|
|
} else {
|
|
expectedSubnets[*s.Subnet.ID] = nil
|
|
}
|
|
}
|
|
|
|
for _, s := range a.SubnetMappings {
|
|
eIP, ok := expectedSubnets[*s.Subnet.ID]
|
|
if !ok {
|
|
return fmt.Errorf("network load balancers do not support detaching subnets")
|
|
}
|
|
if fi.ValueOf(eIP) != fi.ValueOf(s.PrivateIPv4Address) || fi.ValueOf(eIP) != fi.ValueOf(s.AllocationID) {
|
|
return fmt.Errorf("network load balancers do not support modifying address settings")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (_ *NetworkLoadBalancer) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *NetworkLoadBalancer) error {
|
|
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")
|
|
}
|
|
for _, tg := range e.TargetGroups {
|
|
if tg.ARN == nil {
|
|
return fmt.Errorf("missing required target group ARN for NLB creation %v", tg)
|
|
}
|
|
}
|
|
|
|
loadBalancerName = *e.LoadBalancerName
|
|
|
|
{
|
|
request := &elbv2.CreateLoadBalancerInput{}
|
|
request.Name = e.LoadBalancerName
|
|
request.Scheme = e.Scheme
|
|
request.Type = e.Type
|
|
request.IpAddressType = e.IpAddressType
|
|
request.Tags = awsup.ELBv2Tags(e.Tags)
|
|
|
|
for _, subnetMapping := range e.SubnetMappings {
|
|
request.SubnetMappings = append(request.SubnetMappings, &elbv2.SubnetMapping{
|
|
SubnetId: subnetMapping.Subnet.ID,
|
|
AllocationId: subnetMapping.AllocationID,
|
|
PrivateIPv4Address: subnetMapping.PrivateIPv4Address,
|
|
})
|
|
}
|
|
|
|
klog.V(2).Infof("Creating NLB %q", loadBalancerName)
|
|
|
|
response, err := t.Cloud.ELBV2().CreateLoadBalancer(request)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating NLB %q: %w", loadBalancerName, err)
|
|
}
|
|
if len(response.LoadBalancers) != 1 {
|
|
return fmt.Errorf("error creating NLB %q: found %d", loadBalancerName, len(response.LoadBalancers))
|
|
}
|
|
|
|
lb := response.LoadBalancers[0]
|
|
e.DNSName = lb.DNSName
|
|
e.HostedZoneId = lb.CanonicalHostedZoneId
|
|
e.VPC = &VPC{ID: lb.VpcId}
|
|
loadBalancerArn = fi.ValueOf(lb.LoadBalancerArn)
|
|
|
|
}
|
|
|
|
// Wait for all load balancer components to be created (including network interfaces needed for NoneDNS).
|
|
// Limiting this to clusters using NoneDNS because load balancer creation is quite slow.
|
|
for _, tg := range e.TargetGroups {
|
|
if strings.HasPrefix(fi.ValueOf(tg.Name), "kops-controller") {
|
|
klog.Infof("Waiting for load balancer %q to be created...", loadBalancerName)
|
|
request := &elbv2.DescribeLoadBalancersInput{
|
|
Names: []*string{&loadBalancerName},
|
|
}
|
|
err := t.Cloud.ELBV2().WaitUntilLoadBalancerAvailable(request)
|
|
if err != nil {
|
|
return fmt.Errorf("error waiting for NLB %q: %w", loadBalancerName, err)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
{
|
|
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)
|
|
|
|
lb, err := findNetworkLoadBalancerByLoadBalancerName(t.Cloud, loadBalancerName)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting load balancer by name: %v", err)
|
|
}
|
|
|
|
loadBalancerArn = fi.ValueOf(lb.LoadBalancerArn)
|
|
|
|
if changes.IpAddressType != nil {
|
|
request := &elbv2.SetIpAddressTypeInput{
|
|
IpAddressType: e.IpAddressType,
|
|
LoadBalancerArn: lb.LoadBalancerArn,
|
|
}
|
|
if _, err := t.Cloud.ELBV2().SetIpAddressType(request); err != nil {
|
|
return fmt.Errorf("error setting the IP addresses type: %v", err)
|
|
}
|
|
}
|
|
|
|
if changes.SubnetMappings != nil {
|
|
actualSubnets := make(map[string]*string)
|
|
for _, s := range a.SubnetMappings {
|
|
// actualSubnets[*s.Subnet.ID] = s
|
|
if s.AllocationID != nil {
|
|
actualSubnets[*s.Subnet.ID] = s.AllocationID
|
|
}
|
|
if s.PrivateIPv4Address != nil {
|
|
actualSubnets[*s.Subnet.ID] = s.PrivateIPv4Address
|
|
}
|
|
}
|
|
|
|
var awsSubnetMappings []*elbv2.SubnetMapping
|
|
hasChanges := false
|
|
for _, s := range e.SubnetMappings {
|
|
aIP, ok := actualSubnets[*s.Subnet.ID]
|
|
if !ok || (fi.ValueOf(s.PrivateIPv4Address) != fi.ValueOf(aIP) && fi.ValueOf(s.AllocationID) != fi.ValueOf(aIP)) {
|
|
hasChanges = true
|
|
}
|
|
awsSubnetMappings = append(awsSubnetMappings, &elbv2.SubnetMapping{
|
|
SubnetId: s.Subnet.ID,
|
|
AllocationId: s.AllocationID,
|
|
PrivateIPv4Address: s.PrivateIPv4Address,
|
|
})
|
|
}
|
|
|
|
if hasChanges {
|
|
request := &elbv2.SetSubnetsInput{}
|
|
request.SetLoadBalancerArn(loadBalancerArn)
|
|
request.SetSubnetMappings(awsSubnetMappings)
|
|
|
|
klog.V(2).Infof("Attaching Load Balancer to new subnets")
|
|
if _, err := t.Cloud.ELBV2().SetSubnets(request); err != nil {
|
|
return fmt.Errorf("error attaching load balancer to new subnets: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if err := t.RemoveELBV2Tags(loadBalancerArn, e.Tags); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := e.modifyLoadBalancerAttributes(t, a, e, changes, loadBalancerArn); err != nil {
|
|
klog.Infof("error modifying NLB attributes: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type terraformNetworkLoadBalancer struct {
|
|
Name string `cty:"name"`
|
|
Internal bool `cty:"internal"`
|
|
Type string `cty:"load_balancer_type"`
|
|
SubnetMappings []terraformNetworkLoadBalancerSubnetMapping `cty:"subnet_mapping"`
|
|
CrossZoneLoadBalancing bool `cty:"enable_cross_zone_load_balancing"`
|
|
AccessLog *terraformNetworkLoadBalancerAccessLog `cty:"access_logs"`
|
|
|
|
Tags map[string]string `cty:"tags"`
|
|
}
|
|
|
|
type terraformNetworkLoadBalancerSubnetMapping struct {
|
|
Subnet *terraformWriter.Literal `cty:"subnet_id"`
|
|
AllocationID *string `cty:"allocation_id"`
|
|
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,
|
|
Internal: fi.ValueOf(e.Scheme) == elbv2.LoadBalancerSchemeEnumInternal,
|
|
Type: elbv2.LoadBalancerTypeEnumNetwork,
|
|
Tags: e.Tags,
|
|
CrossZoneLoadBalancing: fi.ValueOf(e.CrossZoneLoadBalancing),
|
|
}
|
|
|
|
for _, subnetMapping := range e.SubnetMappings {
|
|
nlbTF.SubnetMappings = append(nlbTF.SubnetMappings, terraformNetworkLoadBalancerSubnetMapping{
|
|
Subnet: subnetMapping.Subnet.TerraformLink(),
|
|
AllocationID: subnetMapping.AllocationID,
|
|
PrivateIPv4Address: subnetMapping.PrivateIPv4Address,
|
|
})
|
|
}
|
|
|
|
if e.AccessLog != nil && fi.ValueOf(e.AccessLog.Enabled) {
|
|
nlbTF.AccessLog = &terraformNetworkLoadBalancerAccessLog{
|
|
Enabled: e.AccessLog.Enabled,
|
|
S3BucketName: e.AccessLog.S3BucketName,
|
|
S3BucketPrefix: e.AccessLog.S3BucketPrefix,
|
|
}
|
|
}
|
|
|
|
err := t.RenderResource("aws_lb", *e.Name, 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.Name, listener.Port), listenerTF)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (e *NetworkLoadBalancer) TerraformLink(params ...string) *terraformWriter.Literal {
|
|
prop := "id"
|
|
if len(params) > 0 {
|
|
prop = params[0]
|
|
}
|
|
return terraformWriter.LiteralProperty("aws_lb", *e.Name, prop)
|
|
}
|
|
|
|
// FindDeletions schedules deletion of the corresponding legacy classic load balancer when it no longer has targets.
|
|
func (e *NetworkLoadBalancer) FindDeletions(context *fi.CloudupContext) ([]fi.CloudupDeletion, error) {
|
|
if e.CLBName == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
cloud := context.T.Cloud.(awsup.AWSCloud)
|
|
|
|
lb, err := cloud.FindELBByNameTag(fi.ValueOf(e.CLBName))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if lb == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Testing shows that the instances are deregistered immediately after the apply_cluster.
|
|
// TODO: Figure out how to delay deregistration until instances are terminated.
|
|
//if len(lb.Instances) > 0 {
|
|
// klog.V(2).Infof("CLB %s has targets; not scheduling deletion", *lb.LoadBalancerName)
|
|
// return nil, nil
|
|
//}
|
|
|
|
actual := &deleteClassicLoadBalancer{}
|
|
actual.LoadBalancerName = lb.LoadBalancerName
|
|
|
|
klog.V(4).Infof("Found CLB %+v", actual)
|
|
|
|
return []fi.CloudupDeletion{actual}, nil
|
|
}
|
|
|
|
type deleteClassicLoadBalancer struct {
|
|
// LoadBalancerName is the name in ELB, possibly different from our name
|
|
// (ELB is restricted as to names, so we have limited choices!)
|
|
LoadBalancerName *string
|
|
}
|
|
|
|
func (d deleteClassicLoadBalancer) Delete(t fi.CloudupTarget) error {
|
|
awsTarget, ok := t.(*awsup.AWSAPITarget)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected target type for deletion: %T", t)
|
|
}
|
|
|
|
_, err := awsTarget.Cloud.ELB().DeleteLoadBalancer(&elb.DeleteLoadBalancerInput{
|
|
LoadBalancerName: d.LoadBalancerName,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("deleting classic LoadBalancer: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d deleteClassicLoadBalancer) TaskName() string {
|
|
return "ClassicLoadBalancer"
|
|
}
|
|
|
|
func (d deleteClassicLoadBalancer) Item() string {
|
|
return *d.LoadBalancerName
|
|
}
|