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
@@ -0,0 +1,152 @@
// Copyright (c) IBM Corp. 2014, 2026
// SPDX-License-Identifier: MPL-2.0

package resourcemanager

import (
"context"
"errors"
"fmt"
"strings"

"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/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"google.golang.org/api/iam/v1"

"github.com/hashicorp/terraform-provider-google/google/registry"
"github.com/hashicorp/terraform-provider-google/google/services/iambeta"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
)

func init() {
registry.FrameworkListResource{
Name: "google_service_account_key",
ProductName: "resourcemanager",
Func: NewGoogleServiceAccountKeyListResource,
}.Register()
}

type GoogleServiceAccountKeyListResource struct {
tpgresource.ListResourceMetadata
}

// GoogleServiceAccountKeyListModel matches [ListResourceMetadata.ListConfigFields] (tfsdk names and types).
type GoogleServiceAccountKeyListModel struct {
ServiceAccountID types.String `tfsdk:"service_account_id"`
}

func NewGoogleServiceAccountKeyListResource() list.ListResource {
listR := &GoogleServiceAccountKeyListResource{}
listR.TypeName = "google_service_account_key"
listR.SDKv2Resource = ResourceGoogleServiceAccountKey()
listR.ListConfigFields = []tpgresource.ListConfigField{{Name: "service_account_id", Kind: tpgresource.ListConfigKindString, Optional: false}}
return listR
}

func (listR *GoogleServiceAccountKeyListResource) List(ctx context.Context, listReq list.ListRequest, stream *list.ListResultsStream) {
var data GoogleServiceAccountKeyListModel
diags := listReq.Config.Get(ctx, &data)
if diags.HasError() {
stream.Results = list.ListResultsStreamDiagnostics(diags)
return
}
if data.ServiceAccountID.IsNull() || data.ServiceAccountID.ValueString() == "" {
diags = append(diags, diag.NewErrorDiagnostic(
"Missing required argument",
`The "service_account_id" argument is required and must not be empty.`,
))
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
}
serviceAccountID := data.ServiceAccountID.ValueString()

errServiceAccountKeyListStreamClosed := errors.New("stream closed")
stream.Results = func(push func(list.ListResult) bool) {
err := ListServiceAccountKeys(listR.Client, serviceAccountID, func(rd *schema.ResourceData) error {
result := listReq.NewListResult(ctx)

if err := listR.SetResult(ctx, listReq.IncludeResource, &result, rd, "name"); err != nil {
return err
}

if !push(result) {
return errServiceAccountKeyListStreamClosed
}
return nil
})
// A closed stream is not an error: return without pushing again.
if err == nil || errors.Is(err, errServiceAccountKeyListStreamClosed) {
return
}
diags.AddError("API Error", err.Error())
result := listReq.NewListResult(ctx)
result.Diagnostics = diags
push(result)
}
}

func flattenGoogleServiceAccountKeyListItem(res map[string]interface{}, d *schema.ResourceData, config *transport_tpg.Config) error {
var key iam.ServiceAccountKey
if err := tpgresource.Convert(res, &key); err != nil {
return err
}
d.SetId(key.Name)
return flattenServiceAccountKey(d, config, &key)
}

func ListServiceAccountKeys(config *transport_tpg.Config, serviceAccountID string, callback func(rd *schema.ResourceData) error) error {
if config == nil {
return fmt.Errorf("provider client is not configured")
}
d := ResourceGoogleServiceAccountKey().Data(&terraform.InstanceState{})
if err := d.Set("service_account_id", serviceAccountID); err != nil {
return fmt.Errorf("error setting service_account_id on temporary resource data: %w", err)
}

var (
url string
err error
)
if strings.Contains(serviceAccountID, "projects/") {
url = transport_tpg.BaseUrl(iambeta.Product, config) + serviceAccountID + "/keys"
} else {
url, err = tpgresource.ReplaceVars(d, config, transport_tpg.BaseUrl(iambeta.Product, config)+"projects/-/serviceAccounts/{{service_account_id}}/keys")
if err != nil {
return err
}
}

billingProject := ""
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}

userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}

return transport_tpg.ListPages(transport_tpg.ListPagesOptions{
Config: config,
TempData: d,
Resource: ResourceGoogleServiceAccountKey(),
ListURL: url,
BillingProject: billingProject,
UserAgent: userAgent,
ItemName: "keys",
Flattener: flattenGoogleServiceAccountKeyListItem,
Callback: callback,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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"
_ "github.com/hashicorp/terraform-provider-google/google/services/resourcemanager"
)

func TestAccListGoogleServiceAccountKey_queryIdentity(t *testing.T) {
t.Parallel()

accountId := fmt.Sprintf("tf-test-%s", acctest.RandString(t, 10))
project := envvar.GetTestProjectFromEnv()

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: testAccListGoogleServiceAccountKey_setup(project, accountId),
},
{
Query: true,
Config: testAccListGoogleServiceAccountKey_query(project, accountId),
QueryResultChecks: []querycheck.QueryResultCheck{
querycheck.ExpectLengthAtLeast("google_service_account_key.all", 1),
querycheck.ExpectIdentity("google_service_account_key.all", map[string]knownvalue.Check{
"name": knownvalue.NotNull(),
"service_account_id": knownvalue.NotNull(),
}),
},
},
},
})
}

func testAccListGoogleServiceAccountKey_setup(project, accountId string) string {
return fmt.Sprintf(`
resource "google_service_account" "sa" {
project = "%s"
account_id = "%s"
display_name = "Test SA for key listing"
}

resource "google_service_account_key" "key" {
service_account_id = google_service_account.sa.name
}
`, project, accountId)
}

func testAccListGoogleServiceAccountKey_query(project, accountId string) string {
return fmt.Sprintf(`
list "google_service_account_key" "all" {
provider = google
config {
service_account_id = "projects/%s/serviceAccounts/%s@%s.iam.gserviceaccount.com"
}
}
`, project, accountId, project)
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"google.golang.org/api/iam/v1"
iam "google.golang.org/api/iam/v1"
)

func ResourceGoogleServiceAccountKey() *schema.Resource {
Expand All @@ -23,6 +23,22 @@ func ResourceGoogleServiceAccountKey() *schema.Resource {
Update: resourceGoogleServiceAccountKeyUpdate,
Delete: resourceGoogleServiceAccountKeyDelete,

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

CustomizeDiff: customdiff.All(
tpgresource.DefaultProviderDeletionPolicy("DELETE"),
),
Expand Down Expand Up @@ -142,6 +158,12 @@ func resourceGoogleServiceAccountKeyCreate(d *schema.ResourceData, meta interfac
}

d.SetId(sak.Name)
if err := tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
"service_account_id": serviceAccountName,
"name": sak.Name,
}); err != nil {
return err
}
// Data only available on create.
if err := d.Set("valid_after", sak.ValidAfterTime); err != nil {
return fmt.Errorf("Error setting valid_after: %s", err)
Expand Down Expand Up @@ -192,6 +214,17 @@ func resourceGoogleServiceAccountKeyRead(d *schema.ResourceData, meta interface{
}
}

if err := flattenServiceAccountKey(d, config, sak); err != nil {
return err
}

return tpgresource.SetResourceIdentityAttributes(d, map[string]interface{}{
"service_account_id": d.Get("service_account_id").(string),
"name": sak.Name,
})
}

func flattenServiceAccountKey(d *schema.ResourceData, config *transport_tpg.Config, sak *iam.ServiceAccountKey) error {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be done in its own PR to ensure that the flattener is done properly

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a flattener implementation in #18241, does that cover what you're looking for here?

if err := d.Set("name", sak.Name); err != nil {
return fmt.Errorf("Error setting name: %s", err)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
subcategory: "Cloud Platform"
page_title: "google_service_account_key (list)"
description: |-
List Google Cloud IAM service account keys for a service account for use with
terraform query and .tfquery.hcl files.
---

# google_service_account_key (list)

## Overview

Lists service account keys belonging to a given service account.

Use this list resource 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](https://registry.terraform.io/providers/hashicorp/google/latest/docs/guides/using_list_resources_with_terraform_query).

## Example Usage

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

config {
service_account_id = "projects/my-project/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com"
}
}
```

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

## Configuration (`config` block)

* `service_account_id` - (Required) Parent service account to list keys for. This may be either
the full resource name (for example
`projects/my-project/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com`) or the
service account email.

## Identity Attributes

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

* `service_account_id` - Parent service account identifier.
* `name` - Full key resource name, such as
`projects/my-project/serviceAccounts/my-sa@my-project.iam.gserviceaccount.com/keys/1234567890abcdef`.

## Resource Attributes

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