kops/upup/pkg/fi/cloudup/gcetasks/subnet.go

346 lines
8.8 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 gcetasks
import (
"fmt"
"reflect"
compute "google.golang.org/api/compute/v1"
"k8s.io/klog/v2"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/gce"
"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
"k8s.io/kops/upup/pkg/fi/cloudup/terraformWriter"
)
// +kops:fitask
type Subnet struct {
Name *string
Lifecycle fi.Lifecycle
Network *Network
Region *string
CIDR *string
// StackType indicates the address families supported (IPV4_IPV6 or IPV4_ONLY)
StackType *string
// Ipv6AccessType indicates whether the IPv6 addresses are accessible externally
Ipv6AccessType *string
SecondaryIpRanges map[string]string
Shared *bool
}
var _ fi.CompareWithID = &Subnet{}
func (e *Subnet) CompareWithID() *string {
return e.Name
}
func (e *Subnet) Find(c *fi.CloudupContext) (*Subnet, error) {
cloud := c.T.Cloud.(gce.GCECloud)
_, project, err := gce.ParseNameAndProjectFromNetworkID(c.T.Cluster.Spec.Networking.NetworkID)
if err != nil {
return nil, fmt.Errorf("error parsing network name from cluster spec: %w", err)
} else if project == "" {
project = cloud.Project()
}
s, err := cloud.Compute().Subnetworks().Get(project, cloud.Region(), *e.Name)
if err != nil {
if gce.IsNotFound(err) {
return nil, nil
}
return nil, fmt.Errorf("error listing Subnets: %w", err)
}
actual := &Subnet{}
actual.Name = &s.Name
actual.Network = &Network{Name: fi.PtrTo(lastComponent(s.Network))}
actual.Region = fi.PtrTo(lastComponent(s.Region))
actual.CIDR = &s.IpCidrRange
actual.StackType = &s.StackType
actual.Ipv6AccessType = &s.Ipv6AccessType
shared := fi.ValueOf(e.Shared)
{
actual.SecondaryIpRanges = make(map[string]string)
for _, r := range s.SecondaryIpRanges {
if shared {
// In the shared case, only show differences on the ranges we specified
if _, found := e.SecondaryIpRanges[r.RangeName]; !found {
continue
}
}
actual.SecondaryIpRanges[r.RangeName] = r.IpCidrRange
}
}
// Prevent spurious changes
actual.Lifecycle = e.Lifecycle
actual.Name = e.Name
actual.Shared = e.Shared
return actual, nil
}
func (e *Subnet) Run(c *fi.CloudupContext) error {
return fi.CloudupDefaultDeltaRunMethod(e, c)
}
func (_ *Subnet) CheckChanges(a, e, changes *Subnet) error {
return nil
}
func (_ *Subnet) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Subnet) error {
shared := fi.ValueOf(e.Shared)
if shared {
// Verify the subnet was found
if a == nil {
return fmt.Errorf("Subnet with name %q not found", fi.ValueOf(e.Name))
}
}
cloud := t.Cloud
project := cloud.Project()
if a == nil {
klog.V(2).Infof("Creating Subnet with CIDR: %q", fi.ValueOf(e.CIDR))
subnet := &compute.Subnetwork{
IpCidrRange: fi.ValueOf(e.CIDR),
Name: *e.Name,
Network: e.Network.URL(project),
StackType: fi.ValueOf(e.StackType),
Ipv6AccessType: fi.ValueOf(e.Ipv6AccessType),
}
for k, v := range e.SecondaryIpRanges {
subnet.SecondaryIpRanges = append(subnet.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
RangeName: k,
IpCidrRange: v,
})
}
op, err := cloud.Compute().Subnetworks().Insert(t.Cloud.Project(), t.Cloud.Region(), subnet)
if err != nil {
return fmt.Errorf("error creating Subnet: %v", err)
}
if err := t.Cloud.WaitForOp(op); err != nil {
return fmt.Errorf("error waiting for Subnet creation to complete: %w", err)
}
} else {
if changes.SecondaryIpRanges != nil {
// Update is split into two calls as GCE does not allow us to add and remove ranges in the same call
if err := updateSecondaryRanges(cloud, "add", e); err != nil {
return err
}
if !shared {
if err := updateSecondaryRanges(cloud, "remove", e); err != nil {
return err
}
}
changes.SecondaryIpRanges = nil
}
if changes.StackType != nil {
if err := updateStackTypeAndIPv6AccessType(cloud, e); err != nil {
return err
}
changes.StackType = nil
changes.Ipv6AccessType = nil
}
empty := &Subnet{}
if !reflect.DeepEqual(empty, changes) {
return fmt.Errorf("cannot apply changes to Subnet: %v", changes)
}
}
return nil
}
func updateSecondaryRanges(cloud gce.GCECloud, op string, e *Subnet) error {
// We need to refetch to patch it
subnet, err := cloud.Compute().Subnetworks().Get(cloud.Project(), cloud.Region(), *e.Name)
if err != nil {
return fmt.Errorf("error fetching subnet for patch: %w", err)
}
expectedRanges := e.SecondaryIpRanges
actualRanges := make(map[string]string)
for _, r := range subnet.SecondaryIpRanges {
actualRanges[r.RangeName] = r.IpCidrRange
}
// Cannot add and remove ranges in the same call
switch op {
case "add":
patch := false
for k, v := range expectedRanges {
if actualRanges[k] != v {
actualRanges[k] = v
subnet.SecondaryIpRanges = append(subnet.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
RangeName: k,
IpCidrRange: v,
})
patch = true
}
}
if !patch {
return nil
}
case "remove":
patch := false
if len(actualRanges) != len(expectedRanges) {
patch = true
} else {
for k := range expectedRanges {
if actualRanges[k] != e.SecondaryIpRanges[k] {
patch = true
}
}
}
if !patch {
return nil
}
subnet.SecondaryIpRanges = nil
for k, v := range expectedRanges {
subnet.SecondaryIpRanges = append(subnet.SecondaryIpRanges, &compute.SubnetworkSecondaryRange{
RangeName: k,
IpCidrRange: v,
})
}
}
patchOp, err := cloud.Compute().Subnetworks().Patch(cloud.Project(), cloud.Region(), subnet.Name, subnet)
if err != nil {
return fmt.Errorf("error patching Subnet: %w", err)
}
if err := cloud.WaitForOp(patchOp); err != nil {
return fmt.Errorf("error waiting for Subnet patch to complete: %w", err)
}
return nil
}
func updateStackTypeAndIPv6AccessType(cloud gce.GCECloud, e *Subnet) error {
// We need to refetch to patch it
subnet, err := cloud.Compute().Subnetworks().Get(cloud.Project(), cloud.Region(), *e.Name)
if err != nil {
return fmt.Errorf("error fetching subnet for patch: %w", err)
}
subnet.StackType = fi.ValueOf(e.StackType)
subnet.Ipv6AccessType = fi.ValueOf(e.Ipv6AccessType)
patchOp, err := cloud.Compute().Subnetworks().Patch(cloud.Project(), cloud.Region(), subnet.Name, subnet)
if err != nil {
return fmt.Errorf("error patching Subnet: %w", err)
}
if err := cloud.WaitForOp(patchOp); err != nil {
return fmt.Errorf("error waiting for Subnet patch to complete: %w", err)
}
return nil
}
func (e *Subnet) URL(project string, region string) string {
u := gce.GoogleCloudURL{
Version: "v1",
Project: project,
Name: *e.Name,
Type: "subnetworks",
Region: region,
}
return u.BuildURL()
}
type terraformSubnet struct {
Name *string `cty:"name"`
Network *terraformWriter.Literal `cty:"network"`
Region *string `cty:"region"`
CIDR *string `cty:"ip_cidr_range"`
// SecondaryIPRange defines additional IP ranges
SecondaryIPRange []terraformSubnetRange `cty:"secondary_ip_range"`
StackType *string `cty:"stack_type"`
Ipv6AccessType *string `cty:"ipv6_access_type"`
}
type terraformSubnetRange struct {
Name string `cty:"range_name"`
CIDR string `cty:"ip_cidr_range"`
}
func (_ *Subnet) RenderSubnet(t *terraform.TerraformTarget, a, e, changes *Subnet) error {
shared := fi.ValueOf(e.Shared)
if shared {
// Not terraform owned / managed
return nil
}
tf := &terraformSubnet{
Name: e.Name,
Network: e.Network.TerraformLink(),
Region: e.Region,
CIDR: e.CIDR,
StackType: e.StackType,
Ipv6AccessType: e.Ipv6AccessType,
}
for k, v := range e.SecondaryIpRanges {
tf.SecondaryIPRange = append(tf.SecondaryIPRange, terraformSubnetRange{
Name: k,
CIDR: v,
})
}
return t.RenderResource("google_compute_subnetwork", *e.Name, tf)
}
func (e *Subnet) TerraformLink() *terraformWriter.Literal {
shared := fi.ValueOf(e.Shared)
if shared {
if e.Name == nil {
klog.Fatalf("GCEName must be set, if subnet is shared: %#v", e)
}
name := *e.Name
if e.Network != nil && e.Network.Project != nil {
name = *e.Network.Project + "/" + name
}
klog.V(4).Infof("reusing existing subnet with name %q", name)
return terraformWriter.LiteralFromStringValue(name)
}
return terraformWriter.LiteralProperty("google_compute_subnetwork", *e.Name, "name")
}