Skip to content

Commit 050e184

Browse files
authored
Merge pull request #42525 from pavloos/f-wafv2-api-key
[New Resource] - AWS WAF APIKey
2 parents de5fe65 + a30e138 commit 050e184

File tree

10 files changed

+600
-122
lines changed

10 files changed

+600
-122
lines changed

.changelog/42525.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-resource
2+
aws_wafv2_api_key
3+
```

internal/errs/fwdiag/diags.go

+7
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ func DiagnosticString(d diag.Diagnostic) string {
3838
return buf.String()
3939
}
4040

41+
func NewParsingResourceIDErrorDiagnostic(err error) diag.Diagnostic {
42+
return diag.NewErrorDiagnostic(
43+
"Parsing Resource ID",
44+
err.Error(),
45+
)
46+
}
47+
4148
func NewResourceNotFoundWarningDiagnostic(err error) diag.Diagnostic {
4249
return diag.NewWarningDiagnostic(
4350
"AWS resource not found during refresh",

internal/service/wafv2/api_key.go

+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package wafv2
5+
6+
import (
7+
"context"
8+
9+
"github.com/YakDriver/regexache"
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/service/wafv2"
12+
awstypes "github.com/aws/aws-sdk-go-v2/service/wafv2/types"
13+
"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
14+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
15+
"github.com/hashicorp/terraform-plugin-framework/path"
16+
"github.com/hashicorp/terraform-plugin-framework/resource"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier"
20+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
21+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
22+
"github.com/hashicorp/terraform-plugin-framework/types"
23+
"github.com/hashicorp/terraform-provider-aws/internal/errs"
24+
"github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag"
25+
intflex "github.com/hashicorp/terraform-provider-aws/internal/flex"
26+
"github.com/hashicorp/terraform-provider-aws/internal/framework"
27+
fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex"
28+
fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types"
29+
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
30+
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
31+
"github.com/hashicorp/terraform-provider-aws/names"
32+
)
33+
34+
// @FrameworkResource("aws_wafv2_api_key", name="API Key")
35+
func newAPIKeyResource(_ context.Context) (resource.ResourceWithConfigure, error) {
36+
return &apiKeyResource{}, nil
37+
}
38+
39+
type apiKeyResource struct {
40+
framework.ResourceWithConfigure
41+
framework.WithNoUpdate
42+
}
43+
44+
func (r *apiKeyResource) Schema(ctx context.Context, request resource.SchemaRequest, response *resource.SchemaResponse) {
45+
response.Schema = schema.Schema{
46+
Description: "Provides a WAFv2 API Key resource.",
47+
Attributes: map[string]schema.Attribute{
48+
"api_key": schema.StringAttribute{
49+
Computed: true,
50+
Sensitive: true,
51+
Description: "The API key value. This is sensitive and not included in responses.",
52+
},
53+
names.AttrScope: schema.StringAttribute{
54+
CustomType: fwtypes.StringEnumType[awstypes.Scope](),
55+
Required: true,
56+
PlanModifiers: []planmodifier.String{
57+
stringplanmodifier.RequiresReplace(),
58+
},
59+
Description: "Specifies whether this is for an AWS CloudFront distribution or for a regional application. Valid values are CLOUDFRONT or REGIONAL.",
60+
},
61+
"token_domains": schema.SetAttribute{
62+
CustomType: fwtypes.SetOfStringType,
63+
Required: true,
64+
ElementType: types.StringType,
65+
PlanModifiers: []planmodifier.Set{
66+
setplanmodifier.RequiresReplace(),
67+
},
68+
Validators: []validator.Set{
69+
setvalidator.SizeAtLeast(1),
70+
setvalidator.SizeAtMost(5),
71+
setvalidator.ValueStringsAre(
72+
stringvalidator.RegexMatches(
73+
// Based on RFC 1034, RFC 1123, and RFC 5890
74+
// - Domain labels must start and end with alphanumeric characters
75+
// - Domain labels can contain hyphens but not at start or end
76+
// - Each domain label can be up to 63 characters
77+
// - Must contain at least one period (separating domain and TLD)
78+
regexache.MustCompile(`^([0-9A-Za-z]([0-9A-Za-z-]{0,61}[0-9A-Za-z])?\.)+[0-9A-Za-z]([0-9A-Za-z-]{0,61}[0-9A-Za-z])?$`),
79+
"domain names must follow DNS format with valid characters: A-Z, a-z, 0-9, -(hyphen) or . (period)",
80+
),
81+
stringvalidator.LengthAtMost(253),
82+
),
83+
},
84+
Description: "The domains that you want to be able to use the API key with, for example example.com. Maximum of 5 domains.",
85+
},
86+
},
87+
}
88+
}
89+
90+
func (r *apiKeyResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) {
91+
var data apiKeyResourceModel
92+
response.Diagnostics.Append(request.Plan.Get(ctx, &data)...)
93+
if response.Diagnostics.HasError() {
94+
return
95+
}
96+
97+
conn := r.Meta().WAFV2Client(ctx)
98+
99+
input := wafv2.CreateAPIKeyInput{
100+
Scope: data.Scope.ValueEnum(),
101+
TokenDomains: fwflex.ExpandFrameworkStringValueSet(ctx, data.TokenDomains),
102+
}
103+
104+
output, err := conn.CreateAPIKey(ctx, &input)
105+
106+
if err != nil {
107+
response.Diagnostics.AddError("creating WAFv2 API Key", err.Error())
108+
109+
return
110+
}
111+
112+
// Set values for unknowns.
113+
data.APIKey = fwflex.StringToFramework(ctx, output.APIKey)
114+
115+
response.Diagnostics.Append(response.State.Set(ctx, data)...)
116+
}
117+
118+
func (r *apiKeyResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) {
119+
var data apiKeyResourceModel
120+
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
121+
if response.Diagnostics.HasError() {
122+
return
123+
}
124+
125+
conn := r.Meta().WAFV2Client(ctx)
126+
127+
output, err := findAPIKeyByTwoPartKey(ctx, conn, data.APIKey.ValueString(), data.Scope.ValueEnum())
128+
129+
if tfresource.NotFound(err) {
130+
response.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err))
131+
response.State.RemoveResource(ctx)
132+
133+
return
134+
}
135+
136+
if err != nil {
137+
response.Diagnostics.AddError("reading WAFv2 API Key", err.Error())
138+
139+
return
140+
}
141+
142+
// Set attributes for import.
143+
response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...)
144+
if response.Diagnostics.HasError() {
145+
return
146+
}
147+
148+
response.Diagnostics.Append(response.State.Set(ctx, &data)...)
149+
}
150+
151+
func (r *apiKeyResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) {
152+
var data apiKeyResourceModel
153+
response.Diagnostics.Append(request.State.Get(ctx, &data)...)
154+
if response.Diagnostics.HasError() {
155+
return
156+
}
157+
158+
conn := r.Meta().WAFV2Client(ctx)
159+
160+
input := wafv2.DeleteAPIKeyInput{
161+
APIKey: fwflex.StringFromFramework(ctx, data.APIKey),
162+
Scope: data.Scope.ValueEnum(),
163+
}
164+
165+
_, err := conn.DeleteAPIKey(ctx, &input)
166+
167+
if errs.IsA[*awstypes.WAFNonexistentItemException](err) {
168+
return
169+
}
170+
171+
if err != nil {
172+
response.Diagnostics.AddError("deleting WAFv2 API Key", err.Error())
173+
174+
return
175+
}
176+
}
177+
178+
func (r *apiKeyResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) {
179+
const (
180+
apiKeyIDParts = 2
181+
)
182+
parts, err := intflex.ExpandResourceId(request.ID, apiKeyIDParts, true)
183+
184+
if err != nil {
185+
response.Diagnostics.Append(fwdiag.NewParsingResourceIDErrorDiagnostic(err))
186+
187+
return
188+
}
189+
190+
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root("api_key"), parts[0])...)
191+
response.Diagnostics.Append(response.State.SetAttribute(ctx, path.Root(names.AttrScope), parts[1])...)
192+
}
193+
194+
func findAPIKeyByTwoPartKey(ctx context.Context, conn *wafv2.Client, key string, scope awstypes.Scope) (*awstypes.APIKeySummary, error) {
195+
input := wafv2.ListAPIKeysInput{
196+
Scope: scope,
197+
}
198+
199+
return findAPIKey(ctx, conn, &input, func(v *awstypes.APIKeySummary) bool {
200+
return aws.ToString(v.APIKey) == key
201+
})
202+
}
203+
204+
func findAPIKey(ctx context.Context, conn *wafv2.Client, input *wafv2.ListAPIKeysInput, filter tfslices.Predicate[*awstypes.APIKeySummary]) (*awstypes.APIKeySummary, error) {
205+
output, err := findAPIKeys(ctx, conn, input, filter)
206+
207+
if err != nil {
208+
return nil, err
209+
}
210+
211+
return tfresource.AssertSingleValueResult(output)
212+
}
213+
214+
func findAPIKeys(ctx context.Context, conn *wafv2.Client, input *wafv2.ListAPIKeysInput, filter tfslices.Predicate[*awstypes.APIKeySummary]) ([]awstypes.APIKeySummary, error) {
215+
var output []awstypes.APIKeySummary
216+
217+
err := listAPIKeysPages(ctx, conn, input, func(page *wafv2.ListAPIKeysOutput, lastPage bool) bool {
218+
if page == nil {
219+
return !lastPage
220+
}
221+
222+
for _, v := range page.APIKeySummaries {
223+
if filter(&v) {
224+
output = append(output, v)
225+
}
226+
}
227+
228+
return !lastPage
229+
})
230+
231+
return output, err
232+
}
233+
234+
type apiKeyResourceModel struct {
235+
APIKey types.String `tfsdk:"api_key"`
236+
Scope fwtypes.StringEnum[awstypes.Scope] `tfsdk:"scope"`
237+
TokenDomains fwtypes.SetOfString `tfsdk:"token_domains"`
238+
}

0 commit comments

Comments
 (0)