-
Notifications
You must be signed in to change notification settings - Fork 233
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
Changes from 20 commits
a92c752
be0706f
ee0fcfb
d8c9ba1
a71e563
df849f1
0f7d8a6
baf2044
227eeab
ea9c565
0682668
25860a7
8994232
692ac09
26e8f05
fb29a0a
a8a423b
3b3c344
c23cc3d
609abe8
fd76b29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
--- | ||
|
||
# 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 = "<>" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this line needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
# ] | ||
# } | ||
|
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"` | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this actually used? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.", | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
}, | ||||||
}, | ||||||
} | ||||||
} | ||||||
|
||||||
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)...) | ||||||
} |
There was a problem hiding this comment.
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).