diff --git a/go.mod b/go.mod index a83930bb1..944cdef25 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,8 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) +require github.com/hashicorp/terraform-plugin-log v0.9.0 + require ( github.com/BurntSushi/toml v1.4.0 // indirect github.com/Kunde21/markdownfmt/v3 v3.1.0 // indirect @@ -95,7 +97,6 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-plugin v1.6.2 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -178,3 +179,5 @@ require ( ) replace github.com/hashicorp/terraform-exec v0.21.0 => github.com/hrmsk66/terraform-exec v0.21.0 + +replace github.com/grafana/grafana-openapi-client-go => /Users/eleijonmarck/dev/grafana/grafana-openapi-client-go diff --git a/go.sum b/go.sum index 4ca5916ae..5d93b532e 100644 --- a/go.sum +++ b/go.sum @@ -140,8 +140,6 @@ github.com/grafana/amixr-api-go-client v0.0.16 h1:CXdqnLKjvo6QoNPBKxmZ2kGKCcUoAl github.com/grafana/amixr-api-go-client v0.0.16/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE= github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240807172819-ac10800522a3 h1:CVLTffnWgBGvVaXfUUcSgFrZbiMzvj0/Hpi909zdeG0= github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240807172819-ac10800522a3/go.mod h1:u9d0BESoKlztYm93CpoRleQjMbYBcZ+JOLHHP2nN6Wg= -github.com/grafana/grafana-openapi-client-go v0.0.0-20240723170622-ae2c94b7c9a3 h1:W35ScJIkeyLuDlOo3F+u1JSRRvmoIYYf/ghA/17Y18Q= -github.com/grafana/grafana-openapi-client-go v0.0.0-20240723170622-ae2c94b7c9a3/go.mod h1:hiZnMmXc9KXNUlvkV2BKFsiWuIFF/fF4wGgYWEjBitI= github.com/grafana/grafana-plugin-sdk-go v0.250.0 h1:9EBucp9jLqMx2b8NTlOXH+4OuQWUh6L85c6EJUN8Jdo= github.com/grafana/grafana-plugin-sdk-go v0.250.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk= github.com/grafana/machine-learning-go-client v0.8.2 h1:TvU4e+Kgg4GhwBNYTMjBUNq4tbhcxe0L8w1eo/UfV2M= 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 new file mode 100644 index 000000000..83b5e5a5b --- /dev/null +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules.go @@ -0,0 +1,241 @@ +package grafana + +import ( + "context" + "encoding/json" + "fmt" + + "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" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + // Check interface + _ resource.ResourceWithImportState = (*resourceDataSourceConfigLBACRules)(nil) +) + +var ( + resourceDataSourceConfigLBACRulesName = "grafana_data_source_config_lbac_rules" + resourceDataSourceConfigLBACRulesID = common.NewResourceID( + common.StringIDField("datasource_uid"), + ) +) + +func makeResourceDataSourceConfigLBACRules() *common.Resource { + resourceStruct := &resourceDataSourceConfigLBACRules{} + return common.NewResource( + common.CategoryGrafanaEnterprise, + resourceDataSourceConfigLBACRulesName, + resourceDataSourceConfigLBACRulesID, + resourceStruct, + ) +} + +type LBACRule struct { + TeamID types.String `tfsdk:"team_id"` + TeamUID types.String `tfsdk:"team_uid"` + Rules []types.String `tfsdk:"rules"` +} + +type resourceDataSourceConfigLBACRulesModel struct { + ID types.String `tfsdk:"id"` + DatasourceUID types.String `tfsdk:"datasource_uid"` + Rules types.String `tfsdk:"rules"` +} + +type resourceDataSourceConfigLBACRules struct { + client *common.Client +} + +func (r *resourceDataSourceConfigLBACRules) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = resourceDataSourceConfigLBACRulesName +} + +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. + +!> 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, + }, + "datasource_uid": schema.StringAttribute{ + Required: true, + Description: "The UID of the datasource.", + }, + "rules": schema.StringAttribute{ + Required: true, + Description: "JSON-encoded LBAC rules for the data source. Map of team IDs to lists of rule strings.", + }, + }, + } +} + +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.(*common.Client) +} + +func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + 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()}) + + 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 rule s: %v", err)) + } + + apiRules := make([]*models.TeamLBACRule, 0, len(rulesMap)) + for teamUID, rules := range rulesMap { + apiRules = append(apiRules, &models.TeamLBACRule{ + TeamID: "", + TeamUID: teamUID, + Rules: rules, + }) + } + + tflog.Info(ctx, "Creating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%+v", apiRules)}) + + client := r.client.GrafanaAPI + + params := &enterprise.UpdateTeamLBACRulesAPIParams{ + Context: ctx, + UID: data.DatasourceUID.ValueString(), + Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, + } + + _, err = client.Enterprise.UpdateTeamLBACRulesAPI(params) + 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 = types.StringValue(data.DatasourceUID.ValueString()) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +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 + } + + datasourceUID := data.DatasourceUID.ValueString() + client := r.client.GrafanaAPI + + getResp, err := client.Enterprise.GetTeamLBACRulesAPI(datasourceUID) + if err != nil { + resp.Diagnostics.AddError("Failed to get LBAC rules", err.Error()) + return + } + + rulesMap := make(map[string][]string) + for _, rule := range getResp.Payload.Rules { + rulesMap[rule.TeamUID] = rule.Rules + } + + rulesJSON, err := json.Marshal(rulesMap) + if err != nil { + resp.Diagnostics.AddError("Failed to encode rules", err.Error()) + return + } + + data = resourceDataSourceConfigLBACRulesModel{ + ID: types.StringValue(datasourceUID), + DatasourceUID: types.StringValue(datasourceUID), + Rules: types.StringValue(string(rulesJSON)), + } + + 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 + } + + tflog.Info(ctx, "Updating LBAC rules", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + + 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 teamUID, rules := range rulesMap { + apiRules = append(apiRules, &models.TeamLBACRule{ + TeamID: "", + TeamUID: teamUID, + Rules: rules, + }) + } + tflog.Info(ctx, "Updating LBAC rules with the new rulesmaps", map[string]interface{}{"rulesmaps": fmt.Sprintf("%v+", apiRules)}) + + datasourceUID := data.DatasourceUID.ValueString() + client := r.client.GrafanaAPI + + params := &enterprise.UpdateTeamLBACRulesAPIParams{ + Context: ctx, + UID: datasourceUID, + Body: &models.UpdateTeamLBACCommand{Rules: apiRules}, + } + + _, err = client.Enterprise.UpdateTeamLBACRulesAPI(params) + if err != nil { + resp.Diagnostics.AddError("Failed to update LBAC rules", err.Error()) + return + } + + tflog.Info(ctx, "LBAC rules updated successfully", map[string]interface{}{"datasource_uid": data.DatasourceUID.ValueString()}) + + data.ID = types.StringValue(datasourceUID) + data.DatasourceUID = types.StringValue(datasourceUID) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + 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{}{"id": req.ID}) + + datasourceUID := req.ID + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), 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 new file mode 100644 index 000000000..37dcefb78 --- /dev/null +++ b/internal/resources/grafana/resource_data_source_config_lbac_rules_test.go @@ -0,0 +1,102 @@ +package grafana_test + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "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, ">=11.0.0") + + name := acctest.RandString(10) + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceConfigLBACRules(name), + Check: resource.ComposeAggregateTestCheckFunc( + 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) != 1 { + return fmt.Errorf("expected 1 team id of rules, got %d", len(rulesMap)) + } + + for teamUID, teamRules := range rulesMap { + if !reflect.DeepEqual(teamRules, expectedRules) { + return fmt.Errorf("for team %s, expected rules %v, got %v", teamUID, 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, + }, + }, + }) +} + +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" + + basic_auth_enabled = true + basic_auth_username = "admin" + + lifecycle { + ignore_changes = [json_data_encoded] + } +} + +resource "grafana_data_source_config_lbac_rules" "test" { + datasource_uid = grafana_data_source.test.uid + rules = jsonencode({ + "${grafana_team.test.team_uid}" = [ + "{ foo != \"bar\", foo !~ \"baz\" }", + "{ foo = \"qux\" }" + ] + }) + + depends_on = [ + grafana_team.test, + grafana_data_source.test + ] +} +`, name) +} 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(),