Skip to content

Implement grafana_folder_permission_item resource #1465

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

Merged
merged 4 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
79 changes: 79 additions & 0 deletions docs/resources/folder_permission_item.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "grafana_folder_permission_item Resource - terraform-provider-grafana"
subcategory: "Grafana OSS"
description: |-
Manages a single permission item for a folder. Conflicts with the "grafanafolderpermission" resource which manages the entire set of permissions for a folder.
* Official documentation https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/
* HTTP API https://grafana.com/docs/grafana/latest/developers/http_api/folder_permissions/
---

# grafana_folder_permission_item (Resource)

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.
* [Official documentation](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/access-control/)
* [HTTP API](https://grafana.com/docs/grafana/latest/developers/http_api/folder_permissions/)

## Example Usage

```terraform
resource "grafana_team" "team" {
name = "Team Name"
}

resource "grafana_user" "user" {
email = "[email protected]"
login = "user.name"
password = "my-password"
}

resource "grafana_folder" "collection" {
title = "Folder Title"
}

resource "grafana_folder_permission_item" "on_role" {
folder_uid = grafana_folder.collection.uid
role = "Viewer"
permission = "Edit"
}

resource "grafana_folder_permission_item" "on_team" {
folder_uid = grafana_folder.collection.uid
team = grafana_team.team.id
permission = "View"
}

resource "grafana_folder_permission_item" "on_user" {
folder_uid = grafana_folder.collection.uid
user = grafana_user.user.id
permission = "Admin"
}
```

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

### Required

- `folder_uid` (String) The UID of the folder.
- `permission` (String) the permission to be assigned

### Optional

- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used.
- `role` (String) the role onto which the permission is to be assigned
- `team` (String) the team onto which the permission is to be assigned
- `user` (String) the user onto which the permission is to be assigned

### Read-Only

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

## Import

Import is supported using the following syntax:

```shell
terraform import grafana_folder_permission_item.name "{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
terraform import grafana_folder_permission_item.name "{{ orgID }}:{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
```
2 changes: 2 additions & 0 deletions examples/resources/grafana_folder_permission_item/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
terraform import grafana_folder_permission_item.name "{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
terraform import grafana_folder_permission_item.name "{{ orgID }}:{{ folderUID }}:{{ type (role, team, or user) }}:{{ identifier }}"
32 changes: 32 additions & 0 deletions examples/resources/grafana_folder_permission_item/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
resource "grafana_team" "team" {
name = "Team Name"
}

resource "grafana_user" "user" {
email = "[email protected]"
login = "user.name"
password = "my-password"
}

resource "grafana_folder" "collection" {
title = "Folder Title"
}

resource "grafana_folder_permission_item" "on_role" {
folder_uid = grafana_folder.collection.uid
role = "Viewer"
permission = "Edit"
}

resource "grafana_folder_permission_item" "on_team" {
folder_uid = grafana_folder.collection.uid
team = grafana_team.team.id
permission = "View"
}

resource "grafana_folder_permission_item" "on_user" {
folder_uid = grafana_folder.collection.uid
user = grafana_user.user.id
permission = "Admin"
}

4 changes: 4 additions & 0 deletions internal/common/resource_id.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ type ResourceID struct {
expectedFields []ResourceIDField
}

func (id *ResourceID) Fields() []ResourceIDField {
return id.expectedFields
}

func (id *ResourceID) RequiredFields() []ResourceIDField {
requiredFields := []ResourceIDField{}
for _, f := range id.expectedFields {
Expand Down
144 changes: 144 additions & 0 deletions internal/resources/grafana/common_plugin_framework.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package grafana

import (
"context"
"fmt"
"strconv"

goapi "github.com/grafana/grafana-openapi-client-go/client"
"github.com/grafana/terraform-provider-grafana/v2/internal/common"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
frameworkSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type basePluginFrameworkResource struct {
client *goapi.GrafanaHTTPAPI
config *goapi.TransportConfig
}

func (r *basePluginFrameworkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
resp.Diagnostics.AddError(
"Unconfigured Grafana API client",
"the Grafana API client is required for this resource. Set the auth and url provider attributes",
)

return
}

client, ok := req.ProviderData.(*common.Client)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *common.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client.GrafanaAPI
r.config = client.GrafanaAPIConfig
}

// clientFromExistingOrgResource creates a client from the ID of an org-scoped resource
// Those IDs are in the <orgID>:<resourceID> format
func (r *basePluginFrameworkResource) clientFromExistingOrgResource(idFormat *common.ResourceID, id string) (*goapi.GrafanaHTTPAPI, int64, []any, error) {
client := r.client.Clone()
split, err := idFormat.Split(id)
if err != nil {
return nil, 0, nil, err
}
var orgID int64
if len(split) < len(idFormat.Fields()) {
orgID = client.OrgID()
} else {
orgID = split[0].(int64)
split = split[1:]
client = client.WithOrgID(orgID)
}
return client, orgID, split, nil
}

// clientFromNewOrgResource creates an OpenAPI client from the `org_id` attribute of a resource
// This client is meant to be used in `Create` functions when the ID hasn't already been baked into the resource ID
func (r *basePluginFrameworkResource) clientFromNewOrgResource(orgIDStr string) (*goapi.GrafanaHTTPAPI, int64) {
client := r.client.Clone()
orgID, _ := strconv.ParseInt(orgIDStr, 10, 64)
if orgID == 0 {
orgID = client.OrgID()
} else if orgID > 0 {
client = client.WithOrgID(orgID)
}
return client, orgID
}

// To be used in non-org-scoped resources
// func (r *basePluginFrameworkResource) globalClient() (*goapi.GrafanaHTTPAPI, error) {
// client := r.client.Clone().WithOrgID(0)
// if r.config.APIKey != "" {
// return client, fmt.Errorf("global scope resources cannot be managed with an API key. Use basic auth instead")
// }
// return client, nil
// }

func pluginFrameworkOrgIDAttribute() frameworkSchema.Attribute {
return frameworkSchema.StringAttribute{
Optional: true,
Computed: true,
Description: "The Organization ID. If not set, the Org ID defined in the provider block will be used.",
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
&orgIDAttributePlanModifier{},
},
}
}

type orgIDAttributePlanModifier struct{}

func (d *orgIDAttributePlanModifier) Description(ctx context.Context) string {
return "Ignores the org_id attribute when it is empty, and uses the provider's org_id instead."
}

func (d *orgIDAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
return d.Description(ctx)
}

func (d *orgIDAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
var orgID types.String
diags := req.Plan.GetAttribute(ctx, path.Root("org_id"), &orgID)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// If the org_id is empty, we want to use the provider's org_id
// We don't want to show any diff
if (orgID.IsNull() || orgID.ValueString() == "") && !req.StateValue.IsNull() {
resp.PlanValue = req.StateValue
}
}

type orgScopedAttributePlanModifier struct{}

func (d *orgScopedAttributePlanModifier) Description(ctx context.Context) string {
return "Ignores the orgID part of a resource ID."
}

func (d *orgScopedAttributePlanModifier) MarkdownDescription(ctx context.Context) string {
return d.Description(ctx)
}

func (d *orgScopedAttributePlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
// Equality should ignore the org ID
_, first := SplitOrgResourceID(req.StateValue.ValueString())
_, second := SplitOrgResourceID(resp.PlanValue.ValueString())

if first != "" && first == second {
resp.PlanValue = req.StateValue
}
}
Loading