Skip to content

Commit a2ff67e

Browse files
authored
Resource: Add LBAC for datasources data_source_config_lbac_rules (#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
1 parent 1fd2410 commit a2ff67e

File tree

7 files changed

+445
-0
lines changed

7 files changed

+445
-0
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_data_source_config_lbac_rules Resource - terraform-provider-grafana"
4+
subcategory: "Grafana Enterprise"
5+
description: |-
6+
Manages LBAC rules for a data source.
7+
!> 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.
8+
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/
9+
This resource requires Grafana >=11.5.0.
10+
---
11+
12+
# grafana_data_source_config_lbac_rules (Resource)
13+
14+
Manages LBAC rules for a data source.
15+
16+
!> 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.
17+
18+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/)
19+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/datasource_lbac_rules/)
20+
21+
This resource requires Grafana >=11.5.0.
22+
23+
## Example Usage
24+
25+
```terraform
26+
resource "grafana_team" "team" {
27+
name = "Team Name"
28+
}
29+
30+
resource "grafana_data_source" "test" {
31+
type = "loki"
32+
name = "loki-from-terraform"
33+
url = "https://mylokiurl.net"
34+
basic_auth_enabled = true
35+
basic_auth_username = "username"
36+
37+
json_data_encoded = jsonencode({
38+
authType = "default"
39+
basicAuthPassword = "password"
40+
})
41+
}
42+
43+
# resource "grafana_data_source_config_lbac_rules" "test_rule" {
44+
# datasource_uid = grafana_data_source.test.uid
45+
# rules = jsonencode({
46+
# "${grafana_team.team.team_uid}" = [
47+
# "{ cluster = \"dev-us-central-0\", namespace = \"hosted-grafana\" }",
48+
# "{ foo = \"qux\" }"
49+
# ]
50+
# })
51+
52+
# depends_on = [
53+
# grafana_team.team,
54+
# grafana_data_source.test
55+
# ]
56+
# }
57+
```
58+
59+
<!-- schema generated by tfplugindocs -->
60+
## Schema
61+
62+
### Required
63+
64+
- `datasource_uid` (String) The UID of the datasource.
65+
- `rules` (String) JSON-encoded LBAC rules for the data source. Map of team UIDs to lists of rule strings.
66+
67+
### Read-Only
68+
69+
- `id` (String) The ID of this resource.
70+
71+
## Import
72+
73+
Import is supported using the following syntax:
74+
75+
```shell
76+
terraform import grafana_data_source_config_lbac_rules.name "{{ datasource_uid }}"
77+
```
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
terraform import grafana_data_source_config_lbac_rules.name "{{ datasource_uid }}"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
resource "grafana_team" "team" {
2+
name = "Team Name"
3+
}
4+
5+
resource "grafana_data_source" "test" {
6+
type = "loki"
7+
name = "loki-from-terraform"
8+
url = "https://mylokiurl.net"
9+
basic_auth_enabled = true
10+
basic_auth_username = "username"
11+
12+
json_data_encoded = jsonencode({
13+
authType = "default"
14+
basicAuthPassword = "password"
15+
})
16+
}
17+
18+
# resource "grafana_data_source_config_lbac_rules" "test_rule" {
19+
# datasource_uid = grafana_data_source.test.uid
20+
# rules = jsonencode({
21+
# "${grafana_team.team.team_uid}" = [
22+
# "{ cluster = \"dev-us-central-0\", namespace = \"hosted-grafana\" }",
23+
# "{ foo = \"qux\" }"
24+
# ]
25+
# })
26+
27+
# depends_on = [
28+
# grafana_team.team,
29+
# grafana_data_source.test
30+
# ]
31+
# }
32+
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/grafana/grafana-openapi-client-go/client/enterprise"
9+
"github.com/grafana/grafana-openapi-client-go/models"
10+
"github.com/grafana/terraform-provider-grafana/v3/internal/common"
11+
"github.com/hashicorp/terraform-plugin-framework/path"
12+
"github.com/hashicorp/terraform-plugin-framework/resource"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
14+
"github.com/hashicorp/terraform-plugin-framework/types"
15+
)
16+
17+
// Note: The LBAC Rules API only supports GET and UPDATE operations.
18+
// There is no CREATE or DELETE API endpoint. The UPDATE operation is used
19+
// for all modifications (create/update/delete) by sending the complete desired
20+
// state. Deleting rules is done by sending an empty rules list.
21+
22+
var (
23+
// Check interface
24+
_ resource.ResourceWithImportState = (*resourceDataSourceConfigLBACRules)(nil)
25+
)
26+
27+
var (
28+
resourceDataSourceConfigLBACRulesName = "grafana_data_source_config_lbac_rules"
29+
resourceDataSourceConfigLBACRulesID = common.NewResourceID(
30+
common.StringIDField("datasource_uid"),
31+
)
32+
)
33+
34+
func makeResourceDataSourceConfigLBACRules() *common.Resource {
35+
resourceStruct := &resourceDataSourceConfigLBACRules{}
36+
return common.NewResource(
37+
common.CategoryGrafanaEnterprise,
38+
resourceDataSourceConfigLBACRulesName,
39+
resourceDataSourceConfigLBACRulesID,
40+
resourceStruct,
41+
)
42+
}
43+
44+
type resourceDataSourceConfigLBACRulesModel struct {
45+
ID types.String `tfsdk:"id"`
46+
DatasourceUID types.String `tfsdk:"datasource_uid"`
47+
Rules types.String `tfsdk:"rules"`
48+
}
49+
50+
type resourceDataSourceConfigLBACRules struct {
51+
client *common.Client
52+
}
53+
54+
func (r *resourceDataSourceConfigLBACRules) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
55+
resp.TypeName = resourceDataSourceConfigLBACRulesName
56+
}
57+
58+
func (r *resourceDataSourceConfigLBACRules) Schema(ctx context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
59+
resp.Schema = schema.Schema{
60+
MarkdownDescription: `
61+
Manages LBAC rules for a data source.
62+
63+
!> 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.
64+
65+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/data-source-management/teamlbac/)
66+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/datasource_lbac_rules/)
67+
68+
This resource requires Grafana >=11.5.0.
69+
`,
70+
Attributes: map[string]schema.Attribute{
71+
"id": schema.StringAttribute{
72+
Computed: true,
73+
},
74+
"datasource_uid": schema.StringAttribute{
75+
Required: true,
76+
Description: "The UID of the datasource.",
77+
},
78+
"rules": schema.StringAttribute{
79+
Required: true,
80+
Description: "JSON-encoded LBAC rules for the data source. Map of team UIDs to lists of rule strings.",
81+
},
82+
},
83+
}
84+
}
85+
86+
func (r *resourceDataSourceConfigLBACRules) Configure(ctx context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
87+
if req.ProviderData == nil {
88+
return
89+
}
90+
r.client = req.ProviderData.(*common.Client)
91+
}
92+
93+
// Add this helper function to handle the common update logic
94+
func (r *resourceDataSourceConfigLBACRules) updateRules(ctx context.Context, data *resourceDataSourceConfigLBACRulesModel, rules map[string][]string) error {
95+
apiRules := make([]*models.TeamLBACRule, 0, len(rules))
96+
for teamUID, ruleList := range rules {
97+
apiRules = append(apiRules, &models.TeamLBACRule{
98+
TeamUID: teamUID,
99+
Rules: ruleList,
100+
})
101+
}
102+
103+
params := &enterprise.UpdateTeamLBACRulesAPIParams{
104+
Context: ctx,
105+
UID: data.DatasourceUID.ValueString(),
106+
Body: &models.UpdateTeamLBACCommand{Rules: apiRules},
107+
}
108+
109+
_, err := r.client.GrafanaAPI.Enterprise.UpdateTeamLBACRulesAPI(params)
110+
if err != nil {
111+
return fmt.Errorf("failed to update LBAC rules for datasource %q: %w", data.DatasourceUID.ValueString(), err)
112+
}
113+
return nil
114+
}
115+
116+
func (r *resourceDataSourceConfigLBACRules) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
117+
var data resourceDataSourceConfigLBACRulesModel
118+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
119+
if resp.Diagnostics.HasError() {
120+
return
121+
}
122+
123+
rulesMap := make(map[string][]string)
124+
if err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap); err != nil {
125+
resp.Diagnostics.AddError(
126+
"Invalid rules JSON",
127+
fmt.Sprintf("Failed to parse rules for datasource %q: %v. Please ensure the rules are valid JSON.", data.DatasourceUID.ValueString(), err),
128+
)
129+
return
130+
}
131+
132+
if err := r.updateRules(ctx, &data, rulesMap); err != nil {
133+
resp.Diagnostics.AddError("Failed to create LBAC rules", err.Error())
134+
return
135+
}
136+
137+
data.ID = types.StringValue(data.DatasourceUID.ValueString())
138+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
139+
}
140+
141+
func (r *resourceDataSourceConfigLBACRules) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
142+
var data resourceDataSourceConfigLBACRulesModel
143+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
144+
if resp.Diagnostics.HasError() {
145+
return
146+
}
147+
148+
datasourceUID := data.DatasourceUID.ValueString()
149+
client := r.client.GrafanaAPI
150+
151+
getResp, err := client.Enterprise.GetTeamLBACRulesAPI(datasourceUID)
152+
if err != nil {
153+
resp.Diagnostics.AddError(
154+
"Failed to get LBAC rules",
155+
fmt.Sprintf("Could not read LBAC rules for datasource %q: %v", datasourceUID, err),
156+
)
157+
return
158+
}
159+
160+
rulesMap := make(map[string][]string)
161+
for _, rule := range getResp.Payload.Rules {
162+
rulesMap[rule.TeamUID] = rule.Rules
163+
}
164+
165+
rulesJSON, err := json.Marshal(rulesMap)
166+
if err != nil {
167+
// Marshal error should never happen for a valid map
168+
resp.Diagnostics.AddError(
169+
"Failed to encode rules",
170+
fmt.Sprintf("Could not encode LBAC rules for datasource %q: %v. This is an internal error, please report it.", datasourceUID, err),
171+
)
172+
return
173+
}
174+
175+
data = resourceDataSourceConfigLBACRulesModel{
176+
ID: types.StringValue(datasourceUID),
177+
DatasourceUID: types.StringValue(datasourceUID),
178+
Rules: types.StringValue(string(rulesJSON)),
179+
}
180+
181+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
182+
}
183+
184+
func (r *resourceDataSourceConfigLBACRules) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
185+
var data resourceDataSourceConfigLBACRulesModel
186+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
187+
if resp.Diagnostics.HasError() {
188+
return
189+
}
190+
191+
rulesMap := make(map[string][]string)
192+
if err := json.Unmarshal([]byte(data.Rules.ValueString()), &rulesMap); err != nil {
193+
resp.Diagnostics.AddError(
194+
"Invalid rules JSON",
195+
fmt.Sprintf("Failed to parse updated rules for datasource %q: %v. Please ensure the rules are valid JSON.", data.DatasourceUID.ValueString(), err),
196+
)
197+
return
198+
}
199+
200+
if err := r.updateRules(ctx, &data, rulesMap); err != nil {
201+
resp.Diagnostics.AddError("Failed to update LBAC rules", err.Error())
202+
return
203+
}
204+
205+
data.ID = types.StringValue(data.DatasourceUID.ValueString())
206+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
207+
}
208+
209+
func (r *resourceDataSourceConfigLBACRules) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
210+
var state resourceDataSourceConfigLBACRulesModel
211+
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
212+
if resp.Diagnostics.HasError() {
213+
return
214+
}
215+
216+
// Pass empty rules map to clear all rules
217+
if err := r.updateRules(ctx, &state, make(map[string][]string)); err != nil {
218+
resp.Diagnostics.AddError(
219+
"Failed to delete LBAC rules",
220+
fmt.Sprintf("Could not delete LBAC rules for datasource %q: %v", state.DatasourceUID.ValueString(), err),
221+
)
222+
return
223+
}
224+
}
225+
226+
func (r *resourceDataSourceConfigLBACRules) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
227+
datasourceUID := req.ID
228+
229+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), datasourceUID)...)
230+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("datasource_uid"), datasourceUID)...)
231+
}

0 commit comments

Comments
 (0)