Skip to content

Commit a6c2641

Browse files
abd-googiyabchen
authored andcommitted
Add tags field to Project resource (GoogleCloudPlatform#11440)
1 parent 6e7084f commit a6c2641

File tree

4 files changed

+237
-0
lines changed

4 files changed

+237
-0
lines changed

mmv1/third_party/terraform/acctest/bootstrap_test_utils.go

+138
Original file line numberDiff line numberDiff line change
@@ -1304,3 +1304,141 @@ func SetupProjectsAndGetAccessToken(org, billing, pid, service string, config *t
13041304

13051305
return accessToken, nil
13061306
}
1307+
1308+
const sharedTagKeyPrefix = "tf-bootstrap-tagkey"
1309+
1310+
func BootstrapSharedTestTagKey(t *testing.T, testId string) string {
1311+
org := envvar.GetTestOrgFromEnv(t)
1312+
sharedTagKey := fmt.Sprintf("%s-%s", sharedTagKeyPrefix, testId)
1313+
tagKeyName := fmt.Sprintf("%s/%s", org, sharedTagKey)
1314+
1315+
config := BootstrapConfig(t)
1316+
if config == nil {
1317+
return ""
1318+
}
1319+
1320+
log.Printf("[DEBUG] Getting shared test tag key %q", sharedTagKey)
1321+
getURL := fmt.Sprintf("%stagKeys/namespaced?name=%s", config.TagsBasePath, tagKeyName)
1322+
_, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1323+
Config: config,
1324+
Method: "GET",
1325+
Project: config.Project,
1326+
RawURL: getURL,
1327+
UserAgent: config.UserAgent,
1328+
Timeout: 2 * time.Minute,
1329+
})
1330+
if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 403) {
1331+
log.Printf("[DEBUG] TagKey %q not found, bootstrapping", sharedTagKey)
1332+
tagKeyObj := map[string]interface{}{
1333+
"parent": "organizations/" + org,
1334+
"shortName": sharedTagKey,
1335+
"description": "Bootstrapped tag key for Terraform Acceptance testing",
1336+
}
1337+
1338+
_, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1339+
Config: config,
1340+
Method: "POST",
1341+
Project: config.Project,
1342+
RawURL: config.TagsBasePath + "tagKeys/",
1343+
UserAgent: config.UserAgent,
1344+
Body: tagKeyObj,
1345+
Timeout: 10 * time.Minute,
1346+
})
1347+
if err != nil {
1348+
t.Fatalf("Error bootstrapping shared tag key %q: %s", sharedTagKey, err)
1349+
}
1350+
1351+
log.Printf("[DEBUG] Waiting for shared tag key creation to finish")
1352+
}
1353+
1354+
_, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1355+
Config: config,
1356+
Method: "GET",
1357+
Project: config.Project,
1358+
RawURL: getURL,
1359+
UserAgent: config.UserAgent,
1360+
Timeout: 2 * time.Minute,
1361+
})
1362+
1363+
if err != nil {
1364+
t.Fatalf("Error getting shared tag key %q: %s", sharedTagKey, err)
1365+
}
1366+
1367+
return sharedTagKey
1368+
}
1369+
1370+
const sharedTagValuePrefix = "tf-bootstrap-tagvalue"
1371+
1372+
func BootstrapSharedTestTagValue(t *testing.T, testId string, tagKey string) string {
1373+
org := envvar.GetTestOrgFromEnv(t)
1374+
sharedTagValue := fmt.Sprintf("%s-%s", sharedTagValuePrefix, testId)
1375+
tagKeyName := fmt.Sprintf("%s/%s", org, tagKey)
1376+
tagValueName := fmt.Sprintf("%s/%s", tagKeyName, sharedTagValue)
1377+
1378+
config := BootstrapConfig(t)
1379+
if config == nil {
1380+
return ""
1381+
}
1382+
1383+
log.Printf("[DEBUG] Getting shared test tag value %q", sharedTagValue)
1384+
getURL := fmt.Sprintf("%stagValues/namespaced?name=%s", config.TagsBasePath, tagValueName)
1385+
_, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1386+
Config: config,
1387+
Method: "GET",
1388+
Project: config.Project,
1389+
RawURL: getURL,
1390+
UserAgent: config.UserAgent,
1391+
Timeout: 2 * time.Minute,
1392+
})
1393+
if err != nil && transport_tpg.IsGoogleApiErrorWithCode(err, 403) {
1394+
log.Printf("[DEBUG] TagValue %q not found, bootstrapping", sharedTagValue)
1395+
log.Printf("[DEBUG] Fetching permanent id for tagkey %s", tagKeyName)
1396+
tagKeyGetURL := fmt.Sprintf("%stagKeys/namespaced?name=%s", config.TagsBasePath, tagKeyName)
1397+
tagKeyResponse, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1398+
Config: config,
1399+
Method: "GET",
1400+
Project: config.Project,
1401+
RawURL: tagKeyGetURL,
1402+
UserAgent: config.UserAgent,
1403+
Timeout: 2 * time.Minute,
1404+
})
1405+
if err != nil {
1406+
t.Fatalf("Error getting tag key id for %s : %s", tagKeyName, err)
1407+
}
1408+
tagKeyObj := map[string]interface{}{
1409+
"parent": tagKeyResponse["name"].(string),
1410+
"shortName": sharedTagValue,
1411+
"description": "Bootstrapped tag value for Terraform Acceptance testing",
1412+
}
1413+
1414+
_, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1415+
Config: config,
1416+
Method: "POST",
1417+
Project: config.Project,
1418+
RawURL: config.TagsBasePath + "tagValues/",
1419+
UserAgent: config.UserAgent,
1420+
Body: tagKeyObj,
1421+
Timeout: 10 * time.Minute,
1422+
})
1423+
if err != nil {
1424+
t.Fatalf("Error bootstrapping shared tag value %q: %s", sharedTagValue, err)
1425+
}
1426+
1427+
log.Printf("[DEBUG] Waiting for shared tag value creation to finish")
1428+
}
1429+
1430+
_, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
1431+
Config: config,
1432+
Method: "GET",
1433+
Project: config.Project,
1434+
RawURL: getURL,
1435+
UserAgent: config.UserAgent,
1436+
Timeout: 2 * time.Minute,
1437+
})
1438+
1439+
if err != nil {
1440+
t.Fatalf("Error getting shared tag value %q: %s", sharedTagValue, err)
1441+
}
1442+
1443+
return sharedTagValue
1444+
}

mmv1/third_party/terraform/services/resourcemanager/resource_google_project.go

+12
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ func ResourceGoogleProject() *schema.Resource {
133133
Description: `All of labels (key/value pairs) present on the resource in GCP, including the labels configured through Terraform, other clients and services.`,
134134
Elem: &schema.Schema{Type: schema.TypeString},
135135
},
136+
137+
"tags": {
138+
Type: schema.TypeMap,
139+
Optional: true,
140+
ForceNew: true,
141+
Elem: &schema.Schema{Type: schema.TypeString},
142+
Description: `A map of resource manager tags. Resource manager tag keys and values have the same definition as resource manager tags. Keys must be in the format tagKeys/{tag_key_id}, and values are in the format tagValues/456. The field is ignored when empty.`,
143+
},
136144
},
137145
UseJSONNumber: true,
138146
}
@@ -166,6 +174,10 @@ func resourceGoogleProjectCreate(d *schema.ResourceData, meta interface{}) error
166174
project.Labels = tpgresource.ExpandEffectiveLabels(d)
167175
}
168176

177+
if _, ok := d.GetOk("tags"); ok {
178+
project.Tags = tpgresource.ExpandStringMap(d, "tags")
179+
}
180+
169181
var op *cloudresourcemanager.Operation
170182
err = transport_tpg.Retry(transport_tpg.RetryOptions{
171183
RetryFunc: func() (reqErr error) {

mmv1/third_party/terraform/services/resourcemanager/resource_google_project_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,46 @@ func TestAccProject_migrateParent(t *testing.T) {
234234
})
235235
}
236236

237+
// Test that a Project resource can be created with tags
238+
func TestAccProject_tags(t *testing.T) {
239+
t.Parallel()
240+
241+
org := envvar.GetTestOrgFromEnv(t)
242+
pid := fmt.Sprintf("%s-%d", TestPrefix, acctest.RandInt(t))
243+
tagKey := acctest.BootstrapSharedTestTagKey(t, "crm-projects-tagkey")
244+
tagValue := acctest.BootstrapSharedTestTagValue(t, "crm-projects-tagvalue", tagKey)
245+
acctest.VcrTest(t, resource.TestCase{
246+
PreCheck: func() { acctest.AccTestPreCheck(t) },
247+
ExternalProviders: map[string]resource.ExternalProvider{
248+
"time": {},
249+
},
250+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
251+
Steps: []resource.TestStep{
252+
{
253+
Config: testAccProject_tags(pid, org, map[string]string{org + "/" + tagKey: tagValue}),
254+
Check: resource.ComposeTestCheckFunc(
255+
testAccCheckGoogleProjectExists("google_project.acceptance", pid),
256+
),
257+
},
258+
// Make sure import supports tags
259+
{
260+
ResourceName: "google_project.acceptance",
261+
ImportState: true,
262+
ImportStateVerify: true,
263+
ImportStateVerifyIgnore: []string{"tags", "deletion_policy"}, // we don't read tags back
264+
},
265+
// Update tags tries to replace project but fails due to deletion policy
266+
{
267+
Config: testAccProject_tags(pid, org, map[string]string{}),
268+
ExpectError: regexp.MustCompile("deletion_policy"),
269+
},
270+
{
271+
Config: testAccProject_tagsAllowDestroy(pid, org, map[string]string{org + "/" + tagKey: tagValue}),
272+
},
273+
},
274+
})
275+
}
276+
237277
func testAccCheckGoogleProjectExists(r, pid string) resource.TestCheckFunc {
238278
return func(s *terraform.State) error {
239279
rs, ok := s.RootModule().Resources[r]
@@ -553,3 +593,35 @@ resource "google_folder" "folder1" {
553593
}
554594
`, pid, pid, org, folderName, org)
555595
}
596+
597+
func testAccProject_tags(pid, org string, tags map[string]string) string {
598+
r := fmt.Sprintf(`
599+
resource "google_project" "acceptance" {
600+
project_id = "%s"
601+
name = "%s"
602+
org_id = "%s"
603+
tags = {`, pid, pid, org)
604+
605+
l := ""
606+
for key, value := range tags {
607+
l += fmt.Sprintf("%q = %q\n", key, value)
608+
}
609+
l += fmt.Sprintf("}\n}")
610+
return r + l
611+
}
612+
613+
func testAccProject_tagsAllowDestroy(pid, org string, tags map[string]string) string {
614+
r := fmt.Sprintf(`resource "google_project" "acceptance" {
615+
project_id = "%s"
616+
name = "%s"
617+
org_id = "%s"
618+
deletion_policy = "DELETE"
619+
tags = {`, pid, pid, org)
620+
l := ""
621+
for key, value := range tags {
622+
l += fmt.Sprintf("%q = %q\n", key, value)
623+
}
624+
625+
l += fmt.Sprintf("}\n}")
626+
return r + l
627+
}

mmv1/third_party/terraform/website/docs/r/google_project.html.markdown

+15
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ doc for more information.
2020

2121
~> It is recommended to use the `constraints/compute.skipDefaultNetworkCreation` [constraint](/docs/providers/google/r/google_organization_policy.html) to remove the default network instead of setting `auto_create_network` to false, when possible.
2222

23+
~> It may take a while for the attached tag bindings to be deleted after the project is scheduled to be deleted.
24+
2325
To get more information about projects, see:
2426

2527
* [API documentation](https://cloud.google.com/resource-manager/reference/rest/v1/projects)
@@ -51,6 +53,17 @@ resource "google_folder" "department1" {
5153
}
5254
```
5355

56+
To create a project with a tag
57+
58+
```hcl
59+
resource "google_project" "my_project" {
60+
name = "My Project"
61+
project_id = "your-project-id"
62+
org_id = "1234567"
63+
tags = {"1234567/env":"staging"}
64+
}
65+
```
66+
5467
## Argument Reference
5568

5669
The following arguments are supported:
@@ -100,6 +113,8 @@ The following arguments are supported:
100113
to be abandoned rather than deleted, i.e., the Terraform resource can be deleted without deleting the Project via
101114
the Google API. Possible values are: "PREVENT", "ABANDON", "DELETE". Default value is `PREVENT`.
102115

116+
* `tags` - (Optional) A map of resource manager tags. Resource manager tag keys and values have the same definition as resource manager tags. Keys must be in the format tagKeys/{tag_key_id}, and values are in the format tagValues/456. The field is ignored when empty. The field is immutable and causes resource replacement when mutated.
117+
103118
## Attributes Reference
104119

105120
In addition to the arguments listed above, the following computed attributes are

0 commit comments

Comments
 (0)