diff --git a/README.md b/README.md index 7a9d7f40..027b5c61 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,7 @@ determining that location is as follows: | sa\_role | A role to give the default Service Account for the project (defaults to none) | `string` | `""` | no | | shared\_vpc\_subnets | List of subnets fully qualified subnet IDs (ie. projects/$project\_id/regions/$region/subnetworks/$subnet\_id) | `list(string)` | `[]` | no | | svpc\_host\_project\_id | The ID of the host project which hosts the shared VPC | `string` | `""` | no | +| tag\_binding\_values | Tag values to bind the project to. | `list(string)` | `[]` | no | | usage\_bucket\_name | Name of a GCS bucket to store GCE usage reports in (optional) | `string` | `""` | no | | usage\_bucket\_prefix | Prefix in the GCS bucket to store GCE usage reports in (optional) | `string` | `""` | no | | vpc\_service\_control\_attach\_enabled | Whether the project will be attached to a VPC Service Control Perimeter | `bool` | `false` | no | @@ -185,6 +186,7 @@ determining that location is as follows: | service\_account\_id | The id of the default service account | | service\_account\_name | The fully-qualified name of the default service account | | service\_account\_unique\_id | The unique id of the default service account | +| tag\_bindings | Tag bindings | diff --git a/build/int.cloudbuild.yaml b/build/int.cloudbuild.yaml index f017bf1b..7a480066 100644 --- a/build/int.cloudbuild.yaml +++ b/build/int.cloudbuild.yaml @@ -167,6 +167,21 @@ steps: - verify-quota-project-example name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' args: ['/bin/bash', '-c', 'cft test run TestQuotaProject --stage destroy --verbose'] +- id: apply-tags-project-example + waitFor: + - create-all + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestTagsProject --stage apply --verbose'] +- id: verify-tags-project-example + waitFor: + - apply-tags-project-example + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestTagsProject --stage verify --verbose'] +- id: destroy-tags-project-example + waitFor: + - verify-tags-project-example + name: 'gcr.io/cloud-foundation-cicd/$_DOCKER_IMAGE_DEVELOPER_TOOLS:$_DOCKER_TAG_VERSION_DEVELOPER_TOOLS' + args: ['/bin/bash', '-c', 'cft test run TestTagsProject --stage destroy --verbose'] tags: - 'ci' diff --git a/examples/tags_project/README.md b/examples/tags_project/README.md new file mode 100644 index 00000000..cd395369 --- /dev/null +++ b/examples/tags_project/README.md @@ -0,0 +1,22 @@ +# Project with tags + +This example illustrates how to create a project with a tag binding. + + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| billing\_account | The ID of the billing account to associate this project with | `any` | n/a | yes | +| folder\_id | The ID of a folder to host this project. | `string` | `null` | no | +| organization\_id | The organization id for the associated services | `string` | `"684124036889"` | no | +| tag\_value | value | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| project\_id | The ID of the created project | +| project\_num | The number of the created project | + + diff --git a/examples/tags_project/main.tf b/examples/tags_project/main.tf new file mode 100644 index 00000000..45bd1488 --- /dev/null +++ b/examples/tags_project/main.tf @@ -0,0 +1,28 @@ +/** + * Copyright 2024 Google LLC + * + * 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. + */ + +module "project-factory" { + source = "terraform-google-modules/project-factory/google" + version = "~> 14.0" + + random_project_id = true + name = "simple-tag-project" + org_id = var.organization_id + folder_id = var.folder_id + billing_account = var.billing_account + default_service_account = "deprivilege" + tag_binding_values = [var.tag_value] +} diff --git a/examples/tags_project/outputs.tf b/examples/tags_project/outputs.tf new file mode 100644 index 00000000..40421cdd --- /dev/null +++ b/examples/tags_project/outputs.tf @@ -0,0 +1,26 @@ +/** + * Copyright 2024 Google LLC + * + * 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. + */ + +output "project_id" { + value = module.project-factory.project_id + description = "The ID of the created project" +} + +output "project_num" { + value = module.project-factory.project_number + description = "The number of the created project" +} + diff --git a/examples/tags_project/variables.tf b/examples/tags_project/variables.tf new file mode 100644 index 00000000..60939f3e --- /dev/null +++ b/examples/tags_project/variables.tf @@ -0,0 +1,35 @@ +/** + * Copyright 2024 Google LLC + * + * 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. + */ + +variable "organization_id" { + description = "The organization id for the associated services" + default = "684124036889" +} + +variable "folder_id" { + description = "The ID of a folder to host this project." + type = string + default = null +} + +variable "billing_account" { + description = "The ID of the billing account to associate this project with" +} + +variable "tag_value" { + description = "value" + type = string +} diff --git a/main.tf b/main.tf index b98e0f3f..c2971a0e 100644 --- a/main.tf +++ b/main.tf @@ -68,6 +68,7 @@ module "project-factory" { vpc_service_control_perimeter_name = var.vpc_service_control_perimeter_name vpc_service_control_sleep_duration = var.vpc_service_control_sleep_duration default_network_tier = var.default_network_tier + tag_binding_values = var.tag_binding_values } /****************************************** diff --git a/modules/core_project_factory/main.tf b/modules/core_project_factory/main.tf index 3917419c..dbf0b178 100644 --- a/modules/core_project_factory/main.tf +++ b/modules/core_project_factory/main.tf @@ -371,3 +371,9 @@ resource "google_compute_project_default_network_tier" "default" { project = google_project.main.number network_tier = var.default_network_tier } + +resource "google_tags_tag_binding" "bindings" { + for_each = toset(var.tag_binding_values) + parent = "//cloudresourcemanager.googleapis.com/projects/${google_project.main.number}" + tag_value = "tagValues/${each.value}" +} diff --git a/modules/core_project_factory/outputs.tf b/modules/core_project_factory/outputs.tf index d5b7557b..dbb5f763 100644 --- a/modules/core_project_factory/outputs.tf +++ b/modules/core_project_factory/outputs.tf @@ -95,3 +95,8 @@ output "enabled_api_identities" { description = "Enabled API identities in the project" value = module.project_services.enabled_api_identities } + +output "tag_bindings" { + description = "Tag bindings" + value = google_tags_tag_binding.bindings +} diff --git a/modules/core_project_factory/variables.tf b/modules/core_project_factory/variables.tf index 45fc341d..f5202ed6 100644 --- a/modules/core_project_factory/variables.tf +++ b/modules/core_project_factory/variables.tf @@ -258,3 +258,9 @@ variable "grant_network_role" { type = bool default = true } + +variable "tag_binding_values" { + description = "Tag values to bind the project to." + type = list(string) + default = [] +} diff --git a/modules/core_project_factory/versions.tf b/modules/core_project_factory/versions.tf index f91915da..2dde8dd8 100644 --- a/modules/core_project_factory/versions.tf +++ b/modules/core_project_factory/versions.tf @@ -20,11 +20,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 3.50, < 6" + version = ">= 3.64, < 6" } google-beta = { source = "hashicorp/google-beta" - version = ">= 3.50, < 6" + version = ">= 3.64, < 6" } null = { source = "hashicorp/null" diff --git a/outputs.tf b/outputs.tf index 6e093985..41de9e88 100644 --- a/outputs.tf +++ b/outputs.tf @@ -98,3 +98,8 @@ output "budget_name" { value = module.budget.name description = "The name of the budget if created" } + +output "tag_bindings" { + description = "Tag bindings" + value = module.project-factory.tag_bindings +} diff --git a/test/integration/tags_project/tags_project_test.go b/test/integration/tags_project/tags_project_test.go new file mode 100644 index 00000000..d752dfa1 --- /dev/null +++ b/test/integration/tags_project/tags_project_test.go @@ -0,0 +1,43 @@ +// Copyright 2024 Google LLC +// +// 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. + +package tags_project + +import ( + "fmt" + "testing" + + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/gcloud" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/tft" + "github.com/GoogleCloudPlatform/cloud-foundation-toolkit/infra/blueprint-test/pkg/utils" + "github.com/stretchr/testify/assert" +) + +func TestTagsProject(t *testing.T) { + tagsProjectT := tft.NewTFBlueprintTest(t) + tagsProjectT.DefineVerify(func(assert *assert.Assertions) { + tagsProjectT.DefaultVerify(assert) + + projectNum := tagsProjectT.GetStringOutput("project_num") + tagValue := tagsProjectT.GetTFSetupStringOutput("tag_value") + + parent := fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", projectNum) + projBindings := gcloud.Runf(t, "resource-manager tags bindings list --parent=%s", parent).Array() + assert.Len(projBindings, 1, "expected one binding") + + binding := utils.GetFirstMatchResult(t, projBindings, "parent", parent) + assert.Equalf(fmt.Sprintf("tagValues/%s", tagValue), binding.Get("tagValue").String(), "expected binding to %s", tagValue) + }) + tagsProjectT.Test() +} diff --git a/test/setup/iam.tf b/test/setup/iam.tf index fcb33c61..8f1b2acb 100644 --- a/test/setup/iam.tf +++ b/test/setup/iam.tf @@ -37,6 +37,10 @@ locals { int_required_org_roles = [ "roles/accesscontextmanager.policyAdmin", "roles/resourcemanager.organizationViewer", + # CRUD tags. + "roles/resourcemanager.tagAdmin", + # Binding tags to resources. + "roles/resourcemanager.tagUser" ] } diff --git a/test/setup/outputs.tf b/test/setup/outputs.tf index 27a1631a..87a32b6b 100644 --- a/test/setup/outputs.tf +++ b/test/setup/outputs.tf @@ -58,3 +58,7 @@ output "group_name" { output "service_account_email" { value = google_service_account.int_test.email } + +output "tag_value" { + value = google_tags_tag_value.value.name +} diff --git a/test/setup/tags.tf b/test/setup/tags.tf new file mode 100644 index 00000000..e206cbb7 --- /dev/null +++ b/test/setup/tags.tf @@ -0,0 +1,33 @@ +/** + * Copyright 2024 Google LLC + * + * 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. + */ + +resource "random_string" "key_suffix" { + length = 7 + special = false + upper = false +} + +resource "google_tags_tag_key" "key" { + parent = "organizations/${var.org_id}" + short_name = "pf-key-${random_string.key_suffix.result}" + description = "Sample tag key" +} + +resource "google_tags_tag_value" "value" { + parent = "tagKeys/${google_tags_tag_key.key.name}" + short_name = "sample-val" + description = "Sample val" +} diff --git a/variables.tf b/variables.tf index 263720c0..5dc6c5f5 100644 --- a/variables.tf +++ b/variables.tf @@ -347,3 +347,9 @@ variable "language_tag" { type = string default = "en-US" } + +variable "tag_binding_values" { + description = "Tag values to bind the project to." + type = list(string) + default = [] +}