diff --git a/config.go b/config.go index 09cd750b..9f9eb075 100644 --- a/config.go +++ b/config.go @@ -19,6 +19,7 @@ import ( "google.golang.org/api/dns/v1" "google.golang.org/api/iam/v1" "google.golang.org/api/pubsub/v1" + "google.golang.org/api/servicemanagement/v1" "google.golang.org/api/sqladmin/v1beta4" "google.golang.org/api/storage/v1" ) @@ -38,6 +39,7 @@ type Config struct { clientStorage *storage.Service clientSqlAdmin *sqladmin.Service clientIAM *iam.Service + clientServiceMan *servicemanagement.APIService } func (c *Config) loadAndValidate() error { @@ -130,27 +132,34 @@ func (c *Config) loadAndValidate() error { } c.clientSqlAdmin.UserAgent = userAgent - log.Printf("[INFO] Instatiating Google Pubsub Client...") + log.Printf("[INFO] Instantiating Google Pubsub Client...") c.clientPubsub, err = pubsub.New(client) if err != nil { return err } c.clientPubsub.UserAgent = userAgent - log.Printf("[INFO] Instatiating Google Cloud ResourceManager Client...") + log.Printf("[INFO] Instantiating Google Cloud ResourceManager Client...") c.clientResourceManager, err = cloudresourcemanager.New(client) if err != nil { return err } c.clientResourceManager.UserAgent = userAgent - log.Printf("[INFO] Instatiating Google Cloud IAM Client...") + log.Printf("[INFO] Instantiating Google Cloud IAM Client...") c.clientIAM, err = iam.New(client) if err != nil { return err } c.clientIAM.UserAgent = userAgent + log.Printf("[INFO] Instantiating Google Cloud Service Management Client...") + c.clientServiceMan, err = servicemanagement.New(client) + if err != nil { + return err + } + c.clientServiceMan.UserAgent = userAgent + return nil } diff --git a/import_google_project_test.go b/import_google_project_test.go index b35c8d6b..2bec9461 100644 --- a/import_google_project_test.go +++ b/import_google_project_test.go @@ -4,12 +4,14 @@ import ( "fmt" "testing" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" ) func TestAccGoogleProject_importBasic(t *testing.T) { resourceName := "google_project.acceptance" - conf := fmt.Sprintf(testAccGoogleProject_basic, projectId) + projectId := "terraform-" + acctest.RandString(10) + conf := testAccGoogleProject_import(projectId, org, pname) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -27,3 +29,12 @@ func TestAccGoogleProject_importBasic(t *testing.T) { }, }) } + +func testAccGoogleProject_import(pid, orgId, projectName string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + org_id = "%s" + name = "%s" +}`, pid, orgId, projectName) +} diff --git a/provider.go b/provider.go index ce8ef552..d1263efa 100644 --- a/provider.go +++ b/provider.go @@ -96,6 +96,8 @@ func Provider() terraform.ResourceProvider { "google_sql_database_instance": resourceSqlDatabaseInstance(), "google_sql_user": resourceSqlUser(), "google_project": resourceGoogleProject(), + "google_project_iam_policy": resourceGoogleProjectIamPolicy(), + "google_project_services": resourceGoogleProjectServices(), "google_pubsub_topic": resourcePubsubTopic(), "google_pubsub_subscription": resourcePubsubSubscription(), "google_service_account": resourceGoogleServiceAccount(), diff --git a/resource_google_project.go b/resource_google_project.go index 9e845ed3..4bc26c45 100644 --- a/resource_google_project.go +++ b/resource_google_project.go @@ -13,9 +13,7 @@ import ( ) // resourceGoogleProject returns a *schema.Resource that allows a customer -// to declare a Google Cloud Project resource. // -// Only the 'policy' property of a project may be updated. All other properties -// are computed. +// to declare a Google Cloud Project resource. // // This example shows a project with a policy declared in config: // @@ -25,28 +23,65 @@ import ( // } func resourceGoogleProject() *schema.Resource { return &schema.Resource{ + SchemaVersion: 1, + Create: resourceGoogleProjectCreate, Read: resourceGoogleProjectRead, Update: resourceGoogleProjectUpdate, Delete: resourceGoogleProjectDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, + MigrateState: resourceGoogleProjectMigrateState, Schema: map[string]*schema.Schema{ "id": &schema.Schema{ - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + Deprecated: "The id field has unexpected behaviour and probably doesn't do what you expect. See https://www.terraform.io/docs/providers/google/r/google_project.html#id-field for more information. Please use project_id instead; future versions of Terraform will remove the id field.", }, - "policy_data": &schema.Schema{ + "project_id": &schema.Schema{ Type: schema.TypeString, Optional: true, + ForceNew: true, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // This suppresses the diff if project_id is not set + if new == "" { + return true + } + return false + }, + }, + "skip_delete": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Computed: true, }, "name": &schema.Schema{ Type: schema.TypeString, + Optional: true, Computed: true, }, + "org_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + "policy_data": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + Deprecated: "Use the 'google_project_iam_policy' resource to define policies for a Google Project", + DiffSuppressFunc: jsonPolicyDiffSuppress, + }, + "policy_etag": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Deprecated: "Use the the 'google_project_iam_policy' resource to define policies for a Google Project", + }, "number": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -55,20 +90,55 @@ func resourceGoogleProject() *schema.Resource { } } -// This resource supports creation, but not in the traditional sense. -// A new Google Cloud Project can not be created. Instead, an existing Project -// is initialized and made available as a Terraform resource. func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - project, err := getProject(d, config) - if err != nil { - return err + var pid string + var err error + pid = d.Get("project_id").(string) + if pid == "" { + pid, err = getProject(d, config) + if err != nil { + return fmt.Errorf("Error getting project ID: %v", err) + } + if pid == "" { + return fmt.Errorf("'project_id' must be set in the config") + } } - d.SetId(project) - if err := resourceGoogleProjectRead(d, meta); err != nil { - return err + // we need to check if name and org_id are set, and throw an error if they aren't + // we can't just set these as required on the object, however, as that would break + // all configs that used previous iterations of the resource. + // TODO(paddy): remove this for 0.9 and set these attributes as required. + name, org_id := d.Get("name").(string), d.Get("org_id").(string) + if name == "" { + return fmt.Errorf("`name` must be set in the config if you're creating a project.") + } + if org_id == "" { + return fmt.Errorf("`org_id` must be set in the config if you're creating a project.") + } + + log.Printf("[DEBUG]: Creating new project %q", pid) + project := &cloudresourcemanager.Project{ + ProjectId: pid, + Name: d.Get("name").(string), + Parent: &cloudresourcemanager.ResourceId{ + Id: d.Get("org_id").(string), + Type: "organization", + }, + } + + op, err := config.clientResourceManager.Projects.Create(project).Do() + if err != nil { + return fmt.Errorf("Error creating project %s (%s): %s.", project.ProjectId, project.Name, err) + } + + d.SetId(pid) + + // Wait for the operation to complete + waitErr := resourceManagerOperationWait(config, op, "project to create") + if waitErr != nil { + return waitErr } // Apply the IAM policy if it is set @@ -76,15 +146,14 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error // The policy string is just a marshaled cloudresourcemanager.Policy. // Unmarshal it to a struct. var policy cloudresourcemanager.Policy - if err = json.Unmarshal([]byte(pString.(string)), &policy); err != nil { + if err := json.Unmarshal([]byte(pString.(string)), &policy); err != nil { return err } + log.Printf("[DEBUG] Got policy from config: %#v", policy.Bindings) // Retrieve existing IAM policy from project. This will be merged // with the policy defined here. - // TODO(evanbrown): Add an 'authoritative' flag that allows policy - // in manifest to overwrite existing policy. - p, err := getProjectIamPolicy(project, config) + p, err := getProjectIamPolicy(pid, config) if err != nil { return err } @@ -95,47 +164,98 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error // Apply the merged policy log.Printf("[DEBUG] Setting new policy for project: %#v", p) - _, err = config.clientResourceManager.Projects.SetIamPolicy(project, + _, err = config.clientResourceManager.Projects.SetIamPolicy(pid, &cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do() if err != nil { - return fmt.Errorf("Error applying IAM policy for project %q: %s", project, err) + return fmt.Errorf("Error applying IAM policy for project %q: %s", pid, err) } } - return nil + + return resourceGoogleProjectRead(d, meta) } func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - project, err := getProject(d, config) + pid := d.Id() + + // Read the project + p, err := config.clientResourceManager.Projects.Get(pid).Do() + if err != nil { + if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound { + return fmt.Errorf("Project %q does not exist.", pid) + } + return fmt.Errorf("Error checking project %q: %s", pid, err) + } + + d.Set("project_id", pid) + d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10)) + d.Set("name", p.Name) + + if p.Parent != nil { + d.Set("org_id", p.Parent.Id) + } + + // Read the IAM policy + pol, err := getProjectIamPolicy(pid, config) if err != nil { return err } - d.SetId(project) - // Confirm the project exists. - // TODO(evanbrown): Support project creation - p, err := config.clientResourceManager.Projects.Get(project).Do() + polBytes, err := json.Marshal(pol) if err != nil { - if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound { - return fmt.Errorf("Project %q does not exist. The Google provider does not currently support new project creation.", project) - } - return fmt.Errorf("Error checking project %q: %s", project, err) + return err } - d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10)) - d.Set("name", p.Name) + d.Set("policy_etag", pol.Etag) + d.Set("policy_data", string(polBytes)) return nil } func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) - project, err := getProject(d, config) + pid := d.Id() + + // Read the project + // we need the project even though refresh has already been called + // because the API doesn't support patch, so we need the actual object + p, err := config.clientResourceManager.Projects.Get(pid).Do() if err != nil { - return err + if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound { + return fmt.Errorf("Project %q does not exist.", pid) + } + return fmt.Errorf("Error checking project %q: %s", pid, err) } + // Project name has changed + if ok := d.HasChange("name"); ok { + p.Name = d.Get("name").(string) + // Do update on project + p, err = config.clientResourceManager.Projects.Update(p.ProjectId, p).Do() + if err != nil { + return fmt.Errorf("Error updating project %q: %s", p.Name, err) + } + } + + return updateProjectIamPolicy(d, config, pid) +} + +func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + // Only delete projects if skip_delete isn't set + if !d.Get("skip_delete").(bool) { + pid := d.Id() + _, err := config.clientResourceManager.Projects.Delete(pid).Do() + if err != nil { + return fmt.Errorf("Error deleting project %q: %s", pid, err) + } + } + d.SetId("") + return nil +} + +func updateProjectIamPolicy(d *schema.ResourceData, config *Config, pid string) error { // Policy has changed if ok := d.HasChange("policy_data"); ok { // The policy string is just a marshaled cloudresourcemanager.Policy. @@ -152,15 +272,13 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error newPString = "{}" } - oldPStringf, _ := json.MarshalIndent(oldPString, "", " ") - newPStringf, _ := json.MarshalIndent(newPString, "", " ") - log.Printf("[DEBUG]: Old policy: %v\nNew policy: %v", string(oldPStringf), string(newPStringf)) + log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", oldPString, newPString) var oldPolicy, newPolicy cloudresourcemanager.Policy - if err = json.Unmarshal([]byte(newPString), &newPolicy); err != nil { + if err := json.Unmarshal([]byte(newPString), &newPolicy); err != nil { return err } - if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil { + if err := json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil { return err } @@ -199,7 +317,7 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error // with the policy in the current state // TODO(evanbrown): Add an 'authoritative' flag that allows policy // in manifest to overwrite existing policy. - p, err := getProjectIamPolicy(project, config) + p, err := getProjectIamPolicy(pid, config) if err != nil { return err } @@ -218,86 +336,15 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error } p.Bindings = rolesToMembersBinding(mergedBindingsMap) - log.Printf("[DEBUG] Setting new policy for project: %#v", p) - dump, _ := json.MarshalIndent(p.Bindings, " ", " ") - log.Printf(string(dump)) - _, err = config.clientResourceManager.Projects.SetIamPolicy(project, + log.Printf("[DEBUG] Setting new policy for project: %#v:\n%s", p, string(dump)) + + _, err = config.clientResourceManager.Projects.SetIamPolicy(pid, &cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do() if err != nil { - return fmt.Errorf("Error applying IAM policy for project %q: %s", project, err) + return fmt.Errorf("Error applying IAM policy for project %q: %s", pid, err) } } - return nil } - -func resourceGoogleProjectDelete(d *schema.ResourceData, meta interface{}) error { - d.SetId("") - return nil -} - -// Retrieve the existing IAM Policy for a Project -func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) { - p, err := config.clientResourceManager.Projects.GetIamPolicy(project, - &cloudresourcemanager.GetIamPolicyRequest{}).Do() - - if err != nil { - return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err) - } - return p, nil -} - -// Convert a map of roles->members to a list of Binding -func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding { - bindings := make([]*cloudresourcemanager.Binding, 0) - for role, members := range m { - b := cloudresourcemanager.Binding{ - Role: role, - Members: make([]string, 0), - } - for m, _ := range members { - b.Members = append(b.Members, m) - } - bindings = append(bindings, &b) - } - return bindings -} - -// Map a role to a map of members, allowing easy merging of multiple bindings. -func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool { - bm := make(map[string]map[string]bool) - // Get each binding - for _, b := range bindings { - // Initialize members map - if _, ok := bm[b.Role]; !ok { - bm[b.Role] = make(map[string]bool) - } - // Get each member (user/principal) for the binding - for _, m := range b.Members { - // Add the member - bm[b.Role][m] = true - } - } - return bm -} - -// Merge multiple Bindings such that Bindings with the same Role result in -// a single Binding with combined Members -func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { - bm := rolesToMembersMap(bindings) - rb := make([]*cloudresourcemanager.Binding, 0) - - for role, members := range bm { - var b cloudresourcemanager.Binding - b.Role = role - b.Members = make([]string, 0) - for m, _ := range members { - b.Members = append(b.Members, m) - } - rb = append(rb, &b) - } - - return rb -} diff --git a/resource_google_project_iam_policy.go b/resource_google_project_iam_policy.go new file mode 100644 index 00000000..00890bb6 --- /dev/null +++ b/resource_google_project_iam_policy.go @@ -0,0 +1,417 @@ +package google + +import ( + "encoding/json" + "fmt" + "log" + "sort" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" +) + +func resourceGoogleProjectIamPolicy() *schema.Resource { + return &schema.Resource{ + Create: resourceGoogleProjectIamPolicyCreate, + Read: resourceGoogleProjectIamPolicyRead, + Update: resourceGoogleProjectIamPolicyUpdate, + Delete: resourceGoogleProjectIamPolicyDelete, + + Schema: map[string]*schema.Schema{ + "project": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "policy_data": &schema.Schema{ + Type: schema.TypeString, + Required: true, + DiffSuppressFunc: jsonPolicyDiffSuppress, + }, + "authoritative": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + "etag": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "restore_policy": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "disable_project": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + }, + }, + } +} + +func resourceGoogleProjectIamPolicyCreate(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + pid := d.Get("project").(string) + // Get the policy in the template + p, err := getResourceIamPolicy(d) + if err != nil { + return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err) + } + + // An authoritative policy is applied without regard for any existing IAM + // policy. + if v, ok := d.GetOk("authoritative"); ok && v.(bool) { + log.Printf("[DEBUG] Setting authoritative IAM policy for project %q", pid) + err := setProjectIamPolicy(p, config, pid) + if err != nil { + return err + } + } else { + log.Printf("[DEBUG] Setting non-authoritative IAM policy for project %q", pid) + // This is a non-authoritative policy, meaning it should be merged with + // any existing policy + ep, err := getProjectIamPolicy(pid, config) + if err != nil { + return err + } + + // First, subtract the policy defined in the template from the + // current policy in the project, and save the result. This will + // allow us to restore the original policy at some point (which + // assumes that Terraform owns any common policy that exists in + // the template and project at create time. + rp := subtractIamPolicy(ep, p) + rps, err := json.Marshal(rp) + if err != nil { + return fmt.Errorf("Error marshaling restorable IAM policy: %v", err) + } + d.Set("restore_policy", string(rps)) + + // Merge the policies together + mb := mergeBindings(append(p.Bindings, rp.Bindings...)) + ep.Bindings = mb + if err = setProjectIamPolicy(ep, config, pid); err != nil { + return fmt.Errorf("Error applying IAM policy to project: %v", err) + } + } + d.SetId(pid) + return resourceGoogleProjectIamPolicyRead(d, meta) +} + +func resourceGoogleProjectIamPolicyRead(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG]: Reading google_project_iam_policy") + config := meta.(*Config) + pid := d.Get("project").(string) + + p, err := getProjectIamPolicy(pid, config) + if err != nil { + return err + } + + var bindings []*cloudresourcemanager.Binding + if v, ok := d.GetOk("restore_policy"); ok { + var restored cloudresourcemanager.Policy + // if there's a restore policy, subtract it from the policy_data + err := json.Unmarshal([]byte(v.(string)), &restored) + if err != nil { + return fmt.Errorf("Error unmarshaling restorable IAM policy: %v", err) + } + subtracted := subtractIamPolicy(p, &restored) + bindings = subtracted.Bindings + } else { + bindings = p.Bindings + } + // we only marshal the bindings, because only the bindings get set in the config + pBytes, err := json.Marshal(&cloudresourcemanager.Policy{Bindings: bindings}) + if err != nil { + return fmt.Errorf("Error marshaling IAM policy: %v", err) + } + log.Printf("[DEBUG]: Setting etag=%s", p.Etag) + d.Set("etag", p.Etag) + d.Set("policy_data", string(pBytes)) + return nil +} + +func resourceGoogleProjectIamPolicyUpdate(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG]: Updating google_project_iam_policy") + config := meta.(*Config) + pid := d.Get("project").(string) + + // Get the policy in the template + p, err := getResourceIamPolicy(d) + if err != nil { + return fmt.Errorf("Could not get valid 'policy_data' from resource: %v", err) + } + pBytes, _ := json.Marshal(p) + log.Printf("[DEBUG] Got policy from config: %s", string(pBytes)) + + // An authoritative policy is applied without regard for any existing IAM + // policy. + if v, ok := d.GetOk("authoritative"); ok && v.(bool) { + log.Printf("[DEBUG] Updating authoritative IAM policy for project %q", pid) + err := setProjectIamPolicy(p, config, pid) + if err != nil { + return fmt.Errorf("Error setting project IAM policy: %v", err) + } + d.Set("restore_policy", "") + } else { + log.Printf("[DEBUG] Updating non-authoritative IAM policy for project %q", pid) + // Get the previous policy from state + pp, err := getPrevResourceIamPolicy(d) + if err != nil { + return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err) + } + ppBytes, _ := json.Marshal(pp) + log.Printf("[DEBUG] Got previous version of changed project IAM policy: %s", string(ppBytes)) + + // Get the existing IAM policy from the API + ep, err := getProjectIamPolicy(pid, config) + if err != nil { + return fmt.Errorf("Error retrieving IAM policy from project API: %v", err) + } + epBytes, _ := json.Marshal(ep) + log.Printf("[DEBUG] Got existing version of changed IAM policy from project API: %s", string(epBytes)) + + // Subtract the previous and current policies from the policy retrieved from the API + rp := subtractIamPolicy(ep, pp) + rpBytes, _ := json.Marshal(rp) + log.Printf("[DEBUG] After subtracting the previous policy from the existing policy, remaining policies: %s", string(rpBytes)) + rp = subtractIamPolicy(rp, p) + rpBytes, _ = json.Marshal(rp) + log.Printf("[DEBUG] After subtracting the remaining policies from the config policy, remaining policies: %s", string(rpBytes)) + rps, err := json.Marshal(rp) + if err != nil { + return fmt.Errorf("Error marhsaling restorable IAM policy: %v", err) + } + d.Set("restore_policy", string(rps)) + + // Merge the policies together + mb := mergeBindings(append(p.Bindings, rp.Bindings...)) + ep.Bindings = mb + if err = setProjectIamPolicy(ep, config, pid); err != nil { + return fmt.Errorf("Error applying IAM policy to project: %v", err) + } + } + + return resourceGoogleProjectIamPolicyRead(d, meta) +} + +func resourceGoogleProjectIamPolicyDelete(d *schema.ResourceData, meta interface{}) error { + log.Printf("[DEBUG]: Deleting google_project_iam_policy") + config := meta.(*Config) + pid := d.Get("project").(string) + + // Get the existing IAM policy from the API + ep, err := getProjectIamPolicy(pid, config) + if err != nil { + return fmt.Errorf("Error retrieving IAM policy from project API: %v", err) + } + // Deleting an authoritative policy will leave the project with no policy, + // and unaccessible by anyone without org-level privs. For this reason, the + // "disable_project" property must be set to true, forcing the user to ack + // this outcome + if v, ok := d.GetOk("authoritative"); ok && v.(bool) { + if v, ok := d.GetOk("disable_project"); !ok || !v.(bool) { + return fmt.Errorf("You must set 'disable_project' to true before deleting an authoritative IAM policy") + } + ep.Bindings = make([]*cloudresourcemanager.Binding, 0) + + } else { + // A non-authoritative policy should set the policy to the value of "restore_policy" in state + // Get the previous policy from state + rp, err := getRestoreIamPolicy(d) + if err != nil { + return fmt.Errorf("Error retrieving previous version of changed project IAM policy: %v", err) + } + ep.Bindings = rp.Bindings + } + if err = setProjectIamPolicy(ep, config, pid); err != nil { + return fmt.Errorf("Error applying IAM policy to project: %v", err) + } + d.SetId("") + return nil +} + +// Subtract all bindings in policy b from policy a, and return the result +func subtractIamPolicy(a, b *cloudresourcemanager.Policy) *cloudresourcemanager.Policy { + am := rolesToMembersMap(a.Bindings) + + for _, b := range b.Bindings { + if _, ok := am[b.Role]; ok { + for _, m := range b.Members { + delete(am[b.Role], m) + } + if len(am[b.Role]) == 0 { + delete(am, b.Role) + } + } + } + a.Bindings = rolesToMembersBinding(am) + return a +} + +func setProjectIamPolicy(policy *cloudresourcemanager.Policy, config *Config, pid string) error { + // Apply the policy + pbytes, _ := json.Marshal(policy) + log.Printf("[DEBUG] Setting policy %#v for project: %s", string(pbytes), pid) + _, err := config.clientResourceManager.Projects.SetIamPolicy(pid, + &cloudresourcemanager.SetIamPolicyRequest{Policy: policy}).Do() + + if err != nil { + return fmt.Errorf("Error applying IAM policy for project %q. Policy is %+s, error is %s", pid, policy, err) + } + return nil +} + +// Get a cloudresourcemanager.Policy from a schema.ResourceData +func getResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) { + ps := d.Get("policy_data").(string) + // The policy string is just a marshaled cloudresourcemanager.Policy. + policy := &cloudresourcemanager.Policy{} + if err := json.Unmarshal([]byte(ps), policy); err != nil { + return nil, fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err) + } + return policy, nil +} + +// Get the previous cloudresourcemanager.Policy from a schema.ResourceData if the +// resource has changed +func getPrevResourceIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) { + var policy *cloudresourcemanager.Policy = &cloudresourcemanager.Policy{} + if d.HasChange("policy_data") { + v, _ := d.GetChange("policy_data") + if err := json.Unmarshal([]byte(v.(string)), policy); err != nil { + return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err) + } + } + return policy, nil +} + +// Get the restore_policy that can be used to restore a project's IAM policy to its +// state before it was adopted into Terraform +func getRestoreIamPolicy(d *schema.ResourceData) (*cloudresourcemanager.Policy, error) { + if v, ok := d.GetOk("restore_policy"); ok { + policy := &cloudresourcemanager.Policy{} + if err := json.Unmarshal([]byte(v.(string)), policy); err != nil { + return nil, fmt.Errorf("Could not unmarshal previous policy %s:\n: %v", v, err) + } + return policy, nil + } + return nil, fmt.Errorf("Resource does not have a 'restore_policy' attribute defined.") +} + +// Retrieve the existing IAM Policy for a Project +func getProjectIamPolicy(project string, config *Config) (*cloudresourcemanager.Policy, error) { + p, err := config.clientResourceManager.Projects.GetIamPolicy(project, + &cloudresourcemanager.GetIamPolicyRequest{}).Do() + + if err != nil { + return nil, fmt.Errorf("Error retrieving IAM policy for project %q: %s", project, err) + } + return p, nil +} + +// Convert a map of roles->members to a list of Binding +func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager.Binding { + bindings := make([]*cloudresourcemanager.Binding, 0) + for role, members := range m { + b := cloudresourcemanager.Binding{ + Role: role, + Members: make([]string, 0), + } + for m, _ := range members { + b.Members = append(b.Members, m) + } + bindings = append(bindings, &b) + } + return bindings +} + +// Map a role to a map of members, allowing easy merging of multiple bindings. +func rolesToMembersMap(bindings []*cloudresourcemanager.Binding) map[string]map[string]bool { + bm := make(map[string]map[string]bool) + // Get each binding + for _, b := range bindings { + // Initialize members map + if _, ok := bm[b.Role]; !ok { + bm[b.Role] = make(map[string]bool) + } + // Get each member (user/principal) for the binding + for _, m := range b.Members { + // Add the member + bm[b.Role][m] = true + } + } + return bm +} + +// Merge multiple Bindings such that Bindings with the same Role result in +// a single Binding with combined Members +func mergeBindings(bindings []*cloudresourcemanager.Binding) []*cloudresourcemanager.Binding { + bm := rolesToMembersMap(bindings) + rb := make([]*cloudresourcemanager.Binding, 0) + + for role, members := range bm { + var b cloudresourcemanager.Binding + b.Role = role + b.Members = make([]string, 0) + for m, _ := range members { + b.Members = append(b.Members, m) + } + rb = append(rb, &b) + } + + return rb +} + +func jsonPolicyDiffSuppress(k, old, new string, d *schema.ResourceData) bool { + var oldPolicy, newPolicy cloudresourcemanager.Policy + if err := json.Unmarshal([]byte(old), &oldPolicy); err != nil { + log.Printf("[ERROR] Could not unmarshal old policy %s: %v", old, err) + return false + } + if err := json.Unmarshal([]byte(new), &newPolicy); err != nil { + log.Printf("[ERROR] Could not unmarshal new policy %s: %v", new, err) + return false + } + if newPolicy.Etag != oldPolicy.Etag { + return false + } + if newPolicy.Version != oldPolicy.Version { + return false + } + if len(newPolicy.Bindings) != len(oldPolicy.Bindings) { + return false + } + sort.Sort(sortableBindings(newPolicy.Bindings)) + sort.Sort(sortableBindings(oldPolicy.Bindings)) + for pos, newBinding := range newPolicy.Bindings { + oldBinding := oldPolicy.Bindings[pos] + if oldBinding.Role != newBinding.Role { + return false + } + if len(oldBinding.Members) != len(newBinding.Members) { + return false + } + sort.Strings(oldBinding.Members) + sort.Strings(newBinding.Members) + for i, newMember := range newBinding.Members { + oldMember := oldBinding.Members[i] + if newMember != oldMember { + return false + } + } + } + return true +} + +type sortableBindings []*cloudresourcemanager.Binding + +func (b sortableBindings) Len() int { + return len(b) +} +func (b sortableBindings) Swap(i, j int) { + b[i], b[j] = b[j], b[i] +} +func (b sortableBindings) Less(i, j int) bool { + return b[i].Role < b[j].Role +} diff --git a/resource_google_project_iam_policy_test.go b/resource_google_project_iam_policy_test.go new file mode 100644 index 00000000..57e9a296 --- /dev/null +++ b/resource_google_project_iam_policy_test.go @@ -0,0 +1,626 @@ +package google + +import ( + "encoding/json" + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/cloudresourcemanager/v1" +) + +func TestSubtractIamPolicy(t *testing.T) { + table := []struct { + a *cloudresourcemanager.Policy + b *cloudresourcemanager.Policy + expect cloudresourcemanager.Policy + }{ + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "3", + "4", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + }, + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{}, + }, + }, + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "2", + }, + }, + }, + }, + }, + { + a: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + b: &cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{ + { + Role: "a", + Members: []string{ + "1", + "2", + "3", + }, + }, + { + Role: "b", + Members: []string{ + "1", + "2", + "3", + }, + }, + }, + }, + expect: cloudresourcemanager.Policy{ + Bindings: []*cloudresourcemanager.Binding{}, + }, + }, + } + + for _, test := range table { + c := subtractIamPolicy(test.a, test.b) + sort.Sort(sortableBindings(c.Bindings)) + for i, _ := range c.Bindings { + sort.Strings(c.Bindings[i].Members) + } + + if !reflect.DeepEqual(derefBindings(c.Bindings), derefBindings(test.expect.Bindings)) { + t.Errorf("\ngot %+v\nexpected %+v", derefBindings(c.Bindings), derefBindings(test.expect.Bindings)) + } + } +} + +// Test that an IAM policy can be applied to a project +func TestAccGoogleProjectIamPolicy_basic(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project + resource.TestStep{ + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccGoogleProjectExistingPolicy(pid), + ), + }, + // Apply an IAM policy from a data source. The application + // merges policies, so we validate the expected state. + resource.TestStep{ + Config: testAccGoogleProjectAssociatePolicyBasic(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectIamPolicyIsMerged("google_project_iam_policy.acceptance", "data.google_iam_policy.admin", pid), + ), + }, + // Finally, remove the custom IAM policy from config and apply, then + // confirm that the project is in its original state. + resource.TestStep{ + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccGoogleProjectExistingPolicy(pid), + ), + }, + }, + }) +} + +func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Get the project resource + project, ok := s.RootModule().Resources[projectRes] + if !ok { + return fmt.Errorf("Not found: %s", projectRes) + } + // The project ID should match the config's project ID + if project.Primary.ID != pid { + return fmt.Errorf("Expected project %q to match ID %q in state", pid, project.Primary.ID) + } + + var projectP, policyP cloudresourcemanager.Policy + // The project should have a policy + ps, ok := project.Primary.Attributes["policy_data"] + if !ok { + return fmt.Errorf("Project resource %q did not have a 'policy_data' attribute. Attributes were %#v", project.Primary.Attributes["id"], project.Primary.Attributes) + } + if err := json.Unmarshal([]byte(ps), &projectP); err != nil { + return fmt.Errorf("Could not unmarshal %s:\n: %v", ps, err) + } + + // The data policy resource should have a policy + policy, ok := s.RootModule().Resources[policyRes] + if !ok { + return fmt.Errorf("Not found: %s", policyRes) + } + ps, ok = policy.Primary.Attributes["policy_data"] + if !ok { + return fmt.Errorf("Data policy resource %q did not have a 'policy_data' attribute. Attributes were %#v", policy.Primary.Attributes["id"], project.Primary.Attributes) + } + if err := json.Unmarshal([]byte(ps), &policyP); err != nil { + return err + } + + // The bindings in both policies should be identical + sort.Sort(sortableBindings(projectP.Bindings)) + sort.Sort(sortableBindings(policyP.Bindings)) + if !reflect.DeepEqual(derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) { + return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) + } + + // Merge the project policy in Terraform state with the policy the project had before the config was applied + expected := make([]*cloudresourcemanager.Binding, 0) + expected = append(expected, originalPolicy.Bindings...) + expected = append(expected, projectP.Bindings...) + expectedM := mergeBindings(expected) + + // Retrieve the actual policy from the project + c := testAccProvider.Meta().(*Config) + actual, err := getProjectIamPolicy(pid, c) + if err != nil { + return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err) + } + actualM := mergeBindings(actual.Bindings) + + sort.Sort(sortableBindings(actualM)) + sort.Sort(sortableBindings(expectedM)) + // The bindings should match, indicating the policy was successfully applied and merged + if !reflect.DeepEqual(derefBindings(actualM), derefBindings(expectedM)) { + return fmt.Errorf("Actual and expected project policies do not match: actual policy is %+v, expected policy is %+v", derefBindings(actualM), derefBindings(expectedM)) + } + + return nil + } +} + +func TestIamRolesToMembersBinding(t *testing.T) { + table := []struct { + expect []*cloudresourcemanager.Binding + input map[string]map[string]bool + }{ + { + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + }, + input: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + }, + input: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + expect: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{}, + }, + }, + input: map[string]map[string]bool{ + "role-1": map[string]bool{}, + }, + }, + } + + for _, test := range table { + got := rolesToMembersBinding(test.input) + + sort.Sort(sortableBindings(got)) + for i, _ := range got { + sort.Strings(got[i].Members) + } + + if !reflect.DeepEqual(derefBindings(got), derefBindings(test.expect)) { + t.Errorf("got %+v, expected %+v", derefBindings(got), derefBindings(test.expect)) + } + } +} +func TestIamRolesToMembersMap(t *testing.T) { + table := []struct { + input []*cloudresourcemanager.Binding + expect map[string]map[string]bool + }{ + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + }, + expect: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + "member-1", + "member-2", + }, + }, + }, + expect: map[string]map[string]bool{ + "role-1": map[string]bool{ + "member-1": true, + "member-2": true, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + }, + }, + expect: map[string]map[string]bool{ + "role-1": map[string]bool{}, + }, + }, + } + + for _, test := range table { + got := rolesToMembersMap(test.input) + if !reflect.DeepEqual(got, test.expect) { + t.Errorf("got %+v, expected %+v", got, test.expect) + } + } +} + +func TestIamMergeBindings(t *testing.T) { + table := []struct { + input []*cloudresourcemanager.Binding + expect []cloudresourcemanager.Binding + }{ + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + }, + }, + { + Role: "role-1", + Members: []string{ + "member-3", + }, + }, + }, + expect: []cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + "member-3", + }, + }, + }, + }, + { + input: []*cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-3", + "member-4", + }, + }, + { + Role: "role-1", + Members: []string{ + "member-2", + "member-1", + }, + }, + { + Role: "role-2", + Members: []string{ + "member-1", + }, + }, + { + Role: "role-1", + Members: []string{ + "member-5", + }, + }, + { + Role: "role-3", + Members: []string{ + "member-1", + }, + }, + { + Role: "role-2", + Members: []string{ + "member-2", + }, + }, + }, + expect: []cloudresourcemanager.Binding{ + { + Role: "role-1", + Members: []string{ + "member-1", + "member-2", + "member-3", + "member-4", + "member-5", + }, + }, + { + Role: "role-2", + Members: []string{ + "member-1", + "member-2", + }, + }, + { + Role: "role-3", + Members: []string{ + "member-1", + }, + }, + }, + }, + } + + for _, test := range table { + got := mergeBindings(test.input) + sort.Sort(sortableBindings(got)) + for i, _ := range got { + sort.Strings(got[i].Members) + } + + if !reflect.DeepEqual(derefBindings(got), test.expect) { + t.Errorf("\ngot %+v\nexpected %+v", derefBindings(got), test.expect) + } + } +} + +func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding { + db := make([]cloudresourcemanager.Binding, len(b)) + + for i, v := range b { + db[i] = *v + sort.Strings(db[i].Members) + } + return db +} + +// Confirm that a project has an IAM policy with at least 1 binding +func testAccGoogleProjectExistingPolicy(pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + c := testAccProvider.Meta().(*Config) + var err error + originalPolicy, err = getProjectIamPolicy(pid, c) + if err != nil { + return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", pid, err) + } + if len(originalPolicy.Bindings) == 0 { + return fmt.Errorf("Refuse to run test against project with zero IAM Bindings. This is likely an error in the test code that is not properly identifying the IAM policy of a project.") + } + return nil + } +} + +func testAccGoogleProjectAssociatePolicyBasic(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} +resource "google_project_iam_policy" "acceptance" { + project = "${google_project.acceptance.id}" + policy_data = "${data.google_iam_policy.admin.policy_data}" +} +data "google_iam_policy" "admin" { + binding { + role = "roles/storage.objectViewer" + members = [ + "user:evanbrown@google.com", + ] + } + binding { + role = "roles/compute.instanceAdmin" + members = [ + "user:evanbrown@google.com", + "user:evandbrown@gmail.com", + ] + } +} +`, pid, name, org) +} + +func testAccGoogleProject_create(pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +}`, pid, name, org) +} diff --git a/resource_google_project_migrate.go b/resource_google_project_migrate.go new file mode 100644 index 00000000..09fccd31 --- /dev/null +++ b/resource_google_project_migrate.go @@ -0,0 +1,47 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/terraform" +) + +func resourceGoogleProjectMigrateState(v int, s *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) { + if s.Empty() { + log.Println("[DEBUG] Empty InstanceState; nothing to migrate.") + return s, nil + } + + switch v { + case 0: + log.Println("[INFO] Found Google Project State v0; migrating to v1") + s, err := migrateGoogleProjectStateV0toV1(s, meta.(*Config)) + if err != nil { + return s, err + } + return s, nil + default: + return s, fmt.Errorf("Unexpected schema version: %d", v) + } +} + +// This migration adjusts google_project resources to include several additional attributes +// required to support project creation/deletion that was added in V1. +func migrateGoogleProjectStateV0toV1(s *terraform.InstanceState, config *Config) (*terraform.InstanceState, error) { + log.Printf("[DEBUG] Attributes before migration: %#v", s.Attributes) + + s.Attributes["skip_delete"] = "true" + s.Attributes["project_id"] = s.ID + + if s.Attributes["policy_data"] != "" { + p, err := getProjectIamPolicy(s.ID, config) + if err != nil { + return s, fmt.Errorf("Could not retrieve project's IAM policy while attempting to migrate state from V0 to V1: %v", err) + } + s.Attributes["policy_etag"] = p.Etag + } + + log.Printf("[DEBUG] Attributes after migration: %#v", s.Attributes) + return s, nil +} diff --git a/resource_google_project_migrate_test.go b/resource_google_project_migrate_test.go new file mode 100644 index 00000000..8aeff364 --- /dev/null +++ b/resource_google_project_migrate_test.go @@ -0,0 +1,70 @@ +package google + +import ( + "testing" + + "github.com/hashicorp/terraform/terraform" +) + +func TestGoogleProjectMigrateState(t *testing.T) { + cases := map[string]struct { + StateVersion int + Attributes map[string]string + Expected map[string]string + Meta interface{} + }{ + "deprecate policy_data and support creation/deletion": { + StateVersion: 0, + Attributes: map[string]string{}, + Expected: map[string]string{ + "project_id": "test-project", + "skip_delete": "true", + }, + Meta: &Config{}, + }, + } + + for tn, tc := range cases { + is := &terraform.InstanceState{ + ID: "test-project", + Attributes: tc.Attributes, + } + is, err := resourceGoogleProjectMigrateState( + tc.StateVersion, is, tc.Meta) + + if err != nil { + t.Fatalf("bad: %s, err: %#v", tn, err) + } + + for k, v := range tc.Expected { + if is.Attributes[k] != v { + t.Fatalf( + "bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v", + tn, k, v, k, is.Attributes[k], is.Attributes) + } + } + } +} + +func TestGoogleProjectMigrateState_empty(t *testing.T) { + var is *terraform.InstanceState + var meta *Config + + // should handle nil + is, err := resourceGoogleProjectMigrateState(0, is, meta) + + if err != nil { + t.Fatalf("err: %#v", err) + } + if is != nil { + t.Fatalf("expected nil instancestate, got: %#v", is) + } + + // should handle non-nil but empty + is = &terraform.InstanceState{} + is, err = resourceGoogleProjectMigrateState(0, is, meta) + + if err != nil { + t.Fatalf("err: %#v", err) + } +} diff --git a/resource_google_project_services.go b/resource_google_project_services.go new file mode 100644 index 00000000..84bcd95a --- /dev/null +++ b/resource_google_project_services.go @@ -0,0 +1,214 @@ +package google + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/servicemanagement/v1" +) + +func resourceGoogleProjectServices() *schema.Resource { + return &schema.Resource{ + Create: resourceGoogleProjectServicesCreate, + Read: resourceGoogleProjectServicesRead, + Update: resourceGoogleProjectServicesUpdate, + Delete: resourceGoogleProjectServicesDelete, + + 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, + }, + }, + } +} + +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) + 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) + if err != nil { + return err + } + + 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) + 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") + 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) + } + } + + for k, _ := range cfgMap { + err := enableService(k, 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) ([]string, error) { + apiServices := make([]string, 0) + // Get services from the API + svcResp, err := config.clientServiceMan.Services.List().ConsumerId("project:" + pid).Do() + if err != nil { + return apiServices, err + } + for _, v := range svcResp.Services { + apiServices = append(apiServices, v.ServiceName) + } + return apiServices, nil +} + +func enableService(s, pid string, config *Config) error { + esr := newEnableServiceRequest(pid) + sop, err := config.clientServiceMan.Services.Enable(s, esr).Do() + if err != nil { + return fmt.Errorf("Error enabling service %q for project %q: %v", s, pid, err) + } + // Wait for the operation to complete + waitErr := serviceManagementOperationWait(config, sop, "api to enable") + if waitErr != nil { + return waitErr + } + return nil +} +func disableService(s, pid string, config *Config) error { + dsr := newDisableServiceRequest(pid) + sop, err := config.clientServiceMan.Services.Disable(s, dsr).Do() + if err != nil { + return fmt.Errorf("Error disabling service %q for project %q: %v", s, pid, err) + } + // Wait for the operation to complete + waitErr := serviceManagementOperationWait(config, sop, "api to disable") + if waitErr != nil { + return waitErr + } + return nil +} + +func newEnableServiceRequest(pid string) *servicemanagement.EnableServiceRequest { + return &servicemanagement.EnableServiceRequest{ConsumerId: "project:" + pid} +} + +func newDisableServiceRequest(pid string) *servicemanagement.DisableServiceRequest { + return &servicemanagement.DisableServiceRequest{ConsumerId: "project:" + pid} +} + +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 +} diff --git a/resource_google_project_services_test.go b/resource_google_project_services_test.go new file mode 100644 index 00000000..dff073b2 --- /dev/null +++ b/resource_google_project_services_test.go @@ -0,0 +1,178 @@ +package google + +import ( + "bytes" + "fmt" + "log" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "google.golang.org/api/servicemanagement/v1" +) + +// Test that services can be enabled and disabled on a project +func TestAccGoogleProjectServices_basic(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + services1 := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"} + services2 := []string{"cloudresourcemanager.googleapis.com"} + oobService := "iam.googleapis.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project with some services + resource.TestStep{ + Config: testAccGoogleProjectAssociateServicesBasic(services1, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services1, pid), + ), + }, + // Update services to remove one + resource.TestStep{ + Config: testAccGoogleProjectAssociateServicesBasic(services2, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services2, pid), + ), + }, + // Add a service out-of-band and ensure it is removed + resource.TestStep{ + PreConfig: func() { + config := testAccProvider.Meta().(*Config) + enableService(oobService, pid, config) + }, + Config: testAccGoogleProjectAssociateServicesBasic(services2, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services2, pid), + ), + }, + }, + }) +} + +// Test that services are authoritative when a project has existing +// sevices not represented in config +func TestAccGoogleProjectServices_authoritative(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + services := []string{"cloudresourcemanager.googleapis.com"} + oobService := "iam.googleapis.com" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project with no services + resource.TestStep{ + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // Add a service out-of-band, then apply a config that creates a service. + // It should remove the out-of-band service. + resource.TestStep{ + PreConfig: func() { + config := testAccProvider.Meta().(*Config) + enableService(oobService, pid, config) + }, + Config: testAccGoogleProjectAssociateServicesBasic(services, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + +// Test that services are authoritative when a project has existing +// sevices, some which are represented in the config and others +// that are not +func TestAccGoogleProjectServices_authoritative2(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) + oobServices := []string{"iam.googleapis.com", "cloudresourcemanager.googleapis.com"} + services := []string{"iam.googleapis.com"} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Create a new project with no services + resource.TestStep{ + Config: testAccGoogleProject_create(pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testAccCheckGoogleProjectExists("google_project.acceptance", pid), + ), + }, + // Add a service out-of-band, then apply a config that creates a service. + // It should remove the out-of-band service. + resource.TestStep{ + PreConfig: func() { + config := testAccProvider.Meta().(*Config) + for _, s := range oobServices { + enableService(s, pid, config) + } + }, + Config: testAccGoogleProjectAssociateServicesBasic(services, pid, pname, org), + Check: resource.ComposeTestCheckFunc( + testProjectServicesMatch(services, pid), + ), + }, + }, + }) +} + +func testAccGoogleProjectAssociateServicesBasic(services []string, pid, name, org string) string { + return fmt.Sprintf(` +resource "google_project" "acceptance" { + project_id = "%s" + name = "%s" + org_id = "%s" +} +resource "google_project_services" "acceptance" { + project = "${google_project.acceptance.project_id}" + services = [%s] +} +`, pid, name, org, testStringsToString(services)) +} + +func testProjectServicesMatch(services []string, pid string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + + apiServices, err := getApiServices(pid, config) + if err != nil { + return fmt.Errorf("Error listing services for project %q: %v", pid, err) + } + + sort.Strings(services) + sort.Strings(apiServices) + if !reflect.DeepEqual(services, apiServices) { + return fmt.Errorf("Services in config (%v) do not exactly match services returned by API (%v)", services, apiServices) + } + + return nil + } +} + +func testStringsToString(s []string) string { + var b bytes.Buffer + for i, v := range s { + b.WriteString(fmt.Sprintf("\"%s\"", v)) + if i < len(s)-1 { + b.WriteString(",") + } + } + r := b.String() + log.Printf("[DEBUG]: Converted list of strings to %s", r) + return b.String() +} + +func testManagedServicesToString(svcs []*servicemanagement.ManagedService) string { + var b bytes.Buffer + for _, s := range svcs { + b.WriteString(s.ServiceName) + } + return b.String() +} diff --git a/resource_google_project_test.go b/resource_google_project_test.go index 161b6b4e..aa3c03c5 100644 --- a/resource_google_project_test.go +++ b/resource_google_project_test.go @@ -1,24 +1,23 @@ package google import ( - "encoding/json" "fmt" "os" - "reflect" - "sort" "testing" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" "google.golang.org/api/cloudresourcemanager/v1" ) var ( - projectId = multiEnvSearch([]string{ - "GOOGLE_PROJECT", - "GCLOUD_PROJECT", - "CLOUDSDK_CORE_PROJECT", + org = multiEnvSearch([]string{ + "GOOGLE_ORG", }) + + pname = "Terraform Acceptance Tests" + originalPolicy *cloudresourcemanager.Policy ) func multiEnvSearch(ks []string) string { @@ -30,77 +29,26 @@ func multiEnvSearch(ks []string) string { return "" } -// Test that a Project resource can be created and destroyed -func TestAccGoogleProject_associate(t *testing.T) { +// Test that a Project resource can be created and an IAM policy +// associated +func TestAccGoogleProject_create(t *testing.T) { + pid := "terraform-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, Steps: []resource.TestStep{ + // This step imports an existing project resource.TestStep{ - Config: fmt.Sprintf(testAccGoogleProject_basic, projectId), + Config: testAccGoogleProject_create(pid, pname, org), Check: resource.ComposeTestCheckFunc( - testAccCheckGoogleProjectExists("google_project.acceptance"), + testAccCheckGoogleProjectExists("google_project.acceptance", pid), ), }, }, }) } -// Test that a Project resource can be created, an IAM Policy -// associated with it, and then destroyed -func TestAccGoogleProject_iamPolicy1(t *testing.T) { - var policy *cloudresourcemanager.Policy - resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, - CheckDestroy: testAccCheckGoogleProjectDestroy, - Steps: []resource.TestStep{ - // First step inventories the project's existing IAM policy - resource.TestStep{ - Config: fmt.Sprintf(testAccGoogleProject_basic, projectId), - Check: resource.ComposeTestCheckFunc( - testAccGoogleProjectExistingPolicy(policy), - ), - }, - // Second step applies an IAM policy from a data source. The application - // merges policies, so we validate the expected state. - resource.TestStep{ - Config: fmt.Sprintf(testAccGoogleProject_policy1, projectId), - Check: resource.ComposeTestCheckFunc( - testAccCheckGoogleProjectExists("google_project.acceptance"), - testAccCheckGoogleProjectIamPolicyIsMerged("google_project.acceptance", "data.google_iam_policy.admin", policy), - ), - }, - // Finally, remove the custom IAM policy from config and apply, then - // confirm that the project is in its original state. - resource.TestStep{ - Config: fmt.Sprintf(testAccGoogleProject_basic, projectId), - }, - }, - }) -} - -func testAccCheckGoogleProjectDestroy(s *terraform.State) error { - return nil -} - -// Retrieve the existing policy (if any) for a GCP Project -func testAccGoogleProjectExistingPolicy(p *cloudresourcemanager.Policy) resource.TestCheckFunc { - return func(s *terraform.State) error { - c := testAccProvider.Meta().(*Config) - var err error - p, err = getProjectIamPolicy(projectId, c) - if err != nil { - return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", projectId, err) - } - if len(p.Bindings) == 0 { - return fmt.Errorf("Refuse to run test against project with zero IAM Bindings. This is likely an error in the test code that is not properly identifying the IAM policy of a project.") - } - return nil - } -} - -func testAccCheckGoogleProjectExists(r string) resource.TestCheckFunc { +func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[r] if !ok { @@ -111,349 +59,29 @@ func testAccCheckGoogleProjectExists(r string) resource.TestCheckFunc { return fmt.Errorf("No ID is set") } - if rs.Primary.ID != projectId { - return fmt.Errorf("Expected project %q to match ID %q in state", projectId, rs.Primary.ID) + if rs.Primary.ID != pid { + return fmt.Errorf("Expected project %q to match ID %q in state", pid, rs.Primary.ID) } return nil } } -func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes string, original *cloudresourcemanager.Policy) resource.TestCheckFunc { - return func(s *terraform.State) error { - // Get the project resource - project, ok := s.RootModule().Resources[projectRes] - if !ok { - return fmt.Errorf("Not found: %s", projectRes) - } - // The project ID should match the config's project ID - if project.Primary.ID != projectId { - return fmt.Errorf("Expected project %q to match ID %q in state", projectId, project.Primary.ID) - } - - var projectP, policyP cloudresourcemanager.Policy - // The project should have a policy - ps, ok := project.Primary.Attributes["policy_data"] - if !ok { - return fmt.Errorf("Project resource %q did not have a 'policy_data' attribute. Attributes were %#v", project.Primary.Attributes["id"], project.Primary.Attributes) - } - if err := json.Unmarshal([]byte(ps), &projectP); err != nil { - return err - } - - // The data policy resource should have a policy - policy, ok := s.RootModule().Resources[policyRes] - if !ok { - return fmt.Errorf("Not found: %s", policyRes) - } - ps, ok = policy.Primary.Attributes["policy_data"] - if !ok { - return fmt.Errorf("Data policy resource %q did not have a 'policy_data' attribute. Attributes were %#v", policy.Primary.Attributes["id"], project.Primary.Attributes) - } - if err := json.Unmarshal([]byte(ps), &policyP); err != nil { - return err - } - - // The bindings in both policies should be identical - if !reflect.DeepEqual(derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) { - return fmt.Errorf("Project and data source policies do not match: project policy is %+v, data resource policy is %+v", derefBindings(projectP.Bindings), derefBindings(policyP.Bindings)) - } - - // Merge the project policy in Terrafomr state with the policy the project had before the config was applied - expected := make([]*cloudresourcemanager.Binding, 0) - expected = append(expected, original.Bindings...) - expected = append(expected, projectP.Bindings...) - expectedM := mergeBindings(expected) - - // Retrieve the actual policy from the project - c := testAccProvider.Meta().(*Config) - actual, err := getProjectIamPolicy(projectId, c) - if err != nil { - return fmt.Errorf("Failed to retrieve IAM Policy for project %q: %s", projectId, err) - } - actualM := mergeBindings(actual.Bindings) - - // The bindings should match, indicating the policy was successfully applied and merged - if !reflect.DeepEqual(derefBindings(actualM), derefBindings(expectedM)) { - return fmt.Errorf("Actual and expected project policies do not match: actual policy is %+v, expected policy is %+v", derefBindings(actualM), derefBindings(expectedM)) - } - - return nil - } -} - -func TestIamRolesToMembersBinding(t *testing.T) { - table := []struct { - expect []*cloudresourcemanager.Binding - input map[string]map[string]bool - }{ - { - expect: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - }, - }, - }, - input: map[string]map[string]bool{ - "role-1": map[string]bool{ - "member-1": true, - "member-2": true, - }, - }, - }, - { - expect: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - }, - }, - }, - input: map[string]map[string]bool{ - "role-1": map[string]bool{ - "member-1": true, - "member-2": true, - }, - }, - }, - { - expect: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{}, - }, - }, - input: map[string]map[string]bool{ - "role-1": map[string]bool{}, - }, - }, - } - - for _, test := range table { - got := rolesToMembersBinding(test.input) - - sort.Sort(Binding(got)) - for i, _ := range got { - sort.Strings(got[i].Members) - } - - if !reflect.DeepEqual(derefBindings(got), derefBindings(test.expect)) { - t.Errorf("got %+v, expected %+v", derefBindings(got), derefBindings(test.expect)) - } - } -} -func TestIamRolesToMembersMap(t *testing.T) { - table := []struct { - input []*cloudresourcemanager.Binding - expect map[string]map[string]bool - }{ - { - input: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - }, - }, - }, - expect: map[string]map[string]bool{ - "role-1": map[string]bool{ - "member-1": true, - "member-2": true, - }, - }, - }, - { - input: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - "member-1", - "member-2", - }, - }, - }, - expect: map[string]map[string]bool{ - "role-1": map[string]bool{ - "member-1": true, - "member-2": true, - }, - }, - }, - { - input: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - }, - }, - expect: map[string]map[string]bool{ - "role-1": map[string]bool{}, - }, - }, - } - - for _, test := range table { - got := rolesToMembersMap(test.input) - if !reflect.DeepEqual(got, test.expect) { - t.Errorf("got %+v, expected %+v", got, test.expect) - } - } -} - -func TestIamMergeBindings(t *testing.T) { - table := []struct { - input []*cloudresourcemanager.Binding - expect []cloudresourcemanager.Binding - }{ - { - input: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - }, - }, - { - Role: "role-1", - Members: []string{ - "member-3", - }, - }, - }, - expect: []cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - "member-3", - }, - }, - }, - }, - { - input: []*cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-3", - "member-4", - }, - }, - { - Role: "role-1", - Members: []string{ - "member-2", - "member-1", - }, - }, - { - Role: "role-2", - Members: []string{ - "member-1", - }, - }, - { - Role: "role-1", - Members: []string{ - "member-5", - }, - }, - { - Role: "role-3", - Members: []string{ - "member-1", - }, - }, - { - Role: "role-2", - Members: []string{ - "member-2", - }, - }, - }, - expect: []cloudresourcemanager.Binding{ - { - Role: "role-1", - Members: []string{ - "member-1", - "member-2", - "member-3", - "member-4", - "member-5", - }, - }, - { - Role: "role-2", - Members: []string{ - "member-1", - "member-2", - }, - }, - { - Role: "role-3", - Members: []string{ - "member-1", - }, - }, - }, - }, - } - - for _, test := range table { - got := mergeBindings(test.input) - sort.Sort(Binding(got)) - for i, _ := range got { - sort.Strings(got[i].Members) - } - - if !reflect.DeepEqual(derefBindings(got), test.expect) { - t.Errorf("\ngot %+v\nexpected %+v", derefBindings(got), test.expect) - } - } -} - -func derefBindings(b []*cloudresourcemanager.Binding) []cloudresourcemanager.Binding { - db := make([]cloudresourcemanager.Binding, len(b)) - - for i, v := range b { - db[i] = *v - } - return db -} - -type Binding []*cloudresourcemanager.Binding - -func (b Binding) Len() int { - return len(b) -} -func (b Binding) Swap(i, j int) { - b[i], b[j] = b[j], b[i] -} -func (b Binding) Less(i, j int) bool { - return b[i].Role < b[j].Role -} - -var testAccGoogleProject_basic = ` +func testAccGoogleProjectImportExisting(pid string) string { + return fmt.Sprintf(` resource "google_project" "acceptance" { - id = "%v" -}` + project_id = "%s" -var testAccGoogleProject_policy1 = ` +} +`, pid) +} + +func testAccGoogleProjectImportExistingWithIam(pid string) string { + return fmt.Sprintf(` resource "google_project" "acceptance" { - id = "%v" + project_id = "%v" policy_data = "${data.google_iam_policy.admin.policy_data}" } - data "google_iam_policy" "admin" { binding { role = "roles/storage.objectViewer" @@ -468,4 +96,5 @@ data "google_iam_policy" "admin" { "user:evandbrown@gmail.com", ] } -}` +}`, pid) +} diff --git a/resource_google_service_account_test.go b/resource_google_service_account_test.go index ecf01480..6377be39 100644 --- a/resource_google_service_account_test.go +++ b/resource_google_service_account_test.go @@ -9,6 +9,14 @@ import ( "github.com/hashicorp/terraform/terraform" ) +var ( + projectId = multiEnvSearch([]string{ + "GOOGLE_PROJECT", + "GCLOUD_PROJECT", + "CLOUDSDK_CORE_PROJECT", + }) +) + // Test that a service account resource can be created, updated, and destroyed func TestAccGoogleServiceAccount_basic(t *testing.T) { accountId := "a" + acctest.RandString(10) diff --git a/resourcemanager_operation.go b/resourcemanager_operation.go new file mode 100644 index 00000000..32c6d343 --- /dev/null +++ b/resourcemanager_operation.go @@ -0,0 +1,64 @@ +package google + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "google.golang.org/api/cloudresourcemanager/v1" +) + +type ResourceManagerOperationWaiter struct { + Service *cloudresourcemanager.Service + Op *cloudresourcemanager.Operation +} + +func (w *ResourceManagerOperationWaiter) RefreshFunc() resource.StateRefreshFunc { + return func() (interface{}, string, error) { + op, err := w.Service.Operations.Get(w.Op.Name).Do() + + if err != nil { + return nil, "", err + } + + log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name) + + return op, fmt.Sprint(op.Done), nil + } +} + +func (w *ResourceManagerOperationWaiter) Conf() *resource.StateChangeConf { + return &resource.StateChangeConf{ + Pending: []string{"false"}, + Target: []string{"true"}, + Refresh: w.RefreshFunc(), + } +} + +func resourceManagerOperationWait(config *Config, op *cloudresourcemanager.Operation, activity string) error { + return resourceManagerOperationWaitTime(config, op, activity, 4) +} + +func resourceManagerOperationWaitTime(config *Config, op *cloudresourcemanager.Operation, activity string, timeoutMin int) error { + w := &ResourceManagerOperationWaiter{ + Service: config.clientResourceManager, + Op: op, + } + + state := w.Conf() + state.Delay = 10 * time.Second + state.Timeout = time.Duration(timeoutMin) * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for %s: %s", activity, err) + } + + op = opRaw.(*cloudresourcemanager.Operation) + if op.Error != nil { + return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message) + } + + return nil +} diff --git a/serviceman_operation.go b/serviceman_operation.go new file mode 100644 index 00000000..299cd1e8 --- /dev/null +++ b/serviceman_operation.go @@ -0,0 +1,67 @@ +package google + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/resource" + "google.golang.org/api/servicemanagement/v1" +) + +type ServiceManagementOperationWaiter struct { + Service *servicemanagement.APIService + Op *servicemanagement.Operation +} + +func (w *ServiceManagementOperationWaiter) RefreshFunc() resource.StateRefreshFunc { + return func() (interface{}, string, error) { + var op *servicemanagement.Operation + var err error + + op, err = w.Service.Operations.Get(w.Op.Name).Do() + + if err != nil { + return nil, "", err + } + + log.Printf("[DEBUG] Got %v while polling for operation %s's 'done' status", op.Done, w.Op.Name) + + return op, fmt.Sprint(op.Done), nil + } +} + +func (w *ServiceManagementOperationWaiter) Conf() *resource.StateChangeConf { + return &resource.StateChangeConf{ + Pending: []string{"false"}, + Target: []string{"true"}, + Refresh: w.RefreshFunc(), + } +} + +func serviceManagementOperationWait(config *Config, op *servicemanagement.Operation, activity string) error { + return serviceManagementOperationWaitTime(config, op, activity, 4) +} + +func serviceManagementOperationWaitTime(config *Config, op *servicemanagement.Operation, activity string, timeoutMin int) error { + w := &ServiceManagementOperationWaiter{ + Service: config.clientServiceMan, + Op: op, + } + + state := w.Conf() + state.Delay = 10 * time.Second + state.Timeout = time.Duration(timeoutMin) * time.Minute + state.MinTimeout = 2 * time.Second + opRaw, err := state.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for %s: %s", activity, err) + } + + op = opRaw.(*servicemanagement.Operation) + if op.Error != nil { + return fmt.Errorf("Error code %v, message: %s", op.Error.Code, op.Error.Message) + } + + return nil +}