diff --git a/products/accesscontextmanager/terraform.yaml b/products/accesscontextmanager/terraform.yaml index 26a2283f9eb8..ca150ca354d0 100644 --- a/products/accesscontextmanager/terraform.yaml +++ b/products/accesscontextmanager/terraform.yaml @@ -14,6 +14,13 @@ --- !ruby/object:Provider::Terraform::Config overrides: !ruby/object:Overrides::ResourceOverrides AccessPolicy: !ruby/object:Overrides::Terraform::ResourceOverride + docs: !ruby/object:Provider::Terraform::Docs + warning: | + If you are using User ADCs (Application Default Credentials) with this resource, + you must specify a `billing_project` and set `user_project_override` to true + in the provider configuration. Otherwise the ACM API will return a 403 error. + Your account must have the `serviceusage.services.use` permission on the + `billing_project` you defined. timeouts: !ruby/object:Api::Timeouts insert_minutes: 6 update_minutes: 6 @@ -36,6 +43,13 @@ overrides: !ruby/object:Overrides::ResourceOverrides custom_code: !ruby/object:Provider::Terraform::CustomCode post_create: templates/terraform/post_create/accesspolicy.erb AccessLevel: !ruby/object:Overrides::Terraform::ResourceOverride + docs: !ruby/object:Provider::Terraform::Docs + warning: | + If you are using User ADCs (Application Default Credentials) with this resource, + you must specify a `billing_project` and set `user_project_override` to true + in the provider configuration. Otherwise the ACM API will return a 403 error. + Your account must have the `serviceusage.services.use` permission on the + `billing_project` you defined. timeouts: !ruby/object:Api::Timeouts insert_minutes: 6 update_minutes: 6 @@ -61,6 +75,13 @@ overrides: !ruby/object:Overrides::ResourceOverrides encoder: templates/terraform/encoders/access_level_never_send_parent.go.erb custom_import: templates/terraform/custom_import/set_access_policy_parent_from_self_link.go.erb ServicePerimeter: !ruby/object:Overrides::Terraform::ResourceOverride + docs: !ruby/object:Provider::Terraform::Docs + warning: | + If you are using User ADCs (Application Default Credentials) with this resource, + you must specify a `billing_project` and set `user_project_override` to true + in the provider configuration. Otherwise the ACM API will return a 403 error. + Your account must have the `serviceusage.services.use` permission on the + `billing_project` you defined. timeouts: !ruby/object:Api::Timeouts insert_minutes: 6 update_minutes: 6 @@ -99,6 +120,13 @@ overrides: !ruby/object:Overrides::ResourceOverrides encoder: templates/terraform/encoders/access_level_never_send_parent.go.erb custom_import: templates/terraform/custom_import/set_access_policy_parent_from_self_link.go.erb ServicePerimeterResource: !ruby/object:Overrides::Terraform::ResourceOverride + docs: !ruby/object:Provider::Terraform::Docs + warning: | + If you are using User ADCs (Application Default Credentials) with this resource, + you must specify a `billing_project` and set `user_project_override` to true + in the provider configuration. Otherwise the ACM API will return a 403 error. + Your account must have the `serviceusage.services.use` permission on the + `billing_project` you defined. autogen_async: true exclude_validator: true # Skipping the sweeper due to the non-standard base_url and because this is fine-grained under ServicePerimeter diff --git a/templates/terraform/pre_create/cloud_asset_feed.go.erb b/templates/terraform/pre_create/cloud_asset_feed.go.erb index a8354702cc68..3f6167a7f11d 100644 --- a/templates/terraform/pre_create/cloud_asset_feed.go.erb +++ b/templates/terraform/pre_create/cloud_asset_feed.go.erb @@ -1,16 +1,4 @@ -// This should never happen, but the linter complains otherwise with ineffectual assignment to `project` -if project == "dummy lint" { - log.Printf("[DEBUG] Found project in url: %s", project) -} + // Send the project ID in the X-Goog-User-Project header. origUserProjectOverride := config.UserProjectOverride config.UserProjectOverride = true -// If we have a billing project, use that one in the header. -bp, bpok := d.GetOk("billing_project") -if bpok && bp != "" { - project = bp.(string) -} else { - // otherwise, use the resource's project - rp, _ := d.GetOk("project") - project = rp.(string) -} diff --git a/templates/terraform/resource.erb b/templates/terraform/resource.erb index 6ce370200843..0210181460e8 100644 --- a/templates/terraform/resource.erb +++ b/templates/terraform/resource.erb @@ -189,20 +189,29 @@ func resource<%= resource_name -%>Create(d *schema.ResourceData, meta interface{ } <% end -%> <% end -%> + billingProject := "" + <% if has_project -%> project, err := getProject(d, config) if err != nil { return err } + billingProject = project <% end -%> + <% if object.supports_indirect_user_project_override -%> - var project string if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + billingProject = parts[1] } <% end -%> + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + <%= lines(compile(pwd + '/' + object.custom_code.pre_create)) if object.custom_code.pre_create -%> - res, err := sendRequestWithTimeout(config, "<%= object.create_verb.to_s.upcase -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, url, obj, d.Timeout(schema.TimeoutCreate)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) + res, err := sendRequestWithTimeout(config, "<%= object.create_verb.to_s.upcase -%>", billingProject, url, obj, d.Timeout(schema.TimeoutCreate)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { <% if object.custom_code.post_create_failure && object.async.nil? # Only add if not handled by async error handling -%> resource<%= resource_name -%>PostCreateFailure(d, meta) @@ -335,19 +344,28 @@ func resource<%= resource_name -%>PollRead(d *schema.ResourceData, meta interfac return nil, err } - <% if has_project -%> + billingProject := "" + +<% if has_project -%> project, err := getProject(d, config) if err != nil { return nil, err } - <% end -%> - <% if object.supports_indirect_user_project_override -%> - var project string + billingProject = project +<% end -%> + +<% if object.supports_indirect_user_project_override -%> if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + billingProject = parts[1] } - <% end -%> - res, err := sendRequest(config, "<%= object.read_verb.to_s.upcase -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, url, nil<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) +<% end -%> + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "<%= object.read_verb.to_s.upcase -%>", billingProject, url, nil<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { return res, err } @@ -394,19 +412,28 @@ func resource<%= resource_name -%>Read(d *schema.ResourceData, meta interface{}) return err } + billingProject := "" + <% if has_project -%> project, err := getProject(d, config) if err != nil { return err } + billingProject = project <% end -%> + <% if object.supports_indirect_user_project_override -%> - var project string if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + billingProject = parts[1] } <% end -%> - res, err := sendRequest(config, "<%= object.read_verb.to_s.upcase -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, url, nil<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequest(config, "<%= object.read_verb.to_s.upcase -%>", billingProject, url, nil<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { <% if object.read_error_transform -%> return handleNotFoundError(<%= object.read_error_transform %>(err), d, fmt.Sprintf("<%= resource_name -%> %q", d.Id())) @@ -502,11 +529,14 @@ func resource<%= resource_name -%>Read(d *schema.ResourceData, meta interface{}) func resource<%= resource_name -%>Update(d *schema.ResourceData, meta interface{}) error { config := meta.(*Config) + billingProject := "" + <% if has_project -%> project, err := getProject(d, config) if err != nil { return err } + billingProject = project <% end -%> <% if object.input -%> @@ -526,10 +556,16 @@ if <%= props.map { |prop| "d.HasChange(\"#{prop.name.underscore}\")" }.join ' || } <% if object.supports_indirect_user_project_override -%> if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + billingProject = parts[1] } <% end -%> - getRes, err := sendRequest(config, "<%= object.read_verb.to_s.upcase -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, getUrl, nil<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + getRes, err := sendRequest(config, "<%= object.read_verb.to_s.upcase -%>", billingProject, getUrl, nil<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { return handleNotFoundError(err, d, fmt.Sprintf("<%= resource_name -%> %q", d.Id())) } @@ -594,12 +630,17 @@ if <%= props.map { |prop| "d.HasChange(\"#{prop.name.underscore}\")" }.join ' || return err } <% if object.supports_indirect_user_project_override -%> - var project string if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + billingProject = parts[1] } <% end -%> - res, err := sendRequestWithTimeout(config, "<%= key[:update_verb] -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, url, obj, d.Timeout(schema.TimeoutUpdate)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "<%= key[:update_verb] -%>", billingProject, url, obj, d.Timeout(schema.TimeoutUpdate)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { return fmt.Errorf("Error updating <%= object.name -%> %q: %s", d.Id(), err) } else { @@ -690,12 +731,17 @@ if <%= props.map { |prop| "d.HasChange(\"#{prop.name.underscore}\")" }.join ' || } <% end -%> <% if object.supports_indirect_user_project_override -%> - var project string - if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { + billingProject = parts[1] } <% end -%> - res, err := sendRequestWithTimeout(config, "<%= object.update_verb -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, url, obj, d.Timeout(schema.TimeoutUpdate)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) + + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "<%= object.update_verb -%>", billingProject, url, obj, d.Timeout(schema.TimeoutUpdate)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { return fmt.Errorf("Error updating <%= object.name -%> %q: %s", d.Id(), err) @@ -743,11 +789,14 @@ func resource<%= resource_name -%>Delete(d *schema.ResourceData, meta interface{ <% else -%> config := meta.(*Config) + billingProject := "" + <% if has_project -%> project, err := getProject(d, config) if err != nil { return err } + billingProject = project <% end -%> <% if object.mutex -%> @@ -781,14 +830,20 @@ func resource<%= resource_name -%>Delete(d *schema.ResourceData, meta interface{ <% end -%> <% end -%> <% if object.supports_indirect_user_project_override -%> - var project string + if parts := regexp.MustCompile(`projects\/([^\/]+)\/`).FindStringSubmatch(url); parts != nil { - project = parts[1] + billingProject = parts[1] } + <% end -%> log.Printf("[DEBUG] Deleting <%= object.name -%> %q", d.Id()) - res, err := sendRequestWithTimeout(config, "<%= object.delete_verb.to_s.upcase -%>", <% if has_project || object.supports_indirect_user_project_override %>project<% else %>""<% end %>, url, obj, d.Timeout(schema.TimeoutDelete)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) + // err == nil indicates that the billing_project value was found + if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp + } + + res, err := sendRequestWithTimeout(config, "<%= object.delete_verb.to_s.upcase -%>", billingProject, url, obj, d.Timeout(schema.TimeoutDelete)<%= object.error_retry_predicates ? ", " + object.error_retry_predicates.join(',') : "" -%>) if err != nil { return handleNotFoundError(err, d, "<%= object.name -%>") } diff --git a/third_party/terraform/utils/config.go.erb b/third_party/terraform/utils/config.go.erb index a1d3a2816173..6c827dcf61ae 100644 --- a/third_party/terraform/utils/config.go.erb +++ b/third_party/terraform/utils/config.go.erb @@ -67,6 +67,7 @@ type Config struct { Credentials string AccessToken string Project string + BillingProject string Region string Zone string Scopes []string diff --git a/third_party/terraform/utils/field_helpers.go b/third_party/terraform/utils/field_helpers.go index cb81f103070b..cb246f39531a 100644 --- a/third_party/terraform/utils/field_helpers.go +++ b/third_party/terraform/utils/field_helpers.go @@ -231,6 +231,17 @@ func getProjectFromSchema(projectSchemaField string, d TerraformResourceData, co return "", fmt.Errorf("%s: required field is not set", projectSchemaField) } +func getBillingProjectFromSchema(billingProjectSchemaField string, d TerraformResourceData, config *Config) (string, error) { + res, ok := d.GetOk(billingProjectSchemaField) + if ok && billingProjectSchemaField != "" { + return res.(string), nil + } + if config.BillingProject != "" { + return config.BillingProject, nil + } + return "", fmt.Errorf("%s: required field is not set", billingProjectSchemaField) +} + type OrganizationFieldValue struct { OrgId string Name string diff --git a/third_party/terraform/utils/provider.go.erb b/third_party/terraform/utils/provider.go.erb index 3587bd93e384..92b212785747 100644 --- a/third_party/terraform/utils/provider.go.erb +++ b/third_party/terraform/utils/provider.go.erb @@ -51,6 +51,14 @@ func Provider() terraform.ResourceProvider { }, nil), }, + "billing_project": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "GOOGLE_BILLING_PROJECT", + }, nil), + }, + "region": &schema.Schema{ Type: schema.TypeString, Optional: true, @@ -101,6 +109,9 @@ func Provider() terraform.ResourceProvider { "user_project_override": { Type: schema.TypeBool, Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{ + "USER_PROJECT_OVERRIDE", + }, nil), }, "request_timeout": { @@ -426,6 +437,7 @@ func providerConfigure(d *schema.ResourceData, p *schema.Provider, terraformVers Region: d.Get("region").(string), Zone: d.Get("zone").(string), UserProjectOverride: d.Get("user_project_override").(bool), + BillingProject: d.Get("billing_project").(string), terraformVersion: terraformVersion, } diff --git a/third_party/terraform/utils/utils.go.erb b/third_party/terraform/utils/utils.go.erb index 13c9b5e519b7..fe3aac9b171f 100644 --- a/third_party/terraform/utils/utils.go.erb +++ b/third_party/terraform/utils/utils.go.erb @@ -60,6 +60,12 @@ func getProject(d TerraformResourceData, config *Config) (string, error) { return getProjectFromSchema("project", d, config) } +// getBillingProject reads the "billing_project" field from the given resource data and falls +// back to the provider's value if not given. If no value is found, an error is returned. +func getBillingProject(d TerraformResourceData, config *Config) (string, error) { + return getBillingProjectFromSchema("billing_project", d, config) +} + // getProjectFromDiff reads the "project" field from the given diff and falls // back to the provider's value if not given. If the provider's value is not // given, an error is returned. diff --git a/third_party/terraform/website/docs/guides/provider_reference.html.markdown b/third_party/terraform/website/docs/guides/provider_reference.html.markdown index 5ae71b387b33..796b2ca0e50d 100644 --- a/third_party/terraform/website/docs/guides/provider_reference.html.markdown +++ b/third_party/terraform/website/docs/guides/provider_reference.html.markdown @@ -107,6 +107,11 @@ resource project for preconditions, quota, and billing, instead of the project the credentials belong to. Not all resources support this- see the documentation for each resource to learn whether it does. +* `billing_project` - (Optional) This fields specifies a project that's used for +preconditions, quota, and billing for requests. All resources that support user project +overrides will use this project instead of the resource's project (if available). This +field is ignored if `user_project_override` is set to false or unset. + * `{{service}}_custom_endpoint` - (Optional) The endpoint for a service's APIs, such as `compute_custom_endpoint`. Defaults to the production GCP endpoint for the service. This can be used to configure the Google provider to communicate @@ -179,6 +184,13 @@ following ordered by precedence. --- +* `billing_project` - (Optional) This fields allows Terraform to set X-Goog-User-Project +for APIs that require a billing project to be specified like Access Context Manager APIs if +User ADCs are being used. This can also be +specified using the `GOOGLE_BILLING_PROJECT` environment variable. + +--- + * `region` - (Optional) The default region to manage resources in. If another region is specified on a regional resource, it will take precedence. Alternatively, this can be specified using the `GOOGLE_REGION` environment @@ -360,7 +372,8 @@ to create the resource. This may help in those cases. * `user_project_override` - (Optional) Defaults to false. If true, uses the resource project for preconditions, quota, and billing, instead of the project the credentials belong to. Not all resources support this- see the -documentation for each resource to learn whether it does. +documentation for each resource to learn whether it does. Alternatively, this can +be specified using the `USER_PROJECT_OVERRIDE` environment variable. When set to false, the project the credentials belong to will be billed for the request, and quota / API enablement checks will be done against that project.