Skip to content

Commit 5532831

Browse files
glennsartic4po
andauthored
Add resource tfe_audit_trail_token (#1533)
* add resource tfe_audit_trail_token * add test case * add document * add changelog * make logs better * Convert tfe_audit_trail_token resource to v5 Plugin This commit migrates the tfe_audit_trail_token resource from the older SDK to V5 of the Terraform Plugin. This commit also modifies the tests to create a business subscription as Audit Trails are not available on Trial subscriptions (missing the audit-logging entitlement). * Output TF version on tests This commit emits the Terraform version which is useful when debugging failed CI runs. --------- Co-authored-by: Max Cai <[email protected]>
1 parent 40f4591 commit 5532831

File tree

8 files changed

+661
-3
lines changed

8 files changed

+661
-3
lines changed

.github/actions/test-provider-tfe/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ runs:
116116
MOD_TFE: github.com/hashicorp/terraform-provider-tfe/internal/provider
117117
MOD_VERSION: github.com/hashicorp/terraform-provider-tfe/version
118118
run: |
119+
terraform --version
119120
gotestsum --junitfile summary.xml --format short-verbose -- $MOD_PROVIDER $MOD_TFE $MOD_VERSION -v -timeout=30m -run "${{ steps.test_split.outputs.run }}"
120121
121122
- name: Upload test artifacts

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ FEATURES:
1717

1818
## v0.59.0
1919

20+
FEATURES:
21+
* `r/tfe_audit_trail_token` is a new resource for managing audit trail tokens in organization, by @c4po [1488](https://github.com/hashicorp/terraform-provider-tfe/pull/1488)
22+
2023
## BREAKING CHANGES
2124

2225
* `r/tfe_team`: Default "secret" visibility has been removed from tfe_team because it now requires explicit or owner access. The default, "organization", is now computed by the platform. by @brandonc [#1439](https://github.com/hashicorp/terraform-provider-tfe/pull/1439)

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ require (
3131
golang.org/x/sys v0.25.0 // indirect
3232
golang.org/x/text v0.14.0 // indirect
3333
golang.org/x/time v0.6.0 // indirect
34-
google.golang.org/protobuf v1.33.0 // indirect
34+
google.golang.org/protobuf v1.34.0 // indirect
3535
)
3636

3737
require (
@@ -59,6 +59,7 @@ require (
5959
)
6060

6161
require (
62+
github.com/hashicorp/terraform-plugin-framework-timetypes v0.3.0
6263
github.com/hashicorp/terraform-plugin-log v0.9.0
6364
github.com/stretchr/testify v1.9.0
6465
go.uber.org/mock v0.4.0

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRy
8989
github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk=
9090
github.com/hashicorp/terraform-plugin-framework v1.8.0 h1:P07qy8RKLcoBkCrY2RHJer5AEvJnDuXomBgou6fD8kI=
9191
github.com/hashicorp/terraform-plugin-framework v1.8.0/go.mod h1:/CpTukO88PcL/62noU7cuyaSJ4Rsim+A/pa+3rUVufY=
92+
github.com/hashicorp/terraform-plugin-framework-timetypes v0.3.0 h1:egR4InfakWkgepZNUATWGwkrPhaAYOTEybPfEol+G/I=
93+
github.com/hashicorp/terraform-plugin-framework-timetypes v0.3.0/go.mod h1:9vjvl36aY1p6KltaA5QCvGC5hdE/9t4YuhGftw6WOgE=
9294
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc=
9395
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg=
9496
github.com/hashicorp/terraform-plugin-go v0.22.2 h1:5o8uveu6eZUf5J7xGPV0eY0TPXg3qpmwX9sce03Bxnc=
@@ -228,8 +230,8 @@ google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
228230
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
229231
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
230232
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
231-
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
232-
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
233+
google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
234+
google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
233235
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
234236
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
235237
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

internal/provider/provider_next.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ func (p *frameworkProvider) DataSources(ctx context.Context) []func() datasource
128128

129129
func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Resource {
130130
return []func() resource.Resource{
131+
NewAuditTrailTokenResource,
131132
NewOrganizationRunTaskGlobalSettingsResource,
132133
NewOrganizationRunTaskResource,
133134
NewRegistryGPGKeyResource,
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
package provider
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"time"
10+
11+
tfe "github.com/hashicorp/go-tfe"
12+
"github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes"
13+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
14+
"github.com/hashicorp/terraform-plugin-framework/resource"
15+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
16+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
17+
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
20+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
21+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
22+
"github.com/hashicorp/terraform-plugin-framework/types"
23+
"github.com/hashicorp/terraform-plugin-log/tflog"
24+
)
25+
26+
type resourceAuditTrailToken struct {
27+
config ConfiguredClient
28+
}
29+
30+
var _ resource.Resource = &resourceAuditTrailToken{}
31+
var _ resource.ResourceWithConfigure = &resourceAuditTrailToken{}
32+
var _ resource.ResourceWithImportState = &resourceAuditTrailToken{}
33+
var _ resource.ResourceWithModifyPlan = &resourceAuditTrailToken{}
34+
35+
func NewAuditTrailTokenResource() resource.Resource {
36+
return &resourceAuditTrailToken{}
37+
}
38+
39+
type modelTFEAuditTrailTokenV0 struct {
40+
ID types.String `tfsdk:"id"`
41+
Organization types.String `tfsdk:"organization"`
42+
Token types.String `tfsdk:"token"`
43+
ExpiredAt timetypes.RFC3339 `tfsdk:"expired_at"`
44+
ForceRegenerate types.Bool `tfsdk:"force_regenerate"`
45+
}
46+
47+
func modelFromTFEOrganizationToken(v *tfe.OrganizationToken, organization string, token types.String, forceRegen types.Bool) modelTFEAuditTrailTokenV0 {
48+
result := modelTFEAuditTrailTokenV0{
49+
Organization: types.StringValue(organization),
50+
ID: types.StringValue(organization),
51+
ForceRegenerate: forceRegen,
52+
Token: token,
53+
}
54+
55+
if !v.ExpiredAt.IsZero() {
56+
result.ExpiredAt = timetypes.NewRFC3339TimeValue(v.ExpiredAt)
57+
}
58+
59+
return result
60+
}
61+
62+
func (r *resourceAuditTrailToken) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
63+
resp.TypeName = req.ProviderTypeName + "_audit_trail_token"
64+
}
65+
66+
func (r *resourceAuditTrailToken) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
67+
// If an audit trail token uses the default organization, then if the deafault org. changes, it should trigger a modification
68+
modifyPlanForDefaultOrganizationChange(ctx, r.config.Organization, req.State, req.Config, req.Plan, resp)
69+
}
70+
71+
func (r *resourceAuditTrailToken) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
72+
// Prevent panic if the provider has not been configured.
73+
if req.ProviderData == nil {
74+
return
75+
}
76+
77+
client, ok := req.ProviderData.(ConfiguredClient)
78+
if !ok {
79+
resp.Diagnostics.AddError(
80+
"Unexpected resource Configure type",
81+
fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData),
82+
)
83+
}
84+
r.config = client
85+
}
86+
87+
func (r *resourceAuditTrailToken) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
88+
resp.Schema = schema.Schema{
89+
Version: 0,
90+
Attributes: map[string]schema.Attribute{
91+
"id": schema.StringAttribute{
92+
Computed: true,
93+
Description: "Service-generated identifier for the token",
94+
PlanModifiers: []planmodifier.String{
95+
stringplanmodifier.UseStateForUnknown(),
96+
},
97+
},
98+
"expired_at": schema.StringAttribute{
99+
Description: "The time when the audit trail token will expire. This must be a valid ISO8601 timestamp.",
100+
CustomType: timetypes.RFC3339Type{},
101+
Optional: true,
102+
PlanModifiers: []planmodifier.String{
103+
stringplanmodifier.RequiresReplace(),
104+
},
105+
},
106+
"organization": schema.StringAttribute{
107+
Description: "Name of the organization. If omitted, organization must be defined in the provider config.",
108+
Optional: true,
109+
Computed: true,
110+
Validators: []validator.String{
111+
stringvalidator.LengthAtLeast(1),
112+
},
113+
PlanModifiers: []planmodifier.String{
114+
stringplanmodifier.RequiresReplace(),
115+
},
116+
},
117+
"token": schema.StringAttribute{
118+
Description: "The authentication token for accessing Audit Trails.",
119+
Sensitive: true,
120+
Computed: true,
121+
PlanModifiers: []planmodifier.String{
122+
stringplanmodifier.UseStateForUnknown(),
123+
},
124+
},
125+
"force_regenerate": schema.BoolAttribute{
126+
Description: "When set to true will force the audit trail token to be recreated.",
127+
Optional: true,
128+
PlanModifiers: []planmodifier.Bool{
129+
boolplanmodifier.RequiresReplace(),
130+
},
131+
},
132+
},
133+
}
134+
}
135+
136+
func (r *resourceAuditTrailToken) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
137+
var state modelTFEAuditTrailTokenV0
138+
139+
// Read Terraform current state into the model
140+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
141+
if resp.Diagnostics.HasError() {
142+
return
143+
}
144+
145+
var organization string
146+
resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.State, &organization)...)
147+
if resp.Diagnostics.HasError() {
148+
return
149+
}
150+
151+
tokenType := tfe.AuditTrailToken
152+
153+
tflog.Debug(ctx, "Reading audit trail token")
154+
token, err := r.config.Client.OrganizationTokens.ReadWithOptions(ctx, organization, tfe.OrganizationTokenReadOptions{TokenType: &tokenType})
155+
if err != nil {
156+
if errors.Is(err, tfe.ErrResourceNotFound) {
157+
resp.State.RemoveResource(ctx)
158+
} else {
159+
resp.Diagnostics.AddError("Error reading Organization Audit Trail Token", "Could not read Organization Audit Trail Token, unexpected error: "+err.Error())
160+
}
161+
return
162+
}
163+
164+
result := modelFromTFEOrganizationToken(token, organization, state.Token, state.ForceRegenerate)
165+
166+
// Save updated data into Terraform state
167+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
168+
}
169+
170+
func (r *resourceAuditTrailToken) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
171+
var plan modelTFEAuditTrailTokenV0
172+
173+
// Read Terraform planned changes into the model
174+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
175+
if resp.Diagnostics.HasError() {
176+
return
177+
}
178+
179+
tokenType := tfe.AuditTrailToken
180+
181+
var organization string
182+
resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.Plan, &organization)...)
183+
184+
if resp.Diagnostics.HasError() {
185+
return
186+
}
187+
188+
// Check if an audit trail token already exists for the organization and only
189+
// continue if the force_regenerate flag is set.
190+
tflog.Debug(ctx, fmt.Sprintf("Check if an audit trail token already exists for organization: %s", organization))
191+
if token, err := r.config.Client.OrganizationTokens.ReadWithOptions(ctx, organization, tfe.OrganizationTokenReadOptions{TokenType: &tokenType}); err != nil {
192+
if !errors.Is(err, tfe.ErrResourceNotFound) {
193+
resp.Diagnostics.AddError("Error while checking if an audit token exists for organization", fmt.Sprintf("error checking if an audit token exists for organization %s: %s", organization, err))
194+
return
195+
}
196+
} else if token != nil {
197+
if !plan.ForceRegenerate.ValueBool() {
198+
resp.Diagnostics.AddError("An audit trail token already exists", fmt.Sprintf("an audit trail token already exists for organization: %s", organization))
199+
return
200+
}
201+
tflog.Debug(ctx, fmt.Sprintf("Regenerating existing audit trail token for organization: %s", organization))
202+
}
203+
204+
options := tfe.OrganizationTokenCreateOptions{
205+
TokenType: &tokenType,
206+
}
207+
208+
// Optional ExpiryAt
209+
expireString := plan.ExpiredAt.ValueString()
210+
if expireString != "" {
211+
expiry, err := time.Parse(time.RFC3339, expireString)
212+
if err != nil {
213+
resp.Diagnostics.AddError("Invalid date", fmt.Sprintf("%s must be a valid date or time, provided in iso8601 format", expireString))
214+
return
215+
}
216+
options.ExpiredAt = &expiry
217+
}
218+
219+
tflog.Debug(ctx, fmt.Sprintf("Create audit trail token for organization %s", organization))
220+
token, err := r.config.Client.OrganizationTokens.CreateWithOptions(ctx, organization, options)
221+
if err != nil {
222+
resp.Diagnostics.AddError("Unable to create organization audit trail token", err.Error())
223+
return
224+
}
225+
226+
result := modelFromTFEOrganizationToken(token, organization, types.StringValue(token.Token), plan.ForceRegenerate)
227+
228+
// Save data into Terraform state
229+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
230+
}
231+
232+
func (r *resourceAuditTrailToken) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
233+
resp.Diagnostics.AddError("Audit trail tokens cannot be updated", "Audit trail tokens cannot be updated. Please regenerate token.")
234+
}
235+
236+
func (r *resourceAuditTrailToken) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
237+
var state modelTFEAuditTrailTokenV0
238+
diags := req.State.Get(ctx, &state)
239+
resp.Diagnostics.Append(diags...)
240+
if resp.Diagnostics.HasError() {
241+
return
242+
}
243+
244+
var organization string
245+
resp.Diagnostics.Append(r.config.dataOrDefaultOrganization(ctx, req.State, &organization)...)
246+
if resp.Diagnostics.HasError() {
247+
return
248+
}
249+
tokenType := tfe.AuditTrailToken
250+
251+
options := tfe.OrganizationTokenDeleteOptions{
252+
TokenType: &tokenType,
253+
}
254+
255+
tflog.Debug(ctx, fmt.Sprintf("Delete organization audit trail token %s", organization))
256+
err := r.config.Client.OrganizationTokens.DeleteWithOptions(ctx, organization, options)
257+
// Ignore 404s for delete
258+
if err != nil && !errors.Is(err, tfe.ErrResourceNotFound) {
259+
resp.Diagnostics.AddError(
260+
"Error deleting organization audit trail token",
261+
fmt.Sprintf("Couldn't delete organization audit trail token %s: %s", organization, err.Error()),
262+
)
263+
}
264+
// Resource is implicitly deleted from resp.State if diagnostics have no errors.
265+
}
266+
267+
func (r *resourceAuditTrailToken) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
268+
organization := req.ID
269+
270+
tokenType := tfe.AuditTrailToken
271+
272+
tflog.Debug(ctx, "Reading audit trail token")
273+
if token, err := r.config.Client.OrganizationTokens.ReadWithOptions(ctx, organization, tfe.OrganizationTokenReadOptions{TokenType: &tokenType}); err != nil {
274+
resp.Diagnostics.AddError("Error importing organization audit trail token", err.Error())
275+
} else if token == nil {
276+
resp.Diagnostics.AddError(
277+
"Error importing organization audit trail token",
278+
"Audit trail token does not exist or has no details",
279+
)
280+
} else {
281+
result := modelFromTFEOrganizationToken(token, organization, basetypes.NewStringNull(), basetypes.NewBoolNull())
282+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
283+
}
284+
}

0 commit comments

Comments
 (0)