Skip to content

Commit e1fda79

Browse files
authored
Merge pull request #1390 from hashicorp/TF-17010-provider-resource-tfe-stack
New resource: tfe_stack
2 parents d5c41d2 + c419687 commit e1fda79

File tree

8 files changed

+505
-6
lines changed

8 files changed

+505
-6
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ provider_tfe_override.tfrc
2121

2222
# mkdocs build output
2323
site/
24+
.envrc

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ require (
1212
github.com/hashicorp/go-multierror v1.1.1 // indirect
1313
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
1414
github.com/hashicorp/go-slug v0.15.2
15-
github.com/hashicorp/go-tfe v1.57.0
16-
github.com/hashicorp/go-version v1.6.0
15+
github.com/hashicorp/go-tfe v1.58.0
16+
github.com/hashicorp/go-version v1.7.0
1717
github.com/hashicorp/hcl v1.0.0
1818
github.com/hashicorp/hcl/v2 v2.19.1 // indirect
1919
github.com/hashicorp/terraform-plugin-framework v1.8.0

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH
6666
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
6767
github.com/hashicorp/go-slug v0.15.2 h1:/ioIpE4bWVN/d7pG2qMrax0a7xe9vOA66S+fz7fZmGY=
6868
github.com/hashicorp/go-slug v0.15.2/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ=
69-
github.com/hashicorp/go-tfe v1.57.0 h1:sggR6C4CrtNAJqoCRNoZwBQnRfnWuImR6xbg2sUAbiU=
70-
github.com/hashicorp/go-tfe v1.57.0/go.mod h1:XnTtBj3tVQ4uFkcFsv8Grn+O1CVcIcceL1uc2AgUcaU=
69+
github.com/hashicorp/go-tfe v1.58.0 h1:aJXrStDBG+YJLkgDYswfNiKTRHQxKqT/9C1VuvujRkE=
70+
github.com/hashicorp/go-tfe v1.58.0/go.mod h1:XnTtBj3tVQ4uFkcFsv8Grn+O1CVcIcceL1uc2AgUcaU=
7171
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
7272
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
7373
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
74-
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
75-
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
74+
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
75+
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
7676
github.com/hashicorp/hc-install v0.6.3 h1:yE/r1yJvWbtrJ0STwScgEnCanb0U9v7zp0Gbkmcoxqs=
7777
github.com/hashicorp/hc-install v0.6.3/go.mod h1:KamGdbodYzlufbWh4r9NRo8y6GLHWZP2GBtdnms1Ln0=
7878
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=

internal/provider/provider_next.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ func (p *frameworkProvider) Resources(ctx context.Context) []func() resource.Res
145145
NewDataRetentionPolicyResource,
146146
NewResourceWorkspaceSettings,
147147
NewSAMLSettingsResource,
148+
NewStackResource,
148149
NewTestVariableResource,
149150
NewWorkspaceRunTaskResource,
150151
}
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/go-tfe"
11+
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
18+
"github.com/hashicorp/terraform-plugin-framework/types"
19+
"github.com/hashicorp/terraform-plugin-log/tflog"
20+
)
21+
22+
// Ensure provider defined types fully satisfy framework interfaces.
23+
var _ resource.Resource = &resourceTFEStack{}
24+
var _ resource.ResourceWithConfigure = &resourceTFEStack{}
25+
var _ resource.ResourceWithImportState = &resourceTFEStack{}
26+
27+
func NewStackResource() resource.Resource {
28+
return &resourceTFEStack{}
29+
}
30+
31+
// resourceTFEStack implements the tfe_stack resource type
32+
type resourceTFEStack struct {
33+
config ConfiguredClient
34+
}
35+
36+
func (r *resourceTFEStack) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
37+
resp.TypeName = req.ProviderTypeName + "_stack"
38+
}
39+
40+
func (r *resourceTFEStack) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
41+
pathVCSRepoOAuthTokenID := path.Expressions{
42+
path.MatchRelative().AtParent().AtName("oauth_token_id"),
43+
}
44+
pathGHAInstallationID := path.Expressions{
45+
path.MatchRelative().AtParent().AtName("github_app_installation_id"),
46+
}
47+
48+
resp.Schema = schema.Schema{
49+
Description: "Defines a Stack resource. Note that a Stack cannot be destroyed if it contains deployments that have underlying managed resources.",
50+
Version: 1,
51+
52+
Blocks: map[string]schema.Block{
53+
"vcs_repo": schema.SingleNestedBlock{
54+
Description: "VCS repository configuration for the Stack.",
55+
Attributes: map[string]schema.Attribute{
56+
"identifier": schema.StringAttribute{
57+
Description: "Identifier of the VCS repository.",
58+
Required: true,
59+
},
60+
"branch": schema.StringAttribute{
61+
Description: "The repository branch that Terraform should use. This defaults to the respository's default branch (e.g. main).",
62+
Optional: true,
63+
},
64+
"github_app_installation_id": schema.StringAttribute{
65+
Description: "The installation ID of the GitHub App. This conflicts with `oauth_token_id` and can only be used if `oauth_token_id` is not used.",
66+
Optional: true,
67+
Validators: []validator.String{
68+
stringvalidator.AtLeastOneOf(pathVCSRepoOAuthTokenID...),
69+
stringvalidator.ConflictsWith(pathVCSRepoOAuthTokenID...),
70+
},
71+
},
72+
"oauth_token_id": schema.StringAttribute{
73+
Description: "The VCS Connection to use. This ID can be obtained from a `tfe_oauth_client` resource. This conflicts with `github_app_installation_id` and can only be used if `github_app_installation_id` is not used.",
74+
Optional: true,
75+
Validators: []validator.String{
76+
stringvalidator.AtLeastOneOf(pathGHAInstallationID...),
77+
stringvalidator.ConflictsWith(pathGHAInstallationID...),
78+
},
79+
},
80+
},
81+
},
82+
},
83+
84+
Attributes: map[string]schema.Attribute{
85+
"id": schema.StringAttribute{
86+
Description: "ID of the Stack.",
87+
Computed: true,
88+
PlanModifiers: []planmodifier.String{
89+
stringplanmodifier.UseStateForUnknown(),
90+
},
91+
},
92+
"project_id": schema.StringAttribute{
93+
Description: "ID of the project that the Stack belongs to.",
94+
Required: true,
95+
PlanModifiers: []planmodifier.String{
96+
stringplanmodifier.RequiresReplace(),
97+
},
98+
},
99+
"name": schema.StringAttribute{
100+
Description: "Name of the Stack",
101+
Required: true,
102+
},
103+
"description": schema.StringAttribute{
104+
Description: "Description of the Stack",
105+
Optional: true,
106+
},
107+
"deployment_names": schema.SetAttribute{
108+
Description: "The time when the Stack was created.",
109+
Computed: true,
110+
ElementType: types.StringType,
111+
},
112+
"created_at": schema.StringAttribute{
113+
Description: "The time when the stack was created.",
114+
Computed: true,
115+
PlanModifiers: []planmodifier.String{
116+
stringplanmodifier.UseStateForUnknown(),
117+
},
118+
},
119+
"updated_at": schema.StringAttribute{
120+
Description: "The time when the stack was last updated.",
121+
Computed: true,
122+
},
123+
},
124+
}
125+
}
126+
127+
// Configure implements resource.ResourceWithConfigure
128+
func (r *resourceTFEStack) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
129+
// Prevent panic if the provider has not been configured.
130+
if req.ProviderData == nil {
131+
return
132+
}
133+
134+
client, ok := req.ProviderData.(ConfiguredClient)
135+
if !ok {
136+
resp.Diagnostics.AddError(
137+
"Unexpected resource Configure type",
138+
fmt.Sprintf("Expected tfe.ConfiguredClient, got %T. This is a bug in the tfe provider, so please report it on GitHub.", req.ProviderData),
139+
)
140+
}
141+
r.config = client
142+
}
143+
144+
func (r *resourceTFEStack) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
145+
var plan modelTFEStack
146+
147+
// Read Terraform plan data into the model
148+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
149+
150+
if resp.Diagnostics.HasError() {
151+
return
152+
}
153+
154+
if resp.Diagnostics.HasError() {
155+
return
156+
}
157+
158+
options := tfe.StackCreateOptions{
159+
Name: plan.Name.ValueString(),
160+
VCSRepo: &tfe.StackVCSRepo{
161+
Identifier: plan.VCSRepo.Identifier.ValueString(),
162+
Branch: plan.VCSRepo.Branch.ValueString(),
163+
GHAInstallationID: plan.VCSRepo.GHAInstallationID.ValueString(),
164+
OAuthTokenID: plan.VCSRepo.OAuthTokenID.ValueString(),
165+
},
166+
Project: &tfe.Project{
167+
ID: plan.ProjectID.ValueString(),
168+
},
169+
}
170+
171+
if !plan.Description.IsNull() {
172+
options.Description = tfe.String(plan.Description.ValueString())
173+
}
174+
175+
tflog.Debug(ctx, "Creating stack")
176+
stack, err := r.config.Client.Stacks.Create(ctx, options)
177+
if err != nil {
178+
resp.Diagnostics.AddError("Unable to create stack", err.Error())
179+
return
180+
}
181+
182+
result := modelFromTFEStack(stack)
183+
184+
// Save data into Terraform state
185+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
186+
}
187+
188+
func (r *resourceTFEStack) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
189+
var state modelTFEStack
190+
191+
// Read Terraform prior state data into the model
192+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
193+
194+
if resp.Diagnostics.HasError() {
195+
return
196+
}
197+
198+
tflog.Debug(ctx, fmt.Sprintf("Reading stack %q", state.ID.ValueString()))
199+
stack, err := r.config.Client.Stacks.Read(ctx, state.ID.ValueString())
200+
if err != nil {
201+
resp.Diagnostics.AddError("Unable to read stack", err.Error())
202+
return
203+
}
204+
205+
result := modelFromTFEStack(stack)
206+
207+
// Save updated data into Terraform state
208+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
209+
}
210+
211+
func (r *resourceTFEStack) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
212+
var plan modelTFEStack
213+
var state modelTFEStack
214+
215+
// Read Terraform plan data into the model
216+
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
217+
218+
// Read Terraform prior state data into the model
219+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
220+
221+
if resp.Diagnostics.HasError() {
222+
return
223+
}
224+
225+
options := tfe.StackUpdateOptions{
226+
Name: tfe.String(plan.Name.ValueString()),
227+
Description: tfe.String(plan.Description.ValueString()),
228+
VCSRepo: &tfe.StackVCSRepo{
229+
Identifier: plan.VCSRepo.Identifier.ValueString(),
230+
Branch: plan.VCSRepo.Branch.ValueString(),
231+
GHAInstallationID: plan.VCSRepo.GHAInstallationID.ValueString(),
232+
OAuthTokenID: plan.VCSRepo.OAuthTokenID.ValueString(),
233+
},
234+
}
235+
236+
tflog.Debug(ctx, "Updating stack")
237+
stack, err := r.config.Client.Stacks.Update(ctx, state.ID.ValueString(), options)
238+
if err != nil {
239+
resp.Diagnostics.AddError("Unable to update stack", err.Error())
240+
return
241+
}
242+
243+
result := modelFromTFEStack(stack)
244+
245+
// Save data into Terraform state
246+
resp.Diagnostics.Append(resp.State.Set(ctx, &result)...)
247+
}
248+
249+
func (r *resourceTFEStack) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
250+
var state modelTFEStack
251+
252+
// Read Terraform prior state data into the model
253+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
254+
255+
if resp.Diagnostics.HasError() {
256+
return
257+
}
258+
259+
tflog.Debug(ctx, "Deleting stack")
260+
err := r.config.Client.Stacks.Delete(ctx, state.ID.ValueString())
261+
if err != nil {
262+
resp.Diagnostics.AddError("Unable to delete stack", err.Error())
263+
return
264+
}
265+
}
266+
267+
func (r *resourceTFEStack) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
268+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), req.ID)...)
269+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"fmt"
8+
"math/rand"
9+
"testing"
10+
"time"
11+
12+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
13+
)
14+
15+
func TestAccTFEStackResource_basic(t *testing.T) {
16+
skipUnlessBeta(t)
17+
18+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
19+
orgName := fmt.Sprintf("tst-terraform-%d", rInt)
20+
21+
resource.Test(t, resource.TestCase{
22+
PreCheck: func() { testAccPreCheck(t) },
23+
ProtoV5ProviderFactories: testAccMuxedProviders,
24+
Steps: []resource.TestStep{
25+
{
26+
Config: testAccTFEStackResourceConfig(orgName, envGithubToken, "brandonc/pet-nulls-stack"),
27+
Check: resource.ComposeAggregateTestCheckFunc(
28+
resource.TestCheckResourceAttrSet("tfe_stack.foobar", "id"),
29+
resource.TestCheckResourceAttrSet("tfe_stack.foobar", "project_id"),
30+
resource.TestCheckResourceAttr("tfe_stack.foobar", "name", "example-stack"),
31+
resource.TestCheckResourceAttr("tfe_stack.foobar", "description", "Just an ordinary stack"),
32+
resource.TestCheckResourceAttr("tfe_stack.foobar", "vcs_repo.identifier", "brandonc/pet-nulls-stack"),
33+
resource.TestCheckResourceAttrSet("tfe_stack.foobar", "vcs_repo.oauth_token_id"),
34+
resource.TestCheckResourceAttrSet("tfe_stack.foobar", "created_at"),
35+
resource.TestCheckResourceAttrSet("tfe_stack.foobar", "updated_at"),
36+
),
37+
},
38+
{
39+
ResourceName: "tfe_stack.foobar",
40+
ImportState: true,
41+
ImportStateVerify: true,
42+
},
43+
},
44+
})
45+
}
46+
47+
func testAccTFEStackResourceConfig(orgName, ghToken, ghRepoIdentifier string) string {
48+
return fmt.Sprintf(`
49+
resource "tfe_organization" "foobar" {
50+
name = "%s"
51+
52+
}
53+
54+
resource "tfe_project" "example" {
55+
name = "example"
56+
organization = tfe_organization.foobar.name
57+
}
58+
59+
resource "tfe_oauth_client" "foobar" {
60+
organization = tfe_organization.foobar.name
61+
api_url = "https://api.github.com"
62+
http_url = "https://github.com"
63+
oauth_token = "%s"
64+
service_provider = "github"
65+
}
66+
67+
resource "tfe_stack" "foobar" {
68+
name = "example-stack"
69+
description = "Just an ordinary stack"
70+
project_id = tfe_project.example.id
71+
72+
vcs_repo {
73+
identifier = "%s"
74+
oauth_token_id = tfe_oauth_client.foobar.oauth_token_id
75+
}
76+
}
77+
`, orgName, ghToken, ghRepoIdentifier)
78+
}

0 commit comments

Comments
 (0)