package google import ( "fmt" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" 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, 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, }, // 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{} { result := make([]map[string]interface{}, 0, len(disks)) for i, disk := range disks { diskMap := make(map[string]interface{}) if disk.InitializeParams != nil { var source_img = fmt.Sprintf("disk.%d.source_image", i) if d.Get(source_img) == nil || d.Get(source_img) == "" { diskMap["source_image"] = GetResourceNameFromSelfLink(disk.InitializeParams.SourceImage) } else { diskMap["source_image"] = d.Get(source_img) } 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 } 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) } } 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 { if err = d.Set("disk", flattenDisks(instanceTemplate.Properties.Disks, d)); 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) } } 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 }