providers/google: Support managing projects
Add support for creating, updating, and deleting projects, as well as their enabled services and their IAM policies. Various concessions were made for backwards compatibility, and will be removed in 0.9 or 0.10.
This commit is contained in:
parent
e81d096699
commit
b9e9e777c8
|
@ -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
|
@ -2834,6 +2834,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