Skip to content

Commit b269d72

Browse files
authored
[feat][gov] Add data source + resource for users (#54)
* 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 data source + resource for users * docs * lint * lint * lint * still fighting with local linting.... * one more * better docs
1 parent 3e517a6 commit b269d72

File tree

16 files changed

+1580
-0
lines changed

16 files changed

+1580
-0
lines changed

docs/data-sources/users.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
---
2+
page_title: "Data Source: retool_users"
3+
description: |-
4+
Users data source allows you to retrieve a list of users in Retool.
5+
---
6+
7+
# Data Source: retool_users
8+
9+
Users data source allows you to retrieve a list of users in Retool.
10+
11+
## Example Usage
12+
13+
```terraform
14+
data "retool_users" "all_users" {}
15+
16+
output "all_users" {
17+
value = data.retool_users.all_users.users
18+
}
19+
20+
# Filter active users
21+
output "active_users" {
22+
value = [for user in data.retool_users.all_users.users : user if user.active]
23+
}
24+
25+
# Filter admin users
26+
output "admin_users" {
27+
value = [for user in data.retool_users.all_users.users : user if user.is_admin]
28+
}
29+
```
30+
31+
<!-- schema generated by tfplugindocs -->
32+
## Schema
33+
34+
### Read-Only
35+
36+
- `users` (Attributes List) A list of users (see [below for nested schema](#nestedatt--users))
37+
38+
<a id="nestedatt--users"></a>
39+
### Nested Schema for `users`
40+
41+
Read-Only:
42+
43+
- `active` (Boolean) Whether the user is active or not.
44+
- `created_at` (String) The timestamp when the user was created.
45+
- `email` (String) The email address of the user.
46+
- `first_name` (String) The first name of the user.
47+
- `id` (String) The ID of the user. Currently this is the same as legacy_id but will change in the future.
48+
- `is_admin` (Boolean) Whether the user is an admin or not.
49+
- `last_active` (String) The timestamp when the user was last active.
50+
- `last_name` (String) The last name of the user.
51+
- `legacy_id` (String) The legacy ID of the user.
52+
- `metadata` (String) JSON string containing custom metadata for the user.
53+
- `two_factor_auth_enabled` (Boolean) Whether two-factor authentication is enabled for this user.
54+
- `user_type` (String) The user type.
55+
56+

docs/resources/user.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---
2+
page_title: "Resource: retool_user"
3+
description: |-
4+
A user represents an individual with access to the Retool instance. Users can be granted access to apps, resources, and workflows through group memberships and permissions.
5+
Note: Deleting a user resource sets the user to inactive (active: false) rather than permanently removing them from Retool.
6+
---
7+
8+
# Resource: retool_user
9+
10+
A user represents an individual with access to the Retool instance. Users can be granted access to apps, resources, and workflows through group memberships and permissions.
11+
12+
Note: Deleting a user resource sets the user to inactive (active: false) rather than permanently removing them from Retool.
13+
14+
## Example Usage
15+
16+
```terraform
17+
resource "retool_user" "example" {
18+
email = "user@example.com"
19+
first_name = "John"
20+
last_name = "Doe"
21+
active = true
22+
metadata = "{\"department\":\"engineering\",\"team\":\"platform\"}"
23+
}
24+
```
25+
26+
<!-- schema generated by tfplugindocs -->
27+
## Schema
28+
29+
### Required
30+
31+
- `email` (String) The email address of the user. Cannot be changed after creation.
32+
- `first_name` (String) The first name of the user.
33+
- `last_name` (String) The last name of the user.
34+
35+
### Optional
36+
37+
- `active` (Boolean) Whether the user is active or not. Default is true.
38+
- `metadata` (String) JSON string containing custom metadata for the user.
39+
- `user_type` (String) The user type. Accepted values vary by Retool instance configuration.
40+
41+
### Read-Only
42+
43+
- `created_at` (String) The timestamp when the user was created.
44+
- `id` (String) The ID of the user. Currently this is the same as legacy_id but will change in the future.
45+
- `is_admin` (Boolean) Whether the user is an admin or not.
46+
- `last_active` (String) The timestamp when the user was last active.
47+
- `legacy_id` (String) The legacy ID of the user.
48+
- `two_factor_auth_enabled` (Boolean) Whether two-factor authentication is enabled for this user.
49+
50+
## Import
51+
52+
Import is supported using the following syntax:
53+
54+
```shell
55+
# User can be imported by specifying the id
56+
terraform import retool_user.example "abc123"
57+
```
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
data "retool_users" "all_users" {}
2+
3+
output "all_users" {
4+
value = data.retool_users.all_users.users
5+
}
6+
7+
# Filter active users
8+
output "active_users" {
9+
value = [for user in data.retool_users.all_users.users : user if user.active]
10+
}
11+
12+
# Filter admin users
13+
output "admin_users" {
14+
value = [for user in data.retool_users.all_users.users : user if user.is_admin]
15+
}
16+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# User can be imported by specifying the id
2+
terraform import retool_user.example "abc123"
3+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
resource "retool_user" "example" {
2+
email = "user@example.com"
3+
first_name = "John"
4+
last_name = "Doe"
5+
active = true
6+
metadata = "{\"department\":\"engineering\",\"team\":\"platform\"}"
7+
}
8+

internal/provider/provider.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"github.com/tryretool/terraform-provider-retool/internal/provider/sourcecontrolsettings"
3030
"github.com/tryretool/terraform-provider-retool/internal/provider/space"
3131
"github.com/tryretool/terraform-provider-retool/internal/provider/sso"
32+
"github.com/tryretool/terraform-provider-retool/internal/provider/user"
3233
"github.com/tryretool/terraform-provider-retool/internal/provider/utils"
3334
"github.com/tryretool/terraform-provider-retool/internal/sdk/api"
3435
)
@@ -293,6 +294,7 @@ func (p *retoolProvider) DataSources(_ context.Context) []func() datasource.Data
293294
folder.NewDataSource,
294295
group.NewDataSource,
295296
environments.NewDataSource,
297+
user.NewDataSource,
296298
}
297299
}
298300

@@ -308,5 +310,6 @@ func (p *retoolProvider) Resources(_ context.Context) []func() resource.Resource
308310
sourcecontrol.NewResource,
309311
sourcecontrolsettings.NewResource,
310312
configurationvariable.NewResource,
313+
user.NewResource,
311314
}
312315
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Package user provides implementation of the User resource and Users data source.
2+
package user
3+
4+
import (
5+
"context"
6+
"fmt"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/datasource"
9+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/types"
11+
"github.com/hashicorp/terraform-plugin-log/tflog"
12+
13+
"github.com/tryretool/terraform-provider-retool/internal/provider/utils"
14+
"github.com/tryretool/terraform-provider-retool/internal/sdk/api"
15+
)
16+
17+
// NewDataSource creates a new data source for users.
18+
func NewDataSource() datasource.DataSource {
19+
return &usersDataSource{}
20+
}
21+
22+
// Ensure the implementation satisfies the expected interfaces.
23+
var (
24+
_ datasource.DataSource = &usersDataSource{}
25+
_ datasource.DataSourceWithConfigure = &usersDataSource{}
26+
)
27+
28+
type usersDataSource struct {
29+
client *api.APIClient
30+
}
31+
32+
type usersDataSourceModel struct {
33+
Users []userDataSourceModel `tfsdk:"users"`
34+
}
35+
36+
type userDataSourceModel struct {
37+
ID types.String `tfsdk:"id"`
38+
LegacyID types.String `tfsdk:"legacy_id"`
39+
Email types.String `tfsdk:"email"`
40+
FirstName types.String `tfsdk:"first_name"`
41+
LastName types.String `tfsdk:"last_name"`
42+
Active types.Bool `tfsdk:"active"`
43+
Metadata types.String `tfsdk:"metadata"`
44+
UserType types.String `tfsdk:"user_type"`
45+
CreatedAt types.String `tfsdk:"created_at"`
46+
LastActive types.String `tfsdk:"last_active"`
47+
IsAdmin types.Bool `tfsdk:"is_admin"`
48+
TwoFactorAuthEnabled types.Bool `tfsdk:"two_factor_auth_enabled"`
49+
}
50+
51+
// Configure adds the provider configured client to the data source.
52+
func (d *usersDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
53+
// Add a nil check when handling ProviderData because Terraform
54+
// sets that data after it calls the ConfigureProvider RPC.
55+
if req.ProviderData == nil {
56+
return
57+
}
58+
59+
providerData, ok := req.ProviderData.(*utils.ProviderData)
60+
if !ok {
61+
resp.Diagnostics.AddError(
62+
"Unexpected Data Source Configure Type",
63+
fmt.Sprintf("Expected *utils.ProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData),
64+
)
65+
return
66+
}
67+
68+
d.client = providerData.Client
69+
}
70+
71+
func (d *usersDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
72+
resp.TypeName = req.ProviderTypeName + "_users"
73+
}
74+
75+
func (d *usersDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
76+
resp.Schema = schema.Schema{
77+
Description: "Users data source allows you to retrieve a list of users in Retool.",
78+
Attributes: map[string]schema.Attribute{
79+
"users": schema.ListNestedAttribute{
80+
Computed: true,
81+
Description: "A list of users",
82+
NestedObject: schema.NestedAttributeObject{
83+
Attributes: map[string]schema.Attribute{
84+
"id": schema.StringAttribute{
85+
Computed: true,
86+
Description: "The ID of the user. Currently this is the same as legacy_id but will change in the future.",
87+
},
88+
"legacy_id": schema.StringAttribute{
89+
Computed: true,
90+
Description: "The legacy ID of the user.",
91+
},
92+
"email": schema.StringAttribute{
93+
Computed: true,
94+
Description: "The email address of the user.",
95+
},
96+
"first_name": schema.StringAttribute{
97+
Computed: true,
98+
Description: "The first name of the user.",
99+
},
100+
"last_name": schema.StringAttribute{
101+
Computed: true,
102+
Description: "The last name of the user.",
103+
},
104+
"active": schema.BoolAttribute{
105+
Computed: true,
106+
Description: "Whether the user is active or not.",
107+
},
108+
"metadata": schema.StringAttribute{
109+
Computed: true,
110+
Description: "JSON string containing custom metadata for the user.",
111+
},
112+
"user_type": schema.StringAttribute{
113+
Computed: true,
114+
Description: "The user type.",
115+
},
116+
"created_at": schema.StringAttribute{
117+
Computed: true,
118+
Description: "The timestamp when the user was created.",
119+
},
120+
"last_active": schema.StringAttribute{
121+
Computed: true,
122+
Description: "The timestamp when the user was last active.",
123+
},
124+
"is_admin": schema.BoolAttribute{
125+
Computed: true,
126+
Description: "Whether the user is an admin or not.",
127+
},
128+
"two_factor_auth_enabled": schema.BoolAttribute{
129+
Computed: true,
130+
Description: "Whether two-factor authentication is enabled for this user.",
131+
},
132+
},
133+
},
134+
},
135+
},
136+
}
137+
}
138+
139+
func (d *usersDataSource) Read(ctx context.Context, _ datasource.ReadRequest, resp *datasource.ReadResponse) {
140+
var state usersDataSourceModel
141+
142+
users, httpResponse, err := d.client.UsersAPI.UsersGet(ctx).Execute()
143+
if err != nil {
144+
resp.Diagnostics.AddError(
145+
"Unable to Read Users via Retool API",
146+
err.Error(),
147+
)
148+
tflog.Error(ctx, "Error reading users", utils.AddHTTPStatusCode(map[string]any{"error": err.Error()}, httpResponse))
149+
return
150+
}
151+
tflog.Info(ctx, "Read Users via Retool API", map[string]any{"num_users": len(users.Data)})
152+
153+
for _, user := range users.Data {
154+
userModel := userDataSourceModel{
155+
ID: types.StringValue(user.Id),
156+
LegacyID: types.StringValue(fmt.Sprintf("%.0f", user.LegacyId)),
157+
Email: types.StringValue(user.Email),
158+
Active: types.BoolValue(user.Active),
159+
IsAdmin: types.BoolValue(user.IsAdmin),
160+
UserType: types.StringValue(user.UserType),
161+
TwoFactorAuthEnabled: types.BoolValue(user.TwoFactorAuthEnabled),
162+
CreatedAt: types.StringValue(user.CreatedAt.String()),
163+
}
164+
165+
// Handle nullable fields.
166+
if user.FirstName.Get() != nil {
167+
userModel.FirstName = types.StringValue(*user.FirstName.Get())
168+
} else {
169+
userModel.FirstName = types.StringNull()
170+
}
171+
172+
if user.LastName.Get() != nil {
173+
userModel.LastName = types.StringValue(*user.LastName.Get())
174+
} else {
175+
userModel.LastName = types.StringNull()
176+
}
177+
178+
if user.LastActive.Get() != nil {
179+
userModel.LastActive = types.StringValue(user.LastActive.Get().String())
180+
} else {
181+
userModel.LastActive = types.StringNull()
182+
}
183+
184+
// Handle metadata.
185+
if len(user.Metadata) > 0 {
186+
metadataStr, err := utils.MapToJSONString(user.Metadata)
187+
if err != nil {
188+
resp.Diagnostics.AddError(
189+
"Error serializing metadata",
190+
fmt.Sprintf("Could not serialize metadata for user %s: %s", user.Email, err.Error()),
191+
)
192+
return
193+
}
194+
userModel.Metadata = types.StringValue(metadataStr)
195+
} else {
196+
userModel.Metadata = types.StringNull()
197+
}
198+
199+
state.Users = append(state.Users, userModel)
200+
}
201+
202+
// Set state.
203+
diags := resp.State.Set(ctx, &state)
204+
resp.Diagnostics.Append(diags...)
205+
if resp.Diagnostics.HasError() {
206+
return
207+
}
208+
}

0 commit comments

Comments
 (0)