Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ var generatedListResources = []func() list.ListResource{}

var handwrittenListResources = []func() list.ListResource{
listResourceFunc(resourcemanager.NewGoogleServiceAccountListResource()),
listResourceFunc(resourcemanager.NewGoogleProjectServiceListResource()),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0

package resourcemanager

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/list"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

"github.com/hashicorp/terraform-provider-google/google/tpgresource"
)

type GoogleProjectServiceListResource struct {
tpgresource.ListResourceMetadata
}

// GoogleProjectServiceListModel matches [ListResourceMetadata.ListConfigFields] (tfsdk names and types).
type GoogleProjectServiceListModel struct {
Project types.String `tfsdk:"project"`
}

func NewGoogleProjectServiceListResource() list.ListResource {
listR := &GoogleProjectServiceListResource{}
listR.TypeName = "google_project_service"
listR.SDKv2Resource = ResourceGoogleProjectService()
listR.ListConfigFields = []tpgresource.ListConfigField{{Name: "project", Kind: tpgresource.ListConfigKindString, Optional: true}}
return listR
}

func (listR *GoogleProjectServiceListResource) List(ctx context.Context, listReq list.ListRequest, stream *list.ListResultsStream) {
var data GoogleProjectServiceListModel
diags := listReq.Config.Get(ctx, &data)
if diags.HasError() {
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
if listR.Client == nil {
diags = append(diags, diag.NewErrorDiagnostic(
"Provider not configured",
"The Google provider client is not available; ensure the provider is configured (e.g. credentials and default project).",
))
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
project := tpgresource.GetResourceNameFromSelfLink(listR.GetProject(data.Project))

tempData := ResourceGoogleProjectService().Data(&terraform.InstanceState{})
if err := tempData.Set("project", project); err != nil {
diags.AddError("Config Error", fmt.Sprintf("Error setting project: %s", err))
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}

userAgent, err := tpgresource.GenerateUserAgentString(tempData, listR.Client.UserAgent)
if err != nil {
diags.AddError("Config Error", err.Error())
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
billingProject := project
if bp, err := tpgresource.GetBillingProject(tempData, listR.Client); err == nil {
billingProject = bp
}

servicesList, err := ListCurrentlyEnabledServices(project, billingProject, userAgent, listR.Client, 10*time.Minute)
if err != nil {
diags.AddError("API Error", err.Error())
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}

stream.Results = func(push func(list.ListResult) bool) {
for serviceName := range servicesList {
rd := ResourceGoogleProjectService().Data(&terraform.InstanceState{})
if err := rd.Set("project", project); err != nil {
diags.AddError("Config Error", fmt.Sprintf("Error setting project: %s", err))
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
if err := rd.Set("service", serviceName); err != nil {
diags.AddError("Config Error", fmt.Sprintf("Error setting service: %s", err))
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
rd.SetId(fmt.Sprintf("%s/%s", project, serviceName))

result := listReq.NewListResult(ctx)
if err := listR.SetResult(ctx, listReq.IncludeResource, &result, rd, "service"); err != nil {
diags.AddError("Schema Error", err.Error())
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
if !push(result) {
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
}
stream.Results = list.ListResultsStreamDiagnostics(diags)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package resourcemanager_test

import (
"fmt"
"testing"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/querycheck"
"github.com/hashicorp/terraform-plugin-testing/tfversion"

"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
)

// TestAccProjectServiceListResource_queryIdentity lists enabled project services via the
// provider list resource API and asserts a known identity appears (Terraform 1.14+).
func TestAccProjectServiceListResource_queryIdentity(t *testing.T) {
t.Parallel()

service := "iam.googleapis.com"
project := acctest.BootstrapProject(t, "tf-boot-proj-svc-list-", envvar.GetTestBillingAccountFromEnv(t), []string{service}).ProjectId

acctest.VcrTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_14_0),
},
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccProjectServiceList_prereq(project, service),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("google_project_service.prereq", "service", service),
resource.TestCheckResourceAttr("google_project_service.prereq", "project", project),
),
},
{
Query: true,
Config: testAccProjectServiceListQuery(project),
QueryResultChecks: []querycheck.QueryResultCheck{
querycheck.ExpectIdentity("google_project_service.all_in_project", map[string]knownvalue.Check{
"service": knownvalue.StringExact(service),
"project": knownvalue.StringExact(project),
}),
querycheck.ExpectLengthAtLeast("google_project_service.all_in_project", 1),
},
},
},
})
}

func testAccProjectServiceList_prereq(project, service string) string {
return fmt.Sprintf(`
resource "google_project_service" "prereq" {
project = %q
service = %q
}
`, project, service)
}

func testAccProjectServiceListQuery(project string) string {
return fmt.Sprintf(`
list "google_project_service" "all_in_project" {
provider = google

config {
project = %q
}
}
`, project)
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ func ResourceGoogleProjectService() *schema.Resource {
tpgresource.DefaultProviderProject,
),

Identity: &schema.ResourceIdentity{
Version: 1,
SchemaFunc: func() map[string]*schema.Schema {
return map[string]*schema.Schema{
"project": {
Type: schema.TypeString,
OptionalForImport: true,
},
"service": {
Type: schema.TypeString,
RequiredForImport: true,
},
}
},
},

Schema: map[string]*schema.Schema{
"service": {
Type: schema.TypeString,
Expand Down Expand Up @@ -133,6 +149,35 @@ func ResourceGoogleProjectService() *schema.Resource {
}

func resourceGoogleProjectServiceImport(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
if d.Id() == "" {
identity, err := d.Identity()
if err != nil {
return nil, fmt.Errorf("google_project_service import: %w", err)
}
serviceV, ok := identity.GetOk("service")
if !ok {
return nil, fmt.Errorf("import via identity requires identity attribute %q", "service")
}
serviceStr, ok := serviceV.(string)
if !ok || serviceStr == "" {
return nil, fmt.Errorf("import via identity requires identity attribute %q to be a non-empty string", "service")
}
var projectStr string
if projectV, ok := identity.GetOk("project"); ok && projectV != nil {
if s, ok := projectV.(string); ok {
projectStr = s
}
}
if projectStr == "" {
config, ok := m.(*transport_tpg.Config)
if !ok || config.Project == "" {
return nil, fmt.Errorf("import via identity requires identity attribute %q or a default project in the provider configuration", "project")
}
}
projectStr = tpgresource.GetResourceNameFromSelfLink(projectStr)
d.SetId(fmt.Sprintf("%s/%s", projectStr, serviceStr))
}

parts := strings.Split(d.Id(), "/")
if len(parts) != 2 {
return nil, fmt.Errorf("Invalid google_project_service id format for import, expecting `{project}/{service}`, found %s", d.Id())
Expand All @@ -143,6 +188,12 @@ func resourceGoogleProjectServiceImport(d *schema.ResourceData, m interface{}) (
if err := d.Set("service", parts[1]); err != nil {
return nil, fmt.Errorf("Error setting service: %s", err)
}
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
"project": parts[0],
"service": parts[1],
}); err != nil {
return nil, err
}
return []*schema.ResourceData{d}, nil
}

Expand Down Expand Up @@ -173,6 +224,12 @@ func resourceGoogleProjectServiceCreate(d *schema.ResourceData, meta interface{}
if err := d.Set("service", srv); err != nil {
return fmt.Errorf("Error setting service: %s", err)
}
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
"project": project,
"service": srv,
}); err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -235,6 +292,12 @@ func resourceGoogleProjectServiceRead(d *schema.ResourceData, meta interface{})
if err := d.Set("service", srv); err != nil {
return fmt.Errorf("Error setting service: %s", err)
}
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
"project": project,
"service": srv,
}); err != nil {
return err
}
return nil
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

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

// TestAccProjectService_importBlockWithResourceIdentity exercises plannable import using the resource identity block (Terraform 1.12+).
func TestAccProjectService_importBlockWithResourceIdentity(t *testing.T) {
t.Parallel()

project := envvar.GetTestProjectFromEnv()
service := "iam.googleapis.com"

acctest.VcrTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_12_0),
},
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
Steps: []resource.TestStep{
{
Config: testAccProjectService_identityImport(project, service),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("google_project_service.ps", "project", project),
resource.TestCheckResourceAttr("google_project_service.ps", "service", service),
),
},
{
ResourceName: "google_project_service.ps",
ImportState: true,
ImportStateKind: resource.ImportBlockWithResourceIdentity,
},
},
})
}

func testAccProjectService_identityImport(project, service string) string {
return fmt.Sprintf(`
resource "google_project_service" "ps" {
project = "%s"
service = "%s"
}
`, project, service)
}

func testAccCheckProjectService(t *testing.T, services []string, pid string, expectEnabled bool) resource.TestCheckFunc {
return func(s *terraform.State) error {
config := acctest.GoogleProviderConfig(t)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
subcategory: "Cloud Platform"
description: |-
List enabled Google Cloud project services (APIs) for use with terraform query
and .tfquery.hcl files.
---

# google_project_service (list)

Lists **enabled Service Usage APIs** for a Google Cloud project—one entry per enabled
service, matching what you manage with
[`google_project_service`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_service)—for use with
[`terraform query`](https://developer.hashicorp.com/terraform/cli/commands/query) and
**`.tfquery.hcl`** files.

For how list resources work in this provider, file layout, Terraform version requirements, and
shared `list` block arguments, refer to the guide
[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).

## Example

```hcl
list "google_project_service" "all" {
provider = google

config {
# Optional. Defaults to the provider project when omitted.
# project = "other-project"
}
}
```

Run `terraform query` from the directory that contains the `.tfquery.hcl` file.

## Configuration (`config` block)

* `project` - (Optional) Project ID to list enabled services for. If unset, the provider's
configured default project is used (same idea as the managed resource).

## Results

By default each result includes **resource identity** for `google_project_service` (see
[Resource identity](https://developer.hashicorp.com/terraform/language/resources/identities)):

* `service` - API identifier (for example `compute.googleapis.com`) (required for identity).
* `project` - Project ID when applicable.

With `include_resource = true` on the `list` block, results also include the full resource-style
attributes documented for the managed
[`google_project_service` resource](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_service#attributes-reference)
(for example `disable_dependent_services` and `disable_on_destroy` where present in state).
Loading