Skip to content

Commit

Permalink
Resource: Add LBAC for datasources data_source_config_lbac_rules (#…
Browse files Browse the repository at this point in the history
…1797)

* WIP

* WIP

* WIP

* WIP

* working experimental

* revert one file

* reset to main

* commit deleted file

* made the id from the datasourceUID

* Update to use uid

* added example for lbac rules

* added constraint about >=11.4.0

* fix example test

* skip the lbac rules for example tests

* refactor to updateRules on create,delete and update

* review comments
  • Loading branch information
eleijonmarck authored Dec 2, 2024
1 parent 1fd2410 commit a2ff67e
Show file tree
Hide file tree
Showing 7 changed files with 445 additions and 0 deletions.
77 changes: 77 additions & 0 deletions docs/resources/data_source_config_lbac_rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "grafana_data_source_config_lbac_rules Resource - terraform-provider-grafana"
subcategory: "Grafana Enterprise"
description: |-
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/HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/datasource_lbac_rules/
This resource requires Grafana >=11.5.0.
---

# grafana_data_source_config_lbac_rules (Resource)

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/)
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/datasource_lbac_rules/)

This resource requires Grafana >=11.5.0.

## Example Usage

```terraform
resource "grafana_team" "team" {
name = "Team Name"
}
resource "grafana_data_source" "test" {
type = "loki"
name = "loki-from-terraform"
url = "https://mylokiurl.net"
basic_auth_enabled = true
basic_auth_username = "username"
json_data_encoded = jsonencode({
authType = "default"
basicAuthPassword = "password"
})
}
# resource "grafana_data_source_config_lbac_rules" "test_rule" {
# datasource_uid = grafana_data_source.test.uid
# rules = jsonencode({
# "${grafana_team.team.team_uid}" = [
# "{ cluster = \"dev-us-central-0\", namespace = \"hosted-grafana\" }",
# "{ foo = \"qux\" }"
# ]
# })
# depends_on = [
# grafana_team.team,
# grafana_data_source.test
# ]
# }
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `datasource_uid` (String) The UID of the datasource.
- `rules` (String) JSON-encoded LBAC rules for the data source. Map of team UIDs to lists of rule strings.

### Read-Only

- `id` (String) The ID of this resource.

## Import

Import is supported using the following syntax:

```shell
terraform import grafana_data_source_config_lbac_rules.name "{{ datasource_uid }}"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import grafana_data_source_config_lbac_rules.name "{{ datasource_uid }}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
resource "grafana_team" "team" {
name = "Team Name"
}

resource "grafana_data_source" "test" {
type = "loki"
name = "loki-from-terraform"
url = "https://mylokiurl.net"
basic_auth_enabled = true
basic_auth_username = "username"

json_data_encoded = jsonencode({
authType = "default"
basicAuthPassword = "password"
})
}

# resource "grafana_data_source_config_lbac_rules" "test_rule" {
# datasource_uid = grafana_data_source.test.uid
# rules = jsonencode({
# "${grafana_team.team.team_uid}" = [
# "{ cluster = \"dev-us-central-0\", namespace = \"hosted-grafana\" }",
# "{ foo = \"qux\" }"
# ]
# })

# depends_on = [
# grafana_team.team,
# grafana_data_source.test
# ]
# }

231 changes: 231 additions & 0 deletions internal/resources/grafana/resource_data_source_config_lbac_rules.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
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"
)

// Note: The LBAC Rules API only supports GET and UPDATE operations.
// There is no CREATE or DELETE API endpoint. The UPDATE operation is used
// for all modifications (create/update/delete) by sending the complete desired
// state. Deleting rules is done by sending an empty rules list.

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 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) {
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/)
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/datasource_lbac_rules/)
This resource requires Grafana >=11.5.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 UIDs to lists of rule strings.",
},
},
}
}

func (r *resourceDataSourceConfigLBACRules) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*common.Client)
}

// Add this helper function to handle the common update logic
func (r *resourceDataSourceConfigLBACRules) updateRules(ctx context.Context, data *resourceDataSourceConfigLBACRulesModel, rules map[string][]string) error {
apiRules := make([]*models.TeamLBACRule, 0, len(rules))
for teamUID, ruleList := range rules {
apiRules = append(apiRules, &models.TeamLBACRule{
TeamUID: teamUID,
Rules: ruleList,
})
}

params := &enterprise.UpdateTeamLBACRulesAPIParams{
Context: ctx,
UID: data.DatasourceUID.ValueString(),
Body: &models.UpdateTeamLBACCommand{Rules: apiRules},
}

_, err := r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(params)
if err != nil {
return fmt.Errorf("failed to update LBAC rules for datasource %q: %w", data.DatasourceUID.ValueString(), err)
}
return nil
}

func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data resourceDataSourceConfigLBACRulesModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

rulesMap := make(map[string][]string)
if err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap); err != nil {
resp.Diagnostics.AddError(
"Invalid rules JSON",
fmt.Sprintf("Failed to parse rules for datasource %q: %v. Please ensure the rules are valid JSON.", data.DatasourceUID.ValueString(), err),
)
return
}

if err := r.updateRules(ctx, &data, rulesMap); err != nil {
resp.Diagnostics.AddError("Failed to create LBAC rules", err.Error())
return
}

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",
fmt.Sprintf("Could not read LBAC rules for datasource %q: %v", datasourceUID, err),
)
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 {
// Marshal error should never happen for a valid map
resp.Diagnostics.AddError(
"Failed to encode rules",
fmt.Sprintf("Could not encode LBAC rules for datasource %q: %v. This is an internal error, please report it.", datasourceUID, err),
)
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) {
var data resourceDataSourceConfigLBACRulesModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

rulesMap := make(map[string][]string)
if err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap); err != nil {
resp.Diagnostics.AddError(
"Invalid rules JSON",
fmt.Sprintf("Failed to parse updated rules for datasource %q: %v. Please ensure the rules are valid JSON.", data.DatasourceUID.ValueString(), err),
)
return
}

if err := r.updateRules(ctx, &data, rulesMap); err != nil {
resp.Diagnostics.AddError("Failed to update LBAC rules", err.Error())
return
}

data.ID = types.StringValue(data.DatasourceUID.ValueString())
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state resourceDataSourceConfigLBACRulesModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

// Pass empty rules map to clear all rules
if err := r.updateRules(ctx, &state, make(map[string][]string)); err != nil {
resp.Diagnostics.AddError(
"Failed to delete LBAC rules",
fmt.Sprintf("Could not delete LBAC rules for datasource %q: %v", state.DatasourceUID.ValueString(), err),
)
return
}
}

func (r *resourceDataSourceConfigLBACRules) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
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)...)
}
Loading

0 comments on commit a2ff67e

Please sign in to comment.