mirror of https://github.com/kubernetes/kops.git
400 lines
11 KiB
Go
400 lines
11 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"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/service/ec2"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
"k8s.io/klog/v2"
|
|
raws "k8s.io/kops/pkg/resources/aws"
|
|
"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 NatGateway struct {
|
|
Name *string
|
|
Lifecycle fi.Lifecycle
|
|
|
|
ElasticIP *ElasticIP
|
|
Subnet *Subnet
|
|
ID *string
|
|
|
|
EgressId *string
|
|
|
|
// Shared is set if this is a shared NatGateway
|
|
Shared *bool
|
|
|
|
// Tags is a map of aws tags that are added to the NatGateway
|
|
Tags map[string]string
|
|
|
|
// We can't tag NatGateways, so we have to find through a surrogate
|
|
AssociatedRouteTable *RouteTable
|
|
}
|
|
|
|
var _ fi.CompareWithID = &NatGateway{}
|
|
|
|
func (e *NatGateway) CompareWithID() *string {
|
|
// Match by ID (NAT Gateways don't have tags, so they don't have a name in EC2)
|
|
return e.ID
|
|
}
|
|
|
|
func (e *NatGateway) Find(c *fi.CloudupContext) (*NatGateway, error) {
|
|
cloud := c.T.Cloud.(awsup.AWSCloud)
|
|
var ngw *ec2.NatGateway
|
|
actual := &NatGateway{}
|
|
|
|
if fi.ValueOf(e.ID) != "" {
|
|
// We have an existing NGW, lets look up the EIP
|
|
var ngwIds []*string
|
|
ngwIds = append(ngwIds, e.ID)
|
|
|
|
request := &ec2.DescribeNatGatewaysInput{
|
|
NatGatewayIds: ngwIds,
|
|
}
|
|
|
|
response, err := cloud.EC2().DescribeNatGateways(request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing Nat Gateways %v", err)
|
|
}
|
|
|
|
if len(response.NatGateways) != 1 {
|
|
return nil, fmt.Errorf("found %d Nat Gateways with ID %q, expected 1", len(response.NatGateways), fi.ValueOf(e.ID))
|
|
}
|
|
ngw = response.NatGateways[0]
|
|
|
|
if len(ngw.NatGatewayAddresses) != 1 {
|
|
return nil, fmt.Errorf("found %d EIP Addresses for 1 NATGateway, expected 1", len(ngw.NatGatewayAddresses))
|
|
}
|
|
} else {
|
|
// This is the normal/default path
|
|
var err error
|
|
ngw, err = e.findNatGateway(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ngw == nil {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
actual.ID = ngw.NatGatewayId
|
|
|
|
actual.Subnet = e.Subnet
|
|
if len(ngw.NatGatewayAddresses) == 0 {
|
|
// Not sure if this ever happens
|
|
actual.ElasticIP = nil
|
|
} else if len(ngw.NatGatewayAddresses) == 1 {
|
|
actual.ElasticIP = &ElasticIP{ID: ngw.NatGatewayAddresses[0].AllocationId}
|
|
} else {
|
|
return nil, fmt.Errorf("found multiple elastic IPs attached to NatGateway %q", aws.StringValue(ngw.NatGatewayId))
|
|
}
|
|
|
|
// NATGateways now have names and tags so lets pull from there instead.
|
|
actual.Name = findNameTag(ngw.Tags)
|
|
if e.Tags["Name"] == "" {
|
|
// If we're not tagging by name, avoid spurious differences
|
|
actual.Name = e.Name
|
|
}
|
|
actual.Tags = intersectTags(ngw.Tags, e.Tags)
|
|
|
|
// Avoid spurious changes
|
|
actual.Lifecycle = e.Lifecycle
|
|
actual.Shared = e.Shared
|
|
actual.AssociatedRouteTable = e.AssociatedRouteTable
|
|
|
|
e.ID = actual.ID
|
|
return actual, nil
|
|
}
|
|
|
|
func (e *NatGateway) findNatGateway(c *fi.CloudupContext) (*ec2.NatGateway, error) {
|
|
cloud := c.T.Cloud.(awsup.AWSCloud)
|
|
|
|
id := e.ID
|
|
|
|
// Find via route on private route table
|
|
if id == nil && e.AssociatedRouteTable != nil {
|
|
ngw, err := findNatGatewayFromRouteTable(cloud, e.AssociatedRouteTable)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if ngw != nil {
|
|
return ngw, nil
|
|
}
|
|
}
|
|
|
|
// Find via tag on subnet
|
|
// TODO: Obsolete - we can get from the route table instead
|
|
if id == nil && e.Subnet != nil {
|
|
var filters []*ec2.Filter
|
|
filters = append(filters, awsup.NewEC2Filter("key", "AssociatedNatgateway"))
|
|
if e.Subnet.ID == nil {
|
|
klog.V(2).Infof("Unable to find subnet, bypassing Find() for NatGateway")
|
|
return nil, nil
|
|
}
|
|
filters = append(filters, awsup.NewEC2Filter("resource-id", *e.Subnet.ID))
|
|
|
|
request := &ec2.DescribeTagsInput{
|
|
Filters: filters,
|
|
}
|
|
|
|
response, err := cloud.EC2().DescribeTags(request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing tags: %v", err)
|
|
}
|
|
|
|
if response == nil || len(response.Tags) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
if len(response.Tags) != 1 {
|
|
return nil, fmt.Errorf("found multiple tags for: %v", e)
|
|
}
|
|
t := response.Tags[0]
|
|
id = t.Value
|
|
klog.V(2).Infof("Found NatGateway via subnet tag: %v", *id)
|
|
}
|
|
|
|
if id != nil {
|
|
return findNatGatewayById(cloud, id)
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func findNatGatewayById(cloud awsup.AWSCloud, id *string) (*ec2.NatGateway, error) {
|
|
request := &ec2.DescribeNatGatewaysInput{}
|
|
request.NatGatewayIds = []*string{id}
|
|
response, err := cloud.EC2().DescribeNatGateways(request)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing NatGateway %q: %v", aws.StringValue(id), err)
|
|
}
|
|
|
|
if response == nil || len(response.NatGateways) == 0 {
|
|
klog.V(2).Infof("Unable to find NatGateway %q", aws.StringValue(id))
|
|
return nil, nil
|
|
}
|
|
if len(response.NatGateways) != 1 {
|
|
return nil, fmt.Errorf("found multiple NatGateways with id %q", aws.StringValue(id))
|
|
}
|
|
return response.NatGateways[0], nil
|
|
}
|
|
|
|
func findNatGatewayFromRouteTable(cloud awsup.AWSCloud, routeTable *RouteTable) (*ec2.NatGateway, error) {
|
|
// Find via route on private route table
|
|
if routeTable.ID != nil {
|
|
klog.V(2).Infof("trying to match NatGateway via RouteTable %s", *routeTable.ID)
|
|
rt, err := findRouteTableByID(cloud, *routeTable.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error finding associated RouteTable to NatGateway: %v", err)
|
|
}
|
|
|
|
if rt != nil {
|
|
var natGatewayIDs []*string
|
|
natGatewayIDsSeen := map[string]bool{}
|
|
for _, route := range rt.Routes {
|
|
if route.NatGatewayId != nil && !natGatewayIDsSeen[*route.NatGatewayId] {
|
|
natGatewayIDs = append(natGatewayIDs, route.NatGatewayId)
|
|
natGatewayIDsSeen[*route.NatGatewayId] = true
|
|
}
|
|
}
|
|
|
|
if len(natGatewayIDs) == 0 {
|
|
klog.V(2).Infof("no NatGateway found in route table %s", *rt.RouteTableId)
|
|
} else if len(natGatewayIDs) > 1 {
|
|
clusterName, ok := routeTable.Tags[awsup.TagClusterName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("Could not find '%s' tag from route table", awsup.TagClusterName)
|
|
}
|
|
filteredNatGateways := []*ec2.NatGateway{}
|
|
for _, natGatewayID := range natGatewayIDs {
|
|
gw, err := findNatGatewayById(cloud, natGatewayID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if raws.HasOwnedTag(ec2.ResourceTypeNatgateway+":"+fi.ValueOf(natGatewayID), gw.Tags, clusterName) {
|
|
filteredNatGateways = append(filteredNatGateways, gw)
|
|
}
|
|
}
|
|
if len(filteredNatGateways) == 0 {
|
|
klog.V(2).Infof("no kOps NatGateway found in route table %s", *rt.RouteTableId)
|
|
} else if len(filteredNatGateways) > 1 {
|
|
return nil, fmt.Errorf("found multiple kOps NatGateways in route table %s", *rt.RouteTableId)
|
|
} else {
|
|
return filteredNatGateways[0], nil
|
|
}
|
|
} else {
|
|
return findNatGatewayById(cloud, natGatewayIDs[0])
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *NatGateway) CheckChanges(a, e, changes *NatGateway) error {
|
|
// New
|
|
if a == nil {
|
|
if !fi.ValueOf(e.Shared) {
|
|
if e.ElasticIP == nil {
|
|
return fi.RequiredField("ElasticIP")
|
|
}
|
|
if e.Subnet == nil {
|
|
return fi.RequiredField("Subnet")
|
|
}
|
|
}
|
|
if e.AssociatedRouteTable == nil {
|
|
return fi.RequiredField("AssociatedRouteTable")
|
|
}
|
|
}
|
|
|
|
// Delta
|
|
if a != nil {
|
|
if changes.ElasticIP != nil {
|
|
eID := ""
|
|
if e.ElasticIP != nil {
|
|
eID = fi.ValueOf(e.ElasticIP.ID)
|
|
}
|
|
aID := ""
|
|
if a.ElasticIP != nil {
|
|
aID = fi.ValueOf(a.ElasticIP.ID)
|
|
}
|
|
return fi.FieldIsImmutable(eID, aID, field.NewPath("ElasticIP"))
|
|
}
|
|
if changes.Subnet != nil {
|
|
return fi.CannotChangeField("Subnet")
|
|
}
|
|
if changes.ID != nil {
|
|
return fi.CannotChangeField("ID")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (e *NatGateway) Run(c *fi.CloudupContext) error {
|
|
return fi.CloudupDefaultDeltaRunMethod(e, c)
|
|
}
|
|
|
|
func (_ *NatGateway) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *NatGateway) error {
|
|
// New NGW
|
|
|
|
var id *string
|
|
if a == nil {
|
|
|
|
if fi.ValueOf(e.Shared) {
|
|
return fmt.Errorf("NAT gateway %q not found", fi.ValueOf(e.ID))
|
|
}
|
|
|
|
klog.V(2).Infof("Creating Nat Gateway")
|
|
|
|
request := &ec2.CreateNatGatewayInput{
|
|
TagSpecifications: awsup.EC2TagSpecification(ec2.ResourceTypeNatgateway, e.Tags),
|
|
}
|
|
request.AllocationId = e.ElasticIP.ID
|
|
request.SubnetId = e.Subnet.ID
|
|
response, err := t.Cloud.EC2().CreateNatGateway(request)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating Nat Gateway: %v", err)
|
|
}
|
|
e.ID = response.NatGateway.NatGatewayId
|
|
id = e.ID
|
|
} else {
|
|
id = a.ID
|
|
}
|
|
|
|
err := t.AddAWSTags(*e.ID, e.Tags)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to tag NatGateway")
|
|
}
|
|
|
|
// Tag the associated subnet
|
|
if e.Subnet == nil {
|
|
return fmt.Errorf("Subnet not set")
|
|
} else if e.Subnet.ID == nil {
|
|
return fmt.Errorf("Subnet ID not set")
|
|
}
|
|
|
|
// TODO: AssociatedNatgateway tag is obsolete - we can get from the route table instead
|
|
tags := make(map[string]string)
|
|
tags["AssociatedNatgateway"] = *id
|
|
err = t.AddAWSTags(*e.Subnet.ID, tags)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to tag subnet %v", err)
|
|
}
|
|
|
|
// If this is a shared NGW, we need to tag it
|
|
// The tag that implies "shared" is `AssociatedNatgateway`=> NGW-ID
|
|
// This is better than just a tag that's shared because this lets us create a whitelist of these NGWs
|
|
// without doing a bunch more work in `kutil/delete_cluster.go`
|
|
|
|
if fi.ValueOf(e.Shared) {
|
|
if e.AssociatedRouteTable == nil {
|
|
return fmt.Errorf("AssociatedRouteTable not provided")
|
|
}
|
|
klog.V(2).Infof("tagging route table %s to track shared NGW", fi.ValueOf(e.AssociatedRouteTable.ID))
|
|
err = t.AddAWSTags(fi.ValueOf(e.AssociatedRouteTable.ID), tags)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to tag route table %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type terraformNATGateway struct {
|
|
AllocationID *terraformWriter.Literal `cty:"allocation_id"`
|
|
SubnetID *terraformWriter.Literal `cty:"subnet_id"`
|
|
Tag map[string]string `cty:"tags"`
|
|
}
|
|
|
|
func (_ *NatGateway) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *NatGateway) error {
|
|
if fi.ValueOf(e.Shared) {
|
|
if e.ID == nil {
|
|
return fmt.Errorf("ID must be set, if NatGateway is shared: %s", e)
|
|
}
|
|
|
|
klog.V(4).Infof("reusing existing NatGateway with id %q", *e.ID)
|
|
return nil
|
|
}
|
|
|
|
tf := &terraformNATGateway{
|
|
AllocationID: e.ElasticIP.TerraformLink(),
|
|
SubnetID: e.Subnet.TerraformLink(),
|
|
Tag: e.Tags,
|
|
}
|
|
|
|
return t.RenderResource("aws_nat_gateway", *e.Name, tf)
|
|
}
|
|
|
|
func (e *NatGateway) TerraformLink() *terraformWriter.Literal {
|
|
if fi.ValueOf(e.Shared) {
|
|
if e.ID == nil {
|
|
klog.Fatalf("ID must be set, if NatGateway is shared: %s", e)
|
|
}
|
|
|
|
return terraformWriter.LiteralFromStringValue(*e.ID)
|
|
}
|
|
|
|
return terraformWriter.LiteralProperty("aws_nat_gateway", *e.Name, "id")
|
|
}
|