Skip to content

Commit 137c84c

Browse files
Implement grafana_folder_permission_item resource (#1465)
* Implement `grafana_folder_permission_item` resource with new framework Part of #1000 The `_permission` and `role_assignement` resources only allow managing the whole set of permissions/assignments This the first of a series of new `*_item` resources. It's the biggest one because I had to re-implement some concepts (org ID stuff) from the legacy TF SDK * Comment out unused function * Add example * Fix the example
1 parent f3bb045 commit 137c84c

File tree

10 files changed

+742
-2
lines changed

10 files changed

+742
-2
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
# generated by https://github.com/hashicorp/terraform-plugin-docs
3+
page_title: "grafana_folder_permission_item Resource - terraform-provider-grafana"
4+
subcategory: "Grafana OSS"
5+
description: |-
6+
Manages a single permission item for a folder. Conflicts with the "grafanafolderpermission" resource which manages the entire set of permissions for a folder.
7+
* Official documentation https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/
8+
* HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/folder_permissions/
9+
---
10+
11+
# grafana_folder_permission_item (Resource)
12+
13+
Manages a single permission item for a folder. Conflicts with the "grafana_folder_permission" resource which manages the entire set of permissions for a folder.
14+
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/)
15+
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/folder_permissions/)
16+
17+
## Example Usage
18+
19+
```terraform
20+
resource "grafana_team" "team" {
21+
name = "Team Name"
22+
}
23+
24+
resource "grafana_user" "user" {
25+
26+
login = "user.name"
27+
password = "my-password"
28+
}
29+
30+
resource "grafana_folder" "collection" {
31+
title = "Folder Title"
32+
}
33+
34+
resource "grafana_folder_permission_item" "on_role" {
35+
folder_uid = grafana_folder.collection.uid
36+
role = "Viewer"
37+
permission = "Edit"
38+
}
39+
40+
resource "grafana_folder_permission_item" "on_team" {
41+
folder_uid = grafana_folder.collection.uid
42+
team = grafana_team.team.id
43+
permission = "View"
44+
}
45+
46+
resource "grafana_folder_permission_item" "on_user" {
47+
folder_uid = grafana_folder.collection.uid
48+
user = grafana_user.user.id
49+
permission = "Admin"
50+
}
51+
```
52+
53+
<!-- schema generated by tfplugindocs -->
54+
## Schema
55+
56+
### Required
57+
58+
- `folder_uid` (String) The UID of the folder.
59+
- `permission` (String) the permission to be assigned
60+
61+
### Optional
62+
63+
- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
64+
- `role` (String) the role onto which the permission is to be assigned
65+
- `team` (String) the team onto which the permission is to be assigned
66+
- `user` (String) the user onto which the permission is to be assigned
67+
68+
### Read-Only
69+
70+
- `id` (String) The ID of this resource.
71+
72+
## Import
73+
74+
Import is supported using the following syntax:
75+
76+
```shell
77+
terraform import grafana_folder_permission_item.name "{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
78+
terraform import grafana_folder_permission_item.name "{{ orgID }}:{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
79+
```
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
terraform import grafana_folder_permission_item.name "{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
2+
terraform import grafana_folder_permission_item.name "{{ orgID }}:{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
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_user" "user" {
6+
7+
login = "user.name"
8+
password = "my-password"
9+
}
10+
11+
resource "grafana_folder" "collection" {
12+
title = "Folder Title"
13+
}
14+
15+
resource "grafana_folder_permission_item" "on_role" {
16+
folder_uid = grafana_folder.collection.uid
17+
role = "Viewer"
18+
permission = "Edit"
19+
}
20+
21+
resource "grafana_folder_permission_item" "on_team" {
22+
folder_uid = grafana_folder.collection.uid
23+
team = grafana_team.team.id
24+
permission = "View"
25+
}
26+
27+
resource "grafana_folder_permission_item" "on_user" {
28+
folder_uid = grafana_folder.collection.uid
29+
user = grafana_user.user.id
30+
permission = "Admin"
31+
}
32+

internal/common/resource_id.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ type ResourceID struct {
5656
expectedFields []ResourceIDField
5757
}
5858

59+
func (id *ResourceID) Fields() []ResourceIDField {
60+
return id.expectedFields
61+
}
62+
5963
func (id *ResourceID) RequiredFields() []ResourceIDField {
6064
requiredFields := []ResourceIDField{}
6165
for _, f := range id.expectedFields {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package grafana
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
goapi "github.com/grafana/grafana-openapi-client-go/client"
9+
"github.com/grafana/terraform-provider-grafana/v2/internal/common"
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
frameworkSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
15+
"github.com/hashicorp/terraform-plugin-framework/types"
16+
)
17+
18+
type basePluginFrameworkResource struct {
19+
client *goapi.GrafanaHTTPAPI
20+
config *goapi.TransportConfig
21+
}
22+
23+
func (r *basePluginFrameworkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
24+
if req.ProviderData == nil {
25+
resp.Diagnostics.AddError(
26+
"Unconfigured Grafana API client",
27+
"the Grafana API client is required for this resource. Set the auth and url provider attributes",
28+
)
29+
30+
return
31+
}
32+
33+
client, ok := req.ProviderData.(*common.Client)
34+
35+
if !ok {
36+
resp.Diagnostics.AddError(
37+
"Unexpected Resource Configure Type",
38+
fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
39+
)
40+
41+
return
42+
}
43+
44+
r.client = client.GrafanaAPI
45+
r.config = client.GrafanaAPIConfig
46+
}
47+
48+
// clientFromExistingOrgResource creates a client from the ID of an org-scoped resource
49+
// Those IDs are in the <orgID>:<resourceID> format
50+
func (r *basePluginFrameworkResource) clientFromExistingOrgResource(idFormat *common.ResourceID, id string) (*goapi.GrafanaHTTPAPI, int64, []any, error) {
51+
client := r.client.Clone()
52+
split, err := idFormat.Split(id)
53+
if err != nil {
54+
return nil, 0, nil, err
55+
}
56+
var orgID int64
57+
if len(split) < len(idFormat.Fields()) {
58+
orgID = client.OrgID()
59+
} else {
60+
orgID = split[0].(int64)
61+
split = split[1:]
62+
client = client.WithOrgID(orgID)
63+
}
64+
return client, orgID, split, nil
65+
}
66+
67+
// clientFromNewOrgResource creates an OpenAPI client from the `org_id` attribute of a resource
68+
// This client is meant to be used in `Create` functions when the ID hasn't already been baked into the resource ID
69+
func (r *basePluginFrameworkResource) clientFromNewOrgResource(orgIDStr string) (*goapi.GrafanaHTTPAPI, int64) {
70+
client := r.client.Clone()
71+
orgID, _ := strconv.ParseInt(orgIDStr, 10, 64)
72+
if orgID == 0 {
73+
orgID = client.OrgID()
74+
} else if orgID > 0 {
75+
client = client.WithOrgID(orgID)
76+
}
77+
return client, orgID
78+
}
79+
80+
// To be used in non-org-scoped resources
81+
// func (r *basePluginFrameworkResource) globalClient() (*goapi.GrafanaHTTPAPI, error) {
82+
// client := r.client.Clone().WithOrgID(0)
83+
// if r.config.APIKey != "" {
84+
// return client, fmt.Errorf("global scope resources cannot be managed with an API key. Use basic auth instead")
85+
// }
86+
// return client, nil
87+
// }
88+
89+
func pluginFrameworkOrgIDAttribute() frameworkSchema.Attribute {
90+
return frameworkSchema.StringAttribute{
91+
Optional: true,
92+
Computed: true,
93+
Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
94+
PlanModifiers: []planmodifier.String{
95+
stringplanmodifier.RequiresReplace(),
96+
&orgIDAttributePlanModifier{},
97+
},
98+
}
99+
}
100+
101+
type orgIDAttributePlanModifier struct{}
102+
103+
func (d *orgIDAttributePlanModifier) Description(ctx context.Context) string {
104+
return "Ignores the org_id attribute when it is empty, and uses the provider's org_id instead."
105+
}
106+
107+
func (d *orgIDAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
108+
return d.Description(ctx)
109+
}
110+
111+
func (d *orgIDAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
112+
var orgID types.String
113+
diags := req.Plan.GetAttribute(ctx, path.Root("org_id"), &orgID)
114+
resp.Diagnostics.Append(diags...)
115+
if resp.Diagnostics.HasError() {
116+
return
117+
}
118+
119+
// If the org_id is empty, we want to use the provider's org_id
120+
// We don't want to show any diff
121+
if (orgID.IsNull() || orgID.ValueString() == "") && !req.StateValue.IsNull() {
122+
resp.PlanValue = req.StateValue
123+
}
124+
}
125+
126+
type orgScopedAttributePlanModifier struct{}
127+
128+
func (d *orgScopedAttributePlanModifier) Description(ctx context.Context) string {
129+
return "Ignores the orgID part of a resource ID."
130+
}
131+
132+
func (d *orgScopedAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
133+
return d.Description(ctx)
134+
}
135+
136+
func (d *orgScopedAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
137+
// Equality should ignore the org ID
138+
_, first := SplitOrgResourceID(req.StateValue.ValueString())
139+
_, second := SplitOrgResourceID(resp.PlanValue.ValueString())
140+
141+
if first != "" && first == second {
142+
resp.PlanValue = req.StateValue
143+
}
144+
}

0 commit comments

Comments
 (0)