mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-09-06 09:06:44 +00:00
4ec59fccea
When enabling services, after the waiter returns, list the enabled services and ensure the ones we enabled are in there. If not, retry. May not always resolve #1393, but should help. Unfortunately, the real answer is probably either: 1. For us to try and get the API updated to only return the waiter when the service will consistently be available. I don't know how feasible this is, but I'm willing to open a ticket. 2. For us to build retries into ~all our resources to retry for a set amount of time when a service not enabled error is returned. This would greatly slow down the provider in the case of the service legitimately not being enabled, but is how other providers handle this class of problem. Unfortunately, due to the eventual consistency at play, this is a hard issue to reproduce and prove, though it matches with my experience--while testing this patch, one of the tests failed with the error that the serviceusage API hadn't been enabled, but only on step 4 of the test, when calls had already succeeded. Which suggests eventual consistency, to me. Regardless, this patch shouldn't _hurt_ and should mostly be an imperceptible change to users, and should make instances like #1393 less likely.
295 lines
8.0 KiB
Go
295 lines
8.0 KiB
Go
package google
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"google.golang.org/api/googleapi"
|
|
"google.golang.org/api/serviceusage/v1beta1"
|
|
)
|
|
|
|
func resourceGoogleProjectServices() *schema.Resource {
|
|
return &schema.Resource{
|
|
Create: resourceGoogleProjectServicesCreate,
|
|
Read: resourceGoogleProjectServicesRead,
|
|
Update: resourceGoogleProjectServicesUpdate,
|
|
Delete: resourceGoogleProjectServicesDelete,
|
|
Importer: &schema.ResourceImporter{
|
|
State: schema.ImportStatePassthrough,
|
|
},
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"project": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
"services": {
|
|
Type: schema.TypeSet,
|
|
Required: true,
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Set: schema.HashString,
|
|
},
|
|
"disable_on_destroy": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Default: true,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// These services can only be enabled as a side-effect of enabling other services,
|
|
// so don't bother storing them in the config or using them for diffing.
|
|
var ignoreProjectServices = map[string]struct{}{
|
|
"containeranalysis.googleapis.com": struct{}{},
|
|
"dataproc-control.googleapis.com": struct{}{},
|
|
"source.googleapis.com": struct{}{},
|
|
}
|
|
|
|
func resourceGoogleProjectServicesCreate(d *schema.ResourceData, meta interface{}) error {
|
|
config := meta.(*Config)
|
|
pid := d.Get("project").(string)
|
|
|
|
// Get services from config
|
|
cfgServices := getConfigServices(d)
|
|
|
|
// Get services from API
|
|
apiServices, err := getApiServices(pid, config, ignoreProjectServices)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating services: %v", err)
|
|
}
|
|
|
|
// This call disables any APIs that aren't defined in cfgServices,
|
|
// and enables all of those that are
|
|
err = reconcileServices(cfgServices, apiServices, config, pid)
|
|
if err != nil {
|
|
return fmt.Errorf("Error creating services: %v", err)
|
|
}
|
|
|
|
d.SetId(pid)
|
|
return resourceGoogleProjectServicesRead(d, meta)
|
|
}
|
|
|
|
func resourceGoogleProjectServicesRead(d *schema.ResourceData, meta interface{}) error {
|
|
config := meta.(*Config)
|
|
|
|
services, err := getApiServices(d.Id(), config, ignoreProjectServices)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
d.Set("project", d.Id())
|
|
d.Set("services", services)
|
|
return nil
|
|
}
|
|
|
|
func resourceGoogleProjectServicesUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
log.Printf("[DEBUG]: Updating google_project_services")
|
|
config := meta.(*Config)
|
|
pid := d.Get("project").(string)
|
|
|
|
// Get services from config
|
|
cfgServices := getConfigServices(d)
|
|
|
|
// Get services from API
|
|
apiServices, err := getApiServices(pid, config, ignoreProjectServices)
|
|
if err != nil {
|
|
return fmt.Errorf("Error updating services: %v", err)
|
|
}
|
|
|
|
// This call disables any APIs that aren't defined in cfgServices,
|
|
// and enables all of those that are
|
|
err = reconcileServices(cfgServices, apiServices, config, pid)
|
|
if err != nil {
|
|
return fmt.Errorf("Error updating services: %v", err)
|
|
}
|
|
|
|
return resourceGoogleProjectServicesRead(d, meta)
|
|
}
|
|
|
|
func resourceGoogleProjectServicesDelete(d *schema.ResourceData, meta interface{}) error {
|
|
log.Printf("[DEBUG]: Deleting google_project_services")
|
|
|
|
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
|
|
}
|
|
|
|
config := meta.(*Config)
|
|
services := resourceServices(d)
|
|
for _, s := range services {
|
|
disableService(s, d.Id(), config)
|
|
}
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
// This function ensures that the services enabled for a project exactly match that
|
|
// in a config by disabling any services that are returned by the API but not present
|
|
// in the config
|
|
func reconcileServices(cfgServices, apiServices []string, config *Config, pid string) error {
|
|
// Helper to convert slice to map
|
|
m := func(vals []string) map[string]struct{} {
|
|
sm := make(map[string]struct{})
|
|
for _, s := range vals {
|
|
sm[s] = struct{}{}
|
|
}
|
|
return sm
|
|
}
|
|
|
|
cfgMap := m(cfgServices)
|
|
apiMap := m(apiServices)
|
|
|
|
for k, _ := range apiMap {
|
|
if _, ok := cfgMap[k]; !ok {
|
|
// The service in the API is not in the config; disable it.
|
|
err := disableService(k, pid, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// The service exists in the config and the API, so we don't need
|
|
// to re-enable it
|
|
delete(cfgMap, k)
|
|
}
|
|
}
|
|
|
|
keys := make([]string, 0, len(cfgMap))
|
|
for k, _ := range cfgMap {
|
|
keys = append(keys, k)
|
|
}
|
|
err := enableServices(keys, pid, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Retrieve services defined in a config
|
|
func getConfigServices(d *schema.ResourceData) (services []string) {
|
|
if v, ok := d.GetOk("services"); ok {
|
|
for _, svc := range v.(*schema.Set).List() {
|
|
services = append(services, svc.(string))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Retrieve a project's services from the API
|
|
func getApiServices(pid string, config *Config, ignore map[string]struct{}) ([]string, error) {
|
|
apiServices := make([]string, 0)
|
|
// Get services from the API
|
|
token := ""
|
|
for paginate := true; paginate; {
|
|
svcResp, err := config.clientServiceUsage.Services.List("projects/" + pid).PageToken(token).Filter("state:ENABLED").Do()
|
|
if err != nil {
|
|
return apiServices, err
|
|
}
|
|
for _, v := range svcResp.Services {
|
|
// names are returned as projects/{project-number}/services/{service-name}
|
|
nameParts := strings.Split(v.Name, "/")
|
|
name := nameParts[len(nameParts)-1]
|
|
if _, ok := ignore[name]; !ok {
|
|
apiServices = append(apiServices, name)
|
|
}
|
|
}
|
|
token = svcResp.NextPageToken
|
|
paginate = token != ""
|
|
}
|
|
return apiServices, nil
|
|
}
|
|
|
|
func enableService(s, pid string, config *Config) error {
|
|
return enableServices([]string{s}, pid, config)
|
|
}
|
|
|
|
func enableServices(s []string, pid string, config *Config) error {
|
|
err := retryTime(func() error {
|
|
var sop *serviceusage.Operation
|
|
var err error
|
|
if len(s) > 1 {
|
|
req := &serviceusage.BatchEnableServicesRequest{ServiceIds: s}
|
|
sop, err = config.clientServiceUsage.Services.BatchEnable("projects/"+pid, req).Do()
|
|
} else if len(s) == 1 {
|
|
name := fmt.Sprintf("projects/%s/services/%s", pid, s[0])
|
|
sop, err = config.clientServiceUsage.Services.Enable(name, &serviceusage.EnableServiceRequest{}).Do()
|
|
} else {
|
|
// No services to enable
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, waitErr := serviceUsageOperationWait(config, sop, "api to enable")
|
|
if waitErr != nil {
|
|
return waitErr
|
|
}
|
|
services, err := getApiServices(pid, config, map[string]struct{}{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var missing []string
|
|
for _, toEnable := range s {
|
|
var found bool
|
|
for _, service := range services {
|
|
if service == toEnable {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
missing = append(missing, toEnable)
|
|
}
|
|
}
|
|
if len(missing) > 0 {
|
|
// spoof a googleapi Error so retryTime will try again
|
|
return &googleapi.Error{
|
|
Code: 503, // haha, get it, service unavailable
|
|
Message: fmt.Sprintf("The services %s are still being enabled for project %q. This isn't a real API error, this is just eventual consistency.", strings.Join(missing, ", "), pid),
|
|
}
|
|
}
|
|
return nil
|
|
}, 10)
|
|
if err != nil {
|
|
return fmt.Errorf("Error enabling service %q for project %q: %v", s, pid, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func disableService(s, pid string, config *Config) error {
|
|
err := retryTime(func() error {
|
|
name := fmt.Sprintf("projects/%s/services/%s", pid, s)
|
|
sop, err := config.clientServiceUsage.Services.Disable(name, &serviceusage.DisableServiceRequest{}).Do()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Wait for the operation to complete
|
|
_, waitErr := serviceUsageOperationWait(config, sop, "api to disable")
|
|
if waitErr != nil {
|
|
return waitErr
|
|
}
|
|
return nil
|
|
}, 10)
|
|
if err != nil {
|
|
return fmt.Errorf("Error disabling service %q for project %q: %v", s, pid, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func resourceServices(d *schema.ResourceData) []string {
|
|
// Calculate the tags
|
|
var services []string
|
|
if s := d.Get("services"); s != nil {
|
|
ss := s.(*schema.Set)
|
|
services = make([]string, ss.Len())
|
|
for i, v := range ss.List() {
|
|
services[i] = v.(string)
|
|
}
|
|
}
|
|
return services
|
|
}
|