fix: use temp directory for file conversions

Signed-off-by: matttrach <matt.trachier@suse.com>
This commit is contained in:
matttrach 2025-10-04 00:38:18 -05:00
parent 1d94e1ad26
commit 36f267dbc6
No known key found for this signature in database
GPG Key ID: E082F2592F87D4AE
12 changed files with 246 additions and 4 deletions

View File

@ -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.

View File

@ -0,0 +1,4 @@
terraform {
backend "local" {}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -0,0 +1,11 @@
terraform {
required_version = ">= 1.5.0"
required_providers {
file = {
source = "rancher/file"
version = ">= 0.0.1"
}
}
}

1
go.mod
View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}