Skip to content

Commit e6fd7b5

Browse files
authored
Migrate tfe_team_token to the provider framework (#1681)
* Migrate resource * Fix failing test cases * Add descriptions
1 parent 575ea3f commit e6fd7b5

File tree

3 files changed

+179
-97
lines changed

3 files changed

+179
-97
lines changed

internal/provider/provider.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ func Provider() *schema.Provider {
133133
"tfe_team_project_access": resourceTFETeamProjectAccess(),
134134
"tfe_team_member": resourceTFETeamMember(),
135135
"tfe_team_members": resourceTFETeamMembers(),
136-
"tfe_team_token": resourceTFETeamToken(),
137136
"tfe_terraform_version": resourceTFETerraformVersion(),
138137
"tfe_workspace": resourceTFEWorkspace(),
139138
"tfe_variable_set": resourceTFEVariableSet(),

internal/provider/provider_next.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res
166166
NewTestVariableResource,
167167
NewWorkspaceRunTaskResource,
168168
NewNotificationConfigurationResource,
169+
NewTeamTokenResource,
169170
}
170171
}
171172

Lines changed: 178 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,149 +1,231 @@
1-
// Copyright (c) HashiCorp, Inc.
2-
// SPDX-License-Identifier: MPL-2.0
3-
4-
// NOTE: This is a legacy resource and should be migrated to the Plugin
5-
// Framework if substantial modifications are planned. See
6-
// docs/new-resources.md if planning to use this code as boilerplate for
7-
// a new resource.
1+
// // Copyright (c) HashiCorp, Inc.
2+
// // SPDX-License-Identifier: MPL-2.0
83

94
package provider
105

116
import (
127
"context"
138
"errors"
149
"fmt"
15-
"log"
1610
"time"
1711

1812
tfe "github.com/hashicorp/go-tfe"
19-
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/path"
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/boolplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
18+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
19+
"github.com/hashicorp/terraform-plugin-framework/types"
20+
"github.com/hashicorp/terraform-plugin-log/tflog"
2021
)
2122

22-
func resourceTFETeamToken() *schema.Resource {
23-
return &schema.Resource{
24-
Create: resourceTFETeamTokenCreate,
25-
Read: resourceTFETeamTokenRead,
26-
Delete: resourceTFETeamTokenDelete,
27-
Importer: &schema.ResourceImporter{
28-
StateContext: resourceTFETeamTokenImporter,
29-
},
23+
var (
24+
_ resource.ResourceWithConfigure = &resourceTFETeamToken{}
25+
_ resource.ResourceWithImportState = &resourceTFETeamToken{}
26+
)
3027

31-
Schema: map[string]*schema.Schema{
32-
"team_id": {
33-
Type: schema.TypeString,
34-
Required: true,
35-
ForceNew: true,
36-
},
28+
func NewTeamTokenResource() resource.Resource {
29+
return &resourceTFETeamToken{}
30+
}
3731

38-
"force_regenerate": {
39-
Type: schema.TypeBool,
40-
Optional: true,
41-
ForceNew: true,
42-
},
32+
type resourceTFETeamToken struct {
33+
config ConfiguredClient
34+
}
4335

44-
"token": {
45-
Type: schema.TypeString,
46-
Computed: true,
47-
Sensitive: true,
48-
},
36+
type modelTFETeamToken struct {
37+
ID types.String `tfsdk:"id"`
38+
TeamID types.String `tfsdk:"team_id"`
39+
ForceRegenerate types.Bool `tfsdk:"force_regenerate"`
40+
Token types.String `tfsdk:"token"`
41+
ExpiredAt types.String `tfsdk:"expired_at"`
42+
}
4943

50-
"expired_at": {
51-
Type: schema.TypeString,
52-
Optional: true,
53-
ForceNew: true,
44+
func (r *resourceTFETeamToken) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
45+
// Early exit if provider is unconfigured (i.e. we're only validating config or something)
46+
if req.ProviderData == nil {
47+
return
48+
}
49+
client, ok := req.ProviderData.(ConfiguredClient)
50+
if !ok {
51+
resp.Diagnostics.AddError(
52+
"Unexpected resource Configure type",
53+
fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData),
54+
)
55+
}
56+
r.config = client
57+
}
58+
59+
func (r *resourceTFETeamToken) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
60+
resp.TypeName = req.ProviderTypeName + "_team_token"
61+
}
62+
63+
func (r *resourceTFETeamToken) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
64+
resp.Schema = schema.Schema{
65+
Attributes: map[string]schema.Attribute{
66+
"id": schema.StringAttribute{
67+
Description: "The ID of the token",
68+
Computed: true,
69+
PlanModifiers: []planmodifier.String{
70+
stringplanmodifier.UseStateForUnknown(),
71+
},
72+
},
73+
"team_id": schema.StringAttribute{
74+
Description: "ID of the team.",
75+
Required: true,
76+
PlanModifiers: []planmodifier.String{
77+
stringplanmodifier.RequiresReplace(),
78+
},
79+
},
80+
"force_regenerate": schema.BoolAttribute{
81+
Description: "When set to true will force the audit trail token to be recreated.",
82+
Optional: true,
83+
PlanModifiers: []planmodifier.Bool{
84+
boolplanmodifier.RequiresReplace(),
85+
},
86+
},
87+
"token": schema.StringAttribute{
88+
Description: "The generated token.",
89+
Computed: true,
90+
Sensitive: true,
91+
},
92+
"expired_at": schema.StringAttribute{
93+
Description: "The token's expiration date.",
94+
Optional: true,
95+
PlanModifiers: []planmodifier.String{
96+
stringplanmodifier.RequiresReplace(),
97+
},
5498
},
5599
},
100+
Description: "Generates a new team token and overrides existing token if one exists.",
56101
}
57102
}
58103

59-
func resourceTFETeamTokenCreate(d *schema.ResourceData, meta interface{}) error {
60-
config := meta.(ConfiguredClient)
61-
62-
// Get the team ID.
63-
teamID := d.Get("team_id").(string)
104+
func (r *resourceTFETeamToken) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
105+
var plan modelTFETeamToken
106+
diags := req.Plan.Get(ctx, &plan)
107+
resp.Diagnostics.Append(diags...)
108+
if resp.Diagnostics.HasError() {
109+
return
110+
}
64111

65-
log.Printf("[DEBUG] Check if a token already exists for team: %s", teamID)
66-
_, err := config.Client.TeamTokens.Read(ctx, teamID)
112+
teamID := plan.TeamID.ValueString()
113+
tflog.Debug(ctx, fmt.Sprintf("Check if a token already exists for team: %s", teamID))
114+
_, err := r.config.Client.TeamTokens.Read(ctx, teamID)
67115
if err != nil && !errors.Is(err, tfe.ErrResourceNotFound) {
68-
return fmt.Errorf("error checking if a token exists for team %s: %w", teamID, err)
116+
resp.Diagnostics.AddError(
117+
fmt.Sprintf("Error checking if a token exists for team %s", teamID),
118+
err.Error(),
119+
)
120+
return
69121
}
70-
71-
// If error is nil, the token already exists.
72122
if err == nil {
73-
if !d.Get("force_regenerate").(bool) {
74-
return fmt.Errorf("a token already exists for team: %s", teamID)
123+
if !plan.ForceRegenerate.ValueBool() {
124+
resp.Diagnostics.AddError(
125+
fmt.Sprintf("A token already exists for team: %s", teamID),
126+
"Set force_regenerate to true to regenerate the token.",
127+
)
128+
return
75129
}
76-
log.Printf("[DEBUG] Regenerating existing token for team: %s", teamID)
130+
tflog.Debug(ctx, fmt.Sprintf("Regenerating existing token for team: %s", teamID))
77131
}
78132

79-
// Get the token create options.
133+
expiredAt := plan.ExpiredAt.ValueString()
80134
options := tfe.TeamTokenCreateOptions{}
81-
82-
// Check whether the optional expiry was provided.
83-
expiredAt, expiredAtProvided := d.GetOk("expired_at")
84-
85-
// If an expiry was provided, parse it and update the options struct.
86-
if expiredAtProvided {
87-
expiry, err := time.Parse(time.RFC3339, expiredAt.(string))
88-
89-
options.ExpiredAt = &expiry
90-
135+
if !plan.ExpiredAt.IsNull() && expiredAt != "" {
136+
expiry, err := time.Parse(time.RFC3339, expiredAt)
91137
if err != nil {
92-
return fmt.Errorf("%s must be a valid date or time, provided in iso8601 format", expiredAt)
138+
resp.Diagnostics.AddError(
139+
fmt.Sprintf("%s must be a valid date or time, provided in iso8601 format", expiredAt),
140+
err.Error(),
141+
)
142+
return
93143
}
144+
options.ExpiredAt = &expiry
94145
}
95146

96-
log.Printf("[DEBUG] Create new token for team: %s", teamID)
97-
token, err := config.Client.TeamTokens.CreateWithOptions(ctx, teamID, options)
147+
token, err := r.config.Client.TeamTokens.CreateWithOptions(ctx, teamID, options)
98148
if err != nil {
99-
return fmt.Errorf(
100-
"error creating new token for team %s: %w", teamID, err)
149+
resp.Diagnostics.AddError(
150+
fmt.Sprintf("Error creating new token for team %s", teamID),
151+
err.Error(),
152+
)
153+
return
101154
}
102155

103-
d.SetId(teamID)
156+
result := modelFromTFEToken(token, plan.TeamID, plan.ForceRegenerate, plan.ExpiredAt)
157+
resp.Diagnostics.Append(resp.State.Set(ctx, result)...)
158+
}
104159

105-
// We need to set this here in the create function as this value will
106-
// only be returned once during the creation of the token.
107-
d.Set("token", token.Token)
160+
func modelFromTFEToken(token *tfe.TeamToken, teamID types.String, forceRegenerate types.Bool, expiredAt types.String) modelTFETeamToken {
161+
m := modelTFETeamToken{
162+
ID: teamID,
163+
TeamID: teamID,
164+
ForceRegenerate: forceRegenerate,
165+
ExpiredAt: types.StringNull(),
166+
Token: types.StringValue(token.Token),
167+
}
168+
if !expiredAt.IsNull() {
169+
m.ExpiredAt = expiredAt
170+
}
108171

109-
return resourceTFETeamTokenRead(d, meta)
172+
return m
110173
}
111174

112-
func resourceTFETeamTokenRead(d *schema.ResourceData, meta interface{}) error {
113-
config := meta.(ConfiguredClient)
175+
func (r *resourceTFETeamToken) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
176+
var state modelTFETeamToken
177+
diags := req.State.Get(ctx, &state)
178+
resp.Diagnostics.Append(diags...)
179+
if resp.Diagnostics.HasError() {
180+
return
181+
}
114182

115-
log.Printf("[DEBUG] Read the token from team: %s", d.Id())
116-
_, err := config.Client.TeamTokens.Read(ctx, d.Id())
183+
teamID := state.TeamID.ValueString()
184+
tflog.Debug(ctx, fmt.Sprintf("Read the token from team: %s", teamID))
185+
token, err := r.config.Client.TeamTokens.Read(ctx, teamID)
117186
if err != nil {
118-
if err == tfe.ErrResourceNotFound {
119-
log.Printf("[DEBUG] Token for team %s no longer exists", d.Id())
120-
d.SetId("")
121-
return nil
187+
if errors.Is(err, tfe.ErrResourceNotFound) {
188+
tflog.Debug(ctx, fmt.Sprintf("Token for team %s no longer exists", teamID))
189+
resp.State.RemoveResource(ctx)
190+
return
122191
}
123-
return fmt.Errorf("error reading token from team %s: %w", d.Id(), err)
192+
resp.Diagnostics.AddError(
193+
fmt.Sprintf("Error reading token from team %s", teamID),
194+
err.Error(),
195+
)
196+
return
124197
}
198+
result := modelFromTFEToken(token, state.TeamID, state.ForceRegenerate, state.ExpiredAt)
199+
resp.Diagnostics.Append(resp.State.Set(ctx, result)...)
200+
}
125201

126-
return nil
202+
func (r *resourceTFETeamToken) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
203+
// This should never be called, based on the schema
204+
resp.Diagnostics.AddError("Update not supported.", "Please recreate the resource")
127205
}
128206

129-
func resourceTFETeamTokenDelete(d *schema.ResourceData, meta interface{}) error {
130-
config := meta.(ConfiguredClient)
207+
func (r *resourceTFETeamToken) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
208+
var state modelTFETeamToken
209+
diags := req.State.Get(ctx, &state)
210+
resp.Diagnostics.Append(diags...)
211+
if resp.Diagnostics.HasError() {
212+
return
213+
}
131214

132-
log.Printf("[DEBUG] Delete token from team: %s", d.Id())
133-
err := config.Client.TeamTokens.Delete(ctx, d.Id())
134-
if err != nil {
135-
if err == tfe.ErrResourceNotFound {
136-
return nil
215+
teamID := state.TeamID.ValueString()
216+
tflog.Debug(ctx, fmt.Sprintf("Delete the token from team: %s", teamID))
217+
if err := r.config.Client.TeamTokens.Delete(ctx, teamID); err != nil {
218+
if errors.Is(err, tfe.ErrResourceNotFound) {
219+
tflog.Debug(ctx, fmt.Sprintf("Token for team %s no longer exists", teamID))
220+
return
137221
}
138-
return fmt.Errorf("error deleting token from team %s: %w", d.Id(), err)
222+
resp.Diagnostics.AddError(
223+
fmt.Sprintf("Error deleting token from team %s", teamID),
224+
err.Error(),
225+
)
139226
}
140-
141-
return nil
142227
}
143228

144-
func resourceTFETeamTokenImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
145-
// Set the team ID field.
146-
d.Set("team_id", d.Id())
147-
148-
return []*schema.ResourceData{d}, nil
229+
func (r *resourceTFETeamToken) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
230+
resource.ImportStatePassthroughID(ctx, path.Root("team_id"), req, resp)
149231
}

0 commit comments

Comments
 (0)