From a92c7524d5dff6a4607c174a4a2a286c3d4e918e Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Mon, 9 Sep 2024 11:51:43 +0100 Subject: [PATCH 01/10] WIP --- .../resource_data_source_config_lbac_rules.go | 141 ++++++++++++++++++ ...source_data_source_permission_item_test.go | 108 -------------- 2 files changed, 141 insertions(+), 108 deletions(-) create mode 100644 internal/resources/grafana/resource_data_source_config_lbac_rules.go delete mode 100644 internal/resources/grafana/resource_data_source_permission_item_test.go diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go new file mode 100644 index 000000000..5b94ec449 --- /dev/null +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -0,0 +1,141 @@ +package grafana + +import ( + "context" + + "github.com/grafana/grafana-openapi-client-go/client" + "github.com/grafana/grafana-openapi-client-go/client/enterprise" + "github.com/grafana/grafana-openapi-client-go/models" + "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + resourceDataSourceConfigLBACRulesName = "grafana_data_source_config_lbac_rules" + resourceDataSourceConfigLBACRulesID = common.NewResourceID(common.StringIDField("datasourceUID")) +) + +func NewDataSourceConfigLBACRulesResource() resource.Resource { + return &resourceDataSourceConfigLBACRules{} +} + +type resourceDataSourceConfigLBACRulesModel struct { + ID types.String `tfsdk:"id"` + DatasourceUID types.String `tfsdk:"datasource_uid"` + Rules types.Map `tfsdk:"rules"` +} + +type resourceDataSourceConfigLBACRules struct { + client *client.GrafanaHTTPAPI +} + +func (r *resourceDataSourceConfigLBACRules) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = resourceDataSourceConfigLBACRulesName +} + +func (r *resourceDataSourceConfigLBACRules) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Manages LBAC rules for a data source.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "datasource_uid": schema.StringAttribute{ + Required: true, + Description: "The UID of the datasource.", + }, + "rules": schema.MapAttribute{ + Required: true, + Description: "LBAC rules for the data source. Map of team IDs to lists of rule strings.", + ElementType: types.ListType{ + ElemType: types.StringType, + }, + }, + }, + } +} + +func (r *resourceDataSourceConfigLBACRules) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + r.client = req.ProviderData.(*client.GrafanaHTTPAPI) +} + +func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // Not implemented, but satisfies the interface + resp.Diagnostics.AddWarning("Operation not supported", "Create operation is not supported for LBAC rules") +} + +func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data resourceDataSourceConfigLBACRulesModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + getResp, err := r.client.Enterprise.GetTeamLBACRulesAPI(data.DatasourceUID.ValueString()) + if err != nil { + resp.Diagnostics.AddError("Failed to get LBAC rules", err.Error()) + return + } + + rules := make(map[string]types.List) + for teamID, teamRules := range getResp.Payload.Rules { + stringRules := make([]types.String, len(teamRules.Rules)) + for i, rule := range teamRules.Rules { + stringRules[i] = types.StringValue(rule) + } + rules[string(teamID)] = types.ListValueMust(types.StringType, stringRules) + } + + data.Rules = types.MapValueMust(types.ListType{ElemType: types.StringType}, rules) + data.ID = data.DatasourceUID + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data resourceDataSourceConfigLBACRulesModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + updatedRules := make(map[string][]string) + data.Rules.ElementsAs(ctx, &updatedRules, false) + + apiRules := make([]*models.TeamLBACRule, 0) + for teamID, rules := range updatedRules { + teamRule := &models.TeamLBACRule{ + TeamID: teamID, + Rules: rules, // Change this line + } + apiRules = append(apiRules, teamRule) + } + + _, err := r.client.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ + UID: data.DatasourceUID.ValueString(), + Context: ctx, + Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, + }) + if err != nil { + resp.Diagnostics.AddError("Failed to update LBAC rules", err.Error()) + return + } + + data.ID = data.DatasourceUID + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // Not implemented, but satisfies the interface + resp.Diagnostics.AddWarning("Operation not supported", "Delete operation is not supported for LBAC rules") +} + +func (r *resourceDataSourceConfigLBACRules) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("datasource_uid"), req, resp) +} diff --git a/internal/resources/grafana/resource_data_source_permission_item_test.go b/internal/resources/grafana/resource_data_source_permission_item_test.go deleted file mode 100644 index 16a49adc3..000000000 --- a/internal/resources/grafana/resource_data_source_permission_item_test.go +++ /dev/null @@ -1,108 +0,0 @@ -package grafana_test - -import ( - "fmt" - "testing" - - "github.com/grafana/grafana-openapi-client-go/models" - "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" -) - -func TestAccDatasourcePermissionItem_basic(t *testing.T) { - testutils.CheckEnterpriseTestsEnabled(t, ">=9.0.0") - - var ds models.DataSource - name := acctest.RandString(10) - - resource.ParallelTest(t, resource.TestCase{ - ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, - Steps: []resource.TestStep{ - { - Config: testAccDatasourcePermissionItem(name), - Check: resource.ComposeAggregateTestCheckFunc( - datasourcePermissionsCheckExists.exists("grafana_data_source.foo", &ds), - ), - }, - { - ResourceName: "grafana_data_source_permission_item.team", - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "grafana_data_source_permission_item.user", - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "grafana_data_source_permission_item.role", - ImportState: true, - ImportStateVerify: true, - }, - { - ResourceName: "grafana_data_source_permission_item.sa", - ImportState: true, - ImportStateVerify: true, - }, - }, - }) -} - -func testAccDatasourcePermissionItem(name string) string { - return fmt.Sprintf(` -resource "grafana_team" "team" { - name = "%[1]s" -} - -resource "grafana_data_source" "foo" { - name = "%[1]s" - type = "cloudwatch" - - json_data_encoded = jsonencode({ - defaultRegion = "us-east-1" - authType = "keys" - }) - - secure_json_data_encoded = jsonencode({ - accessKey = "123" - secretKey = "456" - }) -} - -resource "grafana_user" "user" { - name = "%[1]s" - email = "%[1]s@example.com" - login = "%[1]s" - password = "hunter2" -} - -resource "grafana_service_account" "sa" { - name = "%[1]s" - role = "Viewer" -} - -resource "grafana_data_source_permission_item" "team" { - datasource_uid = grafana_data_source.foo.uid - team = grafana_team.team.id - permission = "Edit" -} - -resource "grafana_data_source_permission_item" "user" { - datasource_uid = grafana_data_source.foo.uid - user = grafana_user.user.id - permission = "Edit" -} - -resource "grafana_data_source_permission_item" "role" { - datasource_uid = grafana_data_source.foo.uid - role = "Viewer" - permission = "Query" -} - -resource "grafana_data_source_permission_item" "sa" { - datasource_uid = grafana_data_source.foo.uid - user = grafana_service_account.sa.id - permission = "Query" -}`, name) -} From be0706fb49615b0bfec5fe0e03dbc173a3c06118 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Mon, 9 Sep 2024 16:38:37 +0100 Subject: [PATCH 02/10] WIP --- .../resource_alerting_message_template.go | 3 +- .../resource_data_source_config_lbac_rules.go | 29 ++++- ...urce_data_source_config_lbac_rules_test.go | 82 +++++++++++++ .../resources/grafana/resource_folder_test.go | 111 +++++++++--------- internal/resources/grafana/resources.go | 1 + 5 files changed, 163 insertions(+), 63 deletions(-) create mode 100644 internal/resources/grafana/resource_data_source_config_lbac_rules_test.go diff --git a/internal/resources/grafana/resource_alerting_message_template.go b/internal/resources/grafana/resource_alerting_message_template.go index e356a18e6..a3bb335ff 100644 --- a/internal/resources/grafana/resource_alerting_message_template.go +++ b/internal/resources/grafana/resource_alerting_message_template.go @@ -145,7 +145,8 @@ func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta int func deleteMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { client, _, name := OAPIClientFromExistingOrgResource(meta, data.Id()) - _, err := client.Provisioning.DeleteTemplate(name) + params := provisioning.NewDeleteTemplateParams().WithName(name) + _, err := client.Provisioning.DeleteTemplate(params) diag, _ := common.CheckReadError("message template", data, err) return diag } diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go index 5b94ec449..6b4bd1362 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -2,24 +2,37 @@ package grafana import ( "context" + "strconv" "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-openapi-client-go/client/enterprise" "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) +var ( + // Check interface + _ resource.ResourceWithImportState = (*resourceDataSourceConfigLBACRules)(nil) +) + var ( resourceDataSourceConfigLBACRulesName = "grafana_data_source_config_lbac_rules" - resourceDataSourceConfigLBACRulesID = common.NewResourceID(common.StringIDField("datasourceUID")) + resourceDataSourceConfigLBACRulesID = common.NewResourceID(common.StringIDField("datasource_uid")) ) -func NewDataSourceConfigLBACRulesResource() resource.Resource { - return &resourceDataSourceConfigLBACRules{} +func makeResourceDataSourceConfigLBACRules() *common.Resource { + resourceStruct := &resourceDataSourceConfigLBACRules{} + return common.NewResource( + common.CategoryGrafanaEnterprise, + resourceDatasourcePermissionItemName, + resourceDatasourcePermissionItemID, + resourceStruct, + ) } type resourceDataSourceConfigLBACRulesModel struct { @@ -85,14 +98,18 @@ func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resour rules := make(map[string]types.List) for teamID, teamRules := range getResp.Payload.Rules { - stringRules := make([]types.String, len(teamRules.Rules)) + stringRules := make([]attr.Value, len(teamRules.Rules)) for i, rule := range teamRules.Rules { stringRules[i] = types.StringValue(rule) } - rules[string(teamID)] = types.ListValueMust(types.StringType, stringRules) + rules[strconv.Itoa(int(teamID))] = types.ListValueMust(types.StringType, stringRules) } - data.Rules = types.MapValueMust(types.ListType{ElemType: types.StringType}, rules) + rulesAttr := make(map[string]attr.Value) + for k, v := range rules { + rulesAttr[k] = v + } + data.Rules = types.MapValueMust(types.ListType{ElemType: types.StringType}, rulesAttr) data.ID = data.DatasourceUID resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go new file mode 100644 index 000000000..8fbc1b36d --- /dev/null +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go @@ -0,0 +1,82 @@ +package grafana_test + +import ( + "fmt" + "testing" + + "github.com/grafana/grafana-openapi-client-go/models" + "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { + testutils.CheckEnterpriseTestsEnabled(t, ">=9.0.0") + + var ds models.DataSource + name := acctest.RandString(10) + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceConfigLBACRules(name), + Check: resource.ComposeAggregateTestCheckFunc( + datasourceCheckExists.exists("grafana_data_source.test", &ds), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.%", "2"), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team1.#", "2"), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team1.0", "{ foo != \"bar\", foo !~ \"baz\" }"), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team1.1", "{ foo = \"qux\", bar ~ \"quux\" }"), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team2.#", "2"), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team2.0", "{ foo != \"bar\", foo !~ \"baz\" }"), + resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team2.1", "{ foo = \"qux\", bar ~ \"quux\" }"), + ), + }, + { + ResourceName: "grafana_data_source_config_lbac_rules.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccDataSourceConfigLBACRules(name string) string { + return fmt.Sprintf(` +resource "grafana_data_source" "test" { + name = "%[1]s" + type = "loki" + + json_data_encoded = jsonencode({ + defaultRegion = "us-east-1" + authType = "keys" + }) + + secure_json_data_encoded = jsonencode({ + accessKey = "123" + secretKey = "456" + }) +} + +resource "grafana_team" "team1" { + name = "%[1]s-team1" +} + +resource "grafana_team" "team2" { + name = "%[1]s-team2" +} + +resource "grafana_data_source_config_lbac_rules" "test" { + datasource_uid = grafana_data_source.test.uid + rules = { + "${grafana_team.team1.id}" = [ + "{ foo != \"bar\", foo !~ \"baz\" }", + "{ foo = \"qux\", bar ~ \"quux\" }" + ], + "${grafana_team.team2.id}" = [ + "{ foo != \"bar\", foo !~ \"baz\" }", + "{ foo = \"qux\", bar ~ \"quux\" }" + ] + } +}`, name) +} diff --git a/internal/resources/grafana/resource_folder_test.go b/internal/resources/grafana/resource_folder_test.go index 30d027d3f..7a2528ae8 100644 --- a/internal/resources/grafana/resource_folder_test.go +++ b/internal/resources/grafana/resource_folder_test.go @@ -2,7 +2,6 @@ package grafana_test import ( "fmt" - "net/http" "os" "regexp" "strings" @@ -267,65 +266,65 @@ func TestAccFolder_PreventDeletionNested(t *testing.T) { } // This is a bug in Grafana, not the provider. It was fixed in 9.2.7+ and 9.3.0+, this test will check for regressions -func TestAccFolder_createFromDifferentRoles(t *testing.T) { - testutils.CheckOSSTestsEnabled(t, ">=9.2.7") +// func TestAccFolder_createFromDifferentRoles(t *testing.T) { +// testutils.CheckOSSTestsEnabled(t, ">=9.2.7") - for _, tc := range []struct { - role string - expectError *regexp.Regexp - }{ - { - role: "Viewer", - expectError: regexp.MustCompile(fmt.Sprint(http.StatusForbidden)), - }, - { - role: "Editor", - expectError: nil, - }, - } { - t.Run(tc.role, func(t *testing.T) { - var folder models.Folder - var name = acctest.RandomWithPrefix(tc.role + "-key") +// for _, tc := range []struct { +// role string +// expectError *regexp.Regexp +// }{ +// { +// role: "Viewer", +// expectError: regexp.MustCompile(fmt.Sprint(http.StatusForbidden)), +// }, +// { +// role: "Editor", +// expectError: nil, +// }, +// } { +// t.Run(tc.role, func(t *testing.T) { +// var folder models.Folder +// var name = acctest.RandomWithPrefix(tc.role + "-key") - // Create an API key with the correct role and inject it in envvars. This auth will be used when the test runs - client := grafanaTestClient() - resp, err := client.APIKeys.AddAPIkey(&models.AddAPIKeyCommand{ - Name: name, - Role: tc.role, - }) - if err != nil { - t.Fatal(err) - } - defer client.APIKeys.DeleteAPIkey(resp.Payload.ID) - oldValue := os.Getenv("GRAFANA_AUTH") - defer os.Setenv("GRAFANA_AUTH", oldValue) - os.Setenv("GRAFANA_AUTH", resp.Payload.Key) +// // Create an API key with the correct role and inject it in envvars. This auth will be used when the test runs +// client := grafanaTestClient() +// resp, err := client.APIKeys.AddAPIkey(&models.AddAPIKeyCommand{ +// Name: name, +// Role: tc.role, +// }) +// if err != nil { +// t.Fatal(err) +// } +// defer client.APIKeys.DeleteAPIkey(resp.Payload.ID) +// oldValue := os.Getenv("GRAFANA_AUTH") +// defer os.Setenv("GRAFANA_AUTH", oldValue) +// os.Setenv("GRAFANA_AUTH", resp.Payload.Key) - config := fmt.Sprintf(` - resource "grafana_folder" "bar" { - title = "%[1]s" - }`, name) +// config := fmt.Sprintf(` +// resource "grafana_folder" "bar" { +// title = "%[1]s" +// }`, name) - // Do not make parallel, fiddling with auth will break other tests that run in parallel - resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, - CheckDestroy: folderCheckExists.destroyed(&folder, nil), - Steps: []resource.TestStep{ - { - Config: config, - ExpectError: tc.expectError, - Check: resource.ComposeTestCheckFunc( - folderCheckExists.exists("grafana_folder.bar", &folder), - resource.TestMatchResourceAttr("grafana_folder.bar", "id", defaultOrgIDRegexp), - resource.TestMatchResourceAttr("grafana_folder.bar", "uid", common.UIDRegexp), - resource.TestCheckResourceAttr("grafana_folder.bar", "title", name), - ), - }, - }, - }) - }) - } -} +// // Do not make parallel, fiddling with auth will break other tests that run in parallel +// resource.Test(t, resource.TestCase{ +// ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, +// CheckDestroy: folderCheckExists.destroyed(&folder, nil), +// Steps: []resource.TestStep{ +// { +// Config: config, +// ExpectError: tc.expectError, +// Check: resource.ComposeTestCheckFunc( +// folderCheckExists.exists("grafana_folder.bar", &folder), +// resource.TestMatchResourceAttr("grafana_folder.bar", "id", defaultOrgIDRegexp), +// resource.TestMatchResourceAttr("grafana_folder.bar", "uid", common.UIDRegexp), +// resource.TestCheckResourceAttr("grafana_folder.bar", "title", name), +// ), +// }, +// }, +// }) +// }) +// } +// } func testAccFolderWasntRecreated(rn string, oldFolder *models.Folder) resource.TestCheckFunc { return func(s *terraform.State) error { diff --git a/internal/resources/grafana/resources.go b/internal/resources/grafana/resources.go index 99cf44a5e..f78f0d723 100644 --- a/internal/resources/grafana/resources.go +++ b/internal/resources/grafana/resources.go @@ -105,6 +105,7 @@ var Resources = addValidationToResources( makeResourceFolderPermissionItem(), makeResourceDashboardPermissionItem(), makeResourceDatasourcePermissionItem(), + makeResourceDataSourceConfigLBACRules(), makeResourceRoleAssignmentItem(), makeResourceServiceAccountPermissionItem(), resourceAnnotation(), From ee0fcfb0ea23222f9c6d604d0c1dd16b82a5edd0 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 10 Sep 2024 16:45:08 +0100 Subject: [PATCH 03/10] WIP --- .../resource_data_source_config_lbac_rules.go | 129 +++++++++++++----- ...urce_data_source_config_lbac_rules_test.go | 91 +++++++----- 2 files changed, 149 insertions(+), 71 deletions(-) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go index 6b4bd1362..bc4f9c8ec 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -2,17 +2,18 @@ package grafana import ( "context" + "encoding/json" + "fmt" "strconv" - "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-openapi-client-go/client/enterprise" "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/terraform-provider-grafana/v3/internal/common" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" ) var ( @@ -29,20 +30,25 @@ func makeResourceDataSourceConfigLBACRules() *common.Resource { resourceStruct := &resourceDataSourceConfigLBACRules{} return common.NewResource( common.CategoryGrafanaEnterprise, - resourceDatasourcePermissionItemName, - resourceDatasourcePermissionItemID, + resourceDataSourceConfigLBACRulesName, + resourceDataSourceConfigLBACRulesID, resourceStruct, ) } +type LBACRule struct { + TeamID types.String `tfsdk:"team_id"` + Rules []types.String `tfsdk:"rules"` +} + type resourceDataSourceConfigLBACRulesModel struct { ID types.String `tfsdk:"id"` DatasourceUID types.String `tfsdk:"datasource_uid"` - Rules types.Map `tfsdk:"rules"` + Rules types.String `tfsdk:"rules"` } type resourceDataSourceConfigLBACRules struct { - client *client.GrafanaHTTPAPI + client *common.Client } func (r *resourceDataSourceConfigLBACRules) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { @@ -60,81 +66,129 @@ func (r *resourceDataSourceConfigLBACRules) Schema(_ context.Context, _ resource Required: true, Description: "The UID of the datasource.", }, - "rules": schema.MapAttribute{ + "rules": schema.StringAttribute{ Required: true, - Description: "LBAC rules for the data source. Map of team IDs to lists of rule strings.", - ElementType: types.ListType{ - ElemType: types.StringType, - }, + Description: "JSON-encoded LBAC rules for the data source. Map of team IDs to lists of rule strings.", }, }, } } -func (r *resourceDataSourceConfigLBACRules) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { +func (r *resourceDataSourceConfigLBACRules) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + tflog.Info(ctx, "Configuring LBAC rules") if req.ProviderData == nil { return } - r.client = req.ProviderData.(*client.GrafanaHTTPAPI) + r.client = req.ProviderData.(*common.Client) } func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - // Not implemented, but satisfies the interface - resp.Diagnostics.AddWarning("Operation not supported", "Create operation is not supported for LBAC rules") + tflog.Info(ctx, "Creating LBAC rules") + var data resourceDataSourceConfigLBACRulesModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "Creating LBAC rules", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + + var rulesMap map[string][]string + err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap) + if err != nil { + resp.Diagnostics.AddError("Invalid rules JSON", fmt.Sprintf("Failed to parse rules: %v", err)) + return + } + + apiRules := make([]*models.TeamLBACRule, 0, len(rulesMap)) + for teamID, rules := range rulesMap { + apiRules = append(apiRules, &models.TeamLBACRule{ + TeamID: teamID, + Rules: rules, + }) + } + _, err = r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ + UID: data.DatasourceUID.ValueString(), + Context: ctx, + Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, + }) + if err != nil { + resp.Diagnostics.AddError("Failed to create LBAC rules", err.Error()) + return + } + + tflog.Info(ctx, "LBAC rules created successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + + data.ID = data.DatasourceUID + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + tflog.Info(ctx, "Reading LBAC rules") var data resourceDataSourceConfigLBACRulesModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } - getResp, err := r.client.Enterprise.GetTeamLBACRulesAPI(data.DatasourceUID.ValueString()) + tflog.Info(ctx, "Reading LBAC rules", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + + getResp, err := r.client.GrafanaAPI.Enterprise.GetTeamLBACRulesAPI(data.DatasourceUID.ValueString()) if err != nil { resp.Diagnostics.AddError("Failed to get LBAC rules", err.Error()) return } - rules := make(map[string]types.List) + rulesMap := make(map[string][]string) for teamID, teamRules := range getResp.Payload.Rules { - stringRules := make([]attr.Value, len(teamRules.Rules)) - for i, rule := range teamRules.Rules { - stringRules[i] = types.StringValue(rule) - } - rules[strconv.Itoa(int(teamID))] = types.ListValueMust(types.StringType, stringRules) + strTeamID := strconv.FormatInt(int64(teamID), 10) + rulesMap[strTeamID] = teamRules.Rules } - rulesAttr := make(map[string]attr.Value) - for k, v := range rules { - rulesAttr[k] = v + rulesJSON, err := json.Marshal(rulesMap) + if err != nil { + resp.Diagnostics.AddError("Failed to encode rules", err.Error()) + return } - data.Rules = types.MapValueMust(types.ListType{ElemType: types.StringType}, rulesAttr) + + data.Rules = types.StringValue(string(rulesJSON)) data.ID = data.DatasourceUID + tflog.Info(ctx, "LBAC rules read successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + tflog.Info(ctx, "Updating LBAC rules") var data resourceDataSourceConfigLBACRulesModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } - updatedRules := make(map[string][]string) - data.Rules.ElementsAs(ctx, &updatedRules, false) + tflog.Info(ctx, "Updating LBAC rules", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - apiRules := make([]*models.TeamLBACRule, 0) - for teamID, rules := range updatedRules { - teamRule := &models.TeamLBACRule{ - TeamID: teamID, - Rules: rules, // Change this line + rulesMap := make(map[string][]string) + err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap) + if err != nil { + resp.Diagnostics.AddError("Invalid rules JSON", fmt.Sprintf("Failed to parse rules: %v", err)) + return + } + + apiRules := make([]*models.TeamLBACRule, 0, len(rulesMap)) + for teamID, rules := range rulesMap { + _, err := strconv.ParseInt(teamID, 10, 64) + if err != nil { + resp.Diagnostics.AddError("Invalid team ID", fmt.Sprintf("Team ID %s is not a valid integer", teamID)) + return } - apiRules = append(apiRules, teamRule) + apiRules = append(apiRules, &models.TeamLBACRule{ + TeamID: teamID, + Rules: rules, + }) } - _, err := r.client.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ + _, err = r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ UID: data.DatasourceUID.ValueString(), Context: ctx, Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, @@ -144,15 +198,18 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso return } + tflog.Info(ctx, "LBAC rules updated successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + data.ID = data.DatasourceUID resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - // Not implemented, but satisfies the interface + tflog.Warn(ctx, "Delete operation not supported for LBAC rules") resp.Diagnostics.AddWarning("Operation not supported", "Delete operation is not supported for LBAC rules") } func (r *resourceDataSourceConfigLBACRules) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + tflog.Info(ctx, "Importing LBAC rules", map[string]interface{}{"datasource_uid": req.ID}) resource.ImportStatePassthroughID(ctx, path.Root("datasource_uid"), req, resp) } diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go index 8fbc1b36d..da431e9bd 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go @@ -1,7 +1,10 @@ package grafana_test import ( + "encoding/json" "fmt" + "reflect" + "strings" "testing" "github.com/grafana/grafana-openapi-client-go/models" @@ -11,7 +14,7 @@ import ( ) func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { - testutils.CheckEnterpriseTestsEnabled(t, ">=9.0.0") + testutils.CheckEnterpriseTestsEnabled(t, ">=11.0.0") var ds models.DataSource name := acctest.RandString(10) @@ -23,20 +26,44 @@ func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { Config: testAccDataSourceConfigLBACRules(name), Check: resource.ComposeAggregateTestCheckFunc( datasourceCheckExists.exists("grafana_data_source.test", &ds), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.%", "2"), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team1.#", "2"), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team1.0", "{ foo != \"bar\", foo !~ \"baz\" }"), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team1.1", "{ foo = \"qux\", bar ~ \"quux\" }"), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team2.#", "2"), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team2.0", "{ foo != \"bar\", foo !~ \"baz\" }"), - resource.TestCheckResourceAttr("grafana_data_source_config_lbac_rules.test", "rules.team2.1", "{ foo = \"qux\", bar ~ \"quux\" }"), + resource.TestCheckResourceAttrSet("grafana_data_source_config_lbac_rules.test", "rules"), + resource.TestCheckResourceAttrWith("grafana_data_source_config_lbac_rules.test", "rules", func(value string) error { + var rulesMap map[string][]string + err := json.Unmarshal([]byte(value), &rulesMap) + if err != nil { + return fmt.Errorf("failed to parse rules JSON: %v", err) + } + + expectedRules := []string{ + "{ foo != \"bar\", foo !~ \"baz\" }", + "{ foo = \"qux\" }", + } + + if len(rulesMap) != 2 { + return fmt.Errorf("expected 2 team rules, got %d", len(rulesMap)) + } + + for teamID, teamRules := range rulesMap { + if !strings.HasPrefix(teamID, "1:") { + return fmt.Errorf("unexpected team ID format: %s", teamID) + } + if !reflect.DeepEqual(teamRules, expectedRules) { + return fmt.Errorf("for team %s, expected rules %v, got %v", teamID, expectedRules, teamRules) + } + } + + return nil + }), + resource.TestCheckResourceAttrWith("grafana_data_source.test", "json_data_encoded", func(value string) error { + var jsonData map[string]interface{} + err := json.Unmarshal([]byte(value), &jsonData) + if err != nil { + return fmt.Errorf("failed to parse json_data_encoded: %v", err) + } + return nil + }), ), }, - { - ResourceName: "grafana_data_source_config_lbac_rules.test", - ImportState: true, - ImportStateVerify: true, - }, }, }) } @@ -47,36 +74,30 @@ resource "grafana_data_source" "test" { name = "%[1]s" type = "loki" - json_data_encoded = jsonencode({ - defaultRegion = "us-east-1" - authType = "keys" - }) + basic_auth_enabled = true + basic_auth_username = "admin" - secure_json_data_encoded = jsonencode({ - accessKey = "123" - secretKey = "456" - }) + lifecycle { + ignore_changes = [json_data_encoded] + } } resource "grafana_team" "team1" { - name = "%[1]s-team1" + name = "team1" } resource "grafana_team" "team2" { - name = "%[1]s-team2" + name = "team2" } resource "grafana_data_source_config_lbac_rules" "test" { - datasource_uid = grafana_data_source.test.uid - rules = { - "${grafana_team.team1.id}" = [ - "{ foo != \"bar\", foo !~ \"baz\" }", - "{ foo = \"qux\", bar ~ \"quux\" }" - ], - "${grafana_team.team2.id}" = [ - "{ foo != \"bar\", foo !~ \"baz\" }", - "{ foo = \"qux\", bar ~ \"quux\" }" - ] - } -}`, name) + datasource_uid = grafana_data_source.test.uid + rules = { + "${grafana_team.team1.id}" = jsonencode([ + "{ foo != \"bar\", foo !~ \"baz\" }", + "{ foo = \"qux\" }" + ]) + } +} +`, name) } From d8c9ba1be221e67911e1da3304e66ba8c9172df4 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 11 Sep 2024 16:12:18 +0100 Subject: [PATCH 04/10] WIP --- .../resource_data_source_config_lbac_rules.go | 20 ++++++++--- ...urce_data_source_config_lbac_rules_test.go | 35 +++++++++---------- 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go index bc4f9c8ec..f66dc00a1 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -55,7 +55,8 @@ func (r *resourceDataSourceConfigLBACRules) Metadata(_ context.Context, req reso resp.TypeName = resourceDataSourceConfigLBACRulesName } -func (r *resourceDataSourceConfigLBACRules) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *resourceDataSourceConfigLBACRules) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + tflog.Info(ctx, "Creating LBAC rules Schema") resp.Schema = schema.Schema{ MarkdownDescription: "Manages LBAC rules for a data source.", Attributes: map[string]schema.Attribute{ @@ -106,6 +107,9 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso Rules: rules, }) } + + tflog.Info(ctx, "Creating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%+v", apiRules)}) + _, err = r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ UID: data.DatasourceUID.ValueString(), Context: ctx, @@ -118,6 +122,14 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso tflog.Info(ctx, "LBAC rules created successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + // TODO: + // fix teamid setting. + // fix id setting, id of the resource + // fix the lifecycle of the jsondata within grafana + // - make sure you cannot set teamhttpheaders from the json api (updateDatasource), ONLY through lbac/team api + // - magic diffsuprressfunc to ignore changes on json_data_encoded for the specific field teamHttpHeaders + // improvements: + // - make the api "for terraforM" not a json but a struct, to enfore types and schema data.ID = data.DatasourceUID resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -139,9 +151,8 @@ func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resour } rulesMap := make(map[string][]string) - for teamID, teamRules := range getResp.Payload.Rules { - strTeamID := strconv.FormatInt(int64(teamID), 10) - rulesMap[strTeamID] = teamRules.Rules + for _, rule := range getResp.Payload.Rules { + rulesMap[rule.TeamID] = rule.Rules } rulesJSON, err := json.Marshal(rulesMap) @@ -187,6 +198,7 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso Rules: rules, }) } + tflog.Info(ctx, "Updating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%v+", apiRules)}) _, err = r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ UID: data.DatasourceUID.ValueString(), diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go index da431e9bd..333e449f8 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "reflect" - "strings" "testing" "github.com/grafana/grafana-openapi-client-go/models" @@ -39,14 +38,14 @@ func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { "{ foo = \"qux\" }", } - if len(rulesMap) != 2 { - return fmt.Errorf("expected 2 team rules, got %d", len(rulesMap)) + if len(rulesMap) != 1 { + return fmt.Errorf("expected 1 team id of rules, got %d", len(rulesMap)) } for teamID, teamRules := range rulesMap { - if !strings.HasPrefix(teamID, "1:") { - return fmt.Errorf("unexpected team ID format: %s", teamID) - } + // if !strings.HasPrefix(teamID, "1:") { + // return fmt.Errorf("unexpected team ID format: %s", teamID) + // } if !reflect.DeepEqual(teamRules, expectedRules) { return fmt.Errorf("for team %s, expected rules %v, got %v", teamID, expectedRules, teamRules) } @@ -77,27 +76,25 @@ resource "grafana_data_source" "test" { basic_auth_enabled = true basic_auth_username = "admin" - lifecycle { - ignore_changes = [json_data_encoded] - } -} + # FIXME: we need to ignore the attr "teamHttpHeaders" lifecycle inside of the json_data_encoded, if the lbacRules are + # provisioned using the config_lbac_rules -resource "grafana_team" "team1" { - name = "team1" -} + # potentially use: DiffSuppressFunc + # to do: if someone uses json_data to configure the json_data, they do not overwrite the lbacRules, do this in grafana side -resource "grafana_team" "team2" { - name = "team2" + lifecycle { + ignore_changes = [json_data_encoded] + } } resource "grafana_data_source_config_lbac_rules" "test" { datasource_uid = grafana_data_source.test.uid - rules = { - "${grafana_team.team1.id}" = jsonencode([ + rules = jsonencode({ + "1" = [ "{ foo != \"bar\", foo !~ \"baz\" }", "{ foo = \"qux\" }" - ]) - } + ] + }) } `, name) } From a71e56331d90664f5ae8da3e1b9433ba1b2da39b Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 17 Sep 2024 09:29:39 +0100 Subject: [PATCH 05/10] working experimental --- .../resource_data_source_config_lbac_rules.go | 61 +++++++++++-------- ...urce_data_source_config_lbac_rules_test.go | 17 ++++-- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go index f66dc00a1..349766ed3 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -23,7 +23,10 @@ var ( var ( resourceDataSourceConfigLBACRulesName = "grafana_data_source_config_lbac_rules" - resourceDataSourceConfigLBACRulesID = common.NewResourceID(common.StringIDField("datasource_uid")) + resourceDataSourceConfigLBACRulesID = common.NewResourceID( + common.OptionalIntIDField("orgID"), + common.StringIDField("datasource_uid"), + ) ) func makeResourceDataSourceConfigLBACRules() *common.Resource { @@ -43,6 +46,7 @@ type LBACRule struct { type resourceDataSourceConfigLBACRulesModel struct { ID types.String `tfsdk:"id"` + OrgID types.String `tfsdk:"org_id"` DatasourceUID types.String `tfsdk:"datasource_uid"` Rules types.String `tfsdk:"rules"` } @@ -58,7 +62,16 @@ func (r *resourceDataSourceConfigLBACRules) Metadata(_ context.Context, req reso func (r *resourceDataSourceConfigLBACRules) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { tflog.Info(ctx, "Creating LBAC rules Schema") resp.Schema = schema.Schema{ - MarkdownDescription: "Manages LBAC rules for a data source.", + MarkdownDescription: ` +Manages LBAC rules for a data source. + +!> Warning: The resource is experimental and will be subject to change. This resource manages the entire LBAC rules tree, and will overwrite any existing rules. + +* [Official documentation](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/) +* [TODO: HTTP API](no api yet) + +This resource requires Grafana >=11.0.0. +`, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Computed: true, @@ -71,6 +84,7 @@ func (r *resourceDataSourceConfigLBACRules) Schema(ctx context.Context, _ resour Required: true, Description: "JSON-encoded LBAC rules for the data source. Map of team IDs to lists of rule strings.", }, + "org_id": pluginFrameworkOrgIDAttribute(), }, } } @@ -110,7 +124,10 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso tflog.Info(ctx, "Creating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%+v", apiRules)}) - _, err = r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ + orgID, _ := strconv.ParseInt(data.OrgID.ValueString(), 10, 64) + client := r.client.GrafanaAPI.Clone().WithOrgID(orgID) + + _, err = client.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ UID: data.DatasourceUID.ValueString(), Context: ctx, Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, @@ -122,29 +139,21 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso tflog.Info(ctx, "LBAC rules created successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - // TODO: - // fix teamid setting. - // fix id setting, id of the resource - // fix the lifecycle of the jsondata within grafana - // - make sure you cannot set teamhttpheaders from the json api (updateDatasource), ONLY through lbac/team api - // - magic diffsuprressfunc to ignore changes on json_data_encoded for the specific field teamHttpHeaders - // improvements: - // - make the api "for terraforM" not a json but a struct, to enfore types and schema - data.ID = data.DatasourceUID + data.ID = types.StringValue(MakeOrgResourceID(orgID, data.DatasourceUID.ValueString())) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - tflog.Info(ctx, "Reading LBAC rules") var data resourceDataSourceConfigLBACRulesModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } - tflog.Info(ctx, "Reading LBAC rules", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + orgID, datasourceUID := SplitOrgResourceID(data.ID.ValueString()) + client := r.client.GrafanaAPI.Clone().WithOrgID(orgID) - getResp, err := r.client.GrafanaAPI.Enterprise.GetTeamLBACRulesAPI(data.DatasourceUID.ValueString()) + getResp, err := client.Enterprise.GetTeamLBACRulesAPI(datasourceUID) if err != nil { resp.Diagnostics.AddError("Failed to get LBAC rules", err.Error()) return @@ -162,10 +171,6 @@ func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resour } data.Rules = types.StringValue(string(rulesJSON)) - data.ID = data.DatasourceUID - - tflog.Info(ctx, "LBAC rules read successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -200,8 +205,11 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso } tflog.Info(ctx, "Updating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%v+", apiRules)}) - _, err = r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ - UID: data.DatasourceUID.ValueString(), + orgID, datasourceUID := SplitOrgResourceID(data.ID.ValueString()) + client := r.client.GrafanaAPI.Clone().WithOrgID(orgID) + + _, err = client.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ + UID: datasourceUID, Context: ctx, Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, }) @@ -212,7 +220,8 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso tflog.Info(ctx, "LBAC rules updated successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - data.ID = data.DatasourceUID + data.ID = types.StringValue(MakeOrgResourceID(orgID, datasourceUID)) + data.DatasourceUID = types.StringValue(datasourceUID) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -222,6 +231,10 @@ func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req reso } func (r *resourceDataSourceConfigLBACRules) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - tflog.Info(ctx, "Importing LBAC rules", map[string]interface{}{"datasource_uid": req.ID}) - resource.ImportStatePassthroughID(ctx, path.Root("datasource_uid"), req, resp) + tflog.Info(ctx, "Importing LBAC rules", map[string]interface{}{"id": req.ID}) + + orgID, datasourceUID := SplitOrgResourceID(req.ID) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), MakeOrgResourceID(orgID, datasourceUID))...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("datasource_uid"), datasourceUID)...) } diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go index 333e449f8..95e5e1aa5 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "reflect" + "strings" "testing" "github.com/grafana/grafana-openapi-client-go/models" @@ -43,9 +44,10 @@ func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { } for teamID, teamRules := range rulesMap { - // if !strings.HasPrefix(teamID, "1:") { - // return fmt.Errorf("unexpected team ID format: %s", teamID) - // } + t.Logf("teamID: %s", teamID) + if !strings.HasPrefix(teamID, "1:") { + return fmt.Errorf("unexpected team ID format: %s", teamID) + } if !reflect.DeepEqual(teamRules, expectedRules) { return fmt.Errorf("for team %s, expected rules %v, got %v", teamID, expectedRules, teamRules) } @@ -87,14 +89,19 @@ resource "grafana_data_source" "test" { } } +resource "grafana_team" "test" { + name = "test" +} + resource "grafana_data_source_config_lbac_rules" "test" { datasource_uid = grafana_data_source.test.uid + org_id = grafana_team.test.org_id rules = jsonencode({ - "1" = [ + "${grafana_team.test.id}" = [ "{ foo != \"bar\", foo !~ \"baz\" }", "{ foo = \"qux\" }" ] - }) + }) } `, name) } From 0f7d8a610a580499888f26eba87e51a0ed4435cb Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 17 Sep 2024 10:07:30 +0100 Subject: [PATCH 06/10] revert one file --- .../resource_alerting_message_template.go | 67 +++++-------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/internal/resources/grafana/resource_alerting_message_template.go b/internal/resources/grafana/resource_alerting_message_template.go index a3bb335ff..bc7d4c881 100644 --- a/internal/resources/grafana/resource_alerting_message_template.go +++ b/internal/resources/grafana/resource_alerting_message_template.go @@ -7,29 +7,28 @@ import ( "time" "github.com/go-openapi/runtime" - goapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-openapi-client-go/client/provisioning" "github.com/grafana/grafana-openapi-client-go/models" - "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/grafana/terraform-provider-grafana/internal/common" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func resourceMessageTemplate() *common.Resource { - schema := &schema.Resource{ +func ResourceMessageTemplate() *schema.Resource { + return &schema.Resource{ Description: ` Manages Grafana Alerting message templates. -* [Official documentation](https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/terraform-provisioning/) -* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#templates) +* [Official documentation](https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/create-notification-templates/) +* [HTTP API](https://grafana.com/docs/grafana/next/developers/http_api/alerting_provisioning/#templates) This resource requires Grafana 9.1.0 or later. `, - CreateContext: common.WithAlertingMutex[schema.CreateContextFunc](putMessageTemplate), + CreateContext: putMessageTemplate, ReadContext: readMessageTemplate, - UpdateContext: common.WithAlertingMutex[schema.UpdateContextFunc](putMessageTemplate), - DeleteContext: common.WithAlertingMutex[schema.DeleteContextFunc](deleteMessageTemplate), + UpdateContext: putMessageTemplate, + DeleteContext: deleteMessageTemplate, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -51,46 +50,8 @@ This resource requires Grafana 9.1.0 or later. return strings.TrimSpace(v.(string)) }, }, - "disable_provenance": { - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, // TODO: The API doesn't return provenance, so we have to force new for now. - Description: "Allow modifying the message template from other sources than Terraform or the Grafana API.", - }, }, } - - return common.NewLegacySDKResource( - common.CategoryAlerting, - "grafana_message_template", - orgResourceIDString("name"), - schema, - ).WithLister(listerFunctionOrgResource(listMessageTemplate)) -} - -func listMessageTemplate(ctx context.Context, client *goapi.GrafanaHTTPAPI, orgID int64) ([]string, error) { - var ids []string - // Retry if the API returns 500 because it may be that the alertmanager is not ready in the org yet. - // The alertmanager is provisioned asynchronously when the org is created. - if err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { - resp, err := client.Provisioning.GetTemplates() - if err != nil { - if orgID > 1 && (err.(*runtime.APIError).IsCode(500) || err.(*runtime.APIError).IsCode(403)) { - return retry.RetryableError(err) - } - return retry.NonRetryableError(err) - } - - for _, template := range resp.Payload { - ids = append(ids, MakeOrgResourceID(orgID, template.Name)) - } - return nil - }); err != nil { - return nil, err - } - - return ids, nil } func readMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -110,6 +71,9 @@ func readMessageTemplate(ctx context.Context, data *schema.ResourceData, meta in } func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + lock := &meta.(*common.Client).AlertingMutex + lock.Lock() + defer lock.Unlock() client, orgID := OAPIClientFromNewOrgResource(meta, data) name := data.Get("name").(string) @@ -123,9 +87,6 @@ func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta int WithBody(&models.NotificationTemplateContent{ Template: content, }) - if v, ok := data.GetOk("disable_provenance"); ok && v.(bool) { - params.SetXDisableProvenance(&provenanceDisabled) - } if _, err := client.Provisioning.PutTemplate(params); err != nil { if orgID > 1 && err.(*runtime.APIError).IsCode(500) { return retry.RetryableError(err) @@ -143,10 +104,12 @@ func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta int } func deleteMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { + lock := &meta.(*common.Client).AlertingMutex + lock.Lock() + defer lock.Unlock() client, _, name := OAPIClientFromExistingOrgResource(meta, data.Id()) - params := provisioning.NewDeleteTemplateParams().WithName(name) - _, err := client.Provisioning.DeleteTemplate(params) + _, err := client.Provisioning.DeleteTemplate(name) diag, _ := common.CheckReadError("message template", data, err) return diag } From baf2044739f0d925e811d0d745c05dca6232fb21 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 17 Sep 2024 10:09:21 +0100 Subject: [PATCH 07/10] reset to main --- .../resource_alerting_message_template.go | 64 +++++++++++++++---- 1 file changed, 50 insertions(+), 14 deletions(-) diff --git a/internal/resources/grafana/resource_alerting_message_template.go b/internal/resources/grafana/resource_alerting_message_template.go index bc7d4c881..e356a18e6 100644 --- a/internal/resources/grafana/resource_alerting_message_template.go +++ b/internal/resources/grafana/resource_alerting_message_template.go @@ -7,28 +7,29 @@ import ( "time" "github.com/go-openapi/runtime" + goapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grafana-openapi-client-go/client/provisioning" "github.com/grafana/grafana-openapi-client-go/models" - "github.com/grafana/terraform-provider-grafana/internal/common" + "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/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func ResourceMessageTemplate() *schema.Resource { - return &schema.Resource{ +func resourceMessageTemplate() *common.Resource { + schema := &schema.Resource{ Description: ` Manages Grafana Alerting message templates. -* [Official documentation](https://grafana.com/docs/grafana/latest/alerting/manage-notifications/template-notifications/create-notification-templates/) -* [HTTP API](https://grafana.com/docs/grafana/next/developers/http_api/alerting_provisioning/#templates) +* [Official documentation](https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/terraform-provisioning/) +* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/#templates) This resource requires Grafana 9.1.0 or later. `, - CreateContext: putMessageTemplate, + CreateContext: common.WithAlertingMutex[schema.CreateContextFunc](putMessageTemplate), ReadContext: readMessageTemplate, - UpdateContext: putMessageTemplate, - DeleteContext: deleteMessageTemplate, + UpdateContext: common.WithAlertingMutex[schema.UpdateContextFunc](putMessageTemplate), + DeleteContext: common.WithAlertingMutex[schema.DeleteContextFunc](deleteMessageTemplate), Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -50,8 +51,46 @@ This resource requires Grafana 9.1.0 or later. return strings.TrimSpace(v.(string)) }, }, + "disable_provenance": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, // TODO: The API doesn't return provenance, so we have to force new for now. + Description: "Allow modifying the message template from other sources than Terraform or the Grafana API.", + }, }, } + + return common.NewLegacySDKResource( + common.CategoryAlerting, + "grafana_message_template", + orgResourceIDString("name"), + schema, + ).WithLister(listerFunctionOrgResource(listMessageTemplate)) +} + +func listMessageTemplate(ctx context.Context, client *goapi.GrafanaHTTPAPI, orgID int64) ([]string, error) { + var ids []string + // Retry if the API returns 500 because it may be that the alertmanager is not ready in the org yet. + // The alertmanager is provisioned asynchronously when the org is created. + if err := retry.RetryContext(ctx, 2*time.Minute, func() *retry.RetryError { + resp, err := client.Provisioning.GetTemplates() + if err != nil { + if orgID > 1 && (err.(*runtime.APIError).IsCode(500) || err.(*runtime.APIError).IsCode(403)) { + return retry.RetryableError(err) + } + return retry.NonRetryableError(err) + } + + for _, template := range resp.Payload { + ids = append(ids, MakeOrgResourceID(orgID, template.Name)) + } + return nil + }); err != nil { + return nil, err + } + + return ids, nil } func readMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -71,9 +110,6 @@ func readMessageTemplate(ctx context.Context, data *schema.ResourceData, meta in } func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { - lock := &meta.(*common.Client).AlertingMutex - lock.Lock() - defer lock.Unlock() client, orgID := OAPIClientFromNewOrgResource(meta, data) name := data.Get("name").(string) @@ -87,6 +123,9 @@ func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta int WithBody(&models.NotificationTemplateContent{ Template: content, }) + if v, ok := data.GetOk("disable_provenance"); ok && v.(bool) { + params.SetXDisableProvenance(&provenanceDisabled) + } if _, err := client.Provisioning.PutTemplate(params); err != nil { if orgID > 1 && err.(*runtime.APIError).IsCode(500) { return retry.RetryableError(err) @@ -104,9 +143,6 @@ func putMessageTemplate(ctx context.Context, data *schema.ResourceData, meta int } func deleteMessageTemplate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { - lock := &meta.(*common.Client).AlertingMutex - lock.Lock() - defer lock.Unlock() client, _, name := OAPIClientFromExistingOrgResource(meta, data.Id()) _, err := client.Provisioning.DeleteTemplate(name) From 227eeabe53e8092a69260bf1aa9005d6ad9f9697 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 17 Sep 2024 10:53:07 +0100 Subject: [PATCH 08/10] commit deleted file --- ...source_data_source_permission_item_test.go | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 internal/resources/grafana/resource_data_source_permission_item_test.go diff --git a/internal/resources/grafana/resource_data_source_permission_item_test.go b/internal/resources/grafana/resource_data_source_permission_item_test.go new file mode 100644 index 000000000..16a49adc3 --- /dev/null +++ b/internal/resources/grafana/resource_data_source_permission_item_test.go @@ -0,0 +1,108 @@ +package grafana_test + +import ( + "fmt" + "testing" + + "github.com/grafana/grafana-openapi-client-go/models" + "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDatasourcePermissionItem_basic(t *testing.T) { + testutils.CheckEnterpriseTestsEnabled(t, ">=9.0.0") + + var ds models.DataSource + name := acctest.RandString(10) + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDatasourcePermissionItem(name), + Check: resource.ComposeAggregateTestCheckFunc( + datasourcePermissionsCheckExists.exists("grafana_data_source.foo", &ds), + ), + }, + { + ResourceName: "grafana_data_source_permission_item.team", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "grafana_data_source_permission_item.user", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "grafana_data_source_permission_item.role", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "grafana_data_source_permission_item.sa", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccDatasourcePermissionItem(name string) string { + return fmt.Sprintf(` +resource "grafana_team" "team" { + name = "%[1]s" +} + +resource "grafana_data_source" "foo" { + name = "%[1]s" + type = "cloudwatch" + + json_data_encoded = jsonencode({ + defaultRegion = "us-east-1" + authType = "keys" + }) + + secure_json_data_encoded = jsonencode({ + accessKey = "123" + secretKey = "456" + }) +} + +resource "grafana_user" "user" { + name = "%[1]s" + email = "%[1]s@example.com" + login = "%[1]s" + password = "hunter2" +} + +resource "grafana_service_account" "sa" { + name = "%[1]s" + role = "Viewer" +} + +resource "grafana_data_source_permission_item" "team" { + datasource_uid = grafana_data_source.foo.uid + team = grafana_team.team.id + permission = "Edit" +} + +resource "grafana_data_source_permission_item" "user" { + datasource_uid = grafana_data_source.foo.uid + user = grafana_user.user.id + permission = "Edit" +} + +resource "grafana_data_source_permission_item" "role" { + datasource_uid = grafana_data_source.foo.uid + role = "Viewer" + permission = "Query" +} + +resource "grafana_data_source_permission_item" "sa" { + datasource_uid = grafana_data_source.foo.uid + user = grafana_service_account.sa.id + permission = "Query" +}`, name) +} From ea9c56556e80ef3874c10ed561add8dd07a55c99 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Tue, 17 Sep 2024 11:39:42 +0100 Subject: [PATCH 09/10] made the id from the datasourceUID --- .../resource_data_source_config_lbac_rules.go | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go index 349766ed3..ac868d888 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -24,7 +24,6 @@ var ( var ( resourceDataSourceConfigLBACRulesName = "grafana_data_source_config_lbac_rules" resourceDataSourceConfigLBACRulesID = common.NewResourceID( - common.OptionalIntIDField("orgID"), common.StringIDField("datasource_uid"), ) ) @@ -46,7 +45,6 @@ type LBACRule struct { type resourceDataSourceConfigLBACRulesModel struct { ID types.String `tfsdk:"id"` - OrgID types.String `tfsdk:"org_id"` DatasourceUID types.String `tfsdk:"datasource_uid"` Rules types.String `tfsdk:"rules"` } @@ -84,7 +82,6 @@ This resource requires Grafana >=11.0.0. Required: true, Description: "JSON-encoded LBAC rules for the data source. Map of team IDs to lists of rule strings.", }, - "org_id": pluginFrameworkOrgIDAttribute(), }, } } @@ -124,8 +121,7 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso tflog.Info(ctx, "Creating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%+v", apiRules)}) - orgID, _ := strconv.ParseInt(data.OrgID.ValueString(), 10, 64) - client := r.client.GrafanaAPI.Clone().WithOrgID(orgID) + client := r.client.GrafanaAPI _, err = client.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ UID: data.DatasourceUID.ValueString(), @@ -139,7 +135,7 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso tflog.Info(ctx, "LBAC rules created successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - data.ID = types.StringValue(MakeOrgResourceID(orgID, data.DatasourceUID.ValueString())) + data.ID = types.StringValue(data.DatasourceUID.ValueString()) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -150,8 +146,8 @@ func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resour return } - orgID, datasourceUID := SplitOrgResourceID(data.ID.ValueString()) - client := r.client.GrafanaAPI.Clone().WithOrgID(orgID) + datasourceUID := data.ID.ValueString() + client := r.client.GrafanaAPI getResp, err := client.Enterprise.GetTeamLBACRulesAPI(datasourceUID) if err != nil { @@ -205,8 +201,8 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso } tflog.Info(ctx, "Updating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%v+", apiRules)}) - orgID, datasourceUID := SplitOrgResourceID(data.ID.ValueString()) - client := r.client.GrafanaAPI.Clone().WithOrgID(orgID) + datasourceUID := data.ID.ValueString() + client := r.client.GrafanaAPI _, err = client.Enterprise.UpdateTeamLBACRulesAPI(&enterprise.UpdateTeamLBACRulesAPIParams{ UID: datasourceUID, @@ -220,7 +216,7 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso tflog.Info(ctx, "LBAC rules updated successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - data.ID = types.StringValue(MakeOrgResourceID(orgID, datasourceUID)) + data.ID = types.StringValue(datasourceUID) data.DatasourceUID = types.StringValue(datasourceUID) resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -233,8 +229,8 @@ func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req reso func (r *resourceDataSourceConfigLBACRules) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { tflog.Info(ctx, "Importing LBAC rules", map[string]interface{}{"id": req.ID}) - orgID, datasourceUID := SplitOrgResourceID(req.ID) + datasourceUID := req.ID - resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), MakeOrgResourceID(orgID, datasourceUID))...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), datasourceUID)...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("datasource_uid"), datasourceUID)...) } From 25860a7e5dbacde71858c1697fc2e73d1258f155 Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Fri, 8 Nov 2024 15:43:11 +0000 Subject: [PATCH 10/10] Update to use uid --- .../resource_data_source_config_lbac_rules.go | 29 ++++++------- ...urce_data_source_config_lbac_rules_test.go | 41 ++++++++----------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules.go b/internal/resources/grafana/resource_data_source_config_lbac_rules.go index ac868d888..1c0b82762 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "strconv" "github.com/grafana/grafana-openapi-client-go/client/enterprise" "github.com/grafana/grafana-openapi-client-go/models" @@ -39,8 +38,9 @@ func makeResourceDataSourceConfigLBACRules() *common.Resource { } type LBACRule struct { - TeamID types.String `tfsdk:"team_id"` - Rules []types.String `tfsdk:"rules"` + TeamID types.String `tfsdk:"team_id"` + TeamUID types.String `tfsdk:"team_uid"` + Rules []types.String `tfsdk:"rules"` } type resourceDataSourceConfigLBACRulesModel struct { @@ -104,7 +104,7 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso tflog.Info(ctx, "Creating LBAC rules", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) - var rulesMap map[string][]string + rulesMap := make(map[string][]string) err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap) if err != nil { resp.Diagnostics.AddError("Invalid rules JSON", fmt.Sprintf("Failed to parse rules: %v", err)) @@ -112,10 +112,11 @@ func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req reso } apiRules := make([]*models.TeamLBACRule, 0, len(rulesMap)) - for teamID, rules := range rulesMap { + for teamUID, rules := range rulesMap { apiRules = append(apiRules, &models.TeamLBACRule{ - TeamID: teamID, - Rules: rules, + TeamID: "", + TeamUID: teamUID, + Rules: rules, }) } @@ -157,7 +158,7 @@ func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resour rulesMap := make(map[string][]string) for _, rule := range getResp.Payload.Rules { - rulesMap[rule.TeamID] = rule.Rules + rulesMap[rule.TeamUID] = rule.Rules } rulesJSON, err := json.Marshal(rulesMap) @@ -188,15 +189,11 @@ func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req reso } apiRules := make([]*models.TeamLBACRule, 0, len(rulesMap)) - for teamID, rules := range rulesMap { - _, err := strconv.ParseInt(teamID, 10, 64) - if err != nil { - resp.Diagnostics.AddError("Invalid team ID", fmt.Sprintf("Team ID %s is not a valid integer", teamID)) - return - } + for teamUID, rules := range rulesMap { apiRules = append(apiRules, &models.TeamLBACRule{ - TeamID: teamID, - Rules: rules, + TeamID: "", + TeamUID: teamUID, + Rules: rules, }) } tflog.Info(ctx, "Updating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%v+", apiRules)}) diff --git a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go index 95e5e1aa5..37dcefb78 100644 --- a/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go @@ -4,10 +4,8 @@ import ( "encoding/json" "fmt" "reflect" - "strings" "testing" - "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" @@ -16,7 +14,6 @@ import ( func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { testutils.CheckEnterpriseTestsEnabled(t, ">=11.0.0") - var ds models.DataSource name := acctest.RandString(10) resource.ParallelTest(t, resource.TestCase{ @@ -25,7 +22,6 @@ func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { { Config: testAccDataSourceConfigLBACRules(name), Check: resource.ComposeAggregateTestCheckFunc( - datasourceCheckExists.exists("grafana_data_source.test", &ds), resource.TestCheckResourceAttrSet("grafana_data_source_config_lbac_rules.test", "rules"), resource.TestCheckResourceAttrWith("grafana_data_source_config_lbac_rules.test", "rules", func(value string) error { var rulesMap map[string][]string @@ -43,13 +39,9 @@ func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { return fmt.Errorf("expected 1 team id of rules, got %d", len(rulesMap)) } - for teamID, teamRules := range rulesMap { - t.Logf("teamID: %s", teamID) - if !strings.HasPrefix(teamID, "1:") { - return fmt.Errorf("unexpected team ID format: %s", teamID) - } + for teamUID, teamRules := range rulesMap { if !reflect.DeepEqual(teamRules, expectedRules) { - return fmt.Errorf("for team %s, expected rules %v, got %v", teamID, expectedRules, teamRules) + return fmt.Errorf("for team %s, expected rules %v, got %v", teamUID, expectedRules, teamRules) } } @@ -65,12 +57,21 @@ func TestAccDataSourceConfigLBACRules_basic(t *testing.T) { }), ), }, + { + ResourceName: "grafana_data_source_config_lbac_rules.test", + ImportState: true, + ImportStateVerify: true, + }, }, }) } func testAccDataSourceConfigLBACRules(name string) string { return fmt.Sprintf(` +resource "grafana_team" "test" { + name = "%[1]s-team" +} + resource "grafana_data_source" "test" { name = "%[1]s" type = "loki" @@ -78,30 +79,24 @@ resource "grafana_data_source" "test" { basic_auth_enabled = true basic_auth_username = "admin" - # FIXME: we need to ignore the attr "teamHttpHeaders" lifecycle inside of the json_data_encoded, if the lbacRules are - # provisioned using the config_lbac_rules - - # potentially use: DiffSuppressFunc - # to do: if someone uses json_data to configure the json_data, they do not overwrite the lbacRules, do this in grafana side - lifecycle { - ignore_changes = [json_data_encoded] + ignore_changes = [json_data_encoded] } } -resource "grafana_team" "test" { - name = "test" -} - resource "grafana_data_source_config_lbac_rules" "test" { datasource_uid = grafana_data_source.test.uid - org_id = grafana_team.test.org_id rules = jsonencode({ - "${grafana_team.test.id}" = [ + "${grafana_team.test.team_uid}" = [ "{ foo != \"bar\", foo !~ \"baz\" }", "{ foo = \"qux\" }" ] }) + + depends_on = [ + grafana_team.test, + grafana_data_source.test + ] } `, name) }