diff --git a/docs/data-sources/library_panels.md b/docs/data-sources/library_panels.md new file mode 100644 index 000000000..8525b159c --- /dev/null +++ b/docs/data-sources/library_panels.md @@ -0,0 +1,73 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "grafana_library_panels Data Source - terraform-provider-grafana" +subcategory: "Grafana OSS" +description: |- + +--- + +# grafana_library_panels (Data Source) + + + +## Example Usage + +```terraform +resource "grafana_library_panel" "test" { + name = "panelname" + model_json = jsonencode({ + title = "test name" + type = "text" + version = 0 + description = "test description" + }) +} + +resource "grafana_folder" "test" { + title = "Panel Folder" + uid = "panelname-folder" +} + +resource "grafana_library_panel" "folder" { + name = "panelname In Folder" + folder_uid = grafana_folder.test.uid + model_json = jsonencode({ + gridPos = { + x = 0 + y = 0 + h = 10 + w = 10 + } + title = "panel" + type = "text" + version = 0 + }) +} + +data "grafana_library_panels" "all" { + depends_on = [grafana_library_panel.folder, grafana_library_panel.test] +} +``` + + +## Schema + +### Optional + +- `org_id` (String) The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication. + +### Read-Only + +- `id` (String) The ID of this resource. +- `panels` (Set of Object) (see [below for nested schema](#nestedatt--panels)) + + +### Nested Schema for `panels` + +Read-Only: + +- `description` (String) +- `folder_uid` (String) +- `model_json` (String) +- `name` (String) +- `uid` (String) diff --git a/docs/resources/dashboard_permission_item.md b/docs/resources/dashboard_permission_item.md index 75d56e9ba..c1dd837ab 100644 --- a/docs/resources/dashboard_permission_item.md +++ b/docs/resources/dashboard_permission_item.md @@ -59,7 +59,7 @@ resource "grafana_dashboard_permission_item" "team" { ### Optional -- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used. +- `org_id` (String) The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication. - `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 or service account onto which the permission is to be assigned diff --git a/docs/resources/data_source_permission_item.md b/docs/resources/data_source_permission_item.md index 301399a68..c18e7d671 100644 --- a/docs/resources/data_source_permission_item.md +++ b/docs/resources/data_source_permission_item.md @@ -79,7 +79,7 @@ resource "grafana_data_source_permission_item" "service_account" { ### Optional -- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used. +- `org_id` (String) The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication. - `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 or service account onto which the permission is to be assigned diff --git a/docs/resources/folder_permission_item.md b/docs/resources/folder_permission_item.md index 750b7233e..40e2d9fac 100644 --- a/docs/resources/folder_permission_item.md +++ b/docs/resources/folder_permission_item.md @@ -60,7 +60,7 @@ resource "grafana_folder_permission_item" "on_user" { ### Optional -- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used. +- `org_id` (String) The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication. - `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 or service account onto which the permission is to be assigned diff --git a/docs/resources/role_assignment_item.md b/docs/resources/role_assignment_item.md index a46c1c68b..ddcc52171 100644 --- a/docs/resources/role_assignment_item.md +++ b/docs/resources/role_assignment_item.md @@ -65,7 +65,7 @@ resource "grafana_role_assignment_item" "service_account" { ### Optional -- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used. +- `org_id` (String) The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication. - `service_account_id` (String) the service account onto which the role is to be assigned - `team_id` (String) the team onto which the role is to be assigned - `user_id` (String) the user onto which the role is to be assigned diff --git a/docs/resources/service_account_permission_item.md b/docs/resources/service_account_permission_item.md index a0a32fded..4411d3e95 100644 --- a/docs/resources/service_account_permission_item.md +++ b/docs/resources/service_account_permission_item.md @@ -54,7 +54,7 @@ resource "grafana_service_account_permission_item" "on_user" { ### Optional -- `org_id` (String) The Organization ID. If not set, the Org ID defined in the provider block will be used. +- `org_id` (String) The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication. - `team` (String) the team onto which the permission is to be assigned - `user` (String) the user or service account onto which the permission is to be assigned diff --git a/examples/data-sources/grafana_library_panels/data-source.tf b/examples/data-sources/grafana_library_panels/data-source.tf new file mode 100644 index 000000000..8713236cc --- /dev/null +++ b/examples/data-sources/grafana_library_panels/data-source.tf @@ -0,0 +1,34 @@ +resource "grafana_library_panel" "test" { + name = "panelname" + model_json = jsonencode({ + title = "test name" + type = "text" + version = 0 + description = "test description" + }) +} + +resource "grafana_folder" "test" { + title = "Panel Folder" + uid = "panelname-folder" +} + +resource "grafana_library_panel" "folder" { + name = "panelname In Folder" + folder_uid = grafana_folder.test.uid + model_json = jsonencode({ + gridPos = { + x = 0 + y = 0 + h = 10 + w = 10 + } + title = "panel" + type = "text" + version = 0 + }) +} + +data "grafana_library_panels" "all" { + depends_on = [grafana_library_panel.folder, grafana_library_panel.test] +} diff --git a/internal/resources/grafana/common_plugin_framework.go b/internal/resources/grafana/common_plugin_framework.go index 778a85b58..95a1461e2 100644 --- a/internal/resources/grafana/common_plugin_framework.go +++ b/internal/resources/grafana/common_plugin_framework.go @@ -7,6 +7,7 @@ import ( goapi "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" frameworkSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -15,6 +16,49 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) +type basePluginFrameworkDataSource struct { + client *goapi.GrafanaHTTPAPI + config *goapi.TransportConfig +} + +func (r *basePluginFrameworkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Configure is called multiple times (sometimes when ProviderData is not yet available), we only want to configure once + if req.ProviderData == nil || r.client != nil { + 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 +} + +// 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 *basePluginFrameworkDataSource) clientFromNewOrgResource(orgIDStr string) (*goapi.GrafanaHTTPAPI, int64, error) { + if r.client == nil { + return nil, 0, fmt.Errorf("client not configured") + } + + 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, nil +} + type basePluginFrameworkResource struct { client *goapi.GrafanaHTTPAPI config *goapi.TransportConfig @@ -98,7 +142,7 @@ 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.", + Description: "The Organization ID. If not set, the default organization is used for basic authentication, or the one that owns your service account for token authentication.", PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), &orgIDAttributePlanModifier{}, diff --git a/internal/resources/grafana/data_source_library_panels.go b/internal/resources/grafana/data_source_library_panels.go new file mode 100644 index 000000000..8223da30f --- /dev/null +++ b/internal/resources/grafana/data_source_library_panels.go @@ -0,0 +1,106 @@ +package grafana + +import ( + "context" + "encoding/json" + + "github.com/grafana/grafana-openapi-client-go/client/library_elements" + "github.com/grafana/terraform-provider-grafana/v3/internal/common" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var dataSourceLibraryPanelsName = "grafana_library_panels" + +func datasourceLibraryPanels() *common.DataSource { + return common.NewDataSource( + common.CategoryGrafanaOSS, + dataSourceLibraryPanelsName, + &libraryPanelsDataSource{}, + ) +} + +type libraryPanelsDataSource struct { + basePluginFrameworkDataSource +} + +func (r *libraryPanelsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = dataSourceLibraryPanelsName +} + +func (r *libraryPanelsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "org_id": pluginFrameworkOrgIDAttribute(), + "panels": schema.SetAttribute{ + Computed: true, + ElementType: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "name": types.StringType, + "uid": types.StringType, + "description": types.StringType, + "folder_uid": types.StringType, + "model_json": types.StringType, + }, + }, + }, + }, + } +} + +type libraryPanelsDataSourcePanelModel struct { + Name types.String `tfsdk:"name"` + UID types.String `tfsdk:"uid"` + Description types.String `tfsdk:"description"` + FolderUID types.String `tfsdk:"folder_uid"` + ModelJSON types.String `tfsdk:"model_json"` +} + +type libraryPanelsDataSourceModel struct { + ID types.String `tfsdk:"id"` + OrgID types.String `tfsdk:"org_id"` + Panels []libraryPanelsDataSourcePanelModel `tfsdk:"panels"` +} + +func (r *libraryPanelsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + // Read Terraform state data into the model + var data libraryPanelsDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + // Read from API + client, _, err := r.clientFromNewOrgResource(data.OrgID.ValueString()) + if err != nil { + resp.Diagnostics = diag.Diagnostics{diag.NewErrorDiagnostic("Failed to create client", err.Error())} + return + } + params := library_elements.NewGetLibraryElementsParams().WithKind(common.Ref(libraryPanelKind)) + apiResp, err := client.LibraryElements.GetLibraryElements(params) + if err != nil { + resp.Diagnostics = diag.Diagnostics{diag.NewErrorDiagnostic("Failed to get library panels", err.Error())} + return + } + for _, panel := range apiResp.Payload.Result.Elements { + modelJSONBytes, err := json.Marshal(panel.Model) + if err != nil { + resp.Diagnostics = diag.Diagnostics{diag.NewErrorDiagnostic("Failed to get library panel JSON", err.Error())} + return + } + data.Panels = append(data.Panels, libraryPanelsDataSourcePanelModel{ + Name: types.StringValue(panel.Name), + UID: types.StringValue(panel.UID), + Description: types.StringValue(panel.Description), + FolderUID: types.StringValue(panel.Meta.FolderUID), + ModelJSON: types.StringValue(string(modelJSONBytes)), + }) + } + data.ID = types.StringValue(data.OrgID.ValueString()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, data)...) +} diff --git a/internal/resources/grafana/data_source_library_panels_test.go b/internal/resources/grafana/data_source_library_panels_test.go new file mode 100644 index 000000000..38a0c3555 --- /dev/null +++ b/internal/resources/grafana/data_source_library_panels_test.go @@ -0,0 +1,38 @@ +package grafana_test + +import ( + "testing" + + "github.com/grafana/terraform-provider-grafana/v3/internal/testutils" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccDatasourceLibraryPanels_basic(t *testing.T) { + testutils.CheckOSSTestsEnabled(t, ">=8.0.0") + + randomName := acctest.RandString(10) + + resource.ParallelTest(t, resource.TestCase{ + ProtoV5ProviderFactories: testutils.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testutils.TestAccExampleWithReplace(t, "data-sources/grafana_library_panels/data-source.tf", map[string]string{ + "panelname": randomName, + }), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckTypeSetElemNestedAttrs("data.grafana_library_panels.all", "panels.*", map[string]string{ + "description": "test description", + "folder_uid": "", + "panels.0.name": randomName, + }), + resource.TestCheckTypeSetElemNestedAttrs("data.grafana_library_panels.all", "panels.*", map[string]string{ + "description": "", + "folder_uid": randomName + "-folder", + "panels.0.name": randomName + " In Folder", + }), + ), + }, + }, + }) +} diff --git a/internal/resources/grafana/resources.go b/internal/resources/grafana/resources.go index 60fd8f046..99cf44a5e 100644 --- a/internal/resources/grafana/resources.go +++ b/internal/resources/grafana/resources.go @@ -91,6 +91,7 @@ var DataSources = addValidationToDataSources( datasourceFolder(), datasourceFolders(), datasourceLibraryPanel(), + datasourceLibraryPanels(), datasourceUser(), datasourceUsers(), datasourceRole(),