From e85c7113fa25e129ffbe19253495f3101e352730 Mon Sep 17 00:00:00 2001 From: Dave Cunningham Date: Thu, 29 Jan 2015 20:00:02 -0500 Subject: [PATCH 01/13] Allow specifying project or full URL when specifying image --- builtin/providers/google/image.go | 95 ++++++++++++------- .../providers/google/resource_compute_disk.go | 8 +- .../google/resource_compute_instance.go | 8 +- .../google/resource_compute_instance_test.go | 42 ++++++++ .../google/r/compute_disk.html.markdown | 4 +- .../google/r/compute_instance.html.markdown | 5 +- 6 files changed, 120 insertions(+), 42 deletions(-) diff --git a/builtin/providers/google/image.go b/builtin/providers/google/image.go index 48fff5402..074202283 100644 --- a/builtin/providers/google/image.go +++ b/builtin/providers/google/image.go @@ -1,44 +1,75 @@ package google import ( + "fmt" "strings" - - "code.google.com/p/google-api-go-client/compute/v1" ) -// readImage finds the image with the given name. -func readImage(c *Config, name string) (*compute.Image, error) { - // First, always try ourselves first. - image, err := c.clientCompute.Images.Get(c.Project, name).Do() - if err == nil && image != nil && image.SelfLink != "" { - return image, nil - } +// If the given name is a URL, return it. +// If it is of the form project/name, use that URL. +// If it is of the form name then look in the configured project and then hosted image projects. +func resolveImage(c *Config, name string) (string, error) { - // This is a map of names to the project name where a public image is - // hosted. GCE doesn't have an API to simply look up an image without - // a project so we do this jank thing. - imageMap := map[string]string{ - "centos": "centos-cloud", - "coreos": "coreos-cloud", - "debian": "debian-cloud", - "opensuse": "opensuse-cloud", - "rhel": "rhel-cloud", - "sles": "suse-cloud", - "ubuntu": "ubuntu-os-cloud", - } - // If we match a lookup for an alternate project, then try that next. - // If not, we return the error. - var project string - for k, v := range imageMap { - if strings.Contains(name, k) { - project = v - break + if strings.HasPrefix(name, "https://www.googleapis.com/compute/v1/") { + return name, nil + + } else { + splitName := strings.Split(name, "/") + if len(splitName) == 1 { + + // Must infer the project name: + + // First, try the configured project. + image, err := c.clientCompute.Images.Get(c.Project, name).Do() + if err == nil { + return image.SelfLink, nil + } + + // If we match a lookup for an alternate project, then try that next. + // If not, we return the original error. + + // If the image name contains the left hand side, we use the project from the right hand + // side. + imageMap := map[string]string{ + "centos": "centos-cloud", + "coreos": "coreos-cloud", + "debian": "debian-cloud", + "opensuse": "opensuse-cloud", + "rhel": "rhel-cloud", + "sles": "suse-cloud", + "ubuntu": "ubuntu-os-cloud", + "windows": "windows-cloud", + } + var project string + for k, v := range imageMap { + if strings.Contains(name, k) { + project = v + break + } + } + if project == "" { + return "", err + } + + // There was a match, but the image still may not exist, so check it: + image, err = c.clientCompute.Images.Get(project, name).Do() + if err == nil { + return image.SelfLink, nil + } + + return "", err + + } else if len(splitName) == 2 { + image, err := c.clientCompute.Images.Get(splitName[0], splitName[1]).Do() + if err == nil { + return image.SelfLink, nil + } + return "", err + + } else { + return "", fmt.Errorf("Invalid image name, require URL, project/name, or just name: %s", name) } } - if project == "" { - return nil, err - } - return c.clientCompute.Images.Get(project, name).Do() } diff --git a/builtin/providers/google/resource_compute_disk.go b/builtin/providers/google/resource_compute_disk.go index 378b0171e..b4c203467 100644 --- a/builtin/providers/google/resource_compute_disk.go +++ b/builtin/providers/google/resource_compute_disk.go @@ -70,15 +70,15 @@ func resourceComputeDiskCreate(d *schema.ResourceData, meta interface{}) error { // If we were given a source image, load that. if v, ok := d.GetOk("image"); ok { - log.Printf("[DEBUG] Loading image: %s", v.(string)) - image, err := readImage(config, v.(string)) + log.Printf("[DEBUG] Resolving image name: %s", v.(string)) + imageUrl, err := resolveImage(config, v.(string)) if err != nil { return fmt.Errorf( - "Error loading image '%s': %s", + "Error resolving image name '%s': %s", v.(string), err) } - disk.SourceImage = image.SelfLink + disk.SourceImage = imageUrl } if v, ok := d.GetOk("type"); ok { diff --git a/builtin/providers/google/resource_compute_instance.go b/builtin/providers/google/resource_compute_instance.go index 98e9faf95..e4438d876 100644 --- a/builtin/providers/google/resource_compute_instance.go +++ b/builtin/providers/google/resource_compute_instance.go @@ -231,15 +231,17 @@ func resourceComputeInstanceCreate(d *schema.ResourceData, meta interface{}) err // Load up the image for this disk if specified if v, ok := d.GetOk(prefix + ".image"); ok { imageName := v.(string) - image, err := readImage(config, imageName) + + + imageUrl, err := resolveImage(config, imageName) if err != nil { return fmt.Errorf( - "Error loading image '%s': %s", + "Error resolving image name '%s': %s", imageName, err) } disk.InitializeParams = &compute.AttachedDiskInitializeParams{ - SourceImage: image.SelfLink, + SourceImage: imageUrl, } } diff --git a/builtin/providers/google/resource_compute_instance_test.go b/builtin/providers/google/resource_compute_instance_test.go index f765a44c4..424351993 100644 --- a/builtin/providers/google/resource_compute_instance_test.go +++ b/builtin/providers/google/resource_compute_instance_test.go @@ -240,6 +240,48 @@ resource "google_compute_instance" "foobar" { } }` +const testAccComputeInstance_basic2 = ` +resource "google_compute_instance" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + can_ip_forward = false + tags = ["foo", "bar"] + + disk { + image = "debian-cloud/debian-7-wheezy-v20140814" + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } +}` + +const testAccComputeInstance_basic3 = ` +resource "google_compute_instance" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + zone = "us-central1-a" + can_ip_forward = false + tags = ["foo", "bar"] + + disk { + image = "https://www.googleapis.com/compute/v1/projects/debian-cloud/global/images/debian-7-wheezy-v20140814" + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } +}` + const testAccComputeInstance_update = ` resource "google_compute_instance" "foobar" { name = "terraform-test" diff --git a/website/source/docs/providers/google/r/compute_disk.html.markdown b/website/source/docs/providers/google/r/compute_disk.html.markdown index e2fae7380..4a8388bca 100644 --- a/website/source/docs/providers/google/r/compute_disk.html.markdown +++ b/website/source/docs/providers/google/r/compute_disk.html.markdown @@ -30,7 +30,9 @@ The following arguments are supported: * `zone` - (Required) The zone where this disk will be available. -* `image` - (Optional) The machine image to base this disk off of. +* `image` - (Optional) The image from which to initialize this disk. Either the full URL, a + contraction of the form "project/name", or just a name (in which case the current project is +used). * `size` - (Optional) The size of the image in gigabytes. If not specified, it will inherit the size of its base image. diff --git a/website/source/docs/providers/google/r/compute_instance.html.markdown b/website/source/docs/providers/google/r/compute_instance.html.markdown index a9e6a6687..a9cc69fb2 100644 --- a/website/source/docs/providers/google/r/compute_instance.html.markdown +++ b/website/source/docs/providers/google/r/compute_instance.html.markdown @@ -73,8 +73,9 @@ The `disk` block supports: * `disk` - (Required if image not set) The name of the disk (such as those managed by `google_compute_disk`) to attach. -* `image` - (Required if disk not set) The name of the image to base - this disk off of. +* `image` - (Required if disk not set) The image from which to initialize this + disk. Either the full URL, a contraction of the form "project/name", or just + a name (in which case the current project is used). * `auto_delete` - (Optional) Whether or not the disk should be auto-deleted. This defaults to true. From 2552590e227940ef5920684b2d5eacffe5937d85 Mon Sep 17 00:00:00 2001 From: jba Date: Mon, 9 Feb 2015 11:27:00 +0100 Subject: [PATCH 02/13] fix the GOPATH/bin --- Vagrantfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index 687bc09af..6415a58b6 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -15,7 +15,7 @@ cd /opt/go/src && ./all.bash mkdir -p /opt/gopath cat </etc/profile.d/gopath.sh export GOPATH="/opt/gopath" -export PATH="/opt/go/bin:\$GOPATH/bin:\$PATH" +export PATH="/opt/go/bin:$GOPATH/bin:\$PATH" EOF # Make sure the GOPATH is usable by vagrant From dfa97dfc1769fc1fe634725c4953d197e64787af Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 10:29:27 +0000 Subject: [PATCH 03/13] Add Instance Template support to google provider. --- builtin/providers/google/provider.go | 1 + .../resource_compute_instance_template.go | 472 ++++++++++++++++++ 2 files changed, 473 insertions(+) create mode 100644 builtin/providers/google/resource_compute_instance_template.go diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index 37d662eaa..da52e0682 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -35,6 +35,7 @@ func Provider() terraform.ResourceProvider { "google_compute_forwarding_rule": resourceComputeForwardingRule(), "google_compute_http_health_check": resourceComputeHttpHealthCheck(), "google_compute_instance": resourceComputeInstance(), + "google_compute_instance_template": resourceComputeInstanceTemplate(), "google_compute_network": resourceComputeNetwork(), "google_compute_route": resourceComputeRoute(), "google_compute_target_pool": resourceComputeTargetPool(), diff --git a/builtin/providers/google/resource_compute_instance_template.go b/builtin/providers/google/resource_compute_instance_template.go new file mode 100644 index 000000000..25907dd27 --- /dev/null +++ b/builtin/providers/google/resource_compute_instance_template.go @@ -0,0 +1,472 @@ +package google + +import ( + "fmt" + "time" + + "code.google.com/p/google-api-go-client/compute/v1" + "code.google.com/p/google-api-go-client/googleapi" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceComputeInstanceTemplate() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeInstanceTemplateCreate, + Read: resourceComputeInstanceTemplateRead, + Delete: resourceComputeInstanceTemplateDelete, + + // TODO: check which items are optional and set optional: true + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "can_ip_forward": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + + "instance_description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "machine_type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + // TODO: Constraint either source or other disk params + "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, + ForceNew: true, + }, + + "boot": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + ForceNew: true, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "disk_name": &schema.Schema{ + Type: schema.TypeString, + 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, + }, + + "source_image": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "interface": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "mode": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "source": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + }, + }, + }, + + "metadata": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeMap, + }, + }, + + "network": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Required: true, + }, + + "address": &schema.Schema{ + Type: schema.TypeString, + ForceNew: true, + Optional: true, + }, + }, + }, + }, + + "automatic_restart": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "on_host_maintenance": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "service_account": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "email": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + ForceNew: true, + }, + + "scopes": &schema.Schema{ + Type: schema.TypeList, + Required: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + StateFunc: func(v interface{}) string { + return canonicalizeServiceScope(v.(string)) + }, + }, + }, + }, + }, + }, + + "tags": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + + "metadata_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "tags_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func buildDisks(d *schema.ResourceData, meta interface{}) []*compute.AttachedDisk { + disksCount := d.Get("disk.#").(int) + + disks := make([]*compute.AttachedDisk, 0, disksCount) + for i := 0; i < disksCount; i++ { + prefix := fmt.Sprintf("disk.%d", i) + + // Build the disk + var disk compute.AttachedDisk + disk.Type = "PERSISTENT" + disk.Mode = "READ_WRITE" + disk.Interface = "SCSI" + disk.Boot = i == 0 + disk.AutoDelete = true + + if v, ok := d.GetOk(prefix + ".auto_delete"); ok { + disk.AutoDelete = v.(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 = &compute.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 = v.(int64) + } + 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 { + disk.InitializeParams.SourceImage = v.(string) + } + } + + 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 +} + +func buildNetworks(d *schema.ResourceData, meta interface{}) (error, []*compute.NetworkInterface) { + // Build up the list of networks + networksCount := d.Get("network.#").(int) + networks := make([]*compute.NetworkInterface, 0, networksCount) + for i := 0; i < networksCount; i++ { + prefix := fmt.Sprintf("network.%d", i) + + source := "global/networks/default" + if v, ok := d.GetOk(prefix + ".source"); ok { + if v.(string) != "default" { + source = v.(string) + } + } + + // Build the interface + var iface compute.NetworkInterface + iface.AccessConfigs = []*compute.AccessConfig{ + &compute.AccessConfig{ + Type: "ONE_TO_ONE_NAT", + NatIP: d.Get(prefix + ".address").(string), + }, + } + iface.Network = source + + networks = append(networks, &iface) + } + return nil, networks +} + +func resourceComputeInstanceTemplateCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + instanceProperties := &compute.InstanceProperties{} + + instanceProperties.CanIpForward = d.Get("can_ip_forward").(bool) + instanceProperties.Description = d.Get("instance_description").(string) + instanceProperties.MachineType = d.Get("machine_type").(string) + instanceProperties.Disks = buildDisks(d, meta) + instanceProperties.Metadata = resourceInstanceMetadata(d) + err, networks := buildNetworks(d, meta) + if err != nil { + return err + } + instanceProperties.NetworkInterfaces = networks + + instanceProperties.Scheduling = &compute.Scheduling{ + AutomaticRestart: d.Get("automatic_restart").(bool), + } + instanceProperties.Scheduling.OnHostMaintenance = "MIGRATE" + if v, ok := d.GetOk("on_host_maintenance"); ok { + instanceProperties.Scheduling.OnHostMaintenance = v.(string) + } + + serviceAccountsCount := d.Get("service_account.#").(int) + serviceAccounts := make([]*compute.ServiceAccount, 0, serviceAccountsCount) + for i := 0; i < serviceAccountsCount; i++ { + prefix := fmt.Sprintf("service_account.%d", i) + + scopesCount := d.Get(prefix + ".scopes.#").(int) + scopes := make([]string, 0, scopesCount) + for j := 0; j < scopesCount; j++ { + scope := d.Get(fmt.Sprintf(prefix+".scopes.%d", j)).(string) + scopes = append(scopes, canonicalizeServiceScope(scope)) + } + + serviceAccount := &compute.ServiceAccount{ + Email: "default", + Scopes: scopes, + } + + serviceAccounts = append(serviceAccounts, serviceAccount) + } + instanceProperties.ServiceAccounts = serviceAccounts + + instanceProperties.Tags = resourceInstanceTags(d) + + instanceTemplate := compute.InstanceTemplate{ + Description: d.Get("description").(string), + Properties: instanceProperties, + Name: d.Get("name").(string), + } + + op, err := config.clientCompute.InstanceTemplates.Insert( + config.Project, &instanceTemplate).Do() + if err != nil { + return fmt.Errorf("Error creating instance: %s", err) + } + + // Store the ID now + d.SetId(instanceTemplate.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Delay = 10 * time.Second + state.Timeout = 10 * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance template to create: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // The resource didn't actually create + d.SetId("") + + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeInstanceTemplateRead(d, meta) +} + +func resourceComputeInstanceTemplateRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + instanceTemplate, err := config.clientCompute.InstanceTemplates.Get( + config.Project, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading instance template: %s", err) + } + + // Set the metadata fingerprint if there is one. + if instanceTemplate.Properties.Metadata != nil { + d.Set("metadata_fingerprint", instanceTemplate.Properties.Metadata.Fingerprint) + } + + // Set the tags fingerprint if there is one. + if instanceTemplate.Properties.Tags != nil { + d.Set("tags_fingerprint", instanceTemplate.Properties.Tags.Fingerprint) + } + d.Set("self_link", instanceTemplate.SelfLink) + + return nil +} + +func resourceComputeInstanceTemplateDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + op, err := config.clientCompute.InstanceTemplates.Delete( + config.Project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting instance template: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Delay = 5 * time.Second + state.Timeout = 5 * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance template to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} From ec1f874bdc277e90cb7d07e8f4df5cb84a1bd03e Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 10:29:49 +0000 Subject: [PATCH 04/13] Add tests for Instance Template support to google provider. --- ...resource_compute_instance_template_test.go | 278 ++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 builtin/providers/google/resource_compute_instance_template_test.go diff --git a/builtin/providers/google/resource_compute_instance_template_test.go b/builtin/providers/google/resource_compute_instance_template_test.go new file mode 100644 index 000000000..74133089d --- /dev/null +++ b/builtin/providers/google/resource_compute_instance_template_test.go @@ -0,0 +1,278 @@ +package google + +import ( + "fmt" + "testing" + + "code.google.com/p/google-api-go-client/compute/v1" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccComputeInstanceTemplate_basic(t *testing.T) { + var instanceTemplate compute.InstanceTemplate + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceTemplate_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists( + "google_compute_instance_template.foobar", &instanceTemplate), + testAccCheckComputeInstanceTemplateTag(&instanceTemplate, "foo"), + testAccCheckComputeInstanceTemplateMetadata(&instanceTemplate, "foo", "bar"), + testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "debian-7-wheezy-v20140814", true, true), + ), + }, + }, + }) +} + +func TestAccComputeInstanceTemplate_IP(t *testing.T) { + var instanceTemplate compute.InstanceTemplate + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceTemplate_ip, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists( + "google_compute_instance_template.foobar", &instanceTemplate), + testAccCheckComputeInstanceTemplateNetwork(&instanceTemplate), + ), + }, + }, + }) +} + +func TestAccComputeInstanceTemplate_disks(t *testing.T) { + var instanceTemplate compute.InstanceTemplate + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeInstanceTemplateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeInstanceTemplate_disks, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeInstanceTemplateExists( + "google_compute_instance_template.foobar", &instanceTemplate), + testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "debian-7-wheezy-v20140814", true, true), + testAccCheckComputeInstanceTemplateDisk(&instanceTemplate, "foo_existing_disk", false, false), + ), + }, + }, + }) +} + +func testAccCheckComputeInstanceTemplateDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_instance_template" { + continue + } + + _, err := config.clientCompute.InstanceTemplates.Get( + config.Project, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("Instance template still exists") + } + } + + return nil +} + +func testAccCheckComputeInstanceTemplateExists(n string, instanceTemplate *compute.InstanceTemplate) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + config := testAccProvider.Meta().(*Config) + + found, err := config.clientCompute.InstanceTemplates.Get( + config.Project, rs.Primary.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Instance template not found") + } + + *instanceTemplate = *found + + return nil + } +} + +func testAccCheckComputeInstanceTemplateMetadata( + instanceTemplate *compute.InstanceTemplate, + k string, v string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instanceTemplate.Properties.Metadata == nil { + return fmt.Errorf("no metadata") + } + + for _, item := range instanceTemplate.Properties.Metadata.Items { + if k != item.Key { + continue + } + + if v == item.Value { + return nil + } + + return fmt.Errorf("bad value for %s: %s", k, item.Value) + } + + return fmt.Errorf("metadata not found: %s", k) + } +} + +func testAccCheckComputeInstanceTemplateNetwork(instanceTemplate *compute.InstanceTemplate) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, i := range instanceTemplate.Properties.NetworkInterfaces { + for _, c := range i.AccessConfigs { + if c.NatIP == "" { + return fmt.Errorf("no NAT IP") + } + } + } + + return nil + } +} + +func testAccCheckComputeInstanceTemplateDisk(instanceTemplate *compute.InstanceTemplate, source string, delete bool, boot bool) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instanceTemplate.Properties.Disks == nil { + return fmt.Errorf("no disks") + } + + for _, disk := range instanceTemplate.Properties.Disks { + if disk.InitializeParams == nil { + // Check disk source + if disk.Source == source { + if disk.AutoDelete == delete && disk.Boot == boot { + return nil + } + } + } else { + // Check source image + if disk.InitializeParams.SourceImage == source { + if disk.AutoDelete == delete && disk.Boot == boot { + return nil + } + } + } + } + + return fmt.Errorf("Disk not found: %s", source) + } +} + +func testAccCheckComputeInstanceTemplateTag(instanceTemplate *compute.InstanceTemplate, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + if instanceTemplate.Properties.Tags == nil { + return fmt.Errorf("no tags") + } + + for _, k := range instanceTemplate.Properties.Tags.Items { + if k == n { + return nil + } + } + + return fmt.Errorf("tag not found: %s", n) + } +} + +const testAccComputeInstanceTemplate_basic = ` +resource "google_compute_instance_template" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + can_ip_forward = false + tags = ["foo", "bar"] + + disk { + source_image = "debian-7-wheezy-v20140814" + auto_delete = true + boot = true + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +}` + +const testAccComputeInstanceTemplate_ip = ` +resource "google_compute_address" "foo" { + name = "foo" +} + +resource "google_compute_instance_template" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + tags = ["foo", "bar"] + + disk { + source_image = "debian-7-wheezy-v20140814" + } + + network { + source = "default" + address = "${google_compute_address.foo.address}" + } + + metadata { + foo = "bar" + } +}` + +const testAccComputeInstanceTemplate_disks = ` +resource "google_compute_instance_template" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + + disk { + source_image = "debian-7-wheezy-v20140814" + auto_delete = true + boot = true + } + + disk { + source = "foo_existing_disk" + auto_delete = false + boot = false + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } +}` From 7df536345c9bf3b49cffa1c6e3cd23b65ce5d7e3 Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 10:41:21 +0000 Subject: [PATCH 05/13] Add docs for google Instance Template. --- .../r/compute_instance_template.html.markdown | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 website/source/docs/providers/google/r/compute_instance_template.html.markdown diff --git a/website/source/docs/providers/google/r/compute_instance_template.html.markdown b/website/source/docs/providers/google/r/compute_instance_template.html.markdown new file mode 100644 index 000000000..bb1f8bd15 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_instance_template.html.markdown @@ -0,0 +1,149 @@ +--- +layout: "google" +page_title: "Google: google_compute_instance_template" +sidebar_current: "docs-google-resource-instance_template" +description: |- + Manages a VM instance template resource within GCE. +--- + + +# google\_compute\_instance\_template + +Manages a VM instance template resource within GCE. For more information see +[the official documentation](https://cloud.google.com/compute/docs/instance-templates) +and +[API](https://cloud.google.com/compute/docs/reference/latest/instanceTemplates). + + +## Example Usage + +``` +resource "google_compute_instance_template" "foobar" { + name = "terraform-test" + description = "template description" + instance_description = "description assigned to instances" + machine_type = "n1-standard-1" + can_ip_forward = false + automatic_restart = true + on_host_maintenance = "MIGRATE" + tags = ["foo", "bar"] + + # Create a new boot disk from an image + disk { + source_image = "debian-7-wheezy-v20140814" + auto_delete = true + boot = true + } + + # Use an existing disk resource + disk { + source = "foo_existing_disk" + auto_delete = false + boot = false + } + + network { + source = "default" + } + + metadata { + foo = "bar" + } + + service_account { + scopes = ["userinfo-email", "compute-ro", "storage-ro"] + } +} +``` + +## Argument Reference + +Note that changing any field for this resource forces a new resource to be created. + +The following arguments are supported: + +* `name` - (Required) A unique name for the resource, required by GCE. + +* `description` - (Optional) A brief description of this resource. + +* `can_ip_forward` - (Optional) Whether to allow sending and receiving of + packets with non-matching source or destination IPs. + This defaults to false. + +* `instance_description` - (Optional) A brief description to use for instances + created from this template. + +* `machine_type` - (Required) The machine type to create. + +* `disk` - (Required) Disks to attach to instances created from this + template. This can be specified multiple times for multiple disks. + Structure is documented below. + +* `metadata` - (Optional) Metadata key/value pairs to make available from + within instances created from this template. + +* `network` - (Required) Networks to attach to instances created from this template. + This can be specified multiple times for multiple networks. Structure is + documented below. + +* `automatic_restart` - (Optional) Specifies whether the instance should be + automatically restarted if it is terminated by Compute Engine (not + terminated by a user). + This defaults to true. + +* `on_host_maintenance` - (Optional) Defines the maintenance behavior for this + instance. + +* `service_account` - (Optional) Service account to attach to the instance. + +* `tags` - (Optional) Tags to attach to the instance. + + + +The `disk` block supports: + +* `auto_delete` - (Optional) Whether or not the disk should be auto-deleted. + This defaults to true. + +* `boot` - (Optional) Indicates that this is a boot disk. + +* `device_name` - (Optional) A unique device name that is reflected into + the /dev/ tree of a Linux operating system running within the instance. + If not specified, the server chooses a default device name to apply to + this disk. + +* `disk_name` - (Optional) Name of the disk. When not provided, this defaults + to the name of the instance. + +* `source_image` - (Required if source not set) The name of the image to base + this disk off of. + +* `interface` - (Optional) Specifies the disk interface to use for attaching + this disk. + +* `mode` - (Optional) The mode in which to attach this disk, either READ_WRITE + or READ_ONLY. If you are attaching or creating a boot disk, this must + read-write mode. + +* `source` - (Required if source_image not set) The name of the disk (such as + those managed by `google_compute_disk`) to attach. + +* `type` - (Optional) The GCE disk type. + +The `network` block supports: + +* `source` - (Required) The name of the network to attach this interface to. + +* `address` - (Optional) The IP address of a reserved IP address to assign + to this interface. + +The `service_account` block supports: + +* `scopes` - (Required) A list of service scopes. Both OAuth2 URLs and gcloud + short names are supported. + +## Attributes Reference + +The following attributes are exported: + +* `self_link` - The URL of the created resource. From 006b98cdaea640a5cc216362dfcb3a4ad11f5850 Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 10:49:20 +0000 Subject: [PATCH 06/13] Add optional to disk_name field. --- builtin/providers/google/resource_compute_instance_template.go | 1 + 1 file changed, 1 insertion(+) diff --git a/builtin/providers/google/resource_compute_instance_template.go b/builtin/providers/google/resource_compute_instance_template.go index 25907dd27..5ff275dd0 100644 --- a/builtin/providers/google/resource_compute_instance_template.go +++ b/builtin/providers/google/resource_compute_instance_template.go @@ -77,6 +77,7 @@ func resourceComputeInstanceTemplate() *schema.Resource { "disk_name": &schema.Schema{ Type: schema.TypeString, + Optional: true, ForceNew: true, }, From 2b2617790330fd636ea61f0be02ba3b53e1a30ca Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 11:13:55 +0000 Subject: [PATCH 07/13] Add SelfLink field to GCE disk resource. --- builtin/providers/google/resource_compute_disk.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/builtin/providers/google/resource_compute_disk.go b/builtin/providers/google/resource_compute_disk.go index 378b0171e..9f8557cf0 100644 --- a/builtin/providers/google/resource_compute_disk.go +++ b/builtin/providers/google/resource_compute_disk.go @@ -46,6 +46,11 @@ func resourceComputeDisk() *schema.Resource { Optional: true, ForceNew: true, }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -132,7 +137,7 @@ func resourceComputeDiskCreate(d *schema.ResourceData, meta interface{}) error { func resourceComputeDiskRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - _, err := config.clientCompute.Disks.Get( + disk, err := config.clientCompute.Disks.Get( config.Project, d.Get("zone").(string), d.Id()).Do() if err != nil { if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { @@ -145,6 +150,8 @@ func resourceComputeDiskRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error reading disk: %s", err) } + d.Set("self_link", disk.SelfLink) + return nil } From 4e1a421652f7004b3c29d75aeea7a9c7f9b8994d Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 11:14:15 +0000 Subject: [PATCH 08/13] Add SelfLink field to GCE firewall resource. --- builtin/providers/google/resource_compute_firewall.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/builtin/providers/google/resource_compute_firewall.go b/builtin/providers/google/resource_compute_firewall.go index 9cbe5b53b..09d9ca250 100644 --- a/builtin/providers/google/resource_compute_firewall.go +++ b/builtin/providers/google/resource_compute_firewall.go @@ -86,6 +86,11 @@ func resourceComputeFirewall() *schema.Resource { return hashcode.String(v.(string)) }, }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -159,7 +164,7 @@ func resourceComputeFirewallCreate(d *schema.ResourceData, meta interface{}) err func resourceComputeFirewallRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - _, err := config.clientCompute.Firewalls.Get( + firewall, err := config.clientCompute.Firewalls.Get( config.Project, d.Id()).Do() if err != nil { if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { @@ -172,6 +177,8 @@ func resourceComputeFirewallRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("Error reading firewall: %s", err) } + d.Set("self_link", firewall.SelfLink) + return nil } From 91b000fd31102f2135078e9ba18c67ae22158765 Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 11:14:37 +0000 Subject: [PATCH 09/13] Add SelfLink field to GCE network resource. --- builtin/providers/google/resource_compute_network.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/builtin/providers/google/resource_compute_network.go b/builtin/providers/google/resource_compute_network.go index b79ac2ade..4254da721 100644 --- a/builtin/providers/google/resource_compute_network.go +++ b/builtin/providers/google/resource_compute_network.go @@ -33,6 +33,11 @@ func resourceComputeNetwork() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -98,6 +103,7 @@ func resourceComputeNetworkRead(d *schema.ResourceData, meta interface{}) error } d.Set("gateway_ipv4", network.GatewayIPv4) + d.Set("self_link", network.SelfLink) return nil } From a3ca34ac88438b138711621d7d0e481d542634fb Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 11:15:07 +0000 Subject: [PATCH 10/13] Add SelfLink field to GCE route resource. --- builtin/providers/google/resource_compute_route.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/builtin/providers/google/resource_compute_route.go b/builtin/providers/google/resource_compute_route.go index 0c15dbaa4..02aa72652 100644 --- a/builtin/providers/google/resource_compute_route.go +++ b/builtin/providers/google/resource_compute_route.go @@ -80,6 +80,11 @@ func resourceComputeRoute() *schema.Resource { return hashcode.String(v.(string)) }, }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -183,7 +188,7 @@ func resourceComputeRouteCreate(d *schema.ResourceData, meta interface{}) error func resourceComputeRouteRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - _, err := config.clientCompute.Routes.Get( + route, err := config.clientCompute.Routes.Get( config.Project, d.Id()).Do() if err != nil { if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { @@ -196,6 +201,8 @@ func resourceComputeRouteRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("Error reading route: %#v", err) } + d.Set("self_link", route.SelfLink) + return nil } From 418bf2a692a1ab9a9d6d141848762ad5f6ebe3dc Mon Sep 17 00:00:00 2001 From: David Watson Date: Tue, 10 Feb 2015 14:31:43 +0000 Subject: [PATCH 11/13] Remove leftover todo comment. --- builtin/providers/google/resource_compute_instance_template.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/builtin/providers/google/resource_compute_instance_template.go b/builtin/providers/google/resource_compute_instance_template.go index 5ff275dd0..074e45695 100644 --- a/builtin/providers/google/resource_compute_instance_template.go +++ b/builtin/providers/google/resource_compute_instance_template.go @@ -16,8 +16,6 @@ func resourceComputeInstanceTemplate() *schema.Resource { Read: resourceComputeInstanceTemplateRead, Delete: resourceComputeInstanceTemplateDelete, - // TODO: check which items are optional and set optional: true - Schema: map[string]*schema.Schema{ "name": &schema.Schema{ Type: schema.TypeString, From 81b5c238ef4b01141497263dbe5d7db5d0fa895b Mon Sep 17 00:00:00 2001 From: Phil Frost Date: Tue, 10 Feb 2015 11:10:03 -0500 Subject: [PATCH 12/13] Clarify usage of `aws_elb` Address confusion regarding ELB in VPC or EC2-classic. See #958. --- website/source/docs/providers/aws/r/elb.html.markdown | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/website/source/docs/providers/aws/r/elb.html.markdown b/website/source/docs/providers/aws/r/elb.html.markdown index f851a5552..31e1eb448 100644 --- a/website/source/docs/providers/aws/r/elb.html.markdown +++ b/website/source/docs/providers/aws/r/elb.html.markdown @@ -51,15 +51,18 @@ resource "aws_elb" "bar" { The following arguments are supported: * `name` - (Required) The name of the ELB -* `availability_zones` - (Optional) The AZ's to serve traffic in. +* `availability_zones` - (Required for an EC2-classic ELB) The AZ's to serve traffic in. * `security_groups` - (Optional) A list of security group IDs to assign to the ELB. -* `subnets` - (Optional) A list of subnets to attach to the ELB. +* `subnets` - (Required for a VPC ELB) A list of subnet IDs to attach to the ELB. * `instances` - (Optional) A list of instance ids to place in the ELB pool. * `internal` - (Optional) If true, ELB will be an internal ELB. * `listener` - (Required) A list of listener blocks. Listeners documented below. * `health_check` - (Optional) A health_check block. Health Check documented below. * `cross_zone_load_balancing` - (Optional) Enable cross-zone load balancing. +Exactly one of `availability_zones` or `subnets` must be specified: this +determines if the ELB exists in a VPC or in EC2-classic. + Listeners support the following: * `instance_port` - (Required) The port on the instance to route to From f8e6edb8c33da8fc1545305fe6adbee1656149ad Mon Sep 17 00:00:00 2001 From: Clint Shryock Date: Tue, 10 Feb 2015 14:41:39 -0800 Subject: [PATCH 13/13] update README on running acceptance tests. Mostly formatting/typo fix --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 616065b43..bda9d7d72 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,13 @@ If you're working on a feature of a provider and want to verify it is functionin To run the acceptance tests, invoke `make testacc`: ```sh -$ make testacc TEST=./builtin/providers/aws TESTARGS='-run=VPC' +$ make testacc TEST=./builtin/providers/aws TESTARGS='-run=Vpc' +go generate ./... +TF_ACC=1 go test ./builtin/providers/aws -v -run=Vpc -timeout 45m +=== RUN TestAccVpc_basic +2015/02/10 14:11:17 [INFO] Test: Using us-west-2 as test region +[...] +[...] ... ```