Skip to content

Commit dc2fd3e

Browse files
authored
resourcemanager: add list resource for google_project_service (GoogleCloudPlatform#17136)
1 parent d3e73f9 commit dc2fd3e

6 files changed

Lines changed: 324 additions & 0 deletions

File tree

mmv1/third_party/terraform/fwprovider/framework_provider_mmv1_resources.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ var generatedListResources = []func() list.ListResource{}
1616

1717
var handwrittenListResources = []func() list.ListResource{
1818
listResourceFunc(resourcemanager.NewGoogleServiceAccountListResource()),
19+
listResourceFunc(resourcemanager.NewGoogleProjectServiceListResource()),
1920
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) IBM Corp. 2014, 2026
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package resourcemanager
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-framework/list"
12+
"github.com/hashicorp/terraform-plugin-framework/types"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
14+
15+
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
16+
)
17+
18+
type GoogleProjectServiceListResource struct {
19+
tpgresource.ListResourceMetadata
20+
}
21+
22+
// GoogleProjectServiceListModel matches [ListResourceMetadata.ListConfigFields] (tfsdk names and types).
23+
type GoogleProjectServiceListModel struct {
24+
Project types.String `tfsdk:"project"`
25+
}
26+
27+
func NewGoogleProjectServiceListResource() list.ListResource {
28+
listR := &GoogleProjectServiceListResource{}
29+
listR.TypeName = "google_project_service"
30+
listR.SDKv2Resource = ResourceGoogleProjectService()
31+
listR.ListConfigFields = []tpgresource.ListConfigField{{Name: "project", Kind: tpgresource.ListConfigKindString, Optional: true}}
32+
return listR
33+
}
34+
35+
func (listR *GoogleProjectServiceListResource) List(ctx context.Context, listReq list.ListRequest, stream *list.ListResultsStream) {
36+
var data GoogleProjectServiceListModel
37+
diags := listReq.Config.Get(ctx, &data)
38+
if diags.HasError() {
39+
stream.Results = list.ListResultsStreamDiagnostics(diags)
40+
return
41+
}
42+
if listR.Client == nil {
43+
diags = append(diags, diag.NewErrorDiagnostic(
44+
"Provider not configured",
45+
"The Google provider client is not available; ensure the provider is configured (e.g. credentials and default project).",
46+
))
47+
stream.Results = list.ListResultsStreamDiagnostics(diags)
48+
return
49+
}
50+
project := tpgresource.GetResourceNameFromSelfLink(listR.GetProject(data.Project))
51+
52+
tempData := ResourceGoogleProjectService().Data(&terraform.InstanceState{})
53+
if err := tempData.Set("project", project); err != nil {
54+
diags.AddError("Config Error", fmt.Sprintf("Error setting project: %s", err))
55+
stream.Results = list.ListResultsStreamDiagnostics(diags)
56+
return
57+
}
58+
59+
// BatchRequestReadServices (serviceusage_batching.go) calls ListCurrentlyEnabledServices
60+
// in resource_google_project.go, which uses Service Usage Services.List(...).Pages(...)
61+
servicesRaw, err := BatchRequestReadServices(project, tempData, listR.Client)
62+
if err != nil {
63+
diags.AddError("API Error", err.Error())
64+
stream.Results = list.ListResultsStreamDiagnostics(diags)
65+
return
66+
}
67+
servicesList := servicesRaw.(map[string]struct{})
68+
69+
stream.Results = func(push func(list.ListResult) bool) {
70+
for serviceName := range servicesList {
71+
rd := ResourceGoogleProjectService().Data(&terraform.InstanceState{})
72+
if err := rd.Set("project", project); err != nil {
73+
diags.AddError("Config Error", fmt.Sprintf("Error setting project: %s", err))
74+
stream.Results = list.ListResultsStreamDiagnostics(diags)
75+
return
76+
}
77+
if err := rd.Set("service", serviceName); err != nil {
78+
diags.AddError("Config Error", fmt.Sprintf("Error setting service: %s", err))
79+
stream.Results = list.ListResultsStreamDiagnostics(diags)
80+
return
81+
}
82+
rd.SetId(fmt.Sprintf("%s/%s", project, serviceName))
83+
84+
result := listReq.NewListResult(ctx)
85+
if err := listR.SetResult(ctx, listReq.IncludeResource, &result, rd, "service"); err != nil {
86+
diags.AddError("Schema Error", err.Error())
87+
stream.Results = list.ListResultsStreamDiagnostics(diags)
88+
return
89+
}
90+
if !push(result) {
91+
stream.Results = list.ListResultsStreamDiagnostics(diags)
92+
return
93+
}
94+
}
95+
stream.Results = list.ListResultsStreamDiagnostics(diags)
96+
}
97+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package resourcemanager_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
8+
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
9+
"github.com/hashicorp/terraform-plugin-testing/querycheck"
10+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
11+
12+
"github.com/hashicorp/terraform-provider-google/google/acctest"
13+
"github.com/hashicorp/terraform-provider-google/google/envvar"
14+
)
15+
16+
// TestAccProjectServiceListResource_queryIdentity lists enabled project services via the
17+
// provider list resource API and asserts a known identity appears (Terraform 1.14+).
18+
func TestAccProjectServiceListResource_queryIdentity(t *testing.T) {
19+
t.Parallel()
20+
21+
project := envvar.GetTestProjectFromEnv()
22+
service := "iam.googleapis.com"
23+
24+
acctest.VcrTest(t, resource.TestCase{
25+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
26+
tfversion.SkipBelow(tfversion.Version1_14_0),
27+
},
28+
PreCheck: func() { acctest.AccTestPreCheck(t) },
29+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
30+
Steps: []resource.TestStep{
31+
{
32+
Config: testAccProjectServiceList_prereq(project, service),
33+
Check: resource.ComposeTestCheckFunc(
34+
resource.TestCheckResourceAttr("google_project_service.prereq", "service", service),
35+
resource.TestCheckResourceAttr("google_project_service.prereq", "project", project),
36+
),
37+
},
38+
{
39+
Query: true,
40+
Config: testAccProjectServiceListQuery(project),
41+
QueryResultChecks: []querycheck.QueryResultCheck{
42+
querycheck.ExpectIdentity("google_project_service.all_in_project", map[string]knownvalue.Check{
43+
"service": knownvalue.StringExact(service),
44+
"project": knownvalue.StringExact(project),
45+
}),
46+
querycheck.ExpectLengthAtLeast("google_project_service.all_in_project", 1),
47+
},
48+
},
49+
},
50+
})
51+
}
52+
53+
func testAccProjectServiceList_prereq(project, service string) string {
54+
return fmt.Sprintf(`
55+
resource "google_project_service" "prereq" {
56+
project = %q
57+
service = %q
58+
}
59+
`, project, service)
60+
}
61+
62+
func testAccProjectServiceListQuery(project string) string {
63+
return fmt.Sprintf(`
64+
list "google_project_service" "all_in_project" {
65+
provider = google
66+
67+
config {
68+
project = %q
69+
}
70+
}
71+
`, project)
72+
}

mmv1/third_party/terraform/services/resourcemanager/resource_google_project_service.go.tmpl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,22 @@ func ResourceGoogleProjectService() *schema.Resource {
9797
tpgresource.DefaultProviderProject,
9898
),
9999
100+
Identity: &schema.ResourceIdentity{
101+
Version: 1,
102+
SchemaFunc: func() map[string]*schema.Schema {
103+
return map[string]*schema.Schema{
104+
"project": {
105+
Type: schema.TypeString,
106+
OptionalForImport: true,
107+
},
108+
"service": {
109+
Type: schema.TypeString,
110+
RequiredForImport: true,
111+
},
112+
}
113+
},
114+
},
115+
100116
Schema: map[string]*schema.Schema{
101117
"service": {
102118
Type: schema.TypeString,
@@ -133,6 +149,35 @@ func ResourceGoogleProjectService() *schema.Resource {
133149
}
134150
135151
func resourceGoogleProjectServiceImport(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
152+
if d.Id() == "" {
153+
identity, err := d.Identity()
154+
if err != nil {
155+
return nil, fmt.Errorf("google_project_service import: %w", err)
156+
}
157+
serviceV, ok := identity.GetOk("service")
158+
if !ok {
159+
return nil, fmt.Errorf("import via identity requires identity attribute %q", "service")
160+
}
161+
serviceStr, ok := serviceV.(string)
162+
if !ok || serviceStr == "" {
163+
return nil, fmt.Errorf("import via identity requires identity attribute %q to be a non-empty string", "service")
164+
}
165+
var projectStr string
166+
if projectV, ok := identity.GetOk("project"); ok && projectV != nil {
167+
if s, ok := projectV.(string); ok {
168+
projectStr = s
169+
}
170+
}
171+
if projectStr == "" {
172+
config, ok := m.(*transport_tpg.Config)
173+
if !ok || config.Project == "" {
174+
return nil, fmt.Errorf("import via identity requires identity attribute %q or a default project in the provider configuration", "project")
175+
}
176+
}
177+
projectStr = tpgresource.GetResourceNameFromSelfLink(projectStr)
178+
d.SetId(fmt.Sprintf("%s/%s", projectStr, serviceStr))
179+
}
180+
136181
parts := strings.Split(d.Id(), "/")
137182
if len(parts) != 2 {
138183
return nil, fmt.Errorf("Invalid google_project_service id format for import, expecting `{project}/{service}`, found %s", d.Id())
@@ -143,6 +188,12 @@ func resourceGoogleProjectServiceImport(d *schema.ResourceData, m interface{}) (
143188
if err := d.Set("service", parts[1]); err != nil {
144189
return nil, fmt.Errorf("Error setting service: %s", err)
145190
}
191+
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
192+
"project": parts[0],
193+
"service": parts[1],
194+
}); err != nil {
195+
return nil, err
196+
}
146197
return []*schema.ResourceData{d}, nil
147198
}
148199
@@ -173,6 +224,12 @@ func resourceGoogleProjectServiceCreate(d *schema.ResourceData, meta interface{}
173224
if err := d.Set("service", srv); err != nil {
174225
return fmt.Errorf("Error setting service: %s", err)
175226
}
227+
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
228+
"project": project,
229+
"service": srv,
230+
}); err != nil {
231+
return err
232+
}
176233
return nil
177234
}
178235
@@ -235,6 +292,12 @@ func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{})
235292
if err := d.Set("service", srv); err != nil {
236293
return fmt.Errorf("Error setting service: %s", err)
237294
}
295+
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
296+
"project": project,
297+
"service": srv,
298+
}); err != nil {
299+
return err
300+
}
238301
return nil
239302
}
240303

mmv1/third_party/terraform/services/resourcemanager/resource_google_project_service_test.go.tmpl

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
1414
"github.com/hashicorp/terraform-plugin-testing/terraform"
15+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
1516
)
1617

1718
// Test that services can be enabled and disabled on a project
@@ -190,6 +191,45 @@ func TestAccProjectService_renamedService(t *testing.T) {
190191
})
191192
}
192193

194+
// TestAccProjectService_importBlockWithResourceIdentity exercises plannable import using the resource identity block (Terraform 1.12+).
195+
func TestAccProjectService_importBlockWithResourceIdentity(t *testing.T) {
196+
t.Parallel()
197+
198+
project := envvar.GetTestProjectFromEnv()
199+
service := "iam.googleapis.com"
200+
201+
acctest.VcrTest(t, resource.TestCase{
202+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
203+
tfversion.SkipBelow(tfversion.Version1_12_0),
204+
},
205+
PreCheck: func() { acctest.AccTestPreCheck(t) },
206+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
207+
Steps: []resource.TestStep{
208+
{
209+
Config: testAccProjectService_identityImport(project, service),
210+
Check: resource.ComposeTestCheckFunc(
211+
resource.TestCheckResourceAttr("google_project_service.ps", "project", project),
212+
resource.TestCheckResourceAttr("google_project_service.ps", "service", service),
213+
),
214+
},
215+
{
216+
ResourceName: "google_project_service.ps",
217+
ImportState: true,
218+
ImportStateKind: resource.ImportBlockWithResourceIdentity,
219+
},
220+
},
221+
})
222+
}
223+
224+
func testAccProjectService_identityImport(project, service string) string {
225+
return fmt.Sprintf(`
226+
resource "google_project_service" "ps" {
227+
project = "%s"
228+
service = "%s"
229+
}
230+
`, project, service)
231+
}
232+
193233
func testAccCheckProjectService(t *testing.T, services []string, pid string, expectEnabled bool) resource.TestCheckFunc {
194234
return func(s *terraform.State) error {
195235
config := acctest.GoogleProviderConfig(t)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
subcategory: "Cloud Platform"
3+
description: |-
4+
List enabled Google Cloud project services (APIs) for use with terraform query
5+
and .tfquery.hcl files.
6+
---
7+
8+
# google_project_service (list)
9+
10+
Lists **enabled Service Usage APIs** for a Google Cloud project—one entry per enabled
11+
service, matching what you manage with
12+
[`google_project_service`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_service)—for use with
13+
[`terraform query`](https://developer.hashicorp.com/terraform/cli/commands/query) and
14+
**`.tfquery.hcl`** files.
15+
16+
For how list resources work in this provider, file layout, Terraform version requirements, and
17+
shared `list` block arguments, refer to the guide
18+
[Use list resources with terraform query (Google Cloud provider)](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/using_list_resources_with_terraform_query).
19+
20+
## Example
21+
22+
```hcl
23+
list "google_project_service" "all" {
24+
provider = google
25+
26+
config {
27+
# Optional. Defaults to the provider project when omitted.
28+
# project = "other-project"
29+
}
30+
}
31+
```
32+
33+
Run `terraform query` from the directory that contains the `.tfquery.hcl` file.
34+
35+
## Configuration (`config` block)
36+
37+
* `project` - (Optional) Project ID to list enabled services for. If unset, the provider's
38+
configured default project is used (same idea as the managed resource).
39+
40+
## Results
41+
42+
By default each result includes **resource identity** for `google_project_service` (see
43+
[Resource identity](https://developer.hashicorp.com/terraform/language/resources/identities)):
44+
45+
* `service` - API identifier (for example `compute.googleapis.com`) (required for identity).
46+
* `project` - Project ID when applicable.
47+
48+
With `include_resource = true` on the `list` block, results also include the full resource-style
49+
attributes documented for the managed
50+
[`google_project_service` resource](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_service#attributes-reference)
51+
(for example `disable_dependent_services` and `disable_on_destroy` where present in state).

0 commit comments

Comments
 (0)