Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add google_project_iam_binding and google_project_iam_member resources. #171

Merged
merged 27 commits into from
Jul 28, 2017

Conversation

paddycarver
Copy link
Contributor

@paddycarver paddycarver commented Jul 4, 2017

Add a resource that manages just a single binding within a Google
project's IAM Policy.

Add a resource that manages just a single member of a single binding
within a Google project's IAM Policy.

Note that these resources should not be used when
google_project_iam_policy is used, or they will fight over which is
correct. They should also not be assigned the same role within the
same project, or they will fight with each other.

This also required wrapping the error returned from setProjectIamPolicy,
as we need to test to see if it's a 409 error and retry, which can't be
done if we just use fmt.Errorf.

The code that is here works, and the tests pass. I still need to add a google_project_iam_member resource that controls IAM at the member level, which is the full solution to this problem. That will be pushed to this PR shortly. This is now complete and ready for review.

This should resolve #72 and #58.

Add a resource that manages just a single binding within a Google
project's IAM Policy.

Note that this resource should not be used when
google_project_iam_policy is used, or they will fight over which is
correct.

This also required wrapping the error returned from setProjectIamPolicy,
as we need to test to see if it's a 409 error and retry, which can't be
done if we just use fmt.Errorf.
Add docs to the website for google_project_iam_binding.
Changing the role is ForceNew, because the role is part of the ID.

Make reads go through to the Binding functions, not the Policy
functions. That's embarrassing.
It shouldn't be an error, we should just remove that binding from state.
Add the missing parameter to Errorf statements.
Adds the google_project_iam_member resource, which just ensures that a
single member has a single role.

google_project_iam_member should not be used to grant permissions to a
role controlled by google_project_iam_binding or to a policy controlled
by google_project_iam_policy, as they'll fight for control.
Fix a sentence in the binding docs, and add docs for members.
@paddycarver paddycarver changed the title Add google_project_iam_binding resource. Add google_project_iam_binding and google_project_iam_member resources. Jul 4, 2017
} else if err != nil {
return fmt.Errorf("Error applying IAM policy to project: %v", err)
}
break
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally up to you, but I think this structure would be a bit more clear:

for {
  // stuff
  if err == nil {
    // yay, successful update
    break
  } else if isConflictError(err) {
    // stuff
    continue
  } else {
    return fmt.Errorf(stuff)
}

Type: schema.TypeString,
},
},
"etag": &schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're doing a read-modify-write, do we actually need to store the etag?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code doesn't use it for anything, but I couldn't think of a good reason not to expose it.

break
}
if toRemove < 0 {
return resourceGoogleProjectIamBindingRead(d, meta)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we need to do another read here, or can we just set the id to "" and be done?


p.Bindings = append(p.Bindings[:toRemove], p.Bindings[toRemove+1:]...)

log.Printf("[DEBUG]: Setting policy for project %q to %+v\n", pid, p)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the policy seems to be done exactly the same in create, update, and delete- any way we could have a separate function that gets called by all 3?

// Get a cloudresourcemanager.Binding from a schema.ResourceData
func getResourceIamBinding(d *schema.ResourceData) *cloudresourcemanager.Binding {
members := d.Get("members").(*schema.Set).List()
m := make([]string, 0, len(members))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you want you can do m := convertStringArr(members) (the fn is in a weird place right now but it's getting moved to provider.go to make it easier to find in #152

func testAccGoogleProjectAssociateBindingBasic(pid, name, org string) string {
return fmt.Sprintf(`
resource "google_project" "acceptance" {
project_id = "%s"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind lining up your =s?

org_id = "%s"
}
resource "google_project_iam_binding" "acceptance" {
project = "${google_project.acceptance.id}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why id and not project_id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Force of habit? I dunno. I'll change to project_id to be more explicit. :)

}
}
d.Set("etag", p.Etag)
d.Set("member", member)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there is a binding server-side but not with the member we expect?

Required: true,
ForceNew: true,
},
"etag": &schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments from iam binding apply here as well

@kkallday
Copy link

kkallday commented Jul 6, 2017

cc @davewalter @rainmaker @rizwanreza @acrmp @joshzarrabi @madamkiwi @ryanmoran

Rewrite the structure of our if statement to make the conditions under
which each portion executes a bit more clear.
We can just set the ID of bindings that are scheduled to be deleted but
don't exist in the API, we don't need an entire separate read request.
We were repeating that logic a lot, so this helper just reads a policy,
calls the passed modify function on the policy, then writes the policy
back and takes care of the optimistic concurrency logic for the caller.
So now all the caller has to do is the unique part, which is the modify
function.
Helps keep things explicit.
Use the new projectIamPolicyReadModifyWrite helper to manage the RMW
loop for our policy member resource.

Handle the case of having a binding server-side that doesn't have the
member we expect more elegantly.
For clarity and explicitness(? is that a word?) use the project_id
instead of Terraform's internal id in our test configs.
@paddycarver
Copy link
Contributor Author

Think I addressed all your comments @danawillow, thanks for the thorough review. Let me know how the updated code looks. :)

Delete: resourceGoogleProjectIamBindingDelete,

Schema: map[string]*schema.Schema{
"project": &schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super minor nit: can remove the duplicate type declarations here, and the rest of the schema struct:

"project": { 

Providers: testAccProviders,
Steps: []resource.TestStep{
// Create a new project
resource.TestStep{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with the duplicate type declarations here

}, pid),
),
},
// Apply an updated IAM binding
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for completeness, I wouldn't mind another step in here that removes a single member from the binding

),
},
// Apply an IAM binding
resource.TestStep{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need this test step in here? I wonder if this test would actually give us more signal if it jumped from one binding to two (plus, the previous test essentially does the first two teststeps of this one)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused as to what you're suggesting, I'm sorry. The first step applies Binding A. The second step then adds Binding B. So this is testing that, once a binding already exists, you can add another binding. Which seems to be what the second sentence of your comment is asking for, so I think I'm misunderstanding somewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh whoops my second sentence was meant to say from zero to two. I'm fine with leaving the test as it is though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just add another test, no big deal. We should totally test that. :)

if err != nil {
return err
}
d.SetId(pid + ":" + p.Role)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A lot of the resources we've been adding import to lately have settled on "/" as a separator for ids. Maybe do that for consistency?

break
}
if binding == nil {
d.SetId("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A log message here would probably be useful.

}
}
if member == "" {
d.SetId("")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

likewise

return err
}

return resourceGoogleProjectIamBindingRead(d, meta)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of the final read?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates the state file with the state according to the API. In this case, confirms the API no longer has the binding in it, and removes it. If for whatever reason the delete function silently failed and didn't actually delete the binding, Terraform wouldn't lose it.

}

p.Bindings = append(p.Bindings[:toRemove], p.Bindings[toRemove+1:]...)
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to set the id to "" here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The read call does that. :)

break
}
if bindingToRemove < 0 {
return nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comments from the other file around setting the id and doing the final read

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this happens in the read-modify-write helper function, we don't need to call SetId, because when it returns nil, it just returns back up to the Delete function. The Delete calls read, which will verify the member does not actually exist anymore, and then removes it from state.

`google_project_iam_binding` can be used per role.

* `project` - (Optional) The project ID. If not specified, uses the
ID of the project configured with the provider.## Attributes Reference
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing newlines

* `role` - (Required) The role that should be applied.

* `project` - (Optional) The project ID. If not specified, uses the
ID of the project configured with the provider.## Attributes Reference
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too

Some struct types can be inferred instead of being repeated, so let's
take advantage of that.
Add a test case that updates a binding to remove a member.
Switch to using / as a separator for IDs, instead of :.
Update member IDs to use / instead of :.

Make sure we're logging any time we remove something from state.
Tests need to have unique names. Whoooops.

Also, the Elem property accepts an interface I guess, which means we
actually need the struct type repetition there.
We don't need to set the ID to "" in read-modify-write helpers, because
once they're done, we read anyways to update state based on the changes.
And that read checks if the binding/member still exists, and does the
SetId("") if it doesn't.

This way, we stick with state only getting set based on the API state,
not by what we think the state will be.
That's embarrassing. How did I miss that?
@paddycarver
Copy link
Contributor Author

Latest push should address all comments. :)

Copy link
Contributor

@danawillow danawillow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm super excited about these changes, thanks @paddycarver! Merge at will.

@paddycarver paddycarver merged commit 37df582 into master Jul 28, 2017
negz pushed a commit to negz/terraform-provider-google that referenced this pull request Oct 17, 2017
Add google_project_iam_binding and google_project_iam_member resources.
@paultyng paultyng deleted the paddy_iam branch October 29, 2018 19:29
chrisst pushed a commit to chrisst/terraform-provider-google that referenced this pull request Nov 9, 2018
Add google_project_iam_binding and google_project_iam_member resources.
@ghost
Copy link

ghost commented Nov 16, 2018

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.

If you feel this issue should be reopened, we encourage creating a new issue linking back to this one for added context. If you feel I made an error 🤖 🙉 , please reach out to my human friends 👉 [email protected]. Thanks!

@ghost ghost locked and limited conversation to collaborators Nov 16, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature request: allow updating non-authoritative Google IAM project policies (restore_policy)
4 participants