Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource: Add LBAC for datasources data_source_config_lbac_rules #1797

Merged
merged 21 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.4.0.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we actually need to set it to 11.5.0 - 11.4.0 will not include the changes that we need (see this internal thread).

---

# 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.4.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 = "<>"
})
}

# 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 IDs 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 = "<>"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this line needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not needed at all. just showing that you need to have a password to connect with the datasource from basicAuth

})
}

# 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
# ]
# }

237 changes: 237 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,237 @@
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 LBACRule struct {
TeamID types.String `tfsdk:"team_id"`
TeamUID types.String `tfsdk:"team_uid"`
Rules []types.String `tfsdk:"rules"`
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait it is unused, i should really start to make my structs private on first creation. how do you spot this? with the linter


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.4.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.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Description: "JSON-encoded LBAC rules for the data source. Map of team IDs to lists of rule strings.",
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) {
eleijonmarck marked this conversation as resolved.
Show resolved Hide resolved
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
Loading