From 36f267dbc676b6c3e7076db1736d432f84e498df Mon Sep 17 00:00:00 2001 From: matttrach Date: Sat, 4 Oct 2025 00:38:18 -0500 Subject: [PATCH] fix: use temp directory for file conversions Signed-off-by: matttrach --- .../local_snapshot_multiple/README.md | 23 ++++ .../local_snapshot_multiple/backend.tf | 4 + .../use-cases/local_snapshot_multiple/main.tf | 30 ++++++ .../local_snapshot_multiple/outputs.tf | 11 ++ .../local_snapshot_multiple/variables.tf | 15 +++ .../local_snapshot_multiple/versions.tf | 11 ++ go.mod | 1 + internal/provider/file_client/file_client.go | 1 + .../file_client/file_memory_client.go | 7 ++ .../provider/file_client/file_os_client.go | 21 ++++ .../file_local_snapshot/snapshot_resource.go | 26 ++++- test/local_snapshot/multiple/basic_test.go | 100 ++++++++++++++++++ 12 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 examples/use-cases/local_snapshot_multiple/README.md create mode 100644 examples/use-cases/local_snapshot_multiple/backend.tf create mode 100644 examples/use-cases/local_snapshot_multiple/main.tf create mode 100644 examples/use-cases/local_snapshot_multiple/outputs.tf create mode 100644 examples/use-cases/local_snapshot_multiple/variables.tf create mode 100644 examples/use-cases/local_snapshot_multiple/versions.tf create mode 100644 test/local_snapshot/multiple/basic_test.go diff --git a/examples/use-cases/local_snapshot_multiple/README.md b/examples/use-cases/local_snapshot_multiple/README.md new file mode 100644 index 0000000..b1ff1a7 --- /dev/null +++ b/examples/use-cases/local_snapshot_multiple/README.md @@ -0,0 +1,23 @@ +# Basic Snapshot Use Case + +This is an example of how you could use the file_snapshot resource. +WARNING! Please remember that Terraform must load the entire state into memory, + ensure you have enough memory on the server running Terraform to store or retrieve the data you are storing. +For larger files, please see the snapshot_compressed use-case for more details. + +We use the uuid() function for testing purposes. +Every update, the file will be changed and the snapshot will remain the same. + +# Updating the snapshot + +To get the snapshot to update you can send in the "update" argument and change it. +The snapshot will update on that apply and remain static until the update argument is changed again. + +# Base 64 Decode + +Notice that the snapshot outputs use base64decode to return the actual file's value. + +# Snapshots are Sensitive + +You could achieve the goals of this resource using a terraform_data with some life-cycle options, except for this part. +The Snapshot resource's "snapshot" attribute is sensitive, this keeps sensitive or long files from being spewed into the logs. diff --git a/examples/use-cases/local_snapshot_multiple/backend.tf b/examples/use-cases/local_snapshot_multiple/backend.tf new file mode 100644 index 0000000..a1b7f59 --- /dev/null +++ b/examples/use-cases/local_snapshot_multiple/backend.tf @@ -0,0 +1,4 @@ + +terraform { + backend "local" {} +} diff --git a/examples/use-cases/local_snapshot_multiple/main.tf b/examples/use-cases/local_snapshot_multiple/main.tf new file mode 100644 index 0000000..88cde1c --- /dev/null +++ b/examples/use-cases/local_snapshot_multiple/main.tf @@ -0,0 +1,30 @@ + +provider "file" {} + +locals { + update = (var.update == "" ? "code-change-necessary" : var.update) + pesky_id = uuid() + name = var.name + directory = var.directory + count = 5 + files = [for i in range(local.count) : format("%s_%d", local.name, i)] +} +# on first update the pesky_id and the snapshot will match +# on subsequent updates the snapshot will remain as the first id and the pesky_id will change +# when the update input is changed, then the snapshot will match again + +resource "file_local" "snapshot_use_case_multiple" { + name = local.name + directory = local.directory + contents = local.pesky_id +} + +resource "file_local_snapshot" "use_case_multiple" { + depends_on = [ + file_local.snapshot_use_case_multiple, + ] + for_each = toset(local.files) + name = local.name + directory = local.directory + update_trigger = local.update +} diff --git a/examples/use-cases/local_snapshot_multiple/outputs.tf b/examples/use-cases/local_snapshot_multiple/outputs.tf new file mode 100644 index 0000000..71f8346 --- /dev/null +++ b/examples/use-cases/local_snapshot_multiple/outputs.tf @@ -0,0 +1,11 @@ + +output "pesky_id" { + value = local.pesky_id +} + +output "snapshots" { + value = [ + for s in file_local_snapshot.use_case_multiple : base64decode(s.snapshot) + ] + sensitive = true +} diff --git a/examples/use-cases/local_snapshot_multiple/variables.tf b/examples/use-cases/local_snapshot_multiple/variables.tf new file mode 100644 index 0000000..4c50c03 --- /dev/null +++ b/examples/use-cases/local_snapshot_multiple/variables.tf @@ -0,0 +1,15 @@ + +variable "update" { + type = string + default = "code-change-necessary" +} + +variable "directory" { + type = string + default = "." +} + +variable "name" { + type = string + default = "snapshot_resource_basic_use_case.txt" +} diff --git a/examples/use-cases/local_snapshot_multiple/versions.tf b/examples/use-cases/local_snapshot_multiple/versions.tf new file mode 100644 index 0000000..2b7ea2e --- /dev/null +++ b/examples/use-cases/local_snapshot_multiple/versions.tf @@ -0,0 +1,11 @@ + + +terraform { + required_version = ">= 1.5.0" + required_providers { + file = { + source = "rancher/file" + version = ">= 0.0.1" + } + } +} diff --git a/go.mod b/go.mod index 76598aa..ab25a31 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.0 require ( github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/terraform-plugin-framework v1.16.0 github.com/hashicorp/terraform-plugin-framework-validators v0.18.0 github.com/hashicorp/terraform-plugin-go v0.29.0 diff --git a/internal/provider/file_client/file_client.go b/internal/provider/file_client/file_client.go index 16a74ef..cbea2b9 100644 --- a/internal/provider/file_client/file_client.go +++ b/internal/provider/file_client/file_client.go @@ -10,4 +10,5 @@ type FileClient interface { Compress(directory string, name string, compressedName string) error Encode(directory string, name string, encodedName string) error Hash(directory string, name string) (string, error) // Sha256Hash, error + Copy(currentPath string, newPath string) error } diff --git a/internal/provider/file_client/file_memory_client.go b/internal/provider/file_client/file_memory_client.go index 3f4bf4a..8c0eb58 100644 --- a/internal/provider/file_client/file_memory_client.go +++ b/internal/provider/file_client/file_memory_client.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "path/filepath" ) type MemoryFileClient struct { @@ -83,3 +84,9 @@ func (c *MemoryFileClient) Hash(directory string, name string) (string, error) { return hashString, nil } + +func (c *MemoryFileClient) Copy(currentPath string, newPath string) error { + c.file["directory"] = filepath.Dir(newPath) + c.file["name"] = filepath.Base(newPath) + return nil +} diff --git a/internal/provider/file_client/file_os_client.go b/internal/provider/file_client/file_os_client.go index a4a532c..ce2116a 100644 --- a/internal/provider/file_client/file_os_client.go +++ b/internal/provider/file_client/file_os_client.go @@ -142,3 +142,24 @@ func (c *OsFileClient) Hash(directory string, name string) (string, error) { hexContents := hex.EncodeToString(contentsHash) return hexContents, nil } + +func (c *OsFileClient) Copy(currentPath string, newPath string) error { + srcFile, err := os.Open(currentPath) + if err != nil { + return err + } + defer srcFile.Close() + + destFile, err := os.Create(newPath) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, srcFile) + if err != nil { + return err + } + + return nil +} diff --git a/internal/provider/file_local_snapshot/snapshot_resource.go b/internal/provider/file_local_snapshot/snapshot_resource.go index 826e776..e06da60 100644 --- a/internal/provider/file_local_snapshot/snapshot_resource.go +++ b/internal/provider/file_local_snapshot/snapshot_resource.go @@ -3,7 +3,10 @@ package file_local_snapshot import ( "context" "fmt" + "os" + "path/filepath" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -132,9 +135,24 @@ func (r *LocalSnapshotResource) Create(ctx context.Context, req resource.CreateR pDir := plan.Directory.ValueString() pCompress := plan.Compress.ValueBool() + tempDir := filepath.Join(os.TempDir(), "terraform-provider-file", "snapshot", uuid.New().String()) + err := os.MkdirAll(tempDir, os.ModePerm) + if err != nil { + resp.Diagnostics.AddError("Error creating temporary directory: ", err.Error()) + return + } + defer os.RemoveAll(tempDir) + + // copy the file to a temporary directory to prevent issues with encoding and compressing large files + err = r.client.Copy(filepath.Join(pDir, pName), filepath.Join(tempDir, pName)) + if err != nil { + resp.Diagnostics.AddError("Error copying file to temporary directory: ", err.Error()) + return + } + name := pName if pCompress { - err := r.client.Compress(pDir, pName, "compressed_"+pName) + err := r.client.Compress(tempDir, pName, "compressed_"+pName) if err != nil { resp.Diagnostics.AddError("Error compressing file: ", err.Error()) return @@ -142,19 +160,19 @@ func (r *LocalSnapshotResource) Create(ctx context.Context, req resource.CreateR name = "compressed_" + pName } - err := r.client.Encode(pDir, name, "encoded_"+pName) + err = r.client.Encode(tempDir, name, "encoded_"+pName) if err != nil { resp.Diagnostics.AddError("Error encoding file: ", err.Error()) return } - _, encodedContents, err := r.client.Read(pDir, "encoded_"+pName) + _, encodedContents, err := r.client.Read(tempDir, "encoded_"+pName) if err != nil { resp.Diagnostics.AddError("Error reading encoded file: ", err.Error()) return } plan.LocalSnapshot = types.StringValue(encodedContents) - hash, err := r.client.Hash(pDir, pName) + hash, err := r.client.Hash(tempDir, name) if err != nil { resp.Diagnostics.AddError("Error hashing file: ", err.Error()) return diff --git a/test/local_snapshot/multiple/basic_test.go b/test/local_snapshot/multiple/basic_test.go new file mode 100644 index 0000000..2467926 --- /dev/null +++ b/test/local_snapshot/multiple/basic_test.go @@ -0,0 +1,100 @@ +package basic + +import ( + "path/filepath" + "testing" + + "github.com/gruntwork-io/terratest/modules/terraform" + util "github.com/rancher/terraform-provider-file/test" + "github.com/stretchr/testify/assert" +) + +func TestSnapshotMultipleBasic(t *testing.T) { + t.Parallel() + + id := util.GetId() + directory := "local_snapshot_multiple" + repoRoot, err := util.GetRepoRoot(t) + if err != nil { + t.Fatalf("Error getting git root directory: %v", err) + } + exampleDir := filepath.Join(repoRoot, "examples", "use-cases", directory) + testDir := filepath.Join(repoRoot, "test", "data", id) + + err = util.Setup(t, id, "test/data") + if err != nil { + t.Log("Test failed, tearing down...") + util.TearDown(t, testDir, &terraform.Options{}) + t.Fatalf("Error creating test data directories: %s", err) + } + statePath := filepath.Join(testDir, "tfstate") + terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{ + TerraformDir: exampleDir, + Vars: map[string]interface{}{ + "directory": testDir, + "name": "basic_test.txt", + }, + BackendConfig: map[string]interface{}{ + "path": statePath, + }, + EnvVars: map[string]string{ + "TF_DATA_DIR": testDir, + "TF_CLI_CONFIG_FILE": filepath.Join(repoRoot, "test", ".terraformrc"), + "TF_IN_AUTOMATION": "1", + "TF_CLI_ARGS_init": "-no-color", + "TF_CLI_ARGS_plan": "-no-color", + "TF_CLI_ARGS_apply": "-no-color", + "TF_CLI_ARGS_destroy": "-no-color", + "TF_CLI_ARGS_output": "-no-color", + }, + RetryableTerraformErrors: util.GetRetryableTerraformErrors(), + NoColor: true, + Upgrade: true, + // ExtraArgs: terraform.ExtraArgs{ Output: []string{"-json"} }, + }) + + _, err = terraform.InitAndApplyE(t, terraformOptions) + if err != nil { + t.Log("Test failed, tearing down...") + util.TearDown(t, testDir, terraformOptions) + t.Fatalf("Error creating file: %s", err) + } + outputs, err := terraform.OutputAllE(t, terraformOptions) + if err != nil { + t.Log("Output failed, moving along...") + } + + pesky_id := outputs["pesky_id"] + snapshots, ok := outputs["snapshots"].([]interface{}) + if !ok { + t.Fatalf("snapshots is not a []interface{}") + } + a := assert.New(t) + a.Equal(pesky_id, snapshots[0], "On the first run the snapshot will match the id.") + + _, err = terraform.InitAndApplyE(t, terraformOptions) + if err != nil { + t.Log("Test failed, tearing down...") + util.TearDown(t, testDir, terraformOptions) + t.Fatalf("Error creating file: %s", err) + } + outputs, err = terraform.OutputAllE(t, terraformOptions) + if err != nil { + t.Log("Output failed, moving along...") + } + + pesky_id = outputs["pesky_id"] + snapshots, ok = outputs["snapshots"].([]interface{}) + if !ok { + t.Fatalf("snapshots is not a []interface{}") + } + a.NotEqual(pesky_id, snapshots[0], "On subsequent runs the id will change, but the snapshot won't.") + + if t.Failed() { + t.Log("Test failed...") + } else { + t.Log("Test passed...") + } + t.Log("Test complete, tearing down...") + util.TearDown(t, testDir, terraformOptions) +}