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

#644 Add Support For BigQuery Access Control #1931

Merged

Conversation

chrismorgan-hb
Copy link
Contributor

@chrismorgan-hb chrismorgan-hb commented Aug 22, 2018

I went ahead and added separate tests, leaving the "basic" tests to prove that the existing way (no explicit access block) continues to work.

Fixes #644

This is my first non-toy Go code and my first contribution to this project, so feedback is most welcome.

@ghost ghost added the size/l label Aug 22, 2018
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.

Awesome, I'm super excited about this! I have a bunch of comments but overall structure looks good.

"domain": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"access.group_by_email", "access.special_group", "access.user_by_email", "access.view"},
Copy link
Contributor

Choose a reason for hiding this comment

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

Sadly this won't work. ConflictsWith needs to point to an exact address of a field, and since this is a list, there's no way to wildcard all indexes. If you want a check like this, you'll just have to do it at apply-time (inside the create fn)

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 API error message is pretty good in this case, so I just dropped this validation on our side.

da := bigquery.DatasetAccess{}
msi := m.(map[string]interface{})
da.Role = msi["role"].(string)
if val, ok := msi["domain"]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would need to confirm 100% since I don't remember which cases this applies to and which it doesn't, but I think the map will actually have all values in it, even if they weren't set, which would mean you could do:

da := bigquery.DatasetAccess{
  Role: msi["role"].(string),
  Domain: msi["domain}.(string),
  [etc]
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing it that way unfortunately breaks existing tests (where access isn't set at all)

Copy link
Contributor

Choose a reason for hiding this comment

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

Really? You're already inside a block that checks whether access is set once you've gotten to this point.

access := []*bigquery.DatasetAccess{}
for _, m := range v.([]interface{}) {
da := bigquery.DatasetAccess{}
msi := m.(map[string]interface{})
Copy link
Contributor

Choose a reason for hiding this comment

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

does msi just stand for map[string]interface? if so maybe a sliiiightly more descriptive var name could be used

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

"role": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateRole,
Copy link
Contributor

Choose a reason for hiding this comment

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

We already have a validateFn that you can use for enums: validation.StringInSlice([]string{[values here]}, false),

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice! Done.

access := make([]map[string]interface{}, 0, len(a))
for _, da := range a {
var ai map[string]interface{}
ai = make(map[string]interface{})
Copy link
Contributor

Choose a reason for hiding this comment

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

you shouldn't need to do this in two lines, you can just do

ai := make(map[string]interface{})

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

location = "EU"
default_table_expiration_ms = 3600000

access = [
Copy link
Contributor

Choose a reason for hiding this comment

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

this can just be

access {
  role = [...]
  user_by_email = [...]
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

resource "google_bigquery_dataset" "access_test" {
dataset_id = "%s"
friendly_name = "foo"
description = "This is a foo description"
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm fine with keeping these here, but I would also be fine with the test only setting required fields + access-related stuff

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Removed optional fields.

location = "EU"
default_table_expiration_ms = 3600000

access = [
Copy link
Contributor

Choose a reason for hiding this comment

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

likewise, this would just be

access {
  ...
}
access {
  ...
}

},
{
view = {
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.

could this be project_id = "${google_bigquery_dataset.other_dataset.project}"? would that make sense semantically?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, that's more consistent.

@@ -73,6 +84,9 @@ The following arguments are supported:

* `labels` - (Optional) A mapping of labels to assign to the resource.

* `access` - (Optional) An array of objects that define dataset access for
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add a "structure is documented below" statement and then docs for each of the attributes? search for that string in our existing docs and you'll find a bunch of examples.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor Author

@chrismorgan-hb chrismorgan-hb left a comment

Choose a reason for hiding this comment

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

Thanks for the comments!

"role": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validateRole,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice! Done.

"domain": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ConflictsWith: []string{"access.group_by_email", "access.special_group", "access.user_by_email", "access.view"},
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 API error message is pretty good in this case, so I just dropped this validation on our side.

da := bigquery.DatasetAccess{}
msi := m.(map[string]interface{})
da.Role = msi["role"].(string)
if val, ok := msi["domain"]; ok {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing it that way unfortunately breaks existing tests (where access isn't set at all)

access := []*bigquery.DatasetAccess{}
for _, m := range v.([]interface{}) {
da := bigquery.DatasetAccess{}
msi := m.(map[string]interface{})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

access := make([]map[string]interface{}, 0, len(a))
for _, da := range a {
var ai map[string]interface{}
ai = make(map[string]interface{})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}
if da.View != nil {
view := make(map[string]string)
view["project_id"] = da.View.ProjectId
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Neat! Done.

resource "google_bigquery_dataset" "access_test" {
dataset_id = "%s"
friendly_name = "foo"
description = "This is a foo description"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Removed optional fields.

location = "EU"
default_table_expiration_ms = 3600000

access = [
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

},
{
view = {
project_id = "%s"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, that's more consistent.

@@ -73,6 +84,9 @@ The following arguments are supported:

* `labels` - (Optional) A mapping of labels to assign to the resource.

* `access` - (Optional) An array of objects that define dataset access for
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor Author

@chrismorgan-hb chrismorgan-hb left a comment

Choose a reason for hiding this comment

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

Also fixed markdown formatting with the nested list of special groups

@chrismorgan-hb
Copy link
Contributor Author

Friendly ping @danawillow (now that you're back from vacation and I'm sure your inbox is swamped).

da := bigquery.DatasetAccess{}
msi := m.(map[string]interface{})
da.Role = msi["role"].(string)
if val, ok := msi["domain"]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

Really? You're already inside a block that checks whether access is set once you've gotten to this point.

func flattenAccess(a []*bigquery.DatasetAccess) []map[string]interface{} {
access := make([]map[string]interface{}, 0, len(a))
for _, da := range a {
ai := make(map[string]interface{})
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this section can also be done in a single block:

ai := map[string]interface{}{
  "role": da.Role,
  ..etc...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -228,6 +308,7 @@ func resourceBigQueryDatasetRead(d *schema.ResourceData, meta interface{}) error
d.Set("project", id.Project)
d.Set("etag", res.Etag)
d.Set("labels", res.Labels)
d.Set("access", flattenAccess(res.Access))
Copy link
Contributor

Choose a reason for hiding this comment

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

for setting values that aren't primitives, we're trying to get in the habit of checking the error returned from d.Set (we've had some confusing bugs in the past that would have shown up much sooner if we had error-checked). something like:

if err := d.Set("access", flattenAccess(res.Access)); err != nil {
  return err
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

func testAccBigQueryDatasetWithOneAccess(datasetID string) string {
return fmt.Sprintf(`
resource "google_bigquery_dataset" "access_test" {
dataset_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.

nit: since this isn't trying to align with anything else, you can move the equals sign way back over to the left

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}
access {
role = "READER"
domain = "example.com"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: looks like a tab snuck in here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

}`, datasetID)
}

func getBigQueryTableWithView(datasetID, tableID string) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this func only used in the one place? If so, may as well just inline it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

{
role = "OWNER"
user_by_email = "[email protected]"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you look at this section? The formatting seems to have gotten a bit wonky.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@@ -26,6 +26,17 @@ resource "google_bigquery_dataset" "default" {
labels {
env = "default"
}

access = [
Copy link
Contributor

Choose a reason for hiding this comment

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

this should be:

access {
  role   = "READER"
  domain = "example.com"
}
access {
  role           = "WRITER"
  group_by_email = "[email protected]"
}

There's nothing wrong with the other way, this is just more canonical.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

the user specified by the other member of the access object. The following
string values are supported: `READER`, `WRITER`, `OWNER`.

* `domain` - (Pick one) A domain to grant access to.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not quite sure what "Pick one" means in this context, do you mean Optional?

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 was mimic-ing the BQ API documentation, it basically means you have to pick exactly one of domain, group_by_email, special_group, user_by_email, or view. If there's a way to be more clear, I'm happy to change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, got it. I'd just try to match the rest of the Terraform documentation, where we just say Required or Optional there. I think that Pick One itself is pretty redundant, right? There isn't a way to specify multiple of the same field, so you don't really have a choice but to pick one (or zero, but that's covered in Required vs Optional).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a little more nuanced because these are keys in a map, not values that can be taken by a single field. I updated the docs to reflect that (while marking these optional). Let me know what you think.

Optional: true,
},
"view": &schema.Schema{
Type: schema.TypeMap,
Copy link
Contributor

Choose a reason for hiding this comment

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

This has specific keys that have to be in the map, correct? If so, then this should be another nested object (list with MaxSize: 1 and a defined schema) so we don't allow users to set keys in the map that aren't valid options.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

@danawillow
Copy link
Contributor

Oh also! Fun GH trick: if you write fixes #644 in the body of your PR, it'll close the issue automatically: https://help.github.com/articles/closing-issues-using-keywords/

@ghost ghost added the size/l label Sep 10, 2018
@danawillow
Copy link
Contributor

Yup that looks good to me, thanks! I'll go ahead and merge this.

@danawillow danawillow merged commit 5599183 into hashicorp:master Sep 11, 2018
chrisst pushed a commit to chrisst/terraform-provider-google that referenced this pull request Nov 9, 2018
* Updates based on PR comments

* Fix markdown nested list

* Second round of feedback

* Clarify pick one docs
@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.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants