Skip to content

Commit 81ea4e1

Browse files
authored
[feat][gov] Add resource configuration provider (#56)
* update repo to reflect v.2.9.0 of the api * working notes in readme all tests passing, working on a local server with real examples docs lint fixes add comment on folder deletion recursion notes on reviewing better handling for config variables as secret update docs lint lint add resources resource docs fix test lint lint lint docs update wip on resource configs Bump golang.org/x/crypto from 0.23.0 to 0.35.0 (#53) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.35.0. - [Commits](golang/crypto@v0.23.0...v0.35.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.35.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [feat][gov] Update repo to reflect v.2.9.0 of the API (#52) * update repo to reflect v.2.9.0 of the api * fixes to cursor slop * notes in readme * all tests passing, working on a local server with real examples * docs * lint fixes * add comment on folder deletion recursion * notes on reviewing * better handling for config variables as secret * update docs * lint * lint [feat][gov] Add ability to manage resources (#55) * update repo to reflect v.2.9.0 of the api * fixes to cursor slop * notes in readme * all tests passing, working on a local server with real examples * docs * lint fixes * add comment on folder deletion recursion * notes on reviewing * better handling for config variables as secret * update docs * lint * lint * add resources resource * docs * fix test * lint * lint * lint * docs update * more accurate docs probable fixes more linting * docs * more linting * mention ignoring options in docs * tf file
1 parent b269d72 commit 81ea4e1

File tree

10 files changed

+1484
-0
lines changed

10 files changed

+1484
-0
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
page_title: "Resource: retool_resource_configuration"
3+
description: |-
4+
A resource configuration defines environment-specific configuration for a Retool resource. This allows you to configure different connection settings, credentials, and options for a resource across different environments (e.g., production, staging, development).
5+
For example, you might have a PostgreSQL resource that connects to different database instances in production vs. staging environments, each with different credentials and connection strings.
6+
---
7+
8+
# Resource: retool_resource_configuration
9+
10+
A resource configuration defines environment-specific configuration for a Retool resource. This allows you to configure different connection settings, credentials, and options for a resource across different environments (e.g., production, staging, development).
11+
12+
For example, you might have a PostgreSQL resource that connects to different database instances in production vs. staging environments, each with different credentials and connection strings.
13+
14+
## Example Usage
15+
16+
```terraform
17+
# First, create a resource
18+
resource "retool_resource" "postgres" {
19+
display_name = "PostgreSQL Database"
20+
type = "postgresql"
21+
options = jsonencode({
22+
host = "localhost"
23+
port = 5432
24+
database = "mydb"
25+
user = "default_user"
26+
})
27+
}
28+
29+
# Get environments
30+
data "retool_environments" "all" {}
31+
32+
# Create a production-specific configuration
33+
resource "retool_resource_configuration" "postgres_production" {
34+
resource_id = retool_resource.postgres.id
35+
environment_id = [for env in data.retool_environments.all.environments : env.id if env.name == "production"][0]
36+
37+
options = jsonencode({
38+
host = "prod-db.example.com"
39+
port = 5432
40+
database = "production_db"
41+
user = "prod_user"
42+
password = "prod_password"
43+
ssl = true
44+
})
45+
46+
lifecycle {
47+
# Ignore changes to options since the API adds default values
48+
ignore_changes = [options]
49+
}
50+
}
51+
52+
# Create a staging-specific configuration
53+
resource "retool_resource_configuration" "postgres_staging" {
54+
resource_id = retool_resource.postgres.id
55+
environment_id = [for env in data.retool_environments.all.environments : env.id if env.name == "staging"][0]
56+
57+
options = jsonencode({
58+
host = "staging-db.example.com"
59+
port = 5432
60+
database = "staging_db"
61+
user = "staging_user"
62+
password = "staging_password"
63+
ssl = true
64+
})
65+
66+
lifecycle {
67+
ignore_changes = [options]
68+
}
69+
}
70+
```
71+
72+
<!-- schema generated by tfplugindocs -->
73+
## Schema
74+
75+
### Required
76+
77+
- `environment_id` (String) The UUID of the environment for this configuration. Cannot be changed after creation.
78+
- `options` (String, Sensitive) JSON string containing the environment-specific resource configuration options. The structure varies by resource type and mirrors the options structure used when creating a resource.
79+
- `resource_id` (String) The UUID or name of the resource to configure. Cannot be changed after creation.
80+
81+
### Read-Only
82+
83+
- `created_at` (String) The timestamp when the resource configuration was created.
84+
- `id` (String) The UUID of the resource configuration.
85+
- `updated_at` (String) The timestamp when the resource configuration was last updated.
86+
87+
## Import
88+
89+
Import is supported using the following syntax:
90+
91+
```shell
92+
#!/bin/bash
93+
94+
# Import a resource configuration using its ID
95+
terraform import retool_resource_configuration.example 01234567-89ab-cdef-0123-456789abcdef
96+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
3+
# Import a resource configuration using its ID
4+
terraform import retool_resource_configuration.example 01234567-89ab-cdef-0123-456789abcdef
5+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# First, create a resource
2+
resource "retool_resource" "postgres" {
3+
display_name = "PostgreSQL Database"
4+
type = "postgresql"
5+
options = jsonencode({
6+
host = "localhost"
7+
port = 5432
8+
database = "mydb"
9+
user = "default_user"
10+
})
11+
}
12+
13+
# Get environments
14+
data "retool_environments" "all" {}
15+
16+
# Create a production-specific configuration
17+
resource "retool_resource_configuration" "postgres_production" {
18+
resource_id = retool_resource.postgres.id
19+
environment_id = [for env in data.retool_environments.all.environments : env.id if env.name == "production"][0]
20+
21+
options = jsonencode({
22+
host = "prod-db.example.com"
23+
port = 5432
24+
database = "production_db"
25+
user = "prod_user"
26+
password = "prod_password"
27+
ssl = true
28+
})
29+
30+
lifecycle {
31+
# Ignore changes to options since the API adds default values
32+
ignore_changes = [options]
33+
}
34+
}
35+
36+
# Create a staging-specific configuration
37+
resource "retool_resource_configuration" "postgres_staging" {
38+
resource_id = retool_resource.postgres.id
39+
environment_id = [for env in data.retool_environments.all.environments : env.id if env.name == "staging"][0]
40+
41+
options = jsonencode({
42+
host = "staging-db.example.com"
43+
port = 5432
44+
database = "staging_db"
45+
user = "staging_user"
46+
password = "staging_password"
47+
ssl = true
48+
})
49+
50+
lifecycle {
51+
ignore_changes = [options]
52+
}
53+
}
54+

internal/provider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/tryretool/terraform-provider-retool/internal/provider/folder"
2525
"github.com/tryretool/terraform-provider-retool/internal/provider/group"
2626
"github.com/tryretool/terraform-provider-retool/internal/provider/permissions"
27+
"github.com/tryretool/terraform-provider-retool/internal/provider/resourceconfiguration"
2728
"github.com/tryretool/terraform-provider-retool/internal/provider/retoolresource"
2829
"github.com/tryretool/terraform-provider-retool/internal/provider/sourcecontrol"
2930
"github.com/tryretool/terraform-provider-retool/internal/provider/sourcecontrolsettings"
@@ -304,6 +305,7 @@ func (p *retoolProvider) Resources(_ context.Context) []func() resource.Resource
304305
folder.NewResource,
305306
group.NewResource,
306307
permissions.NewResource,
308+
resourceconfiguration.NewResource,
307309
retoolresource.NewResource,
308310
space.NewResource,
309311
sso.NewResource,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package resourceconfiguration
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
7+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
8+
)
9+
10+
// jsonSemanticEqualsPlanModifier is a plan modifier that suppresses diffs when JSON is semantically equal.
11+
type jsonSemanticEqualsPlanModifier struct{}
12+
13+
// Description returns a human-readable description of the plan modifier.
14+
func (m jsonSemanticEqualsPlanModifier) Description(_ context.Context) string {
15+
return "Suppresses diff when JSON values are semantically equal"
16+
}
17+
18+
// MarkdownDescription returns a markdown description of the plan modifier.
19+
func (m jsonSemanticEqualsPlanModifier) MarkdownDescription(_ context.Context) string {
20+
return "Suppresses diff when JSON values are semantically equal"
21+
}
22+
23+
// PlanModifyString implements the plan modification logic.
24+
func (m jsonSemanticEqualsPlanModifier) PlanModifyString(_ context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
25+
// Do nothing if there is no state value.
26+
if req.StateValue.IsNull() {
27+
return
28+
}
29+
30+
// Do nothing if there is no plan value.
31+
if req.PlanValue.IsUnknown() || req.PlanValue.IsNull() {
32+
return
33+
}
34+
35+
// Do nothing if the values are already equal.
36+
if req.StateValue.Equal(req.PlanValue) {
37+
return
38+
}
39+
40+
// Parse both JSON values.
41+
var stateJSON, planJSON interface{}
42+
if err := json.Unmarshal([]byte(req.StateValue.ValueString()), &stateJSON); err != nil {
43+
// If we can't parse the state value, let Terraform handle the diff.
44+
return
45+
}
46+
if err := json.Unmarshal([]byte(req.PlanValue.ValueString()), &planJSON); err != nil {
47+
// If we can't parse the plan value, let Terraform handle the diff.
48+
return
49+
}
50+
51+
// Check if the plan JSON contains all the fields from the state JSON with the same values.
52+
// This allows the API to add additional fields without causing a diff.
53+
if jsonContains(stateJSON, planJSON) {
54+
// Values are semantically equal, use the state value to suppress the diff.
55+
resp.PlanValue = req.StateValue
56+
}
57+
}
58+
59+
// jsonContains checks if container contains all fields from subset with matching values.
60+
// This is a simplified check that works for our use case.
61+
func jsonContains(container, subset interface{}) bool {
62+
switch subsetVal := subset.(type) {
63+
case map[string]interface{}:
64+
containerMap, ok := container.(map[string]interface{})
65+
if !ok {
66+
return false
67+
}
68+
// Check that all fields in subset exist in container with matching values.
69+
for key, subVal := range subsetVal {
70+
contVal, exists := containerMap[key]
71+
if !exists {
72+
// Subset has a field that container doesn't have.
73+
return false
74+
}
75+
if !jsonContains(contVal, subVal) {
76+
return false
77+
}
78+
}
79+
return true
80+
case []interface{}:
81+
containerSlice, ok := container.([]interface{})
82+
if !ok || len(containerSlice) != len(subsetVal) {
83+
return false
84+
}
85+
// Arrays must match exactly (same length and same values in same order).
86+
for i, subVal := range subsetVal {
87+
if !jsonContains(containerSlice[i], subVal) {
88+
return false
89+
}
90+
}
91+
return true
92+
default:
93+
// For primitive values, use direct comparison.
94+
return container == subset
95+
}
96+
}
97+
98+
// JSONSemanticEquals returns a plan modifier that suppresses diffs when JSON is semantically equal.
99+
func JSONSemanticEquals() planmodifier.String {
100+
return jsonSemanticEqualsPlanModifier{}
101+
}

0 commit comments

Comments
 (0)