diff --git a/docs/data-sources/all_usergroups.md b/docs/data-sources/all_usergroups.md new file mode 100644 index 0000000..0fb9d7f --- /dev/null +++ b/docs/data-sources/all_usergroups.md @@ -0,0 +1,33 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "slack_all_usergroups Data Source - slack" +subcategory: "" +description: |- + Retrieve all Slack user groups. +--- + +# slack_all_usergroups (Data Source) + +Retrieve all Slack user groups. + + + + +## Schema + +### Read-Only + +- `total_usergroups` (Number) Total number of user groups retrieved. +- `usergroups` (Attributes List) List of Slack user groups. (see [below for nested schema](#nestedatt--usergroups)) + + +### Nested Schema for `usergroups` + +Read-Only: + +- `channels` (List of String) Channels shared by the user group. +- `description` (String) Description of the user group. +- `handle` (String) Handle of the user group (unique identifier). +- `id` (String) User group's Slack ID. +- `name` (String) Name of the user group. +- `users` (List of String) List of user IDs in the user group. diff --git a/docs/data-sources/all_users.md b/docs/data-sources/all_users.md new file mode 100644 index 0000000..50fc63c --- /dev/null +++ b/docs/data-sources/all_users.md @@ -0,0 +1,30 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "slack_all_users Data Source - slack" +subcategory: "" +description: |- + +--- + +# slack_all_users (Data Source) + + + + + + +## Schema + +### Read-Only + +- `total_users` (Number) Number of users returned. +- `users` (Attributes List) List of activated and non-bot Slack users. (see [below for nested schema](#nestedatt--users)) + + +### Nested Schema for `users` + +Read-Only: + +- `email` (String) User's email address. +- `id` (String) User's Slack ID. +- `name` (String) User's name. diff --git a/docs/data-sources/user.md b/docs/data-sources/user.md new file mode 100644 index 0000000..8fb0cb4 --- /dev/null +++ b/docs/data-sources/user.md @@ -0,0 +1,25 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "slack_user Data Source - slack" +subcategory: "" +description: |- + Retrieve Slack user information. Either id or email must be specified, but not both. +--- + +# slack_user (Data Source) + +Retrieve Slack user information. Either `id` or `email` must be specified, but not both. + + + + +## Schema + +### Optional + +- `email` (String) Email of the user to look up. +- `id` (String) Slack user ID to look up. + +### Read-Only + +- `name` (String) User's name. diff --git a/docs/data-sources/user_data.md b/docs/data-sources/user_data.md deleted file mode 100644 index 01aa7a9..0000000 --- a/docs/data-sources/user_data.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -# generated by https://github.com/hashicorp/terraform-plugin-docs -page_title: "slack_user_data Data Source - slack" -subcategory: "" -description: |- - Retrieve Slack user information. ---- - -# slack_user_data (Data Source) - -Retrieve Slack user information. - - - - -## Schema - -### Required - -- `user_id` (String) Slack user ID to look up. - -### Read-Only - -- `display_name` (String) User's display name. -- `email` (String) Email of the user. -- `id` (String) Unique identifier for Terraform state. -- `real_name` (String) User's real name. diff --git a/docs/index.md b/docs/index.md index 7ec4404..637e08c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,6 @@ provider "scaffolding" { ## Schema -### Required +### Optional -- `slack_token` (String, Sensitive) Slack token to authenticate API calls. +- `slack_token` (String, Sensitive) Slack token to authenticate API calls. Can also be set with the `SLACK_TOKEN` environment variable. diff --git a/go.mod b/go.mod index a5816e0..b2e2357 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.4 require ( github.com/hashicorp/terraform-plugin-framework v1.13.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.16.0 github.com/hashicorp/terraform-plugin-go v0.25.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.11.0 diff --git a/go.sum b/go.sum index 8680fdf..e8342f7 100644 --- a/go.sum +++ b/go.sum @@ -81,6 +81,8 @@ github.com/hashicorp/terraform-json v0.23.0 h1:sniCkExU4iKtTADReHzACkk8fnpQXrdD2 github.com/hashicorp/terraform-json v0.23.0/go.mod h1:MHdXbBAbSg0GvzuWazEGKAn/cyNfIB7mN6y7KJN6y2c= github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.0 h1:O9QqGoYDzQT7lwTXUsZEtgabeWW96zUBh47Smn2lkFA= +github.com/hashicorp/terraform-plugin-framework-validators v0.16.0/go.mod h1:Bh89/hNmqsEWug4/XWKYBwtnw3tbz5BAy1L1OgvbIaY= github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/internal/provider/data_source_all_usergroups.go b/internal/provider/data_source_all_usergroups.go new file mode 100644 index 0000000..4504347 --- /dev/null +++ b/internal/provider/data_source_all_usergroups.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/slack-go/slack" +) + +var _ datasource.DataSource = &AllUserGroupsDataSource{} + +func NewAllUserGroupsDataSource() datasource.DataSource { + return &AllUserGroupsDataSource{} +} + +type AllUserGroupsDataSource struct { + client *slack.Client +} + +type AllUserGroupsDataSourceModel struct { + TotalUserGroups types.Int64 `tfsdk:"total_usergroups"` + UserGroups []AllUserGroupsDataSourceGroupItem `tfsdk:"usergroups"` +} + +type AllUserGroupsDataSourceGroupItem struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Handle types.String `tfsdk:"handle"` + Channels []types.String `tfsdk:"channels"` + Users []types.String `tfsdk:"users"` +} + +func (d *AllUserGroupsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_all_usergroups" +} + +func (d *AllUserGroupsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Retrieve all Slack user groups.", + Attributes: map[string]schema.Attribute{ + "total_usergroups": schema.Int64Attribute{ + Description: "Total number of user groups retrieved.", + Computed: true, + }, + "usergroups": schema.ListNestedAttribute{ + Description: "List of Slack user groups.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "User group's Slack ID.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the user group.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the user group.", + Computed: true, + }, + "handle": schema.StringAttribute{ + Description: "Handle of the user group (unique identifier).", + Computed: true, + }, + "channels": schema.ListAttribute{ + Description: "Channels shared by the user group.", + ElementType: types.StringType, + Computed: true, + }, + "users": schema.ListAttribute{ + Description: "List of user IDs in the user group.", + ElementType: types.StringType, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (d *AllUserGroupsDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + providerData, ok := req.ProviderData.(*SlackProviderData) + if !ok || providerData.Client == nil { + resp.Diagnostics.AddError( + "Invalid Provider Data", + fmt.Sprintf("Expected *SlackProviderData with initialized client, got: %T", req.ProviderData), + ) + return + } + + d.client = providerData.Client +} + +func (d *AllUserGroupsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data AllUserGroupsDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + userGroups, err := d.client.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(true)) + if err != nil { + resp.Diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to fetch Slack user groups: %s", err), + ) + return + } + + tflog.Trace(ctx, "Fetched Slack user groups", map[string]any{"total_usergroups": len(userGroups)}) + + var resultingList []AllUserGroupsDataSourceGroupItem + for _, group := range userGroups { + groupItem := AllUserGroupsDataSourceGroupItem{ + ID: types.StringValue(group.ID), + Name: types.StringValue(group.Name), + Description: types.StringValue(group.Description), + Handle: types.StringValue(group.Handle), + } + + channels := make([]types.String, len(group.Prefs.Channels)) + for i, ch := range group.Prefs.Channels { + channels[i] = types.StringValue(ch) + } + groupItem.Channels = channels + + users := make([]types.String, len(group.Users)) + for i, u := range group.Users { + users[i] = types.StringValue(u) + } + groupItem.Users = users + + resultingList = append(resultingList, groupItem) + } + + data.UserGroups = resultingList + data.TotalUserGroups = types.Int64Value(int64(len(resultingList))) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_source_all_users.go b/internal/provider/data_source_all_users.go new file mode 100644 index 0000000..c28c0cb --- /dev/null +++ b/internal/provider/data_source_all_users.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/slack-go/slack" +) + +var _ datasource.DataSource = &AllUsersDataSource{} + +func NewAllUsersDataSource() datasource.DataSource { + return &AllUsersDataSource{} +} + +type AllUsersDataSource struct { + client *slack.Client +} + +type AllUsersDataSourceModel struct { + Totalusers types.Int64 `tfsdk:"total_users"` + Users []AllUsersDataSourceModelUserItem `tfsdk:"users"` +} + +type AllUsersDataSourceModelUserItem struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` +} + +func (d *AllUsersDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_all_users" +} + +func (d *AllUsersDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "total_users": schema.Int64Attribute{ + Description: "Number of users returned.", + Computed: true, + }, + "users": schema.ListNestedAttribute{ + Description: "List of activated and non-bot Slack users.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "User's Slack ID.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "User's name.", + Computed: true, + }, + "email": schema.StringAttribute{ + Description: "User's email address.", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func (d *AllUsersDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + providerData, ok := req.ProviderData.(*SlackProviderData) + if !ok || providerData.Client == nil { + resp.Diagnostics.AddError( + "Invalid Provider Data", + fmt.Sprintf("Expected *SlackProviderData with initialized client, got: %T", req.ProviderData), + ) + return + } + d.client = providerData.Client +} + +func (d *AllUsersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data AllUsersDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + users, err := d.client.GetUsersContext(ctx) + if err != nil { + resp.Diagnostics.AddError( + "Client Error", + fmt.Sprintf("Unable to fetch Slack users: %s", err), + ) + return + } + + tflog.Trace(ctx, "Fetched Slack users", map[string]any{"total_users": len(users)}) + + var resultingList []AllUsersDataSourceModelUserItem + for _, user := range users { + if !user.Deleted && !user.IsBot { + resultingList = append(resultingList, AllUsersDataSourceModelUserItem{ + ID: types.StringValue(user.ID), + Name: types.StringValue(user.Name), + Email: types.StringValue(user.Profile.Email), + }) + } + } + + data.Users = resultingList + data.Totalusers = types.Int64Value(int64(len(resultingList))) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/data_source_user.go b/internal/provider/data_source_user.go index 536a508..163fceb 100644 --- a/internal/provider/data_source_user.go +++ b/internal/provider/data_source_user.go @@ -7,8 +7,11 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/slack-go/slack" @@ -25,39 +28,35 @@ type UserDataSource struct { } type UserDataSourceModel struct { - UserID types.String `tfsdk:"user_id"` - Email types.String `tfsdk:"email"` - RealName types.String `tfsdk:"real_name"` - DisplayName types.String `tfsdk:"display_name"` - ID types.String `tfsdk:"id"` + Email types.String `tfsdk:"email"` + Name types.String `tfsdk:"name"` + ID types.String `tfsdk:"id"` } func (d *UserDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_user_data" + resp.TypeName = req.ProviderTypeName + "_user" } func (d *UserDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "Retrieve Slack user information.", + MarkdownDescription: "Retrieve Slack user information. Either `id` or `email` must be specified, but not both.", Attributes: map[string]schema.Attribute{ - "user_id": schema.StringAttribute{ + "id": schema.StringAttribute{ MarkdownDescription: "Slack user ID to look up.", - Required: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("email")), + }, }, "email": schema.StringAttribute{ - MarkdownDescription: "Email of the user.", - Computed: true, - }, - "real_name": schema.StringAttribute{ - MarkdownDescription: "User's real name.", - Computed: true, + MarkdownDescription: "Email of the user to look up.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf(path.MatchRelative().AtParent().AtName("id")), + }, }, - "display_name": schema.StringAttribute{ - MarkdownDescription: "User's display name.", - Computed: true, - }, - "id": schema.StringAttribute{ - MarkdownDescription: "Unique identifier for Terraform state.", + "name": schema.StringAttribute{ + MarkdownDescription: "User's name.", Computed: true, }, }, @@ -90,7 +89,17 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } - user, err := d.client.GetUserInfo(data.UserID.ValueString()) + var ( + user *slack.User + err error + ) + + if !data.ID.IsNull() { + user, err = d.client.GetUserInfo(data.ID.ValueString()) + } else { + user, err = d.client.GetUserByEmail(data.Email.ValueString()) + } + if err != nil { resp.Diagnostics.AddError( "Client Error", @@ -99,12 +108,19 @@ func (d *UserDataSource) Read(ctx context.Context, req datasource.ReadRequest, r return } + if user.Deleted { + resp.Diagnostics.AddError( + "User is deactivated", + "User is deactivated in Slack", + ) + return + } + data.Email = types.StringValue(user.Profile.Email) - data.RealName = types.StringValue(user.RealName) - data.DisplayName = types.StringValue(user.Profile.DisplayName) + data.Name = types.StringValue(user.Name) data.ID = types.StringValue(user.ID) - tflog.Trace(ctx, "Fetched Slack user data", map[string]any{"user_id": user.ID}) + tflog.Trace(ctx, "Fetched Slack user data", map[string]any{"id": user.ID}) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index ea2d037..a7bce2d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -6,6 +6,7 @@ package provider import ( "context" "fmt" + "os" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/function" @@ -41,8 +42,8 @@ func (p *SlackProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "slack_token": schema.StringAttribute{ - MarkdownDescription: "Slack token to authenticate API calls.", - Required: true, + MarkdownDescription: "Slack token to authenticate API calls. Can also be set with the `SLACK_TOKEN` environment variable.", + Optional: true, Sensitive: true, }, }, @@ -56,15 +57,23 @@ func (p *SlackProvider) Configure(ctx context.Context, req provider.ConfigureReq return } - if data.SlackToken.IsNull() || data.SlackToken.IsUnknown() { - resp.Diagnostics.AddError( - "Missing Slack Token", - "The `slack_token` must be provided to authenticate API calls.", - ) - return + slackToken := data.SlackToken.ValueString() + + // If slack_token was not set in the provider block, check the environment variable. + if slackToken == "" { + envToken, ok := os.LookupEnv("SLACK_TOKEN") + if !ok || envToken == "" { + resp.Diagnostics.AddError( + "Missing Slack Token", + "`slack_token` was not set in the provider block, and `SLACK_TOKEN` is not set in the environment.", + ) + return + } + slackToken = envToken } + tflog.Info(ctx, "Configuring slack client") - client := slack.New(data.SlackToken.ValueString()) + client := slack.New(slackToken) _, err := client.AuthTest() if err != nil { resp.Diagnostics.AddError( @@ -87,6 +96,8 @@ func (p *SlackProvider) Resources(ctx context.Context) []func() resource.Resourc func (p *SlackProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewUserDataSource, + NewAllUsersDataSource, + NewAllUserGroupsDataSource, } }