providers/google: Support managing projects

Add support for creating, updating, and deleting projects, as well as
their enabled services and their IAM policies.

Various concessions were made for backwards compatibility, and will be
removed in 0.9 or 0.10.
This commit is contained in:
Evan Brown 2016-11-22 22:55:40 -08:00 committed by Paddy
parent e81d096699
commit b9e9e777c8
21 changed files with 12879 additions and 548 deletions

View File

@ -19,6 +19,7 @@ import (
"google.golang.org/api/dns/v1" "google.golang.org/api/dns/v1"
"google.golang.org/api/iam/v1" "google.golang.org/api/iam/v1"
"google.golang.org/api/pubsub/v1" "google.golang.org/api/pubsub/v1"
"google.golang.org/api/servicemanagement/v1"
"google.golang.org/api/sqladmin/v1beta4" "google.golang.org/api/sqladmin/v1beta4"
"google.golang.org/api/storage/v1" "google.golang.org/api/storage/v1"
) )
@ -38,6 +39,7 @@ type Config struct {
clientStorage *storage.Service clientStorage *storage.Service
clientSqlAdmin *sqladmin.Service clientSqlAdmin *sqladmin.Service
clientIAM *iam.Service clientIAM *iam.Service
clientServiceMan *servicemanagement.APIService
} }
func (c *Config) loadAndValidate() error { func (c *Config) loadAndValidate() error {
@ -130,27 +132,34 @@ func (c *Config) loadAndValidate() error {
} }
c.clientSqlAdmin.UserAgent = userAgent c.clientSqlAdmin.UserAgent = userAgent
log.Printf("[INFO] Instatiating Google Pubsub Client...") log.Printf("[INFO] Instantiating Google Pubsub Client...")
c.clientPubsub, err = pubsub.New(client) c.clientPubsub, err = pubsub.New(client)
if err != nil { if err != nil {
return err return err
} }
c.clientPubsub.UserAgent = userAgent 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) c.clientResourceManager, err = cloudresourcemanager.New(client)
if err != nil { if err != nil {
return err return err
} }
c.clientResourceManager.UserAgent = userAgent 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) c.clientIAM, err = iam.New(client)
if err != nil { if err != nil {
return err return err
} }
c.clientIAM.UserAgent = userAgent 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 return nil
} }

View File

@ -4,12 +4,14 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
) )
func TestAccGoogleProject_importBasic(t *testing.T) { func TestAccGoogleProject_importBasic(t *testing.T) {
resourceName := "google_project.acceptance" 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{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, 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)
}

View File

@ -96,6 +96,8 @@ func Provider() terraform.ResourceProvider {
"google_sql_database_instance": resourceSqlDatabaseInstance(), "google_sql_database_instance": resourceSqlDatabaseInstance(),
"google_sql_user": resourceSqlUser(), "google_sql_user": resourceSqlUser(),
"google_project": resourceGoogleProject(), "google_project": resourceGoogleProject(),
"google_project_iam_policy": resourceGoogleProjectIamPolicy(),
"google_project_services": resourceGoogleProjectServices(),
"google_pubsub_topic": resourcePubsubTopic(), "google_pubsub_topic": resourcePubsubTopic(),
"google_pubsub_subscription": resourcePubsubSubscription(), "google_pubsub_subscription": resourcePubsubSubscription(),
"google_service_account": resourceGoogleServiceAccount(), "google_service_account": resourceGoogleServiceAccount(),

View File

@ -13,9 +13,7 @@ import (
) )
// resourceGoogleProject returns a *schema.Resource that allows a customer // resourceGoogleProject returns a *schema.Resource that allows a customer
// to declare a Google Cloud Project resource. // // to declare a Google Cloud Project resource.
// Only the 'policy' property of a project may be updated. All other properties
// are computed.
// //
// This example shows a project with a policy declared in config: // This example shows a project with a policy declared in config:
// //
@ -25,28 +23,65 @@ import (
// } // }
func resourceGoogleProject() *schema.Resource { func resourceGoogleProject() *schema.Resource {
return &schema.Resource{ return &schema.Resource{
SchemaVersion: 1,
Create: resourceGoogleProjectCreate, Create: resourceGoogleProjectCreate,
Read: resourceGoogleProjectRead, Read: resourceGoogleProjectRead,
Update: resourceGoogleProjectUpdate, Update: resourceGoogleProjectUpdate,
Delete: resourceGoogleProjectDelete, Delete: resourceGoogleProjectDelete,
Importer: &schema.ResourceImporter{ Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough, State: schema.ImportStatePassthrough,
}, },
MigrateState: resourceGoogleProjectMigrateState,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"id": &schema.Schema{ "id": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Required: true, Optional: true,
ForceNew: 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, Type: schema.TypeString,
Optional: true, 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{ "name": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true,
Computed: 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{ "number": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Computed: true, 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 { func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config) config := meta.(*Config)
project, err := getProject(d, config) var pid string
if err != nil { var err error
return err 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) // we need to check if name and org_id are set, and throw an error if they aren't
if err := resourceGoogleProjectRead(d, meta); err != nil { // we can't just set these as required on the object, however, as that would break
return err // 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 // 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. // The policy string is just a marshaled cloudresourcemanager.Policy.
// Unmarshal it to a struct. // Unmarshal it to a struct.
var policy cloudresourcemanager.Policy 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 return err
} }
log.Printf("[DEBUG] Got policy from config: %#v", policy.Bindings)
// Retrieve existing IAM policy from project. This will be merged // Retrieve existing IAM policy from project. This will be merged
// with the policy defined here. // with the policy defined here.
// TODO(evanbrown): Add an 'authoritative' flag that allows policy p, err := getProjectIamPolicy(pid, config)
// in manifest to overwrite existing policy.
p, err := getProjectIamPolicy(project, config)
if err != nil { if err != nil {
return err return err
} }
@ -95,47 +164,98 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error
// Apply the merged policy // Apply the merged policy
log.Printf("[DEBUG] Setting new policy for project: %#v", p) 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() &cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do()
if err != nil { 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 { func resourceGoogleProjectRead(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config) 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 { if err != nil {
return err return err
} }
d.SetId(project)
// Confirm the project exists. polBytes, err := json.Marshal(pol)
// TODO(evanbrown): Support project creation
p, err := config.clientResourceManager.Projects.Get(project).Do()
if err != nil { if err != nil {
if v, ok := err.(*googleapi.Error); ok && v.Code == http.StatusNotFound { return err
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)
} }
d.Set("number", strconv.FormatInt(int64(p.ProjectNumber), 10)) d.Set("policy_etag", pol.Etag)
d.Set("name", p.Name) d.Set("policy_data", string(polBytes))
return nil return nil
} }
func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error { func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error {
config := meta.(*Config) 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 { 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 // Policy has changed
if ok := d.HasChange("policy_data"); ok { if ok := d.HasChange("policy_data"); ok {
// The policy string is just a marshaled cloudresourcemanager.Policy. // The policy string is just a marshaled cloudresourcemanager.Policy.
@ -152,15 +272,13 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
newPString = "{}" newPString = "{}"
} }
oldPStringf, _ := json.MarshalIndent(oldPString, "", " ") log.Printf("[DEBUG]: Old policy: %q\nNew policy: %q", oldPString, newPString)
newPStringf, _ := json.MarshalIndent(newPString, "", " ")
log.Printf("[DEBUG]: Old policy: %v\nNew policy: %v", string(oldPStringf), string(newPStringf))
var oldPolicy, newPolicy cloudresourcemanager.Policy 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 return err
} }
if err = json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil { if err := json.Unmarshal([]byte(oldPString), &oldPolicy); err != nil {
return err return err
} }
@ -199,7 +317,7 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
// with the policy in the current state // with the policy in the current state
// TODO(evanbrown): Add an 'authoritative' flag that allows policy // TODO(evanbrown): Add an 'authoritative' flag that allows policy
// in manifest to overwrite existing policy. // in manifest to overwrite existing policy.
p, err := getProjectIamPolicy(project, config) p, err := getProjectIamPolicy(pid, config)
if err != nil { if err != nil {
return err return err
} }
@ -218,86 +336,15 @@ func resourceGoogleProjectUpdate(d *schema.ResourceData, meta interface{}) error
} }
p.Bindings = rolesToMembersBinding(mergedBindingsMap) p.Bindings = rolesToMembersBinding(mergedBindingsMap)
log.Printf("[DEBUG] Setting new policy for project: %#v", p)
dump, _ := json.MarshalIndent(p.Bindings, " ", " ") dump, _ := json.MarshalIndent(p.Bindings, " ", " ")
log.Printf(string(dump)) log.Printf("[DEBUG] Setting new policy for project: %#v:\n%s", p, string(dump))
_, err = config.clientResourceManager.Projects.SetIamPolicy(project,
_, err = config.clientResourceManager.Projects.SetIamPolicy(pid,
&cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do() &cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do()
if err != nil { 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 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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -1,24 +1,23 @@
package google package google
import ( import (
"encoding/json"
"fmt" "fmt"
"os" "os"
"reflect"
"sort"
"testing" "testing"
"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"google.golang.org/api/cloudresourcemanager/v1" "google.golang.org/api/cloudresourcemanager/v1"
) )
var ( var (
projectId = multiEnvSearch([]string{ org = multiEnvSearch([]string{
"GOOGLE_PROJECT", "GOOGLE_ORG",
"GCLOUD_PROJECT",
"CLOUDSDK_CORE_PROJECT",
}) })
pname = "Terraform Acceptance Tests"
originalPolicy *cloudresourcemanager.Policy
) )
func multiEnvSearch(ks []string) string { func multiEnvSearch(ks []string) string {
@ -30,77 +29,26 @@ func multiEnvSearch(ks []string) string {
return "" return ""
} }
// Test that a Project resource can be created and destroyed // Test that a Project resource can be created and an IAM policy
func TestAccGoogleProject_associate(t *testing.T) { // associated
func TestAccGoogleProject_create(t *testing.T) {
pid := "terraform-" + acctest.RandString(10)
resource.Test(t, resource.TestCase{ resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) }, PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders, Providers: testAccProviders,
Steps: []resource.TestStep{ Steps: []resource.TestStep{
// This step imports an existing project
resource.TestStep{ resource.TestStep{
Config: fmt.Sprintf(testAccGoogleProject_basic, projectId), Config: testAccGoogleProject_create(pid, pname, org),
Check: resource.ComposeTestCheckFunc( Check: resource.ComposeTestCheckFunc(
testAccCheckGoogleProjectExists("google_project.acceptance"), testAccCheckGoogleProjectExists("google_project.acceptance", pid),
), ),
}, },
}, },
}) })
} }
// Test that a Project resource can be created, an IAM Policy func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc {
// 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 {
return func(s *terraform.State) error { return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[r] rs, ok := s.RootModule().Resources[r]
if !ok { if !ok {
@ -111,349 +59,29 @@ func testAccCheckGoogleProjectExists(r string) resource.TestCheckFunc {
return fmt.Errorf("No ID is set") return fmt.Errorf("No ID is set")
} }
if rs.Primary.ID != projectId { if rs.Primary.ID != pid {
return fmt.Errorf("Expected project %q to match ID %q in state", projectId, rs.Primary.ID) return fmt.Errorf("Expected project %q to match ID %q in state", pid, rs.Primary.ID)
} }
return nil return nil
} }
} }
func testAccCheckGoogleProjectIamPolicyIsMerged(projectRes, policyRes string, original *cloudresourcemanager.Policy) resource.TestCheckFunc { func testAccGoogleProjectImportExisting(pid string) string {
return func(s *terraform.State) error { return fmt.Sprintf(`
// 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 = `
resource "google_project" "acceptance" { 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" { resource "google_project" "acceptance" {
id = "%v" project_id = "%v"
policy_data = "${data.google_iam_policy.admin.policy_data}" policy_data = "${data.google_iam_policy.admin.policy_data}"
} }
data "google_iam_policy" "admin" { data "google_iam_policy" "admin" {
binding { binding {
role = "roles/storage.objectViewer" role = "roles/storage.objectViewer"
@ -468,4 +96,5 @@ data "google_iam_policy" "admin" {
"user:evandbrown@gmail.com", "user:evandbrown@gmail.com",
] ]
} }
}` }`, pid)
}

View File

@ -9,6 +9,14 @@ import (
"github.com/hashicorp/terraform/terraform" "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 // Test that a service account resource can be created, updated, and destroyed
func TestAccGoogleServiceAccount_basic(t *testing.T) { func TestAccGoogleServiceAccount_basic(t *testing.T) {
accountId := "a" + acctest.RandString(10) accountId := "a" + acctest.RandString(10)

View File

@ -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
}

View File

@ -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
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

6
vendor/vendor.json vendored
View File

@ -2834,6 +2834,12 @@
"revision": "3cc2e591b550923a2c5f0ab5a803feda924d5823", "revision": "3cc2e591b550923a2c5f0ab5a803feda924d5823",
"revisionTime": "2016-11-27T23:54:21Z" "revisionTime": "2016-11-27T23:54:21Z"
}, },
{
"checksumSHA1": "4xiJmsULiSTn2iO4zUYtMgJqJSQ=",
"path": "google.golang.org/api/servicemanagement/v1",
"revision": "c8d75a8ec737f9b8b1ed2676c28feedbe21f543f",
"revisionTime": "2016-11-21T18:05:46Z"
},
{ {
"checksumSHA1": "moKPpECJZQR/mANGD26E7Pbxn4I=", "checksumSHA1": "moKPpECJZQR/mANGD26E7Pbxn4I=",
"path": "google.golang.org/api/sqladmin/v1beta4", "path": "google.golang.org/api/sqladmin/v1beta4",

View File

@ -8,29 +8,24 @@ description: |-
# google\_project # google\_project
Allows management of an existing Google Cloud Platform project, and is Allows creation and management of a Google Cloud Platform project and its
currently limited to adding or modifying the IAM Policy for the project. associated enabled services/APIs.
When adding a policy to a project, the policy will be merged with the Projects created with this resource must be associated with an Organization.
project's existing policy. The policy is always specified in a See the [Organization documentation](https://cloud.google.com/resource-manager/docs/quickstart) for more details.
`google_iam_policy` data source and referenced from the project's
`policy_data` attribute. The service account used to run Terraform when creating a `google_project`
resource must have `roles/resourcemanager.projectCreator`. See the
[Access Control for Organizations Using IAM](https://cloud.google.com/resource-manager/docs/access-control-org)
doc for more information.
## Example Usage ## Example Usage
```js ```js
resource "google_project" "my_project" { resource "google_project" "my_project" {
id = "your-project-id" project_id = "your-project-id"
policy_data = "${data.google_iam_policy.admin.policy_data}" org_id = "1234567"
} services = ["compute_component", "storage-component-json.googleapis.com", "iam.googleapis.com"]
data "google_iam_policy" "admin" {
binding {
role = "roles/storage.objectViewer"
members = [
"user:evandbrown@gmail.com",
]
}
} }
``` ```
@ -38,24 +33,55 @@ data "google_iam_policy" "admin" {
The following arguments are supported: The following arguments are supported:
* `id` - (Required) The project ID. * `project_id` - (Optional) The project ID.
Changing this forces a new project to be referenced. Changing this forces a new project to be created. If this attribute is not
set, `id` must be set. As `id` is deprecated, consider this attribute
required. If you are using `project_id` and creating a new project, the
`org_id` and `name` attributes are also required.
* `policy` - (Optional) The `google_iam_policy` data source that represents * `id` - (Deprecated) The project ID.
the IAM policy that will be applied to the project. The policy will be This attribute has unexpected behaviour and probably does not work
merged with any existing policy applied to the project. as users would expect; it has been deprecated, and will be removed in future
versions of Terraform. The `project_id` attribute should be used instead. See
[below](#id-field) for more information about its behaviour.
Changing this updates the policy. * `project_id` - (Required) The project ID.
Changing this forces a new project to be created.
Deleting this removes the policy, but leaves the original project policy * `org_id` - (Optional) The numeric ID of the organization this project belongs to.
intact. If there are overlapping `binding` entries between the original This is required if you are creating a new project.
project policy and the data source policy, they will be removed. Changing this forces a new project to be created.
* `name` - (Optional) The display name of the project.
This is required if you are creating a new project.
* `services` - (Optional) The services/APIs that are enabled for this project.
For a list of available services, run `gcloud beta service-management list`
* `skip_delete` - (Optional) If true, the Terraform resource can be deleted
without deleting the Project via the Google API.
* `policy_data` - (Deprecated) The IAM policy associated with the project.
This argument is no longer supported, and will be removed in a future version
of Terraform. It should be replaced with a `google_project_iam_policy` resource.
## Attributes Reference ## Attributes Reference
In addition to the arguments listed above, the following computed attributes are In addition to the arguments listed above, the following computed attributes are
exported: exported:
* `name` - The name of the project.
* `number` - The numeric identifier of the project. * `number` - The numeric identifier of the project.
* `policy_etag` - (Deprecated) The etag of the project's IAM policy, used to
determine if the IAM policy has changed. Please use `google_project_iam_policy`'s
`etag` property instead; future versions of Terraform will remove the `policy_etag`
attribute
## ID Field
In previous versions of Terraform, `google_project` resources used an `id` field in
config files to specify the project ID. Unfortunately, due to limitations in Terraform,
this field always looked empty to Terraform. Terraform fell back on using the project
the Google Cloud provider is configured with. If you're using the `id` field in your
configurations, know that it is being ignored, and its value will always be seen as the
ID of the project being used to authenticate Terraform's requests. You should move to the
`project_id` field as soon as possible.

View File

@ -0,0 +1,69 @@
---
layout: "google"
page_title: "Google: google_project_iam_policy"
sidebar_current: "docs-google-project-iam-policy"
description: |-
Allows management of an IAM policy for a Google Cloud Platform project.
---
# google\_project\_iam\_policy
Allows creation and management of an IAM policy for an existing Google Cloud
Platform project.
## Example Usage
```js
resource "google_project_iam_policy" "project" {
project = "your-project-id"
policy_data = "${data.google_iam_policy.admin.policy_data}"
}
data "google_iam_policy" "admin" {
binding {
role = "roles/editor"
members = [
"user:jane@example.com",
]
}
}
```
## Argument Reference
The following arguments are supported:
* `project` - (Required) The project ID.
Changing this forces a new project to be created.
* `policy_data` - (Required) The `google_iam_policy` data source that represents
the IAM policy that will be applied to the project. The policy will be
merged with any existing policy applied to the project.
Changing this updates the policy.
Deleting this removes the policy, but leaves the original project policy
intact. If there are overlapping `binding` entries between the original
project policy and the data source policy, they will be removed.
* `authoritative` - (Optional) A boolean value indicating if this policy
should overwrite any existing IAM policy on the project. When set to true,
**any policies not in your config file will be removed**. This can **lock
you out** of your project until an Organization Administrator grants you
access again, so please exercise caution. If this argument is `true` and you
want to delete the resource, you must set the `disable_project` argument to
`true`, acknowledging that the project will be inaccessible to anyone but the
Organization Admins, as it will no longer have an IAM policy.
* `disable_project` - (Optional) A boolean value that must be set to `true`
if you want to delete a `google_project_iam_policy` that is authoritative.
## Attributes Reference
In addition to the arguments listed above, the following computed attributes are
exported:
* `etag` - (Computed) The etag of the project's IAM policy.
* `restore_policy` - (Computed) The IAM policy that will be resotred when a
non-authoritative policy resource is deleted.

View File

@ -0,0 +1,32 @@
---
layout: "google"
page_title: "Google: google_project_services"
sidebar_current: "docs-google-project-services"
description: |-
Allows management of API services for a Google Cloud Platform project.
---
# google\_project\_services
Allows management of enabled API services for an existing Google Cloud
Platform project. Services in an existing project that are not defined
in the config will be removed.
## Example Usage
```js
resource "google_project_services" "project" {
project_id = "your-project-id"
services = ["iam.googleapis.com", "cloudresourcemanager.googleapis.com"]
}
```
## Argument Reference
The following arguments are supported:
* `project_id` - (Required) The project ID.
Changing this forces a new project to be created.
* `services` - (Required) The list of services that are enabled. Supports
update.

View File

@ -25,6 +25,12 @@
<li<%= sidebar_current("docs-google-project") %>> <li<%= sidebar_current("docs-google-project") %>>
<a href="/docs/providers/google/r/google_project.html">google_project</a> <a href="/docs/providers/google/r/google_project.html">google_project</a>
</li> </li>
<li<%= sidebar_current("docs-google-project-iam-policy") %>>
<a href="/docs/providers/google/r/google_project_iam_policy.html">google_project_iam_policy</a>
</li>
<li<%= sidebar_current("docs-google-project-services") %>>
<a href="/docs/providers/google/r/google_project_services.html">google_project_services</a>
</li>
<li<%= sidebar_current("docs-google-service-account") %>> <li<%= sidebar_current("docs-google-service-account") %>>
<a href="/docs/providers/google/r/google_service_account.html">google_service_account</a> <a href="/docs/providers/google/r/google_service_account.html">google_service_account</a>
</li> </li>