From 1859c558fe2d5b3b2920b964104ff219d59dde2a Mon Sep 17 00:00:00 2001 From: Vincent Roseberry Date: Mon, 20 Nov 2017 17:01:39 -0800 Subject: [PATCH] Refactor project iam binding and member resources to improve reusability (#744) * Refactor project iam binding and member resources to improve reusability * Use default mask when updating project iam policy * Add a doc comment for the ResourceIamUpdater interface --- google/iam.go | 111 +++++++++++ google/iam_project.go | 67 +++++++ google/provider.go | 4 +- google/resource_google_project_iam_binding.go | 183 ----------------- google/resource_google_project_iam_member.go | 186 ------------------ ...resource_google_project_iam_member_test.go | 4 + google/resource_google_project_iam_policy.go | 75 ------- google/resource_iam_binding.go | 171 ++++++++++++++++ google/resource_iam_member.go | 156 +++++++++++++++ 9 files changed, 511 insertions(+), 446 deletions(-) create mode 100644 google/iam.go create mode 100644 google/iam_project.go delete mode 100644 google/resource_google_project_iam_binding.go delete mode 100644 google/resource_google_project_iam_member.go create mode 100644 google/resource_iam_binding.go create mode 100644 google/resource_iam_member.go diff --git a/google/iam.go b/google/iam.go new file mode 100644 index 00000000..b373faf2 --- /dev/null +++ b/google/iam.go @@ -0,0 +1,111 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + "log" + "time" +) + +// The ResourceIamUpdater interface is implemented for each GCP resource supporting IAM policy. +// +// Implementations should keep track of the resource identifier. +type ResourceIamUpdater interface { + // Fetch the existing IAM policy attached to a resource. + GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) + + // Replaces the existing IAM Policy attached to a resource. + SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error + + // A mutex guards against concurrent to call to the SetResourceIamPolicy method. + // The mutex key should be made of the resource type and resource id. + // For example: `iam-project-{id}`. + GetMutexKey() string + + // Returns the unique resource identifier. + GetResourceId() string + + // Textual description of this resource to be used in error message. + // The description should include the unique resource identifier. + DescribeResource() string +} + +type newResourceIamUpdaterFunc func(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) +type iamPolicyModifyFunc func(p *cloudresourcemanager.Policy) error + +func iamPolicyReadModifyWrite(updater ResourceIamUpdater, modify iamPolicyModifyFunc) error { + mutexKey := updater.GetMutexKey() + mutexKV.Lock(mutexKey) + defer mutexKV.Unlock(mutexKey) + + for { + backoff := time.Second + log.Printf("[DEBUG]: Retrieving policy for %s\n", updater.DescribeResource()) + p, err := updater.GetResourceIamPolicy() + if err != nil { + return err + } + log.Printf("[DEBUG]: Retrieved policy for %s: %+v\n", updater.DescribeResource(), p) + + err = modify(p) + if err != nil { + return err + } + + log.Printf("[DEBUG]: Setting policy for %s to %+v\n", updater.DescribeResource(), p) + err = updater.SetResourceIamPolicy(p) + if err == nil { + break + } + if isConflictError(err) { + log.Printf("[DEBUG]: Concurrent policy changes, restarting read-modify-write after %s\n", backoff) + time.Sleep(backoff) + backoff = backoff * 2 + if backoff > 30*time.Second { + return fmt.Errorf("Error applying IAM policy to %s: too many concurrent policy changes.\n", updater.DescribeResource()) + } + continue + } + return fmt.Errorf("Error applying IAM policy for %s: %v", updater.DescribeResource(), err) + } + log.Printf("[DEBUG]: Set policy for %s", updater.DescribeResource()) + return nil +} + +// 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 +} + +// 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 +} diff --git a/google/iam_project.go b/google/iam_project.go new file mode 100644 index 00000000..8f765ac2 --- /dev/null +++ b/google/iam_project.go @@ -0,0 +1,67 @@ +package google + +import ( + "fmt" + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" +) + +var IamProjectSchema = map[string]*schema.Schema{ + "project": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, +} + +type ProjectIamUpdater struct { + resourceId string + Config *Config +} + +func NewProjectIamUpdater(d *schema.ResourceData, config *Config) (ResourceIamUpdater, error) { + pid, err := getProject(d, config) + if err != nil { + return nil, err + } + + return &ProjectIamUpdater{ + resourceId: pid, + Config: config, + }, nil +} + +func (u *ProjectIamUpdater) GetResourceIamPolicy() (*cloudresourcemanager.Policy, error) { + p, err := u.Config.clientResourceManager.Projects.GetIamPolicy(u.resourceId, + &cloudresourcemanager.GetIamPolicyRequest{}).Do() + + if err != nil { + return nil, fmt.Errorf("Error retrieving IAM policy for %s: %s", u.DescribeResource(), err) + } + + return p, nil +} + +func (u *ProjectIamUpdater) SetResourceIamPolicy(policy *cloudresourcemanager.Policy) error { + _, err := u.Config.clientResourceManager.Projects.SetIamPolicy(u.resourceId, &cloudresourcemanager.SetIamPolicyRequest{ + Policy: policy, + }).Do() + + if err != nil { + return fmt.Errorf("Error setting IAM policy for %s: %s", u.DescribeResource(), err) + } + + return nil +} + +func (u *ProjectIamUpdater) GetResourceId() string { + return u.resourceId +} + +func (u *ProjectIamUpdater) GetMutexKey() string { + return fmt.Sprintf("iam-project-%s", u.resourceId) +} + +func (u *ProjectIamUpdater) DescribeResource() string { + return fmt.Sprintf("project %q", u.resourceId) +} diff --git a/google/provider.go b/google/provider.go index 514f6349..b7516f12 100644 --- a/google/provider.go +++ b/google/provider.go @@ -132,8 +132,8 @@ func Provider() terraform.ResourceProvider { "google_organization_policy": resourceGoogleOrganizationPolicy(), "google_project": resourceGoogleProject(), "google_project_iam_policy": resourceGoogleProjectIamPolicy(), - "google_project_iam_binding": resourceGoogleProjectIamBinding(), - "google_project_iam_member": resourceGoogleProjectIamMember(), + "google_project_iam_binding": ResourceIamBinding(IamProjectSchema, NewProjectIamUpdater), + "google_project_iam_member": ResourceIamMember(IamProjectSchema, NewProjectIamUpdater), "google_project_service": resourceGoogleProjectService(), "google_project_iam_custom_role": resourceGoogleProjectIamCustomRole(), "google_project_services": resourceGoogleProjectServices(), diff --git a/google/resource_google_project_iam_binding.go b/google/resource_google_project_iam_binding.go deleted file mode 100644 index c1751be3..00000000 --- a/google/resource_google_project_iam_binding.go +++ /dev/null @@ -1,183 +0,0 @@ -package google - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform/helper/schema" - "google.golang.org/api/cloudresourcemanager/v1" -) - -func resourceGoogleProjectIamBinding() *schema.Resource { - return &schema.Resource{ - Create: resourceGoogleProjectIamBindingCreate, - Read: resourceGoogleProjectIamBindingRead, - Update: resourceGoogleProjectIamBindingUpdate, - Delete: resourceGoogleProjectIamBindingDelete, - - Schema: map[string]*schema.Schema{ - "project": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - "role": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "members": { - Type: schema.TypeSet, - Required: true, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - }, - "etag": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func resourceGoogleProjectIamBindingCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - // Get the binding in the template - log.Println("[DEBUG]: Reading google_project_iam_binding") - p := getResourceIamBinding(d) - mutexKV.Lock(projectIamBindingMutexKey(pid, p.Role)) - defer mutexKV.Unlock(projectIamBindingMutexKey(pid, p.Role)) - - err = projectIamPolicyReadModifyWrite(d, config, pid, func(ep *cloudresourcemanager.Policy) error { - // Merge the bindings together - ep.Bindings = mergeBindings(append(ep.Bindings, p)) - return nil - }) - if err != nil { - return err - } - d.SetId(pid + "/" + p.Role) - return resourceGoogleProjectIamBindingRead(d, meta) -} - -func resourceGoogleProjectIamBindingRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - eBinding := getResourceIamBinding(d) - - log.Println("[DEBUG]: Retrieving policy for project", pid) - p, err := getProjectIamPolicy(pid, config) - if err != nil { - return err - } - log.Printf("[DEBUG]: Retrieved policy for project %q: %+v\n", pid, p) - - var binding *cloudresourcemanager.Binding - for _, b := range p.Bindings { - if b.Role != eBinding.Role { - continue - } - binding = b - break - } - if binding == nil { - log.Printf("[DEBUG]: Binding for role %q not found in policy for %q, removing from state file.\n", eBinding.Role, pid) - d.SetId("") - return nil - } - d.Set("etag", p.Etag) - d.Set("members", binding.Members) - d.Set("role", binding.Role) - return nil -} - -func resourceGoogleProjectIamBindingUpdate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - binding := getResourceIamBinding(d) - mutexKV.Lock(projectIamBindingMutexKey(pid, binding.Role)) - defer mutexKV.Unlock(projectIamBindingMutexKey(pid, binding.Role)) - - err = projectIamPolicyReadModifyWrite(d, config, pid, func(p *cloudresourcemanager.Policy) error { - var found bool - for pos, b := range p.Bindings { - if b.Role != binding.Role { - continue - } - found = true - p.Bindings[pos] = binding - break - } - if !found { - p.Bindings = append(p.Bindings, binding) - } - return nil - }) - if err != nil { - return err - } - - return resourceGoogleProjectIamBindingRead(d, meta) -} - -func resourceGoogleProjectIamBindingDelete(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - binding := getResourceIamBinding(d) - mutexKV.Lock(projectIamBindingMutexKey(pid, binding.Role)) - defer mutexKV.Unlock(projectIamBindingMutexKey(pid, binding.Role)) - - err = projectIamPolicyReadModifyWrite(d, config, pid, func(p *cloudresourcemanager.Policy) error { - toRemove := -1 - for pos, b := range p.Bindings { - if b.Role != binding.Role { - continue - } - toRemove = pos - break - } - if toRemove < 0 { - log.Printf("[DEBUG]: Policy bindings for project %q did not include a binding for role %q", pid, binding.Role) - return nil - } - - p.Bindings = append(p.Bindings[:toRemove], p.Bindings[toRemove+1:]...) - return nil - }) - if err != nil { - return err - } - - return resourceGoogleProjectIamBindingRead(d, meta) -} - -// Get a cloudresourcemanager.Binding from a schema.ResourceData -func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding { - members := d.Get("members").(*schema.Set).List() - return &cloudresourcemanager.Binding{ - Members: convertStringArr(members), - Role: d.Get("role").(string), - } -} - -func projectIamBindingMutexKey(pid, role string) string { - return fmt.Sprintf("google-project-iam-binding-%s-%s", pid, role) -} diff --git a/google/resource_google_project_iam_member.go b/google/resource_google_project_iam_member.go deleted file mode 100644 index c5ed04f9..00000000 --- a/google/resource_google_project_iam_member.go +++ /dev/null @@ -1,186 +0,0 @@ -package google - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform/helper/schema" - "google.golang.org/api/cloudresourcemanager/v1" -) - -func resourceGoogleProjectIamMember() *schema.Resource { - return &schema.Resource{ - Create: resourceGoogleProjectIamMemberCreate, - Read: resourceGoogleProjectIamMemberRead, - Delete: resourceGoogleProjectIamMemberDelete, - - Schema: map[string]*schema.Schema{ - "project": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - }, - "role": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "member": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "etag": { - Type: schema.TypeString, - Computed: true, - }, - }, - } -} - -func resourceGoogleProjectIamMemberCreate(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - // Get the binding in the template - log.Println("[DEBUG]: Reading google_project_iam_member") - p := getResourceIamMember(d) - mutexKV.Lock(projectIamMemberMutexKey(pid, p.Role, p.Members[0])) - defer mutexKV.Unlock(projectIamMemberMutexKey(pid, p.Role, p.Members[0])) - - err = projectIamPolicyReadModifyWrite(d, config, pid, func(ep *cloudresourcemanager.Policy) error { - // find the binding - var binding *cloudresourcemanager.Binding - for _, b := range ep.Bindings { - if b.Role != p.Role { - continue - } - binding = b - break - } - if binding == nil { - binding = &cloudresourcemanager.Binding{ - Role: p.Role, - Members: p.Members, - } - } - - // Merge the bindings together - ep.Bindings = mergeBindings(append(ep.Bindings, p)) - return nil - }) - if err != nil { - return err - } - d.SetId(pid + "/" + p.Role + "/" + p.Members[0]) - return resourceGoogleProjectIamMemberRead(d, meta) -} - -func resourceGoogleProjectIamMemberRead(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - eMember := getResourceIamMember(d) - - log.Println("[DEBUG]: Retrieving policy for project", pid) - p, err := getProjectIamPolicy(pid, config) - if err != nil { - return err - } - log.Printf("[DEBUG]: Retrieved policy for project %q: %+v\n", pid, p) - - var binding *cloudresourcemanager.Binding - for _, b := range p.Bindings { - if b.Role != eMember.Role { - continue - } - binding = b - break - } - if binding == nil { - log.Printf("[DEBUG]: Binding for role %q does not exist in policy of project %q, removing member %q from state.", eMember.Role, pid, eMember.Members[0]) - d.SetId("") - return nil - } - var member string - for _, m := range binding.Members { - if m == eMember.Members[0] { - member = m - } - } - if member == "" { - log.Printf("[DEBUG]: Member %q for binding for role %q does not exist in policy of project %q, removing from state.", eMember.Members[0], eMember.Role, pid) - d.SetId("") - return nil - } - d.Set("etag", p.Etag) - d.Set("member", member) - d.Set("role", binding.Role) - return nil -} - -func resourceGoogleProjectIamMemberDelete(d *schema.ResourceData, meta interface{}) error { - config := meta.(*Config) - pid, err := getProject(d, config) - if err != nil { - return err - } - - member := getResourceIamMember(d) - mutexKV.Lock(projectIamMemberMutexKey(pid, member.Role, member.Members[0])) - defer mutexKV.Unlock(projectIamMemberMutexKey(pid, member.Role, member.Members[0])) - - err = projectIamPolicyReadModifyWrite(d, config, pid, func(p *cloudresourcemanager.Policy) error { - bindingToRemove := -1 - for pos, b := range p.Bindings { - if b.Role != member.Role { - continue - } - bindingToRemove = pos - break - } - if bindingToRemove < 0 { - log.Printf("[DEBUG]: Binding for role %q does not exist in policy of project %q, so member %q can't be on it.", member.Role, pid, member.Members[0]) - return nil - } - binding := p.Bindings[bindingToRemove] - memberToRemove := -1 - for pos, m := range binding.Members { - if m != member.Members[0] { - continue - } - memberToRemove = pos - break - } - if memberToRemove < 0 { - log.Printf("[DEBUG]: Member %q for binding for role %q does not exist in policy of project %q.", member.Members[0], member.Role, pid) - return nil - } - binding.Members = append(binding.Members[:memberToRemove], binding.Members[memberToRemove+1:]...) - p.Bindings[bindingToRemove] = binding - return nil - }) - if err != nil { - return err - } - - return resourceGoogleProjectIamMemberRead(d, meta) -} - -// Get a cloudresourcemanager.Binding from a schema.ResourceData -func getResourceIamMember(d *schema.ResourceData) *cloudresourcemanager.Binding { - return &cloudresourcemanager.Binding{ - Members: []string{d.Get("member").(string)}, - Role: d.Get("role").(string), - } -} - -func projectIamMemberMutexKey(pid, role, member string) string { - return fmt.Sprintf("google-project-iam-member-%s-%s-%s", pid, role, member) -} diff --git a/google/resource_google_project_iam_member_test.go b/google/resource_google_project_iam_member_test.go index 8180e609..00f33fa2 100644 --- a/google/resource_google_project_iam_member_test.go +++ b/google/resource_google_project_iam_member_test.go @@ -45,6 +45,8 @@ func TestAccGoogleProjectIamMember_multiple(t *testing.T) { t.Parallel() org := getTestOrgFromEnv(t) + skipIfEnvNotSet(t, "GOOGLE_ORG") + pid := "terraform-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -86,6 +88,8 @@ func TestAccGoogleProjectIamMember_remove(t *testing.T) { t.Parallel() org := getTestOrgFromEnv(t) + skipIfEnvNotSet(t, "GOOGLE_ORG") + pid := "terraform-" + acctest.RandString(10) resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, diff --git a/google/resource_google_project_iam_policy.go b/google/resource_google_project_iam_policy.go index f8051d0c..28d90d79 100644 --- a/google/resource_google_project_iam_policy.go +++ b/google/resource_google_project_iam_policy.go @@ -5,7 +5,6 @@ import ( "fmt" "log" "sort" - "time" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/helper/schema" @@ -342,43 +341,6 @@ func rolesToMembersBinding(m map[string]map[string]bool) []*cloudresourcemanager 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 { @@ -433,40 +395,3 @@ func (b sortableBindings) Swap(i, j int) { func (b sortableBindings) Less(i, j int) bool { return b[i].Role < b[j].Role } - -type iamPolicyModifyFunc func(p *cloudresourcemanager.Policy) error - -func projectIamPolicyReadModifyWrite(d *schema.ResourceData, config *Config, pid string, modify iamPolicyModifyFunc) error { - for { - backoff := time.Second - log.Printf("[DEBUG]: Retrieving policy for project %q\n", pid) - p, err := getProjectIamPolicy(pid, config) - if err != nil { - return err - } - log.Printf("[DEBUG]: Retrieved policy for project %q: %+v\n", pid, p) - - err = modify(p) - if err != nil { - return err - } - - log.Printf("[DEBUG]: Setting policy for project %q to %+v\n", pid, p) - err = setProjectIamPolicy(p, config, pid) - if err == nil { - break - } - if isConflictError(err) { - log.Printf("[DEBUG]: Concurrent policy changes, restarting read-modify-write after %s\n", backoff) - time.Sleep(backoff) - backoff = backoff * 2 - if backoff > 30*time.Second { - return fmt.Errorf("Error applying IAM policy to project %q: too many concurrent policy changes.\n", pid) - } - continue - } - return fmt.Errorf("Error applying IAM policy to project: %v", err) - } - log.Printf("[DEBUG]: Set policy for project %q\n", pid) - return nil -} diff --git a/google/resource_iam_binding.go b/google/resource_iam_binding.go new file mode 100644 index 00000000..771f2f13 --- /dev/null +++ b/google/resource_iam_binding.go @@ -0,0 +1,171 @@ +package google + +import ( + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + "log" +) + +var iamBindingSchema = map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "members": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, +} + +func ResourceIamBinding(parentSpecificSchema map[string]*schema.Schema, newUpdaterFunc newResourceIamUpdaterFunc) *schema.Resource { + return &schema.Resource{ + Create: resourceIamBindingCreate(newUpdaterFunc), + Read: resourceIamBindingRead(newUpdaterFunc), + Update: resourceIamBindingUpdate(newUpdaterFunc), + Delete: resourceIamBindingDelete(newUpdaterFunc), + + Schema: mergeSchemas(iamBindingSchema, parentSpecificSchema), + } +} + +func resourceIamBindingCreate(newUpdaterFunc newResourceIamUpdaterFunc) schema.CreateFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := newUpdaterFunc(d, config) + if err != nil { + return err + } + + p := getResourceIamBinding(d) + err = iamPolicyReadModifyWrite(updater, func(ep *cloudresourcemanager.Policy) error { + // Creating a binding does not remove existing members if they are not in the provided members list. + // This prevents removing existing permission without the user's knowledge. + // Instead, a diff is shown in that case after creation. Subsequent calls to update will remove any + // existing members not present in the provided list. + ep.Bindings = mergeBindings(append(ep.Bindings, p)) + return nil + }) + if err != nil { + return err + } + d.SetId(updater.GetResourceId() + "/" + p.Role) + return resourceIamBindingRead(newUpdaterFunc)(d, meta) + } +} + +func resourceIamBindingRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.ReadFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := newUpdaterFunc(d, config) + if err != nil { + return err + } + + eBinding := getResourceIamBinding(d) + p, err := updater.GetResourceIamPolicy() + if err != nil { + return err + } + log.Printf("[DEBUG]: Retrieved policy for %s: %+v\n", updater.DescribeResource(), p) + + var binding *cloudresourcemanager.Binding + for _, b := range p.Bindings { + if b.Role != eBinding.Role { + continue + } + binding = b + break + } + if binding == nil { + log.Printf("[DEBUG]: Binding for role %q not found in policy for %s, removing from state file.\n", eBinding.Role, updater.DescribeResource()) + d.SetId("") + return nil + } + d.Set("etag", p.Etag) + d.Set("members", binding.Members) + d.Set("role", binding.Role) + return nil + } +} + +func resourceIamBindingUpdate(newUpdaterFunc newResourceIamUpdaterFunc) schema.UpdateFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := NewProjectIamUpdater(d, config) + if err != nil { + return err + } + + binding := getResourceIamBinding(d) + err = iamPolicyReadModifyWrite(updater, func(p *cloudresourcemanager.Policy) error { + var found bool + for pos, b := range p.Bindings { + if b.Role != binding.Role { + continue + } + found = true + p.Bindings[pos] = binding + break + } + if !found { + p.Bindings = append(p.Bindings, binding) + } + return nil + }) + if err != nil { + return err + } + + return resourceIamBindingRead(newUpdaterFunc)(d, meta) + } +} + +func resourceIamBindingDelete(newUpdaterFunc newResourceIamUpdaterFunc) schema.DeleteFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := NewProjectIamUpdater(d, config) + if err != nil { + return err + } + + binding := getResourceIamBinding(d) + err = iamPolicyReadModifyWrite(updater, func(p *cloudresourcemanager.Policy) error { + toRemove := -1 + for pos, b := range p.Bindings { + if b.Role != binding.Role { + continue + } + toRemove = pos + break + } + if toRemove < 0 { + log.Printf("[DEBUG]: Policy bindings for %s did not include a binding for role %q", updater.DescribeResource(), binding.Role) + return nil + } + + p.Bindings = append(p.Bindings[:toRemove], p.Bindings[toRemove+1:]...) + return nil + }) + if err != nil { + return err + } + + return resourceIamBindingRead(newUpdaterFunc)(d, meta) + } +} + +func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding { + members := d.Get("members").(*schema.Set).List() + return &cloudresourcemanager.Binding{ + Members: convertStringArr(members), + Role: d.Get("role").(string), + } +} diff --git a/google/resource_iam_member.go b/google/resource_iam_member.go new file mode 100644 index 00000000..3fe927a0 --- /dev/null +++ b/google/resource_iam_member.go @@ -0,0 +1,156 @@ +package google + +import ( + "github.com/hashicorp/terraform/helper/schema" + "google.golang.org/api/cloudresourcemanager/v1" + "log" +) + +var IamMemberBaseSchema = map[string]*schema.Schema{ + "role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "member": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "etag": { + Type: schema.TypeString, + Computed: true, + }, +} + +func ResourceIamMember(parentSpecificSchema map[string]*schema.Schema, newUpdaterFunc newResourceIamUpdaterFunc) *schema.Resource { + return &schema.Resource{ + Create: resourceIamMemberCreate(newUpdaterFunc), + Read: resourceIamMemberRead(newUpdaterFunc), + Delete: resourceIamMemberDelete(newUpdaterFunc), + + Schema: mergeSchemas(IamMemberBaseSchema, parentSpecificSchema), + } +} + +func getResourceIamMember(d *schema.ResourceData) *cloudresourcemanager.Binding { + return &cloudresourcemanager.Binding{ + Members: []string{d.Get("member").(string)}, + Role: d.Get("role").(string), + } +} + +func resourceIamMemberCreate(newUpdaterFunc newResourceIamUpdaterFunc) schema.CreateFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := newUpdaterFunc(d, config) + if err != nil { + return err + } + + p := getResourceIamMember(d) + err = iamPolicyReadModifyWrite(updater, func(ep *cloudresourcemanager.Policy) error { + // Merge the bindings together + ep.Bindings = mergeBindings(append(ep.Bindings, p)) + return nil + }) + if err != nil { + return err + } + d.SetId(updater.GetResourceId() + "/" + p.Role + "/" + p.Members[0]) + return resourceIamMemberRead(newUpdaterFunc)(d, meta) + } +} + +func resourceIamMemberRead(newUpdaterFunc newResourceIamUpdaterFunc) schema.ReadFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := newUpdaterFunc(d, config) + if err != nil { + return err + } + + eMember := getResourceIamMember(d) + p, err := updater.GetResourceIamPolicy() + if err != nil { + return err + } + log.Printf("[DEBUG]: Retrieved policy for %s: %+v\n", updater.DescribeResource(), p) + + var binding *cloudresourcemanager.Binding + for _, b := range p.Bindings { + if b.Role != eMember.Role { + continue + } + binding = b + break + } + if binding == nil { + log.Printf("[DEBUG]: Binding for role %q does not exist in policy of %s, removing member %q from state.", eMember.Role, updater.DescribeResource(), eMember.Members[0]) + d.SetId("") + return nil + } + var member string + for _, m := range binding.Members { + if m == eMember.Members[0] { + member = m + } + } + if member == "" { + log.Printf("[DEBUG]: Member %q for binding for role %q does not exist in policy of %s, removing from state.", eMember.Members[0], eMember.Role, updater.DescribeResource()) + d.SetId("") + return nil + } + d.Set("etag", p.Etag) + d.Set("member", member) + d.Set("role", binding.Role) + return nil + } +} + +func resourceIamMemberDelete(newUpdaterFunc newResourceIamUpdaterFunc) schema.DeleteFunc { + return func(d *schema.ResourceData, meta interface{}) error { + config := meta.(*Config) + updater, err := newUpdaterFunc(d, config) + if err != nil { + return err + } + + member := getResourceIamMember(d) + err = iamPolicyReadModifyWrite(updater, func(p *cloudresourcemanager.Policy) error { + bindingToRemove := -1 + for pos, b := range p.Bindings { + if b.Role != member.Role { + continue + } + bindingToRemove = pos + break + } + if bindingToRemove < 0 { + log.Printf("[DEBUG]: Binding for role %q does not exist in policy of project %q, so member %q can't be on it.", member.Role, updater.GetResourceId(), member.Members[0]) + return nil + } + binding := p.Bindings[bindingToRemove] + memberToRemove := -1 + for pos, m := range binding.Members { + if m != member.Members[0] { + continue + } + memberToRemove = pos + break + } + if memberToRemove < 0 { + log.Printf("[DEBUG]: Member %q for binding for role %q does not exist in policy of project %q.", member.Members[0], member.Role, updater.GetResourceId()) + return nil + } + binding.Members = append(binding.Members[:memberToRemove], binding.Members[memberToRemove+1:]...) + p.Bindings[bindingToRemove] = binding + return nil + }) + if err != nil { + return err + } + + return resourceIamMemberRead(newUpdaterFunc)(d, meta) + } +}