From 4faf641ad7e1ea9120a40cf9fb431b95d0c55ab6 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 27 Feb 2025 18:57:02 -0800 Subject: [PATCH 01/17] Get to no redline state --- internal/service/lakeformation/opt_in.go | 1076 ++++++++++++++++++++++ 1 file changed, 1076 insertions(+) create mode 100644 internal/service/lakeformation/opt_in.go diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go new file mode 100644 index 000000000000..15d1c3df8184 --- /dev/null +++ b/internal/service/lakeformation/opt_in.go @@ -0,0 +1,1076 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lakeformation + +import ( + "context" + "errors" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lakeformation" + awstypes "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" + "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_lakeformation_opt_in", name="Opt In") +func newResourceOptIn(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceOptIn{} + + // r.SetDefaultCreateTimeout(30 * time.Minute) + // r.SetDefaultUpdateTimeout(30 * time.Minute) + // r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +const ( + ResNameOptIn = "Opt In" +) + +type resourceOptIn struct { + framework.ResourceWithConfigure + framework.WithTimeouts + framework.WithNoOpUpdate[resourceOptIn] +} + +func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "last_updated_by": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, + "last_modified": schema.StringAttribute{ + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "condition": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Condition](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "expression": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + "principal": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[DataLakePrincipal](ctx), + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "data_lake_principal_identifier": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + "catalog": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Catalog](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "catalog_id": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + "data_cells_filter": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[DataCellsFilter](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "database_name": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Optional: true, + }, + "table_catalog_id": schema.StringAttribute{ + Optional: true, + }, + "table_name": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + "data_location": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[DataLocation](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrResourceARN: schema.StringAttribute{ + Required: true, + }, + "catalog_id": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + }, + }, + }, + names.AttrDatabase: schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Database](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "lf_tag": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[LFTag](ctx), + Validators: []validator.List{ + listvalidator.IsRequired(), + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptionalComputed(), + names.AttrKey: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 128), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrValue: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + stringvalidator.RegexMatches(regexache.MustCompile(`^([\p{L}\p{Z}\p{N}_.:\*\/=+\-@%]*)$`), ""), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "lf_tag_expression": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "lf_tag_policy": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[LFTagPolicy](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "resource_type": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.ResourceType](), + }, + names.AttrCatalogID: catalogIDSchemaOptionalComputed(), + names.AttrExpression: schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Optional: true, + }, + "expression_name": schema.StringAttribute{ + Optional: true, + }, + }, + }, + }, + "table": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[table](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + names.AttrDatabaseName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrName: schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName(names.AttrName), + path.MatchRelative().AtParent().AtName("wildcard"), + ), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "wildcard": schema.BoolAttribute{ + Optional: true, + Validators: []validator.Bool{ + boolvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName(names.AttrName), + path.MatchRelative().AtParent().AtName("wildcard"), + ), + }, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + "table_with_columns": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[tableWithColumns](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + "column_names": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("column_names"), + path.MatchRelative().AtParent().AtName("column_wildcard"), + ), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + }, + names.AttrDatabaseName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "column_wildcard": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[columnWildcardData](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("column_names"), + path.MatchRelative().AtParent().AtName("column_wildcard"), + ), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "excluded_column_names": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + Optional: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + }, + }, + }, + + // "timeouts": timeouts.Block(ctx, timeouts.Opts{ + // Create: true, + // Update: true, + // Delete: true, + // }), + }, + } +} + +func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().LakeFormationClient(ctx) + + var plan resourceOptInData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := lakeformation.CreateLakeFormationOptInInput{} + + resp.Diagnostics.Append(fwflex.Expand(ctx, plan, &in)...) + if resp.Diagnostics.HasError() { + return + } + + output, err := conn.CreateLakeFormationOptIn(ctx, &in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, plan.Principal.String(), err), + err.Error(), + ) + return + } + + if output == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, plan.Principal.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(fwflex.Flatten(ctx, output, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 7. Save the request plan to response state + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceOptIn) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().LakeFormationClient(ctx) + + var state resourceOptInData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + optinResource, diags := state.Resource.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + optin := newOptInResourcer(optinResource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + opinr := optin.expandOptInResource(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + principalData, diags := state.Principal.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findOptInByID(ctx, conn, principalData.DataLakePrincipalIdentifier.ValueString(), opinr) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionSetting, ResNameOptIn, principalData.DataLakePrincipalIdentifier.String(), err), + err.Error(), + ) + return + } + + resp.Diagnostics.Append(fwflex.Flatten(ctx, out, &state)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().LakeFormationClient(ctx) + + var state resourceOptInData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + in := &lakeformation.DeleteLakeFormationOptInInput{} + optinResource, diags := state.Resource.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + optin := newOptInResourcer(optinResource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + principalData, diags := state.Principal.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + + in.Resource = optin.expandOptInResource(ctx, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + // out, err := findOptInByID(ctx, conn, principalData.DataLakePrincipalIdentifier.ValueString(), in.Resource) + // if err != nil { + // resp.Diagnostics.AddError( + // create.ProblemStandardMessage(names.LakeFormation, create.ErrActionSetting, ResNameOptIn, principalData.DataLakePrincipalIdentifier.String(), err), + // err.Error(), + // ) + // return + // } + + if _, err := conn.DeleteLakeFormationOptIn(ctx, in); err != nil { + if errs.IsA[*awstypes.EntityNotFoundException](err) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionDeleting, ResNameOptIn, principalData.DataLakePrincipalIdentifier.String(), err), + err.Error(), + ) + return + } + + // deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + // _, err = waitOptInDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + // if err != nil { + // resp.Diagnostics.AddError( + // create.ProblemStandardMessage(names.LakeFormation, create.ErrActionWaitingForDeletion, ResNameOptIn, state.ID.String(), err), + // err.Error(), + // ) + // return + // } +} + +func (r *resourceOptIn) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func findOptIns(ctx context.Context, conn *lakeformation.Client, input *lakeformation.ListLakeFormationOptInsInput, filter tfslices.Predicate[*awstypes.LakeFormationOptInsInfo]) ([]awstypes.LakeFormationOptInsInfo, error) { + var output []awstypes.LakeFormationOptInsInfo + + pages := lakeformation.NewListLakeFormationOptInsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*awstypes.EntityNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { + return nil, err + } + + for _, v := range page.LakeFormationOptInsInfoList { + if filter(&v) { + output = append(output, v) + } + } + } + + return output, nil +} + +func findOptInByID(ctx context.Context, conn *lakeformation.Client, id string, resource *awstypes.Resource) (*awstypes.LakeFormationOptInsInfo, error) { + + in := &lakeformation.ListLakeFormationOptInsInput{} + + in.Resource = resource + + return findOptIn(ctx, conn, in, tfslices.Predicate[*awstypes.LakeFormationOptInsInfo](func(v *awstypes.LakeFormationOptInsInfo) bool { + return aws.ToString(v.Principal.DataLakePrincipalIdentifier) == id + })) +} + +func findOptIn(ctx context.Context, conn *lakeformation.Client, input *lakeformation.ListLakeFormationOptInsInput, filter tfslices.Predicate[*awstypes.LakeFormationOptInsInfo]) (*awstypes.LakeFormationOptInsInfo, error) { + output, err := findOptIns(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSingleValueResult(output) +} + +type optInResourcer interface { + expandOptInResource(context.Context, *diag.Diagnostics) *awstypes.Resource + findOptIn(context.Context, *lakeformation.ListLakeFormationOptInsOutput, *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] + // findOptInByAttr(context.Context, *lakeformation.Client, string, string) (*awstypes.LakeFormationOptInsInfo, error) +} + +type catalogResource struct { + data *ResourceData +} + +type dbResource struct { + data *ResourceData +} + +type dcfResource struct { + data *ResourceData +} + +type dlResource struct { + data *ResourceData +} + +type lftagResource struct { + data *ResourceData +} + +type lfteResource struct { + data *ResourceData +} + +type lftpResource struct { + data *ResourceData +} + +type tbResource struct { + data *ResourceData +} + +type tbcResource struct { + data *ResourceData +} + +func newOptInResourcer(data *ResourceData, diags *diag.Diagnostics) optInResourcer { + switch { + case !data.Catalog.IsNull(): + return &catalogResource{data: data} + case !data.Database.IsNull(): + return &dbResource{data: data} + case !data.DataCellsFilter.IsNull(): + return &dcfResource{data: data} + case !data.DataLocation.IsNull(): + return &dlResource{data: data} + case !data.LFTag.IsNull(): + return &lftagResource{data: data} + case !data.LFTagExpression.IsNull(): + return &lfteResource{data: data} + case !data.LFTagPolicy.IsNull(): + return &lftpResource{data: data} + case !data.Table.IsNull(): + return &tbResource{data: data} + case !data.TableWithColumns.IsNull(): + return &tbcResource{data: data} + default: + diags.AddError("unexpected resource type", + "unexpected resource type") + return nil + } +} + +// //////////////////////// CATALOG ////////////////////////// +func (d *catalogResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + catalogptr, err := d.data.Catalog.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var catalog awstypes.CatalogResource + diags.Append(fwflex.Expand(ctx, catalogptr, &catalog)...) + if diags.HasError() { + return nil + } + + r.Catalog = &catalog + return &r +} + +func (d *catalogResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + catalog, err := d.data.Catalog.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.Catalog != nil { + if aws.ToString(v.Resource.Catalog.Id) == catalog.ID.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + Catalog: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &Catalog{ + ID: fwflex.StringToFramework(ctx, v.Resource.Catalog.Id), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +//////////////////////////////////////////////////////////// + +////////////////////////// DATABASE ////////////////////////// + +func (d *dbResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + dbptr, err := d.data.Database.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var db awstypes.DatabaseResource + diags.Append(fwflex.Expand(ctx, dbptr, &db)...) + if diags.HasError() { + return nil + } + + r.Database = &db + return &r +} + +func (d *dbResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + db, err := d.data.Database.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.Database != nil { + if aws.ToString(v.Resource.Database.Name) == db.Name.ValueString() && + aws.ToString(v.Resource.Database.CatalogId) == db.CatalogID.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + Database: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &Database{ + Name: fwflex.StringToFramework(ctx, v.Resource.Database.Name), + CatalogID: fwflex.StringToFramework(ctx, v.Resource.Database.CatalogId), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +////////////////////////////////////////////////////////////// + +// //////////////////DATA_CELLS_FILTER////////////////////////// +func (d *dcfResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + dcfptr, err := d.data.DataCellsFilter.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var dcf awstypes.DataCellsFilterResource + diags.Append(fwflex.Expand(ctx, dcfptr, &dcf)...) + if diags.HasError() { + return nil + } + + r.DataCellsFilter = &dcf + return &r +} + +func (d *dcfResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + dcf, err := d.data.DataCellsFilter.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.DataCellsFilter != nil { + if aws.ToString(v.Resource.DataCellsFilter.Name) == dcf.Name.ValueString() && + aws.ToString(v.Resource.DataCellsFilter.DatabaseName) == dcf.DatabaseName.ValueString() && + aws.ToString(v.Resource.DataCellsFilter.TableCatalogId) == dcf.TableCatalogID.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + DataCellsFilter: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &DataCellsFilter{ + Name: fwflex.StringToFramework(ctx, v.Resource.DataCellsFilter.Name), + DatabaseName: fwflex.StringToFramework(ctx, v.Resource.DataCellsFilter.DatabaseName), + TableCatalogID: fwflex.StringToFramework(ctx, v.Resource.DataCellsFilter.TableCatalogId), + TableName: fwflex.StringToFramework(ctx, v.Resource.DataCellsFilter.TableName), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +///////////////////////////////////////////////////////////////////////////// + +// /////////////////////DATA_LOCATION//////////////////////////// +func (d *dlResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + dlptr, err := d.data.DataLocation.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var dl awstypes.DataLocationResource + diags.Append(fwflex.Expand(ctx, dlptr, &dl)...) + if diags.HasError() { + return nil + } + + r.DataLocation = &dl + return &r +} + +func (d *dlResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + dl, err := d.data.DataLocation.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.DataLocation != nil { + if aws.ToString(v.Resource.DataLocation.ResourceArn) == dl.ResourceArn.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + DataLocation: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &DataLocation{ + ResourceArn: fwflex.StringToFramework(ctx, v.Resource.DataLocation.ResourceArn), + CatalogID: fwflex.StringToFramework(ctx, v.Resource.DataLocation.CatalogId), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +///////////////////////////////////////////////////////////////////////////////////////////// + +// //////////////////////// LFTAG //////////////////////////////////////////////////// +func (d *lftagResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + lftagptr, err := d.data.LFTag.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var lftag awstypes.LFTagKeyResource + diags.Append(fwflex.Expand(ctx, lftagptr, &lftag)...) + if diags.HasError() { + return nil + } + + r.LFTag = &lftag + return &r +} + +func (d *lftagResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + lftag, err := d.data.LFTag.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.LFTag != nil { + if aws.ToString(v.Resource.LFTag.TagKey) == lftag.Key.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + LFTag: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &LFTag{ + Key: fwflex.StringToFramework(ctx, v.Resource.LFTag.TagKey), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +// ///////////////////////LFTAG EXPRESSION////////////////////// +func (d *lfteResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + lfteptr, err := d.data.LFTagExpression.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var lfte awstypes.LFTagExpressionResource + diags.Append(fwflex.Expand(ctx, lfteptr, &lfte)...) + if diags.HasError() { + return nil + } + + r.LFTagExpression = &lfte + return &r +} + +func (d *lfteResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + lfte, err := d.data.LFTagExpression.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.LFTagExpression != nil { + if aws.ToString(v.Resource.LFTagExpression.Name) == lfte.Name.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + LFTagExpression: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &LFTagExpression{ + Name: fwflex.StringToFramework(ctx, v.Resource.LFTagExpression.Name), + CatalogID: fwflex.StringToFramework(ctx, v.Resource.LFTagExpression.CatalogId), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +// /////////////////////LFTAG POLICY //////////////////////////////////// +func (d *lftpResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + lftptr, err := d.data.LFTagPolicy.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var lft awstypes.LFTagPolicyResource + diags.Append(fwflex.Expand(ctx, lftptr, &lft)...) + if diags.HasError() { + return nil + } + + r.LFTagPolicy = &lft + return &r +} + +func (d *lftpResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + lftp, err := d.data.LFTagPolicy.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.LFTagPolicy != nil { + if aws.ToString((*string)(&v.Resource.LFTagPolicy.ResourceType)) == lftp.ResourceType.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + LFTagPolicy: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &LFTagPolicy{ + ResourceType: fwtypes.StringEnumValue(v.Resource.LFTagPolicy.ResourceType), + CatalogID: fwflex.StringToFramework(ctx, v.Resource.LFTagPolicy.CatalogId), + ExpressionName: fwflex.StringToFramework(ctx, v.Resource.LFTagPolicy.ExpressionName), + }), + }) + return out + } + } + } + + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +// ///////////////////////TABLE//////////////////////////////// +func (d *tbResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + tableptr, err := d.data.Table.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var table awstypes.TableResource + diags.Append(fwflex.Expand(ctx, tableptr, &table)...) + if diags.HasError() { + return nil + } + + r.Table = &table + return &r +} + +func (d *tbResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + tb, err := d.data.Table.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.Table != nil { + if aws.ToString(v.Resource.Table.Name) == tb.Name.ValueString() && + aws.ToString(v.Resource.Table.DatabaseName) == tb.DatabaseName.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + Table: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &table{ + Name: fwflex.StringToFramework(ctx, v.Resource.Table.Name), + }), + }) + return out + } + } + } + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +// ////////////////////////////////////////////////////////////////////////////////////////////// +func (d *tbcResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { + var r awstypes.Resource + tbcptr, err := d.data.Table.ToPtr(ctx) + diags.Append(err...) + if diags.HasError() { + return nil + } + + var tbc awstypes.TableWithColumnsResource + diags.Append(fwflex.Expand(ctx, tbcptr, &tbc)...) + if diags.HasError() { + return nil + } + + r.TableWithColumns = &tbc + return &r +} + +func (d *tbcResource) findOptIn(ctx context.Context, input *lakeformation.ListLakeFormationOptInsOutput, diags *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] { + tbc, err := d.data.TableWithColumns.ToPtr(ctx) + if err != nil { + diags.Append(err...) + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) + } + + for _, v := range input.LakeFormationOptInsInfoList { + if v.Resource != nil && v.Resource.TableWithColumns != nil { + if aws.ToString(v.Resource.TableWithColumns.Name) == tbc.Name.ValueString() && + aws.ToString(v.Resource.TableWithColumns.DatabaseName) == tbc.DatabaseName.ValueString() { + out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ + TableWithColumns: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &tableWithColumns{ + Name: fwflex.StringToFramework(ctx, v.Resource.TableWithColumns.Name), + }), + }) + return out + } + } + } + return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) +} + +//////////////////////////////////////////////////////////////////////////////////////////////// + +type resourceOptInData struct { + Principal fwtypes.ListNestedObjectValueOf[DataLakePrincipal] `tfsdk:"principal"` + Resource fwtypes.ListNestedObjectValueOf[ResourceData] `tfsdk:"resource"` + Condition fwtypes.ListNestedObjectValueOf[Condition] `tfsdk:"condition"` + LastUpdatedBy timetypes.RFC3339 `tfsdk:"last_updated_by"` + LastModified types.String `tfsdk:"last_modified"` +} + +type DataLakePrincipal struct { + DataLakePrincipalIdentifier types.String `tfsdk:"data_lake_principal_identifier"` +} + +type ResourceData struct { + Catalog fwtypes.ListNestedObjectValueOf[Catalog] `tfsdk:"catalog"` + DataCellsFilter fwtypes.ListNestedObjectValueOf[DataCellsFilter] `tfsdk:"data_cells_filter"` + DataLocation fwtypes.ListNestedObjectValueOf[DataLocation] `tfsdk:"data_location"` + Database fwtypes.ListNestedObjectValueOf[Database] `tfsdk:"database"` + LFTag fwtypes.ListNestedObjectValueOf[LFTag] `tfsdk:"lf_tag"` + LFTagExpression fwtypes.ListNestedObjectValueOf[LFTagExpression] `tfsdk:"lf_tag_expression"` + LFTagPolicy fwtypes.ListNestedObjectValueOf[LFTagPolicy] `tfsdk:"lf_tag_policy"` + Table fwtypes.ListNestedObjectValueOf[table] `tfsdk:"table"` + TableWithColumns fwtypes.ListNestedObjectValueOf[tableWithColumns] `tfsdk:"table_with_columns"` +} + +type Catalog struct { + ID types.String `tfsdk:"id"` +} + +type Condition struct { + Expression types.String `tfsdk:"expression"` +} + +type DataCellsFilter struct { + DatabaseName types.String `tfsdk:"database_name"` + Name types.String `tfsdk:"name"` + TableCatalogID types.String `tfsdk:"table_catalog_id"` + TableName types.String `tfsdk:"table_name"` +} + +type DataLocation struct { + ResourceArn types.String `tfsdk:"resource_arn"` + CatalogID types.String `tfsdk:"catalog_id"` +} + +type LFTagExpression struct { + Name types.String `tfsdk:"name"` + CatalogID types.String `tfsdk:"catalog_id"` +} + +type LFTagPolicy struct { + ResourceType fwtypes.StringEnum[awstypes.ResourceType] `tfsdk:"resource_type"` + CatalogID types.String `tfsdk:"catalog_id"` + Expression fwtypes.ListValueOf[types.String] `tfsdk:"expression"` + ExpressionName types.String `tfsdk:"expression_name"` +} From 3413e3d733db04891f4153bf087af3cc44833d64 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 27 Feb 2025 21:48:01 -0800 Subject: [PATCH 02/17] No red lines in test file --- internal/service/lakeformation/opt_in_test.go | 369 ++++++++++++++++++ 1 file changed, 369 insertions(+) create mode 100644 internal/service/lakeformation/opt_in_test.go diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go new file mode 100644 index 000000000000..04fb5618517a --- /dev/null +++ b/internal/service/lakeformation/opt_in_test.go @@ -0,0 +1,369 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lakeformation_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lakeformation" + "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" + awstypes "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + tflakeformation "github.com/hashicorp/terraform-provider-aws/internal/service/lakeformation" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccLakeFormationOptIn_basic(t *testing.T) { + ctx := acctest.Context(t) + // TIP: This is a long-running test guard for tests that run longer than + // 300s (5 min) generally. + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var optin lakeformation.ListLakeFormationOptInsOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lakeformation_opt_in.test" + databaseName := "aws_glue_catalog_database.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LakeFormationServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LakeFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckOptInDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOptInConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckOptInExists(ctx, resourceName, &optin), + resource.TestCheckResourceAttr(resourceName, "principals.#", "1"), + resource.TestCheckResourceAttr(resourceName, "resource.0.principal.data_lake_principal_identifier", databaseName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + // ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + }, + }, + }) +} + +func TestAccLakeFormationOptIn_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var optin lakeformation.ListLakeFormationOptInsOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lakeformation_opt_in.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LakeFormationServiceID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LakeFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckOptInDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccOptInConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckOptInExists(ctx, resourceName, &optin), + // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, + // but expects a new resource factory function as the third argument. To expose this + // private function to the testing package, you may need to add a line like the following + // to exports_test.go: + // + // var ResourceOptIn = newResourceOptIn + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflakeformation.ResourceOptIn, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LakeFormationClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lakeformation_opt_in" { + continue + } + + // Extract principal from state + principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] + if principalID == "" { + return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, errors.New("principal identifier not found in state")) + } + + // Create resource based on what's in state + input := &lakeformation.ListLakeFormationOptInsInput{ + Resource: &awstypes.Resource{}, + Principal: &awstypes.DataLakePrincipal{ + DataLakePrincipalIdentifier: aws.String(principalID), + }, + } + + // Check each possible resource type + if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, + } + } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + Database: &awstypes.DatabaseResource{ + Name: aws.String(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + DataCellsFilter: &awstypes.DataCellsFilterResource{ + Name: aws.String(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + DataLocation: &awstypes.DataLocationResource{ + ResourceArn: aws.String(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + LFTag: &awstypes.LFTagKeyResource{ + TagKey: aws.String(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + LFTagExpression: &awstypes.LFTagExpressionResource{ + Name: aws.String(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + LFTagPolicy: &awstypes.LFTagPolicyResource{ + ResourceType: awstypes.ResourceType(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + Table: &awstypes.TableResource{ + DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { + input.Resource = &awstypes.Resource{ + TableWithColumns: &awstypes.TableWithColumnsResource{ + Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), + }, + } + } + + _, err := conn.ListLakeFormationOptIns(ctx, input) + + if errs.IsA[*awstypes.EntityNotFoundException](err) { + continue + } + + if errs.IsAErrorMessageContains[*awstypes.InvalidInputException](err, "not found") { + continue + } + + if err != nil { + return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, err) + } + + return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformation.ListLakeFormationOptInsOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).LakeFormationClient(ctx) + + // Extract principal from state + principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] + if principalID == "" { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("principal identifier not set")) + } + + // Create input with resource based on what's in state + in := &lakeformation.ListLakeFormationOptInsInput{} + var resource *types.Resource + + // Check each possible resource type + if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { + resource = &types.Resource{ + Catalog: &types.CatalogResource{Id: aws.String(v)}, + } + } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { + resource = &types.Resource{ + Database: &types.DatabaseResource{ + Name: aws.String(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.database.0.catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { + resource = &types.Resource{ + DataCellsFilter: &types.DataCellsFilterResource{ + Name: aws.String(v), + // DatabaseName: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.database_name"]), + // TableCatalogId: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.table_catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { + resource = &types.Resource{ + DataLocation: &types.DataLocationResource{ + ResourceArn: aws.String(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.data_location.0.catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { + resource = &types.Resource{ + LFTag: &types.LFTagKeyResource{ + TagKey: aws.String(v), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { + resource = &types.Resource{ + LFTagExpression: &types.LFTagExpressionResource{ + Name: aws.String(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_expression.0.catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { + resource = &types.Resource{ + LFTagPolicy: &types.LFTagPolicyResource{ + ResourceType: types.ResourceType(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.catalog_id"]), + // ExpressionName: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.expression_name"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { + resource = &types.Resource{ + Table: &types.TableResource{ + // Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { + resource = &types.Resource{ + TableWithColumns: &types.TableWithColumnsResource{ + Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), + }, + } + } + + if resource == nil { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("no valid resource found in state")) + } + + in.Resource = resource + + out, err := tflakeformation.FindOptInByID(ctx, conn, principalID, resource) + if err != nil { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, principalID, err) + } + + *optin = lakeformation.ListLakeFormationOptInsOutput{ + LakeFormationOptInsInfoList: []types.LakeFormationOptInsInfo{*out}, + } + + return nil + } +} + +func testAccOptInConfig_basic(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + path = "/" + + assume_role_policy = jsonencode({ + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "glue.${data.aws_partition.current.dns_suffix}" + } + }] + Version = "2012-10-17" + }) +} + +resource "aws_glue_catalog_database" "test" { + name = %[1]q +} + +data "aws_caller_identity" "current" {} + +data "aws_iam_session_context" "current" { + arn = data.aws_caller_identity.current.arn +} + +resource "aws_lakeformation_data_lake_settings" "test" { + admins = [data.aws_iam_session_context.current.issuer_arn] +} + +resource "aws_lakeformation_permissions" "test" { + permissions = ["ALTER", "CREATE_TABLE", "DROP"] + permissions_with_grant_option = ["CREATE_TABLE"] + principal = aws_iam_role.test.arn + + database { + name = aws_glue_catalog_database.test.name + } + + # for consistency, ensure that admins are setup before testing + depends_on = [aws_lakeformation_data_lake_settings.test] +} + +resource "aws_lakeformation_opt_in" "test" { + principal { + data_lake_principal_identifier = aws_iam_role.test.arn + } + + resource { + database { + name = aws_glue_catalog_database.test.name + catalog_id = data.aws_caller_identity.current.account_id + } + } +}`, rName) +} From 0c5e36d2b660df16603070bcb6b6753b6bcd6cc6 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 27 Feb 2025 23:47:01 -0800 Subject: [PATCH 03/17] Add markdown --- .../docs/r/lakeformation_opt_in.html.markdown | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 website/docs/r/lakeformation_opt_in.html.markdown diff --git a/website/docs/r/lakeformation_opt_in.html.markdown b/website/docs/r/lakeformation_opt_in.html.markdown new file mode 100644 index 000000000000..6ba003f08036 --- /dev/null +++ b/website/docs/r/lakeformation_opt_in.html.markdown @@ -0,0 +1,126 @@ +--- +subcategory: "Lake Formation" +layout: "aws" +page_title: "AWS: aws_lakeformation_opt_in" +description: |- + Terraform resource for managing an AWS Lake Formation Opt In. +--- + +# Resource: aws_lakeformation_opt_in + +Terraform resource for managing an AWS Lake Formation Opt In. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_lakeformation_opt_in" "example" { +} +``` + +## Argument Reference + +The following arguments are required: + +* `principal` - (Required) Lake Formation principal. Supported principals are IAM users or IAM roles. See [Principal](#principal) for more details. +* `reosurce` - (Required) Structure for the resource. See [Resource](#resource) for more details. + + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `condition` - Lake Formation condition, which applies to permissions and opt-ins that contain an expression. +* `last_modified` - Last modified date and time of the record. +* `last_updated` - User who updated the record. + +### Principal + +The following arguments are required: +* `data_lake_principal` - (Required) Identifier for the Lake Formation principal. + +### Resource + +* `catalog` - Identifier for the Data Catalog. By default, the account ID. The Data Catalog is the persistent metadata store. It contains database definitions, table definitions, and other control information to manage your Lake Formation environment. See [Catalog](#catalog) for more details. +* `data_cells_filter` - Data cell filter. See [Data Cells Filter](#data-cells-filter) for more details. +* `data_location` - Location of an Amazon S3 path where permissions are granted or revoked. See [Data Location](#data-location) for more details. +* `database` - Database for the resource. Unique to the Data Catalog. A database is a set of associated table definitions organized into a logical group. You can Grant and Revoke database permissions to a principal. See [Database](#database) for more details. +* `lf_tag` - LF-tag key and values attached to a resource. +* `lf_tag_expression` - Logical expression composed of one or more LF-Tag key:value pairs. See [LF-Tag Expression](#lf-tag-expression) for more details. +* `lf_tag_policy` - List of LF-Tag conditions or saved LF-Tag expressions that define a resource's LF-Tag policy. See [LF-Tag Policy](#lf-tag-policy) for more details. +* `table` - Table for the resource. A table is a metadata definition that represents your data. You can Grant and Revoke table privileges to a principal. See [Table](#table) for more details. +* `table_with_columns` - Table with columns for the resource. A principal with permissions to this resource can select metadata from the columns of a table in the Data Catalog and the underlying data in Amazon S3. See [Table With Columns](#table-with-columns) for more details. + +### Catalog + +* `id` - Identifier for the catalog resource. + +### Data Cells Filter + +* `database_name` - Database in the Glue Data Catalog. +* `name` - Name of the data cells filter. +* `table_catalog_id` - ID of the catalog to which the table belongs. +* `table_name` - Name of the table. + +### Data Location + +* `resource_arn` - ARN that uniquely identifies the data location resource. +* `catalog_id` - Identifier for the Data Catalog where the location is registered with Lake Formation. By default, it is the account ID of the caller. + +### Database + +* `name` - Name of the database resource.Unique to the Data Catalog. +* `catalog_id` - Identifier for the Data Catalog. By default, it is the account ID of the caller. + +### LF-Tag Expression + +* `name` - Name of the LF-Tag expression to grand permissions on. +* `catalog_id` - Identifier for the Data Catalog. By default, it is the account ID of the caller. + +### LF-Tag Policy + +* `resource_type` - Resource type for which the LF-tag policy applies. +* `catalog_id` - Identifier for the Data Catalog. By default, it is the account ID of the caller. The Data Catalog is the persistent metadata store. It contains database definitions, table definitions, and other control information to manage your Lake Formation environment. +* `expression` - List of LF-tag conditions or a saved expression that apply to the resource's LF-Tag policy. +* `expression_name` - If provided, permissions are granted to the Data Catalog resources whose assigned LF-Tags match the expression body of the saved expression under the provided ExpressionName . + +### Table + +* `database_name` - The name of the database for the table. Unique to a Data Catalog. A database is a set of associated table definitions organized into a logical group. You can Grant and Revoke database privileges to a principal. +* `name` - Name of the table. +* `catalog_id` - Identifier for the Data Catalog. By default, it is the account ID of the caller. +* `table_wild_card` - Wildcard object representing every table under a database. At least one of TableResource$Name or TableResource$TableWildcard is required. + +### Table With Columns + +* `database_name` - The name of the database for the table. Unique to a Data Catalog. A database is a set of associated table definitions organized into a logical group. You can Grant and Revoke database privileges to a principal. +* `name` - Name of the table. +* `catalog_id` - Identifier for the Data Catalog. By default, it is the account ID of the caller. +* `column_names` - List of column names for the table. At least one of ColumnNames or ColumnWildcard is required. +* `column_wildcard` - Wildcard specified by a ColumnWildcard object. At least one of ColumnNames or ColumnWildcard is required. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `60m`) +* `update` - (Default `180m`) +* `delete` - (Default `90m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Lake Formation Opt In using the `example_id_arg`. For example: + +```terraform +import { + to = aws_lakeformation_opt_in.example + id = "opt_in-id-12345678" +} +``` + +Using `terraform import`, import Lake Formation Opt In using the `example_id_arg`. For example: + +```console +% terraform import aws_lakeformation_opt_in.example opt_in-id-12345678 +``` From 70f641b0eb304a9982df79415aded87fd1470b9d Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 27 Feb 2025 23:47:23 -0800 Subject: [PATCH 04/17] make gen --- internal/service/lakeformation/exports_test.go | 2 ++ internal/service/lakeformation/service_package_gen.go | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/internal/service/lakeformation/exports_test.go b/internal/service/lakeformation/exports_test.go index 9850aaa73a6a..063523e10aef 100644 --- a/internal/service/lakeformation/exports_test.go +++ b/internal/service/lakeformation/exports_test.go @@ -7,10 +7,12 @@ package lakeformation var ( ResourceDataCellsFilter = newResourceDataCellsFilter ResourceResourceLFTag = newResourceResourceLFTag + ResourceOptIn = newResourceOptIn FindDataCellsFilterByID = findDataCellsFilterByID FindResourceLFTagByID = findResourceLFTagByID LFTagParseResourceID = lfTagParseResourceID + FindOptInByID = findOptInByID ValidPrincipal = validPrincipal ) diff --git a/internal/service/lakeformation/service_package_gen.go b/internal/service/lakeformation/service_package_gen.go index 24130370c859..c1fad3db968b 100644 --- a/internal/service/lakeformation/service_package_gen.go +++ b/internal/service/lakeformation/service_package_gen.go @@ -25,6 +25,11 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic TypeName: "aws_lakeformation_data_cells_filter", Name: "Data Cells Filter", }, + { + Factory: newResourceOptIn, + TypeName: "aws_lakeformation_opt_in", + Name: "Opt In", + }, { Factory: newResourceResourceLFTag, TypeName: "aws_lakeformation_resource_lf_tag", From ecc53fda1f1e65be8ca97497888bb3ef019e663a Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Fri, 28 Feb 2025 10:33:44 -0800 Subject: [PATCH 05/17] Get to working state --- internal/service/lakeformation/opt_in.go | 571 ++++++++++-------- internal/service/lakeformation/opt_in_test.go | 208 +++---- 2 files changed, 436 insertions(+), 343 deletions(-) diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go index 15d1c3df8184..7d89e418bee8 100644 --- a/internal/service/lakeformation/opt_in.go +++ b/internal/service/lakeformation/opt_in.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -60,286 +61,305 @@ type resourceOptIn struct { } func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "last_updated_by": schema.StringAttribute{ - CustomType: timetypes.RFC3339Type{}, - Computed: true, - }, - "last_modified": schema.StringAttribute{ - Computed: true, + catalogLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Catalog](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrID: schema.StringAttribute{ + Optional: true, + }, }, }, - Blocks: map[string]schema.Block{ - "condition": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[Condition](ctx), - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "expression": schema.StringAttribute{ - Computed: true, - }, - }, + } + + dataCellsFilterLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[DataCellsFilter](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "database_name": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Optional: true, + }, + "table_catalog_id": schema.StringAttribute{ + Optional: true, + }, + "table_name": schema.StringAttribute{ + Optional: true, }, }, - "principal": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[DataLakePrincipal](ctx), - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), + }, + } + + dataLocationLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[DataLocation](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrResourceARN: schema.StringAttribute{ + Required: true, }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "data_lake_principal_identifier": schema.StringAttribute{ - Required: true, - }, - }, + "catalog_id": schema.StringAttribute{ + Optional: true, + Computed: true, }, }, - "catalog": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[Catalog](ctx), - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "catalog_id": schema.StringAttribute{ - Optional: true, - }, + }, + } + + databaseLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Database](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), }, }, }, - "data_cells_filter": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[DataCellsFilter](ctx), - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "database_name": schema.StringAttribute{ - Optional: true, - }, - "name": schema.StringAttribute{ - Optional: true, - }, - "table_catalog_id": schema.StringAttribute{ - Optional: true, - }, - "table_name": schema.StringAttribute{ - Optional: true, - }, + }, + } + + lfTagLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[LFTag](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptionalComputed(), + names.AttrKey: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 128), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrValue: schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + stringvalidator.RegexMatches(regexache.MustCompile(`^([\p{L}\p{Z}\p{N}_.:\*\/=+\-@%]*)$`), ""), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), }, }, }, - "data_location": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[DataLocation](ctx), - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - names.AttrResourceARN: schema.StringAttribute{ - Required: true, - }, - "catalog_id": schema.StringAttribute{ - Optional: true, - Computed: true, - }, + }, + } + + lftagExpressionLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[LFTagExpression](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), }, }, }, - names.AttrDatabase: schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[Database](ctx), - Validators: []validator.List{ - listvalidator.SizeAtMost(1), + }, + } + + lfTagPolicyLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[LFTagPolicy](ctx), + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "resource_type": schema.StringAttribute{ + Required: true, + CustomType: fwtypes.StringEnumType[awstypes.ResourceType](), }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), + names.AttrCatalogID: catalogIDSchemaOptionalComputed(), + "expression": schema.ListAttribute{ + CustomType: fwtypes.ListOfStringType, + ElementType: types.StringType, + Optional: true, }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - names.AttrCatalogID: catalogIDSchemaOptional(), - names.AttrName: schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, + "expression_name": schema.StringAttribute{ + Optional: true, + }, + }, + }, + } + + tableLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Table](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + names.AttrDatabaseName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrName: schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName(names.AttrName), + path.MatchRelative().AtParent().AtName("wildcard"), + ), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "wildcard": schema.BoolAttribute{ + Optional: true, + Validators: []validator.Bool{ + boolvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName(names.AttrName), + path.MatchRelative().AtParent().AtName("wildcard"), + ), + }, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), }, }, }, - "lf_tag": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[LFTag](ctx), - Validators: []validator.List{ - listvalidator.IsRequired(), - listvalidator.SizeAtMost(1), + }, + } + + tableWCLNB := schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[TableWithColumns](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + names.AttrCatalogID: catalogIDSchemaOptional(), + "column_names": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + Optional: true, + Validators: []validator.Set{ + setvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("column_names"), + path.MatchRelative().AtParent().AtName("column_wildcard"), + ), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), + names.AttrDatabaseName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - names.AttrCatalogID: catalogIDSchemaOptionalComputed(), - names.AttrKey: schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 128), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - names.AttrValue: schema.StringAttribute{ - Required: true, - Validators: []validator.String{ - stringvalidator.LengthBetween(1, 255), - stringvalidator.RegexMatches(regexache.MustCompile(`^([\p{L}\p{Z}\p{N}_.:\*\/=+\-@%]*)$`), ""), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, + names.AttrName: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), }, }, }, - "lf_tag_expression": schema.ListNestedBlock{ - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - names.AttrCatalogID: catalogIDSchemaOptional(), - names.AttrName: schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), + Blocks: map[string]schema.Block{ + "column_wildcard": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[columnWildcardData](ctx), + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + listvalidator.AtLeastOneOf( + path.MatchRelative().AtParent().AtName("column_names"), + path.MatchRelative().AtParent().AtName("column_wildcard"), + ), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "excluded_column_names": schema.SetAttribute{ + CustomType: fwtypes.SetOfStringType, + Optional: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, }, }, }, }, }, - "lf_tag_policy": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[LFTagPolicy](ctx), + }, + } + + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "last_updated_by": schema.StringAttribute{ + Computed: true, + }, + "last_modified": schema.StringAttribute{ + CustomType: timetypes.RFC3339Type{}, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "condition": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[Condition](ctx), NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "resource_type": schema.StringAttribute{ - Required: true, - CustomType: fwtypes.StringEnumType[awstypes.ResourceType](), - }, - names.AttrCatalogID: catalogIDSchemaOptionalComputed(), - names.AttrExpression: schema.ListAttribute{ - CustomType: fwtypes.ListOfStringType, - ElementType: types.StringType, - Optional: true, - }, - "expression_name": schema.StringAttribute{ - Optional: true, + "expression": schema.StringAttribute{ + Computed: true, }, }, }, }, - "table": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[table](ctx), + "principal": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[DataLakePrincipal](ctx), Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), + listvalidator.SizeAtLeast(1), }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - names.AttrCatalogID: catalogIDSchemaOptional(), - names.AttrDatabaseName: schema.StringAttribute{ + "data_lake_principal_identifier": schema.StringAttribute{ Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - names.AttrName: schema.StringAttribute{ - Optional: true, - Validators: []validator.String{ - stringvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName(names.AttrName), - path.MatchRelative().AtParent().AtName("wildcard"), - ), - }, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - "wildcard": schema.BoolAttribute{ - Optional: true, - Validators: []validator.Bool{ - boolvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName(names.AttrName), - path.MatchRelative().AtParent().AtName("wildcard"), - ), - }, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, }, }, }, }, - "table_with_columns": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[tableWithColumns](ctx), - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, + "resource_data": schema.ListNestedBlock{ + CustomType: fwtypes.NewListNestedObjectTypeOf[ResourceData](ctx), NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - names.AttrCatalogID: catalogIDSchemaOptional(), - "column_names": schema.SetAttribute{ - CustomType: fwtypes.SetOfStringType, - Optional: true, - Validators: []validator.Set{ - setvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName("column_names"), - path.MatchRelative().AtParent().AtName("column_wildcard"), - ), - }, - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - }, - }, - names.AttrDatabaseName: schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - }, - names.AttrName: schema.StringAttribute{ - Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - stringplanmodifier.UseStateForUnknown(), - }, - }, - }, Blocks: map[string]schema.Block{ - "column_wildcard": schema.ListNestedBlock{ - CustomType: fwtypes.NewListNestedObjectTypeOf[columnWildcardData](ctx), - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - listvalidator.AtLeastOneOf( - path.MatchRelative().AtParent().AtName("column_names"), - path.MatchRelative().AtParent().AtName("column_wildcard"), - ), - }, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "excluded_column_names": schema.SetAttribute{ - CustomType: fwtypes.SetOfStringType, - Optional: true, - PlanModifiers: []planmodifier.Set{ - setplanmodifier.RequiresReplace(), - }, - }, - }, - }, - }, + "catalog": catalogLNB, + "database": databaseLNB, + "data_cells_filter": dataCellsFilterLNB, + "data_location": dataLocationLNB, + "lf_tag": lfTagLNB, + "lf_tag_expression": lftagExpressionLNB, + "lf_tag_policy": lfTagPolicyLNB, + "table": tableLNB, + "table_with_columns": tableWCLNB, }, }, }, - - // "timeouts": timeouts.Block(ctx, timeouts.Opts{ - // Create: true, - // Update: true, - // Delete: true, - // }), }, } } @@ -353,6 +373,18 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, return } + // r := newOptInResourcer(resourceData, &diags) + // if diags.HasError() { + // resp.Diagnostics.Append(diags...) + // return + // } + + // resource := r.expandOptInResource(ctx, &diags) + // if diags.HasError() { + // resp.Diagnostics.Append(diags...) + // return + // } + in := lakeformation.CreateLakeFormationOptInInput{} resp.Diagnostics.Append(fwflex.Expand(ctx, plan, &in)...) @@ -360,10 +392,16 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, return } + principal, diags := plan.Principal.ToPtr(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + output, err := conn.CreateLakeFormationOptIn(ctx, &in) if err != nil { resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, plan.Principal.String(), err), + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, principal.DataLakePrincipalIdentifier.ValueString(), err), err.Error(), ) return @@ -371,12 +409,24 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, if output == nil { resp.Diagnostics.AddError( - create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, plan.Principal.String(), nil), + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, principal.DataLakePrincipalIdentifier.ValueString(), nil), errors.New("empty output").Error(), ) return } + lstrsc, err := conn.ListLakeFormationOptIns(ctx, &lakeformation.ListLakeFormationOptInsInput{}) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionSetting, ResNameOptIn, principal.DataLakePrincipalIdentifier.ValueString(), err), + err.Error(), + ) + return + } + + plan.LastModified = fwflex.TimeToFramework(ctx, lstrsc.LakeFormationOptInsInfoList[0].LastModified) + plan.LastUpdatedBy = fwflex.StringValueToFramework(ctx, *lstrsc.LakeFormationOptInsInfoList[0].LastUpdatedBy) + resp.Diagnostics.Append(fwflex.Flatten(ctx, output, &plan)...) if resp.Diagnostics.HasError() { return @@ -430,6 +480,15 @@ func (r *resourceOptIn) Read(ctx context.Context, req resource.ReadRequest, resp return } + if out.LastModified != nil { + state.LastModified = timetypes.NewRFC3339TimePointerValue(out.LastModified) + } + + if out.LastUpdatedBy != nil { + // state.LastUpdatedBy = types.StringValue(*out.LastUpdatedBy) + state.LastUpdatedBy = fwflex.StringToFramework(ctx, out.LastUpdatedBy) + } + resp.Diagnostics.Append(fwflex.Flatten(ctx, out, &state)...) resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } @@ -461,6 +520,9 @@ func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, return } + in.Principal = &awstypes.DataLakePrincipal{ + DataLakePrincipalIdentifier: aws.String(principalData.DataLakePrincipalIdentifier.ValueString()), + } in.Resource = optin.expandOptInResource(ctx, &resp.Diagnostics) if resp.Diagnostics.HasError() { @@ -498,6 +560,22 @@ func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, // } } +func (r *resourceOptIn) ConfigValidators(_ context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.ExactlyOneOf( + path.MatchRoot("resource_data").AtListIndex(0).AtName("catalog"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("data_cells_filter"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("data_location"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("database"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("lf_tag"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("lf_tag_expression"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("lf_tag_policy"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("table"), + path.MatchRoot("resource_data").AtListIndex(0).AtName("table_with_columns"), + ), + } +} + func (r *resourceOptIn) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } @@ -964,7 +1042,7 @@ func (d *tbResource) findOptIn(ctx context.Context, input *lakeformation.ListLak if aws.ToString(v.Resource.Table.Name) == tb.Name.ValueString() && aws.ToString(v.Resource.Table.DatabaseName) == tb.DatabaseName.ValueString() { out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ - Table: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &table{ + Table: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &Table{ Name: fwflex.StringToFramework(ctx, v.Resource.Table.Name), }), }) @@ -978,7 +1056,7 @@ func (d *tbResource) findOptIn(ctx context.Context, input *lakeformation.ListLak // ////////////////////////////////////////////////////////////////////////////////////////////// func (d *tbcResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource - tbcptr, err := d.data.Table.ToPtr(ctx) + tbcptr, err := d.data.TableWithColumns.ToPtr(ctx) diags.Append(err...) if diags.HasError() { return nil @@ -1006,7 +1084,7 @@ func (d *tbcResource) findOptIn(ctx context.Context, input *lakeformation.ListLa if aws.ToString(v.Resource.TableWithColumns.Name) == tbc.Name.ValueString() && aws.ToString(v.Resource.TableWithColumns.DatabaseName) == tbc.DatabaseName.ValueString() { out := fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &ResourceData{ - TableWithColumns: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &tableWithColumns{ + TableWithColumns: fwtypes.NewListNestedObjectValueOfPtrMust(ctx, &TableWithColumns{ Name: fwflex.StringToFramework(ctx, v.Resource.TableWithColumns.Name), }), }) @@ -1021,10 +1099,10 @@ func (d *tbcResource) findOptIn(ctx context.Context, input *lakeformation.ListLa type resourceOptInData struct { Principal fwtypes.ListNestedObjectValueOf[DataLakePrincipal] `tfsdk:"principal"` - Resource fwtypes.ListNestedObjectValueOf[ResourceData] `tfsdk:"resource"` + Resource fwtypes.ListNestedObjectValueOf[ResourceData] `tfsdk:"resource_data"` Condition fwtypes.ListNestedObjectValueOf[Condition] `tfsdk:"condition"` - LastUpdatedBy timetypes.RFC3339 `tfsdk:"last_updated_by"` - LastModified types.String `tfsdk:"last_modified"` + LastUpdatedBy types.String `tfsdk:"last_updated_by"` + LastModified timetypes.RFC3339 `tfsdk:"last_modified"` } type DataLakePrincipal struct { @@ -1039,8 +1117,8 @@ type ResourceData struct { LFTag fwtypes.ListNestedObjectValueOf[LFTag] `tfsdk:"lf_tag"` LFTagExpression fwtypes.ListNestedObjectValueOf[LFTagExpression] `tfsdk:"lf_tag_expression"` LFTagPolicy fwtypes.ListNestedObjectValueOf[LFTagPolicy] `tfsdk:"lf_tag_policy"` - Table fwtypes.ListNestedObjectValueOf[table] `tfsdk:"table"` - TableWithColumns fwtypes.ListNestedObjectValueOf[tableWithColumns] `tfsdk:"table_with_columns"` + Table fwtypes.ListNestedObjectValueOf[Table] `tfsdk:"table"` + TableWithColumns fwtypes.ListNestedObjectValueOf[TableWithColumns] `tfsdk:"table_with_columns"` } type Catalog struct { @@ -1074,3 +1152,18 @@ type LFTagPolicy struct { Expression fwtypes.ListValueOf[types.String] `tfsdk:"expression"` ExpressionName types.String `tfsdk:"expression_name"` } + +type Table struct { + CatalogID types.String `tfsdk:"catalog_id"` + DatabaseName types.String `tfsdk:"database_name"` + Name types.String `tfsdk:"name"` + Wildcard types.Bool `tfsdk:"wildcard"` +} + +type TableWithColumns struct { + CatalogID types.String `tfsdk:"catalog_id"` + ColumnNames fwtypes.SetValueOf[types.String] `tfsdk:"column_names"` + ColumnWildcard fwtypes.ListNestedObjectValueOf[columnWildcardData] `tfsdk:"column_wildcard"` + DatabaseName types.String `tfsdk:"database_name"` + Name types.String `tfsdk:"name"` +} diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 04fb5618517a..552788346111 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -55,9 +55,9 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, // ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, }, }, @@ -202,109 +202,109 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { } func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformation.ListLakeFormationOptInsOutput) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[name] - if !ok { - return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("not found")) - } - - if rs.Primary.ID == "" { - return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("not set")) - } - - conn := acctest.Provider.Meta().(*conns.AWSClient).LakeFormationClient(ctx) - - // Extract principal from state - principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] - if principalID == "" { - return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("principal identifier not set")) - } - - // Create input with resource based on what's in state - in := &lakeformation.ListLakeFormationOptInsInput{} - var resource *types.Resource - - // Check each possible resource type - if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { - resource = &types.Resource{ - Catalog: &types.CatalogResource{Id: aws.String(v)}, - } - } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { - resource = &types.Resource{ - Database: &types.DatabaseResource{ - Name: aws.String(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.database.0.catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { - resource = &types.Resource{ - DataCellsFilter: &types.DataCellsFilterResource{ - Name: aws.String(v), - // DatabaseName: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.database_name"]), - // TableCatalogId: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.table_catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { - resource = &types.Resource{ - DataLocation: &types.DataLocationResource{ - ResourceArn: aws.String(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.data_location.0.catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { - resource = &types.Resource{ - LFTag: &types.LFTagKeyResource{ + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).LakeFormationClient(ctx) + + // Extract principal from state + principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] + if principalID == "" { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("principal identifier not set")) + } + + // Create input with resource based on what's in state + in := &lakeformation.ListLakeFormationOptInsInput{} + var resource *types.Resource + + // Check each possible resource type + if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { + resource = &types.Resource{ + Catalog: &types.CatalogResource{Id: aws.String(v)}, + } + } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { + resource = &types.Resource{ + Database: &types.DatabaseResource{ + Name: aws.String(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.database.0.catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { + resource = &types.Resource{ + DataCellsFilter: &types.DataCellsFilterResource{ + Name: aws.String(v), + // DatabaseName: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.database_name"]), + // TableCatalogId: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.table_catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { + resource = &types.Resource{ + DataLocation: &types.DataLocationResource{ + ResourceArn: aws.String(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.data_location.0.catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { + resource = &types.Resource{ + LFTag: &types.LFTagKeyResource{ TagKey: aws.String(v), }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { - resource = &types.Resource{ - LFTagExpression: &types.LFTagExpressionResource{ - Name: aws.String(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_expression.0.catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { - resource = &types.Resource{ - LFTagPolicy: &types.LFTagPolicyResource{ - ResourceType: types.ResourceType(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.catalog_id"]), - // ExpressionName: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.expression_name"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { - resource = &types.Resource{ - Table: &types.TableResource{ - // Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { - resource = &types.Resource{ - TableWithColumns: &types.TableWithColumnsResource{ - Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), - }, - } - } - - if resource == nil { - return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("no valid resource found in state")) - } - - in.Resource = resource - - out, err := tflakeformation.FindOptInByID(ctx, conn, principalID, resource) - if err != nil { - return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, principalID, err) - } - - *optin = lakeformation.ListLakeFormationOptInsOutput{ - LakeFormationOptInsInfoList: []types.LakeFormationOptInsInfo{*out}, - } - - return nil - } + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { + resource = &types.Resource{ + LFTagExpression: &types.LFTagExpressionResource{ + Name: aws.String(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_expression.0.catalog_id"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { + resource = &types.Resource{ + LFTagPolicy: &types.LFTagPolicyResource{ + ResourceType: types.ResourceType(v), + // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.catalog_id"]), + // ExpressionName: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.expression_name"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { + resource = &types.Resource{ + Table: &types.TableResource{ + // Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), + }, + } + } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { + resource = &types.Resource{ + TableWithColumns: &types.TableWithColumnsResource{ + Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), + }, + } + } + + if resource == nil { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("no valid resource found in state")) + } + + in.Resource = resource + + out, err := tflakeformation.FindOptInByID(ctx, conn, principalID, resource) + if err != nil { + return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, principalID, err) + } + + *optin = lakeformation.ListLakeFormationOptInsOutput{ + LakeFormationOptInsInfoList: []types.LakeFormationOptInsInfo{*out}, + } + + return nil + } } func testAccOptInConfig_basic(rName string) string { From 20d67dd9bd5ec9509480c5de160134f024d5c5f9 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Fri, 28 Feb 2025 10:50:27 -0800 Subject: [PATCH 06/17] Comment cleanup --- internal/service/lakeformation/opt_in.go | 37 ------------------------ 1 file changed, 37 deletions(-) diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go index 7d89e418bee8..d58cf2556bed 100644 --- a/internal/service/lakeformation/opt_in.go +++ b/internal/service/lakeformation/opt_in.go @@ -43,10 +43,6 @@ import ( func newResourceOptIn(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceOptIn{} - // r.SetDefaultCreateTimeout(30 * time.Minute) - // r.SetDefaultUpdateTimeout(30 * time.Minute) - // r.SetDefaultDeleteTimeout(30 * time.Minute) - return r, nil } @@ -373,18 +369,6 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, return } - // r := newOptInResourcer(resourceData, &diags) - // if diags.HasError() { - // resp.Diagnostics.Append(diags...) - // return - // } - - // resource := r.expandOptInResource(ctx, &diags) - // if diags.HasError() { - // resp.Diagnostics.Append(diags...) - // return - // } - in := lakeformation.CreateLakeFormationOptInInput{} resp.Diagnostics.Append(fwflex.Expand(ctx, plan, &in)...) @@ -432,7 +416,6 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, return } - // TIP: -- 7. Save the request plan to response state resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) } @@ -485,7 +468,6 @@ func (r *resourceOptIn) Read(ctx context.Context, req resource.ReadRequest, resp } if out.LastUpdatedBy != nil { - // state.LastUpdatedBy = types.StringValue(*out.LastUpdatedBy) state.LastUpdatedBy = fwflex.StringToFramework(ctx, out.LastUpdatedBy) } @@ -529,15 +511,6 @@ func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, return } - // out, err := findOptInByID(ctx, conn, principalData.DataLakePrincipalIdentifier.ValueString(), in.Resource) - // if err != nil { - // resp.Diagnostics.AddError( - // create.ProblemStandardMessage(names.LakeFormation, create.ErrActionSetting, ResNameOptIn, principalData.DataLakePrincipalIdentifier.String(), err), - // err.Error(), - // ) - // return - // } - if _, err := conn.DeleteLakeFormationOptIn(ctx, in); err != nil { if errs.IsA[*awstypes.EntityNotFoundException](err) { return @@ -548,16 +521,6 @@ func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, ) return } - - // deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - // _, err = waitOptInDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) - // if err != nil { - // resp.Diagnostics.AddError( - // create.ProblemStandardMessage(names.LakeFormation, create.ErrActionWaitingForDeletion, ResNameOptIn, state.ID.String(), err), - // err.Error(), - // ) - // return - // } } func (r *resourceOptIn) ConfigValidators(_ context.Context) []resource.ConfigValidator { From bfa0525ed157c5a0f28b11c4a9a5d29f9e8297a0 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Fri, 28 Feb 2025 12:58:38 -0800 Subject: [PATCH 07/17] Various Linter fixes; Change import func --- internal/service/lakeformation/opt_in.go | 38 +++++--- internal/service/lakeformation/opt_in_test.go | 94 ++++++++++--------- .../docs/r/lakeformation_opt_in.html.markdown | 4 +- 3 files changed, 76 insertions(+), 60 deletions(-) diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go index d58cf2556bed..e9624ca454a2 100644 --- a/internal/service/lakeformation/opt_in.go +++ b/internal/service/lakeformation/opt_in.go @@ -72,16 +72,16 @@ func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, CustomType: fwtypes.NewListNestedObjectTypeOf[DataCellsFilter](ctx), NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "database_name": schema.StringAttribute{ + names.AttrDatabaseName: schema.StringAttribute{ Optional: true, }, - "name": schema.StringAttribute{ + names.AttrName: schema.StringAttribute{ Optional: true, }, "table_catalog_id": schema.StringAttribute{ Optional: true, }, - "table_name": schema.StringAttribute{ + names.AttrTableName: schema.StringAttribute{ Optional: true, }, }, @@ -95,7 +95,7 @@ func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, names.AttrResourceARN: schema.StringAttribute{ Required: true, }, - "catalog_id": schema.StringAttribute{ + names.AttrCatalogID: schema.StringAttribute{ Optional: true, Computed: true, }, @@ -177,12 +177,12 @@ func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, CustomType: fwtypes.NewListNestedObjectTypeOf[LFTagPolicy](ctx), NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "resource_type": schema.StringAttribute{ + names.AttrResourceType: schema.StringAttribute{ Required: true, CustomType: fwtypes.StringEnumType[awstypes.ResourceType](), }, names.AttrCatalogID: catalogIDSchemaOptionalComputed(), - "expression": schema.ListAttribute{ + names.AttrExpression: schema.ListAttribute{ CustomType: fwtypes.ListOfStringType, ElementType: types.StringType, Optional: true, @@ -317,17 +317,17 @@ func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, }, }, Blocks: map[string]schema.Block{ - "condition": schema.ListNestedBlock{ + names.AttrCondition: schema.ListNestedBlock{ CustomType: fwtypes.NewListNestedObjectTypeOf[Condition](ctx), NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "expression": schema.StringAttribute{ + names.AttrExpression: schema.StringAttribute{ Computed: true, }, }, }, }, - "principal": schema.ListNestedBlock{ + names.AttrPrincipal: schema.ListNestedBlock{ CustomType: fwtypes.NewListNestedObjectTypeOf[DataLakePrincipal](ctx), Validators: []validator.List{ listvalidator.SizeAtLeast(1), @@ -345,7 +345,7 @@ func (r *resourceOptIn) Schema(ctx context.Context, req resource.SchemaRequest, NestedObject: schema.NestedBlockObject{ Blocks: map[string]schema.Block{ "catalog": catalogLNB, - "database": databaseLNB, + names.AttrDatabase: databaseLNB, "data_cells_filter": dataCellsFilterLNB, "data_location": dataLocationLNB, "lf_tag": lfTagLNB, @@ -503,7 +503,7 @@ func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, } in.Principal = &awstypes.DataLakePrincipal{ - DataLakePrincipalIdentifier: aws.String(principalData.DataLakePrincipalIdentifier.ValueString()), + DataLakePrincipalIdentifier: principalData.DataLakePrincipalIdentifier.ValueStringPointer(), } in.Resource = optin.expandOptInResource(ctx, &resp.Diagnostics) @@ -529,7 +529,7 @@ func (r *resourceOptIn) ConfigValidators(_ context.Context) []resource.ConfigVal path.MatchRoot("resource_data").AtListIndex(0).AtName("catalog"), path.MatchRoot("resource_data").AtListIndex(0).AtName("data_cells_filter"), path.MatchRoot("resource_data").AtListIndex(0).AtName("data_location"), - path.MatchRoot("resource_data").AtListIndex(0).AtName("database"), + path.MatchRoot("resource_data").AtListIndex(0).AtName(names.AttrDatabase), path.MatchRoot("resource_data").AtListIndex(0).AtName("lf_tag"), path.MatchRoot("resource_data").AtListIndex(0).AtName("lf_tag_expression"), path.MatchRoot("resource_data").AtListIndex(0).AtName("lf_tag_policy"), @@ -540,7 +540,18 @@ func (r *resourceOptIn) ConfigValidators(_ context.Context) []resource.ConfigVal } func (r *resourceOptIn) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + principalID := req.ID + + var data resourceOptInData + principal, diags := fwtypes.NewListNestedObjectValueOfPtr(ctx, &DataLakePrincipal{ + DataLakePrincipalIdentifier: types.StringValue(principalID), + }) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + data.Principal = principal + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func findOptIns(ctx context.Context, conn *lakeformation.Client, input *lakeformation.ListLakeFormationOptInsInput, filter tfslices.Predicate[*awstypes.LakeFormationOptInsInfo]) ([]awstypes.LakeFormationOptInsInfo, error) { @@ -571,7 +582,6 @@ func findOptIns(ctx context.Context, conn *lakeformation.Client, input *lakeform } func findOptInByID(ctx context.Context, conn *lakeformation.Client, id string, resource *awstypes.Resource) (*awstypes.LakeFormationOptInsInfo, error) { - in := &lakeformation.ListLakeFormationOptInsInput{} in.Resource = resource diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 552788346111..8d0abb76a343 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -11,7 +11,6 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lakeformation" - "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" awstypes "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -40,7 +39,7 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) - acctest.PreCheckPartitionHasService(t, names.LakeFormationServiceID) + acctest.PreCheckPartitionHasService(t, names.LakeFormation) }, ErrorCheck: acctest.ErrorCheck(t, names.LakeFormationServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -58,6 +57,7 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, + ImportStateIdFunc: testAccOptInImportStateIDFunc(resourceName), // ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, }, }, @@ -87,12 +87,6 @@ func TestAccLakeFormationOptIn_disappears(t *testing.T) { Config: testAccOptInConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckOptInExists(ctx, resourceName, &optin), - // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, - // but expects a new resource factory function as the third argument. To expose this - // private function to the testing package, you may need to add a line like the following - // to exports_test.go: - // - // var ResourceOptIn = newResourceOptIn acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflakeformation.ResourceOptIn, resourceName), ), ExpectNonEmptyPlan: true, @@ -110,13 +104,11 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { continue } - // Extract principal from state principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] if principalID == "" { return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, errors.New("principal identifier not found in state")) } - // Create resource based on what's in state input := &lakeformation.ListLakeFormationOptInsInput{ Resource: &awstypes.Resource{}, Principal: &awstypes.DataLakePrincipal{ @@ -124,7 +116,6 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { }, } - // Check each possible resource type if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { input.Resource = &awstypes.Resource{ Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, @@ -214,74 +205,71 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat conn := acctest.Provider.Meta().(*conns.AWSClient).LakeFormationClient(ctx) - // Extract principal from state principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] if principalID == "" { return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("principal identifier not set")) } - // Create input with resource based on what's in state in := &lakeformation.ListLakeFormationOptInsInput{} - var resource *types.Resource + var resource *awstypes.Resource - // Check each possible resource type if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { - resource = &types.Resource{ - Catalog: &types.CatalogResource{Id: aws.String(v)}, + resource = &awstypes.Resource{ + Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, } } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { - resource = &types.Resource{ - Database: &types.DatabaseResource{ + resource = &awstypes.Resource{ + Database: &awstypes.DatabaseResource{ Name: aws.String(v), // CatalogId: aws.String(rs.Primary.Attributes["resource.0.database.0.catalog_id"]), }, } } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { - resource = &types.Resource{ - DataCellsFilter: &types.DataCellsFilterResource{ + resource = &awstypes.Resource{ + DataCellsFilter: &awstypes.DataCellsFilterResource{ Name: aws.String(v), // DatabaseName: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.database_name"]), // TableCatalogId: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.table_catalog_id"]), }, } } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { - resource = &types.Resource{ - DataLocation: &types.DataLocationResource{ + resource = &awstypes.Resource{ + DataLocation: &awstypes.DataLocationResource{ ResourceArn: aws.String(v), // CatalogId: aws.String(rs.Primary.Attributes["resource.0.data_location.0.catalog_id"]), }, } } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { - resource = &types.Resource{ - LFTag: &types.LFTagKeyResource{ + resource = &awstypes.Resource{ + LFTag: &awstypes.LFTagKeyResource{ TagKey: aws.String(v), }, } } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { - resource = &types.Resource{ - LFTagExpression: &types.LFTagExpressionResource{ + resource = &awstypes.Resource{ + LFTagExpression: &awstypes.LFTagExpressionResource{ Name: aws.String(v), // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_expression.0.catalog_id"]), }, } } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { - resource = &types.Resource{ - LFTagPolicy: &types.LFTagPolicyResource{ - ResourceType: types.ResourceType(v), + resource = &awstypes.Resource{ + LFTagPolicy: &awstypes.LFTagPolicyResource{ + ResourceType: awstypes.ResourceType(v), // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.catalog_id"]), // ExpressionName: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.expression_name"]), }, } } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { - resource = &types.Resource{ - Table: &types.TableResource{ + resource = &awstypes.Resource{ + Table: &awstypes.TableResource{ // Name: aws.String(v), DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), }, } } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { - resource = &types.Resource{ - TableWithColumns: &types.TableWithColumnsResource{ + resource = &awstypes.Resource{ + TableWithColumns: &awstypes.TableWithColumnsResource{ Name: aws.String(v), DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), }, @@ -300,13 +288,24 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat } *optin = lakeformation.ListLakeFormationOptInsOutput{ - LakeFormationOptInsInfoList: []types.LakeFormationOptInsInfo{*out}, + LakeFormationOptInsInfoList: []awstypes.LakeFormationOptInsInfo{*out}, } return nil } } +func testAccOptInImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.Attributes["principal.0.data_lake_principal_identifier"], nil + } +} + func testAccOptInConfig_basic(rName string) string { return fmt.Sprintf(` data "aws_partition" "current" {} @@ -316,13 +315,22 @@ resource "aws_iam_role" "test" { path = "/" assume_role_policy = jsonencode({ - Statement = [{ - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "glue.${data.aws_partition.current.dns_suffix}" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "glue.${data.aws_partition.current.dns_suffix}" + } + }, + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "lakeformation.amazonaws.com" + } } - }] + ] Version = "2012-10-17" }) } @@ -359,9 +367,9 @@ resource "aws_lakeformation_opt_in" "test" { data_lake_principal_identifier = aws_iam_role.test.arn } - resource { + resource_data { database { - name = aws_glue_catalog_database.test.name + name = aws_glue_catalog_database.test.name catalog_id = data.aws_caller_identity.current.account_id } } diff --git a/website/docs/r/lakeformation_opt_in.html.markdown b/website/docs/r/lakeformation_opt_in.html.markdown index 6ba003f08036..5782c208b727 100644 --- a/website/docs/r/lakeformation_opt_in.html.markdown +++ b/website/docs/r/lakeformation_opt_in.html.markdown @@ -26,7 +26,6 @@ The following arguments are required: * `principal` - (Required) Lake Formation principal. Supported principals are IAM users or IAM roles. See [Principal](#principal) for more details. * `reosurce` - (Required) Structure for the resource. See [Resource](#resource) for more details. - ## Attribute Reference This resource exports the following attributes in addition to the arguments above: @@ -37,8 +36,7 @@ This resource exports the following attributes in addition to the arguments abov ### Principal -The following arguments are required: -* `data_lake_principal` - (Required) Identifier for the Lake Formation principal. +* `data_lake_principal` - Identifier for the Lake Formation principal. ### Resource From 220d9947d0481819ccf53b1327fed611096008c3 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Fri, 28 Feb 2025 16:57:11 -0800 Subject: [PATCH 08/17] Linter fixes --- internal/service/lakeformation/opt_in.go | 30 ++++--------------- internal/service/lakeformation/opt_in_test.go | 10 +++---- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go index e9624ca454a2..a4c91c2a13ba 100644 --- a/internal/service/lakeformation/opt_in.go +++ b/internal/service/lakeformation/opt_in.go @@ -582,9 +582,12 @@ func findOptIns(ctx context.Context, conn *lakeformation.Client, input *lakeform } func findOptInByID(ctx context.Context, conn *lakeformation.Client, id string, resource *awstypes.Resource) (*awstypes.LakeFormationOptInsInfo, error) { - in := &lakeformation.ListLakeFormationOptInsInput{} - - in.Resource = resource + in := &lakeformation.ListLakeFormationOptInsInput{ + Principal: &awstypes.DataLakePrincipal{ + DataLakePrincipalIdentifier: aws.String(id), + }, + Resource: resource, + } return findOptIn(ctx, conn, in, tfslices.Predicate[*awstypes.LakeFormationOptInsInfo](func(v *awstypes.LakeFormationOptInsInfo) bool { return aws.ToString(v.Principal.DataLakePrincipalIdentifier) == id @@ -604,7 +607,6 @@ func findOptIn(ctx context.Context, conn *lakeformation.Client, input *lakeforma type optInResourcer interface { expandOptInResource(context.Context, *diag.Diagnostics) *awstypes.Resource findOptIn(context.Context, *lakeformation.ListLakeFormationOptInsOutput, *diag.Diagnostics) fwtypes.ListNestedObjectValueOf[ResourceData] - // findOptInByAttr(context.Context, *lakeformation.Client, string, string) (*awstypes.LakeFormationOptInsInfo, error) } type catalogResource struct { @@ -670,7 +672,6 @@ func newOptInResourcer(data *ResourceData, diags *diag.Diagnostics) optInResourc } } -// //////////////////////// CATALOG ////////////////////////// func (d *catalogResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource catalogptr, err := d.data.Catalog.ToPtr(ctx) @@ -712,10 +713,6 @@ func (d *catalogResource) findOptIn(ctx context.Context, input *lakeformation.Li return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -//////////////////////////////////////////////////////////// - -////////////////////////// DATABASE ////////////////////////// - func (d *dbResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource dbptr, err := d.data.Database.ToPtr(ctx) @@ -759,9 +756,6 @@ func (d *dbResource) findOptIn(ctx context.Context, input *lakeformation.ListLak return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -////////////////////////////////////////////////////////////// - -// //////////////////DATA_CELLS_FILTER////////////////////////// func (d *dcfResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource dcfptr, err := d.data.DataCellsFilter.ToPtr(ctx) @@ -808,9 +802,6 @@ func (d *dcfResource) findOptIn(ctx context.Context, input *lakeformation.ListLa return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -///////////////////////////////////////////////////////////////////////////// - -// /////////////////////DATA_LOCATION//////////////////////////// func (d *dlResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource dlptr, err := d.data.DataLocation.ToPtr(ctx) @@ -853,9 +844,6 @@ func (d *dlResource) findOptIn(ctx context.Context, input *lakeformation.ListLak return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -///////////////////////////////////////////////////////////////////////////////////////////// - -// //////////////////////// LFTAG //////////////////////////////////////////////////// func (d *lftagResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource lftagptr, err := d.data.LFTag.ToPtr(ctx) @@ -897,7 +885,6 @@ func (d *lftagResource) findOptIn(ctx context.Context, input *lakeformation.List return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -// ///////////////////////LFTAG EXPRESSION////////////////////// func (d *lfteResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource lfteptr, err := d.data.LFTagExpression.ToPtr(ctx) @@ -940,7 +927,6 @@ func (d *lfteResource) findOptIn(ctx context.Context, input *lakeformation.ListL return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -// /////////////////////LFTAG POLICY //////////////////////////////////// func (d *lftpResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource lftptr, err := d.data.LFTagPolicy.ToPtr(ctx) @@ -984,7 +970,6 @@ func (d *lftpResource) findOptIn(ctx context.Context, input *lakeformation.ListL return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -// ///////////////////////TABLE//////////////////////////////// func (d *tbResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource tableptr, err := d.data.Table.ToPtr(ctx) @@ -1026,7 +1011,6 @@ func (d *tbResource) findOptIn(ctx context.Context, input *lakeformation.ListLak return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -// ////////////////////////////////////////////////////////////////////////////////////////////// func (d *tbcResource) expandOptInResource(ctx context.Context, diags *diag.Diagnostics) *awstypes.Resource { var r awstypes.Resource tbcptr, err := d.data.TableWithColumns.ToPtr(ctx) @@ -1068,8 +1052,6 @@ func (d *tbcResource) findOptIn(ctx context.Context, input *lakeformation.ListLa return fwtypes.NewListNestedObjectValueOfNull[ResourceData](ctx) } -//////////////////////////////////////////////////////////////////////////////////////////////// - type resourceOptInData struct { Principal fwtypes.ListNestedObjectValueOf[DataLakePrincipal] `tfsdk:"principal"` Resource fwtypes.ListNestedObjectValueOf[ResourceData] `tfsdk:"resource_data"` diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 8d0abb76a343..6cadfcb2c720 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -77,7 +77,7 @@ func TestAccLakeFormationOptIn_disappears(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) - acctest.PreCheckPartitionHasService(t, names.LakeFormationServiceID) + acctest.PreCheckPartitionHasService(t, names.LakeFormation) }, ErrorCheck: acctest.ErrorCheck(t, names.LakeFormationServiceID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, @@ -317,15 +317,15 @@ resource "aws_iam_role" "test" { assume_role_policy = jsonencode({ Statement = [ { - Action = "sts:AssumeRole" - Effect = "Allow" + Action = "sts:AssumeRole" + Effect = "Allow" Principal = { Service = "glue.${data.aws_partition.current.dns_suffix}" } }, { - Action = "sts:AssumeRole" - Effect = "Allow" + Action = "sts:AssumeRole" + Effect = "Allow" Principal = { Service = "lakeformation.amazonaws.com" } From 1570f2ee0406e209d6e969294f77cd7450256922 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Sun, 2 Mar 2025 22:33:30 -0800 Subject: [PATCH 09/17] Cleanup; more permissions --- internal/service/lakeformation/opt_in_test.go | 90 ++++++++++++------- 1 file changed, 57 insertions(+), 33 deletions(-) diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 6cadfcb2c720..d56b6d0f9611 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -12,7 +12,6 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lakeformation" awstypes "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" - sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" @@ -32,7 +31,6 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { } var optin lakeformation.ListLakeFormationOptInsOutput - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lakeformation_opt_in.test" databaseName := "aws_glue_catalog_database.test" @@ -46,7 +44,7 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { CheckDestroy: testAccCheckOptInDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccOptInConfig_basic(rName), + Config: testingOptIn_basic, Check: resource.ComposeTestCheckFunc( testAccCheckOptInExists(ctx, resourceName, &optin), resource.TestCheckResourceAttr(resourceName, "principals.#", "1"), @@ -58,7 +56,6 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { ImportState: true, ImportStateVerify: true, ImportStateIdFunc: testAccOptInImportStateIDFunc(resourceName), - // ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, }, }, }) @@ -71,7 +68,6 @@ func TestAccLakeFormationOptIn_disappears(t *testing.T) { } var optin lakeformation.ListLakeFormationOptInsOutput - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lakeformation_opt_in.test" resource.ParallelTest(t, resource.TestCase{ @@ -84,7 +80,7 @@ func TestAccLakeFormationOptIn_disappears(t *testing.T) { CheckDestroy: testAccCheckOptInDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccOptInConfig_basic(rName), + Config: testingOptIn_basic, Check: resource.ComposeTestCheckFunc( testAccCheckOptInExists(ctx, resourceName, &optin), acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflakeformation.ResourceOptIn, resourceName), @@ -306,26 +302,28 @@ func testAccOptInImportStateIDFunc(resourceName string) resource.ImportStateIdFu } } -func testAccOptInConfig_basic(rName string) string { - return fmt.Sprintf(` +const testingOptIn_basic = ` + data "aws_partition" "current" {} +data "aws_caller_identity" "current" {} + resource "aws_iam_role" "test" { - name = %[1]q + name = "testinglocal" path = "/" assume_role_policy = jsonencode({ Statement = [ { - Action = "sts:AssumeRole" - Effect = "Allow" + Action = "sts:AssumeRole" + Effect = "Allow" Principal = { Service = "glue.${data.aws_partition.current.dns_suffix}" } }, { - Action = "sts:AssumeRole" - Effect = "Allow" + Action = "sts:AssumeRole" + Effect = "Allow" Principal = { Service = "lakeformation.amazonaws.com" } @@ -335,31 +333,57 @@ resource "aws_iam_role" "test" { }) } -resource "aws_glue_catalog_database" "test" { - name = %[1]q -} - -data "aws_caller_identity" "current" {} +resource "aws_iam_policy" "lakeformation_admin_permissions" { + name = "LakeFormationAdminPermissions" + description = "Admin permissions for managing Lake Formation and Glue Catalog" -data "aws_iam_session_context" "current" { - arn = data.aws_caller_identity.current.arn + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "lakeformation:GrantPermissions", + "lakeformation:RevokePermissions", + "lakeformation:GetDataLakeSettings", + "lakeformation:ListPermissions", + "lakeformation:PutDataLakeSettings", + "lakeformation:ListLakeFormationOptIns", + "lakeformation:UpdateTable", + "lakeformation:CreateDatabase", + "lakeformation:DeleteDatabase", + "lakeformation:CreateTable", + "lakeformation:DeleteTable" + ] + Resource = "*" + }, + { + Effect = "Allow" + Action = [ + "glue:CreateDatabase", + "glue:UpdateTable", + "glue:DeleteTable", + "glue:CreateTable", + "glue:DeleteDatabase" + ] + Resource = "*" + } + ] + }) } -resource "aws_lakeformation_data_lake_settings" "test" { - admins = [data.aws_iam_session_context.current.issuer_arn] +resource "aws_iam_policy_attachment" "lakeformation_admin_attachment" { + name = "lakeformation-attach-policy" + policy_arn = aws_iam_policy.lakeformation_admin_permissions.arn + roles = [aws_iam_role.test.name] } -resource "aws_lakeformation_permissions" "test" { - permissions = ["ALTER", "CREATE_TABLE", "DROP"] - permissions_with_grant_option = ["CREATE_TABLE"] - principal = aws_iam_role.test.arn - - database { - name = aws_glue_catalog_database.test.name - } +resource "aws_glue_catalog_database" "test" { + name = "testinglocal" +} - # for consistency, ensure that admins are setup before testing - depends_on = [aws_lakeformation_data_lake_settings.test] +data "aws_iam_session_context" "current" { + arn = data.aws_caller_identity.current.arn } resource "aws_lakeformation_opt_in" "test" { @@ -373,5 +397,5 @@ resource "aws_lakeformation_opt_in" "test" { catalog_id = data.aws_caller_identity.current.account_id } } -}`, rName) } + ` From 282d7c8f3c85129b5475373b34025a8d51ad0a16 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 02:46:09 -0800 Subject: [PATCH 10/17] CleanUp --- internal/service/lakeformation/opt_in.go | 40 +-- internal/service/lakeformation/opt_in_test.go | 243 +++++++++--------- 2 files changed, 144 insertions(+), 139 deletions(-) diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go index a4c91c2a13ba..e8fdf85dfed8 100644 --- a/internal/service/lakeformation/opt_in.go +++ b/internal/service/lakeformation/opt_in.go @@ -6,6 +6,8 @@ package lakeformation import ( "context" "errors" + "fmt" + "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" @@ -382,7 +384,20 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, return } - output, err := conn.CreateLakeFormationOptIn(ctx, &in) + var output *lakeformation.CreateLakeFormationOptInOutput + err := retry.RetryContext(ctx, 2*IAMPropagationTimeout, func() *retry.RetryError { + var err error + output, err = conn.CreateLakeFormationOptIn(ctx, &in) + if err != nil { + if errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "Insufficient Lake Formation permission(s) on Catalog") { + time.Sleep(5 * time.Second) + return retry.RetryableError(err) + } + return retry.NonRetryableError(fmt.Errorf("creating Lake Formation opt-in: %w", err)) + } + return nil + }) + if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, principal.DataLakePrincipalIdentifier.ValueString(), err), @@ -491,6 +506,14 @@ func (r *resourceOptIn) Delete(ctx context.Context, req resource.DeleteRequest, return } + if optinResource == nil { + resp.Diagnostics.AddWarning( + create.ProblemStandardMessage(names.LakeFormation, create.ErrActionDeleting, ResNameOptIn, "unknown", errors.New("resource data is nil")), + "resource data is nil", + ) + return + } + optin := newOptInResourcer(optinResource, &resp.Diagnostics) if resp.Diagnostics.HasError() { return @@ -539,21 +562,6 @@ func (r *resourceOptIn) ConfigValidators(_ context.Context) []resource.ConfigVal } } -func (r *resourceOptIn) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - principalID := req.ID - - var data resourceOptInData - principal, diags := fwtypes.NewListNestedObjectValueOfPtr(ctx, &DataLakePrincipal{ - DataLakePrincipalIdentifier: types.StringValue(principalID), - }) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - data.Principal = principal - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} - func findOptIns(ctx context.Context, conn *lakeformation.Client, input *lakeformation.ListLakeFormationOptInsInput, filter tfslices.Predicate[*awstypes.LakeFormationOptInsInfo]) ([]awstypes.LakeFormationOptInsInfo, error) { var output []awstypes.LakeFormationOptInsInfo diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index d56b6d0f9611..5b59b581a8c6 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -12,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lakeformation" awstypes "github.com/aws/aws-sdk-go-v2/service/lakeformation/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" @@ -32,6 +33,8 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { var optin lakeformation.ListLakeFormationOptInsOutput resourceName := "aws_lakeformation_opt_in.test" + rName := sdkacctest.RandomWithPrefix("tf-acc-test") + roleName := "aws_iam_role.test" databaseName := "aws_glue_catalog_database.test" resource.ParallelTest(t, resource.TestCase{ @@ -44,19 +47,16 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { CheckDestroy: testAccCheckOptInDestroy(ctx), Steps: []resource.TestStep{ { - Config: testingOptIn_basic, + Config: testAccOptInConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckOptInExists(ctx, resourceName, &optin), - resource.TestCheckResourceAttr(resourceName, "principals.#", "1"), - resource.TestCheckResourceAttr(resourceName, "resource.0.principal.data_lake_principal_identifier", databaseName), + resource.TestCheckResourceAttr(resourceName, "principal.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "principal.0.data_lake_principal_identifier", roleName, "arn"), + resource.TestCheckResourceAttr(resourceName, "resource_data.#", "1"), + resource.TestCheckResourceAttr(resourceName, "resource_data.0.database.#", "1"), + resource.TestCheckResourceAttrPair(resourceName, "resource_data.0.database.0.name", databaseName, "name"), ), }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateIdFunc: testAccOptInImportStateIDFunc(resourceName), - }, }, }) } @@ -69,6 +69,7 @@ func TestAccLakeFormationOptIn_disappears(t *testing.T) { var optin lakeformation.ListLakeFormationOptInsOutput resourceName := "aws_lakeformation_opt_in.test" + rName := sdkacctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { @@ -80,12 +81,12 @@ func TestAccLakeFormationOptIn_disappears(t *testing.T) { CheckDestroy: testAccCheckOptInDestroy(ctx), Steps: []resource.TestStep{ { - Config: testingOptIn_basic, + Config: testAccOptInConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckOptInExists(ctx, resourceName, &optin), acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflakeformation.ResourceOptIn, resourceName), ), - ExpectNonEmptyPlan: true, + ExpectNonEmptyPlan: false, }, }, }) @@ -101,73 +102,69 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { } principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] - if principalID == "" { - return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, errors.New("principal identifier not found in state")) - } - input := &lakeformation.ListLakeFormationOptInsInput{ - Resource: &awstypes.Resource{}, + in := &lakeformation.ListLakeFormationOptInsInput{ Principal: &awstypes.DataLakePrincipal{ - DataLakePrincipalIdentifier: aws.String(principalID), + DataLakePrincipalIdentifier: &principalID, }, + Resource: &awstypes.Resource{}, } - if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + if v, ok := rs.Primary.Attributes["resource_data.0.catalog.0.id"]; ok && v != "" { + in.Resource = &awstypes.Resource{ Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, } - } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.database.0.name"]; ok && v != "" { + in.Resource = &awstypes.Resource{ Database: &awstypes.DatabaseResource{ Name: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]; ok && v != "" { + in.Resource = &awstypes.Resource{ DataCellsFilter: &awstypes.DataCellsFilterResource{ Name: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]; ok && v != "" { + in.Resource = &awstypes.Resource{ DataLocation: &awstypes.DataLocationResource{ ResourceArn: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag.0.key"]; ok && v != "" { + in.Resource = &awstypes.Resource{ LFTag: &awstypes.LFTagKeyResource{ TagKey: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]; ok && v != "" { + in.Resource = &awstypes.Resource{ LFTagExpression: &awstypes.LFTagExpressionResource{ Name: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]; ok && v != "" { + in.Resource = &awstypes.Resource{ LFTagPolicy: &awstypes.LFTagPolicyResource{ ResourceType: awstypes.ResourceType(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.table.0.name"]; ok && v != "" { + in.Resource = &awstypes.Resource{ Table: &awstypes.TableResource{ - DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), + Name: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { - input.Resource = &awstypes.Resource{ + } else if v, ok := rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]; ok && v != "" { + in.Resource = &awstypes.Resource{ TableWithColumns: &awstypes.TableWithColumnsResource{ - Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), + Name: aws.String(v), }, } } - _, err := conn.ListLakeFormationOptIns(ctx, input) + _, err := conn.ListLakeFormationOptIns(ctx, in) if errs.IsA[*awstypes.EntityNotFoundException](err) { continue @@ -177,10 +174,14 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { continue } + // If the lake formation admin has been revoked, there will be access denied instead of entity not found + if errs.IsA[*awstypes.AccessDeniedException](err) { + continue + } + if err != nil { return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, err) } - return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, errors.New("not destroyed")) } @@ -209,65 +210,65 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat in := &lakeformation.ListLakeFormationOptInsInput{} var resource *awstypes.Resource - if v, ok := rs.Primary.Attributes["resource.0.catalog.0.id"]; ok && v != "" { + if v, ok := rs.Primary.Attributes["resource_data.0.catalog.0.id"]; ok && v != "" { resource = &awstypes.Resource{ Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, } - } else if v, ok := rs.Primary.Attributes["resource.0.database.0.name"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.database.0.name"]; ok && v != "" { resource = &awstypes.Resource{ Database: &awstypes.DatabaseResource{ - Name: aws.String(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.database.0.catalog_id"]), + Name: aws.String(v), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.data_cells_filter.0.name"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]; ok && v != "" { resource = &awstypes.Resource{ DataCellsFilter: &awstypes.DataCellsFilterResource{ - Name: aws.String(v), - // DatabaseName: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.database_name"]), - // TableCatalogId: aws.String(rs.Primary.Attributes["resource.0.data_cells_filter.0.table_catalog_id"]), + Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), + TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.data_location.0.resource_arn"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]; ok && v != "" { resource = &awstypes.Resource{ DataLocation: &awstypes.DataLocationResource{ ResourceArn: aws.String(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.data_location.0.catalog_id"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag.0.key"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag.0.key"]; ok && v != "" { resource = &awstypes.Resource{ LFTag: &awstypes.LFTagKeyResource{ TagKey: aws.String(v), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_expression.0.name"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]; ok && v != "" { resource = &awstypes.Resource{ LFTagExpression: &awstypes.LFTagExpressionResource{ - Name: aws.String(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_expression.0.catalog_id"]), + Name: aws.String(v), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.lf_tag_policy.0.resource_type"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]; ok && v != "" { resource = &awstypes.Resource{ LFTagPolicy: &awstypes.LFTagPolicyResource{ - ResourceType: awstypes.ResourceType(v), - // CatalogId: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.catalog_id"]), - // ExpressionName: aws.String(rs.Primary.Attributes["resource.0.lf_tag_policy.0.expression_name"]), + ResourceType: awstypes.ResourceType(v), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), + ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.table.0.name"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.table.0.name"]; ok && v != "" { resource = &awstypes.Resource{ Table: &awstypes.TableResource{ - // Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table.0.database_name"]), + Name: aws.String(v), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), }, } - } else if v, ok := rs.Primary.Attributes["resource.0.table_with_columns.0.name"]; ok && v != "" { + } else if v, ok := rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]; ok && v != "" { resource = &awstypes.Resource{ TableWithColumns: &awstypes.TableWithColumnsResource{ Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource.0.table_with_columns.0.database_name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), }, } } @@ -291,41 +292,35 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat } } -func testAccOptInImportStateIDFunc(resourceName string) resource.ImportStateIdFunc { - return func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources[resourceName] - if !ok { - return "", fmt.Errorf("Not found: %s", resourceName) - } - - return rs.Primary.Attributes["principal.0.data_lake_principal_identifier"], nil - } -} - -const testingOptIn_basic = ` - +func testAccOptInConfig_basic(rName string) string { + return fmt.Sprintf(` data "aws_partition" "current" {} data "aws_caller_identity" "current" {} +data "aws_iam_session_context" "current" { + arn = data.aws_caller_identity.current.arn +} + +resource "aws_s3_bucket" "test" { + bucket = %[1]q +} + resource "aws_iam_role" "test" { - name = "testinglocal" + name = %[1]q path = "/" assume_role_policy = jsonencode({ Statement = [ { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "glue.${data.aws_partition.current.dns_suffix}" - } - }, - { - Action = "sts:AssumeRole" - Effect = "Allow" + Action = "sts:AssumeRole" + Effect = "Allow" Principal = { - Service = "lakeformation.amazonaws.com" + Service = [ + "glue.${data.aws_partition.current.dns_suffix}", + "lakeformation.amazonaws.com", + "s3.amazonaws.com" + ] } } ] @@ -333,38 +328,30 @@ resource "aws_iam_role" "test" { }) } -resource "aws_iam_policy" "lakeformation_admin_permissions" { - name = "LakeFormationAdminPermissions" - description = "Admin permissions for managing Lake Formation and Glue Catalog" +resource "aws_iam_role_policy" "test" { + name = "test_policy" + role = aws_iam_role.test.id policy = jsonencode({ - Version = "2012-10-17" + Version = "2012-10-17" Statement = [ { - Effect = "Allow" - Action = [ - "lakeformation:GrantPermissions", - "lakeformation:RevokePermissions", - "lakeformation:GetDataLakeSettings", - "lakeformation:ListPermissions", - "lakeformation:PutDataLakeSettings", - "lakeformation:ListLakeFormationOptIns", - "lakeformation:UpdateTable", - "lakeformation:CreateDatabase", - "lakeformation:DeleteDatabase", - "lakeformation:CreateTable", - "lakeformation:DeleteTable" - ] - Resource = "*" - }, - { - Effect = "Allow" - Action = [ - "glue:CreateDatabase", - "glue:UpdateTable", - "glue:DeleteTable", - "glue:CreateTable", - "glue:DeleteDatabase" + Effect = "Allow" + Action = [ + "lakeformation:*", + "glue:*", + "s3:GetBucketLocation", + "s3:ListAllMyBuckets", + "s3:GetObjectVersion", + "s3:GetBucketAcl", + "s3:GetObject", + "s3:GetObjectACL", + "s3:PutObject", + "s3:PutObjectAcl", + "iam:ListUsers", + "iam:ListRoles", + "iam:GetRole", + "iam:GetRolePolicy" ] Resource = "*" } @@ -372,18 +359,27 @@ resource "aws_iam_policy" "lakeformation_admin_permissions" { }) } -resource "aws_iam_policy_attachment" "lakeformation_admin_attachment" { - name = "lakeformation-attach-policy" - policy_arn = aws_iam_policy.lakeformation_admin_permissions.arn - roles = [aws_iam_role.test.name] +resource "aws_lakeformation_data_lake_settings" "test" { + admins = [ + data.aws_iam_session_context.current.issuer_arn, + aws_iam_role.test.arn + ] + depends_on = [aws_iam_role_policy.test] + + lifecycle { + ignore_changes = [admins] + } } resource "aws_glue_catalog_database" "test" { - name = "testinglocal" + name = %[1]q + depends_on = [aws_lakeformation_data_lake_settings.test] } -data "aws_iam_session_context" "current" { - arn = data.aws_caller_identity.current.arn +resource "aws_lakeformation_resource" "test" { + arn = aws_s3_bucket.test.arn + role_arn = aws_iam_role.test.arn + depends_on = [aws_lakeformation_data_lake_settings.test] } resource "aws_lakeformation_opt_in" "test" { @@ -398,4 +394,5 @@ resource "aws_lakeformation_opt_in" "test" { } } } - ` +`, rName) +} From 3b5ba5fe54643237b7888756f15b004667849569 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 02:50:41 -0800 Subject: [PATCH 11/17] More cleanup --- internal/service/lakeformation/opt_in_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 5b59b581a8c6..ac565ce21c7a 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -319,7 +319,7 @@ resource "aws_iam_role" "test" { Service = [ "glue.${data.aws_partition.current.dns_suffix}", "lakeformation.amazonaws.com", - "s3.amazonaws.com" + "s3.amazonaws.com" ] } } @@ -333,7 +333,7 @@ resource "aws_iam_role_policy" "test" { role = aws_iam_role.test.id policy = jsonencode({ - Version = "2012-10-17" + Version = "2012-10-17" Statement = [ { Effect = "Allow" @@ -366,7 +366,7 @@ resource "aws_lakeformation_data_lake_settings" "test" { ] depends_on = [aws_iam_role_policy.test] - lifecycle { + lifecycle { ignore_changes = [admins] } } From d74ac035b5facfab4719b8299b836934e45dca18 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 02:53:27 -0800 Subject: [PATCH 12/17] Another linter fix --- internal/service/lakeformation/opt_in_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index ac565ce21c7a..5e85b5c5d9e3 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -372,7 +372,7 @@ resource "aws_lakeformation_data_lake_settings" "test" { } resource "aws_glue_catalog_database" "test" { - name = %[1]q + name = %[1]q depends_on = [aws_lakeformation_data_lake_settings.test] } From 3d2dabd069446f5d791de822c803813121596d6a Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 03:26:02 -0800 Subject: [PATCH 13/17] Yet more fixes --- internal/service/lakeformation/opt_in.go | 9 +++++---- internal/service/lakeformation/opt_in_test.go | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/service/lakeformation/opt_in.go b/internal/service/lakeformation/opt_in.go index e8fdf85dfed8..65ffbb619439 100644 --- a/internal/service/lakeformation/opt_in.go +++ b/internal/service/lakeformation/opt_in.go @@ -6,8 +6,6 @@ package lakeformation import ( "context" "errors" - "fmt" - "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" @@ -390,14 +388,17 @@ func (r *resourceOptIn) Create(ctx context.Context, req resource.CreateRequest, output, err = conn.CreateLakeFormationOptIn(ctx, &in) if err != nil { if errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "Insufficient Lake Formation permission(s) on Catalog") { - time.Sleep(5 * time.Second) return retry.RetryableError(err) } - return retry.NonRetryableError(fmt.Errorf("creating Lake Formation opt-in: %w", err)) + return retry.NonRetryableError(err) } return nil }) + if tfresource.TimedOut(err) { + output, err = conn.CreateLakeFormationOptIn(ctx, &in) + } + if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.LakeFormation, create.ErrActionCreating, ResNameOptIn, principal.DataLakePrincipalIdentifier.ValueString(), err), diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 5e85b5c5d9e3..547bdd7f2ba8 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -51,10 +51,10 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckOptInExists(ctx, resourceName, &optin), resource.TestCheckResourceAttr(resourceName, "principal.#", "1"), - resource.TestCheckResourceAttrPair(resourceName, "principal.0.data_lake_principal_identifier", roleName, "arn"), + resource.TestCheckResourceAttrPair(resourceName, "principal.0.data_lake_principal_identifier", roleName, names.AttrARN), resource.TestCheckResourceAttr(resourceName, "resource_data.#", "1"), resource.TestCheckResourceAttr(resourceName, "resource_data.0.database.#", "1"), - resource.TestCheckResourceAttrPair(resourceName, "resource_data.0.database.0.name", databaseName, "name"), + resource.TestCheckResourceAttrPair(resourceName, "resource_data.0.database.0.name", databaseName, names.AttrName), ), }, }, From 875007566a2581dd4840f4cb70641950fff1d430 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 05:04:15 -0800 Subject: [PATCH 14/17] Add changelog --- .changelog/41611.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/41611.txt diff --git a/.changelog/41611.txt b/.changelog/41611.txt new file mode 100644 index 000000000000..fef2c6b52e2f --- /dev/null +++ b/.changelog/41611.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_lakeformation_opt_in +``` From 49c67bb0cbab8179b120469b5b13dc4a74ad429c Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 11:30:20 -0800 Subject: [PATCH 15/17] Typo --- website/docs/r/lakeformation_opt_in.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/lakeformation_opt_in.html.markdown b/website/docs/r/lakeformation_opt_in.html.markdown index 5782c208b727..97bf9534dd6e 100644 --- a/website/docs/r/lakeformation_opt_in.html.markdown +++ b/website/docs/r/lakeformation_opt_in.html.markdown @@ -24,7 +24,7 @@ resource "aws_lakeformation_opt_in" "example" { The following arguments are required: * `principal` - (Required) Lake Formation principal. Supported principals are IAM users or IAM roles. See [Principal](#principal) for more details. -* `reosurce` - (Required) Structure for the resource. See [Resource](#resource) for more details. +* `resource_data` - (Required) Structure for the resource. See [Resource](#resource) for more details. ## Attribute Reference From ea8dcf147800b905510c8eada9a5465f985a154a Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Mon, 3 Mar 2025 15:49:54 -0800 Subject: [PATCH 16/17] Review Changes --- internal/service/lakeformation/opt_in_test.go | 305 +++++++++--------- 1 file changed, 157 insertions(+), 148 deletions(-) diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index 547bdd7f2ba8..d7253f278816 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -25,11 +25,6 @@ import ( func TestAccLakeFormationOptIn_basic(t *testing.T) { ctx := acctest.Context(t) - // TIP: This is a long-running test guard for tests that run longer than - // 300s (5 min) generally. - if testing.Short() { - t.Skip("skipping long-running test in short mode") - } var optin lakeformation.ListLakeFormationOptInsOutput resourceName := "aws_lakeformation_opt_in.test" @@ -63,9 +58,6 @@ func TestAccLakeFormationOptIn_basic(t *testing.T) { func TestAccLakeFormationOptIn_disappears(t *testing.T) { ctx := acctest.Context(t) - if testing.Short() { - t.Skip("skipping long-running test in short mode") - } var optin lakeformation.ListLakeFormationOptInsOutput resourceName := "aws_lakeformation_opt_in.test" @@ -102,84 +94,97 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { } principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] - - in := &lakeformation.ListLakeFormationOptInsInput{ + in := lakeformation.ListLakeFormationOptInsInput{ Principal: &awstypes.DataLakePrincipal{ - DataLakePrincipalIdentifier: &principalID, + DataLakePrincipalIdentifier: aws.String(principalID), }, Resource: &awstypes.Resource{}, } - if v, ok := rs.Primary.Attributes["resource_data.0.catalog.0.id"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.database.0.name"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - Database: &awstypes.DatabaseResource{ - Name: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - DataCellsFilter: &awstypes.DataCellsFilterResource{ - Name: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - DataLocation: &awstypes.DataLocationResource{ - ResourceArn: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag.0.key"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - LFTag: &awstypes.LFTagKeyResource{ - TagKey: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - LFTagExpression: &awstypes.LFTagExpressionResource{ - Name: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - LFTagPolicy: &awstypes.LFTagPolicyResource{ - ResourceType: awstypes.ResourceType(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.table.0.name"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - Table: &awstypes.TableResource{ - Name: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]; ok && v != "" { - in.Resource = &awstypes.Resource{ - TableWithColumns: &awstypes.TableWithColumnsResource{ - Name: aws.String(v), - }, - } - } - - _, err := conn.ListLakeFormationOptIns(ctx, in) - - if errs.IsA[*awstypes.EntityNotFoundException](err) { - continue - } + type resourceConstructor func(*terraform.ResourceState) *awstypes.Resource - if errs.IsAErrorMessageContains[*awstypes.InvalidInputException](err, "not found") { - continue + resourceConstructors := map[string]resourceConstructor{ + "resource_data.0.catalog.0.id": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{Catalog: &awstypes.CatalogResource{Id: aws.String(rs.Primary.Attributes["resource_data.0.catalog.0.id"])}} + }, + "resource_data.0.database.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + Database: &awstypes.DatabaseResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.database.0.name"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), + }, + } + }, + "resource_data.0.data_cells_filter.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + DataCellsFilter: &awstypes.DataCellsFilterResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), + TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), + }, + } + }, + "resource_data.0.data_location.0.resource_arn": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + DataLocation: &awstypes.DataLocationResource{ + ResourceArn: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), + }, + } + }, + "resource_data.0.lf_tag.0.key": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{LFTag: &awstypes.LFTagKeyResource{TagKey: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag.0.key"])}} + }, + "resource_data.0.lf_tag_expression.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + LFTagExpression: &awstypes.LFTagExpressionResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), + }, + } + }, + "resource_data.0.lf_tag_policy.0.resource_type": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + LFTagPolicy: &awstypes.LFTagPolicyResource{ + ResourceType: awstypes.ResourceType(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), + ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), + }, + } + }, + "resource_data.0.table.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + Table: &awstypes.TableResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.table.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), + }, + } + }, + "resource_data.0.table_with_columns.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + TableWithColumns: &awstypes.TableWithColumnsResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), + }, + } + }, } - // If the lake formation admin has been revoked, there will be access denied instead of entity not found - if errs.IsA[*awstypes.AccessDeniedException](err) { - continue + for path, constructor := range resourceConstructors { + if v, ok := rs.Primary.Attributes[path]; ok && v != "" { + in.Resource = constructor(rs) + break + } } + _, err := tflakeformation.FindOptInByID(ctx, conn, principalID, in.Resource) if err != nil { + if errs.IsAErrorMessageContains[*awstypes.AccessDeniedException](err, "Insufficient Lake Formation permission(s) on Catalog") { + return nil + } + if errs.IsAErrorMessageContains[*awstypes.EntityNotFoundException](err, "") { + return nil + } return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, err) } return create.Error(names.LakeFormation, create.ErrActionCheckingDestroyed, tflakeformation.ResNameOptIn, rs.Primary.ID, errors.New("not destroyed")) @@ -202,91 +207,95 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat conn := acctest.Provider.Meta().(*conns.AWSClient).LakeFormationClient(ctx) - principalID := rs.Primary.Attributes["principal.0.data_lake_principal_identifier"] - if principalID == "" { - return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("principal identifier not set")) - } - + principalID := rs.Primary.ID in := &lakeformation.ListLakeFormationOptInsInput{} - var resource *awstypes.Resource - if v, ok := rs.Primary.Attributes["resource_data.0.catalog.0.id"]; ok && v != "" { - resource = &awstypes.Resource{ - Catalog: &awstypes.CatalogResource{Id: aws.String(v)}, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.database.0.name"]; ok && v != "" { - resource = &awstypes.Resource{ - Database: &awstypes.DatabaseResource{ - Name: aws.String(v), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]; ok && v != "" { - resource = &awstypes.Resource{ - DataCellsFilter: &awstypes.DataCellsFilterResource{ - Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), - TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]; ok && v != "" { - resource = &awstypes.Resource{ - DataLocation: &awstypes.DataLocationResource{ - ResourceArn: aws.String(v), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag.0.key"]; ok && v != "" { - resource = &awstypes.Resource{ - LFTag: &awstypes.LFTagKeyResource{ - TagKey: aws.String(v), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]; ok && v != "" { - resource = &awstypes.Resource{ - LFTagExpression: &awstypes.LFTagExpressionResource{ - Name: aws.String(v), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]; ok && v != "" { - resource = &awstypes.Resource{ - LFTagPolicy: &awstypes.LFTagPolicyResource{ - ResourceType: awstypes.ResourceType(v), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), - ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.table.0.name"]; ok && v != "" { - resource = &awstypes.Resource{ - Table: &awstypes.TableResource{ - Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), - }, - } - } else if v, ok := rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]; ok && v != "" { - resource = &awstypes.Resource{ - TableWithColumns: &awstypes.TableWithColumnsResource{ - Name: aws.String(v), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), - }, + type resourceConstructor func(*terraform.ResourceState) *awstypes.Resource + + resourceConstructors := map[string]resourceConstructor{ + "resource_data.0.catalog.0.id": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{Catalog: &awstypes.CatalogResource{Id: aws.String(rs.Primary.Attributes["resource_data.0.catalog.0.id"])}} + }, + "resource_data.0.database.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + Database: &awstypes.DatabaseResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.database.0.name"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), + }, + } + }, + "resource_data.0.data_cells_filter.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + DataCellsFilter: &awstypes.DataCellsFilterResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), + TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), + }, + } + }, + "resource_data.0.data_location.0.resource_arn": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + DataLocation: &awstypes.DataLocationResource{ + ResourceArn: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), + }, + } + }, + "resource_data.0.lf_tag.0.key": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{LFTag: &awstypes.LFTagKeyResource{TagKey: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag.0.key"])}} + }, + "resource_data.0.lf_tag_expression.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + LFTagExpression: &awstypes.LFTagExpressionResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), + }, + } + }, + "resource_data.0.lf_tag_policy.0.resource_type": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + LFTagPolicy: &awstypes.LFTagPolicyResource{ + ResourceType: awstypes.ResourceType(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), + ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), + }, + } + }, + "resource_data.0.table.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + Table: &awstypes.TableResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.table.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), + }, + } + }, + "resource_data.0.table_with_columns.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + TableWithColumns: &awstypes.TableWithColumnsResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), + }, + } + }, + } + + for path, constructor := range resourceConstructors { + if v, ok := rs.Primary.Attributes[path]; ok && v != "" { + in.Resource = constructor(rs) + break } } - if resource == nil { + if in.Resource == nil { return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("no valid resource found in state")) } - in.Resource = resource - - out, err := tflakeformation.FindOptInByID(ctx, conn, principalID, resource) + out, err := conn.ListLakeFormationOptIns(ctx, in) if err != nil { return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, principalID, err) } - *optin = lakeformation.ListLakeFormationOptInsOutput{ - LakeFormationOptInsInfoList: []awstypes.LakeFormationOptInsInfo{*out}, - } + *optin = *out return nil } From ecd95f93db03ea4ade5d48db4e650bda202486d9 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Tue, 4 Mar 2025 12:50:38 -0800 Subject: [PATCH 17/17] Create helper function for cleaner code --- internal/service/lakeformation/opt_in_test.go | 233 +++++++----------- 1 file changed, 83 insertions(+), 150 deletions(-) diff --git a/internal/service/lakeformation/opt_in_test.go b/internal/service/lakeformation/opt_in_test.go index d7253f278816..8350b9ed3f05 100644 --- a/internal/service/lakeformation/opt_in_test.go +++ b/internal/service/lakeformation/opt_in_test.go @@ -101,81 +101,7 @@ func testAccCheckOptInDestroy(ctx context.Context) resource.TestCheckFunc { Resource: &awstypes.Resource{}, } - type resourceConstructor func(*terraform.ResourceState) *awstypes.Resource - - resourceConstructors := map[string]resourceConstructor{ - "resource_data.0.catalog.0.id": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{Catalog: &awstypes.CatalogResource{Id: aws.String(rs.Primary.Attributes["resource_data.0.catalog.0.id"])}} - }, - "resource_data.0.database.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - Database: &awstypes.DatabaseResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.database.0.name"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), - }, - } - }, - "resource_data.0.data_cells_filter.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - DataCellsFilter: &awstypes.DataCellsFilterResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), - TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), - }, - } - }, - "resource_data.0.data_location.0.resource_arn": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - DataLocation: &awstypes.DataLocationResource{ - ResourceArn: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), - }, - } - }, - "resource_data.0.lf_tag.0.key": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{LFTag: &awstypes.LFTagKeyResource{TagKey: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag.0.key"])}} - }, - "resource_data.0.lf_tag_expression.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - LFTagExpression: &awstypes.LFTagExpressionResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), - }, - } - }, - "resource_data.0.lf_tag_policy.0.resource_type": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - LFTagPolicy: &awstypes.LFTagPolicyResource{ - ResourceType: awstypes.ResourceType(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), - ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), - }, - } - }, - "resource_data.0.table.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - Table: &awstypes.TableResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.table.0.name"]), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), - }, - } - }, - "resource_data.0.table_with_columns.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - TableWithColumns: &awstypes.TableWithColumnsResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), - }, - } - }, - } - - for path, constructor := range resourceConstructors { - if v, ok := rs.Primary.Attributes[path]; ok && v != "" { - in.Resource = constructor(rs) - break - } - } + in.Resource = constructOptInResource(rs) _, err := tflakeformation.FindOptInByID(ctx, conn, principalID, in.Resource) if err != nil { @@ -210,81 +136,7 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat principalID := rs.Primary.ID in := &lakeformation.ListLakeFormationOptInsInput{} - type resourceConstructor func(*terraform.ResourceState) *awstypes.Resource - - resourceConstructors := map[string]resourceConstructor{ - "resource_data.0.catalog.0.id": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{Catalog: &awstypes.CatalogResource{Id: aws.String(rs.Primary.Attributes["resource_data.0.catalog.0.id"])}} - }, - "resource_data.0.database.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - Database: &awstypes.DatabaseResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.database.0.name"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), - }, - } - }, - "resource_data.0.data_cells_filter.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - DataCellsFilter: &awstypes.DataCellsFilterResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), - TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), - }, - } - }, - "resource_data.0.data_location.0.resource_arn": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - DataLocation: &awstypes.DataLocationResource{ - ResourceArn: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), - }, - } - }, - "resource_data.0.lf_tag.0.key": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{LFTag: &awstypes.LFTagKeyResource{TagKey: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag.0.key"])}} - }, - "resource_data.0.lf_tag_expression.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - LFTagExpression: &awstypes.LFTagExpressionResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), - }, - } - }, - "resource_data.0.lf_tag_policy.0.resource_type": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - LFTagPolicy: &awstypes.LFTagPolicyResource{ - ResourceType: awstypes.ResourceType(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]), - CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), - ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), - }, - } - }, - "resource_data.0.table.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - Table: &awstypes.TableResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.table.0.name"]), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), - }, - } - }, - "resource_data.0.table_with_columns.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { - return &awstypes.Resource{ - TableWithColumns: &awstypes.TableWithColumnsResource{ - Name: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]), - DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), - }, - } - }, - } - - for path, constructor := range resourceConstructors { - if v, ok := rs.Primary.Attributes[path]; ok && v != "" { - in.Resource = constructor(rs) - break - } - } + in.Resource = constructOptInResource(rs) if in.Resource == nil { return create.Error(names.LakeFormation, create.ErrActionCheckingExistence, tflakeformation.ResNameOptIn, name, errors.New("no valid resource found in state")) @@ -301,6 +153,87 @@ func testAccCheckOptInExists(ctx context.Context, name string, optin *lakeformat } } +func constructOptInResource(rs *terraform.ResourceState) *awstypes.Resource { + type resourceConstructor func(*terraform.ResourceState) *awstypes.Resource + + resourceConstructors := map[string]resourceConstructor{ + "resource_data.0.catalog.0.id": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{Catalog: &awstypes.CatalogResource{Id: aws.String(rs.Primary.Attributes["resource_data.0.catalog.0.id"])}} + }, + "resource_data.0.database.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + Database: &awstypes.DatabaseResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.database.0.name"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.database.0.catalog_id"]), + }, + } + }, + "resource_data.0.data_cells_filter.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + DataCellsFilter: &awstypes.DataCellsFilterResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.database_name"]), + TableCatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_cells_filter.0.table_catalog_id"]), + }, + } + }, + "resource_data.0.data_location.0.resource_arn": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + DataLocation: &awstypes.DataLocationResource{ + ResourceArn: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.resource_arn"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.data_location.0.catalog_id"]), + }, + } + }, + "resource_data.0.lf_tag.0.key": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{LFTag: &awstypes.LFTagKeyResource{TagKey: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag.0.key"])}} + }, + "resource_data.0.lf_tag_expression.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + LFTagExpression: &awstypes.LFTagExpressionResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.name"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_expression.0.catalog_id"]), + }, + } + }, + "resource_data.0.lf_tag_policy.0.resource_type": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + LFTagPolicy: &awstypes.LFTagPolicyResource{ + ResourceType: awstypes.ResourceType(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.resource_type"]), + CatalogId: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.catalog_id"]), + ExpressionName: aws.String(rs.Primary.Attributes["resource_data.0.lf_tag_policy.0.expression_name"]), + }, + } + }, + "resource_data.0.table.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + Table: &awstypes.TableResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.table.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table.0.database_name"]), + }, + } + }, + "resource_data.0.table_with_columns.0.name": func(rs *terraform.ResourceState) *awstypes.Resource { + return &awstypes.Resource{ + TableWithColumns: &awstypes.TableWithColumnsResource{ + Name: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.name"]), + DatabaseName: aws.String(rs.Primary.Attributes["resource_data.0.table_with_columns.0.database_name"]), + }, + } + }, + } + + var resource *awstypes.Resource + for path, constructor := range resourceConstructors { + if v, ok := rs.Primary.Attributes[path]; ok && v != "" { + resource = constructor(rs) + break + } + } + + return resource +} + func testAccOptInConfig_basic(rName string) string { return fmt.Sprintf(` data "aws_partition" "current" {}