Merge pull request #959 from BashtonLtd/gce_instance_template

GCE instance template support
This commit is contained in:
Paul Hinze 2015-02-10 08:57:45 -06:00
commit 319be5d465
3 changed files with 750 additions and 0 deletions

View File

@ -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(),

View File

@ -0,0 +1,471 @@
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,
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,
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,
},
"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
}

View File

@ -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"
}
}`