diff --git a/builtin/providers/google/operation.go b/builtin/providers/google/operation.go index fb79703c6..0971e3f5b 100644 --- a/builtin/providers/google/operation.go +++ b/builtin/providers/google/operation.go @@ -3,6 +3,7 @@ package google import ( "bytes" "fmt" + "log" "github.com/hashicorp/terraform/helper/resource" "google.golang.org/api/compute/v1" @@ -52,6 +53,8 @@ func (w *OperationWaiter) RefreshFunc() resource.StateRefreshFunc { return nil, "", err } + log.Printf("[DEBUG] Got %q when asking for operation %q", op.Status, w.Op.Name) + return op, op.Status, nil } } diff --git a/builtin/providers/google/provider.go b/builtin/providers/google/provider.go index d7e293303..a7438995f 100644 --- a/builtin/providers/google/provider.go +++ b/builtin/providers/google/provider.go @@ -36,6 +36,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "google_compute_autoscaler": resourceComputeAutoscaler(), "google_compute_address": resourceComputeAddress(), + "google_compute_backend_service": resourceComputeBackendService(), "google_compute_disk": resourceComputeDisk(), "google_compute_firewall": resourceComputeFirewall(), "google_compute_forwarding_rule": resourceComputeForwardingRule(), diff --git a/builtin/providers/google/resource_compute_backend_service.go b/builtin/providers/google/resource_compute_backend_service.go new file mode 100644 index 000000000..a8826f8e4 --- /dev/null +++ b/builtin/providers/google/resource_compute_backend_service.go @@ -0,0 +1,408 @@ +package google + +import ( + "bytes" + "fmt" + "log" + "regexp" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" +) + +func resourceComputeBackendService() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeBackendServiceCreate, + Read: resourceComputeBackendServiceRead, + Update: resourceComputeBackendServiceUpdate, + Delete: resourceComputeBackendServiceDelete, + + Schema: map[string]*schema.Schema{ + "backend": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "balancing_mode": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "UTILIZATION", + }, + "capacity_scaler": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Default: 1, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "max_rate": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "max_rate_per_instance": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + }, + "max_utilization": &schema.Schema{ + Type: schema.TypeFloat, + Optional: true, + Default: 0.8, + }, + }, + }, + Optional: true, + Set: resourceGoogleComputeBackendServiceBackendHash, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "health_checks": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Set: schema.HashString, + }, + + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + re := `^(?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)$` + if !regexp.MustCompile(re).MatchString(value) { + errors = append(errors, fmt.Errorf( + "%q (%q) doesn't match regexp %q", k, value, re)) + } + return + }, + }, + + "port_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + + "fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceComputeBackendServiceCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + hc := d.Get("health_checks").(*schema.Set).List() + healthChecks := make([]string, 0, len(hc)) + for _, v := range hc { + healthChecks = append(healthChecks, v.(string)) + } + + service := compute.BackendService{ + Name: d.Get("name").(string), + HealthChecks: healthChecks, + } + + if v, ok := d.GetOk("backend"); ok { + service.Backends = expandBackends(v.(*schema.Set).List()) + } + + if v, ok := d.GetOk("description"); ok { + service.Description = v.(string) + } + + if v, ok := d.GetOk("port_name"); ok { + service.PortName = v.(string) + } + + if v, ok := d.GetOk("protocol"); ok { + service.Protocol = v.(string) + } + + if v, ok := d.GetOk("timeout_sec"); ok { + service.TimeoutSec = int64(v.(int)) + } + + log.Printf("[DEBUG] Creating new Backend Service: %#v", service) + op, err := config.clientCompute.BackendServices.Insert( + config.Project, &service).Do() + if err != nil { + return fmt.Errorf("Error creating backend service: %s", err) + } + + log.Printf("[DEBUG] Waiting for new backend service, operation: %#v", op) + + d.SetId(service.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Region: config.Region, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for backend service 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 resourceComputeBackendServiceRead(d, meta) +} + +func resourceComputeBackendServiceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + service, err := config.clientCompute.BackendServices.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 service: %s", err) + } + + d.Set("description", service.Description) + d.Set("port_name", service.PortName) + d.Set("protocol", service.Protocol) + d.Set("timeout_sec", service.TimeoutSec) + d.Set("fingerprint", service.Fingerprint) + d.Set("self_link", service.SelfLink) + + d.Set("backend", flattenBackends(service.Backends)) + d.Set("health_checks", service.HealthChecks) + + return nil +} + +func resourceComputeBackendServiceUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + hc := d.Get("health_checks").(*schema.Set).List() + healthChecks := make([]string, 0, len(hc)) + for _, v := range hc { + healthChecks = append(healthChecks, v.(string)) + } + + service := compute.BackendService{ + Name: d.Get("name").(string), + Fingerprint: d.Get("fingerprint").(string), + HealthChecks: healthChecks, + } + + if d.HasChange("backend") { + service.Backends = expandBackends(d.Get("backend").(*schema.Set).List()) + } + if d.HasChange("description") { + service.Description = d.Get("description").(string) + } + if d.HasChange("port_name") { + service.PortName = d.Get("port_name").(string) + } + if d.HasChange("protocol") { + service.Protocol = d.Get("protocol").(string) + } + if d.HasChange("timeout_sec") { + service.TimeoutSec = int64(d.Get("timeout_sec").(int)) + } + + log.Printf("[DEBUG] Updating existing Backend Service %q: %#v", d.Id(), service) + op, err := config.clientCompute.BackendServices.Update( + config.Project, d.Id(), &service).Do() + if err != nil { + return fmt.Errorf("Error updating backend service: %s", err) + } + + d.SetId(service.Name) + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Region: config.Region, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for backend service to update: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + return resourceComputeBackendServiceRead(d, meta) +} + +func resourceComputeBackendServiceDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + log.Printf("[DEBUG] Deleting backend service %s", d.Id()) + op, err := config.clientCompute.BackendServices.Delete( + config.Project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting backend service: %s", err) + } + + // Wait for the operation to complete + w := &OperationWaiter{ + Service: config.clientCompute, + Op: op, + Project: config.Project, + Region: config.Region, + Type: OperationWaitGlobal, + } + state := w.Conf() + state.Timeout = 2 * time.Minute + state.MinTimeout = 1 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for backend service to delete: %s", err) + } + op = opRaw.(*compute.Operation) + if op.Error != nil { + // Return the error + return OperationError(*op.Error) + } + + d.SetId("") + return nil +} + +func expandBackends(configured []interface{}) []*compute.Backend { + backends := make([]*compute.Backend, 0, len(configured)) + + for _, raw := range configured { + data := raw.(map[string]interface{}) + + b := compute.Backend{ + Group: data["group"].(string), + } + + if v, ok := data["balancing_mode"]; ok { + b.BalancingMode = v.(string) + } + if v, ok := data["capacity_scaler"]; ok { + b.CapacityScaler = v.(float64) + } + if v, ok := data["description"]; ok { + b.Description = v.(string) + } + if v, ok := data["max_rate"]; ok { + b.MaxRate = int64(v.(int)) + } + if v, ok := data["max_rate_per_instance"]; ok { + b.MaxRatePerInstance = v.(float64) + } + if v, ok := data["max_rate_per_instance"]; ok { + b.MaxUtilization = v.(float64) + } + + backends = append(backends, &b) + } + + return backends +} + +func flattenBackends(backends []*compute.Backend) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(backends)) + + for _, b := range backends { + data := make(map[string]interface{}) + + data["balancing_mode"] = b.BalancingMode + data["capacity_scaler"] = b.CapacityScaler + data["description"] = b.Description + data["group"] = b.Group + data["max_rate"] = b.MaxRate + data["max_rate_per_instance"] = b.MaxRatePerInstance + data["max_utilization"] = b.MaxUtilization + + result = append(result, data) + } + + return result +} + +func resourceGoogleComputeBackendServiceBackendHash(v interface{}) int { + if v == nil { + return 0 + } + + var buf bytes.Buffer + m := v.(map[string]interface{}) + + buf.WriteString(fmt.Sprintf("%s-", m["group"].(string))) + + if v, ok := m["balancing_mode"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + if v, ok := m["capacity_scaler"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v.(float64))) + } + if v, ok := m["description"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + if v, ok := m["max_rate"]; ok { + buf.WriteString(fmt.Sprintf("%d-", int64(v.(int)))) + } + if v, ok := m["max_rate_per_instance"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v.(float64))) + } + if v, ok := m["max_rate_per_instance"]; ok { + buf.WriteString(fmt.Sprintf("%f-", v.(float64))) + } + + return hashcode.String(buf.String()) +} diff --git a/builtin/providers/google/resource_compute_backend_service_test.go b/builtin/providers/google/resource_compute_backend_service_test.go new file mode 100644 index 000000000..70b420ba4 --- /dev/null +++ b/builtin/providers/google/resource_compute_backend_service_test.go @@ -0,0 +1,193 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/compute/v1" +) + +func TestAccComputeBackendService_basic(t *testing.T) { + var svc compute.BackendService + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeBackendServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeBackendService_basic, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeBackendServiceExists( + "google_compute_backend_service.foobar", &svc), + ), + }, + resource.TestStep{ + Config: testAccComputeBackendService_basicModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeBackendServiceExists( + "google_compute_backend_service.foobar", &svc), + ), + }, + }, + }) +} + +func TestAccComputeBackendService_withBackend(t *testing.T) { + var svc compute.BackendService + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeBackendServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeBackendService_withBackend, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeBackendServiceExists( + "google_compute_backend_service.lipsum", &svc), + ), + }, + }, + }) + + if svc.TimeoutSec != 10 { + t.Errorf("Expected TimeoutSec == 10, got %d", svc.TimeoutSec) + } + if svc.Protocol != "HTTP" { + t.Errorf("Expected Protocol to be HTTP, got %q", svc.Protocol) + } + if len(svc.Backends) != 1 { + t.Errorf("Expected 1 backend, got %d", len(svc.Backends)) + } +} + +func testAccCheckComputeBackendServiceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_backend_service" { + continue + } + + _, err := config.clientCompute.BackendServices.Get( + config.Project, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("Backend service still exists") + } + } + + return nil +} + +func testAccCheckComputeBackendServiceExists(n string, svc *compute.BackendService) 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.BackendServices.Get( + config.Project, rs.Primary.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("Backend service not found") + } + + *svc = *found + + return nil + } +} + +const testAccComputeBackendService_basic = ` +resource "google_compute_backend_service" "foobar" { + name = "blablah" + health_checks = ["${google_compute_http_health_check.zero.self_link}"] +} + +resource "google_compute_http_health_check" "zero" { + name = "tf-test-zero" + request_path = "/" + check_interval_sec = 1 + timeout_sec = 1 +} +` + +const testAccComputeBackendService_basicModified = ` +resource "google_compute_backend_service" "foobar" { + name = "blablah" + health_checks = ["${google_compute_http_health_check.one.self_link}"] +} + +resource "google_compute_http_health_check" "zero" { + name = "tf-test-zero" + request_path = "/" + check_interval_sec = 1 + timeout_sec = 1 +} + +resource "google_compute_http_health_check" "one" { + name = "tf-test-one" + request_path = "/one" + check_interval_sec = 30 + timeout_sec = 30 +} +` + +const testAccComputeBackendService_withBackend = ` +resource "google_compute_backend_service" "lipsum" { + name = "hello-world-bs" + description = "Hello World 1234" + port_name = "http" + protocol = "HTTP" + timeout_sec = 10 + + backend { + group = "${google_compute_instance_group_manager.foobar.instance_group}" + } + + health_checks = ["${google_compute_http_health_check.default.self_link}"] +} + +resource "google_compute_instance_group_manager" "foobar" { + name = "terraform-test" + instance_template = "${google_compute_instance_template.foobar.self_link}" + base_instance_name = "foobar" + zone = "us-central1-f" + target_size = 1 +} + +resource "google_compute_instance_template" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + + network_interface { + network = "default" + } + + disk { + source_image = "debian-7-wheezy-v20140814" + auto_delete = true + boot = true + } +} + +resource "google_compute_http_health_check" "default" { + name = "test2" + request_path = "/" + check_interval_sec = 1 + timeout_sec = 1 +} +` diff --git a/website/source/docs/providers/google/r/compute_backend_service.html.markdown b/website/source/docs/providers/google/r/compute_backend_service.html.markdown new file mode 100644 index 000000000..c9d9396c5 --- /dev/null +++ b/website/source/docs/providers/google/r/compute_backend_service.html.markdown @@ -0,0 +1,92 @@ +--- +layout: "google" +page_title: "Google: google_compute_backend_service" +sidebar_current: "docs-google-resource-backend-service" +description: |- + Creates a Backend Service resource for Google Compute Engine. +--- + +# google\_compute\_backend\_service + +A Backend Service defines a group of virtual machines that will serve traffic for load balancing. + +## Example Usage + +``` +resource "google_compute_backend_service" "foobar" { + name = "blablah" + description = "Hello World 1234" + port_name = "http" + protocol = "HTTP" + timeout_sec = 10 + + backend { + group = "${google_compute_instance_group_manager.foo.instance_group}" + } + + health_checks = ["${google_compute_http_health_check.default.self_link}"] +} + +resource "google_compute_instance_group_manager" "foo" { + name = "terraform-test" + instance_template = "${google_compute_instance_template.foobar.self_link}" + base_instance_name = "foobar" + zone = "us-central1-f" + target_size = 1 +} + +resource "google_compute_instance_template" "foobar" { + name = "terraform-test" + machine_type = "n1-standard-1" + + network_interface { + network = "default" + } + + disk { + source_image = "debian-7-wheezy-v20140814" + auto_delete = true + boot = true + } +} + +resource "google_compute_http_health_check" "default" { + name = "test" + request_path = "/" + check_interval_sec = 1 + timeout_sec = 1 +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the backend service. +* `health_checks` - (Required) Specifies a list of HTTP health check objects + for checking the health of the backend service. +* `description` - (Optional) The textual description for the backend service. +* `backend` - (Optional) The list of backends that serve this BackendService. See *Backend* below. +* `port_name` - (Optional) The name of a service that has been added to + an instance group in this backend. See [related docs](https://cloud.google.com/compute/docs/instance-groups/#specifying_service_endpoints) + for details. Defaults to http. +* `protocol` - (Optional) The protocol for incoming requests. Defaults to `HTTP`. +* `timeout_sec` - (Optional) The number of secs to wait for a backend to respond + to a request before considering the request failed. Defaults to `30`. + +**Backend** supports the following attributes: + +* `group` - (Required) The name or URI of a Compute Engine instance group (`google_compute_instance_group_manager.xyz.instance_group`) that can receive traffic. +* `balancing_mode` - (Optional) Defines the strategy for balancing load. Defaults to `UTILIZATION` +* `capacity_scaler` - (Optional) A float in the range [0, 1.0] that scales the maximum parameters for the group (e.g., max rate). A value of 0.0 will cause no requests to be sent to the group (i.e., it adds the group in a drained state). The default is 1.0. +* `description` - (Optional) Textual description for the backend. +* `max_rate` - (Optional) Maximum requests per second (RPS) that the group can handle. +* `max_rate_per_instance` - (Optional) The maximum per-instance requests per second (RPS). +* `max_utilization` - (Optional) The target CPU utilization for the group as a float in the range [0.0, 1.0]. This flag can only be provided when the balancing mode is `UTILIZATION`. Defaults to `0.8`. + +## Attributes Reference + +The following attributes are exported: + +* `name` - The name of the resource. +* `self_link` - The URI of the created resource. diff --git a/website/source/layouts/google.erb b/website/source/layouts/google.erb index b14669168..8d1c0fb2c 100644 --- a/website/source/layouts/google.erb +++ b/website/source/layouts/google.erb @@ -21,6 +21,10 @@ google_compute_autoscaler + > + google_compute_backend_service + + > google_compute_disk