|
| 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