terraform-provider-google/google/resource_google_project_service.go
Seth Vargo 40094ba417 Lions, tigers, and services being enabled with "precondition failed", oh my! (#1565)
* Use errwrap to retain original error

* Use built-in Page function, only return names when listing services

This removes the custom logic on pagination and uses the built-in Page function in the SDK to make things a bit simpler. Additionally, I added a field filter to only return service names, which drastically reduces the size of the API call (important for slow connections, given how frequently this function is executed).

Also added errwrap to better trace where errors originate.

* Add helper function for diffing string slices

This just looked really nasty inline

* Batch 20 services at a time, handle precondition failed, better errwrap

This commit does three things:

1. It batches services to be enabled 20 at a time. The API fails if you try to enable more than 20 services, and this is documented in the SDK and API. I learned this the hard way. I think Terraform should "do the right thing" here and batch them in series' of twenty, which is what this does. Each batch is tried in serial, but I think making it parallelized is not worth the complexity tradeoffs.

2. Handle the precondition failed error that occurs randomly. This just started happened, but it affects at least two APIs consistently, and a rudimentary test showed that it failed 78% of the time (78/100 times in an hour). We should fix this upstream, but that failure rate also necessitates (in my opinion) some mitigation on the Terraform side until a fix is in place at the API level.

3. Use errwrap on errors for better tracing. It was really difficult to trace exactly which error was being throw. That's fixed.

* Updates from code review
2018-05-31 09:26:40 -07:00

137 lines
3.3 KiB
Go

package google
import (
"fmt"
"log"
"strings"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceGoogleProjectService() *schema.Resource {
return &schema.Resource{
Create: resourceGoogleProjectServiceCreate,
Read: resourceGoogleProjectServiceRead,
Delete: resourceGoogleProjectServiceDelete,
Update: resourceGoogleProjectServiceUpdate,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"service": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"project": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"disable_on_destroy": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
},
}
}
func resourceGoogleProjectServiceCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
project, err := getProject(d, config)
if err != nil {
return err
}
srv := d.Get("service").(string)
if err = enableService(srv, project, config); err != nil {
return errwrap.Wrapf("Error enabling service: {{err}}", err)
}
d.SetId(projectServiceId{project, srv}.terraformId())
return resourceGoogleProjectServiceRead(d, meta)
}
func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
id, err := parseProjectServiceId(d.Id())
if err != nil {
return err
}
services, err := getApiServices(id.project, config, map[string]struct{}{})
if err != nil {
return err
}
d.Set("project", id.project)
for _, s := range services {
if s == id.service {
d.Set("service", s)
return nil
}
}
// The service is not enabled server-side, so remove it from state
d.SetId("")
return nil
}
func resourceGoogleProjectServiceDelete(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config)
if disable := d.Get("disable_on_destroy"); !(disable.(bool)) {
log.Printf("Not disabling service '%s', because disable_on_destroy is false.", d.Id())
d.SetId("")
return nil
}
id, err := parseProjectServiceId(d.Id())
if err != nil {
return err
}
if err = disableService(id.service, id.project, config); err != nil {
return fmt.Errorf("Error disabling service: %s", err)
}
d.SetId("")
return nil
}
func resourceGoogleProjectServiceUpdate(d *schema.ResourceData, meta interface{}) error {
// The only thing that can be updated without a ForceNew is whether to disable the service on resource delete.
// This doesn't require any calls to any APIs since it's all internal state.
// This update is a no-op.
return nil
}
// Parts that make up the id of a `google_project_service` resource.
// Project is included in order to allow multiple projects to enable the same service within the same Terraform state
type projectServiceId struct {
project string
service string
}
func (id projectServiceId) terraformId() string {
return fmt.Sprintf("%s/%s", id.project, id.service)
}
func parseProjectServiceId(id string) (*projectServiceId, error) {
parts := strings.Split(id, "/")
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid google_project_service id format, expecting `{project}/{service}`, found %s", id)
}
return &projectServiceId{parts[0], parts[1]}, nil
}