Skip to content
Merged
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
239 changes: 239 additions & 0 deletions mmv1/third_party/terraform/tpgiamresource/resource_iam_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

// Shared core for IAM list resources (member, binding, policy).
// iamListCore holds the fields and methods that are common to all IAM list resources
// types. Each concrete type (IamMemberListResource, IamBindingListResource, IamPolicyListResource)
// embeds this struct and implements the methods that are specific to that type.
// iamListCore and adds only its type-specific result=shapping(List + yield/build methods).

// suportedScopeFields are the scope dimensions added to a list config
// automatically when the member resource's schema declares them.
// All are optional: when ommited, the value is resolved downstream from the provider
// config(GetProject/GetRegion/GetZone/GetLocation) or environment variables.

package tpgiamresource

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/list"
listschema "github.com/hashicorp/terraform-plugin-framework/list/schema"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

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

// suportedScopeFields are the scope dimensions added to a list config
// automatically when the member resource's schema declares them.
// All are optional: when ommited, the value is resolved downstream from the provider
// config(GetProject/GetRegion/GetZone/GetLocation) or environment variables.
var supportedScopeFields = []tpgresource.ListConfigField{
{Name: "project", Kind: tpgresource.ListConfigKindString, Optional: true},
{Name: "region", Kind: tpgresource.ListConfigKindString, Optional: true},
{Name: "zone", Kind: tpgresource.ListConfigKindString, Optional: true},
{Name: "location", Kind: tpgresource.ListConfigKindString, Optional: true},
}

// IamListCallConfig holds resource-specific pieces shared by all IAM list types.
// (Renamed conceptually from IAmMemberListCallConfig; see alias below for
// backwards compatibility with existin member call sites
type IamListCallConfig struct {
ListPagesOptions transport_tpg.ListPagesOptions
ParentResourceField string
EnableRoleFilter bool
EnableMemberFilter bool
}

// IamMemberListCallConfig is an alias for IamListCallConfig, for backwards compatibility with existing member call sites.
type IamMemberListCallConfig = IamListCallConfig

// IamMemberListResource lists IAM member rows by reading IAM policies on one or more policy targets.
type IamListCore struct {
tpgresource.ListResourceMetadata

typeName string
iamResource *schema.Resource // the underlying member/binding SDKv2 resource.
iamResourceSchema map[string]*schema.Schema // parent-identifying fields (project, zone, name, …)
listBlockSchema listschema.Schema
listCallConfig IamListCallConfig
newUpdater NewResourceIamUpdaterFunc
Client *transport_tpg.Config
}

func buildIamListCore(typeName string, iamResource *schema.Resource, newUpdater NewResourceIamUpdaterFunc, listCallConfig IamListCallConfig) IamListCore {
if iamResource.Identity == nil {
panic("tpgiamresource: IAM List resource requires a iamResource with identity (use IamWithResourceIdentity)")
}

listConfigFields := []tpgresource.ListConfigField{
{
Name: listCallConfig.ParentResourceField,
Kind: tpgresource.ListConfigKindString,
},
}

// Auto-expose target-scope dimensions (project/region/zone/location) when the
// member resource declares them and they are not already the parent field.
for _, sf := range supportedScopeFields {
if sf.Name == listCallConfig.ParentResourceField {
continue // scope dimension is itself the parent (e.g. project-iam-member)
}
if _, ok := iamResource.Schema[sf.Name]; ok {
listConfigFields = append(listConfigFields, sf)
}
}

if listCallConfig.EnableRoleFilter {
listConfigFields = append(listConfigFields, tpgresource.ListConfigField{
Name: "role",
Kind: tpgresource.ListConfigKindString,
Optional: true,
})
}

if listCallConfig.EnableMemberFilter {
listConfigFields = append(listConfigFields, tpgresource.ListConfigField{
Name: "member",
Kind: tpgresource.ListConfigKindString,
Optional: true,
})
}

iamResourceSchema := make(map[string]*schema.Schema)
for _, field := range listConfigFields {
if field.Name == "role" || field.Name == "member" {
continue
}
if schemaField, ok := iamResource.Schema[field.Name]; ok {
iamResourceSchema[field.Name] = schemaField
}
}

return IamListCore{
ListResourceMetadata: tpgresource.ListResourceMetadata{
TypeName: typeName,
SDKv2Resource: iamResource,
ListConfigFields: listConfigFields,
},
typeName: typeName,
iamResource: iamResource,
iamResourceSchema: iamResourceSchema,
listCallConfig: listCallConfig,
newUpdater: newUpdater,
}
}

func (r *IamListCore) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = r.typeName
}

func (r *IamListCore) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
r.Defaults(req, resp)

if req.ProviderData == nil {
return
}

config, ok := req.ProviderData.(*transport_tpg.Config)
if !ok {
resp.Diagnostics.AddError(
"Unexpected provider data type",
fmt.Sprintf("Expected *transport_tpg.Config, got %T", req.ProviderData),
)

return
}
r.Client = config
}

func (r *IamListCore) RawV5Schemas(ctx context.Context, _ list.RawV5SchemaRequest, resp *list.RawV5SchemaResponse) {
resp.ProtoV5Schema = r.iamResource.ProtoSchema(ctx)()
if fn := r.iamResource.ProtoIdentitySchema(ctx); fn != nil {
resp.ProtoV5IdentitySchema = fn()
}
}

// discoverPolicyTargets returns one ResourceData per GCP resource whose IAM policy should be read.
func (r *IamListCore) discoverPolicyTargets(ctx context.Context, req list.ListRequest) ([]*schema.ResourceData, error) {
baseRd := r.iamResource.TestResourceData()

// Set every target-identifying field (parent + scope dimensions like project/region/
// zone/location) from the list config onto the ResourceData the updater reads.
// Provider-default fallback is handled downstream by the updators's
// updater's GetProject/GetRegion/GetZone/GetLocation when a value is omitted.
for name := range r.iamResourceSchema {
var v types.String
d := req.Config.GetAttribute(ctx, path.Root(name), &v)
if d.HasError() {
return nil, fmt.Errorf("%s", d.Errors()[0].Detail())
}
if v.IsNull() || v.IsUnknown() {
continue
}
if err := baseRd.Set(name, v.ValueString()); err != nil {
return nil, fmt.Errorf("setting %s: %w", name, err)
}
}

if r.listCallConfig.ListPagesOptions.Callback == nil {
return []*schema.ResourceData{baseRd}, nil
}

if r.Client == nil {
return nil, fmt.Errorf("provider client nil")
}

var targets []*schema.ResourceData

listOpts := r.listCallConfig.ListPagesOptions
listOpts.Config = r.Client
listOpts.TempData = baseRd
listOpts.Resource = r.iamResource
listOpts.UserAgent = r.Client.UserAgent

if listOpts.ItemName == "" {
listOpts.ItemName = "items"
}

listOpts.Callback = func(rd *schema.ResourceData) error {
targetRd := r.iamResource.TestResourceData()
targets = append(targets, targetRd)
return nil
}

if err := transport_tpg.ListPages(listOpts); err != nil {
return nil, fmt.Errorf("listing Iam policy targets: %w", err)
}
return targets, nil
}

func (r *IamMemberListResource) readFilters(ctx context.Context, req list.ListRequest) (string, string, diag.Diagnostics) {
var diags diag.Diagnostics
roleFilter := ""
memberFilter := ""

if r.listCallConfig.EnableRoleFilter {
var v types.String
diags.Append(req.Config.GetAttribute(ctx, path.Root("role"), &v)...)
if !v.IsNull() && !v.IsUnknown() {
roleFilter = v.ValueString()
}
}

if r.listCallConfig.EnableMemberFilter {
var v types.String
diags.Append(req.Config.GetAttribute(ctx, path.Root("member"), &v)...)
if !v.IsNull() && !v.IsUnknown() {
memberFilter = v.ValueString()
}
}

return roleFilter, memberFilter, diags
}
Loading
Loading