package google import ( "fmt" "log" "github.com/hashicorp/terraform/helper/schema" strcase "github.com/stoewer/go-strcase" computeBeta "google.golang.org/api/compute/v0.beta" compute "google.golang.org/api/compute/v1" ) func resourceComputeInstanceFromTemplate() *schema.Resource { return &schema.Resource{ Create: resourceComputeInstanceFromTemplateCreate, Read: resourceComputeInstanceRead, Update: resourceComputeInstanceUpdate, Delete: resourceComputeInstanceDelete, // Import doesn't really make sense, because you could just import // as a google_compute_instance. Timeouts: resourceComputeInstance().Timeouts, Schema: computeInstanceFromTemplateSchema(), CustomizeDiff: resourceComputeInstance().CustomizeDiff, } } func computeInstanceFromTemplateSchema() map[string]*schema.Schema { s := resourceComputeInstance().Schema for _, field := range []string{"boot_disk", "machine_type", "network_interface"} { // The user can set these fields as an override, but doesn't need to - // the template values will be used if they're unset. s[field].Required = false s[field].Optional = true } // schema.SchemaConfigModeAttr allows these fields to be removed in Terraform 0.12. // Passing field_name = [] in this mode differentiates between an intentionally empty // block vs an ignored computed block. nic := s["network_interface"].Elem.(*schema.Resource) nic.Schema["alias_ip_range"].ConfigMode = schema.SchemaConfigModeAttr nic.Schema["access_config"].ConfigMode = schema.SchemaConfigModeAttr for _, field := range []string{"attached_disk", "guest_accelerator", "service_account", "scratch_disk"} { s[field].ConfigMode = schema.SchemaConfigModeAttr } // Remove deprecated/removed fields that are never d.Set. We can't // programatically remove all of them, because some of them still have d.Set // calls. for _, field := range []string{"disk", "network"} { delete(s, field) } recurseOnSchema(s, func(field *schema.Schema) { // We don't want to accidentally use default values to override the instance // template, so remove defaults. field.Default = nil // Make non-required fields computed since they'll be set by the template. // Leave deprecated and removed fields alone because we don't set them. if !field.Required && !(field.Deprecated != "" || field.Removed != "") { field.Computed = true } }) s["source_instance_template"] = &schema.Schema{ Type: schema.TypeString, Required: true, ForceNew: true, } return s } func recurseOnSchema(s map[string]*schema.Schema, f func(*schema.Schema)) { for _, field := range s { f(field) if e := field.Elem; e != nil { if r, ok := e.(*schema.Resource); ok { recurseOnSchema(r.Schema, f) } } } } func resourceComputeInstanceFromTemplateCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) project, err := getProject(d, config) if err != nil { return err } // Get the zone z, err := getZone(d, config) if err != nil { return err } log.Printf("[DEBUG] Loading zone: %s", z) zone, err := config.clientCompute.Zones.Get(project, z).Do() if err != nil { return fmt.Errorf("Error loading zone '%s': %s", z, err) } instance, err := expandComputeInstance(project, zone, d, config) if err != nil { return err } tpl, err := ParseInstanceTemplateFieldValue(d.Get("source_instance_template").(string), d, config) if err != nil { return err } it, err := config.clientComputeBeta.InstanceTemplates.Get(project, tpl.Name).Do() if err != nil { return err } instance.Disks, err = adjustInstanceFromTemplateDisks(d, config, it, zone, project) if err != nil { return err } // Force send all top-level fields that have been set in case they're overridden to zero values. // TODO: consider doing so for nested fields as well. // Initialize ForceSendFields to empty so we don't get things that the instance resource // always force-sends. instance.ForceSendFields = []string{} for f, s := range computeInstanceFromTemplateSchema() { // It seems that GetOkExists always returns true for sets. // TODO: confirm this and file issue against Terraform core. // In the meantime, don't force send sets. if s.Type == schema.TypeSet { continue } if _, exists := d.GetOkExists(f); exists { // Assume for now that all fields are exact snake_case versions of the API fields. // This won't necessarily always be true, but it serves as a good approximation and // can be adjusted later as we discover issues. instance.ForceSendFields = append(instance.ForceSendFields, strcase.UpperCamelCase(f)) } } log.Printf("[INFO] Requesting instance creation") op, err := config.clientComputeBeta.Instances.Insert(project, zone.Name, instance).SourceInstanceTemplate(tpl.RelativeLink()).Do() if err != nil { return fmt.Errorf("Error creating instance: %s", err) } // Store the ID now d.SetId(instance.Name) // Wait for the operation to complete waitErr := computeSharedOperationWaitTime(config.clientCompute, op, project, int(d.Timeout(schema.TimeoutCreate).Minutes()), "instance to create") if waitErr != nil { // The resource didn't actually create d.SetId("") return waitErr } return resourceComputeInstanceRead(d, meta) } // Instances have disks spread across multiple schema properties. This function // ensures that overriding one of these properties does not override the others. func adjustInstanceFromTemplateDisks(d *schema.ResourceData, config *Config, it *computeBeta.InstanceTemplate, zone *compute.Zone, project string) ([]*computeBeta.AttachedDisk, error) { disks := []*computeBeta.AttachedDisk{} if _, hasBootDisk := d.GetOk("boot_disk"); hasBootDisk { bootDisk, err := expandBootDisk(d, config, zone, project) if err != nil { return nil, err } disks = append(disks, bootDisk) } else { // boot disk was not overridden, so use the one from the instance template for _, disk := range it.Properties.Disks { if disk.Boot { if dt := disk.InitializeParams.DiskType; dt != "" { // Instances need a URL for the disk type, but instance templates // only have the name (since they're global). disk.InitializeParams.DiskType = fmt.Sprintf("zones/%s/diskTypes/%s", zone.Name, dt) } disks = append(disks, disk) break } } } if _, hasScratchDisk := d.GetOk("scratch_disk"); hasScratchDisk { scratchDisks, err := expandScratchDisks(d, config, zone, project) if err != nil { return nil, err } disks = append(disks, scratchDisks...) } else { // scratch disks were not overridden, so use the ones from the instance template for _, disk := range it.Properties.Disks { if disk.Type == "SCRATCH" { disks = append(disks, disk) } } } attachedDisksCount := d.Get("attached_disk.#").(int) if attachedDisksCount > 0 { for i := 0; i < attachedDisksCount; i++ { diskConfig := d.Get(fmt.Sprintf("attached_disk.%d", i)).(map[string]interface{}) disk, err := expandAttachedDisk(diskConfig, d, config) if err != nil { return nil, err } disks = append(disks, disk) } } else { // attached disks were not overridden, so use the ones from the instance template for _, disk := range it.Properties.Disks { if !disk.Boot && disk.Type != "SCRATCH" { if s := disk.Source; s != "" { // Instances need a URL for the disk source, but instance templates // only have the name (since they're global). disk.Source = fmt.Sprintf("zones/%s/disks/%s", zone.Name, s) } disks = append(disks, disk) } } } return disks, nil }