Merge pull request #10425 from evandbrown/google-project-templates
providers/google: Create and manage Google Cloud Platform Projects
This commit is contained in:
commit
54aca85855
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
2857
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-api.json
generated
vendored
Normal file
2857
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-api.json
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
7946
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-gen.go
generated
vendored
Normal file
7946
vendor/google.golang.org/api/servicemanagement/v1/servicemanagement-gen.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -2788,6 +2788,12 @@
|
|||
"revision": "3cc2e591b550923a2c5f0ab5a803feda924d5823",
|
||||
"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=",
|
||||
"path": "google.golang.org/api/sqladmin/v1beta4",
|
||||
|
|
82
website/source/docs/providers/google/r/google_project.html.markdown
Normal file → Executable file
82
website/source/docs/providers/google/r/google_project.html.markdown
Normal file → Executable file
|
@ -8,29 +8,24 @@ description: |-
|
|||
|
||||
# google\_project
|
||||
|
||||
Allows management of an existing Google Cloud Platform project, and is
|
||||
currently limited to adding or modifying the IAM Policy for the project.
|
||||
Allows creation and management of a Google Cloud Platform project and its
|
||||
associated enabled services/APIs.
|
||||
|
||||
When adding a policy to a project, the policy will be merged with the
|
||||
project's existing policy. The policy is always specified in a
|
||||
`google_iam_policy` data source and referenced from the project's
|
||||
`policy_data` attribute.
|
||||
Projects created with this resource must be associated with an Organization.
|
||||
See the [Organization documentation](https://cloud.google.com/resource-manager/docs/quickstart) for more details.
|
||||
|
||||
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
|
||||
|
||||
```js
|
||||
resource "google_project" "my_project" {
|
||||
id = "your-project-id"
|
||||
policy_data = "${data.google_iam_policy.admin.policy_data}"
|
||||
}
|
||||
|
||||
data "google_iam_policy" "admin" {
|
||||
binding {
|
||||
role = "roles/storage.objectViewer"
|
||||
members = [
|
||||
"user:evandbrown@gmail.com",
|
||||
]
|
||||
}
|
||||
project_id = "your-project-id"
|
||||
org_id = "1234567"
|
||||
services = ["compute_component", "storage-component-json.googleapis.com", "iam.googleapis.com"]
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -38,24 +33,55 @@ data "google_iam_policy" "admin" {
|
|||
|
||||
The following arguments are supported:
|
||||
|
||||
* `id` - (Required) The project ID.
|
||||
Changing this forces a new project to be referenced.
|
||||
* `project_id` - (Optional) The project ID.
|
||||
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
|
||||
the IAM policy that will be applied to the project. The policy will be
|
||||
merged with any existing policy applied to the project.
|
||||
* `id` - (Deprecated) The project ID.
|
||||
This attribute has unexpected behaviour and probably does not work
|
||||
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
|
||||
intact. If there are overlapping `binding` entries between the original
|
||||
project policy and the data source policy, they will be removed.
|
||||
* `org_id` - (Optional) The numeric ID of the organization this project belongs to.
|
||||
This is required if you are creating a new project.
|
||||
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
|
||||
|
||||
In addition to the arguments listed above, the following computed attributes are
|
||||
exported:
|
||||
|
||||
* `name` - The name 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.
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -25,6 +25,12 @@
|
|||
<li<%= sidebar_current("docs-google-project") %>>
|
||||
<a href="/docs/providers/google/r/google_project.html">google_project</a>
|
||||
</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") %>>
|
||||
<a href="/docs/providers/google/r/google_service_account.html">google_service_account</a>
|
||||
</li>
|
||||
|
|
Loading…
Reference in New Issue