diff --git a/docs/data-sources/oncall_user.md b/docs/data-sources/oncall_user.md index 1e244a857..0941bccbb 100644 --- a/docs/data-sources/oncall_user.md +++ b/docs/data-sources/oncall_user.md @@ -28,5 +28,5 @@ data "grafana_oncall_user" "alex" { ### Read-Only - `email` (String) The email of the user. -- `id` (String) The ID of this resource. +- `id` (String) The ID of the user. - `role` (String) The role of the user. diff --git a/docs/data-sources/oncall_users.md b/docs/data-sources/oncall_users.md new file mode 100644 index 000000000..7a78f87dd --- /dev/null +++ b/docs/data-sources/oncall_users.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "grafana_oncall_users Data Source - terraform-provider-grafana" +subcategory: "OnCall" +description: |- + HTTP API https://grafana.com/docs/oncall/latest/oncall-api-reference/users/ +--- + +# grafana_oncall_users (Data Source) + +* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/users/) + + + + +## Schema + +### Read-Only + +- `id` (String) The ID of this resource. +- `users` (List of Object) (see [below for nested schema](#nestedatt--users)) + + +### Nested Schema for `users` + +Read-Only: + +- `email` (String) +- `id` (String) +- `role` (String) +- `username` (String) diff --git a/internal/resources/oncall/data_source_user.go b/internal/resources/oncall/data_source_user.go index 106178df1..a82e72d0f 100644 --- a/internal/resources/oncall/data_source_user.go +++ b/internal/resources/oncall/data_source_user.go @@ -2,64 +2,80 @@ package oncall import ( "context" + "fmt" onCallAPI "github.com/grafana/amixr-api-go-client" "github.com/grafana/terraform-provider-grafana/v3/internal/common" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) +var dataSourceUserName = "grafana_oncall_user" + func dataSourceUser() *common.DataSource { - schema := &schema.Resource{ - Description: ` -* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/users/) -`, - ReadContext: withClient[schema.ReadContextFunc](dataSourceUserRead), - Schema: map[string]*schema.Schema{ - "username": { - Type: schema.TypeString, + return common.NewDataSource(common.CategoryOnCall, dataSourceUserName, &userDataSource{}) +} + +type userDataSource struct { + basePluginFrameworkDataSource +} + +func (r *userDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = dataSourceUserName +} + +func (r *userDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/users/)", + Attributes: map[string]schema.Attribute{ + "username": schema.StringAttribute{ Required: true, Description: "The username of the user.", }, - "email": { - Type: schema.TypeString, + "id": schema.StringAttribute{ + Computed: true, + Description: "The ID of the user.", + }, + "email": schema.StringAttribute{ Computed: true, Description: "The email of the user.", }, - "role": { - Type: schema.TypeString, + "role": schema.StringAttribute{ Computed: true, Description: "The role of the user.", }, }, } - return common.NewLegacySDKDataSource(common.CategoryOnCall, "grafana_oncall_user", schema) } -func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics { - options := &onCallAPI.ListUserOptions{} - usernameData := d.Get("username").(string) - - options.Username = usernameData +func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Read Terraform state data into the model + var data userDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) - usersResponse, _, err := client.Users.ListUsers(options) + options := &onCallAPI.ListUserOptions{ + Username: data.Username.ValueString(), + } + usersResponse, _, err := r.client.Users.ListUsers(options) if err != nil { - return diag.FromErr(err) + resp.Diagnostics.AddError("Failed to list users", err.Error()) + return } if len(usersResponse.Users) == 0 { - return diag.Errorf("couldn't find a user matching: %s", options.Username) + resp.Diagnostics.AddError("user not found", fmt.Sprintf("couldn't find a user matching: %s", options.Username)) + return } else if len(usersResponse.Users) != 1 { - return diag.Errorf("more than one user found matching: %s", options.Username) + resp.Diagnostics.AddError("more than one user found", fmt.Sprintf("more than one user found matching: %s", options.Username)) + return } user := usersResponse.Users[0] + data.ID = basetypes.NewStringValue(user.ID) + data.Email = basetypes.NewStringValue(user.Email) + data.Role = basetypes.NewStringValue(user.Role) - d.Set("email", user.Email) - d.Set("username", user.Username) - d.Set("role", user.Role) - - d.SetId(user.ID) - - return nil + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) } diff --git a/internal/resources/oncall/data_source_users.go b/internal/resources/oncall/data_source_users.go new file mode 100644 index 000000000..f468e453c --- /dev/null +++ b/internal/resources/oncall/data_source_users.go @@ -0,0 +1,101 @@ +package oncall + +import ( + "context" + + onCallAPI "github.com/grafana/amixr-api-go-client" + "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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-framework/types/basetypes" +) + +var dataSourceUsersName = "grafana_oncall_users" + +func dataSourceUsers() *common.DataSource { + return common.NewDataSource(common.CategoryOnCall, dataSourceUsersName, &usersDataSource{}) +} + +type usersDataSource struct { + basePluginFrameworkDataSource +} + +func (r *usersDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = dataSourceUsersName +} + +func (r *usersDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "* [HTTP API](https://grafana.com/docs/oncall/latest/oncall-api-reference/users/)", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "users": schema.ListAttribute{ + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "username": types.StringType, + "email": types.StringType, + "role": types.StringType, + }, + }, + Computed: true, + }, + }, + } +} + +type userDataSourceModel struct { + ID basetypes.StringValue `tfsdk:"id"` + Username basetypes.StringValue `tfsdk:"username"` + Email basetypes.StringValue `tfsdk:"email"` + Role basetypes.StringValue `tfsdk:"role"` +} + +type usersDataSourceModel struct { + ID basetypes.StringValue `tfsdk:"id"` + Users []userDataSourceModel `tfsdk:"users"` +} + +func (r *usersDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Read Terraform state data into the model + var data usersDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + allUsers := []userDataSourceModel{} + page := 1 + for { + options := &onCallAPI.ListUserOptions{ + ListOptions: onCallAPI.ListOptions{ + Page: page, + }, + } + usersResponse, _, err := r.client.Users.ListUsers(options) + if err != nil { + resp.Diagnostics.AddError("Failed to list users", err.Error()) + return + } + + for _, user := range usersResponse.Users { + allUsers = append(allUsers, userDataSourceModel{ + ID: basetypes.NewStringValue(user.ID), + Username: basetypes.NewStringValue(user.Username), + Email: basetypes.NewStringValue(user.Email), + Role: basetypes.NewStringValue(user.Role), + }) + } + + if usersResponse.PaginatedResponse.Next == nil { + break + } + } + + data.ID = basetypes.NewStringValue("oncall_users") // singleton + data.Users = allUsers + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} diff --git a/internal/resources/oncall/resource_user_notification_rule_test.go b/internal/resources/oncall/resource_user_notification_rule_test.go index 6bcf9def0..36058dae5 100644 --- a/internal/resources/oncall/resource_user_notification_rule_test.go +++ b/internal/resources/oncall/resource_user_notification_rule_test.go @@ -15,12 +15,8 @@ func TestAccUserNotificationRule_basic(t *testing.T) { testutils.CheckCloudInstanceTestsEnabled(t) var ( - // We need an actual user to test the resource - // This is a user created from my personal email, but it can be replaced by any existing user - userID = "joeyorlando" resourceName = "grafana_oncall_user_notification_rule.test-acc-user_notification_rule" - - testSteps []resource.TestStep + testSteps []resource.TestStep ruleTypes = []string{ "wait", @@ -41,7 +37,6 @@ func TestAccUserNotificationRule_basic(t *testing.T) { config string testCheckFuncFunctions = []resource.TestCheckFunc{ testAccCheckOnCallUserNotificationRuleResourceExists(resourceName), - // resource.TestCheckResourceAttr(resourceName, "user_id", userID), resource.TestCheckResourceAttr(resourceName, "position", "1"), resource.TestCheckResourceAttr(resourceName, "type", ruleType), resource.TestCheckResourceAttr(resourceName, "important", fmt.Sprintf("%t", important)), @@ -49,10 +44,10 @@ func TestAccUserNotificationRule_basic(t *testing.T) { ) if ruleType == "wait" { - config = testAccOnCallUserNotificationRuleWait(userID, important) + config = testAccOnCallUserNotificationRuleWait(important) testCheckFuncFunctions = append(testCheckFuncFunctions, resource.TestCheckResourceAttr(resourceName, "duration", "300")) } else { - config = testAccOnCallUserNotificationRuleNotificationStep(ruleType, userID, important) + config = testAccOnCallUserNotificationRuleNotificationStep(ruleType, important) } testSteps = append(testSteps, resource.TestStep{ @@ -89,35 +84,33 @@ func testAccCheckOnCallUserNotificationRuleResourceDestroy(s *terraform.State) e return nil } -func testAccOnCallUserNotificationRuleWait(userName string, important bool) string { +func testAccOnCallUserNotificationRuleWait(important bool) string { return fmt.Sprintf(` -data "grafana_oncall_user" "user" { - username = "%s" -} +# Grab the first user from the full list of users +data "grafana_oncall_users" "all" {} resource "grafana_oncall_user_notification_rule" "test-acc-user_notification_rule" { - user_id = data.grafana_oncall_user.user.id + user_id = data.grafana_oncall_users.all.users[0].id type = "wait" position = 1 duration = 300 important = %t } -`, userName, important) +`, important) } -func testAccOnCallUserNotificationRuleNotificationStep(ruleType, userName string, important bool) string { +func testAccOnCallUserNotificationRuleNotificationStep(ruleType string, important bool) string { return fmt.Sprintf(` -data "grafana_oncall_user" "user" { - username = "%s" -} +# Grab the first user from the full list of users +data "grafana_oncall_users" "all" {} resource "grafana_oncall_user_notification_rule" "test-acc-user_notification_rule" { - user_id = data.grafana_oncall_user.user.id + user_id = data.grafana_oncall_users.all.users[0].id type = "%s" position = 1 important = %t } -`, userName, ruleType, important) +`, ruleType, important) } func testAccCheckOnCallUserNotificationRuleResourceExists(name string) resource.TestCheckFunc { diff --git a/internal/resources/oncall/resources.go b/internal/resources/oncall/resources.go index a971818fc..2411949bc 100644 --- a/internal/resources/oncall/resources.go +++ b/internal/resources/oncall/resources.go @@ -6,6 +6,7 @@ import ( onCallAPI "github.com/grafana/amixr-api-go-client" "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -38,6 +39,30 @@ func (r *basePluginFrameworkResource) Configure(ctx context.Context, req resourc r.client = client.OnCallClient } +type basePluginFrameworkDataSource struct { + client *onCallAPI.Client +} + +func (r *basePluginFrameworkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once + if req.ProviderData == nil || r.client != nil { + return + } + + client, ok := req.ProviderData.(*common.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client.OnCallClient +} + type crudWithClientFunc func(ctx context.Context, d *schema.ResourceData, client *onCallAPI.Client) diag.Diagnostics func withClient[T schema.CreateContextFunc | schema.UpdateContextFunc | schema.ReadContextFunc | schema.DeleteContextFunc](f crudWithClientFunc) T { @@ -51,7 +76,6 @@ func withClient[T schema.CreateContextFunc | schema.UpdateContextFunc | schema.R } var DataSources = []*common.DataSource{ - dataSourceUser(), dataSourceEscalationChain(), dataSourceSchedule(), dataSourceSlackChannel(), @@ -59,6 +83,8 @@ var DataSources = []*common.DataSource{ dataSourceUserGroup(), dataSourceTeam(), dataSourceIntegration(), + dataSourceUser(), + dataSourceUsers(), } var Resources = []*common.Resource{