diff --git a/cmd/generate/main.go b/cmd/generate/main.go index 64018e26e..ee9979b1a 100644 --- a/cmd/generate/main.go +++ b/cmd/generate/main.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/terraform-provider-grafana/v3/pkg/generate" "github.com/fatih/color" + goVersion "github.com/hashicorp/go-version" "github.com/urfave/cli/v2" ) @@ -68,6 +69,24 @@ This supports a glob format. Examples: EnvVars: []string{"TFGEN_INCLUDE_RESOURCES"}, Required: false, }, + &cli.BoolFlag{ + Name: "output-credentials", + Usage: "Output credentials in the generated resources", + EnvVars: []string{"TFGEN_OUTPUT_CREDENTIALS"}, + Value: false, + }, + &cli.StringFlag{ + Name: "terraform-install-dir", + Usage: `Directory to install Terraform to. If not set, a temporary directory will be created.`, + EnvVars: []string{"TFGEN_TERRAFORM_INSTALL_DIR"}, + Required: false, + }, + &cli.StringFlag{ + Name: "terraform-install-version", + Usage: `Version of Terraform to install. If not set, the latest version _tested in this tool_ will be installed.`, + EnvVars: []string{"TFGEN_TERRAFORM_INSTALL_VERSION"}, + Required: false, + }, // Grafana OSS flags &cli.StringFlag{ @@ -157,10 +176,11 @@ This supports a glob format. Examples: func parseFlags(ctx *cli.Context) (*generate.Config, error) { config := &generate.Config{ - OutputDir: ctx.String("output-dir"), - Clobber: ctx.Bool("clobber"), - Format: generate.OutputFormat(ctx.String("output-format")), - ProviderVersion: ctx.String("terraform-provider-version"), + OutputDir: ctx.String("output-dir"), + Clobber: ctx.Bool("clobber"), + Format: generate.OutputFormat(ctx.String("output-format")), + ProviderVersion: ctx.String("terraform-provider-version"), + OutputCredentials: ctx.Bool("output-credentials"), Grafana: &generate.GrafanaConfig{ URL: ctx.String("grafana-url"), Auth: ctx.String("grafana-auth"), @@ -177,6 +197,16 @@ func parseFlags(ctx *cli.Context) (*generate.Config, error) { StackServiceAccountName: ctx.String("cloud-stack-service-account-name"), }, IncludeResources: ctx.StringSlice("include-resources"), + TerraformInstallConfig: generate.TerraformInstallConfig{ + InstallDir: ctx.String("terraform-install-dir"), + }, + } + var err error + if tfVersion := ctx.String("terraform-install-version"); tfVersion != "" { + config.TerraformInstallConfig.Version, err = goVersion.NewVersion(ctx.String("terraform-install-version")) + if err != nil { + return nil, fmt.Errorf("terraform-install-version must be a valid version: %w", err) + } } if config.ProviderVersion == "" { @@ -184,7 +214,7 @@ func parseFlags(ctx *cli.Context) (*generate.Config, error) { } // Validate flags - err := newFlagValidations(). + err = newFlagValidations(). atLeastOne("grafana-url", "cloud-access-policy-token"). conflicting( []string{"grafana-url", "grafana-auth", "synthetic-monitoring-url", "synthetic-monitoring-access-token", "oncall-url", "oncall-access-token"}, 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/go.mod b/go.mod index e3a3fa7b2..23af00c58 100644 --- a/go.mod +++ b/go.mod @@ -169,7 +169,7 @@ require ( google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect - google.golang.org/grpc v1.64.0 // indirect + google.golang.org/grpc v1.64.1 // indirect google.golang.org/protobuf v1.34.0 // indirect gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index d8c9d9931..114659042 100644 --- a/go.sum +++ b/go.sum @@ -134,8 +134,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1 github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4 h1:e7cZfDiNodjQn63be9m8zfnvMEQAMqHVFswjcbdlspk= -github.com/grafana/amixr-api-go-client v0.0.12-0.20240410110211-c9f68db085c4/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE= github.com/grafana/amixr-api-go-client v0.0.12 h1:oEHZTBhxoZ35EsfeccZBJGPKhZUVOmdSir3WWnSJMLc= github.com/grafana/amixr-api-go-client v0.0.12/go.mod h1:N6x26XUrM5zGtK5zL5vNJnAn2JFMxLFPPLTw/6pDkFE= github.com/grafana/grafana-com-public-clients/go/gcom v0.0.0-20240322153219-42c6a1d2bcab h1:/5R8NO996/keDkZqKXEkU3/QgFs1wzChKYkakjsBpRk= @@ -513,8 +511,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be h1: google.golang.org/genproto/googleapis/api v0.0.0-20240415180920-8c6c420018be/go.mod h1:dvdCTIoAGbkWbcIKBniID56/7XHTt6WfxXNMxuziJ+w= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA= +google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= 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(), diff --git a/pkg/generate/cloud.go b/pkg/generate/cloud.go index 5b5a1d136..a12c73f29 100644 --- a/pkg/generate/cloud.go +++ b/pkg/generate/cloud.go @@ -76,17 +76,17 @@ func generateCloudResources(ctx context.Context, cfg *Config) ([]stack, error) { return nil, err } - postprocessor := &postprocessor{} - if postprocessor.plannedState, err = getPlannedState(ctx, cfg); err != nil { + plannedState, err := getPlannedState(ctx, cfg) + if err != nil { return nil, err } - if err := postprocessor.stripDefaults(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil { + if err := stripDefaults(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil { return nil, err } - if err := postprocessor.wrapJSONFieldsInFunction(filepath.Join(cfg.OutputDir, "cloud-resources.tf")); err != nil { + if err := wrapJSONFieldsInFunction(filepath.Join(cfg.OutputDir, "cloud-resources.tf")); err != nil { return nil, err } - if err := postprocessor.replaceReferences(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), nil); err != nil { + if err := replaceReferences(filepath.Join(cfg.OutputDir, "cloud-resources.tf"), plannedState, nil); err != nil { return nil, err } diff --git a/pkg/generate/config.go b/pkg/generate/config.go index 15ec5d06d..26924d4d1 100644 --- a/pkg/generate/config.go +++ b/pkg/generate/config.go @@ -1,6 +1,9 @@ package generate -import "github.com/hashicorp/terraform-exec/tfexec" +import ( + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-exec/tfexec" +) type OutputFormat string @@ -29,6 +32,11 @@ type CloudConfig struct { StackServiceAccountName string } +type TerraformInstallConfig struct { + InstallDir string + Version *version.Version +} + type Config struct { // IncludeResources is a list of patterns to filter resources by. // If a resource name matches any of the patterns, it will be included in the output. @@ -37,10 +45,13 @@ type Config struct { // OutputDir is the directory to write the generated files to. OutputDir string // Clobber will overwrite existing files in the output directory. - Clobber bool - Format OutputFormat - ProviderVersion string - Grafana *GrafanaConfig - Cloud *CloudConfig - Terraform *tfexec.Terraform + Clobber bool + OutputCredentials bool + Format OutputFormat + ProviderVersion string + Grafana *GrafanaConfig + Cloud *CloudConfig + + TerraformInstallConfig TerraformInstallConfig + Terraform *tfexec.Terraform } diff --git a/pkg/generate/generate.go b/pkg/generate/generate.go index bba1563a7..8dbc85323 100644 --- a/pkg/generate/generate.go +++ b/pkg/generate/generate.go @@ -63,6 +63,7 @@ func Generate(ctx context.Context, cfg *Config) error { cfg.Terraform = tf if cfg.Cloud != nil { + log.Printf("Generating cloud resources") stacks, err := generateCloudResources(ctx, cfg) if err != nil { return err @@ -86,18 +87,26 @@ func Generate(ctx context.Context, cfg *Config) error { onCallToken: cfg.Grafana.OnCallAccessToken, onCallURL: cfg.Grafana.OnCallURL, } + log.Printf("Generating Grafana resources") if err := generateGrafanaResources(ctx, cfg, stack, true); err != nil { return err } } - if cfg.Format == OutputFormatJSON { - return convertToTFJSON(cfg.OutputDir) - } if cfg.Format == OutputFormatCrossplane { return convertToCrossplane(cfg) } + if !cfg.OutputCredentials { + if err := redactCredentials(cfg.OutputDir); err != nil { + return fmt.Errorf("failed to redact credentials: %w", err) + } + } + + if cfg.Format == OutputFormatJSON { + return convertToTFJSON(cfg.OutputDir) + } + return nil } diff --git a/pkg/generate/generate_test.go b/pkg/generate/generate_test.go index 51546fbb7..f7d24a1f4 100644 --- a/pkg/generate/generate_test.go +++ b/pkg/generate/generate_test.go @@ -22,6 +22,9 @@ func TestAccGenerate(t *testing.T) { } testutils.CheckOSSTestsEnabled(t) + // Install Terraform to a temporary directory to avoid reinstalling it for each test case. + installDir := t.TempDir() + cases := []struct { name string config string @@ -113,6 +116,20 @@ func TestAccGenerate(t *testing.T) { }) }, }, + { + name: "with-creds", + config: testutils.TestAccExample(t, "resources/grafana_dashboard/resource.tf"), + generateConfig: func(cfg *generate.Config) { + cfg.IncludeResources = []string{"doesnot.exist"} + cfg.OutputCredentials = true + }, + check: func(t *testing.T, tempDir string) { + assertFiles(t, tempDir, "testdata/generate/empty-with-creds", []string{ + ".terraform", + ".terraform.lock.hcl", + }) + }, + }, { name: "alerting-in-org", config: func() string { @@ -161,6 +178,9 @@ func TestAccGenerate(t *testing.T) { URL: "http://localhost:3000", Auth: "admin:admin", }, + TerraformInstallConfig: generate.TerraformInstallConfig{ + InstallDir: installDir, + }, } if tc.generateConfig != nil { tc.generateConfig(&config) diff --git a/pkg/generate/grafana.go b/pkg/generate/grafana.go index 3e5855584..c7d017e16 100644 --- a/pkg/generate/grafana.go +++ b/pkg/generate/grafana.go @@ -88,20 +88,20 @@ func generateGrafanaResources(ctx context.Context, cfg *Config, stack stack, gen stripDefaultsExtraFields["org_id"] = `"1"` // Remove org_id if it's the default } - postprocessor := &postprocessor{} - if postprocessor.plannedState, err = getPlannedState(ctx, cfg); err != nil { + plannedState, err := getPlannedState(ctx, cfg) + if err != nil { return err } - if err := postprocessor.stripDefaults(generatedFilename("resources.tf"), stripDefaultsExtraFields); err != nil { + if err := stripDefaults(generatedFilename("resources.tf"), stripDefaultsExtraFields); err != nil { return err } - if err := postprocessor.abstractDashboards(generatedFilename("resources.tf")); err != nil { + if err := abstractDashboards(generatedFilename("resources.tf")); err != nil { return err } - if err := postprocessor.wrapJSONFieldsInFunction(generatedFilename("resources.tf")); err != nil { + if err := wrapJSONFieldsInFunction(generatedFilename("resources.tf")); err != nil { return err } - if err := postprocessor.replaceReferences(generatedFilename("resources.tf"), []string{ + if err := replaceReferences(generatedFilename("resources.tf"), plannedState, []string{ "*.org_id=grafana_organization.id", }); err != nil { return err diff --git a/pkg/generate/postprocessing.go b/pkg/generate/postprocessing.go index f55bae94e..3467ef59c 100644 --- a/pkg/generate/postprocessing.go +++ b/pkg/generate/postprocessing.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "log" "os" "path/filepath" "strconv" @@ -140,193 +139,189 @@ var knownReferences = []string{ "grafana_team_preferences.team_id=grafana_team.id", } -type postprocessor struct { - plannedState *tfjson.Plan -} - -func (p *postprocessor) replaceReferences(fpath string, extraKnownReferences []string) error { - file, err := p.readHCLFile(fpath) - if err != nil { - return err - } - - hasChanges := false - - knownReferences := knownReferences - knownReferences = append(knownReferences, extraKnownReferences...) - - plannedResources := p.plannedState.PlannedValues.RootModule.Resources - - for _, block := range file.Body().Blocks() { - var blockResource *tfjson.StateResource - for _, plannedResource := range plannedResources { - if plannedResource.Type == block.Labels()[0] && plannedResource.Name == block.Labels()[1] { - blockResource = plannedResource - break - } - } - if blockResource == nil { - return fmt.Errorf("resource %s.%s not found in planned state", block.Labels()[0], block.Labels()[1]) - } +func replaceReferences(fpath string, plannedState *tfjson.Plan, extraKnownReferences []string) error { + return postprocessFile(fpath, func(file *hclwrite.File) error { + knownReferences := knownReferences + knownReferences = append(knownReferences, extraKnownReferences...) - for attrName := range block.Body().Attributes() { - attrValue := blockResource.AttributeValues[attrName] - attrReplaced := false + plannedResources := plannedState.PlannedValues.RootModule.Resources - // Check the field name. If it has a possible reference, we have to search for it in the resources - for _, ref := range knownReferences { - if attrReplaced { + for _, block := range file.Body().Blocks() { + var blockResource *tfjson.StateResource + for _, plannedResource := range plannedResources { + if plannedResource.Type == block.Labels()[0] && plannedResource.Name == block.Labels()[1] { + blockResource = plannedResource break } + } + if blockResource == nil { + return fmt.Errorf("resource %s.%s not found in planned state", block.Labels()[0], block.Labels()[1]) + } - refFrom := strings.Split(ref, "=")[0] - refTo := strings.Split(ref, "=")[1] - hasPossibleReference := refFrom == fmt.Sprintf("%s.%s", block.Labels()[0], attrName) || (strings.HasPrefix(refFrom, "*.") && strings.HasSuffix(refFrom, fmt.Sprintf(".%s", attrName))) - if !hasPossibleReference { - continue - } + for attrName := range block.Body().Attributes() { + attrValue := blockResource.AttributeValues[attrName] + attrReplaced := false - refToResource := strings.Split(refTo, ".")[0] - refToAttr := strings.Split(refTo, ".")[1] + // Check the field name. If it has a possible reference, we have to search for it in the resources + for _, ref := range knownReferences { + if attrReplaced { + break + } - for _, plannedResource := range plannedResources { - if plannedResource.Type != refToResource { + refFrom := strings.Split(ref, "=")[0] + refTo := strings.Split(ref, "=")[1] + hasPossibleReference := refFrom == fmt.Sprintf("%s.%s", block.Labels()[0], attrName) || (strings.HasPrefix(refFrom, "*.") && strings.HasSuffix(refFrom, fmt.Sprintf(".%s", attrName))) + if !hasPossibleReference { continue } - valueFromRef := plannedResource.AttributeValues[refToAttr] - // If the value from the first block matches the value from the second block, we have a reference - if attrValue == valueFromRef { - // Replace the value with the reference - block.Body().SetAttributeTraversal(attrName, traversal(plannedResource.Type, plannedResource.Name, refToAttr)) - hasChanges = true - attrReplaced = true - break + refToResource := strings.Split(refTo, ".")[0] + refToAttr := strings.Split(refTo, ".")[1] + + for _, plannedResource := range plannedResources { + if plannedResource.Type != refToResource { + continue + } + + valueFromRef := plannedResource.AttributeValues[refToAttr] + // If the value from the first block matches the value from the second block, we have a reference + if attrValue == valueFromRef { + // Replace the value with the reference + block.Body().SetAttributeTraversal(attrName, traversal(plannedResource.Type, plannedResource.Name, refToAttr)) + attrReplaced = true + break + } } } } } - } - - if hasChanges { - log.Printf("Updating file: %s\n", fpath) - return os.WriteFile(fpath, file.Bytes(), 0600) - } - - return nil + return nil + }) } -func (p *postprocessor) stripDefaults(fpath string, extraFieldsToRemove map[string]any) error { - file, err := p.readHCLFile(fpath) +func redactCredentials(dir string) error { + files, err := os.ReadDir(dir) if err != nil { return err } - hasChanges := false - for _, block := range file.Body().Blocks() { - if s := p.stripDefaultsFromBlock(block, extraFieldsToRemove); s { - hasChanges = true + for _, file := range files { + if !strings.HasSuffix(file.Name(), ".tf") { + continue + } + fpath := filepath.Join(dir, file.Name()) + err := postprocessFile(fpath, func(file *hclwrite.File) error { + for _, block := range file.Body().Blocks() { + if block.Type() != "provider" { + continue + } + for name := range block.Body().Attributes() { + if strings.Contains(name, "auth") || strings.Contains(name, "token") { + block.Body().SetAttributeValue(name, cty.StringVal("REDACTED")) + } + } + } + + return nil + }) + if err != nil { + return err } } - if hasChanges { - log.Printf("Updating file: %s\n", fpath) - return os.WriteFile(fpath, file.Bytes(), 0600) - } + return nil } -func (p *postprocessor) wrapJSONFieldsInFunction(fpath string) error { - file, err := p.readHCLFile(fpath) - if err != nil { - return err - } +func stripDefaults(fpath string, extraFieldsToRemove map[string]any) error { + return postprocessFile(fpath, func(file *hclwrite.File) error { + for _, block := range file.Body().Blocks() { + stripDefaultsFromBlock(block, extraFieldsToRemove) + } + return nil + }) +} - hasChanges := false - // Find json attributes and use jsonencode - for _, block := range file.Body().Blocks() { - for key, attr := range block.Body().Attributes() { - asMap, err := p.attributeToMap(attr) - if err != nil || asMap == nil { - continue +func wrapJSONFieldsInFunction(fpath string) error { + return postprocessFile(fpath, func(file *hclwrite.File) error { + // Find json attributes and use jsonencode + for _, block := range file.Body().Blocks() { + for key, attr := range block.Body().Attributes() { + asMap, err := attributeToMap(attr) + if err != nil || asMap == nil { + continue + } + tokens := hclwrite.TokensForValue(HCL2ValueFromConfigValue(asMap)) + block.Body().SetAttributeRaw(key, hclwrite.TokensForFunctionCall("jsonencode", tokens)) } - tokens := hclwrite.TokensForValue(HCL2ValueFromConfigValue(asMap)) - block.Body().SetAttributeRaw(key, hclwrite.TokensForFunctionCall("jsonencode", tokens)) - hasChanges = true } - } - if hasChanges { - log.Printf("Updating file: %s\n", fpath) - return os.WriteFile(fpath, file.Bytes(), 0600) - } - return nil + return nil + }) } -func (p *postprocessor) abstractDashboards(fpath string) error { +func abstractDashboards(fpath string) error { fDir := filepath.Dir(fpath) outPath := filepath.Join(fDir, "files") - file, err := p.readHCLFile(fpath) - if err != nil { - return err - } + return postprocessFile(fpath, func(file *hclwrite.File) error { + dashboardJsons := map[string][]byte{} + for _, block := range file.Body().Blocks() { + labels := block.Labels() + if len(labels) == 0 || labels[0] != "grafana_dashboard" { + continue + } - hasChanges := false - dashboardJsons := map[string][]byte{} - for _, block := range file.Body().Blocks() { - labels := block.Labels() - if len(labels) == 0 || labels[0] != "grafana_dashboard" { - continue - } + dashboard, err := attributeToJSON(block.Body().GetAttribute("config_json")) + if err != nil { + return err + } - dashboard, err := p.attributeToJSON(block.Body().GetAttribute("config_json")) - if err != nil { - return err - } + if dashboard == nil { + continue + } - if dashboard == nil { - continue - } + writeTo := filepath.Join(outPath, fmt.Sprintf("%s.json", block.Labels()[1])) + + // Replace $${ with ${ in the json. No need to escape in the json file + dashboard = []byte(strings.ReplaceAll(string(dashboard), "$${", "${")) + dashboardJsons[writeTo] = dashboard + + // Hacky relative path with interpolation + relativePath := strings.ReplaceAll(writeTo, fDir, "") + pathWithInterpolation := hclwrite.Tokens{ + {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)}, + {Type: hclsyntax.TokenTemplateInterp, Bytes: []byte(`${`)}, + {Type: hclsyntax.TokenIdent, Bytes: []byte(`path.module`)}, + {Type: hclsyntax.TokenTemplateSeqEnd, Bytes: []byte(`}`)}, + {Type: hclsyntax.TokenQuotedLit, Bytes: []byte(relativePath)}, + {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, + } - writeTo := filepath.Join(outPath, fmt.Sprintf("%s.json", block.Labels()[1])) - - // Replace $${ with ${ in the json. No need to escape in the json file - dashboard = []byte(strings.ReplaceAll(string(dashboard), "$${", "${")) - dashboardJsons[writeTo] = dashboard - - // Hacky relative path with interpolation - relativePath := strings.ReplaceAll(writeTo, fDir, "") - pathWithInterpolation := hclwrite.Tokens{ - {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)}, - {Type: hclsyntax.TokenTemplateInterp, Bytes: []byte(`${`)}, - {Type: hclsyntax.TokenIdent, Bytes: []byte(`path.module`)}, - {Type: hclsyntax.TokenTemplateSeqEnd, Bytes: []byte(`}`)}, - {Type: hclsyntax.TokenQuotedLit, Bytes: []byte(relativePath)}, - {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, + block.Body().SetAttributeRaw( + "config_json", + hclwrite.TokensForFunctionCall("file", pathWithInterpolation), + ) } - block.Body().SetAttributeRaw( - "config_json", - hclwrite.TokensForFunctionCall("file", pathWithInterpolation), - ) + if len(dashboardJsons) == 0 { + return nil + } - hasChanges = true - } - if hasChanges { - log.Printf("Updating file: %s\n", fpath) - os.Mkdir(outPath, 0755) + if err := os.Mkdir(outPath, 0755); err != nil { + return err + } for writeTo, dashboard := range dashboardJsons { err := os.WriteFile(writeTo, dashboard, 0600) if err != nil { - panic(err) + return err } } - return os.WriteFile(fpath, file.Bytes(), 0600) - } - return nil + + return nil + }) } -func (p *postprocessor) attributeToMap(attr *hclwrite.Attribute) (map[string]interface{}, error) { +func attributeToMap(attr *hclwrite.Attribute) (map[string]interface{}, error) { var err error // Convert jsonencode to raw json @@ -355,8 +350,8 @@ func (p *postprocessor) attributeToMap(attr *hclwrite.Attribute) (map[string]int return dashboardMap, nil } -func (p *postprocessor) attributeToJSON(attr *hclwrite.Attribute) ([]byte, error) { - jsonMap, err := p.attributeToMap(attr) +func attributeToJSON(attr *hclwrite.Attribute) ([]byte, error) { + jsonMap, err := attributeToMap(attr) if err != nil || jsonMap == nil { return nil, err } @@ -369,53 +364,61 @@ func (p *postprocessor) attributeToJSON(attr *hclwrite.Attribute) ([]byte, error return jsonMarshalled, nil } -func (p *postprocessor) readHCLFile(fpath string) (*hclwrite.File, error) { +type postprocessingFunc func(*hclwrite.File) error + +func postprocessFile(fpath string, fn postprocessingFunc) error { src, err := os.ReadFile(fpath) if err != nil { - return nil, err + return err } file, diags := hclwrite.ParseConfig(src, fpath, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { - return nil, errors.New(diags.Error()) + return errors.New(diags.Error()) + } + initialBytes := file.Bytes() + + if err := fn(file); err != nil { + return err } - return file, nil + // Write the file only if it has changed + if string(initialBytes) != string(file.Bytes()) { + stat, err := os.Stat(fpath) + if err != nil { + return err + } + + if err := os.WriteFile(fpath, file.Bytes(), stat.Mode()); err != nil { + return err + } + } + + return nil } -func (p *postprocessor) stripDefaultsFromBlock(block *hclwrite.Block, extraFieldsToRemove map[string]any) bool { - hasChanges := false +func stripDefaultsFromBlock(block *hclwrite.Block, extraFieldsToRemove map[string]any) { for _, innblock := range block.Body().Blocks() { - if s := p.stripDefaultsFromBlock(innblock, extraFieldsToRemove); s { - hasChanges = true - } + stripDefaultsFromBlock(innblock, extraFieldsToRemove) if len(innblock.Body().Attributes()) == 0 && len(innblock.Body().Blocks()) == 0 { - if rm := block.Body().RemoveBlock(innblock); rm { - hasChanges = true - } + block.Body().RemoveBlock(innblock) } } for name, attribute := range block.Body().Attributes() { if string(attribute.Expr().BuildTokens(nil).Bytes()) == " null" { - if rm := block.Body().RemoveAttribute(name); rm != nil { - hasChanges = true - } + block.Body().RemoveAttribute(name) } if string(attribute.Expr().BuildTokens(nil).Bytes()) == " {}" { - if rm := block.Body().RemoveAttribute(name); rm != nil { - hasChanges = true - } + block.Body().RemoveAttribute(name) } if string(attribute.Expr().BuildTokens(nil).Bytes()) == " []" { - if rm := block.Body().RemoveAttribute(name); rm != nil { - hasChanges = true - } + block.Body().RemoveAttribute(name) } for key, valueToRemove := range extraFieldsToRemove { if name == key { toRemove := false fieldValue := strings.TrimSpace(string(attribute.Expr().BuildTokens(nil).Bytes())) - fieldValue, err := p.extractJSONEncode(fieldValue) + fieldValue, err := extractJSONEncode(fieldValue) if err != nil { continue } @@ -426,16 +429,14 @@ func (p *postprocessor) stripDefaultsFromBlock(block *hclwrite.Block, extraField toRemove = true } if toRemove { - if rm := block.Body().RemoveAttribute(name); rm != nil { - hasChanges = true - } + block.Body().RemoveAttribute(name) } } } } - return hasChanges } -func (p *postprocessor) extractJSONEncode(value string) (string, error) { + +func extractJSONEncode(value string) (string, error) { if !strings.HasPrefix(value, "jsonencode(") { return "", nil } diff --git a/pkg/generate/terraform.go b/pkg/generate/terraform.go index 97994adfc..17c2449ac 100644 --- a/pkg/generate/terraform.go +++ b/pkg/generate/terraform.go @@ -5,11 +5,13 @@ import ( "encoding/json" "errors" "fmt" + "log" "os" "path/filepath" "strings" "github.com/hashicorp/go-version" + "github.com/hashicorp/hc-install/fs" "github.com/hashicorp/hc-install/product" "github.com/hashicorp/hc-install/releases" "github.com/hashicorp/hcl/v2" @@ -20,14 +22,42 @@ import ( ) func setupTerraform(cfg *Config) (*tfexec.Terraform, error) { - installer := &releases.ExactVersion{ - Product: product.Terraform, - Version: version.Must(version.NewVersion("1.8.4")), + var err error + + tfVersion := cfg.TerraformInstallConfig.Version + if tfVersion == nil { + // Not using latest to avoid unexpected breaking changes + log.Printf("No Terraform version specified, defaulting to version 1.8.5") + tfVersion = version.Must(version.NewVersion("1.8.5")) } - execPath, err := installer.Install(context.Background()) - if err != nil { - return nil, fmt.Errorf("error installing Terraform: %s", err) + // Check if Terraform is already installed + var execPath string + if cfg.TerraformInstallConfig.InstallDir != "" { + finder := fs.ExactVersion{ + Product: product.Terraform, + Version: tfVersion, + ExtraPaths: []string{ + cfg.TerraformInstallConfig.InstallDir, + }, + } + + if execPath, err = finder.Find(context.Background()); err == nil { + log.Printf("Terraform %s already installed at %s", tfVersion, execPath) + } + } + + // Install Terraform if not found + if execPath == "" { + log.Printf("Installing Terraform %s", tfVersion) + installer := &releases.ExactVersion{ + Product: product.Terraform, + Version: tfVersion, + InstallDir: cfg.TerraformInstallConfig.InstallDir, + } + if execPath, err = installer.Install(context.Background()); err != nil { + return nil, fmt.Errorf("error installing Terraform: %s", err) + } } tf, err := tfexec.NewTerraform(cfg.OutputDir, execPath) diff --git a/pkg/generate/testdata/generate/alerting-in-org/provider.tf b/pkg/generate/testdata/generate/alerting-in-org/provider.tf index 0538588ae..c1954c662 100644 --- a/pkg/generate/testdata/generate/alerting-in-org/provider.tf +++ b/pkg/generate/testdata/generate/alerting-in-org/provider.tf @@ -9,5 +9,5 @@ terraform { provider "grafana" { url = "http://localhost:3000" - auth = "admin:admin" + auth = "REDACTED" } diff --git a/pkg/generate/testdata/generate/dashboard-filtered/provider.tf b/pkg/generate/testdata/generate/dashboard-filtered/provider.tf index 0538588ae..c1954c662 100644 --- a/pkg/generate/testdata/generate/dashboard-filtered/provider.tf +++ b/pkg/generate/testdata/generate/dashboard-filtered/provider.tf @@ -9,5 +9,5 @@ terraform { provider "grafana" { url = "http://localhost:3000" - auth = "admin:admin" + auth = "REDACTED" } diff --git a/pkg/generate/testdata/generate/dashboard-json/provider.tf.json b/pkg/generate/testdata/generate/dashboard-json/provider.tf.json index 3274a219e..fd24e6bec 100644 --- a/pkg/generate/testdata/generate/dashboard-json/provider.tf.json +++ b/pkg/generate/testdata/generate/dashboard-json/provider.tf.json @@ -2,7 +2,7 @@ "provider": { "grafana": [ { - "auth": "admin:admin", + "auth": "REDACTED", "url": "http://localhost:3000" } ] diff --git a/pkg/generate/testdata/generate/dashboard/provider.tf b/pkg/generate/testdata/generate/dashboard/provider.tf index 0538588ae..c1954c662 100644 --- a/pkg/generate/testdata/generate/dashboard/provider.tf +++ b/pkg/generate/testdata/generate/dashboard/provider.tf @@ -9,5 +9,5 @@ terraform { provider "grafana" { url = "http://localhost:3000" - auth = "admin:admin" + auth = "REDACTED" } diff --git a/pkg/generate/testdata/generate/empty-with-creds/imports.tf b/pkg/generate/testdata/generate/empty-with-creds/imports.tf new file mode 100644 index 000000000..404a262f1 --- /dev/null +++ b/pkg/generate/testdata/generate/empty-with-creds/imports.tf @@ -0,0 +1 @@ +# No resources were found diff --git a/pkg/generate/testdata/generate/empty-with-creds/provider.tf b/pkg/generate/testdata/generate/empty-with-creds/provider.tf new file mode 100644 index 000000000..0538588ae --- /dev/null +++ b/pkg/generate/testdata/generate/empty-with-creds/provider.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + grafana = { + source = "grafana/grafana" + version = "3.0.0" + } + } +} + +provider "grafana" { + url = "http://localhost:3000" + auth = "admin:admin" +} diff --git a/pkg/generate/testdata/generate/empty-with-creds/resources.tf b/pkg/generate/testdata/generate/empty-with-creds/resources.tf new file mode 100644 index 000000000..404a262f1 --- /dev/null +++ b/pkg/generate/testdata/generate/empty-with-creds/resources.tf @@ -0,0 +1 @@ +# No resources were found diff --git a/pkg/generate/testdata/generate/empty/provider.tf b/pkg/generate/testdata/generate/empty/provider.tf index 0538588ae..c1954c662 100644 --- a/pkg/generate/testdata/generate/empty/provider.tf +++ b/pkg/generate/testdata/generate/empty/provider.tf @@ -9,5 +9,5 @@ terraform { provider "grafana" { url = "http://localhost:3000" - auth = "admin:admin" + auth = "REDACTED" } diff --git a/testdata/integration/test.sh b/testdata/integration/test.sh index d3e507ade..7e70da398 100755 --- a/testdata/integration/test.sh +++ b/testdata/integration/test.sh @@ -53,7 +53,8 @@ ${REPO_ROOT}/terraform-provider-grafana-generate \ --grafana-url ${GRAFANA_URL} \ --grafana-auth "admin:admin" \ --clobber \ - --output-dir ${SCRIPT_DIR}/generated + --output-dir ${SCRIPT_DIR}/generated \ + --output-credentials ${REPO_ROOT}/terraform-provider-grafana-generate \ --terraform-provider-version "v3.0.0" \ @@ -61,7 +62,8 @@ ${REPO_ROOT}/terraform-provider-grafana-generate \ --grafana-auth "admin:admin" \ --clobber \ --output-dir ${SCRIPT_DIR}/generated-json \ - --output-format json + --output-format json \ + --output-credentials # Test the generated code for dir in "generated" "generated-json" ; do