diff --git a/api/resource/iam_policy.rb b/api/resource/iam_policy.rb index ff95136fe771..fd913023a9f1 100644 --- a/api/resource/iam_policy.rb +++ b/api/resource/iam_policy.rb @@ -30,11 +30,40 @@ class IamPolicy < Api::Object # While Compute subnetwork uses {resource}/getIamPolicy attr_reader :method_name_separator + # The terraform type of the parent resource if it is not the same as the + # IAM resource. The IAP product needs these as its IAM policies refer + # to compute resources + attr_reader :parent_resource_type + + # Some resources allow retrieving the IAM policy with GET requests, + # others expect POST requests + attr_reader :fetch_iam_policy_verb + + # Certain resources allow different sets of roles to be set with IAM policies + # This is a role that is acceptable for the given IAM policy resource for use in tests + attr_reader :allowed_iam_role + + # Certain resources need an attribute other than "id" from their parent resource + # Especially when a parent is not the same type as the IAM resource + attr_reader :parent_resource_attribute + + # If the IAM resource test needs a new project to be created, this is the name of the project + attr_reader :test_project_name + + # Resource name may need a custom diff suppress function. Default is to use + # compareSelfLinkOrResourceName + attr_reader :custom_diff_suppress + def validate super check :exclude, type: :boolean, default: false check :method_name_separator, type: String, default: '/' + check :parent_resource_type, type: String + check :fetch_iam_policy_verb, type: Symbol, default: :GET, allowed: %i[GET POST] + check :allowed_iam_role, type: String, default: 'roles/viewer' + check :parent_resource_attribute, type: String, default: 'id' + check :test_project_name, type: String end end end diff --git a/build/terraform b/build/terraform index 1246621080fa..2bfcf069aa39 160000 --- a/build/terraform +++ b/build/terraform @@ -1 +1 @@ -Subproject commit 1246621080faa6366b20750efe3c9d35cbef9025 +Subproject commit 2bfcf069aa39c593fdfee73222e9dfca83cd8469 diff --git a/build/terraform-beta b/build/terraform-beta index 43af0afd382d..0ca79ed11cbb 160000 --- a/build/terraform-beta +++ b/build/terraform-beta @@ -1 +1 @@ -Subproject commit 43af0afd382d6297b88d807d3bbee1f97e4327f2 +Subproject commit 0ca79ed11cbb5a6cbb13579322386bbd46108cb4 diff --git a/build/terraform-mapper b/build/terraform-mapper index 670010538372..ba6c8535428b 160000 --- a/build/terraform-mapper +++ b/build/terraform-mapper @@ -1 +1 @@ -Subproject commit 67001053837274140ea6665fb365c86e6f5c09e8 +Subproject commit ba6c8535428b660d6fc649744c9fb3ecbef7fd17 diff --git a/products/iap/api.yaml b/products/iap/api.yaml new file mode 100644 index 000000000000..c32c8b9f3e76 --- /dev/null +++ b/products/iap/api.yaml @@ -0,0 +1,75 @@ +# Copyright 2019 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- !ruby/object:Api::Product +name: Iap +display_name: Identity-Aware Proxy +versions: + - !ruby/object:Api::Product::Version + name: ga + base_url: https://iap.googleapis.com/v1/ +scopes: + - https://www.googleapis.com/auth/cloud-platform +apis_required: + - !ruby/object:Api::Product::ApiReference + name: Cloud Identity-Aware Proxy + url: https://console.cloud.google.com/apis/library/iap.googleapis.com/ +objects: + - !ruby/object:Api::Resource + name: 'Web' + base_url: 'projects/{{project}}/iap_web' + self_link: 'projects/{{project}}/iap_web' + exclude_resource: true + description: | + Only used to generate IAM resources + properties: + - !ruby/object:Api::Type::String + name: 'name' + description: Dummy property. + required: true + - !ruby/object:Api::Resource + name: 'WebTypeCompute' + base_url: 'projects/{{project}}/iap_web/compute' + self_link: 'projects/{{project}}/iap_web/compute' + exclude_resource: true + description: | + Only used to generate IAM resources + properties: + - !ruby/object:Api::Type::String + name: 'name' + description: Dummy property. + required: true + - !ruby/object:Api::Resource + name: 'WebTypeAppEngine' + base_url: 'projects/{{project}}/iap_web/appengine-{{appId}}' + self_link: 'projects/{{project}}/iap_web/appengine-{{appId}}' + exclude_resource: true + description: | + Only used to generate IAM resources + properties: + - !ruby/object:Api::Type::String + name: 'appId' + description: Id of the App Engine application. + required: true + - !ruby/object:Api::Resource + name: 'WebBackendService' + base_url: 'projects/{{project}}/iap_web/compute/services/{{backendServiceName}}' + self_link: 'projects/{{project}}/iap_web/compute/services/{{backendServiceName}}' + exclude_resource: true + description: | + Only used to generate IAM resources + properties: + - !ruby/object:Api::Type::String + name: 'backendServiceName' + description: Name or self link of a backend service. + required: true diff --git a/products/iap/terraform.yaml b/products/iap/terraform.yaml new file mode 100644 index 000000000000..d219991274c7 --- /dev/null +++ b/products/iap/terraform.yaml @@ -0,0 +1,92 @@ +# Copyright 2019 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- !ruby/object:Provider::Terraform::Config +overrides: !ruby/object:Overrides::ResourceOverrides + Web: !ruby/object:Overrides::Terraform::ResourceOverride + id_format: "projects/{{project}}/iap_web" + import_format: ["projects/{{project}}/iap_web"] + iam_policy: !ruby/object:Api::Resource::IamPolicy + exclude: false + method_name_separator: ':' + parent_resource_type: 'google_project_service' + fetch_iam_policy_verb: :POST + allowed_iam_role: 'roles/iap.httpsResourceAccessor' + parent_resource_attribute: 'project' + examples: + - !ruby/object:Provider::Terraform::Examples + name: "iap_project" + primary_resource_id: "project_service" + primary_resource_name: "fmt.Sprintf(\"tf-test%s\", context[\"random_suffix\"])" + test_env_vars: + org_id: :ORG_ID + WebTypeCompute: !ruby/object:Overrides::Terraform::ResourceOverride + iam_policy: !ruby/object:Api::Resource::IamPolicy + exclude: false + method_name_separator: ':' + parent_resource_type: 'google_project_service' + parent_resource_attribute: 'project' + fetch_iam_policy_verb: :POST + allowed_iam_role: 'roles/iap.httpsResourceAccessor' + id_format: "projects/{{project}}/iap_web/compute" + import_format: ["projects/{{project}}/iap_web/compute"] + examples: + - !ruby/object:Provider::Terraform::Examples + name: "iap_project" + primary_resource_id: "project_service" + primary_resource_name: "fmt.Sprintf(\"tf-test%s\", context[\"random_suffix\"])" + test_env_vars: + org_id: :ORG_ID + WebTypeAppEngine: !ruby/object:Overrides::Terraform::ResourceOverride + iam_policy: !ruby/object:Api::Resource::IamPolicy + exclude: false + method_name_separator: ':' + parent_resource_type: 'google_app_engine_application' + parent_resource_attribute: 'app_id' + fetch_iam_policy_verb: :POST + allowed_iam_role: 'roles/iap.httpsResourceAccessor' + test_project_name: "tf-test" + custom_diff_suppress: 'templates/terraform/iam/iap_web_appengine_diff_suppress.go.erb' + id_format: "projects/{{project}}/iap_web/appengine-{{appId}}" + import_format: ["projects/{{project}}/iap_web/appengine-{{appId}}"] + examples: + - !ruby/object:Provider::Terraform::Examples + name: "iap_appengine" + primary_resource_id: "app" + primary_resource_name: "context[\"project_id\"]" + test_env_vars: + org_id: :ORG_ID + WebBackendService: !ruby/object:Overrides::Terraform::ResourceOverride + iam_policy: !ruby/object:Api::Resource::IamPolicy + exclude: false + method_name_separator: ':' + parent_resource_type: 'google_compute_backend_service' + parent_resource_attribute: 'name' + fetch_iam_policy_verb: :POST + allowed_iam_role: 'roles/iap.httpsResourceAccessor' + id_format: "projects/{{project}}/iap_web/compute/services/{{backendServiceName}}" + import_format: ["projects/{{project}}/iap_web/compute/services/{{backendServiceName}}"] + examples: + - !ruby/object:Provider::Terraform::Examples + name: "backend_service_basic" + primary_resource_id: "default" + vars: + backend_service_name: "backend-service" + http_health_check_name: "health-check" + primary_resource_name: "fmt.Sprintf(\"backend-service%s\", context[\"random_suffix\"])" +# This is for copying files over +files: !ruby/object:Provider::Config::Files + # These files have templating (ERB) code that will be run. + # This is usually to add licensing info, autogeneration notices, etc. + compile: +<%= lines(indent(compile('provider/terraform/product~compile.yaml'), 4)) -%> diff --git a/provider/terraform.rb b/provider/terraform.rb index 73e330f35aec..d890ddc9cfea 100644 --- a/provider/terraform.rb +++ b/provider/terraform.rb @@ -242,5 +242,9 @@ def build_object_data(object, output_folder, version) build_env ) end + + def extract_identifiers(url) + url.scan(/\{\{(\w+)\}\}/).flatten + end end end diff --git a/templates/terraform/examples/base_configs/iam_test_file.go.erb b/templates/terraform/examples/base_configs/iam_test_file.go.erb index b1dedc655165..7b01173fc39a 100644 --- a/templates/terraform/examples/base_configs/iam_test_file.go.erb +++ b/templates/terraform/examples/base_configs/iam_test_file.go.erb @@ -17,7 +17,7 @@ import ( <% resource_name = product_ns + object.name -%> <% individual_url = object.self_link_url -params = individual_url.scan(/({{)(\w+)(}})/).map { |arr| arr[1] } +params = extract_identifiers(individual_url.gsub('{{name}}', "{{#{object.name.underscore}}}")) -%> <% tf_product = (@config.legacy_name || product_ns).underscore @@ -26,34 +26,37 @@ params = individual_url.scan(/({{)(\w+)(}})/).map { |arr| arr[1] } <% import_url = individual_url.gsub(/({{)(\w+)(}})/, '%s').gsub(object.__product.base_url, '') -%> <% import_str = Array.new(params.length, '%s').join('/') -%> <% import_qualifiers = [] -%> -<% params.each do |param| -%> +<% params.each_with_index do |param, i| -%> <% if param == 'project' -%> - <% import_qualifiers.push('getTestProjectFromEnv()') -%> + <% if i != params.size - 1 -%> + <%# If the last parameter is project then we want to create a new project to use for the test, so don't default from the environment -%> + <% if object.iam_policy.test_project_name.nil? -%> + <% import_qualifiers.push('getTestProjectFromEnv()') -%> + <% else -%> + <% import_qualifiers.push('context["project_id"]') -%> + <% end -%> + <% end -%> <% elsif param == 'zone' -%> - <% import_qualifiers.push('getTestZoneFromEnv(t)') -%> + <% import_qualifiers.push('getTestZoneFromEnv()') -%> <% elsif param == 'region' || param == 'location' -%> - <% import_qualifiers.push('getTestRegionFromEnv(t)') -%> + <% import_qualifiers.push('getTestRegionFromEnv()') -%> <% end -%> <% end -%> func TestAcc<%= resource_name -%>IamBindingGenerated(t *testing.T) { t.Parallel() - context := map[string]interface{}{ - "random_suffix": acctest.RandString(10), - "role": "roles/editor", - } +<%= lines(compile('templates/terraform/iam/iam_context.go.erb')) -%> resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheck<%= resource_name -%>Destroy, Steps: []resource.TestStep{ { Config: testAcc<%= resource_name -%>IamBinding_basicGenerated(context), }, { ResourceName: "<%= terraform_name -%>_iam_binding.foo", - ImportStateId: fmt.Sprintf("<%= import_url -%> roles/editor", <%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), + ImportStateId: fmt.Sprintf("<%= import_url -%> <%= object.iam_policy.allowed_iam_role -%>"<% unless import_qualifiers.empty? -%>, <% end -%><%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), ImportState: true, ImportStateVerify: true, }, @@ -63,7 +66,7 @@ func TestAcc<%= resource_name -%>IamBindingGenerated(t *testing.T) { }, { ResourceName: "<%= terraform_name -%>_iam_binding.foo", - ImportStateId: fmt.Sprintf("<%= import_url -%> roles/editor", <%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), + ImportStateId: fmt.Sprintf("<%= import_url -%> <%= object.iam_policy.allowed_iam_role -%>"<% unless import_qualifiers.empty? -%>, <% end -%><%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), ImportState: true, ImportStateVerify: true, }, @@ -74,15 +77,11 @@ func TestAcc<%= resource_name -%>IamBindingGenerated(t *testing.T) { func TestAcc<%= resource_name -%>IamMemberGenerated(t *testing.T) { t.Parallel() - context := map[string]interface{}{ - "random_suffix": acctest.RandString(10), - "role": "roles/editor", - } +<%= lines(compile('templates/terraform/iam/iam_context.go.erb')) -%> resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheck<%= resource_name -%>Destroy, Steps: []resource.TestStep{ { // Test Iam Member creation (no update for member, no need to test) @@ -90,7 +89,7 @@ func TestAcc<%= resource_name -%>IamMemberGenerated(t *testing.T) { }, { ResourceName: "<%= terraform_name -%>_iam_member.foo", - ImportStateId: fmt.Sprintf("<%= import_url -%> roles/editor user:admin@hashicorptest.com", <%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), + ImportStateId: fmt.Sprintf("<%= import_url -%> <%= object.iam_policy.allowed_iam_role -%> user:admin@hashicorptest.com"<% unless import_qualifiers.empty? -%>, <% end -%><%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), ImportState: true, ImportStateVerify: true, }, @@ -101,22 +100,18 @@ func TestAcc<%= resource_name -%>IamMemberGenerated(t *testing.T) { func TestAcc<%= resource_name -%>IamPolicyGenerated(t *testing.T) { t.Parallel() - context := map[string]interface{}{ - "random_suffix": acctest.RandString(10), - "role": "roles/editor", - } +<%= lines(compile('templates/terraform/iam/iam_context.go.erb')) -%> resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheck<%= resource_name -%>Destroy, Steps: []resource.TestStep{ { Config: testAcc<%= resource_name -%>IamPolicy_basicGenerated(context), }, { ResourceName: "<%= terraform_name -%>_iam_policy.foo", - ImportStateId: fmt.Sprintf("<%= import_url -%>", <%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), + ImportStateId: fmt.Sprintf("<%= import_url -%>"<% unless import_qualifiers.empty? -%>, <% end -%><%= import_qualifiers.join(', ') -%>, <%= example.primary_resource_name -%>), ImportState: true, ImportStateVerify: true, }, @@ -129,9 +124,9 @@ func testAcc<%= resource_name -%>IamMember_basicGenerated(context map[string]int <%= example.config_test_body -%> resource "<%= terraform_name -%>_iam_member" "foo" { - <%= object.name.underscore -%> = "${<%= terraform_name -%>.<%= example.primary_resource_id -%>.id}" - role = "%{role}" - member = "user:admin@hashicorptest.com" +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + role = "%{role}" + member = "user:admin@hashicorptest.com" } `, context) } @@ -142,14 +137,14 @@ func testAcc<%= resource_name -%>IamPolicy_basicGenerated(context map[string]int data "google_iam_policy" "foo" { binding { - role = "%{role}" + role = "%{role}" members = ["user:admin@hashicorptest.com"] } } resource "<%= terraform_name -%>_iam_policy" "foo" { - <%= object.name.underscore -%> = "${<%= terraform_name -%>.<%= example.primary_resource_id -%>.id}" - policy_data = "${data.google_iam_policy.foo.policy_data}" +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + policy_data = "${data.google_iam_policy.foo.policy_data}" } `, context) } @@ -159,9 +154,9 @@ func testAcc<%= resource_name -%>IamBinding_basicGenerated(context map[string]in <%= example.config_test_body -%> resource "<%= terraform_name -%>_iam_binding" "foo" { - <%= object.name.underscore -%> = "${<%= terraform_name -%>.<%= example.primary_resource_id -%>.id}" - role = "%{role}" - members = ["user:admin@hashicorptest.com"] +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + role = "%{role}" + members = ["user:admin@hashicorptest.com"] } `, context) } @@ -171,9 +166,9 @@ func testAcc<%= resource_name -%>IamBinding_updateGenerated(context map[string]i <%= example.config_test_body -%> resource "<%= terraform_name -%>_iam_binding" "foo" { - <%= object.name.underscore -%> = "${<%= terraform_name -%>.<%= example.primary_resource_id -%>.id}" - role = "%{role}" - members = ["user:admin@hashicorptest.com", "user:paddy@hashicorp.com"] +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + role = "%{role}" + members = ["user:admin@hashicorptest.com", "user:paddy@hashicorp.com"] } `, context) } diff --git a/templates/terraform/examples/iap_appengine.tf.erb b/templates/terraform/examples/iap_appengine.tf.erb new file mode 100644 index 000000000000..931226248fd6 --- /dev/null +++ b/templates/terraform/examples/iap_appengine.tf.erb @@ -0,0 +1,15 @@ +resource "google_project" "my_project" { + name = "%{project_id}" + project_id = "%{project_id}" + org_id = "%{org_id}" +} + +resource "google_project_service" "project_service" { + project = "${google_project.my_project.project_id}" + service = "iap.googleapis.com" +} + +resource "google_app_engine_application" "app" { + project = "${google_project_service.project_service.project}" + location_id = "us-central" +} diff --git a/templates/terraform/examples/iap_project.tf.erb b/templates/terraform/examples/iap_project.tf.erb new file mode 100644 index 000000000000..8d93a6af5a0e --- /dev/null +++ b/templates/terraform/examples/iap_project.tf.erb @@ -0,0 +1,10 @@ +resource "google_project" "project" { + project_id = "tf-test%{random_suffix}" + name = "tf-test%{random_suffix}" + org_id = "%{org_id}" +} + +resource "google_project_service" "project_service" { + project = "${google_project.project.project_id}" + service = "iap.googleapis.com" +} diff --git a/templates/terraform/iam/iam_attributes.go.erb b/templates/terraform/iam/iam_attributes.go.erb new file mode 100644 index 000000000000..1591fe647095 --- /dev/null +++ b/templates/terraform/iam/iam_attributes.go.erb @@ -0,0 +1,13 @@ +<% example = object.examples.reject(&:skip_test) + .reject { |e| @api.version_obj_or_closest(version) < @api.version_obj_or_closest(e.min_version) } + .first -%> +<% parent_resource_last_param_name = extract_identifiers(object.self_link_url).last -%> +<% parent_resource_type_type = object.iam_policy.parent_resource_type || terraform_name -%> +<% params.each_with_index do |p, i| -%> +<% if i == params.size - 1 -%> +<% attribute = object.iam_policy.parent_resource_attribute || parent_resource_last_param_name.underscore -%> +<% else -%> +<% attribute = p.underscore -%> +<% end -%> + <%= p.underscore -%> = "${<%= parent_resource_type_type -%>.<%= example.primary_resource_id -%>.<%= attribute -%>}" +<% end -%> \ No newline at end of file diff --git a/templates/terraform/iam/iam_context.go.erb b/templates/terraform/iam/iam_context.go.erb new file mode 100644 index 000000000000..9169e2b51b8a --- /dev/null +++ b/templates/terraform/iam/iam_context.go.erb @@ -0,0 +1,12 @@ +context := map[string]interface{}{ + "random_suffix": acctest.RandString(10), + "role": "<%= object.iam_policy.allowed_iam_role -%>", +<% unless object.iam_policy.test_project_name.nil? -%> + "project_id" : fmt.Sprintf("<%= object.iam_policy.test_project_name -%>%s", acctest.RandString(10)), +<% end -%> +<% example.test_env_vars&.each do |var_name, var_type| -%> +<% if var_type == :ORG_ID -%> + "<%= var_name -%>": getTestOrgFromEnv(t), +<% end -%> +<% end -%> +} \ No newline at end of file diff --git a/templates/terraform/iam/iap_web_appengine_diff_suppress.go.erb b/templates/terraform/iam/iap_web_appengine_diff_suppress.go.erb new file mode 100644 index 000000000000..9ea4f0101332 --- /dev/null +++ b/templates/terraform/iam/iap_web_appengine_diff_suppress.go.erb @@ -0,0 +1,12 @@ +func <%= resource_name -%>DiffSuppress(_, old, new string, _ *schema.ResourceData) bool { + newParts := strings.Split(new, "appengine-") + + if len(newParts) == 1 { + // `new` is only the app engine id + // `old` is always a long name + if strings.HasSuffix(old, fmt.Sprintf("appengine-%s", new)) { + return true + } + } + return old == new +} \ No newline at end of file diff --git a/templates/terraform/iam_policy.go.erb b/templates/terraform/iam_policy.go.erb index 6a78dec604d2..b6c29853cce8 100644 --- a/templates/terraform/iam_policy.go.erb +++ b/templates/terraform/iam_policy.go.erb @@ -29,12 +29,12 @@ import ( <% resource_name = product_ns + object.name -%> <% resource_uri = object.self_link_url -resource_params = resource_uri.gsub('{{name}}', "{{#{object.name.underscore}}}").scan(/({{)(\w+)(}})/).map { |arr| arr[1] } +resource_params = extract_identifiers(resource_uri.gsub('{{name}}', "{{#{object.name.underscore}}}")) -%> var <%= resource_name -%>IamSchema = map[string]*schema.Schema{ -<% resource_params.each do |param| -%> - "<%= param -%>": { +<% resource_params.each_with_index do |param, i| -%> + "<%= param.underscore -%>": { Type: schema.TypeString, <% if ['project', 'zone', 'region', 'location'].include?(param) -%> Computed: true, @@ -43,16 +43,25 @@ var <%= resource_name -%>IamSchema = map[string]*schema.Schema{ Required: true, <% end # if ...include?(param) -%> ForceNew: true, -<% if param == object.name.underscore -%> +<%# The last parameter can be used as a long name for IAM policies -%> +<% if i == resource_params.size - 1 -%> +<% if object.iam_policy.custom_diff_suppress.nil? -%> DiffSuppressFunc: compareSelfLinkOrResourceName, +<% else -%> + DiffSuppressFunc: <%= resource_name -%>DiffSuppress, +<% end -%> <% end # param == object.name -%> }, -<% end # resource_params.each -%> +<% end # i == resource_params.size - 1 -%> } +<% unless object.iam_policy.custom_diff_suppress.nil? -%> +<%= lines(compile(object.iam_policy.custom_diff_suppress)) -%> +<% end -%> + type <%= resource_name -%>IamUpdater struct { <% resource_params.each do |param| -%> - <%= param -%> string + <%= param.camelize(:lower) -%> string <% end # resource_params.each -%> d *schema.ResourceData Config *Config @@ -73,7 +82,8 @@ func <%= resource_name -%>IamUpdaterProducer(d *schema.ResourceData, config *Con values["project"] = project <% end -%> - m, err := getImportIdQualifiers([]string{"<%= import_id_formats(object).map{|s| format2regex s}.map{|s| s.gsub('', "<#{object.name.underscore}>")}.join('","') -%>"}, d, config, d.Get("<%= object.name.downcase -%>").(string)) + // We may have gotten either a long or short name, so attempt to parse long name if possible + m, err := getImportIdQualifiers([]string{"<%= import_id_formats(object).map{|s| format2regex s}.map{|s| s.gsub('', "<#{object.name.underscore}>")}.join('","') -%>"}, d, config, d.Get("<%= resource_params.last.underscore -%>").(string)) if err != nil { return nil, err } @@ -84,11 +94,26 @@ func <%= resource_name -%>IamUpdaterProducer(d *schema.ResourceData, config *Con u := &<%= resource_name -%>IamUpdater{ <% resource_params.each do |param| -%> - <%= param -%>: values["<%= param -%>"], + <%= param.camelize(:lower) -%>: values["<%= param -%>"], <% end -%> d: d, Config: config, } + +<%# Set all URL qualifiers in state so that we have consistent storage of needed fields -%> +<% resource_params.each_with_index do |param, i| -%> +<% if i == resource_params.size - 1 -%> +<% if param == 'project' -%> + d.Set("project", u.project) +<% else -%> +<%# Set the last parameter as the long name (unless it is project) -%> + d.Set("<%= resource_params.last.underscore -%>", u.GetResourceId()) +<% end -%> +<% else -%> + d.Set("<%= param.underscore -%>", u.<%= param.camelize(:lower) -%>) +<% end -%> +<% end -%> + d.SetId(u.GetResourceId()) return u, nil @@ -117,12 +142,18 @@ func <%= resource_name -%>IdParseFunc(d *schema.ResourceData, config *Config) er u := &<%= resource_name -%>IamUpdater{ <% resource_params.each do |param| -%> - <%= param -%>: values["<%= param -%>"], + <%= param.camelize(:lower) -%>: values["<%= param -%>"], <% end -%> d: d, Config: config, } - d.Set("<%= object.name.underscore -%>", u.GetResourceId()) +<% if resource_params.last == 'project' -%> +<%# Resource is only identified by project, so only set project -%> + d.Set("project", u.project) +<% else -%> +<%# Set resource long name in state, this has all the information that we need to identify it -%> + d.Set("<%= resource_params.last.underscore -%>", u.GetResourceId()) +<% end -%> d.SetId(u.GetResourceId()) return nil } @@ -137,7 +168,7 @@ func (u *<%= resource_name -%>IamUpdater) GetResourceIamPolicy() (*cloudresource } <% end -%> - policy, err := sendRequest(u.Config, "GET", <% if resource_params.include?('project') %>project<% else %>""<% end %>, url, nil) + policy, err := sendRequest(u.Config, "<%= object.iam_policy.fetch_iam_policy_verb.to_s.upcase -%>", <% if resource_params.include?('project') %>project<% else %>""<% end %>, url, nil) if err != nil { return nil, errwrap.Wrapf(fmt.Sprintf("Error retrieving IAM policy for %s: {{err}}", u.DescribeResource()), err) } @@ -178,7 +209,8 @@ func (u *<%= resource_name -%>IamUpdater) SetResourceIamPolicy(policy *cloudreso } <% import_url = resource_uri.gsub(/({{)(\w+)(}})/, '%s').gsub(object.__product.base_url, '') -%> -<% string_qualifiers = resource_params.map{|param| "u.#{param}"}.join(', ') -%> +<% string_qualifiers = resource_params.map{|param| "u.#{param.camelize(:lower)}"}.join(', ') -%> +<%# TODO(slevenick): this should use normal resource qualify methods to replace base_url -%> func (u *<%= resource_name -%>IamUpdater) qualify<%= object.name -%>Url(methodIdentifier string) string { return fmt.Sprintf("<%= object.__product.base_url -%>%s<%= object.iam_policy.method_name_separator -%>%s", fmt.Sprintf("<%= import_url -%>", <%= string_qualifiers -%>), methodIdentifier) } diff --git a/templates/terraform/resource_iam.html.markdown.erb b/templates/terraform/resource_iam.html.markdown.erb index 44f953ecd551..4c1319d96674 100644 --- a/templates/terraform/resource_iam.html.markdown.erb +++ b/templates/terraform/resource_iam.html.markdown.erb @@ -68,21 +68,27 @@ See [Provider Versions](https://terraform.io/docs/providers/google/provider_vers <% end -%> <% markdown_escaped_name = terraform_name.gsub("_", "\\_") %> +<% +params = extract_identifiers(object.self_link_url) +-%> +<% +url_properties = object.all_user_properties.select { |param| params.include?(param.name) } +-%> ## <%= markdown_escaped_name -%>\_policy ```hcl data "google_iam_policy" "admin" { - binding { - role = "roles/editor" - members = [ - "user:jane@example.com", - ] - } + binding { + role = "<%= object.iam_policy.allowed_iam_role -%>" + members = [ + "user:jane@example.com", + ] + } } resource "<%= terraform_name -%>_policy" "editor" { - <%= object.name.underscore -%> = "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}")-%>" - policy_data = "${data.google_iam_policy.admin.policy_data}" +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + policy_data = "${data.google_iam_policy.admin.policy_data}" } ``` @@ -90,12 +96,11 @@ resource "<%= terraform_name -%>_policy" "editor" { ```hcl resource "<%= terraform_name -%>_binding" "editor" { - <%= object.name.underscore -%> = "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}")-%>" - - role = "roles/editor" - members = [ - "user:jane@example.com", - ] +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + role = "<%= object.iam_policy.allowed_iam_role -%>" + members = [ + "user:jane@example.com", + ] } ``` @@ -103,10 +108,9 @@ resource "<%= terraform_name -%>_binding" "editor" { ```hcl resource "<%= terraform_name -%>_member" "editor" { - <%= object.name.underscore -%> = "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}")-%>" - - role = "roles/editor" - member = "user:jane@example.com" +<%= lines(compile('templates/terraform/iam/iam_attributes.go.erb')) -%> + role = "<%= object.iam_policy.allowed_iam_role -%>" + member = "user:jane@example.com" } ``` @@ -114,10 +118,19 @@ resource "<%= terraform_name -%>_member" "editor" { The following arguments are supported: -* `<%= object.name.downcase -%>` - (Required) The <%= object.name.downcase -%> name or id to bind to attach IAM policy to. +<% url_properties.each do |param| -%> +<% if param.name == "name" -%> +* `<%= object.name.underscore -%>` - (Required) Used to find the parent resource to bind the IAM policy to +<% else -%> +* `<%= param.name.underscore -%>` - (Required) <%= param.description -%> Used to find the parent resource to bind the IAM policy to +<% end -%> +<% end -%> +<% if object.base_url.include?("{{project}}")-%> +<%# The following new line allow for project to be bullet-formatted properly. -%> -* `project` - (Optional) The project in which the resource belongs. If it - is not provided, the provider project is used. +* `project` - (Optional) The ID of the project in which the resource belongs. + If it is not provided, the project will be parsed from the identifier of the parent resource. If no project is provided in the parent identifier and no project is specified, the provider project is used. +<% end -%> * `member/members` - (Required) Identities that will be granted the privilege in `role`. Each entry can have one of the following values: @@ -140,18 +153,18 @@ The following arguments are supported: In addition to the arguments listed above, the following computed attributes are exported: -* `etag` - (Computed) The etag of the <%= object.name.downcase -%>'s IAM policy. +* `etag` - (Computed) The etag of the IAM policy. ## Import -<%= product_ns -%> <%= object.name.downcase -%> IAM resources can be imported using the project, <%= object.name.downcase -%> name, role and member. +<%= product_ns -%> <%= object.name.downcase -%> IAM resources can be imported using the project, resource identifiers, role and member. ``` -$ terraform import <%= terraform_name -%>_policy.editor <%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}") -%> +$ terraform import <%= terraform_name -%>_policy.editor <%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}") %> -$ terraform import <%= terraform_name -%>_binding.editor "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}") -%> roles/editor" +$ terraform import <%= terraform_name -%>_binding.editor "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}") -%> <%= object.iam_policy.allowed_iam_role -%>" -$ terraform import <%= terraform_name -%>_member.editor "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}") -%> roles/editor jane@example.com" +$ terraform import <%= terraform_name -%>_member.editor "<%= object.id_format.gsub('{{name}}', "{{#{object.name.underscore}}}") -%> <%= object.iam_policy.allowed_iam_role -%> jane@example.com" ``` -> If you're importing a resource with beta features, make sure to include `-provider=google-beta` diff --git a/third_party/terraform/resources/resource_app_engine_application.go b/third_party/terraform/resources/resource_app_engine_application.go index 96901c772af8..455cc4f710ff 100644 --- a/third_party/terraform/resources/resource_app_engine_application.go +++ b/third_party/terraform/resources/resource_app_engine_application.go @@ -64,6 +64,10 @@ func resourceAppEngineApplication() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "app_id": { + Type: schema.TypeString, + Computed: true, + }, "url_dispatch_rule": { Type: schema.TypeList, Computed: true, @@ -171,6 +175,7 @@ func resourceAppEngineApplicationRead(d *schema.ResourceData, meta interface{}) d.Set("default_hostname", app.DefaultHostname) d.Set("location_id", app.LocationId) d.Set("name", app.Name) + d.Set("app_id", app.Id) d.Set("serving_status", app.ServingStatus) d.Set("gcr_domain", app.GcrDomain) d.Set("project", pid) diff --git a/third_party/terraform/tests/resource_pubsub_topic_iam_test.go b/third_party/terraform/tests/resource_pubsub_topic_iam_test.go new file mode 100644 index 000000000000..8a1d61652bd3 --- /dev/null +++ b/third_party/terraform/tests/resource_pubsub_topic_iam_test.go @@ -0,0 +1,276 @@ +package google + +import ( + "fmt" + "reflect" + "sort" + "testing" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccPubsubTopicIamBinding(t *testing.T) { + t.Parallel() + + topic := "test-topic-iam-" + acctest.RandString(10) + account := "test-topic-iam-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test IAM Binding creation + Config: testAccPubsubTopicIamBinding_basic(topic, account), + Check: testAccCheckPubsubTopicIam(topic, "roles/pubsub.publisher", []string{ + fmt.Sprintf("serviceAccount:%s-1@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_pubsub_topic_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s roles/pubsub.publisher", getComputedTopicName(getTestProjectFromEnv(), topic)), + ImportState: true, + ImportStateVerify: true, + }, + { + // Test IAM Binding update + Config: testAccPubsubTopicIamBinding_update(topic, account), + Check: testAccCheckPubsubTopicIam(topic, "roles/pubsub.publisher", []string{ + fmt.Sprintf("serviceAccount:%s-1@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + fmt.Sprintf("serviceAccount:%s-2@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_pubsub_topic_iam_binding.foo", + ImportStateId: fmt.Sprintf("%s roles/pubsub.publisher", getComputedTopicName(getTestProjectFromEnv(), topic)), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccPubsubTopicIamBinding_topicName(t *testing.T) { + t.Parallel() + + topic := "test-topic-iam-" + acctest.RandString(10) + account := "test-topic-iam-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test IAM Binding creation + Config: testAccPubsubTopicIamBinding_topicName(topic, account), + Check: testAccCheckPubsubTopicIam(topic, "roles/pubsub.publisher", []string{ + fmt.Sprintf("serviceAccount:%s-1@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + // No import step- imports want the resource to be defined using the full id as the topic + }, + }) +} + +func TestAccPubsubTopicIamMember(t *testing.T) { + t.Parallel() + + topic := "test-topic-iam-" + acctest.RandString(10) + account := "test-topic-iam-" + acctest.RandString(10) + accountEmail := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // Test Iam Member creation (no update for member, no need to test) + Config: testAccPubsubTopicIamMember_basic(topic, account), + Check: testAccCheckPubsubTopicIam(topic, "roles/pubsub.publisher", []string{ + fmt.Sprintf("serviceAccount:%s", accountEmail), + }), + }, + { + ResourceName: "google_pubsub_topic_iam_member.foo", + ImportStateId: fmt.Sprintf("%s roles/pubsub.publisher serviceAccount:%s", getComputedTopicName(getTestProjectFromEnv(), topic), accountEmail), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccPubsubTopicIamPolicy(t *testing.T) { + t.Parallel() + + topic := "test-topic-iam-" + acctest.RandString(10) + account := "test-topic-iam-" + acctest.RandString(10) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccPubsubTopicIamPolicy_basic(topic, account, "roles/pubsub.publisher"), + Check: testAccCheckPubsubTopicIam(topic, "roles/pubsub.publisher", []string{ + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + Config: testAccPubsubTopicIamPolicy_basic(topic, account, "roles/pubsub.subscriber"), + Check: testAccCheckPubsubTopicIam(topic, "roles/pubsub.subscriber", []string{ + fmt.Sprintf("serviceAccount:%s@%s.iam.gserviceaccount.com", account, getTestProjectFromEnv()), + }), + }, + { + ResourceName: "google_pubsub_topic_iam_policy.foo", + ImportStateId: getComputedTopicName(getTestProjectFromEnv(), topic), + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckPubsubTopicIam(topic, role string, members []string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := testAccProvider.Meta().(*Config) + p, err := config.clientPubsub.Projects.Topics.GetIamPolicy(getComputedTopicName(getTestProjectFromEnv(), topic)).Do() + if err != nil { + return err + } + + for _, binding := range p.Bindings { + if binding.Role == role { + sort.Strings(members) + sort.Strings(binding.Members) + + if reflect.DeepEqual(members, binding.Members) { + return nil + } + + return fmt.Errorf("Binding found but expected members is %v, got %v", members, binding.Members) + } + } + + return fmt.Errorf("No binding for role %q", role) + } +} + +func testAccPubsubTopicIamBinding_topicName(topic, account string) string { + return fmt.Sprintf(` +resource "google_pubsub_topic" "topic" { + name = "%s" +} + +resource "google_service_account" "test-account-1" { + account_id = "%s-1" + display_name = "Iam Testing Account" +} + +resource "google_pubsub_topic_iam_binding" "foo" { + project = "%s" + topic = "${google_pubsub_topic.topic.name}" + role = "roles/pubsub.publisher" + members = [ + "serviceAccount:${google_service_account.test-account-1.email}", + ] +} +`, topic, account, getTestProjectFromEnv()) +} + +func testAccPubsubTopicIamBinding_basic(topic, account string) string { + return fmt.Sprintf(` +resource "google_pubsub_topic" "topic" { + name = "%s" +} + +resource "google_service_account" "test-account-1" { + account_id = "%s-1" + display_name = "Iam Testing Account" +} + +resource "google_pubsub_topic_iam_binding" "foo" { + # use the id instead of the name because it's more compatible with import + topic = "${google_pubsub_topic.topic.id}" + role = "roles/pubsub.publisher" + members = [ + "serviceAccount:${google_service_account.test-account-1.email}", + ] +} +`, topic, account) +} + +func testAccPubsubTopicIamBinding_update(topic, account string) string { + return fmt.Sprintf(` +resource "google_pubsub_topic" "topic" { + name = "%s" +} + +resource "google_service_account" "test-account-1" { + account_id = "%s-1" + display_name = "Iam Testing Account" +} + +resource "google_service_account" "test-account-2" { + account_id = "%s-2" + display_name = "Iam Testing Account" +} + +resource "google_pubsub_topic_iam_binding" "foo" { + # use the id instead of the name because it's more compatible with import + topic = "${google_pubsub_topic.topic.id}" + role = "roles/pubsub.publisher" + members = [ + "serviceAccount:${google_service_account.test-account-1.email}", + "serviceAccount:${google_service_account.test-account-2.email}", + ] +} +`, topic, account, account) +} + +func testAccPubsubTopicIamMember_basic(topic, account string) string { + return fmt.Sprintf(` +resource "google_pubsub_topic" "topic" { + name = "%s" +} + +resource "google_service_account" "test-account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +resource "google_pubsub_topic_iam_member" "foo" { + topic = "${google_pubsub_topic.topic.id}" + role = "roles/pubsub.publisher" + member = "serviceAccount:${google_service_account.test-account.email}" +} +`, topic, account) +} + +func testAccPubsubTopicIamPolicy_basic(topic, account, role string) string { + return fmt.Sprintf(` +resource "google_pubsub_topic" "topic" { + name = "%s" +} + +resource "google_service_account" "test-account" { + account_id = "%s" + display_name = "Iam Testing Account" +} + +data "google_iam_policy" "foo" { + binding { + role = "%s" + members = ["serviceAccount:${google_service_account.test-account.email}"] + } +} + +resource "google_pubsub_topic_iam_policy" "foo" { + topic = "${google_pubsub_topic.topic.id}" + policy_data = "${data.google_iam_policy.foo.policy_data}" +} +`, topic, account, role) +} diff --git a/third_party/terraform/utils/import.go b/third_party/terraform/utils/import.go index 999d2330bb39..98c9d7b1a56a 100644 --- a/third_party/terraform/utils/import.go +++ b/third_party/terraform/utils/import.go @@ -101,6 +101,7 @@ func setDefaultValues(idRegex string, d TerraformResourceData, config *Config) e // Parse an import id extracting field values using the given list of regexes. // They are applied in order. The first in the list is tried first. // This does not mutate any of the parameters, returning a map of matches +// Similar to parseImportId in import.go, but less import specific // // e.g: // - projects/(?P[^/]+)/regions/(?P[^/]+)/subnetworks/(?P[^/]+) (applied first) @@ -126,20 +127,22 @@ func getImportIdQualifiers(idRegexes []string, d TerraformResourceData, config * result[fieldName] = fieldValue } - // The first id format is applied first and contains all the fields. defaults, err := getDefaultValues(idRegexes[0], d, config) if err != nil { return nil, err } for k, v := range defaults { - result[k] = v + if _, ok := result[k]; !ok { + // Set any fields that are defaultable and not specified in import ID + result[k] = v + } } return result, nil } } - return nil, fmt.Errorf("Resource id %q doesn't match any of the accepted formats: %v", id, idRegexes) + return nil, fmt.Errorf("Import id %q doesn't match any of the accepted formats: %v", d.Id(), idRegexes) } // Returns a set of default values that are contained in a regular expression diff --git a/third_party/terraform/website-compiled/google.erb b/third_party/terraform/website-compiled/google.erb index 92edafa24b4c..8e572df95d1b 100644 --- a/third_party/terraform/website-compiled/google.erb +++ b/third_party/terraform/website-compiled/google.erb @@ -923,10 +923,10 @@ -<% unless version == 'ga' %> > Google IAP Resources -<% end -%> + > Google ML Engine Resources