2018-06-28 23:09:23 +00:00
|
|
|
package google
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
|
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
|
|
strcase "github.com/stoewer/go-strcase"
|
2018-12-18 18:22:13 +00:00
|
|
|
computeBeta "google.golang.org/api/compute/v0.beta"
|
|
|
|
compute "google.golang.org/api/compute/v1"
|
2018-06-28 23:09:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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"} {
|
2018-09-20 23:49:48 +00:00
|
|
|
// The user can set these fields as an override, but doesn't need to -
|
|
|
|
// the template values will be used if they're unset.
|
2018-06-28 23:09:23 +00:00
|
|
|
s[field].Required = false
|
2018-09-20 23:49:48 +00:00
|
|
|
s[field].Optional = true
|
2018-06-28 23:09:23 +00:00
|
|
|
}
|
|
|
|
|
2019-04-15 21:21:24 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2018-06-28 23:09:23 +00:00
|
|
|
// 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.
|
2019-04-16 01:14:01 +00:00
|
|
|
for _, field := range []string{"disk", "network"} {
|
2018-06-28 23:09:23 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2018-12-18 18:22:13 +00:00
|
|
|
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.
|
2018-06-28 23:09:23 +00:00
|
|
|
// TODO: consider doing so for nested fields as well.
|
2018-12-18 18:22:13 +00:00
|
|
|
// Initialize ForceSendFields to empty so we don't get things that the instance resource
|
|
|
|
// always force-sends.
|
|
|
|
instance.ForceSendFields = []string{}
|
2018-06-28 23:09:23 +00:00
|
|
|
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)
|
|
|
|
}
|
2018-12-18 18:22:13 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|