mirror of
https://github.com/letic/terraform-provider-google.git
synced 2024-10-03 01:01:06 +00:00
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
This commit is contained in:
parent
75a633cf28
commit
1859c558fe
111
google/iam.go
Normal file
111
google/iam.go
Normal file
@ -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
|
||||
}
|
67
google/iam_project.go
Normal file
67
google/iam_project.go
Normal file
@ -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)
|
||||
}
|
@ -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(),
|
||||
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
@ -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) },
|
||||
|
@ -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
|
||||
}
|
||||
|
171
google/resource_iam_binding.go
Normal file
171
google/resource_iam_binding.go
Normal file
@ -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),
|
||||
}
|
||||
}
|
156
google/resource_iam_member.go
Normal file
156
google/resource_iam_member.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user