Skip to content

Commit 924ce94

Browse files
authored
[feat][gov] Add environment resource (#57)
* add environment provider * include all files * working environment resource * lint
1 parent 81ea4e1 commit 924ce94

File tree

11 files changed

+839
-3
lines changed

11 files changed

+839
-3
lines changed

docs/resources/environment.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
page_title: "Resource: retool_environment"
3+
description: |-
4+
Environment resource allows you to create and manage Environments in Retool. Environments are used to manage different settings for your Retool apps in different contexts, such as development, staging, and production.
5+
---
6+
7+
# Resource: retool_environment
8+
9+
Environment resource allows you to create and manage Environments in Retool. Environments are used to manage different settings for your Retool apps in different contexts, such as development, staging, and production.
10+
11+
## Example Usage
12+
13+
```terraform
14+
resource "retool_environment" "development" {
15+
name = "Development"
16+
description = "Development environment"
17+
color = "#FFA500"
18+
}
19+
20+
resource "retool_environment" "staging" {
21+
name = "Staging"
22+
description = "Staging environment for testing"
23+
color = "#FFFF00"
24+
}
25+
26+
resource "retool_environment" "production" {
27+
name = "Production"
28+
description = "Production environment"
29+
color = "#FF0000"
30+
}
31+
32+
resource "retool_environment" "minimal" {
33+
name = "Testing"
34+
color = "#00FF00"
35+
}
36+
```
37+
38+
<!-- schema generated by tfplugindocs -->
39+
## Schema
40+
41+
### Required
42+
43+
- `color` (String) The hexadecimal color code for the environment (e.g., #FF5733).
44+
- `name` (String) The name of the environment.
45+
46+
### Optional
47+
48+
- `description` (String) A brief description of the environment.
49+
50+
### Read-Only
51+
52+
- `created_at` (String) The timestamp when the environment was created.
53+
- `default` (Boolean) Indicates if this is the default environment.
54+
- `id` (String) The unique identifier for the environment.
55+
- `updated_at` (String) The timestamp when the environment was last updated.
56+
57+
## Import
58+
59+
Import is supported using the following syntax:
60+
61+
```shell
62+
# Import an existing environment by ID.
63+
terraform import retool_environment.development "environment-id-here"
64+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Import an existing environment by ID.
2+
terraform import retool_environment.development "environment-id-here"
3+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
resource "retool_environment" "development" {
2+
name = "Development"
3+
description = "Development environment"
4+
color = "#FFA500"
5+
}
6+
7+
resource "retool_environment" "staging" {
8+
name = "Staging"
9+
description = "Staging environment for testing"
10+
color = "#FFFF00"
11+
}
12+
13+
resource "retool_environment" "production" {
14+
name = "Production"
15+
description = "Production environment"
16+
color = "#FF0000"
17+
}
18+
19+
resource "retool_environment" "minimal" {
20+
name = "Testing"
21+
color = "#00FF00"
22+
}
23+
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
// Package environments provides implementation of the Environment resource.
2+
package environments
3+
4+
import (
5+
"context"
6+
7+
"github.com/tryretool/terraform-provider-retool/internal/provider/utils"
8+
"github.com/tryretool/terraform-provider-retool/internal/sdk/api"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
"github.com/hashicorp/terraform-plugin-log/tflog"
17+
)
18+
19+
type environmentResource struct {
20+
client *api.APIClient
21+
}
22+
23+
// Ensure EnvironmentResource implements the tfsdk.Resource interface.
24+
var (
25+
_ resource.Resource = &environmentResource{}
26+
_ resource.ResourceWithConfigure = &environmentResource{}
27+
_ resource.ResourceWithImportState = &environmentResource{}
28+
)
29+
30+
type environmentResourceModel struct {
31+
ID types.String `tfsdk:"id"`
32+
Name types.String `tfsdk:"name"`
33+
Description types.String `tfsdk:"description"`
34+
Color types.String `tfsdk:"color"`
35+
Default types.Bool `tfsdk:"default"`
36+
CreatedAt types.String `tfsdk:"created_at"`
37+
UpdatedAt types.String `tfsdk:"updated_at"`
38+
}
39+
40+
// NewResource creates a new Environment resource.
41+
func NewResource() resource.Resource {
42+
return &environmentResource{}
43+
}
44+
45+
func (r *environmentResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
46+
resp.TypeName = req.ProviderTypeName + "_environment"
47+
}
48+
49+
func (r *environmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
50+
resp.Schema = schema.Schema{
51+
Description: "Environment resource allows you to create and manage Environments in Retool. Environments are used to manage different settings for your Retool apps in different contexts, such as development, staging, and production.",
52+
Attributes: map[string]schema.Attribute{
53+
"id": schema.StringAttribute{
54+
Computed: true,
55+
Description: "The unique identifier for the environment.",
56+
PlanModifiers: []planmodifier.String{
57+
stringplanmodifier.UseStateForUnknown(),
58+
},
59+
},
60+
"name": schema.StringAttribute{
61+
Required: true,
62+
Description: "The name of the environment.",
63+
},
64+
"description": schema.StringAttribute{
65+
Optional: true,
66+
Description: "A brief description of the environment.",
67+
},
68+
"color": schema.StringAttribute{
69+
Required: true,
70+
Description: "The hexadecimal color code for the environment (e.g., #FF5733).",
71+
},
72+
"default": schema.BoolAttribute{
73+
Computed: true,
74+
Description: "Indicates if this is the default environment.",
75+
},
76+
"created_at": schema.StringAttribute{
77+
Computed: true,
78+
Description: "The timestamp when the environment was created.",
79+
PlanModifiers: []planmodifier.String{
80+
stringplanmodifier.UseStateForUnknown(),
81+
},
82+
},
83+
"updated_at": schema.StringAttribute{
84+
Computed: true,
85+
Description: "The timestamp when the environment was last updated.",
86+
},
87+
},
88+
}
89+
}
90+
91+
func (r *environmentResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
92+
if req.ProviderData == nil {
93+
return
94+
}
95+
96+
providerData, ok := req.ProviderData.(*utils.ProviderData)
97+
if !ok {
98+
resp.Diagnostics.AddError("Unexpected Resource Configure Type", "Expected *utils.ProviderData, got: %T. Please report this issue to the provider developers.")
99+
return
100+
}
101+
r.client = providerData.Client
102+
}
103+
104+
func (r *environmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
105+
var plan environmentResourceModel
106+
diags := req.Plan.Get(ctx, &plan)
107+
resp.Diagnostics.Append(diags...)
108+
if resp.Diagnostics.HasError() {
109+
return
110+
}
111+
112+
tflog.Info(ctx, "Creating Environment", map[string]interface{}{"name": plan.Name})
113+
114+
request := api.EnvironmentsPostRequest{
115+
Name: plan.Name.ValueString(),
116+
Color: plan.Color.ValueString(),
117+
}
118+
if !plan.Description.IsNull() && !plan.Description.IsUnknown() {
119+
description := plan.Description.ValueString()
120+
request.Description = &description
121+
}
122+
123+
response, httpResponse, err := r.client.EnvironmentsAPI.EnvironmentsPost(ctx).EnvironmentsPostRequest(request).Execute()
124+
if err != nil {
125+
resp.Diagnostics.AddError(
126+
"Error creating Environment",
127+
"Could not create Environment, unexpected error: "+err.Error(),
128+
)
129+
tflog.Error(ctx, "Error creating Environment", utils.AddHTTPStatusCode(map[string]interface{}{"error": err.Error()}, httpResponse))
130+
return
131+
}
132+
133+
plan.ID = types.StringValue(response.Data.Id)
134+
plan.Name = types.StringValue(response.Data.Name)
135+
plan.Description = types.StringPointerValue(response.Data.Description.Get())
136+
plan.Color = types.StringValue(response.Data.Color)
137+
plan.Default = types.BoolValue(response.Data.Default)
138+
plan.CreatedAt = types.StringValue(response.Data.CreatedAt)
139+
plan.UpdatedAt = types.StringValue(response.Data.UpdatedAt)
140+
141+
diags = resp.State.Set(ctx, &plan)
142+
resp.Diagnostics.Append(diags...)
143+
if resp.Diagnostics.HasError() {
144+
return
145+
}
146+
tflog.Info(ctx, "Environment created", map[string]interface{}{"id": plan.ID})
147+
}
148+
149+
func (r *environmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
150+
var state environmentResourceModel
151+
diags := req.State.Get(ctx, &state)
152+
resp.Diagnostics.Append(diags...)
153+
if resp.Diagnostics.HasError() {
154+
return
155+
}
156+
157+
environmentID := state.ID.ValueString()
158+
tflog.Info(ctx, "Reading Environment", map[string]interface{}{"id": environmentID})
159+
response, httpResponse, err := r.client.EnvironmentsAPI.EnvironmentsEnvironmentIdGet(ctx, environmentID).Execute()
160+
if err != nil {
161+
if httpResponse != nil && httpResponse.StatusCode == 404 {
162+
tflog.Info(ctx, "Environment not found", map[string]any{"id": environmentID})
163+
resp.State.RemoveResource(ctx)
164+
return
165+
}
166+
167+
resp.Diagnostics.AddError(
168+
"Error reading Environment",
169+
"Could not read Environment, unexpected error: "+err.Error(),
170+
)
171+
tflog.Error(ctx, "Error reading Environment", utils.AddHTTPStatusCode(map[string]interface{}{"error": err.Error(), "id": environmentID}, httpResponse))
172+
return
173+
}
174+
state.Name = types.StringValue(response.Data.Name)
175+
state.Description = types.StringPointerValue(response.Data.Description.Get())
176+
state.Color = types.StringValue(response.Data.Color)
177+
state.Default = types.BoolValue(response.Data.Default)
178+
state.CreatedAt = types.StringValue(response.Data.CreatedAt)
179+
state.UpdatedAt = types.StringValue(response.Data.UpdatedAt)
180+
181+
diags = resp.State.Set(ctx, &state)
182+
resp.Diagnostics.Append(diags...)
183+
if resp.Diagnostics.HasError() {
184+
return
185+
}
186+
tflog.Info(ctx, "Environment read", map[string]interface{}{"id": environmentID})
187+
}
188+
189+
func (r *environmentResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
190+
var state environmentResourceModel
191+
diags := req.State.Get(ctx, &state)
192+
resp.Diagnostics.Append(diags...)
193+
if resp.Diagnostics.HasError() {
194+
return
195+
}
196+
197+
var plan environmentResourceModel
198+
diags = req.Plan.Get(ctx, &plan)
199+
resp.Diagnostics.Append(diags...)
200+
if resp.Diagnostics.HasError() {
201+
return
202+
}
203+
204+
environmentID := state.ID.ValueString()
205+
tflog.Info(ctx, "Updating Environment", map[string]interface{}{"id": environmentID, "name": plan.Name.ValueString()})
206+
207+
var operations []api.ReplaceOperation
208+
209+
// Check if name changed.
210+
if !plan.Name.Equal(state.Name) {
211+
nameOp := api.NewReplaceOperation("replace", "/name")
212+
nameOp.Value = plan.Name.ValueStringPointer()
213+
operations = append(operations, *nameOp)
214+
}
215+
216+
// Check if description changed.
217+
if !plan.Description.Equal(state.Description) {
218+
descOp := api.NewReplaceOperation("replace", "/description")
219+
if plan.Description.IsNull() {
220+
descOp.SetValue(nil)
221+
} else {
222+
descOp.Value = plan.Description.ValueStringPointer()
223+
}
224+
operations = append(operations, *descOp)
225+
}
226+
227+
// Check if color changed.
228+
if !plan.Color.Equal(state.Color) {
229+
colorOp := api.NewReplaceOperation("replace", "/color")
230+
colorOp.Value = plan.Color.ValueStringPointer()
231+
operations = append(operations, *colorOp)
232+
}
233+
234+
// Only make the PATCH request if there are operations to perform.
235+
if len(operations) > 0 {
236+
patchRequest := api.NewEnvironmentsEnvironmentIdPatchRequest(operations)
237+
response, httpResponse, err := r.client.EnvironmentsAPI.EnvironmentsEnvironmentIdPatch(ctx, environmentID).EnvironmentsEnvironmentIdPatchRequest(*patchRequest).Execute()
238+
if err != nil {
239+
resp.Diagnostics.AddError(
240+
"Error updating Environment",
241+
"Could not update Environment with id "+environmentID+", unexpected error: "+err.Error(),
242+
)
243+
tflog.Error(ctx, "Error updating Environment", utils.AddHTTPStatusCode(map[string]interface{}{"error": err.Error(), "id": environmentID}, httpResponse))
244+
return
245+
}
246+
247+
plan.Name = types.StringValue(response.Data.Name)
248+
plan.Description = types.StringPointerValue(response.Data.Description.Get())
249+
plan.Color = types.StringValue(response.Data.Color)
250+
plan.Default = types.BoolValue(response.Data.Default)
251+
plan.CreatedAt = types.StringValue(response.Data.CreatedAt)
252+
plan.UpdatedAt = types.StringValue(response.Data.UpdatedAt)
253+
}
254+
255+
diags = resp.State.Set(ctx, &plan)
256+
resp.Diagnostics.Append(diags...)
257+
if resp.Diagnostics.HasError() {
258+
return
259+
}
260+
tflog.Info(ctx, "Environment updated", map[string]interface{}{"id": environmentID})
261+
}
262+
263+
func (r *environmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
264+
var state environmentResourceModel
265+
diags := req.State.Get(ctx, &state)
266+
resp.Diagnostics.Append(diags...)
267+
if resp.Diagnostics.HasError() {
268+
return
269+
}
270+
271+
environmentID := state.ID.ValueString()
272+
tflog.Info(ctx, "Deleting Environment", map[string]interface{}{"id": environmentID})
273+
httpResponse, err := r.client.EnvironmentsAPI.EnvironmentsEnvironmentIdDelete(ctx, environmentID).Execute()
274+
if err != nil && !(httpResponse != nil && httpResponse.StatusCode == 404) {
275+
resp.Diagnostics.AddError(
276+
"Error deleting Environment",
277+
"Could not delete Environment with id "+environmentID+", unexpected error: "+err.Error(),
278+
)
279+
tflog.Error(ctx, "Error deleting Environment", utils.AddHTTPStatusCode(map[string]interface{}{"error": err.Error(), "id": environmentID}, httpResponse))
280+
return
281+
}
282+
}
283+
284+
// ImportState allows importing of an Environment resource.
285+
func (r *environmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
286+
// Retrieve import ID and save to id attribute.
287+
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
288+
}

0 commit comments

Comments
 (0)