Skip to content

Commit 2aa33a7

Browse files
authored
User datasource (#333)
1 parent de6406f commit 2aa33a7

File tree

6 files changed

+454
-0
lines changed

6 files changed

+454
-0
lines changed

internal/provider/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,8 @@ func (p *TerraformCloudProvider) DataSources(ctx context.Context) []func() datas
187187
NewServiceAccountsDataSource,
188188
NewNamespaceDataSource,
189189
NewServiceAccountDataSource,
190+
NewUserDataSource,
191+
NewUsersDataSource,
190192
NewSCIMGroupDataSource,
191193
}
192194
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/datasource"
7+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
8+
cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1"
9+
10+
"github.com/temporalio/terraform-provider-temporalcloud/internal/client"
11+
)
12+
13+
type (
14+
userDataSource struct {
15+
client *client.Client
16+
}
17+
)
18+
19+
var (
20+
_ datasource.DataSource = (*userDataSource)(nil)
21+
_ datasource.DataSourceWithConfigure = (*userDataSource)(nil)
22+
)
23+
24+
func NewUserDataSource() datasource.DataSource {
25+
return &userDataSource{}
26+
}
27+
28+
func (d *userDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
29+
resp.TypeName = req.ProviderTypeName + "_user"
30+
}
31+
32+
func (d *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
33+
if req.ProviderData == nil {
34+
return
35+
}
36+
37+
client, ok := req.ProviderData.(*client.Client)
38+
if !ok {
39+
resp.Diagnostics.AddError(
40+
"Unexpected Data Source Configure Type",
41+
"Expected *client.Client, got: %T. Please report this issue to the provider developers.",
42+
)
43+
return
44+
}
45+
46+
d.client = client
47+
}
48+
49+
func (d *userDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
50+
resp.Schema = schema.Schema{
51+
Description: "Fetches details about a User.",
52+
Attributes: userSchema(true),
53+
}
54+
}
55+
56+
func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
57+
var input userDataModel
58+
resp.Diagnostics.Append(req.Config.Get(ctx, &input)...)
59+
if resp.Diagnostics.HasError() {
60+
return
61+
}
62+
63+
if len(input.ID.ValueString()) == 0 {
64+
resp.Diagnostics.AddError("invalid user id", "user id is required")
65+
return
66+
}
67+
68+
saResp, err := d.client.CloudService().GetUser(ctx, &cloudservicev1.GetUserRequest{
69+
UserId: input.ID.ValueString(),
70+
})
71+
if err != nil {
72+
resp.Diagnostics.AddError("Unable to fetch user", err.Error())
73+
return
74+
}
75+
76+
saDataModel, diags := userToUserDataModel(ctx, saResp.GetUser())
77+
resp.Diagnostics.Append(diags...)
78+
if resp.Diagnostics.HasError() {
79+
return
80+
}
81+
82+
diags = resp.State.Set(ctx, &saDataModel)
83+
resp.Diagnostics.Append(diags...)
84+
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
7+
"github.com/hashicorp/terraform-plugin-framework/diag"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
identityv1 "go.temporal.io/cloud-sdk/api/identity/v1"
10+
11+
"github.com/temporalio/terraform-provider-temporalcloud/internal/provider/enums"
12+
internaltypes "github.com/temporalio/terraform-provider-temporalcloud/internal/types"
13+
)
14+
15+
type (
16+
usersDataModel struct {
17+
ID types.String `tfsdk:"id"`
18+
Users []userDataModel `tfsdk:"users"`
19+
}
20+
21+
userDataModel struct {
22+
ID types.String `tfsdk:"id"`
23+
Email types.String `tfsdk:"email"`
24+
State types.String `tfsdk:"state"`
25+
AccountAccess internaltypes.CaseInsensitiveStringValue `tfsdk:"account_access"`
26+
NamespaceAccesses types.Set `tfsdk:"namespace_accesses"`
27+
CreatedAt types.String `tfsdk:"created_at"`
28+
UpdatedAt types.String `tfsdk:"updated_at"`
29+
}
30+
31+
userNSAccessModel struct {
32+
NamespaceID types.String `tfsdk:"namespace_id"`
33+
Permission types.String `tfsdk:"permission"`
34+
}
35+
)
36+
37+
func userToUserDataModel(ctx context.Context, sa *identityv1.User) (*userDataModel, diag.Diagnostics) {
38+
var diags diag.Diagnostics
39+
stateStr, err := enums.FromResourceState(sa.State)
40+
if err != nil {
41+
diags.AddError("Unable to convert user state", err.Error())
42+
return nil, diags
43+
}
44+
45+
userModel := &userDataModel{
46+
ID: types.StringValue(sa.Id),
47+
Email: types.StringValue(sa.GetSpec().GetEmail()),
48+
State: types.StringValue(stateStr),
49+
CreatedAt: types.StringValue(sa.GetCreatedTime().AsTime().GoString()),
50+
UpdatedAt: types.StringValue(sa.GetLastModifiedTime().AsTime().GoString()),
51+
}
52+
53+
role, err := enums.FromAccountAccessRole(sa.GetSpec().GetAccess().GetAccountAccess().GetRole())
54+
if err != nil {
55+
diags.AddError("Failed to convert account access role", err.Error())
56+
return nil, diags
57+
}
58+
59+
userModel.AccountAccess = internaltypes.CaseInsensitiveString(role)
60+
61+
namespaceAccesses := types.SetNull(types.ObjectType{AttrTypes: userNamespaceAccessAttrs})
62+
63+
if len(sa.GetSpec().GetAccess().GetNamespaceAccesses()) > 0 {
64+
namespaceAccessObjects := make([]types.Object, 0)
65+
for ns, namespaceAccess := range sa.GetSpec().GetAccess().GetNamespaceAccesses() {
66+
permission, err := enums.FromNamespaceAccessPermission(namespaceAccess.GetPermission())
67+
if err != nil {
68+
diags.AddError("Failed to convert namespace access permission", err.Error())
69+
return nil, diags
70+
}
71+
72+
model := userNSAccessModel{
73+
NamespaceID: types.StringValue(ns),
74+
Permission: types.StringValue(permission),
75+
}
76+
obj, d := types.ObjectValueFrom(ctx, userNamespaceAccessAttrs, model)
77+
diags.Append(d...)
78+
if diags.HasError() {
79+
return nil, diags
80+
}
81+
82+
namespaceAccessObjects = append(namespaceAccessObjects, obj)
83+
}
84+
85+
accesses, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: userNamespaceAccessAttrs}, namespaceAccessObjects)
86+
diags.Append(d...)
87+
if diags.HasError() {
88+
return nil, diags
89+
}
90+
namespaceAccesses = accesses
91+
}
92+
userModel.NamespaceAccesses = namespaceAccesses
93+
94+
return userModel, diags
95+
}
96+
97+
func userSchema(idRequired bool) map[string]schema.Attribute {
98+
idAttribute := schema.StringAttribute{
99+
Description: "The unique identifier of the User.",
100+
}
101+
102+
switch idRequired {
103+
case true:
104+
idAttribute.Required = true
105+
case false:
106+
idAttribute.Computed = true
107+
}
108+
109+
return map[string]schema.Attribute{
110+
"id": idAttribute,
111+
"email": schema.StringAttribute{
112+
Description: "The email of the User.",
113+
Computed: true,
114+
},
115+
"state": schema.StringAttribute{
116+
Description: "The current state of the User.",
117+
Computed: true,
118+
},
119+
"account_access": schema.StringAttribute{
120+
CustomType: internaltypes.CaseInsensitiveStringType{},
121+
Description: "The role on the account. Must be one of admin, developer, or read (case-insensitive).",
122+
Computed: true,
123+
},
124+
"namespace_accesses": schema.SetNestedAttribute{
125+
Description: "The set of namespace permissions for this user, including each namespace and its role.",
126+
Optional: true,
127+
Computed: true,
128+
NestedObject: schema.NestedAttributeObject{
129+
Attributes: map[string]schema.Attribute{
130+
"namespace_id": schema.StringAttribute{
131+
Description: "The namespace to assign permissions to.",
132+
Computed: true,
133+
},
134+
"permission": schema.StringAttribute{
135+
CustomType: types.StringType,
136+
Description: "The permission to assign. Must be one of admin, write, or read (case-insensitive)",
137+
Computed: true,
138+
},
139+
},
140+
},
141+
},
142+
"created_at": schema.StringAttribute{
143+
Description: "The creation time of the User.",
144+
Computed: true,
145+
},
146+
"updated_at": schema.StringAttribute{
147+
Description: "The last update time of the User.",
148+
Computed: true,
149+
},
150+
}
151+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package provider
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/hashicorp/terraform-plugin-testing/terraform"
8+
9+
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
10+
)
11+
12+
func TestAccDataSource_User(t *testing.T) {
13+
email := createRandomEmail()
14+
config := func(email string, role string) string {
15+
return fmt.Sprintf(`
16+
provider "temporalcloud" {
17+
18+
}
19+
20+
resource "temporalcloud_user" "terraform" {
21+
email = "%s"
22+
account_access = "%s"
23+
}
24+
25+
data "temporalcloud_user" "terraform" {
26+
id = temporalcloud_user.terraform.id
27+
}
28+
29+
output "user" {
30+
value = data.temporalcloud_user.terraform
31+
}
32+
`, email, role)
33+
}
34+
35+
resource.Test(t, resource.TestCase{
36+
PreCheck: func() {
37+
testAccPreCheck(t)
38+
},
39+
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
40+
Steps: []resource.TestStep{
41+
{
42+
Config: config(email, "read"),
43+
Check: func(s *terraform.State) error {
44+
output, ok := s.RootModule().Outputs["user"]
45+
if !ok {
46+
return fmt.Errorf("missing expected output")
47+
}
48+
49+
outputValue, ok := output.Value.(map[string]any)
50+
if !ok {
51+
return fmt.Errorf("expected value to be map")
52+
}
53+
54+
outputName, ok := outputValue["email"].(string)
55+
if !ok {
56+
return fmt.Errorf("expected value to be a string")
57+
}
58+
if outputName != email {
59+
return fmt.Errorf("expected user email to be %s, got %s", email, outputName)
60+
}
61+
62+
outputState, ok := outputValue["state"].(string)
63+
if !ok {
64+
return fmt.Errorf("expected value to be a string")
65+
}
66+
if outputState != "active" {
67+
return fmt.Errorf("expected user state to be active, got %s", outputState)
68+
}
69+
70+
return nil
71+
},
72+
},
73+
},
74+
})
75+
}

0 commit comments

Comments
 (0)