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

503 lines
13 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"
"strings"
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"
)
var scopeAliases map[string]string
// +kops:fitask
type Instance struct {
Name *string
Lifecycle fi.Lifecycle
Network *Network
Tags []string
Preemptible *bool
Image *string
Disks map[string]*Disk
ServiceAccount *ServiceAccount
CanIPForward *bool
IPAddress *Address
Subnet *Subnet
StackType *string
Scopes []string
Metadata map[string]fi.Resource
Zone *string
MachineType *string
metadataFingerprint string
}
var _ fi.CompareWithID = &Instance{}
func (e *Instance) CompareWithID() *string {
return e.Name
}
func (e *Instance) Find(c *fi.CloudupContext) (*Instance, error) {
cloud := c.T.Cloud.(gce.GCECloud)
r, err := cloud.Compute().Instances().Get(cloud.Project(), *e.Zone, *e.Name)
if err != nil {
if gce.IsNotFound(err) {
return nil, nil
}
return nil, fmt.Errorf("error listing Instances: %v", err)
}
actual := &Instance{}
actual.Name = &r.Name
actual.Tags = append(actual.Tags, r.Tags.Items...)
actual.Zone = fi.PtrTo(lastComponent(r.Zone))
actual.MachineType = fi.PtrTo(lastComponent(r.MachineType))
actual.CanIPForward = &r.CanIpForward
if r.Scheduling != nil {
actual.Preemptible = &r.Scheduling.Preemptible
}
if len(r.NetworkInterfaces) != 0 {
ni := r.NetworkInterfaces[0]
actual.Network = &Network{Name: fi.PtrTo(lastComponent(ni.Network))}
actual.StackType = &ni.StackType
if len(ni.AccessConfigs) != 0 {
ac := ni.AccessConfigs[0]
if ac.NatIP != "" {
addrs, err := cloud.Compute().Addresses().ListWithFilter(cloud.Project(), cloud.Region(), "address eq "+ac.NatIP)
if err != nil {
return nil, fmt.Errorf("error querying for address %q: %v", ac.NatIP, err)
} else if len(addrs) != 0 {
actual.IPAddress = &Address{Name: &addrs[0].Name}
} else {
return nil, fmt.Errorf("address not found %q: %v", ac.NatIP, err)
}
}
}
}
for _, serviceAccount := range r.ServiceAccounts {
for _, scope := range serviceAccount.Scopes {
actual.Scopes = append(actual.Scopes, scopeToShortForm(scope))
}
}
actual.Disks = make(map[string]*Disk)
for i, disk := range r.Disks {
if i == 0 {
source := disk.Source
// TODO: Parse source URL instead of assuming same project/zone?
name := lastComponent(source)
d, err := cloud.Compute().Disks().Get(cloud.Project(), *e.Zone, name)
if err != nil {
if gce.IsNotFound(err) {
return nil, fmt.Errorf("disk not found %q: %v", source, err)
}
return nil, fmt.Errorf("error querying for disk %q: %v", source, err)
}
image, err := ShortenImageURL(cloud.Project(), d.SourceImage)
if err != nil {
return nil, fmt.Errorf("error parsing source image URL: %v", err)
}
actual.Image = fi.PtrTo(image)
} else {
url, err := gce.ParseGoogleCloudURL(disk.Source)
if err != nil {
return nil, fmt.Errorf("unable to parse disk source URL: %q", disk.Source)
}
actual.Disks[disk.DeviceName] = &Disk{Name: &url.Name}
}
}
if r.Metadata != nil {
actual.Metadata = make(map[string]fi.Resource)
for _, i := range r.Metadata.Items {
actual.Metadata[i.Key] = fi.NewStringResource(fi.ValueOf(i.Value))
}
actual.metadataFingerprint = r.Metadata.Fingerprint
}
return actual, nil
}
func (e *Instance) Run(c *fi.CloudupContext) error {
return fi.CloudupDefaultDeltaRunMethod(e, c)
}
func (_ *Instance) CheckChanges(a, e, changes *Instance) error {
return nil
}
func init() {
scopeAliases = map[string]string{
"storage-ro": "https://www.googleapis.com/auth/devstorage.read_only",
"storage-rw": "https://www.googleapis.com/auth/devstorage.read_write",
"compute-ro": "https://www.googleapis.com/auth/compute.read_only",
"compute-rw": "https://www.googleapis.com/auth/compute",
"monitoring": "https://www.googleapis.com/auth/monitoring",
"monitoring-write": "https://www.googleapis.com/auth/monitoring.write",
"logging-write": "https://www.googleapis.com/auth/logging.write",
}
}
func scopeToLongForm(s string) string {
e, found := scopeAliases[s]
if found {
return e
}
return s
}
func scopeToShortForm(s string) string {
for k, v := range scopeAliases {
if v == s {
return k
}
}
return s
}
func (e *Instance) mapToGCE(project string, ipAddressResolver func(*Address) (*string, error)) (*compute.Instance, error) {
zone := *e.Zone
var scheduling *compute.Scheduling
if fi.ValueOf(e.Preemptible) {
scheduling = &compute.Scheduling{
OnHostMaintenance: "TERMINATE",
Preemptible: true,
}
} else {
scheduling = &compute.Scheduling{
AutomaticRestart: fi.PtrTo(true),
// TODO: Migrate or terminate?
OnHostMaintenance: "MIGRATE",
Preemptible: false,
}
}
var disks []*compute.AttachedDisk
disks = append(disks, &compute.AttachedDisk{
InitializeParams: &compute.AttachedDiskInitializeParams{
SourceImage: BuildImageURL(project, *e.Image),
},
Boot: true,
DeviceName: "persistent-disks-0",
Index: 0,
AutoDelete: true,
Mode: "READ_WRITE",
Type: "PERSISTENT",
})
for name, disk := range e.Disks {
disks = append(disks, &compute.AttachedDisk{
Source: disk.URL(project),
AutoDelete: false,
Mode: "READ_WRITE",
DeviceName: name,
})
}
var tags *compute.Tags
if e.Tags != nil {
tags = &compute.Tags{
Items: e.Tags,
}
}
var networkInterfaces []*compute.NetworkInterface
{
networkInterface := &compute.NetworkInterface{
AccessConfigs: []*compute.AccessConfig{{
Type: "ONE_TO_ONE_NAT",
}},
Network: e.Network.URL(project),
}
if e.IPAddress != nil {
addr, err := ipAddressResolver(e.IPAddress)
if err != nil {
return nil, fmt.Errorf("unable to resolve IP for instance: %v", err)
}
if addr == nil {
return nil, fmt.Errorf("instance IP address has not yet been created")
}
networkInterface.AccessConfigs[0].NatIP = *addr
}
if e.Subnet != nil {
networkInterface.Subnetwork = *e.Subnet.Name
}
if e.StackType != nil {
networkInterface.StackType = *e.StackType
}
networkInterfaces = append(networkInterfaces, networkInterface)
}
var serviceAccounts []*compute.ServiceAccount
if e.ServiceAccount != nil {
if e.Scopes != nil {
var scopes []string
for _, s := range e.Scopes {
s = scopeToLongForm(s)
scopes = append(scopes, s)
}
serviceAccounts = append(serviceAccounts, &compute.ServiceAccount{
Email: fi.ValueOf(e.ServiceAccount.Email),
Scopes: scopes,
})
}
}
var metadataItems []*compute.MetadataItems
for key, r := range e.Metadata {
v, err := fi.ResourceAsString(r)
if err != nil {
return nil, fmt.Errorf("error rendering Instance metadata %q: %v", key, err)
}
metadataItems = append(metadataItems, &compute.MetadataItems{
Key: key,
Value: fi.PtrTo(v),
})
}
i := &compute.Instance{
CanIpForward: *e.CanIPForward,
Disks: disks,
MachineType: BuildMachineTypeURL(project, zone, *e.MachineType),
Metadata: &compute.Metadata{
Items: metadataItems,
},
Name: *e.Name,
NetworkInterfaces: networkInterfaces,
Scheduling: scheduling,
ServiceAccounts: serviceAccounts,
Tags: tags,
}
return i, nil
}
func (i *Instance) isZero() bool {
zero := &Instance{}
return reflect.DeepEqual(zero, i)
}
func (_ *Instance) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Instance) error {
cloud := t.Cloud
project := cloud.Project()
zone := *e.Zone
ipAddressResolver := func(ip *Address) (*string, error) {
return ip.IPAddress, nil
}
i, err := e.mapToGCE(project, ipAddressResolver)
if err != nil {
return err
}
if a == nil {
klog.V(2).Infof("Creating instance %q", i.Name)
if _, err := cloud.Compute().Instances().Insert(project, zone, i); err != nil {
return fmt.Errorf("error creating Instance: %v", err)
}
} else {
if changes.Metadata != nil {
klog.V(2).Infof("Updating instance metadata on %q", i.Name)
i.Metadata.Fingerprint = a.metadataFingerprint
op, err := cloud.Compute().Instances().SetMetadata(project, zone, i.Name, i.Metadata)
if err != nil {
return fmt.Errorf("error setting metadata on instance: %v", err)
}
if err := cloud.WaitForOp(op); err != nil {
return fmt.Errorf("error setting metadata on instance: %v", err)
}
changes.Metadata = nil
}
if !changes.isZero() {
klog.Errorf("Cannot apply changes to Instance: %v", changes)
return fmt.Errorf("cannot apply changes to Instance: %v", changes)
}
}
return nil
}
func BuildMachineTypeURL(project, zone, name string) string {
return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/machineTypes/%s", project, zone, name)
}
func BuildImageURL(defaultProject, nameSpec string) string {
tokens := strings.Split(nameSpec, "/")
var project, name string
if len(tokens) == 2 {
project = tokens[0]
name = tokens[1]
} else if len(tokens) == 1 {
project = defaultProject
name = tokens[0]
} else {
klog.Exitf("Cannot parse image spec: %q", nameSpec)
}
u := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/%s", project, name)
klog.V(4).Infof("Mapped image %q to URL %q", nameSpec, u)
return u
}
func ShortenImageURL(defaultProject string, imageURL string) (string, error) {
u, err := gce.ParseGoogleCloudURL(imageURL)
if err != nil {
return "", err
}
if u.Project == defaultProject {
klog.V(4).Infof("Resolved image %q -> %q", imageURL, u.Name)
return u.Name, nil
} else {
klog.V(4).Infof("Resolved image %q -> %q", imageURL, u.Project+"/"+u.Name)
return u.Project + "/" + u.Name, nil
}
}
type terraformInstance struct {
Name string `cty:"name"`
CanIPForward bool `cty:"can_ip_forward"`
MachineType string `cty:"machine_type"`
ServiceAccounts []*terraformTemplateServiceAccount `cty:"service_account"`
Scheduling *terraformScheduling `cty:"scheduling"`
Disks []*terraformInstanceAttachedDisk `cty:"disk"`
NetworkInterfaces []*terraformNetworkInterface `cty:"network_interface"`
Metadata map[string]*terraformWriter.Literal `cty:"metadata"`
MetadataStartupScript *terraformWriter.Literal `cty:"metadata_startup_script"`
Tags []string `cty:"tags"`
Zone string `cty:"zone"`
}
type terraformInstanceAttachedDisk struct {
AutoDelete bool `cty:"auto_delete"`
DeviceName string `cty:"device_name"`
// 'pd-standard', 'pd-ssd', 'local-ssd' etc
Type string `cty:"type"`
Disk string `cty:"disk"`
Image string `cty:"image"`
Scratch bool `cty:"scratch"`
Size int64 `cty:"size"`
}
func (_ *Instance) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Instance) error {
project := t.Project
// This is a "little" hacky...
ipAddressResolver := func(ip *Address) (*string, error) {
tf := "${google_compute_address." + *ip.Name + ".address}"
return &tf, nil
}
i, err := e.mapToGCE(project, ipAddressResolver)
if err != nil {
return err
}
tf := &terraformInstance{
Name: i.Name,
}
tf.CanIPForward = i.CanIpForward
tf.MachineType = lastComponent(i.MachineType)
tf.Zone = i.Zone
tf.Tags = i.Tags.Items
// TF requires zone
if tf.Zone == "" && e.Zone != nil {
tf.Zone = *e.Zone
}
tf.ServiceAccounts = mapServiceAccountsToTerraform([]*ServiceAccount{e.ServiceAccount}, e.Scopes)
for _, d := range i.Disks {
tfd := &terraformInstanceAttachedDisk{
AutoDelete: d.AutoDelete,
Scratch: d.Type == "SCRATCH",
DeviceName: d.DeviceName,
// TODO: Does this need to be a TF link?
Disk: lastComponent(d.Source),
}
if d.InitializeParams != nil {
tfd.Disk = d.InitializeParams.DiskName
tfd.Image = d.InitializeParams.SourceImage
tfd.Type = d.InitializeParams.DiskType
tfd.Size = d.InitializeParams.DiskSizeGb
}
tf.Disks = append(tf.Disks, tfd)
}
tf.NetworkInterfaces = addNetworks(e.StackType, e.Network, e.Subnet, i.NetworkInterfaces)
metadata, err := addMetadata(t, i.Name, i.Metadata)
if err != nil {
return err
}
tf.Metadata = metadata
// Using metadata_startup_script is now mandatory (?)
{
startupScript, found := tf.Metadata["startup-script"]
if found {
delete(tf.Metadata, "startup-script")
}
tf.MetadataStartupScript = startupScript
}
if i.Scheduling != nil {
tf.Scheduling = &terraformScheduling{
AutomaticRestart: fi.ValueOf(i.Scheduling.AutomaticRestart),
OnHostMaintenance: i.Scheduling.OnHostMaintenance,
Preemptible: i.Scheduling.Preemptible,
}
}
return t.RenderResource("google_compute_instance", i.Name, tf)
}