diff --git a/CHANGELOG.md b/CHANGELOG.md index fe238fb3c..975db6376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## [Unreleased] +- Add `file_contents_wo` and `file_contents_wo_version` write-only attributes to `elasticstack_kibana_import_saved_objects` resource as alternatives to `file_contents` - Fix `elasticstack_elasticsearch_snapshot_lifecycle` metadata type conversion causing terraform apply to fail ([#1409](https://github.com/elastic/terraform-provider-elasticstack/issues/1409)) - Add new `elasticstack_elasticsearch_ml_anomaly_detection_job` resource ([#1329](https://github.com/elastic/terraform-provider-elasticstack/pull/1329)) - Add new `elasticstack_elasticsearch_ml_datafeed` resource ([1340](https://github.com/elastic/terraform-provider-elasticstack/pull/1340)) diff --git a/docs/resources/kibana_import_saved_objects.md b/docs/resources/kibana_import_saved_objects.md index 8c41f2efa..02dd0990c 100644 --- a/docs/resources/kibana_import_saved_objects.md +++ b/docs/resources/kibana_import_saved_objects.md @@ -29,12 +29,11 @@ EOT ## Schema -### Required - -- `file_contents` (String) The contents of the exported saved objects file. - ### Optional +- `file_contents` (String) The contents of the exported saved objects file. +- `file_contents_wo` (String, Sensitive) The contents of the exported saved objects file (write-only, not stored in state). +- `file_contents_wo_version` (String) Version or identifier for the file contents (write-only, not stored in state). - `ignore_import_errors` (Boolean) If set to true, errors during the import process will not fail the configuration application - `overwrite` (Boolean) Overwrites saved objects when they already exist. When used, potential conflict errors are automatically resolved by overwriting the destination object. - `space_id` (String) An identifier for the space. If space_id is not provided, the default space is used. diff --git a/internal/kibana/import_saved_objects/acc_test.go b/internal/kibana/import_saved_objects/acc_test.go index 594b39b99..7f4bb4be7 100644 --- a/internal/kibana/import_saved_objects/acc_test.go +++ b/internal/kibana/import_saved_objects/acc_test.go @@ -1,6 +1,7 @@ package import_saved_objects_test import ( + "regexp" "testing" "github.com/elastic/terraform-provider-elasticstack/internal/acctest" @@ -94,3 +95,139 @@ EOT overwrite = true }` } + +func TestAccResourceImportSavedObjectsWriteOnly(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceImportSavedObjectsWriteOnly(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_test", "success", "true"), + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_test", "success_count", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_test", "success_results.#", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_test", "errors.#", "0"), + ), + }, + }, + }) +} + +func testAccResourceImportSavedObjectsWriteOnly() string { + return ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_import_saved_objects" "wo_test" { + overwrite = true + file_contents_wo = <<-EOT +{"attributes":{"buildNum":42747,"defaultIndex":"metricbeat-*","theme:darkMode":true},"coreMigrationVersion":"7.0.0","id":"7.14.0","managed":false,"references":[],"type":"config","typeMigrationVersion":"7.0.0","updated_at":"2021-08-04T02:04:43.306Z","version":"WzY1MiwyXQ=="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} +EOT +} + ` +} + +func TestAccResourceImportSavedObjectsWriteOnlyWithVersion(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceImportSavedObjectsWriteOnlyWithVersion(), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_version_test", "success", "true"), + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_version_test", "success_count", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_version_test", "success_results.#", "1"), + resource.TestCheckResourceAttr("elasticstack_kibana_import_saved_objects.wo_version_test", "errors.#", "0"), + ), + }, + }, + }) +} + +func testAccResourceImportSavedObjectsWriteOnlyWithVersion() string { + return ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_import_saved_objects" "wo_version_test" { + overwrite = true + file_contents_wo = <<-EOT +{"attributes":{"buildNum":42747,"defaultIndex":"metricbeat-*","theme:darkMode":true},"coreMigrationVersion":"7.0.0","id":"7.14.0","managed":false,"references":[],"type":"config","typeMigrationVersion":"7.0.0","updated_at":"2021-08-04T02:04:43.306Z","version":"WzY1MiwyXQ=="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} +EOT + file_contents_wo_version = "1" +} + ` +} + +func TestAccResourceImportSavedObjectsConflictValidation(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceImportSavedObjectsConflict(), + ExpectError: regexp.MustCompile(`cannot be specified when`), + }, + }, + }) +} + +func testAccResourceImportSavedObjectsConflict() string { + return ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_import_saved_objects" "conflict_test" { + overwrite = true + file_contents = <<-EOT +{"attributes":{"buildNum":42747,"defaultIndex":"metricbeat-*","theme:darkMode":true},"coreMigrationVersion":"7.0.0","id":"7.14.0","managed":false,"references":[],"type":"config","typeMigrationVersion":"7.0.0","updated_at":"2021-08-04T02:04:43.306Z","version":"WzY1MiwyXQ=="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} +EOT + file_contents_wo = <<-EOT +{"attributes":{"buildNum":42747,"defaultIndex":"metricbeat-*","theme:darkMode":false},"coreMigrationVersion":"7.0.0","id":"7.14.0","managed":false,"references":[],"type":"config","typeMigrationVersion":"7.0.0","updated_at":"2021-08-04T02:04:43.306Z","version":"WzY1MiwyXQ=="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} +EOT +} + ` +} + +func TestAccResourceImportSavedObjectsDependencyValidation(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.Providers, + Steps: []resource.TestStep{ + { + Config: testAccResourceImportSavedObjectsDependency(), + ExpectError: regexp.MustCompile(`Attribute "file_contents_wo" must be specified when\s+"file_contents_wo_version" is specified`), + }, + }, + }) +} + +func testAccResourceImportSavedObjectsDependency() string { + return ` +provider "elasticstack" { + elasticsearch {} + kibana {} +} + +resource "elasticstack_kibana_import_saved_objects" "dependency_test" { + overwrite = true + file_contents = <<-EOT +{"attributes":{"buildNum":42747,"defaultIndex":"metricbeat-*","theme:darkMode":true},"coreMigrationVersion":"7.0.0","id":"7.14.0","managed":false,"references":[],"type":"config","typeMigrationVersion":"7.0.0","updated_at":"2021-08-04T02:04:43.306Z","version":"WzY1MiwyXQ=="} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":1,"missingRefCount":0,"missingReferences":[]} +EOT + file_contents_wo_version = "v1.0.0" +} + ` +} diff --git a/internal/kibana/import_saved_objects/create.go b/internal/kibana/import_saved_objects/create.go index d429e324d..195f6d3cd 100644 --- a/internal/kibana/import_saved_objects/create.go +++ b/internal/kibana/import_saved_objects/create.go @@ -15,10 +15,10 @@ import ( ) func (r *Resource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { - r.importObjects(ctx, request.Plan, &response.State, &response.Diagnostics) + r.importObjects(ctx, request.Plan, request.Config, &response.State, &response.Diagnostics) } -func (r *Resource) importObjects(ctx context.Context, plan tfsdk.Plan, state *tfsdk.State, diags *diag.Diagnostics) { +func (r *Resource) importObjects(ctx context.Context, plan tfsdk.Plan, config tfsdk.Config, state *tfsdk.State, diags *diag.Diagnostics) { var model modelV0 diags.Append(plan.Get(ctx, &model)...) @@ -32,7 +32,20 @@ func (r *Resource) importObjects(ctx context.Context, plan tfsdk.Plan, state *tf return } - resp, err := kibanaClient.KibanaSavedObject.Import([]byte(model.FileContents.ValueString()), model.Overwrite.ValueBool(), model.SpaceID.ValueString()) + // Determine which file contents to use (file_contents or file_contents_wo) + // Read write-only attributes from config as per Terraform best practices + var fileContentsWO types.String + diags.Append(config.GetAttribute(ctx, path.Root("file_contents_wo"), &fileContentsWO)...) + if diags.HasError() { + return + } + + fileContents := model.FileContents.ValueString() + if !fileContentsWO.IsNull() && !fileContentsWO.IsUnknown() { + fileContents = fileContentsWO.ValueString() + } + + resp, err := kibanaClient.KibanaSavedObject.Import([]byte(fileContents), model.Overwrite.ValueBool(), model.SpaceID.ValueString()) if err != nil { diags.AddError("failed to import saved objects", err.Error()) return diff --git a/internal/kibana/import_saved_objects/schema.go b/internal/kibana/import_saved_objects/schema.go index fd2fe1e9e..bf2124e8f 100644 --- a/internal/kibana/import_saved_objects/schema.go +++ b/internal/kibana/import_saved_objects/schema.go @@ -4,24 +4,27 @@ import ( "context" "github.com/elastic/terraform-provider-elasticstack/internal/clients" + "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "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/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" ) // Ensure provider defined types fully satisfy framework interfaces var _ resource.Resource = &Resource{} var _ resource.ResourceWithConfigure = &Resource{} +var _ resource.ResourceWithConfigValidators = &Resource{} // TODO - Uncomment these lines when we're using a kibana client which supports create_new_copies and compatibility_mode // create_new_copies and compatibility_mode aren't supported by the current version of the Kibana client // We can add these ourselves once https://github.com/elastic/terraform-provider-elasticstack/pull/372 is merged -// var _ resource.ResourceWithConfigValidators = &Resource{} - // func (r *Resource) ConfigValidators(context.Context) []resource.ConfigValidator { // return []resource.ConfigValidator{ // resourcevalidator.Conflicting( @@ -32,6 +35,15 @@ var _ resource.ResourceWithConfigure = &Resource{} // } // } +func (r *Resource) ConfigValidators(context.Context) []resource.ConfigValidator { + return []resource.ConfigValidator{ + resourcevalidator.AtLeastOneOf( + path.MatchRoot("file_contents"), + path.MatchRoot("file_contents_wo"), + ), + } +} + func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Create sets of Kibana saved objects from a file created by the export API. See https://www.elastic.co/guide/en/kibana/current/saved-objects-api-import.html", @@ -67,7 +79,25 @@ func (r *Resource) Schema(_ context.Context, _ resource.SchemaRequest, resp *res // }, "file_contents": schema.StringAttribute{ Description: "The contents of the exported saved objects file.", - Required: true, + Optional: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("file_contents_wo")), + }, + }, + "file_contents_wo": schema.StringAttribute{ + Description: "The contents of the exported saved objects file (write-only, not stored in state).", + Optional: true, + Sensitive: true, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("file_contents")), + }, + }, + "file_contents_wo_version": schema.StringAttribute{ + Description: "Version or identifier for the file contents (write-only, not stored in state).", + Optional: true, + Validators: []validator.String{ + stringvalidator.AlsoRequires(path.MatchRoot("file_contents_wo")), + }, }, "success": schema.BoolAttribute{ @@ -140,9 +170,11 @@ type modelV0 struct { // CreateNewCopies types.Bool `tfsdk:"create_new_copies"` Overwrite types.Bool `tfsdk:"overwrite"` // CompatibilityMode types.Bool `tfsdk:"compatibility_mode"` - FileContents types.String `tfsdk:"file_contents"` - Success types.Bool `tfsdk:"success"` - SuccessCount types.Int64 `tfsdk:"success_count"` - Errors types.List `tfsdk:"errors"` - SuccessResults types.List `tfsdk:"success_results"` + FileContents types.String `tfsdk:"file_contents"` + FileContentsWO types.String `tfsdk:"file_contents_wo"` + FileContentsWOVersion types.String `tfsdk:"file_contents_wo_version"` + Success types.Bool `tfsdk:"success"` + SuccessCount types.Int64 `tfsdk:"success_count"` + Errors types.List `tfsdk:"errors"` + SuccessResults types.List `tfsdk:"success_results"` } diff --git a/internal/kibana/import_saved_objects/update.go b/internal/kibana/import_saved_objects/update.go index 0731c452c..1b3cfb290 100644 --- a/internal/kibana/import_saved_objects/update.go +++ b/internal/kibana/import_saved_objects/update.go @@ -7,5 +7,5 @@ import ( ) func (r *Resource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { - r.importObjects(ctx, request.Plan, &response.State, &response.Diagnostics) + r.importObjects(ctx, request.Plan, request.Config, &response.State, &response.Diagnostics) }