GCE: Allow updating the instance group template

We follow the same pattern as on AWS, where we create a new template,
because the templates are immutable.
This commit is contained in:
Justin Santa Barbara 2017-04-24 22:05:04 -04:00
parent 85b308b3e8
commit 5e5e37e2e2
5 changed files with 208 additions and 111 deletions

View File

@ -390,6 +390,12 @@ func ShortenImageURL(defaultProject string, imageURL string) (string, error) {
}
}
type terraformInstance struct {
terraformInstanceCommon
Name string `json:"name"`
}
func (_ *Instance) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *Instance) error {
project := t.Project
@ -404,13 +410,13 @@ func (_ *Instance) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *
return err
}
tf := &terraformInstanceTemplate{
Name: i.Name,
CanIPForward: i.CanIpForward,
MachineType: lastComponent(i.MachineType),
Zone: i.Zone,
Tags: i.Tags.Items,
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 {

View File

@ -59,7 +59,7 @@ func (e *InstanceGroupManager) Find(c *fi.Context) (*InstanceGroupManager, error
actual.Zone = fi.String(lastComponent(r.Zone))
actual.BaseInstanceName = &r.BaseInstanceName
actual.TargetSize = &r.TargetSize
actual.InstanceTemplate = &InstanceTemplate{Name: fi.String(lastComponent(r.InstanceTemplate))}
actual.InstanceTemplate = &InstanceTemplate{ID: fi.String(lastComponent(r.InstanceTemplate))}
for _, targetPool := range r.TargetPools {
actual.TargetPools = append(actual.TargetPools, &TargetPool{
@ -82,12 +82,17 @@ func (_ *InstanceGroupManager) CheckChanges(a, e, changes *InstanceGroupManager)
func (_ *InstanceGroupManager) RenderGCE(t *gce.GCEAPITarget, a, e, changes *InstanceGroupManager) error {
project := t.Cloud.Project
instanceTemplateURL, err := e.InstanceTemplate.URL(project)
if err != nil {
return err
}
i := &compute.InstanceGroupManager{
Name: *e.Name,
Zone: *e.Zone,
BaseInstanceName: *e.BaseInstanceName,
TargetSize: *e.TargetSize,
InstanceTemplate: e.InstanceTemplate.URL(project),
InstanceTemplate: instanceTemplateURL,
}
for _, targetPool := range e.TargetPools {
@ -121,6 +126,22 @@ func (_ *InstanceGroupManager) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Ins
changes.TargetPools = nil
}
if changes.InstanceTemplate != nil {
request := &compute.InstanceGroupManagersSetInstanceTemplateRequest{
InstanceTemplate: instanceTemplateURL,
}
op, err := t.Cloud.Compute.InstanceGroupManagers.SetInstanceTemplate(t.Cloud.Project, *e.Zone, i.Name, request).Do()
if err != nil {
return fmt.Errorf("error updating InstanceTemplate for InstanceGroupManager: %v", err)
}
if err := t.Cloud.WaitForOp(op); err != nil {
return fmt.Errorf("error updating InstanceTemplate for InstanceGroupManager: %v", err)
}
changes.InstanceTemplate = nil
}
empty := &InstanceGroupManager{}
if !reflect.DeepEqual(empty, changes) {
return fmt.Errorf("Cannot apply changes to InstanceGroupManager: %v", changes)

View File

@ -18,18 +18,24 @@ package gcetasks
import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/golang/glog"
compute "google.golang.org/api/compute/v0.beta"
"k8s.io/kops/pkg/diff"
"k8s.io/kops/upup/pkg/fi"
"k8s.io/kops/upup/pkg/fi/cloudup/gce"
"k8s.io/kops/upup/pkg/fi/cloudup/terraform"
"strings"
)
// InstanceTemplate represents a GCE InstanceTemplate
//go:generate fitask -type=InstanceTemplate
type InstanceTemplate struct {
// Name will be used for as the name prefix
Name *string
Network *Network
Tags []string
@ -47,18 +53,21 @@ type InstanceTemplate struct {
Metadata map[string]*fi.ResourceHolder
MachineType *string
// ID is the actual name
ID *string
}
var _ fi.CompareWithID = &InstanceTemplate{}
func (e *InstanceTemplate) CompareWithID() *string {
return e.Name
return e.ID
}
func (e *InstanceTemplate) Find(c *fi.Context) (*InstanceTemplate, error) {
cloud := c.Cloud.(*gce.GCECloud)
r, err := cloud.Compute.InstanceTemplates.Get(cloud.Project, *e.Name).Do()
response, err := cloud.Compute.InstanceTemplates.List(cloud.Project).Do()
if err != nil {
if gce.IsNotFound(err) {
return nil, nil
@ -66,69 +75,94 @@ func (e *InstanceTemplate) Find(c *fi.Context) (*InstanceTemplate, error) {
return nil, fmt.Errorf("error listing InstanceTemplates: %v", err)
}
actual := &InstanceTemplate{}
actual.Name = &r.Name
p := r.Properties
for _, tag := range p.Tags.Items {
actual.Tags = append(actual.Tags, tag)
}
actual.MachineType = fi.String(lastComponent(p.MachineType))
actual.CanIPForward = &p.CanIpForward
bootDiskImage, err := ShortenImageURL(cloud.Project, p.Disks[0].InitializeParams.SourceImage)
expected, err := e.mapToGCE(cloud.Project)
if err != nil {
return nil, fmt.Errorf("error parsing source image URL: %v", err)
}
actual.BootDiskImage = fi.String(bootDiskImage)
actual.BootDiskType = &p.Disks[0].InitializeParams.DiskType
actual.BootDiskSizeGB = &p.Disks[0].InitializeParams.DiskSizeGb
if p.Scheduling != nil {
actual.Preemptible = &p.Scheduling.Preemptible
}
if len(p.NetworkInterfaces) != 0 {
ni := p.NetworkInterfaces[0]
actual.Network = &Network{Name: fi.String(lastComponent(ni.Network))}
return nil, err
}
for _, serviceAccount := range p.ServiceAccounts {
for _, scope := range serviceAccount.Scopes {
actual.Scopes = append(actual.Scopes, scopeToShortForm(scope))
for _, r := range response.Items {
if !strings.HasPrefix(r.Name, fi.StringValue(e.Name)) {
continue
}
}
//for i, disk := range p.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).Do()
// 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)
// } else {
// imageURL, err := gce.ParseGoogleCloudURL(d.SourceImage)
// if err != nil {
// return nil, fmt.Errorf("unable to parse image URL: %q", d.SourceImage)
// }
// actual.Image = fi.String(imageURL.Project + "/" + imageURL.Name)
// }
// }
//}
if p.Metadata != nil {
actual.Metadata = make(map[string]*fi.ResourceHolder)
for _, meta := range p.Metadata.Items {
actual.Metadata[meta.Key] = fi.WrapResource(fi.NewStringResource(meta.Value))
if !matches(expected, r) {
continue
}
actual := &InstanceTemplate{}
p := r.Properties
for _, tag := range p.Tags.Items {
actual.Tags = append(actual.Tags, tag)
}
actual.MachineType = fi.String(lastComponent(p.MachineType))
actual.CanIPForward = &p.CanIpForward
bootDiskImage, err := ShortenImageURL(cloud.Project, p.Disks[0].InitializeParams.SourceImage)
if err != nil {
return nil, fmt.Errorf("error parsing source image URL: %v", err)
}
actual.BootDiskImage = fi.String(bootDiskImage)
actual.BootDiskType = &p.Disks[0].InitializeParams.DiskType
actual.BootDiskSizeGB = &p.Disks[0].InitializeParams.DiskSizeGb
if p.Scheduling != nil {
actual.Preemptible = &p.Scheduling.Preemptible
}
if len(p.NetworkInterfaces) != 0 {
ni := p.NetworkInterfaces[0]
actual.Network = &Network{Name: fi.String(lastComponent(ni.Network))}
}
for _, serviceAccount := range p.ServiceAccounts {
for _, scope := range serviceAccount.Scopes {
actual.Scopes = append(actual.Scopes, scopeToShortForm(scope))
}
}
// When we deal with additional disks (local disks), we'll need to map them like this...
//for i, disk := range p.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).Do()
// 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)
// } else {
// imageURL, err := gce.ParseGoogleCloudURL(d.SourceImage)
// if err != nil {
// return nil, fmt.Errorf("unable to parse image URL: %q", d.SourceImage)
// }
// actual.Image = fi.String(imageURL.Project + "/" + imageURL.Name)
// }
// }
//}
if p.Metadata != nil {
actual.Metadata = make(map[string]*fi.ResourceHolder)
for _, meta := range p.Metadata.Items {
actual.Metadata[meta.Key] = fi.WrapResource(fi.NewStringResource(meta.Value))
}
}
// Prevent spurious changes
actual.Name = e.Name
actual.ID = &r.Name
if e.ID == nil {
e.ID = actual.ID
}
return actual, nil
}
return actual, nil
return nil, nil
}
func (e *InstanceTemplate) Run(c *fi.Context) error {
@ -168,6 +202,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
var disks []*compute.AttachedDisk
disks = append(disks, &compute.AttachedDisk{
Kind: "compute#attachedDisk",
InitializeParams: &compute.AttachedDiskInitializeParams{
SourceImage: BuildImageURL(project, *e.BootDiskImage),
DiskSizeGb: *e.BootDiskSizeGB,
@ -191,6 +226,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
var networkInterfaces []*compute.NetworkInterface
ni := &compute.NetworkInterface{
AccessConfigs: []*compute.AccessConfig{{
Kind: "compute#accessConfig",
//NatIP: *e.IPAddress.Address,
Type: "ONE_TO_ONE_NAT",
}},
@ -228,7 +264,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
}
i := &compute.InstanceTemplate{
Name: *e.Name,
Kind: "compute#instanceTemplate",
Properties: &compute.InstanceProperties{
CanIpForward: *e.CanIPForward,
@ -237,6 +273,7 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
MachineType: *e.MachineType,
Metadata: &compute.Metadata{
Kind: "compute#metadata",
Items: metadataItems,
},
@ -253,8 +290,53 @@ func (e *InstanceTemplate) mapToGCE(project string) (*compute.InstanceTemplate,
return i, nil
}
func (e *InstanceTemplate) URL(project string) string {
return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/instanceTemplates/%s", project, fi.StringValue(e.Name))
type ByKey []*compute.MetadataItems
func (a ByKey) Len() int { return len(a) }
func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key }
func matches(l, r *compute.InstanceTemplate) bool {
normalizeInstanceProperties := func(v *compute.InstanceProperties) *compute.InstanceProperties {
c := *v
if c.Metadata != nil {
cm := *c.Metadata
c.Metadata = &cm
c.Metadata.Fingerprint = ""
sort.Sort(ByKey(c.Metadata.Items))
}
return &c
}
normalize := func(v *compute.InstanceTemplate) *compute.InstanceTemplate {
c := *v
c.SelfLink = ""
c.CreationTimestamp = ""
c.Id = 0
c.Name = ""
c.Properties = normalizeInstanceProperties(c.Properties)
return &c
}
normalizedL := normalize(l)
normalizedR := normalize(r)
if !reflect.DeepEqual(normalizedL, normalizedR) {
if glog.V(10) {
ls := fi.DebugAsJsonStringIndent(normalizedL)
rs := fi.DebugAsJsonStringIndent(normalizedR)
glog.V(10).Infof("Not equal")
glog.V(10).Infof(diff.FormatDiff(ls, rs))
}
return false
}
return true
}
func (e *InstanceTemplate) URL(project string) (string, error) {
if e.ID == nil {
return "", fmt.Errorf("InstanceTemplate not yet built; ID is not yet known")
}
return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/instanceTemplates/%s", project, *e.ID), nil
}
func (_ *InstanceTemplate) RenderGCE(t *gce.GCEAPITarget, a, e, changes *InstanceTemplate) error {
@ -268,6 +350,10 @@ func (_ *InstanceTemplate) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Instanc
if a == nil {
glog.V(4).Infof("Creating InstanceTemplate %v", i)
name := fi.StringValue(e.Name) + "-" + strconv.FormatInt(time.Now().Unix(), 10)
e.ID = &name
i.Name = name
op, err := t.Cloud.Compute.InstanceTemplates.Insert(t.Cloud.Project, i).Do()
if err != nil {
return fmt.Errorf("error creating InstanceTemplate: %v", err)
@ -277,45 +363,18 @@ func (_ *InstanceTemplate) RenderGCE(t *gce.GCEAPITarget, a, e, changes *Instanc
return fmt.Errorf("error creating InstanceTemplate: %v", err)
}
} else {
// TODO: Make error again
if changes.Metadata != nil {
for k, ev := range e.Metadata {
av := a.Metadata[k]
if av == nil {
glog.Infof("Metadata %q not found", k)
continue
}
evString, err := ev.AsString()
if err != nil {
glog.Infof("Expected.Metadata.%s could not be rendered: %v", k, err)
continue
}
avString, err := av.AsString()
if err != nil {
glog.Infof("Actual.Metadata.%s could not be rendered: %v", k, err)
continue
}
if evString == avString {
continue
}
glog.Infof("Difference in Metadata.%s:", k)
glog.Infof("%s", diff.FormatDiff(avString, evString))
}
}
// TODO: Make error again
//return fmt.Errorf("Cannot apply changes to InstanceTemplate: %v", changes)
glog.Warningf("Cannot apply changes to InstanceTemplate: %v", changes)
return fmt.Errorf("Cannot apply changes to InstanceTemplate: %v", changes)
}
return nil
}
type terraformInstanceTemplate struct {
Name string `json:"name"`
terraformInstanceCommon
NamePrefix string `json:"name_prefix"`
}
type terraformInstanceCommon struct {
CanIPForward bool `json:"can_ip_forward"`
MachineType string `json:"machine_type,omitempty"`
ServiceAccount *terraformServiceAccount `json:"service_account,omitempty"`
@ -377,7 +436,7 @@ type terraformAccessConfig struct {
NatIP *terraform.Literal `json:"nat_ip,omitempty"`
}
func (t *terraformInstanceTemplate) AddNetworks(network *Network, subnet *Subnet, networkInterfacs []*compute.NetworkInterface) {
func (t *terraformInstanceCommon) AddNetworks(network *Network, subnet *Subnet, networkInterfacs []*compute.NetworkInterface) {
for _, g := range networkInterfacs {
tf := &terraformNetworkInterface{}
if network != nil {
@ -402,7 +461,7 @@ func (t *terraformInstanceTemplate) AddNetworks(network *Network, subnet *Subnet
}
}
func (t *terraformInstanceTemplate) AddMetadata(metadata *compute.Metadata) {
func (t *terraformInstanceCommon) AddMetadata(metadata *compute.Metadata) {
if metadata != nil {
if t.Metadata == nil {
t.Metadata = make(map[string]string)
@ -415,7 +474,7 @@ func (t *terraformInstanceTemplate) AddMetadata(metadata *compute.Metadata) {
}
}
func (t *terraformInstanceTemplate) AddServiceAccounts(serviceAccounts []*compute.ServiceAccount) {
func (t *terraformInstanceCommon) AddServiceAccounts(serviceAccounts []*compute.ServiceAccount) {
for _, g := range serviceAccounts {
for _, scope := range g.Scopes {
if t.ServiceAccount == nil {
@ -435,13 +494,14 @@ func (_ *InstanceTemplate) RenderTerraform(t *terraform.TerraformTarget, a, e, c
}
tf := &terraformInstanceTemplate{
Name: i.Name,
CanIPForward: i.Properties.CanIpForward,
//Description: i.Properties.Description,
MachineType: i.Properties.MachineType,
Tags: i.Properties.Tags.Items,
NamePrefix: fi.StringValue(e.Name),
}
tf.CanIPForward = i.Properties.CanIpForward
tf.MachineType = lastComponent(i.Properties.MachineType)
//tf.Zone = i.Properties.Zone
tf.Tags = i.Properties.Tags.Items
tf.AddServiceAccounts(i.Properties.ServiceAccounts)
for _, d := range i.Properties.Disks {

View File

@ -55,7 +55,7 @@ func BuildCloud(cluster *api.Cluster) (fi.Cloud, error) {
project = cluster.Spec.Project
if project == "" {
return nil, fmt.Errorf("project is required for GCE")
return nil, fmt.Errorf("project is required for GCE - try gcloud config get-value project")
}
labels := map[string]string{gce.GceLabelNameKubernetesCluster: gce.SafeClusterName(cluster.ObjectMeta.Name)}

View File

@ -157,6 +157,11 @@ func (t *DryRunTarget) PrintReport(taskMap map[string]Task, out io.Writer) error
field := changes.Field(i)
fieldName := changes.Type().Field(i).Name
if changes.Type().Field(i).PkgPath != "" {
// Not exported
continue
}
fieldValue := ValueAsString(field)
shouldPrint := true
@ -217,6 +222,11 @@ func (t *DryRunTarget) PrintReport(taskMap map[string]Task, out io.Writer) error
}
if valC.Kind() == reflect.Struct {
for i := 0; i < valC.NumField(); i++ {
if valC.Type().Field(i).PkgPath != "" {
// Not exported
continue
}
fieldValC := valC.Field(i)
changed := true