diff --git a/provider.go b/provider.go index e126d756..ce8ef552 100644 --- a/provider.go +++ b/provider.go @@ -69,6 +69,7 @@ func Provider() terraform.ResourceProvider { "google_compute_forwarding_rule": resourceComputeForwardingRule(), "google_compute_global_address": resourceComputeGlobalAddress(), "google_compute_global_forwarding_rule": resourceComputeGlobalForwardingRule(), + "google_compute_health_check": resourceComputeHealthCheck(), "google_compute_http_health_check": resourceComputeHttpHealthCheck(), "google_compute_https_health_check": resourceComputeHttpsHealthCheck(), "google_compute_image": resourceComputeImage(), @@ -78,6 +79,7 @@ func Provider() terraform.ResourceProvider { "google_compute_instance_template": resourceComputeInstanceTemplate(), "google_compute_network": resourceComputeNetwork(), "google_compute_project_metadata": resourceComputeProjectMetadata(), + "google_compute_region_backend_service": resourceComputeRegionBackendService(), "google_compute_route": resourceComputeRoute(), "google_compute_ssl_certificate": resourceComputeSslCertificate(), "google_compute_subnetwork": resourceComputeSubnetwork(), diff --git a/resource_compute_forwarding_rule.go b/resource_compute_forwarding_rule.go index 194845aa..5db03811 100644 --- a/resource_compute_forwarding_rule.go +++ b/resource_compute_forwarding_rule.go @@ -28,10 +28,16 @@ func resourceComputeForwardingRule() *schema.Resource { "target": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, ForceNew: false, }, + "backend_service": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "description": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -52,12 +58,33 @@ func resourceComputeForwardingRule() *schema.Resource { Computed: true, }, + "load_balancing_scheme": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "EXTERNAL", + }, + + "network": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + "port_range": &schema.Schema{ Type: schema.TypeString, Optional: true, ForceNew: true, }, + "ports": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Set: schema.HashString, + }, + "project": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -76,6 +103,13 @@ func resourceComputeForwardingRule() *schema.Resource { Type: schema.TypeString, Computed: true, }, + + "subnetwork": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, }, } } @@ -93,13 +127,24 @@ func resourceComputeForwardingRuleCreate(d *schema.ResourceData, meta interface{ return err } + ps := d.Get("ports").(*schema.Set).List() + ports := make([]string, 0, len(ps)) + for _, v := range ps { + ports = append(ports, v.(string)) + } + frule := &compute.ForwardingRule{ - IPAddress: d.Get("ip_address").(string), - IPProtocol: d.Get("ip_protocol").(string), - Description: d.Get("description").(string), - Name: d.Get("name").(string), - PortRange: d.Get("port_range").(string), - Target: d.Get("target").(string), + BackendService: d.Get("backend_service").(string), + IPAddress: d.Get("ip_address").(string), + IPProtocol: d.Get("ip_protocol").(string), + Description: d.Get("description").(string), + LoadBalancingScheme: d.Get("load_balancing_scheme").(string), + Name: d.Get("name").(string), + Network: d.Get("network").(string), + PortRange: d.Get("port_range").(string), + Ports: ports, + Subnetwork: d.Get("subnetwork").(string), + Target: d.Get("target").(string), } log.Printf("[DEBUG] ForwardingRule insert request: %#v", frule) @@ -186,10 +231,15 @@ func resourceComputeForwardingRuleRead(d *schema.ResourceData, meta interface{}) d.Set("name", frule.Name) d.Set("target", frule.Target) + d.Set("backend_service", frule.BackendService) d.Set("description", frule.Description) + d.Set("load_balancing_scheme", frule.LoadBalancingScheme) + d.Set("network", frule.Network) d.Set("port_range", frule.PortRange) + d.Set("ports", frule.Ports) d.Set("project", project) d.Set("region", region) + d.Set("subnetwork", frule.Subnetwork) d.Set("ip_address", frule.IPAddress) d.Set("ip_protocol", frule.IPProtocol) d.Set("self_link", frule.SelfLink) diff --git a/resource_compute_forwarding_rule_test.go b/resource_compute_forwarding_rule_test.go index 08e9fa51..2ae4a100 100644 --- a/resource_compute_forwarding_rule_test.go +++ b/resource_compute_forwarding_rule_test.go @@ -50,6 +50,27 @@ func TestAccComputeForwardingRule_ip(t *testing.T) { }) } +func TestAccComputeForwardingRule_internalLoadBalancing(t *testing.T) { + serviceName := fmt.Sprintf("tf-%s", acctest.RandString(10)) + checkName := fmt.Sprintf("tf-%s", acctest.RandString(10)) + ruleName := fmt.Sprintf("tf-%s", acctest.RandString(10)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeForwardingRuleDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeForwardingRule_internalLoadBalancing(serviceName, checkName, ruleName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeForwardingRuleExists( + "google_compute_forwarding_rule.foobar"), + ), + }, + }, + }) +} + func testAccCheckComputeForwardingRuleDestroy(s *terraform.State) error { config := testAccProvider.Meta().(*Config) @@ -132,3 +153,31 @@ resource "google_compute_forwarding_rule" "foobar" { } `, addrName, poolName, ruleName) } + +func testAccComputeForwardingRule_internalLoadBalancing(serviceName, checkName, ruleName string) string { + return fmt.Sprintf(` +resource "google_compute_region_backend_service" "foobar-bs" { + name = "%s" + description = "Resource created for Terraform acceptance testing" + health_checks = ["${google_compute_health_check.zero.self_link}"] + region = "us-central1" +} +resource "google_compute_health_check" "zero" { + name = "%s" + description = "Resource created for Terraform acceptance testing" + check_interval_sec = 1 + timeout_sec = 1 + + tcp_health_check { + port = "80" + } +} +resource "google_compute_forwarding_rule" "foobar" { + description = "Resource created for Terraform acceptance testing" + name = "%s" + load_balancing_scheme = "INTERNAL" + backend_service = "${google_compute_region_backend_service.foobar-bs.self_link}" + ports = ["80"] +} +`, serviceName, checkName, ruleName) +} diff --git a/resource_compute_health_check.go b/resource_compute_health_check.go new file mode 100644 index 00000000..de8d7d42 --- /dev/null +++ b/resource_compute_health_check.go @@ -0,0 +1,494 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" +) + +func resourceComputeHealthCheck() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeHealthCheckCreate, + Read: resourceComputeHealthCheckRead, + Delete: resourceComputeHealthCheckDelete, + Update: resourceComputeHealthCheckUpdate, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "check_interval_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 5, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "healthy_threshold": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 2, + }, + + "tcp_health_check": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"ssl_health_check", "http_health_check", "https_health_check"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 80, + }, + "proxy_header": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "NONE", + }, + "request": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "response": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "ssl_health_check": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"tcp_health_check", "http_health_check", "https_health_check"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 443, + }, + "proxy_header": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "NONE", + }, + "request": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "response": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + + "http_health_check": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"tcp_health_check", "ssl_health_check", "https_health_check"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: 80, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "proxy_header": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "NONE", + }, + "request_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + }, + }, + }, + }, + + "https_health_check": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ConflictsWith: []string{"tcp_health_check", "ssl_health_check", "http_health_check"}, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "host": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: 443, + }, + "port": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + }, + "proxy_header": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "NONE", + }, + "request_path": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "/", + }, + }, + }, + }, + + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 5, + }, + + "unhealthy_threshold": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Default: 2, + }, + }, + } +} + +func resourceComputeHealthCheckCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + // Build the parameter + hchk := &compute.HealthCheck{ + Name: d.Get("name").(string), + } + // Optional things + if v, ok := d.GetOk("description"); ok { + hchk.Description = v.(string) + } + if v, ok := d.GetOk("check_interval_sec"); ok { + hchk.CheckIntervalSec = int64(v.(int)) + } + if v, ok := d.GetOk("healthy_threshold"); ok { + hchk.HealthyThreshold = int64(v.(int)) + } + if v, ok := d.GetOk("timeout_sec"); ok { + hchk.TimeoutSec = int64(v.(int)) + } + if v, ok := d.GetOk("unhealthy_threshold"); ok { + hchk.UnhealthyThreshold = int64(v.(int)) + } + + if v, ok := d.GetOk("tcp_health_check"); ok { + hchk.Type = "TCP" + tcpcheck := v.([]interface{})[0].(map[string]interface{}) + tcpHealthCheck := &compute.TCPHealthCheck{} + if val, ok := tcpcheck["port"]; ok { + tcpHealthCheck.Port = int64(val.(int)) + } + if val, ok := tcpcheck["proxy_header"]; ok { + tcpHealthCheck.ProxyHeader = val.(string) + } + if val, ok := tcpcheck["request"]; ok { + tcpHealthCheck.Request = val.(string) + } + if val, ok := tcpcheck["response"]; ok { + tcpHealthCheck.Response = val.(string) + } + hchk.TcpHealthCheck = tcpHealthCheck + } + + if v, ok := d.GetOk("ssl_health_check"); ok { + hchk.Type = "SSL" + sslcheck := v.([]interface{})[0].(map[string]interface{}) + sslHealthCheck := &compute.SSLHealthCheck{} + if val, ok := sslcheck["port"]; ok { + sslHealthCheck.Port = int64(val.(int)) + } + if val, ok := sslcheck["proxy_header"]; ok { + sslHealthCheck.ProxyHeader = val.(string) + } + if val, ok := sslcheck["request"]; ok { + sslHealthCheck.Request = val.(string) + } + if val, ok := sslcheck["response"]; ok { + sslHealthCheck.Response = val.(string) + } + hchk.SslHealthCheck = sslHealthCheck + } + + if v, ok := d.GetOk("http_health_check"); ok { + hchk.Type = "HTTP" + httpcheck := v.([]interface{})[0].(map[string]interface{}) + httpHealthCheck := &compute.HTTPHealthCheck{} + if val, ok := httpcheck["host"]; ok { + httpHealthCheck.Host = val.(string) + } + if val, ok := httpcheck["port"]; ok { + httpHealthCheck.Port = int64(val.(int)) + } + if val, ok := httpcheck["proxy_header"]; ok { + httpHealthCheck.ProxyHeader = val.(string) + } + if val, ok := httpcheck["request_path"]; ok { + httpHealthCheck.RequestPath = val.(string) + } + hchk.HttpHealthCheck = httpHealthCheck + } + + if v, ok := d.GetOk("https_health_check"); ok { + hchk.Type = "HTTPS" + httpscheck := v.([]interface{})[0].(map[string]interface{}) + httpsHealthCheck := &compute.HTTPSHealthCheck{} + if val, ok := httpscheck["host"]; ok { + httpsHealthCheck.Host = val.(string) + } + if val, ok := httpscheck["port"]; ok { + httpsHealthCheck.Port = int64(val.(int)) + } + if val, ok := httpscheck["proxy_header"]; ok { + httpsHealthCheck.ProxyHeader = val.(string) + } + if val, ok := httpscheck["request_path"]; ok { + httpsHealthCheck.RequestPath = val.(string) + } + hchk.HttpsHealthCheck = httpsHealthCheck + } + + log.Printf("[DEBUG] HealthCheck insert request: %#v", hchk) + op, err := config.clientCompute.HealthChecks.Insert( + project, hchk).Do() + if err != nil { + return fmt.Errorf("Error creating HealthCheck: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(hchk.Name) + + err = computeOperationWaitGlobal(config, op, project, "Creating Health Check") + if err != nil { + return err + } + + return resourceComputeHealthCheckRead(d, meta) +} + +func resourceComputeHealthCheckUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + // Build the parameter + hchk := &compute.HealthCheck{ + Name: d.Get("name").(string), + } + // Optional things + if v, ok := d.GetOk("description"); ok { + hchk.Description = v.(string) + } + if v, ok := d.GetOk("check_interval_sec"); ok { + hchk.CheckIntervalSec = int64(v.(int)) + } + if v, ok := d.GetOk("healthy_threshold"); ok { + hchk.HealthyThreshold = int64(v.(int)) + } + if v, ok := d.GetOk("timeout_sec"); ok { + hchk.TimeoutSec = int64(v.(int)) + } + if v, ok := d.GetOk("unhealthy_threshold"); ok { + hchk.UnhealthyThreshold = int64(v.(int)) + } + if v, ok := d.GetOk("tcp_health_check"); ok { + hchk.Type = "TCP" + tcpcheck := v.([]interface{})[0].(map[string]interface{}) + tcpHealthCheck := &compute.TCPHealthCheck{} + if val, ok := tcpcheck["port"]; ok { + tcpHealthCheck.Port = int64(val.(int)) + } + if val, ok := tcpcheck["proxy_header"]; ok { + tcpHealthCheck.ProxyHeader = val.(string) + } + if val, ok := tcpcheck["request"]; ok { + tcpHealthCheck.Request = val.(string) + } + if val, ok := tcpcheck["response"]; ok { + tcpHealthCheck.Response = val.(string) + } + hchk.TcpHealthCheck = tcpHealthCheck + } + if v, ok := d.GetOk("ssl_health_check"); ok { + hchk.Type = "SSL" + sslcheck := v.([]interface{})[0].(map[string]interface{}) + sslHealthCheck := &compute.SSLHealthCheck{} + if val, ok := sslcheck["port"]; ok { + sslHealthCheck.Port = int64(val.(int)) + } + if val, ok := sslcheck["proxy_header"]; ok { + sslHealthCheck.ProxyHeader = val.(string) + } + if val, ok := sslcheck["request"]; ok { + sslHealthCheck.Request = val.(string) + } + if val, ok := sslcheck["response"]; ok { + sslHealthCheck.Response = val.(string) + } + hchk.SslHealthCheck = sslHealthCheck + } + if v, ok := d.GetOk("http_health_check"); ok { + hchk.Type = "HTTP" + httpcheck := v.([]interface{})[0].(map[string]interface{}) + httpHealthCheck := &compute.HTTPHealthCheck{} + if val, ok := httpcheck["host"]; ok { + httpHealthCheck.Host = val.(string) + } + if val, ok := httpcheck["port"]; ok { + httpHealthCheck.Port = int64(val.(int)) + } + if val, ok := httpcheck["proxy_header"]; ok { + httpHealthCheck.ProxyHeader = val.(string) + } + if val, ok := httpcheck["request_path"]; ok { + httpHealthCheck.RequestPath = val.(string) + } + hchk.HttpHealthCheck = httpHealthCheck + } + + if v, ok := d.GetOk("https_health_check"); ok { + hchk.Type = "HTTPS" + httpscheck := v.([]interface{})[0].(map[string]interface{}) + httpsHealthCheck := &compute.HTTPSHealthCheck{} + if val, ok := httpscheck["host"]; ok { + httpsHealthCheck.Host = val.(string) + } + if val, ok := httpscheck["port"]; ok { + httpsHealthCheck.Port = int64(val.(int)) + } + if val, ok := httpscheck["proxy_header"]; ok { + httpsHealthCheck.ProxyHeader = val.(string) + } + if val, ok := httpscheck["request_path"]; ok { + httpsHealthCheck.RequestPath = val.(string) + } + hchk.HttpsHealthCheck = httpsHealthCheck + } + + log.Printf("[DEBUG] HealthCheck patch request: %#v", hchk) + op, err := config.clientCompute.HealthChecks.Patch( + project, hchk.Name, hchk).Do() + if err != nil { + return fmt.Errorf("Error patching HealthCheck: %s", err) + } + + // It probably maybe worked, so store the ID now + d.SetId(hchk.Name) + + err = computeOperationWaitGlobal(config, op, project, "Updating Health Check") + if err != nil { + return err + } + + return resourceComputeHealthCheckRead(d, meta) +} + +func resourceComputeHealthCheckRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + hchk, err := config.clientCompute.HealthChecks.Get( + project, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + log.Printf("[WARN] Removing Health Check %q because it's gone", d.Get("name").(string)) + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading HealthCheck: %s", err) + } + + d.Set("check_interval_sec", hchk.CheckIntervalSec) + d.Set("healthy_threshold", hchk.HealthyThreshold) + d.Set("timeout_sec", hchk.TimeoutSec) + d.Set("unhealthy_threshold", hchk.UnhealthyThreshold) + d.Set("tcp_health_check", hchk.TcpHealthCheck) + d.Set("ssl_health_check", hchk.SslHealthCheck) + d.Set("http_health_check", hchk.HttpHealthCheck) + d.Set("https_health_check", hchk.HttpsHealthCheck) + d.Set("self_link", hchk.SelfLink) + d.Set("name", hchk.Name) + d.Set("description", hchk.Description) + d.Set("project", project) + + return nil +} + +func resourceComputeHealthCheckDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + // Delete the HealthCheck + op, err := config.clientCompute.HealthChecks.Delete( + project, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting HealthCheck: %s", err) + } + + err = computeOperationWaitGlobal(config, op, project, "Deleting Health Check") + if err != nil { + return err + } + + d.SetId("") + return nil +} diff --git a/resource_compute_health_check_test.go b/resource_compute_health_check_test.go new file mode 100644 index 00000000..d97c6c3f --- /dev/null +++ b/resource_compute_health_check_test.go @@ -0,0 +1,308 @@ +package google + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/compute/v1" +) + +func TestAccComputeHealthCheck_tcp(t *testing.T) { + var healthCheck compute.HealthCheck + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeHealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeHealthCheck_tcp, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeHealthCheckExists( + "google_compute_health_check.foobar", &healthCheck), + testAccCheckComputeHealthCheckThresholds( + 3, 3, &healthCheck), + testAccCheckComputeHealthCheckTcpPort(80, &healthCheck), + ), + }, + }, + }) +} + +func TestAccComputeHealthCheck_tcp_update(t *testing.T) { + var healthCheck compute.HealthCheck + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeHealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeHealthCheck_tcp, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeHealthCheckExists( + "google_compute_health_check.foobar", &healthCheck), + testAccCheckComputeHealthCheckThresholds( + 3, 3, &healthCheck), + testAccCheckComputeHealthCheckTcpPort(80, &healthCheck), + ), + }, + resource.TestStep{ + Config: testAccComputeHealthCheck_tcp_update, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeHealthCheckExists( + "google_compute_health_check.foobar", &healthCheck), + testAccCheckComputeHealthCheckThresholds( + 10, 10, &healthCheck), + testAccCheckComputeHealthCheckTcpPort(8080, &healthCheck), + ), + }, + }, + }) +} + +func TestAccComputeHealthCheck_ssl(t *testing.T) { + var healthCheck compute.HealthCheck + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeHealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeHealthCheck_ssl, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeHealthCheckExists( + "google_compute_health_check.foobar", &healthCheck), + testAccCheckComputeHealthCheckThresholds( + 3, 3, &healthCheck), + ), + }, + }, + }) +} + +func TestAccComputeHealthCheck_http(t *testing.T) { + var healthCheck compute.HealthCheck + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeHealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeHealthCheck_http, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeHealthCheckExists( + "google_compute_health_check.foobar", &healthCheck), + testAccCheckComputeHealthCheckThresholds( + 3, 3, &healthCheck), + ), + }, + }, + }) +} + +func TestAccComputeHealthCheck_https(t *testing.T) { + var healthCheck compute.HealthCheck + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeHealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeHealthCheck_https, + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeHealthCheckExists( + "google_compute_health_check.foobar", &healthCheck), + testAccCheckComputeHealthCheckThresholds( + 3, 3, &healthCheck), + ), + }, + }, + }) +} + +func TestAccComputeHealthCheck_tcpAndSsl_shouldFail(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeHealthCheckDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeHealthCheck_tcpAndSsl_shouldFail, + ExpectError: regexp.MustCompile("conflicts with tcp_health_check"), + }, + }, + }) +} + +func testAccCheckComputeHealthCheckDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_health_check" { + continue + } + + _, err := config.clientCompute.HealthChecks.Get( + config.Project, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("HealthCheck %s still exists", rs.Primary.ID) + } + } + + return nil +} + +func testAccCheckComputeHealthCheckExists(n string, healthCheck *compute.HealthCheck) 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.HealthChecks.Get( + config.Project, rs.Primary.ID).Do() + if err != nil { + return err + } + + if found.Name != rs.Primary.ID { + return fmt.Errorf("HealthCheck not found") + } + + *healthCheck = *found + + return nil + } +} + +func testAccCheckErrorCreating(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[n] + if ok { + return fmt.Errorf("HealthCheck %s created successfully with bad config", n) + } + return nil + } +} + +func testAccCheckComputeHealthCheckThresholds(healthy, unhealthy int64, healthCheck *compute.HealthCheck) resource.TestCheckFunc { + return func(s *terraform.State) error { + if healthCheck.HealthyThreshold != healthy { + return fmt.Errorf("HealthyThreshold doesn't match: expected %d, got %d", healthy, healthCheck.HealthyThreshold) + } + + if healthCheck.UnhealthyThreshold != unhealthy { + return fmt.Errorf("UnhealthyThreshold doesn't match: expected %d, got %d", unhealthy, healthCheck.UnhealthyThreshold) + } + + return nil + } +} + +func testAccCheckComputeHealthCheckTcpPort(port int64, healthCheck *compute.HealthCheck) resource.TestCheckFunc { + return func(s *terraform.State) error { + if healthCheck.TcpHealthCheck.Port != port { + return fmt.Errorf("Port doesn't match: expected %v, got %v", port, healthCheck.TcpHealthCheck.Port) + } + return nil + } +} + +var testAccComputeHealthCheck_tcp = fmt.Sprintf(` +resource "google_compute_health_check" "foobar" { + check_interval_sec = 3 + description = "Resource created for Terraform acceptance testing" + healthy_threshold = 3 + name = "health-test-%s" + timeout_sec = 2 + unhealthy_threshold = 3 + tcp_health_check { + } +} +`, acctest.RandString(10)) + +var testAccComputeHealthCheck_tcp_update = fmt.Sprintf(` +resource "google_compute_health_check" "foobar" { + check_interval_sec = 3 + description = "Resource updated for Terraform acceptance testing" + healthy_threshold = 10 + name = "health-test-%s" + timeout_sec = 2 + unhealthy_threshold = 10 + tcp_health_check { + port = "8080" + } +} +`, acctest.RandString(10)) + +var testAccComputeHealthCheck_ssl = fmt.Sprintf(` +resource "google_compute_health_check" "foobar" { + check_interval_sec = 3 + description = "Resource created for Terraform acceptance testing" + healthy_threshold = 3 + name = "health-test-%s" + timeout_sec = 2 + unhealthy_threshold = 3 + ssl_health_check { + port = "443" + } +} +`, acctest.RandString(10)) + +var testAccComputeHealthCheck_http = fmt.Sprintf(` +resource "google_compute_health_check" "foobar" { + check_interval_sec = 3 + description = "Resource created for Terraform acceptance testing" + healthy_threshold = 3 + name = "health-test-%s" + timeout_sec = 2 + unhealthy_threshold = 3 + http_health_check { + port = "80" + } +} +`, acctest.RandString(10)) + +var testAccComputeHealthCheck_https = fmt.Sprintf(` +resource "google_compute_health_check" "foobar" { + check_interval_sec = 3 + description = "Resource created for Terraform acceptance testing" + healthy_threshold = 3 + name = "health-test-%s" + timeout_sec = 2 + unhealthy_threshold = 3 + https_health_check { + port = "443" + } +} +`, acctest.RandString(10)) + +var testAccComputeHealthCheck_tcpAndSsl_shouldFail = fmt.Sprintf(` +resource "google_compute_health_check" "foobar" { + check_interval_sec = 3 + description = "Resource created for Terraform acceptance testing" + healthy_threshold = 3 + name = "health-test-%s" + timeout_sec = 2 + unhealthy_threshold = 3 + + tcp_health_check { + } + ssl_health_check { + } +} +`, acctest.RandString(10)) diff --git a/resource_compute_region_backend_service.go b/resource_compute_region_backend_service.go new file mode 100644 index 00000000..a12bc39e --- /dev/null +++ b/resource_compute_region_backend_service.go @@ -0,0 +1,306 @@ +package google + +import ( + "bytes" + "fmt" + "log" + "regexp" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/compute/v1" + "google.golang.org/api/googleapi" +) + +func resourceComputeRegionBackendService() *schema.Resource { + return &schema.Resource{ + Create: resourceComputeRegionBackendServiceCreate, + Read: resourceComputeRegionBackendServiceRead, + Update: resourceComputeRegionBackendServiceUpdate, + Delete: resourceComputeRegionBackendServiceDelete, + + Schema: map[string]*schema.Schema{ + "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 + }, + }, + + "health_checks": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Required: true, + Set: schema.HashString, + }, + + "backend": &schema.Schema{ + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "group": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + }, + }, + Optional: true, + Set: resourceGoogleComputeRegionBackendServiceBackendHash, + }, + + "description": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "fingerprint": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "protocol": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "self_link": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "timeout_sec": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + }, + } +} + +func resourceComputeRegionBackendServiceCreate(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, + LoadBalancingScheme: "INTERNAL", + } + + 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("protocol"); ok { + service.Protocol = v.(string) + } + + if v, ok := d.GetOk("timeout_sec"); ok { + service.TimeoutSec = int64(v.(int)) + } + + project, err := getProject(d, config) + if err != nil { + return err + } + + region, err := getRegion(d, config) + if err != nil { + return err + } + + log.Printf("[DEBUG] Creating new Region Backend Service: %#v", service) + + op, err := config.clientCompute.RegionBackendServices.Insert( + project, region, &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) + + err = computeOperationWaitRegion(config, op, project, region, "Creating Region Backend Service") + if err != nil { + return err + } + + return resourceComputeRegionBackendServiceRead(d, meta) +} + +func resourceComputeRegionBackendServiceRead(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + region, err := getRegion(d, config) + if err != nil { + return err + } + + service, err := config.clientCompute.RegionBackendServices.Get( + project, region, d.Id()).Do() + if err != nil { + if gerr, ok := err.(*googleapi.Error); ok && gerr.Code == 404 { + // The resource doesn't exist anymore + log.Printf("[WARN] Removing Backend Service %q because it's gone", d.Get("name").(string)) + d.SetId("") + + return nil + } + + return fmt.Errorf("Error reading service: %s", err) + } + + d.Set("description", service.Description) + 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 resourceComputeRegionBackendServiceUpdate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + region, err := getRegion(d, config) + if err != nil { + return err + } + + 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, + LoadBalancingScheme: "INTERNAL", + } + + // Optional things + 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("protocol"); ok { + service.Protocol = v.(string) + } + if v, ok := d.GetOk("timeout_sec"); ok { + service.TimeoutSec = int64(v.(int)) + } + + log.Printf("[DEBUG] Updating existing Backend Service %q: %#v", d.Id(), service) + op, err := config.clientCompute.RegionBackendServices.Update( + project, region, d.Id(), &service).Do() + if err != nil { + return fmt.Errorf("Error updating backend service: %s", err) + } + + d.SetId(service.Name) + + err = computeOperationWaitRegion(config, op, project, region, "Updating Backend Service") + if err != nil { + return err + } + + return resourceComputeRegionBackendServiceRead(d, meta) +} + +func resourceComputeRegionBackendServiceDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + + project, err := getProject(d, config) + if err != nil { + return err + } + + region, err := getRegion(d, config) + if err != nil { + return err + } + + log.Printf("[DEBUG] Deleting backend service %s", d.Id()) + op, err := config.clientCompute.RegionBackendServices.Delete( + project, region, d.Id()).Do() + if err != nil { + return fmt.Errorf("Error deleting backend service: %s", err) + } + + err = computeOperationWaitRegion(config, op, project, region, "Deleting Backend Service") + if err != nil { + return err + } + + d.SetId("") + return nil +} + +func resourceGoogleComputeRegionBackendServiceBackendHash(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["description"]; ok { + buf.WriteString(fmt.Sprintf("%s-", v.(string))) + } + + return hashcode.String(buf.String()) +} diff --git a/resource_compute_region_backend_service_test.go b/resource_compute_region_backend_service_test.go new file mode 100644 index 00000000..98a7d448 --- /dev/null +++ b/resource_compute_region_backend_service_test.go @@ -0,0 +1,262 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/compute/v1" +) + +func TestAccComputeRegionBackendService_basic(t *testing.T) { + serviceName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + checkName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + extraCheckName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + var svc compute.BackendService + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRegionBackendServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRegionBackendService_basic(serviceName, checkName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionBackendServiceExists( + "google_compute_region_backend_service.foobar", &svc), + ), + }, + resource.TestStep{ + Config: testAccComputeRegionBackendService_basicModified( + serviceName, checkName, extraCheckName), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionBackendServiceExists( + "google_compute_region_backend_service.foobar", &svc), + ), + }, + }, + }) +} + +func TestAccComputeRegionBackendService_withBackend(t *testing.T) { + serviceName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + igName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + itName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + checkName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + var svc compute.BackendService + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRegionBackendServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRegionBackendService_withBackend( + serviceName, igName, itName, checkName, 10), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionBackendServiceExists( + "google_compute_region_backend_service.lipsum", &svc), + ), + }, + }, + }) + + if svc.TimeoutSec != 10 { + t.Errorf("Expected TimeoutSec == 10, got %d", svc.TimeoutSec) + } + if svc.Protocol != "TCP" { + t.Errorf("Expected Protocol to be TCP, got %q", svc.Protocol) + } + if len(svc.Backends) != 1 { + t.Errorf("Expected 1 backend, got %d", len(svc.Backends)) + } +} + +func TestAccComputeRegionBackendService_withBackendAndUpdate(t *testing.T) { + serviceName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + igName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + itName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + checkName := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + var svc compute.BackendService + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckComputeRegionBackendServiceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccComputeRegionBackendService_withBackend( + serviceName, igName, itName, checkName, 10), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionBackendServiceExists( + "google_compute_region_backend_service.lipsum", &svc), + ), + }, + resource.TestStep{ + Config: testAccComputeRegionBackendService_withBackend( + serviceName, igName, itName, checkName, 20), + Check: resource.ComposeTestCheckFunc( + testAccCheckComputeRegionBackendServiceExists( + "google_compute_region_backend_service.lipsum", &svc), + ), + }, + }, + }) + + if svc.TimeoutSec != 20 { + t.Errorf("Expected TimeoutSec == 20, got %d", svc.TimeoutSec) + } + if svc.Protocol != "TCP" { + t.Errorf("Expected Protocol to be TCP, got %q", svc.Protocol) + } + if len(svc.Backends) != 1 { + t.Errorf("Expected 1 backend, got %d", len(svc.Backends)) + } +} + +func testAccCheckComputeRegionBackendServiceDestroy(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "google_compute_region_backend_service" { + continue + } + + _, err := config.clientCompute.RegionBackendServices.Get( + config.Project, config.Region, rs.Primary.ID).Do() + if err == nil { + return fmt.Errorf("Backend service still exists") + } + } + + return nil +} + +func testAccCheckComputeRegionBackendServiceExists(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.RegionBackendServices.Get( + config.Project, config.Region, 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 + } +} + +func testAccComputeRegionBackendService_basic(serviceName, checkName string) string { + return fmt.Sprintf(` +resource "google_compute_region_backend_service" "foobar" { + name = "%s" + health_checks = ["${google_compute_health_check.zero.self_link}"] + region = "us-central1" +} + +resource "google_compute_health_check" "zero" { + name = "%s" + check_interval_sec = 1 + timeout_sec = 1 + + tcp_health_check { + port = "80" + } +} +`, serviceName, checkName) +} + +func testAccComputeRegionBackendService_basicModified(serviceName, checkOne, checkTwo string) string { + return fmt.Sprintf(` +resource "google_compute_region_backend_service" "foobar" { + name = "%s" + health_checks = ["${google_compute_health_check.one.self_link}"] + region = "us-central1" +} + +resource "google_compute_health_check" "zero" { + name = "%s" + check_interval_sec = 1 + timeout_sec = 1 + + tcp_health_check { + } +} + +resource "google_compute_health_check" "one" { + name = "%s" + check_interval_sec = 30 + timeout_sec = 30 + + tcp_health_check { + } +} +`, serviceName, checkOne, checkTwo) +} + +func testAccComputeRegionBackendService_withBackend( + serviceName, igName, itName, checkName string, timeout int64) string { + return fmt.Sprintf(` +resource "google_compute_region_backend_service" "lipsum" { + name = "%s" + description = "Hello World 1234" + protocol = "TCP" + region = "us-central1" + timeout_sec = %v + + backend { + group = "${google_compute_instance_group_manager.foobar.instance_group}" + } + + health_checks = ["${google_compute_health_check.default.self_link}"] +} + +resource "google_compute_instance_group_manager" "foobar" { + name = "%s" + 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 = "%s" + machine_type = "n1-standard-1" + + network_interface { + network = "default" + } + + disk { + source_image = "debian-8-jessie-v20160803" + auto_delete = true + boot = true + } +} + +resource "google_compute_health_check" "default" { + name = "%s" + check_interval_sec = 1 + timeout_sec = 1 + + tcp_health_check { + + } +} +`, serviceName, timeout, igName, itName, checkName) +} diff --git a/resource_storage_object_acl.go b/resource_storage_object_acl.go index a73e34b3..9795305b 100644 --- a/resource_storage_object_acl.go +++ b/resource_storage_object_acl.go @@ -150,15 +150,8 @@ func resourceStorageObjectAclRead(d *schema.ResourceData, meta interface{}) erro } for _, v := range res.Items { - role := "" - entity := "" - for key, val := range v.(map[string]interface{}) { - if key == "role" { - role = val.(string) - } else if key == "entity" { - entity = val.(string) - } - } + role := v.Role + entity := v.Entity if _, in := re_local_map[entity]; in { role_entity = append(role_entity, fmt.Sprintf("%s:%s", role, entity)) log.Printf("[DEBUG]: saving re %s-%s", role, entity)