Skip to content

Commit 1a1c0a3

Browse files
committed
create lf_tag_expression resource
1 parent 0315b28 commit 1a1c0a3

File tree

6 files changed

+874
-0
lines changed

6 files changed

+874
-0
lines changed

.changelog/43883.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-resource
2+
aws_lakeformation_lf_tag_expression
3+
```

internal/service/lakeformation/lakeformation_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ func TestAccLakeFormation_serial(t *testing.T) {
8484
"values": testAccLFTag_Values,
8585
"valuesOverFifty": testAccLFTag_Values_overFifty,
8686
},
87+
"LFTagExpression": {
88+
acctest.CtBasic: testAccLFTagExpression_basic,
89+
"values": testAccLFTagExpression_update,
90+
"import": testAccLFTagExpression_import,
91+
},
8792
"ResourceLFTag": {
8893
acctest.CtBasic: testAccResourceLFTag_basic,
8994
acctest.CtDisappears: testAccResourceLFTag_disappears,
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
package lakeformation
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"sort"
8+
9+
"github.com/aws/aws-sdk-go-v2/aws"
10+
"github.com/aws/aws-sdk-go-v2/service/lakeformation"
11+
12+
lfTypes "github.com/aws/aws-sdk-go-v2/service/lakeformation/types"
13+
"github.com/hashicorp/terraform-plugin-framework/diag"
14+
"github.com/hashicorp/terraform-plugin-framework/resource"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/path"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
19+
"github.com/hashicorp/terraform-plugin-framework/types"
20+
21+
"github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag"
22+
"github.com/hashicorp/terraform-provider-aws/internal/framework"
23+
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
24+
)
25+
26+
// @FrameworkResource("aws_lakeformation_lf_tag_expression", name="LF Tag Expression")
27+
func newLFTagExpressionResource(_ context.Context) (resource.ResourceWithConfigure, error) {
28+
return &lfTagExpressionResource{
29+
ResourceWithModel: framework.ResourceWithModel[lfTagExpressionResourceModel]{},
30+
}, nil
31+
}
32+
33+
type lfTagExpressionResource struct {
34+
framework.ResourceWithConfigure
35+
framework.ResourceWithModel[lfTagExpressionResourceModel]
36+
}
37+
38+
func (r *lfTagExpressionResource) Schema(
39+
ctx context.Context,
40+
req resource.SchemaRequest,
41+
resp *resource.SchemaResponse,
42+
) {
43+
resp.Schema = schema.Schema{
44+
Description: "Manages an AWS Lake Formation Tag Expression.",
45+
Attributes: map[string]schema.Attribute{
46+
"id": schema.StringAttribute{
47+
Computed: true,
48+
Description: "Primary identifier (catalog_id:name)",
49+
},
50+
"catalog_id": schema.StringAttribute{
51+
Optional: true,
52+
Computed: true,
53+
Description: "The ID of the Data Catalog.",
54+
},
55+
"name": schema.StringAttribute{
56+
Required: true,
57+
PlanModifiers: []planmodifier.String{
58+
stringplanmodifier.RequiresReplace(),
59+
},
60+
Description: "The name of the LF-Tag Expression.",
61+
},
62+
"description": schema.StringAttribute{
63+
Optional: true,
64+
Description: "A description of the LF-Tag Expression.",
65+
},
66+
"tag_expression": schema.MapAttribute{
67+
Required: true,
68+
ElementType: types.SetType{ElemType: types.StringType},
69+
Description: "Mapping of tag keys to lists of allowed values.",
70+
},
71+
},
72+
}
73+
}
74+
75+
type lfTagExpressionResourceModel struct {
76+
framework.WithRegionModel
77+
ID types.String `tfsdk:"id"`
78+
CatalogId types.String `tfsdk:"catalog_id"`
79+
Name types.String `tfsdk:"name"`
80+
Description types.String `tfsdk:"description"`
81+
TagExpression map[string]types.Set `tfsdk:"tag_expression"`
82+
}
83+
84+
func (r *lfTagExpressionResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
85+
var data lfTagExpressionResourceModel
86+
response.Diagnostics.Append(request.Plan.Get(ctx, &data)...)
87+
if response.Diagnostics.HasError() {
88+
return
89+
}
90+
91+
conn := r.Meta().LakeFormationClient(ctx)
92+
93+
expr, expandDiags := expandLFTagExpression(ctx, data.TagExpression)
94+
response.Diagnostics.Append(expandDiags...)
95+
if response.Diagnostics.HasError() {
96+
return
97+
}
98+
99+
// Get catalog ID, defaulting to account ID if not set
100+
catalogId := data.CatalogId.ValueString()
101+
if catalogId == "" {
102+
catalogId = r.Meta().AccountID(ctx)
103+
data.CatalogId = types.StringValue(catalogId)
104+
}
105+
106+
input := &lakeformation.CreateLFTagExpressionInput{
107+
CatalogId: aws.String(catalogId),
108+
Name: aws.String(data.Name.ValueString()),
109+
Description: func() *string {
110+
if data.Description.IsNull() {
111+
return nil
112+
}
113+
return aws.String(data.Description.ValueString())
114+
}(),
115+
Expression: expr,
116+
}
117+
118+
_, err := conn.CreateLFTagExpression(ctx, input)
119+
120+
if err != nil {
121+
response.Diagnostics.AddError(
122+
"Error Creating LF-Tag Expression",
123+
fmt.Sprintf("Could not create LF-Tag Expression: %s", err),
124+
)
125+
return
126+
}
127+
128+
data.ID = types.StringValue(fmt.Sprintf("%s:%s", data.CatalogId.ValueString(), data.Name.ValueString()))
129+
response.Diagnostics.Append(response.State.Set(ctx, data)...)
130+
}
131+
132+
func (r *lfTagExpressionResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
133+
var data lfTagExpressionResourceModel
134+
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
135+
if response.Diagnostics.HasError() {
136+
return
137+
}
138+
139+
conn := r.Meta().LakeFormationClient(ctx)
140+
name := data.Name.ValueString()
141+
142+
output, err := conn.GetLFTagExpression(ctx, &lakeformation.GetLFTagExpressionInput{
143+
CatalogId: aws.String(data.CatalogId.ValueString()),
144+
Name: aws.String(name),
145+
})
146+
147+
if tfresource.NotFound(err) {
148+
response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err))
149+
response.State.RemoveResource(ctx)
150+
return
151+
}
152+
153+
if err != nil {
154+
response.Diagnostics.AddError(
155+
"Error Reading LF-Tag Expression",
156+
fmt.Sprintf("Could not read LF-Tag Expression %s: %s", name, err),
157+
)
158+
return
159+
}
160+
161+
// Manually populate the model fields from the API response
162+
if output.CatalogId != nil {
163+
data.CatalogId = types.StringValue(*output.CatalogId)
164+
}
165+
if output.Name != nil {
166+
data.Name = types.StringValue(*output.Name)
167+
}
168+
if output.Description != nil {
169+
data.Description = types.StringValue(*output.Description)
170+
}
171+
172+
// Convert the Expression from AWS API format to types.Set map
173+
tagExprMap := make(map[string]types.Set)
174+
for _, tag := range output.Expression {
175+
if tag.TagKey != nil {
176+
setValue, diags := types.SetValueFrom(ctx, types.StringType, tag.TagValues)
177+
response.Diagnostics.Append(diags...)
178+
if response.Diagnostics.HasError() {
179+
return
180+
}
181+
tagExprMap[*tag.TagKey] = setValue
182+
}
183+
}
184+
data.TagExpression = tagExprMap
185+
186+
// Ensure ID is properly set (consistent with Create/Update)
187+
data.ID = types.StringValue(fmt.Sprintf("%s:%s", data.CatalogId.ValueString(), data.Name.ValueString()))
188+
189+
response.Diagnostics.Append(response.State.Set(ctx, &data)...)
190+
}
191+
192+
func (r *lfTagExpressionResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) {
193+
var plan lfTagExpressionResourceModel
194+
response.Diagnostics.Append(request.Plan.Get(ctx, &plan)...)
195+
if response.Diagnostics.HasError() {
196+
return
197+
}
198+
199+
expr, expandDiags := expandLFTagExpression(ctx, plan.TagExpression)
200+
response.Diagnostics.Append(expandDiags...)
201+
if response.Diagnostics.HasError() {
202+
return
203+
}
204+
205+
// Get catalog ID, defaulting to account ID if not set
206+
catalogId := plan.CatalogId.ValueString()
207+
if catalogId == "" {
208+
catalogId = r.Meta().AccountID(ctx)
209+
plan.CatalogId = types.StringValue(catalogId)
210+
}
211+
212+
input := &lakeformation.UpdateLFTagExpressionInput{
213+
CatalogId: aws.String(catalogId),
214+
Name: aws.String(plan.Name.ValueString()),
215+
Description: func() *string {
216+
if plan.Description.IsNull() {
217+
return nil
218+
}
219+
return aws.String(plan.Description.ValueString())
220+
}(),
221+
Expression: expr,
222+
}
223+
224+
if _, err := r.Meta().LakeFormationClient(ctx).UpdateLFTagExpression(ctx, input); err != nil {
225+
response.Diagnostics.AddError(
226+
"Error Updating LF-Tag Expression",
227+
fmt.Sprintf("Could not update LF-Tag Expression: %s", err),
228+
)
229+
return
230+
}
231+
232+
plan.ID = types.StringValue(fmt.Sprintf("%s:%s", plan.CatalogId.ValueString(), plan.Name.ValueString()))
233+
response.Diagnostics.Append(response.State.Set(ctx, &plan)...)
234+
}
235+
236+
func (r *lfTagExpressionResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
237+
var state lfTagExpressionResourceModel
238+
response.Diagnostics.Append(request.State.Get(ctx, &state)...)
239+
if response.Diagnostics.HasError() {
240+
return
241+
}
242+
243+
input := &lakeformation.DeleteLFTagExpressionInput{
244+
CatalogId: aws.String(state.CatalogId.ValueString()),
245+
Name: aws.String(state.Name.ValueString()),
246+
}
247+
if _, err := r.Meta().LakeFormationClient(ctx).DeleteLFTagExpression(ctx, input); err != nil {
248+
if !tfresource.NotFound(err) {
249+
response.Diagnostics.AddError(
250+
"Error Deleting LF-Tag Expression",
251+
fmt.Sprintf("Could not delete LF-Tag Expression: %s", err),
252+
)
253+
}
254+
}
255+
}
256+
257+
func (r *lfTagExpressionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
258+
// Parse the import ID which should be in format "catalog_id:name"
259+
parts := strings.SplitN(req.ID, ":", 2)
260+
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
261+
resp.Diagnostics.AddError(
262+
"Invalid Import ID",
263+
"Import ID must be in format 'catalog_id:name'",
264+
)
265+
return
266+
}
267+
268+
catalogId := parts[0]
269+
name := parts[1]
270+
271+
// Set the parsed values in state
272+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("catalog_id"), catalogId)...)
273+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...)
274+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
275+
276+
if resp.Diagnostics.HasError() {
277+
return
278+
}
279+
}
280+
281+
// expandLFTagExpression converts the native Go map into the AWS LFTag slice.
282+
func expandLFTagExpression(ctx context.Context, m map[string]types.Set) ([]lfTypes.LFTag, diag.Diagnostics) {
283+
var expr []lfTypes.LFTag
284+
var diags diag.Diagnostics
285+
286+
// Sort the keys for deterministic ordering
287+
keys := make([]string, 0, len(m))
288+
for k := range m {
289+
keys = append(keys, k)
290+
}
291+
sort.Strings(keys)
292+
293+
// For each key, sort its values and append to the LFTag list
294+
for _, k := range keys {
295+
set := m[k]
296+
var vals []string
297+
diags.Append(set.ElementsAs(ctx, &vals, false)...)
298+
if diags.HasError() {
299+
return nil, diags
300+
}
301+
302+
sort.Strings(vals)
303+
expr = append(expr, lfTypes.LFTag{
304+
TagKey: aws.String(k),
305+
TagValues: vals,
306+
})
307+
}
308+
309+
return expr, diags
310+
}

0 commit comments

Comments
 (0)