diff --git a/docs/ephemeral-resources/file.md b/docs/ephemeral-resources/file.md new file mode 100644 index 00000000..74faed03 --- /dev/null +++ b/docs/ephemeral-resources/file.md @@ -0,0 +1,75 @@ +--- +page_title: "local_file Ephemeral Resource - terraform-provider-local" +subcategory: "" +description: |- + Generates an ephemeral local file with the given content. +--- + +# local_file (Ephemeral Resource) + +Generates an ephemeral local file with the given content. + +-> **Note**: Ephemeral resources are available in Terraform v1.10 and later. + +~> **Note about resource behaviour** +Ephemeral resources are considered to be sensitive so none of the arguments +can be displayed in output logs. This means if you use an ephemeral resource +in a provisioner the output will be suppressed. This cannot be overridden using +the `nonsensitive` function as while ephemeral values are considered sensitive +they are not actually sensitive values. + +~> **Note about file content** +File content must be specified with _exactly_ one of the arguments `content`, +`content_base64`, or `source`. + +## Example Usage + +```terraform +ephemeral "local_file" "foo" { + content = "foo!" + filename = "foo.bar" +} + +resource "terraform_data" "foo" { + provisioner "local-exec" { + command = "openssl sha256 ${ephemeral.local_file.foo.filename} > ${ephemeral.local_file.foo.filename}.sha256" + } +} +``` + + +## Schema + +### Required + +- `filename` (String) The path to the file that will be created. + Missing parent directories will be created. + If the file already exists, it will be overridden with the given content. + +### Optional + +- `content` (String) Content to store in the file, expected to be a UTF-8 encoded string. + Conflicts with `content_base64` and `source`. + Exactly one of these three arguments must be specified. +- `content_base64` (String) Content to store in the file, expected to be binary encoded as base64 string. + Conflicts with `content` and `source`. + Exactly one of these three arguments must be specified. +- `directory_permission` (String) Permissions to set for directories created (before umask), expressed as string in + [numeric notation](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation). + Default value is `"0777"`. +- `file_permission` (String) Permissions to set for the output file (before umask), expressed as string in + [numeric notation](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation). + Default value is `"0777"`. +- `source` (String) Path to file to use as source for the one we are creating. + Conflicts with `content` and `content_base64`. + Exactly one of these three arguments must be specified. + +### Read-Only + +- `content_base64sha256` (String) Base64 encoded SHA256 checksum of file content. +- `content_base64sha512` (String) Base64 encoded SHA512 checksum of file content. +- `content_md5` (String) MD5 checksum of file content. +- `content_sha1` (String) SHA1 checksum of file content. +- `content_sha256` (String) SHA256 checksum of file content. +- `content_sha512` (String) SHA512 checksum of file content. +- `id` (String) The hexadecimal encoding of the SHA1 checksum of the file content. \ No newline at end of file diff --git a/examples/ephemeral-resources/ephemeral-resource-file.tf b/examples/ephemeral-resources/ephemeral-resource-file.tf new file mode 100644 index 00000000..bccc4fde --- /dev/null +++ b/examples/ephemeral-resources/ephemeral-resource-file.tf @@ -0,0 +1,10 @@ +ephemeral "local_file" "foo" { + content = "foo!" + filename = "foo.bar" +} + +resource "terraform_data" "foo" { + provisioner "local-exec" { + command = "openssl sha256 ${ephemeral.local_file.foo.filename} > ${ephemeral.local_file.foo.filename}.sha256" + } +} diff --git a/go.mod b/go.mod index f18bbe54..b029c80b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/hashicorp/terraform-plugin-framework v1.15.1 github.com/hashicorp/terraform-plugin-framework-validators v0.18.0 github.com/hashicorp/terraform-plugin-go v0.28.0 + github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.13.3 ) @@ -32,7 +33,6 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.23.0 // indirect github.com/hashicorp/terraform-json v0.25.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.5 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect diff --git a/internal/provider/ephemeral_local_file.go b/internal/provider/ephemeral_local_file.go new file mode 100644 index 00000000..e3f2a429 --- /dev/null +++ b/internal/provider/ephemeral_local_file.go @@ -0,0 +1,265 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/terraform-providers/terraform-provider-local/internal/localtypes" +) + +var ( + _ ephemeral.EphemeralResource = (*localFileEphemeralResource)(nil) +) + +func NewLocalFileEphemeralResource() ephemeral.EphemeralResource { + return &localFileEphemeralResource{} +} + +type localFileEphemeralResource struct{} + +func (e *localFileEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "Generates an ephemeral local file with the given content.", + Attributes: map[string]schema.Attribute{ + "filename": schema.StringAttribute{ + Description: "The path to the file that will be created.\n " + + "Missing parent directories will be created.\n " + + "If the file already exists, it will be overridden with the given content.", + Required: true, + }, + "content": schema.StringAttribute{ + Description: "Content to store in the file, expected to be a UTF-8 encoded string.\n " + + "Conflicts with `content_base64` and `source`.\n " + + "Exactly one of these three arguments must be specified.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf( + path.MatchRoot("content_base64"), + path.MatchRoot("source")), + }, + }, + "content_base64": schema.StringAttribute{ + Description: "Content to store in the file, expected to be binary encoded as base64 string.\n " + + "Conflicts with `content` and `source`.\n " + + "Exactly one of these three arguments must be specified.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf( + path.MatchRoot("content"), + path.MatchRoot("source")), + }, + }, + "source": schema.StringAttribute{ + Description: "Path to file to use as source for the one we are creating.\n " + + "Conflicts with `content` and `content_base64`.\n " + + "Exactly one of these three arguments must be specified.", + Optional: true, + Validators: []validator.String{ + stringvalidator.ExactlyOneOf( + path.MatchRoot("content"), + path.MatchRoot("content_base64")), + }, + }, + "file_permission": schema.StringAttribute{ + CustomType: localtypes.NewFilePermissionType(), + Description: "Permissions to set for the output file (before umask), expressed as string in\n " + + "[numeric notation](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation).\n " + + "Default value is `\"0777\"`.", + Optional: true, + Computed: true, + // Can't set a default value for ephemeral resources, this is here as a fingers-crossed placeholder. + // Default: stringdefault.StaticString("0777"), + }, + "directory_permission": schema.StringAttribute{ + CustomType: localtypes.NewFilePermissionType(), + Description: "Permissions to set for directories created (before umask), expressed as string in\n " + + "[numeric notation](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation).\n " + + "Default value is `\"0777\"`.", + Optional: true, + Computed: true, + // Can't set a default value for ephemeral resources, this is here as a fingers-crossed placeholder. + // Default: stringdefault.StaticString("0777"), + }, + "id": schema.StringAttribute{ + Description: "The hexadecimal encoding of the SHA1 checksum of the file content.", + Computed: true, + }, + "content_md5": schema.StringAttribute{ + Description: "MD5 checksum of file content.", + Computed: true, + }, + "content_sha1": schema.StringAttribute{ + Description: "SHA1 checksum of file content.", + Computed: true, + }, + "content_sha256": schema.StringAttribute{ + Description: "SHA256 checksum of file content.", + Computed: true, + }, + "content_base64sha256": schema.StringAttribute{ + Description: "Base64 encoded SHA256 checksum of file content.", + Computed: true, + }, + "content_sha512": schema.StringAttribute{ + Description: "SHA512 checksum of file content.", + Computed: true, + }, + "content_base64sha512": schema.StringAttribute{ + Description: "Base64 encoded SHA512 checksum of file content.", + Computed: true, + }, + }, + } +} + +func (e *localFileEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_file" // local_file +} + +func (e *localFileEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data localFileEphemeralResourceModelV0 + var filePerm, dirPerm string + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + content, err := parseEphemeralLocalFileContent(data) + if err != nil { + resp.Diagnostics.AddError( + "Create ephemeral local file error", + "An unexpected error occurred while parsing ephemeral local file content\n\n+"+ + fmt.Sprintf("Original Error: %s", err), + ) + return + } + + destination := data.Filename.ValueString() + privateData, _ := json.Marshal(localFilePrivateData{Filename: destination}) + resp.Private.SetKey(ctx, "local_file_data", privateData) + + destinationDir := filepath.Dir(destination) + if _, err := os.Stat(destinationDir); err != nil { + dirPerm = data.DirectoryPermission.ValueString() + if dirPerm == "" { + dirPerm = "0777" + } + dirPermData := localtypes.FilePermissionValue{StringValue: basetypes.NewStringValue(dirPerm)} + data.DirectoryPermission = dirPermData + dirMode, _ := strconv.ParseInt(dirPerm, 8, 64) + if err := os.MkdirAll(destinationDir, os.FileMode(dirMode)); err != nil { + resp.Diagnostics.AddError( + "Create local file error", + "An unexpected error occurred while creating file directory\n\n+"+ + fmt.Sprintf("Original Error: %s", err), + ) + return + } + } + + filePerm = data.FilePermission.ValueString() + if filePerm == "" { + filePerm = "0777" + } + filePermData := localtypes.FilePermissionValue{StringValue: basetypes.NewStringValue(filePerm)} + data.FilePermission = filePermData + + fileMode, _ := strconv.ParseInt(filePerm, 8, 64) + + if err := os.WriteFile(destination, content, os.FileMode(fileMode)); err != nil { + resp.Diagnostics.AddError( + "Create local file error", + "An unexpected error occurred while writing the file\n\n+"+ + fmt.Sprintf("Original Error: %s", err), + ) + return + } + + tflog.Debug(ctx, fmt.Sprintf("Created ephemeral file with name: %s", destination)) + + checksums := genFileChecksums(content) + data.ContentMd5 = types.StringValue(checksums.md5Hex) + data.ContentSha1 = types.StringValue(checksums.sha1Hex) + data.ContentSha256 = types.StringValue(checksums.sha256Hex) + data.ContentBase64sha256 = types.StringValue(checksums.sha256Base64) + data.ContentSha512 = types.StringValue(checksums.sha512Hex) + data.ContentBase64sha512 = types.StringValue(checksums.sha512Base64) + + data.ID = types.StringValue(checksums.sha1Hex) + resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...) +} + +func (e *localFileEphemeralResource) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) { + // Destroy the file + privateBytes, diags := req.Private.GetKey(ctx, "local_file_data") + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + var privateData localFilePrivateData + if err := json.Unmarshal(privateBytes, &privateData); err != nil { + resp.Diagnostics.AddError( + "Private data unmarshal error", + "An unexpected error occurred while unmarshaling private data\n\n+"+ + fmt.Sprintf("Original Error: %s", err), + ) + return + } + + if privateData.Filename != "" { + tflog.Debug(ctx, fmt.Sprintf("Deleting ephemeral file: %s", privateData.Filename)) + os.Remove(privateData.Filename) + } +} + +func parseEphemeralLocalFileContent(data localFileEphemeralResourceModelV0) ([]byte, error) { + if !data.ContentBase64.IsNull() && !data.ContentBase64.IsUnknown() { + return base64.StdEncoding.DecodeString(data.ContentBase64.ValueString()) + } + + if !data.Source.IsNull() && !data.Source.IsUnknown() { + sourceFileContent := data.Source.ValueString() + return os.ReadFile(sourceFileContent) + } + + content := data.Content.ValueString() + return []byte(content), nil +} + +type localFileEphemeralResourceModelV0 struct { + Filename types.String `tfsdk:"filename"` + Content types.String `tfsdk:"content"` + ContentBase64 types.String `tfsdk:"content_base64"` + Source types.String `tfsdk:"source"` + FilePermission localtypes.FilePermissionValue `tfsdk:"file_permission"` + DirectoryPermission localtypes.FilePermissionValue `tfsdk:"directory_permission"` + ID types.String `tfsdk:"id"` + ContentMd5 types.String `tfsdk:"content_md5"` + ContentSha1 types.String `tfsdk:"content_sha1"` + ContentSha256 types.String `tfsdk:"content_sha256"` + ContentBase64sha256 types.String `tfsdk:"content_base64sha256"` + ContentSha512 types.String `tfsdk:"content_sha512"` + ContentBase64sha512 types.String `tfsdk:"content_base64sha512"` +} + +type localFilePrivateData struct { + Filename string `json:"filename"` +} diff --git a/internal/provider/ephemeral_local_file_test.go b/internal/provider/ephemeral_local_file_test.go new file mode 100644 index 00000000..597b07b1 --- /dev/null +++ b/internal/provider/ephemeral_local_file_test.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + r "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestEphemeralLocalFile_Basic_SourceContent(t *testing.T) { + sourceDirPath := t.TempDir() + sourceFilePath := filepath.Join(sourceDirPath, "source_file") + sourceFilePath = strings.ReplaceAll(sourceFilePath, `\`, `\\`) + // create a local file that will be used as the "source" file + if err := createSourceFile(sourceFilePath, "local file content"); err != nil { + t.Fatal(err) + } + + destinationDirPath := t.TempDir() + destinationFilePath := filepath.Join(destinationDirPath, "new_file") + destinationFilePath = strings.ReplaceAll(destinationFilePath, `\`, `\\`) + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Steps: []r.TestStep{ + { + Config: testAccConfigEphemeralLocalSourceFile(sourceFilePath, destinationFilePath), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.local_file", tfjsonpath.New("data").AtMapKey("source"), knownvalue.StringExact(sourceFilePath)), + // statecheck.ExpectKnownOutputValue("local_file", knownvalue.StringExact("local file content")), + }, + Check: checkFileDeleted(destinationFilePath), + }, + }, + CheckDestroy: checkFileDeleted(destinationFilePath), + }) +} + +func TestEphemeralLocalFile_Basic_FileContent(t *testing.T) { + f := filepath.Join(t.TempDir(), "local_file") + f = strings.ReplaceAll(f, `\`, `\\`) + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Steps: []r.TestStep{ + { + Config: testAccConfigEphemeralLocalFileContent("This is some content", f), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.local_file", tfjsonpath.New("data").AtMapKey("content"), knownvalue.StringExact("This is some content")), + }, + Check: checkFileDeleted(f), + }, + }, + CheckDestroy: checkFileDeleted(f), + }) +} + +func TestEphemeralLocalFile_Basic_EncodedBase64Content(t *testing.T) { + f := filepath.Join(t.TempDir(), "local_file") + f = strings.ReplaceAll(f, `\`, `\\`) + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Steps: []r.TestStep{ + { + Config: testAccConfigEphemeralLocalFileEncodedBase64Content("VGhpcyBpcyBzb21lIG1vcmUgY29udGVudAo=", f), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.local_file", tfjsonpath.New("data").AtMapKey("content_base64"), knownvalue.StringExact("VGhpcyBpcyBzb21lIG1vcmUgY29udGVudAo=")), + }, + Check: checkFileDeleted(f), + }, + }, + CheckDestroy: checkFileDeleted(f), + }) +} + +func TestEphemeralLocalFile_Basic_DecodedBase64Content(t *testing.T) { + f := filepath.Join(t.TempDir(), "local_file") + f = strings.ReplaceAll(f, `\`, `\\`) + + r.UnitTest(t, r.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: protoV6ProviderFactories(), + Steps: []r.TestStep{ + { + Config: testAccConfigEphemeralLocalFileDecodedBase64Content("This is some base64 content", f), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.local_file", tfjsonpath.New("data").AtMapKey("content_base64"), knownvalue.StringExact("VGhpcyBpcyBzb21lIGJhc2U2NCBjb250ZW50")), + }, + Check: checkFileDeleted(f), + }, + }, + CheckDestroy: checkFileDeleted(f), + }) +} + +func testAccConfigEphemeralLocalSourceFile(source, filename string) string { + return fmt.Sprintf(` +ephemeral "local_file" "file" { + source = %[1]q + filename = %[2]q +} + +provider "echo" { + data = ephemeral.local_file.file +} + +resource "echo" "local_file" {} +`, source, filename) +} + +func testAccConfigEphemeralLocalFileContent(content, filename string) string { + return fmt.Sprintf(` +ephemeral "local_file" "file" { + content = %[1]q + filename = %[2]q +} + +provider "echo" { + data = ephemeral.local_file.file +} + +resource "echo" "local_file" {} +`, content, filename) +} + +func testAccConfigEphemeralLocalFileEncodedBase64Content(content, filename string) string { + return fmt.Sprintf(` +ephemeral "local_file" "file" { + content_base64 = %[1]q + filename = %[2]q +} + +provider "echo" { + data = ephemeral.local_file.file +} + +resource "echo" "local_file" {} +`, content, filename) +} + +func testAccConfigEphemeralLocalFileDecodedBase64Content(content, filename string) string { + return fmt.Sprintf(` +ephemeral "local_file" "file" { + content_base64 = base64encode(%[1]q) + filename = %[2]q +} + +provider "echo" { + data = ephemeral.local_file.file +} + +resource "echo" "local_file" {} +`, content, filename) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 907ce2f3..b9576f04 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,6 +13,7 @@ import ( "encoding/hex" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" @@ -20,7 +21,8 @@ import ( ) var ( - _ provider.ProviderWithFunctions = (*localProvider)(nil) + _ provider.ProviderWithFunctions = (*localProvider)(nil) + _ provider.ProviderWithEphemeralResources = (*localProvider)(nil) ) func New() provider.Provider { @@ -51,6 +53,12 @@ func (p *localProvider) Resources(ctx context.Context) []func() resource.Resourc } } +func (p *localProvider) EphemeralResources(_ context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + NewLocalFileEphemeralResource, + } +} + func (p *localProvider) Functions(ctx context.Context) []func() function.Function { return []func() function.Function{ NewDirectoryExistsFunction, diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index e77983e3..6553cd35 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -11,6 +11,8 @@ import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/echoprovider" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" ) @@ -21,6 +23,12 @@ func protoV5ProviderFactories() map[string]func() (tfprotov5.ProviderServer, err } } +func protoV6ProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { + return map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + } +} + func providerVersion233() map[string]resource.ExternalProvider { return map[string]resource.ExternalProvider{ "local": { diff --git a/templates/ephemeral-resources/file.md.tmpl b/templates/ephemeral-resources/file.md.tmpl new file mode 100644 index 00000000..3373fadc --- /dev/null +++ b/templates/ephemeral-resources/file.md.tmpl @@ -0,0 +1,29 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +-> **Note**: Ephemeral resources are available in Terraform v1.10 and later. + +~> **Note about resource behaviour** +Ephemeral resources are considered to be sensitive so none of the arguments +can be displayed in output logs. This means if you use an ephemeral resource +in a provisioner the output will be suppressed. This cannot be overridden using +the `nonsensitive` function as while ephemeral values are considered sensitive +they are not actually sensitive values. + +~> **Note about file content** +File content must be specified with _exactly_ one of the arguments `content`, +`content_base64`, or `source`. + +## Example Usage + +{{ tffile "examples/ephemeral-resources/ephemeral-resource-file.tf" }} + +{{ .SchemaMarkdown | trimspace }} \ No newline at end of file