terraform-provider-google/google/resource_compute_instance_template.go
Paddy Carver b94881b423 Always set instance template source images on read.
Previously, we were only setting source images for the disks in instance
templates if they weren't set in the config. Instead, let's just always
set them. This fixes our test failures about self_links not matching
names. Also, relative links are safer than just image names, as
Terraform wouldn't notice if another project's image of the same name
got used instead of your project's.

Also fix the setting of tags and tag fingerprints to always set, even if
to the empty value, to fix the tests.
2018-08-21 18:32:25 -07:00

805 lines
23 KiB
Go

package google
import (
"fmt"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
computeBeta "google.golang.org/api/compute/v0.beta"
"google.golang.org/api/googleapi"
)
func resourceComputeInstanceTemplate() *schema.Resource {
return &schema.Resource{
Create: resourceComputeInstanceTemplateCreate,
Read: resourceComputeInstanceTemplateRead,
Delete: resourceComputeInstanceTemplateDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
SchemaVersion: 1,
MigrateState: resourceComputeInstanceTemplateMigrateState,
// A compute instance template is more or less a subset of a compute
// instance. Please attempt to maintain consistency with the
// resource_compute_instance schema when updating this one.
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{"name_prefix"},
ValidateFunc: validateGCPName,
},
"name_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) {
// https://cloud.google.com/compute/docs/reference/latest/instanceTemplates#resource
// uuid is 26 characters, limit the prefix to 37.
value := v.(string)
if len(value) > 37 {
errors = append(errors, fmt.Errorf(
"%q cannot be longer than 37 characters, name is limited to 63", k))
}
return
},
},
"disk": &schema.Schema{
Type: schema.TypeList,
Required: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"auto_delete": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"boot": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Computed: true,
},
"device_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"disk_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"disk_size_gb": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"disk_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"source_image": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DiffSuppressFunc: compareSelfLinkRelativePaths,
ForceNew: true,
},
"interface": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"mode": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"source": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
},
},
},
"machine_type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"automatic_restart": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Removed: "Use 'scheduling.automatic_restart' instead.",
},
"can_ip_forward": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"instance_description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"metadata": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
},
"metadata_startup_script": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"metadata_fingerprint": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"network_interface": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"network": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
DiffSuppressFunc: compareSelfLinkOrResourceName,
},
"address": &schema.Schema{
Type: schema.TypeString,
Computed: true, // Computed because it is set if network_ip is set.
Optional: true,
ForceNew: true,
},
"network_ip": &schema.Schema{
Type: schema.TypeString,
Computed: true, // Computed because it is set if address is set.
Optional: true,
ForceNew: true,
Deprecated: "Please use address",
},
"subnetwork": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
DiffSuppressFunc: compareSelfLinkOrResourceName,
},
"subnetwork_project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"access_config": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"nat_ip": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"network_tier": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice([]string{"PREMIUM", "STANDARD"}, false),
},
// Instance templates will never have an
// 'assigned NAT IP', but we need this in
// the schema to allow us to share flatten
// code with an instance, which could.
"assigned_nat_ip": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
},
},
},
"alias_ip_range": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"ip_cidr_range": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: ipCidrRangeDiffSuppress,
},
"subnetwork_range_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
},
},
},
},
},
},
"on_host_maintenance": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Removed: "Use 'scheduling.on_host_maintenance' instead.",
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"region": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
"scheduling": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Computed: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"preemptible": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
ForceNew: true,
},
"automatic_restart": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
ForceNew: true,
},
"on_host_maintenance": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
},
},
"self_link": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"service_account": &schema.Schema{
Type: schema.TypeList,
MaxItems: 1,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"email": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"scopes": &schema.Schema{
Type: schema.TypeSet,
Required: true,
ForceNew: true,
Elem: &schema.Schema{
Type: schema.TypeString,
StateFunc: func(v interface{}) string {
return canonicalizeServiceScope(v.(string))
},
},
Set: stringScopeHashcode,
},
},
},
},
"guest_accelerator": &schema.Schema{
Type: schema.TypeList,
Optional: true,
ForceNew: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"count": &schema.Schema{
Type: schema.TypeInt,
Required: true,
ForceNew: true,
},
"type": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
DiffSuppressFunc: linkDiffSuppress,
},
},
},
},
"min_cpu_platform": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"tags": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"tags_fingerprint": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"labels": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
},
}
}
func buildDisks(d *schema.ResourceData, config *Config) ([]*computeBeta.AttachedDisk, error) {
project, err := getProject(d, config)
if err != nil {
return nil, err
}
disksCount := d.Get("disk.#").(int)
disks := make([]*computeBeta.AttachedDisk, 0, disksCount)
for i := 0; i < disksCount; i++ {
prefix := fmt.Sprintf("disk.%d", i)
// Build the disk
var disk computeBeta.AttachedDisk
disk.Type = "PERSISTENT"
disk.Mode = "READ_WRITE"
disk.Interface = "SCSI"
disk.Boot = i == 0
disk.AutoDelete = d.Get(prefix + ".auto_delete").(bool)
if v, ok := d.GetOk(prefix + ".boot"); ok {
disk.Boot = v.(bool)
}
if v, ok := d.GetOk(prefix + ".device_name"); ok {
disk.DeviceName = v.(string)
}
if v, ok := d.GetOk(prefix + ".source"); ok {
disk.Source = v.(string)
} else {
disk.InitializeParams = &computeBeta.AttachedDiskInitializeParams{}
if v, ok := d.GetOk(prefix + ".disk_name"); ok {
disk.InitializeParams.DiskName = v.(string)
}
if v, ok := d.GetOk(prefix + ".disk_size_gb"); ok {
disk.InitializeParams.DiskSizeGb = int64(v.(int))
}
disk.InitializeParams.DiskType = "pd-standard"
if v, ok := d.GetOk(prefix + ".disk_type"); ok {
disk.InitializeParams.DiskType = v.(string)
}
if v, ok := d.GetOk(prefix + ".source_image"); ok {
imageName := v.(string)
imageUrl, err := resolveImage(config, project, imageName)
if err != nil {
return nil, fmt.Errorf(
"Error resolving image name '%s': %s",
imageName, err)
}
disk.InitializeParams.SourceImage = imageUrl
}
}
if v, ok := d.GetOk(prefix + ".interface"); ok {
disk.Interface = v.(string)
}
if v, ok := d.GetOk(prefix + ".mode"); ok {
disk.Mode = v.(string)
}
if v, ok := d.GetOk(prefix + ".type"); ok {
disk.Type = v.(string)
}
disks = append(disks, &disk)
}
return disks, nil
}
// We don't share this code with compute instances because instances want a
// partial URL, but instance templates want the bare accelerator name (despite
// the docs saying otherwise).
//
// Using a partial URL on an instance template results in:
// Invalid value for field 'resource.properties.guestAccelerators[0].acceleratorType':
// 'zones/us-east1-b/acceleratorTypes/nvidia-tesla-k80'.
// Accelerator type 'zones/us-east1-b/acceleratorTypes/nvidia-tesla-k80'
// must be a valid resource name (not an url).
func expandInstanceTemplateGuestAccelerators(d TerraformResourceData, config *Config) []*computeBeta.AcceleratorConfig {
configs, ok := d.GetOk("guest_accelerator")
if !ok {
return nil
}
accels := configs.([]interface{})
guestAccelerators := make([]*computeBeta.AcceleratorConfig, 0, len(accels))
for _, raw := range accels {
data := raw.(map[string]interface{})
if data["count"].(int) == 0 {
continue
}
guestAccelerators = append(guestAccelerators, &computeBeta.AcceleratorConfig{
AcceleratorCount: int64(data["count"].(int)),
// We can't use ParseAcceleratorFieldValue here because an instance
// template does not have a zone we can use.
AcceleratorType: data["type"].(string),
})
}
return guestAccelerators
}
func resourceComputeInstanceTemplateCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
instanceProperties := &computeBeta.InstanceProperties{
CanIpForward: d.Get("can_ip_forward").(bool),
Description: d.Get("instance_description").(string),
MachineType: d.Get("machine_type").(string),
MinCpuPlatform: d.Get("min_cpu_platform").(string),
}
disks, err := buildDisks(d, config)
if err != nil {
return err
}
instanceProperties.Disks = disks
metadata, err := resourceInstanceMetadata(d)
if err != nil {
return err
}
instanceProperties.Metadata = metadata
networks, err := expandNetworkInterfaces(d, config)
if err != nil {
return err
}
instanceProperties.NetworkInterfaces = networks
instanceProperties.Scheduling = &computeBeta.Scheduling{}
instanceProperties.Scheduling.OnHostMaintenance = "MIGRATE"
forceSendFieldsScheduling := make([]string, 0, 3)
var hasSendMaintenance bool
hasSendMaintenance = false
if v, ok := d.GetOk("scheduling"); ok {
_schedulings := v.([]interface{})
if len(_schedulings) > 1 {
return fmt.Errorf("Error, at most one `scheduling` block can be defined")
}
_scheduling := _schedulings[0].(map[string]interface{})
// "automatic_restart" has a default value and is always safe to dereference
automaticRestart := _scheduling["automatic_restart"].(bool)
instanceProperties.Scheduling.AutomaticRestart = googleapi.Bool(automaticRestart)
forceSendFieldsScheduling = append(forceSendFieldsScheduling, "AutomaticRestart")
if vp, okp := _scheduling["on_host_maintenance"]; okp {
instanceProperties.Scheduling.OnHostMaintenance = vp.(string)
forceSendFieldsScheduling = append(forceSendFieldsScheduling, "OnHostMaintenance")
hasSendMaintenance = true
}
if vp, okp := _scheduling["preemptible"]; okp {
instanceProperties.Scheduling.Preemptible = vp.(bool)
forceSendFieldsScheduling = append(forceSendFieldsScheduling, "Preemptible")
if vp.(bool) && !hasSendMaintenance {
instanceProperties.Scheduling.OnHostMaintenance = "TERMINATE"
forceSendFieldsScheduling = append(forceSendFieldsScheduling, "OnHostMaintenance")
}
}
}
instanceProperties.Scheduling.ForceSendFields = forceSendFieldsScheduling
instanceProperties.ServiceAccounts = expandServiceAccounts(d.Get("service_account").([]interface{}))
instanceProperties.GuestAccelerators = expandInstanceTemplateGuestAccelerators(d, config)
instanceProperties.Tags = resourceInstanceTags(d)
if _, ok := d.GetOk("labels"); ok {
instanceProperties.Labels = expandLabels(d)
}
var itName string
if v, ok := d.GetOk("name"); ok {
itName = v.(string)
} else if v, ok := d.GetOk("name_prefix"); ok {
itName = resource.PrefixedUniqueId(v.(string))
} else {
itName = resource.UniqueId()
}
instanceTemplate := &computeBeta.InstanceTemplate{
Description: d.Get("description").(string),
Properties: instanceProperties,
Name: itName,
}
op, err := config.clientComputeBeta.InstanceTemplates.Insert(project, instanceTemplate).Do()
if err != nil {
return fmt.Errorf("Error creating instance template: %s", err)
}
// Store the ID now
d.SetId(instanceTemplate.Name)
err = computeSharedOperationWait(config.clientCompute, op, project, "Creating Instance Template")
if err != nil {
return err
}
return resourceComputeInstanceTemplateRead(d, meta)
}
func flattenDisks(disks []*computeBeta.AttachedDisk, d *schema.ResourceData) ([]map[string]interface{}, error) {
result := make([]map[string]interface{}, 0, len(disks))
for _, disk := range disks {
diskMap := make(map[string]interface{})
if disk.InitializeParams != nil {
path, err := getRelativePath(disk.InitializeParams.SourceImage)
if err != nil {
return nil, errwrap.Wrapf("Error getting relative path for source image: {{err}}", err)
}
diskMap["source_image"] = path
diskMap["disk_type"] = disk.InitializeParams.DiskType
diskMap["disk_name"] = disk.InitializeParams.DiskName
diskMap["disk_size_gb"] = disk.InitializeParams.DiskSizeGb
}
diskMap["auto_delete"] = disk.AutoDelete
diskMap["boot"] = disk.Boot
diskMap["device_name"] = disk.DeviceName
diskMap["interface"] = disk.Interface
diskMap["source"] = disk.Source
diskMap["mode"] = disk.Mode
diskMap["type"] = disk.Type
result = append(result, diskMap)
}
return result, nil
}
func resourceComputeInstanceTemplateRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
instanceTemplate, err := config.clientComputeBeta.InstanceTemplates.Get(project, d.Id()).Do()
if err != nil {
return handleNotFoundError(err, d, fmt.Sprintf("Instance Template %q", d.Get("name").(string)))
}
// Set the metadata fingerprint if there is one.
if instanceTemplate.Properties.Metadata != nil {
if err = d.Set("metadata_fingerprint", instanceTemplate.Properties.Metadata.Fingerprint); err != nil {
return fmt.Errorf("Error setting metadata_fingerprint: %s", err)
}
md := instanceTemplate.Properties.Metadata
_md := flattenMetadataBeta(md)
if script, scriptExists := d.GetOk("metadata_startup_script"); scriptExists {
if err = d.Set("metadata_startup_script", script); err != nil {
return fmt.Errorf("Error setting metadata_startup_script: %s", err)
}
delete(_md, "startup-script")
}
if err = d.Set("metadata", _md); err != nil {
return fmt.Errorf("Error setting metadata: %s", err)
}
}
// Set the tags fingerprint if there is one.
if instanceTemplate.Properties.Tags != nil {
if err = d.Set("tags_fingerprint", instanceTemplate.Properties.Tags.Fingerprint); err != nil {
return fmt.Errorf("Error setting tags_fingerprint: %s", err)
}
} else {
d.Set("tags_fingerprint", "")
}
if instanceTemplate.Properties.Labels != nil {
d.Set("labels", instanceTemplate.Properties.Labels)
}
if err = d.Set("self_link", instanceTemplate.SelfLink); err != nil {
return fmt.Errorf("Error setting self_link: %s", err)
}
if err = d.Set("name", instanceTemplate.Name); err != nil {
return fmt.Errorf("Error setting name: %s", err)
}
if instanceTemplate.Properties.Disks != nil {
disks, err := flattenDisks(instanceTemplate.Properties.Disks, d)
if err != nil {
return fmt.Errorf("error flattening disks: %s", err)
}
if err = d.Set("disk", disks); err != nil {
return fmt.Errorf("Error setting disk: %s", err)
}
}
if err = d.Set("description", instanceTemplate.Description); err != nil {
return fmt.Errorf("Error setting description: %s", err)
}
if err = d.Set("machine_type", instanceTemplate.Properties.MachineType); err != nil {
return fmt.Errorf("Error setting machine_type: %s", err)
}
if err = d.Set("min_cpu_platform", instanceTemplate.Properties.MinCpuPlatform); err != nil {
return fmt.Errorf("Error setting min_cpu_platform: %s", err)
}
if err = d.Set("can_ip_forward", instanceTemplate.Properties.CanIpForward); err != nil {
return fmt.Errorf("Error setting can_ip_forward: %s", err)
}
if err = d.Set("instance_description", instanceTemplate.Properties.Description); err != nil {
return fmt.Errorf("Error setting instance_description: %s", err)
}
if err = d.Set("project", project); err != nil {
return fmt.Errorf("Error setting project: %s", err)
}
if instanceTemplate.Properties.NetworkInterfaces != nil {
networkInterfaces, region, _, _, err := flattenNetworkInterfaces(d, config, instanceTemplate.Properties.NetworkInterfaces)
if err != nil {
return err
}
if err = d.Set("network_interface", networkInterfaces); err != nil {
return fmt.Errorf("Error setting network_interface: %s", err)
}
// region is where to look up the subnetwork if there is one attached to the instance template
if region != "" {
if err = d.Set("region", region); err != nil {
return fmt.Errorf("Error setting region: %s", err)
}
}
}
if instanceTemplate.Properties.Scheduling != nil {
scheduling := flattenScheduling(instanceTemplate.Properties.Scheduling)
if err = d.Set("scheduling", scheduling); err != nil {
return fmt.Errorf("Error setting scheduling: %s", err)
}
}
if instanceTemplate.Properties.Tags != nil {
if err = d.Set("tags", instanceTemplate.Properties.Tags.Items); err != nil {
return fmt.Errorf("Error setting tags: %s", err)
}
} else {
if err = d.Set("tags", nil); err != nil {
return fmt.Errorf("Error setting empty tags: %s", err)
}
}
if instanceTemplate.Properties.ServiceAccounts != nil {
if err = d.Set("service_account", flattenServiceAccounts(instanceTemplate.Properties.ServiceAccounts)); err != nil {
return fmt.Errorf("Error setting service_account: %s", err)
}
}
if instanceTemplate.Properties.GuestAccelerators != nil {
if err = d.Set("guest_accelerator", flattenGuestAccelerators(instanceTemplate.Properties.GuestAccelerators)); err != nil {
return fmt.Errorf("Error setting guest_accelerator: %s", err)
}
}
return nil
}
func resourceComputeInstanceTemplateDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
op, err := config.clientCompute.InstanceTemplates.Delete(
project, d.Id()).Do()
if err != nil {
return fmt.Errorf("Error deleting instance template: %s", err)
}
err = computeOperationWait(config.clientCompute, op, project, "Deleting Instance Template")
if err != nil {
return err
}
d.SetId("")
return nil
}