Add local directory (#201)

* feat: add local directory management
---------

Signed-off-by: matttrach <matt.trachier@suse.com>
This commit is contained in:
Matt Trachier 2025-10-01 13:16:45 -05:00 committed by GitHub
parent bd0cb43401
commit a1147e54ec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 2362 additions and 186 deletions

View File

@ -27,11 +27,10 @@ testacc: build
gotestsum --format standard-verbose --jsonfile report.json --post-run-command "./summarize.sh" -- ./... -v -p=1 -timeout=300s; \ gotestsum --format standard-verbose --jsonfile report.json --post-run-command "./summarize.sh" -- ./... -v -p=1 -timeout=300s; \
popd; popd;
debug: build et: build
export REPO_ROOT="../../../."; \ export REPO_ROOT="../../../."; \
export TF_LOG=DEBUG; \
pushd ./test; \ pushd ./test; \
gotestsum --format standard-verbose --jsonfile report.json --post-run-command "./summarize.sh" -- ./... -v -p=1 -timeout=300s; \ gotestsum --format standard-verbose --jsonfile report.json --post-run-command "./summarize.sh" -- ./... -v -p=1 -timeout=300s -run=$(t); \
popd; popd;
.PHONY: fmt lint build install generate test testacc debug .PHONY: fmt lint build install generate test testacc debug

View File

@ -0,0 +1,44 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "file_local_directory Data Source - file"
subcategory: ""
description: |-
LocalDirectory File DataSource
---
# file_local_directory (Data Source)
LocalDirectory File DataSource
## Example Usage
```terraform
# tflint-ignore: terraform_unused_declarations
data "file_local_directory" "basic_example" {
path = "example_directory"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `path` (String) Path to directory.
### Read-Only
- `files` (Attributes List) List of information about files in the directory. (see [below for nested schema](#nestedatt--files))
- `id` (String) Identifier derived from sha256 hash of path.
- `permissions` (String) Permissions of the directory.
<a id="nestedatt--files"></a>
### Nested Schema for `files`
Read-Only:
- `is_directory` (String) A string representation of whether or not the item is a directory or a file. This will be 'true' if the item is a directory, or 'false' if it isn't.
- `last_modified` (String) The UTC date of the last time the file was updated.
- `name` (String) The file's name.
- `permissions` (String) The file's permissions mode expressed in string format, eg. '0600'.
- `size` (String) The file's size in bytes.

View File

@ -1,16 +1,16 @@
--- ---
# generated by https://github.com/hashicorp/terraform-plugin-docs # generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "file_snapshot Data Source - file" page_title: "file_local_snapshot Data Source - file"
subcategory: "" subcategory: ""
description: |- description: |-
File Snapshot data source. File LocalSnapshot data source.
This data source retrieves the contents of a file from the output of a file_snapshot datasource.Warning! Using this resource places the plain text contents of the snapshot in your state file. This data source retrieves the contents of a file from the output of a file_local_snapshot datasource.Warning! Using this resource places the plain text contents of the snapshot in your state file.
--- ---
# file_snapshot (Data Source) # file_local_snapshot (Data Source)
File Snapshot data source. File LocalSnapshot data source.
This data source retrieves the contents of a file from the output of a file_snapshot datasource.Warning! Using this resource places the plain text contents of the snapshot in your state file. This data source retrieves the contents of a file from the output of a file_local_snapshot datasource.Warning! Using this resource places the plain text contents of the snapshot in your state file.

View File

@ -0,0 +1,49 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "file_local_directory Resource - file"
subcategory: ""
description: |-
Local Directory resource.
---
# file_local_directory (Resource)
Local Directory resource.
## Example Usage
```terraform
resource "file_local_directory" "basic_example" {
path = "path/to/new/directory"
permissions = "0700"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `path` (String) Directory path, required. All subdirectories will also be created. Changing this forces recreate.
### Optional
- `permissions` (String) The directory permissions to assign to the directory, defaults to '0700'. In order to automatically create subdirectories the owner must have execute access, ie. '0600' or less prevents the provider from creating subdirectories.
### Read-Only
- `created` (String) The top level directory created. eg. if 'path' = '/path/to/new/directory' and '/path/to' already exists, but the rest doesn't, then 'created' will be '/path/to/new'. This path will be recursively removed during destroy and recreate actions.
- `id` (String) Identifier derived from sha256 hash of path.
## Import
Import is supported using the following syntax:
The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
```shell
# IDENTIFIER="$(echo -n "path/to/file" sha256sum | awk '{print $1}')"
terraform import file_local_directory.example "IDENTIFIER"
# after this is run you will need to refine the resource further by setting the path and created properties.
```

View File

@ -1,15 +1,15 @@
--- ---
# generated by https://github.com/hashicorp/terraform-plugin-docs # generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "file_snapshot Resource - file" page_title: "file_local_snapshot Resource - file"
subcategory: "" subcategory: ""
description: |- description: |-
File Snapshot resource. File LocalSnapshot resource.
This resource saves some content in state and doesn't update it until the trigger argument changes. The refresh phase doesn't update state, instead the state can only change on create or update and only when the update_trigger argument changes. This resource saves some content in state and doesn't update it until the trigger argument changes. The refresh phase doesn't update state, instead the state can only change on create or update and only when the update_trigger argument changes.
--- ---
# file_snapshot (Resource) # file_local_snapshot (Resource)
File Snapshot resource. File LocalSnapshot resource.
This resource saves some content in state and doesn't update it until the trigger argument changes. The refresh phase doesn't update state, instead the state can only change on create or update and only when the update_trigger argument changes. This resource saves some content in state and doesn't update it until the trigger argument changes. The refresh phase doesn't update state, instead the state can only change on create or update and only when the update_trigger argument changes.
## Example Usage ## Example Usage
@ -20,7 +20,7 @@ resource "file_local" "snapshot_file_basic_example" {
name = "snapshot_resource_basic_example.txt" name = "snapshot_resource_basic_example.txt"
contents = "this is an example file that is used to show how snapshots work" contents = "this is an example file that is used to show how snapshots work"
} }
resource "file_snapshot" "basic_example" { resource "file_local_snapshot" "basic_example" {
depends_on = [ depends_on = [
file_local.snapshot_file_basic_example, file_local.snapshot_file_basic_example,
] ]
@ -28,21 +28,21 @@ resource "file_snapshot" "basic_example" {
update_trigger = "an arbitrary string" update_trigger = "an arbitrary string"
} }
output "snapshot_basic" { output "snapshot_basic" {
value = file_snapshot.basic_example.snapshot value = file_local_snapshot.basic_example.snapshot
sensitive = true sensitive = true
} }
# A more advanced use case: # A more advanced use case:
# We use a file_local resource to write a local file in the current directory # We use a file_local resource to write a local file in the current directory
# then we create a snapshot of the file using file_snapshot # then we create a snapshot of the file using file_local_snapshot
# then we update the file using a terraform_data resource # then we update the file using a terraform_data resource
# then we get the contents of the file using a file_local datasource # then we get the contents of the file using a file_local datasource
# then we output both the file_local datasource and file_snapshot resource, observing that they are different # then we output both the file_local datasource and file_local_snapshot resource, observing that they are different
resource "file_local" "snapshot_file_example" { resource "file_local" "snapshot_file_example" {
name = "snapshot_resource_test.txt" name = "snapshot_resource_test.txt"
contents = "this is an example file that is used to show how snapshots work" contents = "this is an example file that is used to show how snapshots work"
} }
resource "file_snapshot" "file_example" { resource "file_local_snapshot" "file_example" {
depends_on = [ depends_on = [
file_local.snapshot_file_example, file_local.snapshot_file_example,
] ]
@ -52,7 +52,7 @@ resource "file_snapshot" "file_example" {
resource "terraform_data" "update_file" { resource "terraform_data" "update_file" {
depends_on = [ depends_on = [
file_local.snapshot_file_example, file_local.snapshot_file_example,
file_snapshot.file_example, file_local_snapshot.file_example,
] ]
provisioner "local-exec" { provisioner "local-exec" {
command = <<-EOT command = <<-EOT
@ -63,7 +63,7 @@ resource "terraform_data" "update_file" {
data "file_local" "snapshot_file_example_after_update" { data "file_local" "snapshot_file_example_after_update" {
depends_on = [ depends_on = [
file_local.snapshot_file_example, file_local.snapshot_file_example,
file_snapshot.file_example, file_local_snapshot.file_example,
terraform_data.update_file, terraform_data.update_file,
] ]
name = "snapshot_resource_test.txt" name = "snapshot_resource_test.txt"
@ -75,7 +75,7 @@ output "file" {
# this updates a file that is used to show how snapshots work # this updates a file that is used to show how snapshots work
} }
output "snapshot" { output "snapshot" {
value = base64decode(file_snapshot.file_example.snapshot) value = base64decode(file_local_snapshot.file_example.snapshot)
sensitive = true sensitive = true
# this is an example file that is used to show how snapshots work # this is an example file that is used to show how snapshots work
} }

View File

@ -0,0 +1,5 @@
# tflint-ignore: terraform_unused_declarations
data "file_local_directory" "basic_example" {
path = "example_directory"
}

View File

@ -0,0 +1,4 @@
# IDENTIFIER="$(echo -n "path/to/file" sha256sum | awk '{print $1}')"
terraform import file_local_directory.example "IDENTIFIER"
# after this is run you will need to refine the resource further by setting the path and created properties.

View File

@ -0,0 +1,5 @@
resource "file_local_directory" "basic_example" {
path = "path/to/new/directory"
permissions = "0700"
}

View File

@ -4,7 +4,7 @@ resource "file_local" "snapshot_file_basic_example" {
name = "snapshot_resource_basic_example.txt" name = "snapshot_resource_basic_example.txt"
contents = "this is an example file that is used to show how snapshots work" contents = "this is an example file that is used to show how snapshots work"
} }
resource "file_snapshot" "basic_example" { resource "file_local_snapshot" "basic_example" {
depends_on = [ depends_on = [
file_local.snapshot_file_basic_example, file_local.snapshot_file_basic_example,
] ]
@ -12,21 +12,21 @@ resource "file_snapshot" "basic_example" {
update_trigger = "an arbitrary string" update_trigger = "an arbitrary string"
} }
output "snapshot_basic" { output "snapshot_basic" {
value = file_snapshot.basic_example.snapshot value = file_local_snapshot.basic_example.snapshot
sensitive = true sensitive = true
} }
# A more advanced use case: # A more advanced use case:
# We use a file_local resource to write a local file in the current directory # We use a file_local resource to write a local file in the current directory
# then we create a snapshot of the file using file_snapshot # then we create a snapshot of the file using file_local_snapshot
# then we update the file using a terraform_data resource # then we update the file using a terraform_data resource
# then we get the contents of the file using a file_local datasource # then we get the contents of the file using a file_local datasource
# then we output both the file_local datasource and file_snapshot resource, observing that they are different # then we output both the file_local datasource and file_local_snapshot resource, observing that they are different
resource "file_local" "snapshot_file_example" { resource "file_local" "snapshot_file_example" {
name = "snapshot_resource_test.txt" name = "snapshot_resource_test.txt"
contents = "this is an example file that is used to show how snapshots work" contents = "this is an example file that is used to show how snapshots work"
} }
resource "file_snapshot" "file_example" { resource "file_local_snapshot" "file_example" {
depends_on = [ depends_on = [
file_local.snapshot_file_example, file_local.snapshot_file_example,
] ]
@ -36,7 +36,7 @@ resource "file_snapshot" "file_example" {
resource "terraform_data" "update_file" { resource "terraform_data" "update_file" {
depends_on = [ depends_on = [
file_local.snapshot_file_example, file_local.snapshot_file_example,
file_snapshot.file_example, file_local_snapshot.file_example,
] ]
provisioner "local-exec" { provisioner "local-exec" {
command = <<-EOT command = <<-EOT
@ -47,7 +47,7 @@ resource "terraform_data" "update_file" {
data "file_local" "snapshot_file_example_after_update" { data "file_local" "snapshot_file_example_after_update" {
depends_on = [ depends_on = [
file_local.snapshot_file_example, file_local.snapshot_file_example,
file_snapshot.file_example, file_local_snapshot.file_example,
terraform_data.update_file, terraform_data.update_file,
] ]
name = "snapshot_resource_test.txt" name = "snapshot_resource_test.txt"
@ -59,7 +59,7 @@ output "file" {
# this updates a file that is used to show how snapshots work # this updates a file that is used to show how snapshots work
} }
output "snapshot" { output "snapshot" {
value = base64decode(file_snapshot.file_example.snapshot) value = base64decode(file_local_snapshot.file_example.snapshot)
sensitive = true sensitive = true
# this is an example file that is used to show how snapshots work # this is an example file that is used to show how snapshots work
} }

View File

@ -0,0 +1,20 @@
# Local Directory Use Case
This is a more advanced use case for adding a directory.
The goal of this use case is to retrieve the files in the directory at a specific point in time.
We don't want the live data because we add files that we don't want included in the output.
These are the steps:
1. we generate a directory
2. add files to it
3. get the directory data
4. save the directory data to a file in the directory
5. snapshot the directory data
6. output the snapshot
The resulting output will always be the files we first placed in the directory excluding the directory data file.
On the initial run the directory data will match the snapshot,
but on subsequent runs the refresh phase will update the directory data to include the directory data file.
Our snapshot data will always exclude this file though, since it only updates when we alter the update trigger.

View File

@ -0,0 +1,73 @@
provider "file" {}
locals {
path = var.path
}
resource "file_local_directory" "basic" {
path = local.path
}
resource "file_local" "a" {
depends_on = [
file_local_directory.basic,
]
name = "a"
directory = local.path
contents = "An example file to place in the directory."
}
resource "file_local" "b" {
depends_on = [
file_local_directory.basic,
]
name = "b"
directory = local.path
contents = "An example file to place in the directory."
}
resource "file_local" "c" {
depends_on = [
file_local_directory.basic,
]
name = "c"
directory = local.path
contents = "An example file to place in the directory."
}
data "file_local_directory" "basic" {
depends_on = [
file_local_directory.basic,
file_local.a,
file_local.b,
file_local.c,
]
path = local.path
}
resource "file_local" "directory_info" {
depends_on = [
file_local_directory.basic,
file_local.a,
file_local.b,
file_local.c,
data.file_local_directory.basic,
]
name = "directory_info.txt"
directory = local.path
contents = jsonencode(data.file_local_directory.basic)
}
resource "file_local_snapshot" "directory_snapshot" {
depends_on = [
file_local_directory.basic,
file_local.a,
file_local.b,
file_local.c,
data.file_local_directory.basic,
file_local.directory_info,
]
name = "directory_info.txt"
directory = local.path
update_trigger = "manual"
}

View File

@ -0,0 +1,14 @@
output "directory_resource" {
value = jsonencode(file_local_directory.basic)
}
output "directory_data_source" {
value = jsonencode(data.file_local_directory.basic)
}
output "snapshot" {
value = base64decode(file_local_snapshot.directory_snapshot.snapshot)
sensitive = true
}

View File

@ -0,0 +1,5 @@
variable "path" {
type = string
}

View File

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

View File

@ -0,0 +1,7 @@
# Local Directory Use Case
This is the most basic use case for adding a directory.
It creates the directory at the path specified, generating all subdirectories necessary to do so.
It records the created directories and deletes only the ones it created on destroy.
This example will only set the required fields, all options accept the default value.

View File

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

View File

@ -0,0 +1,18 @@
provider "file" {}
locals {
path = var.path
}
resource "file_local_directory" "basic" {
path = local.path
}
data "file_local_directory" "basic" {
depends_on = [
file_local_directory.basic,
]
path = local.path
}

View File

@ -0,0 +1,9 @@
output "directory_resource" {
value = jsonencode(file_local_directory.basic)
}
output "directory_data_source" {
value = jsonencode(data.file_local_directory.basic)
}

View File

@ -0,0 +1,5 @@
variable "path" {
type = string
}

View File

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

View File

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

View File

@ -16,7 +16,7 @@ resource "file_local" "snapshot_use_case_basic" {
directory = local.directory directory = local.directory
contents = local.pesky_id contents = local.pesky_id
} }
resource "file_snapshot" "use_case_basic" { resource "file_local_snapshot" "use_case_basic" {
depends_on = [ depends_on = [
file_local.snapshot_use_case_basic, file_local.snapshot_use_case_basic,
] ]

View File

@ -4,6 +4,6 @@ output "pesky_id" {
} }
output "snapshot" { output "snapshot" {
value = base64decode(file_snapshot.use_case_basic.snapshot) value = base64decode(file_local_snapshot.use_case_basic.snapshot)
sensitive = true sensitive = true
} }

View File

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

View File

@ -1,6 +1,6 @@
# Compressed Snapshot Use Case # Compressed Snapshot Use Case
This is an example of how you could use the file_snapshot resource. This is an example of how you could use the file_local_snapshot resource.
WARNING! Please remember that Terraform must load the entire state into memory, WARNING! Please remember that Terraform must load the entire state into memory,
make sure you have the resources available on the machine running Terraform to handle any file you save like this. make sure you have the resources available on the machine running Terraform to handle any file you save like this.

View File

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

View File

@ -16,7 +16,7 @@ resource "file_local" "snapshot_use_case_compressed" {
directory = local.directory directory = local.directory
contents = local.pesky_id contents = local.pesky_id
} }
resource "file_snapshot" "use_case_compressed" { resource "file_local_snapshot" "use_case_compressed" {
depends_on = [ depends_on = [
file_local.snapshot_use_case_compressed, file_local.snapshot_use_case_compressed,
] ]
@ -25,11 +25,11 @@ resource "file_snapshot" "use_case_compressed" {
update_trigger = local.update update_trigger = local.update
compress = true compress = true
} }
data "file_snapshot" "use_case_compressed" { data "file_local_snapshot" "use_case_compressed" {
depends_on = [ depends_on = [
file_local.snapshot_use_case_compressed, file_local.snapshot_use_case_compressed,
file_snapshot.use_case_compressed, file_local_snapshot.use_case_compressed,
] ]
contents = file_snapshot.use_case_compressed.snapshot contents = file_local_snapshot.use_case_compressed.snapshot
decompress = true decompress = true
} }

View File

@ -4,6 +4,6 @@ output "pesky_id" {
} }
output "snapshot" { output "snapshot" {
value = data.file_snapshot.use_case_compressed.data value = data.file_local_snapshot.use_case_compressed.data
sensitive = true sensitive = true
} }

View File

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

View File

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1758262103, "lastModified": 1759070547,
"narHash": "sha256-aBGl3XEOsjWw6W3AHiKibN7FeoG73dutQQEqnd/etR8=", "narHash": "sha256-JVZl8NaVRYb0+381nl7LvPE+A774/dRpif01FKLrYFQ=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "12bd230118a1901a4a5d393f9f56b6ad7e571d01", "rev": "647e5c14cbd5067f44ac86b74f014962df460840",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -0,0 +1,10 @@
package directory_client
type DirectoryClient interface {
Create(path string, permissions string) (string, error) // Base of the newly created path (used in destroy), error
// If directory isn't found the error message must have err.Error() == "directory not found"
Read(path string) (string, map[string]map[string]string, error) // permissions, files info map, error
Update(path string, permissions string) error
Delete(path string) error // "path" should be the return from Create
CreateFile(path string, data string, permissions string, lastModified string) error // create a file in the given directory
}

View File

@ -0,0 +1,68 @@
package directory_client
import (
"fmt"
"path/filepath"
"strings"
)
var _ DirectoryClient = &MemoryDirectoryClient{} // make sure the MemoryDirectoryClient implements the DirectoryClient
type MemoryDirectoryClient struct {
directory map[string]interface{}
}
func (c *MemoryDirectoryClient) Create(path string, permissions string) (string, error) {
path = filepath.Clean(path)
var base string
if filepath.IsAbs(path) {
base = filepath.VolumeName(path) + string(filepath.Separator)
} else {
p := strings.Split(path, string(filepath.Separator))
base = p[0]
}
c.directory = make(map[string]interface{})
c.directory["permissions"] = permissions
c.directory["path"] = path
c.directory["base"] = base
c.directory["info"] = map[string]map[string]string{}
return base, nil
}
func (c *MemoryDirectoryClient) Read(path string) (string, map[string]map[string]string, error) {
if c.directory == nil {
return "", nil, fmt.Errorf("directory not found")
}
permissions, _ := c.directory["permissions"].(string)
info, _ := c.directory["info"].(map[string]map[string]string)
return permissions, info, nil
}
func (c *MemoryDirectoryClient) Update(path string, permissions string) error {
c.directory["permissions"] = permissions
return nil
}
func (c *MemoryDirectoryClient) Delete(path string) error {
c.directory = nil
return nil
}
func (c *MemoryDirectoryClient) CreateFile(path string, data string, permissions string, lastModified string) error {
if c.directory == nil {
return fmt.Errorf("directory not found")
}
info, ok := c.directory["info"].(map[string]map[string]string)
if !ok {
return fmt.Errorf("directory info not found")
}
info[path] = map[string]string{
"Size": fmt.Sprintf("%d", len(data)),
"Mode": permissions,
"ModTime": lastModified,
"IsDir": "false",
}
return nil
}

View File

@ -0,0 +1,147 @@
package directory_client
import (
"fmt"
"os"
"path/filepath"
"strconv"
)
var _ DirectoryClient = &OsDirectoryClient{} // make sure the OsDirectoryClient implements the DirectoryClient
type OsDirectoryClient struct {
}
func (c *OsDirectoryClient) Create(path string, permissions string) (string, error) {
created, err := makePath(path, permissions)
if len(created) > 0 {
fmt.Printf("created: %#v", created)
return created[0], err
} else {
return "", err
}
}
func (c *OsDirectoryClient) Read(path string) (string, map[string]map[string]string, error) {
info, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil, fmt.Errorf("directory not found")
}
return "", nil, err
}
mode := fmt.Sprintf("%#o", info.Mode().Perm())
data, err := os.ReadDir(path)
if err != nil {
return "", nil, err
}
files := make(map[string]map[string]string)
for _, file := range data {
var isDir string
fileInfo, err := file.Info()
if err != nil {
return "", nil, err
}
if fileInfo.IsDir() {
isDir = "true"
} else {
isDir = "false"
}
files[file.Name()] = map[string]string{
"Size": strconv.FormatInt(fileInfo.Size(), 10),
"Mode": fmt.Sprintf("%#o", fileInfo.Mode().Perm()),
"ModTime": fileInfo.ModTime().String(),
"IsDir": isDir,
}
}
return mode, files, nil
}
// The only thing that can be updated is the permissions.
func (c *OsDirectoryClient) Update(path string, permissions string) error {
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
err = os.Chmod(path, os.FileMode(modeInt))
if err != nil {
return err
}
return nil
}
func (c *OsDirectoryClient) Delete(path string) error {
if path == "" {
return nil
}
return os.RemoveAll(path)
}
func makePath(path string, permissions string) ([]string, error) {
var created []string
info, err := os.Stat(path)
if err == nil {
if info.IsDir() {
return created, nil // Path already exists and is a directory.
}
// Path exists but is a file, which is an error.
return nil, fmt.Errorf("path '%s' exists and is not a directory", path)
}
if !os.IsNotExist(err) {
// There was an error, but not that the directory doesn't exist, something is up with the file system.
return nil, err
}
// From here we know the filesystem is ready and the path doesn't exist.
parent := filepath.Dir(path)
if path == parent {
// If we have reached a path with no parent then return the empty list.
// This breaks the recursion.
return created, nil
}
// Start a recursion.
// This will recurse until path = parent, where parentCreated will be the empty list.
parentCreated, err := makePath(parent, permissions)
if err != nil {
return nil, err
}
// Add the parent's created directories to our list.
created = append(created, parentCreated...)
// The first time this point is reached we know:
// - "created" is an empty list.
// - the parent directory exists and is a valid directory.
// - the filesystem is ready and the current path doesn't exist.
// This means we are good to create the current path and return it.
// Any other time we know:
// - the parent directory exists and is a valid directory.
// - the filesystem is ready and the current path doesn't exist.
// - there was no error in the previous recursion.
// This means the previous recursion must have successfully generated a directory.
// In any recursion cycle from this point the parent directory exists and is valid.
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return nil, err
}
if err := os.Mkdir(path, os.FileMode(modeInt)); err != nil {
return nil, err
} else {
// We successfully created the directory, add it to our list.
created = append(created, path)
}
return created, nil
}
// Added to help with testing, use the file client to create files in production.
func (c *OsDirectoryClient) CreateFile(path string, data string, permissions string, lastModified string) error {
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
return os.WriteFile(path, []byte(data), os.FileMode(modeInt))
}

View File

@ -6,6 +6,7 @@ type FileClient interface {
Read(directory string, name string) (string, string, error) // permissions, contents, error Read(directory string, name string) (string, string, error) // permissions, contents, error
Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error
Delete(directory string, name string) error Delete(directory string, name string) error
Compress(directory string, name string, compressedName string) error Compress(directory string, name string, compressedName string) error
Encode(directory string, name string, encodedName string) error Encode(directory string, name string, encodedName string) error
Hash(directory string, name string) (string, error) // Sha256Hash, error Hash(directory string, name string) (string, error) // Sha256Hash, error

View File

@ -0,0 +1,156 @@
// SPDX-License-Identifier: MPL-2.0
package file_local_directory
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
c "github.com/rancher/terraform-provider-file/internal/provider/directory_client"
)
// The `var _` is a special Go construct that results in an unusable variable.
// The purpose of these lines is to make sure our LocalDirectoryFileResource correctly implements the `resource.Resource“ interface.
// These will fail at compilation time if the implementation is not satisfied.
var _ datasource.DataSource = &LocalDirectoryDataSource{}
func NewLocalDirectoryDataSource() datasource.DataSource {
return &LocalDirectoryDataSource{}
}
type LocalDirectoryDataSource struct {
client c.DirectoryClient
}
type LocalDirectoryDataSourceModel struct {
Id types.String `tfsdk:"id"`
Path types.String `tfsdk:"path"`
Permissions types.String `tfsdk:"permissions"`
Files []LocalDirectoryFileInfoModel `tfsdk:"files"`
}
type LocalDirectoryFileInfoModel struct {
Name types.String `tfsdk:"name"`
Size types.String `tfsdk:"size"`
Permissions types.String `tfsdk:"permissions"`
LastModified types.String `tfsdk:"last_modified"`
IsDirectory types.String `tfsdk:"is_directory"`
}
func (r *LocalDirectoryDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_local_directory" // file_local_directory datasource
}
func (r *LocalDirectoryDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "LocalDirectory File DataSource",
Attributes: map[string]schema.Attribute{
"path": schema.StringAttribute{
MarkdownDescription: "Path to directory.",
Required: true,
},
"permissions": schema.StringAttribute{
MarkdownDescription: "Permissions of the directory.",
Computed: true,
},
"files": schema.ListNestedAttribute{
MarkdownDescription: "List of information about files in the directory.",
Computed: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
MarkdownDescription: "The file's name. ",
Computed: true,
},
"size": schema.StringAttribute{
MarkdownDescription: "The file's size in bytes. ",
Computed: true,
},
"permissions": schema.StringAttribute{
MarkdownDescription: "The file's permissions mode expressed in string format, eg. '0600'. ",
Computed: true,
},
"last_modified": schema.StringAttribute{
MarkdownDescription: "The UTC date of the last time the file was updated. ",
Computed: true,
},
"is_directory": schema.StringAttribute{
MarkdownDescription: "A string representation of whether or not the item is a directory or a file. " +
"This will be 'true' if the item is a directory, or 'false' if it isn't.",
Computed: true,
},
},
},
},
"id": schema.StringAttribute{
MarkdownDescription: "Identifier derived from sha256 hash of path. ",
Computed: true,
},
},
}
}
func (r *LocalDirectoryDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
}
// Read runs before all other resources are run, datasources only get the Read function.
func (r *LocalDirectoryDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
tflog.Debug(ctx, fmt.Sprintf("Request Object: %#v", req))
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsDirectoryClient.")
r.client = &c.OsDirectoryClient{}
}
var config LocalDirectoryDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
id := config.Id.ValueString()
path := config.Path.ValueString()
perm, files, err := r.client.Read(path)
if err != nil && err.Error() == "directory not found" {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError("failed to read directory", err.Error())
return
}
if id == "" {
hasher := sha256.New()
hasher.Write([]byte(path))
id = hex.EncodeToString(hasher.Sum(nil))
config.Id = types.StringValue(id)
}
config.Permissions = types.StringValue(perm)
config.Files = []LocalDirectoryFileInfoModel{}
for fileName, fileData := range files {
fileInfo := LocalDirectoryFileInfoModel{
Name: types.StringValue(fileName),
Size: types.StringValue(fileData["Size"]),
Permissions: types.StringValue(fileData["Mode"]),
LastModified: types.StringValue(fileData["ModTime"]),
IsDirectory: types.StringValue(fileData["IsDir"]),
}
config.Files = append(config.Files, fileInfo)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
tflog.Debug(ctx, fmt.Sprintf("Response Object: %#v", *resp))
}

View File

@ -0,0 +1,305 @@
// SPDX-License-Identifier: MPL-2.0
package file_local_directory
import (
"context"
"math/rand"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-go/tftypes"
c "github.com/rancher/terraform-provider-file/internal/provider/directory_client"
)
const (
// echo -n '/tmp/foo' | sha256sum | awk '{print $1}' #.
testDirectoryId = "e2e1dcd28fea64180e4cd859b299ce67c4c02a3cbd49eca0042f7b5b47d241b5"
testDirectoryPath = "/tmp/foo"
defaultDirectoryPerm = "0755"
)
func TestLocalDirectoryDataSourceMetadata(t *testing.T) {
t.Run("Metadata function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryDataSource
want datasource.MetadataResponse
}{
{"Basic test", LocalDirectoryDataSource{}, datasource.MetadataResponse{TypeName: "file_local_directory"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := datasource.MetadataResponse{}
tc.fit.Metadata(context.Background(), datasource.MetadataRequest{ProviderTypeName: "file"}, &res)
got := res
if got != tc.want {
t.Errorf("%#v.Metadata() is %v; want %v", tc.fit, got, tc.want)
}
})
}
})
}
func TestLocalDirectoryDataSourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryDataSource
have datasource.ReadRequest
want datasource.ReadResponse
setup map[string]interface{}
}{
{
"Basic",
LocalDirectoryDataSource{client: &c.MemoryDirectoryClient{}},
// have
getReadDataSourceRequest(t, map[string]interface{}{
"path": testDirectoryPath,
}),
// want
getReadDataSourceResponse(t, map[string]interface{}{
"id": testDirectoryId,
"path": testDirectoryPath,
"permissions": defaultDirectoryPerm,
"files": []interface{}{
map[string]interface{}{
"name": filepath.Join(testDirectoryPath, "test_file_a"),
"size": "10",
"permissions": "0700",
"last_modified": "2025-09-29 16:09:15.039952008 +0000 UTC",
"is_directory": "false",
},
map[string]interface{}{
"name": filepath.Join(testDirectoryPath, "test_file_b"),
"size": "100",
"permissions": "0400",
"last_modified": "2021-02-18 00:56:32 +0000 UTC",
"is_directory": "false",
},
},
}),
// setup
map[string]interface{}{
"path": testDirectoryPath,
"permissions": defaultDirectoryPerm,
"files": []map[string]string{
{
"name": "test_file_a",
"size": "10",
"permissions": "0700",
"lastModified": "2025-09-29 16:09:15.039952008 +0000 UTC",
},
{
"name": "test_file_b",
"size": "100",
"permissions": "0400",
"lastModified": "2021-02-18 00:56:32 +0000 UTC",
},
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
path, ok := tc.setup["path"].(string)
if !ok {
t.Fatalf("path is not a string")
}
permissions, ok := tc.setup["permissions"].(string)
if !ok {
t.Fatalf("permissions is not a string")
}
files, ok := tc.setup["files"].([]map[string]string)
if !ok {
t.Fatalf("files is not a []map[string]string")
}
created, err := tc.fit.client.Create(path, permissions)
if err != nil {
t.Errorf("Error setting up: %v", err)
return
}
characterSet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
rand := rand.New(rand.NewSource(time.Now().UnixNano()))
for _, file := range files {
size, err := strconv.Atoi(file["size"])
if err != nil {
t.Errorf("could not convert size '%s' to an integer: %v", file["size"], err)
return
}
b := make([]byte, size)
for i := range b {
b[i] = characterSet[rand.Intn(len(characterSet))]
}
contents := string(b)
err = tc.fit.client.CreateFile(filepath.Join(path, file["name"]), contents, file["permissions"], file["lastModified"])
if err != nil {
t.Errorf("Error setting up: %v", err)
return
}
}
defer func() {
if err := tc.fit.client.Delete(created); err != nil {
t.Errorf("Error tearing down: %v", err)
return
}
}()
r := getReadDataSourceResponseContainer()
tc.fit.Read(context.Background(), tc.have, &r)
got := r
val := map[string]tftypes.Value{}
err = tc.want.State.Raw.As(&val)
if err != nil {
t.Errorf("Error converting tc.want.State.Raw to map: %v", err)
return
}
rawWantFiles := val["files"]
err = got.State.Raw.As(&val)
if err != nil {
t.Errorf("Error converting got.State.Raw to map: %v", err)
return
}
rawGotFiles := val["files"]
if diff := cmp.Diff(rawWantFiles, rawGotFiles); diff != "" {
t.Errorf("Read() mismatch (-want +got):\n%s", diff)
return
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Read() mismatch (-want +got):\n%s", diff)
return
}
})
}
})
}
//* Helpers *//
func getReadDataSourceRequest(t *testing.T, data map[string]interface{}) datasource.ReadRequest {
objType := getDataObjectAttributeTypes()
val := buildValue(t, objType, data)
return datasource.ReadRequest{
Config: tfsdk.Config{
Raw: val,
Schema: getLocalDirectoryDataSourceSchema().Schema,
},
}
}
func getReadDataSourceResponseContainer() datasource.ReadResponse {
return datasource.ReadResponse{
State: tfsdk.State{Schema: getLocalDirectoryDataSourceSchema().Schema},
}
}
func getReadDataSourceResponse(t *testing.T, data map[string]interface{}) datasource.ReadResponse {
objType := getDataObjectAttributeTypes()
val := buildValue(t, objType, data)
return datasource.ReadResponse{
State: tfsdk.State{
Raw: val,
Schema: getLocalDirectoryDataSourceSchema().Schema,
},
}
}
//* Helper's Helpers *//
func buildValue(t *testing.T, tfType tftypes.Type, data interface{}) tftypes.Value {
if data == nil {
return tftypes.NewValue(tfType, nil)
}
switch typ := tfType.(type) {
case tftypes.Object:
dataMap, ok := data.(map[string]interface{})
if !ok {
t.Fatalf("Expected map[string]interface{} for tftypes.Object, got %T", data)
}
attrValues := make(map[string]tftypes.Value)
for name, attrType := range typ.AttributeTypes {
attrValues[name] = buildValue(t, attrType, dataMap[name])
}
return tftypes.NewValue(typ, attrValues)
case tftypes.List:
dataSlice, ok := data.([]interface{})
if !ok {
t.Fatalf("Expected []interface{} for tftypes.List, got %T", data)
}
elemValues := make([]tftypes.Value, 0, len(dataSlice))
for _, v := range dataSlice {
elemValues = append(elemValues, buildValue(t, typ.ElementType, v))
}
return tftypes.NewValue(typ, elemValues)
default:
// Handle primitive types
if tfType.Is(tftypes.String) {
val, ok := data.(string)
if !ok {
t.Fatalf("Expected string for tftypes.String, got %T", data)
}
return tftypes.NewValue(tfType, val)
}
if tfType.Is(tftypes.Number) {
var numVal interface{}
switch v := data.(type) {
case int:
numVal = v
case float64:
numVal = v
default:
t.Fatalf("Expected int or float64 for tftypes.Number, got %T", data)
}
return tftypes.NewValue(tfType, numVal)
}
if tfType.Is(tftypes.Bool) {
val, ok := data.(bool)
if !ok {
t.Fatalf("Expected bool for tftypes.Bool, got %T", data)
}
return tftypes.NewValue(tfType, val)
}
t.Fatalf("Unsupported tftype: %T", tfType)
return tftypes.Value{}
}
}
func getDataObjectAttributeTypes() tftypes.Object {
return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
"path": tftypes.String,
"permissions": tftypes.String,
"files": tftypes.List{
ElementType: tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"name": tftypes.String,
"size": tftypes.String,
"permissions": tftypes.String,
"last_modified": tftypes.String,
"is_directory": tftypes.String,
},
},
},
},
}
}
func getLocalDirectoryDataSourceSchema() *datasource.SchemaResponse {
var testResource LocalDirectoryDataSource
r := &datasource.SchemaResponse{}
testResource.Schema(context.Background(), datasource.SchemaRequest{}, r)
return r
}

View File

@ -0,0 +1,233 @@
// SPDX-License-Identifier: MPL-2.0
package file_local_directory
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"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/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
c "github.com/rancher/terraform-provider-file/internal/provider/directory_client"
)
// The `var _` is a special Go construct that results in an unusable variable.
// The purpose of these lines is to make sure our LocalDirectoryFileResource correctly implements the `resource.Resource“ interface.
// These will fail at compilation time if the implementation is not satisfied.
var _ resource.Resource = &LocalDirectoryResource{}
var _ resource.ResourceWithImportState = &LocalDirectoryResource{}
func NewLocalDirectoryResource() resource.Resource {
return &LocalDirectoryResource{}
}
type LocalDirectoryResource struct {
client c.DirectoryClient
}
// LocalDirectoryResourceModel describes the resource data model.
type LocalDirectoryResourceModel struct {
Id types.String `tfsdk:"id"`
Path types.String `tfsdk:"path"`
Permissions types.String `tfsdk:"permissions"`
Created types.String `tfsdk:"created"`
}
func (r *LocalDirectoryResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_local_directory" // file_local_directory resource
}
func (r *LocalDirectoryResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Local Directory resource.",
Attributes: map[string]schema.Attribute{
"path": schema.StringAttribute{
MarkdownDescription: "Directory path, required. All subdirectories will also be created. Changing this forces recreate.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"permissions": schema.StringAttribute{
MarkdownDescription: "The directory permissions to assign to the directory, defaults to '0700'. " +
"In order to automatically create subdirectories the owner must have execute access, " +
"ie. '0600' or less prevents the provider from creating subdirectories.",
Optional: true,
Computed: true,
Default: stringdefault.StaticString("0700"),
},
"id": schema.StringAttribute{
MarkdownDescription: "Identifier derived from sha256 hash of path. ",
Computed: true,
},
"created": schema.StringAttribute{
MarkdownDescription: "The top level directory created. " +
"eg. if 'path' = '/path/to/new/directory' and '/path/to' already exists, " +
"but the rest doesn't, then 'created' will be '/path/to/new'. " +
"This path will be recursively removed during destroy and recreate actions.",
Computed: true,
},
},
}
}
// Configure the provider for the resource if necessary.
func (r *LocalDirectoryResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
}
// We should:
// - generate reality and state in the Create function
// - update state to match reality in the Read function
// - update state to config and update reality to config in the Update function by looking for differences in the state and the config (trust read to collect reality)
// - destroy reality and state in the Destroy function
func (r *LocalDirectoryResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Request Object: %#v", req))
var err error
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsDirectoryClient.")
r.client = &c.OsDirectoryClient{}
}
var plan LocalDirectoryResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
path := plan.Path.ValueString()
permString := plan.Permissions.ValueString()
hasher := sha256.New()
hasher.Write([]byte(path))
id := hex.EncodeToString(hasher.Sum(nil))
plan.Id = types.StringValue(id)
cutPath, err := r.client.Create(path, permString)
if err != nil {
resp.Diagnostics.AddError("Error creating file: ", err.Error())
return
}
plan.Created = types.StringValue(cutPath)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
tflog.Debug(ctx, fmt.Sprintf("Response Object: %#v", *resp))
}
func (r *LocalDirectoryResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
tflog.Debug(ctx, fmt.Sprintf("Request Object: %#v", req))
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsDirectoryClient.")
r.client = &c.OsDirectoryClient{}
}
var state LocalDirectoryResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
sPath := state.Path.ValueString()
sPerm := state.Permissions.ValueString()
perm, data, err := r.client.Read(sPath)
if err != nil && err.Error() == "directory not found" {
// force recreate if directory not found
resp.State.RemoveResource(ctx)
return
}
tflog.Debug(ctx, fmt.Sprintf("Read data: %#v", data))
if perm != sPerm {
// update the state with the actual mode
state.Permissions = types.StringValue(perm)
}
// Only update permissions because id, path, and created should never change.
// The directory resource manages a new directory, it is not meant to pull file information.
// To retrieve file information in a directory, use the directory data source.
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
tflog.Debug(ctx, fmt.Sprintf("Response Object: %#v", *resp))
}
func (r *LocalDirectoryResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Request Object: %#v", req))
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsDirectoryClient.")
r.client = &c.OsDirectoryClient{}
}
// Plan represents what is in the config, so plan = config
var config LocalDirectoryResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
cPath := config.Path.ValueString()
cPerm := config.Permissions.ValueString()
// Read updates state with reality, so state = reality
var reality LocalDirectoryResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &reality)...)
if resp.Diagnostics.HasError() {
return
}
rPerm := reality.Permissions.ValueString()
if cPerm != rPerm {
// Only update permissions because id, path, and created should never change.
err := r.client.Update(cPath, cPerm)
if err != nil {
resp.Diagnostics.AddError("Error updating directory permissions: ", err.Error())
return
}
}
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
tflog.Debug(ctx, fmt.Sprintf("Response Object: %#v", *resp))
}
func (r *LocalDirectoryResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
tflog.Debug(ctx, fmt.Sprintf("Request Object: %#v", req))
// Allow the ability to inject a file client, but use the OsDirectoryClient by default.
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default OsDirectoryClient.")
r.client = &c.OsDirectoryClient{}
}
var state LocalDirectoryResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
sCutPath := state.Created.ValueString()
if err := r.client.Delete(sCutPath); err != nil {
resp.Diagnostics.AddError("Failed to delete directory: ", err.Error())
return
}
tflog.Debug(ctx, fmt.Sprintf("Response Object: %#v", *resp))
}
func (r *LocalDirectoryResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
}

View File

@ -0,0 +1,618 @@
// SPDX-License-Identifier: MPL-2.0
package file_local_directory
import (
"context"
"slices"
"strconv"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-go/tftypes"
c "github.com/rancher/terraform-provider-file/internal/provider/directory_client"
)
const (
defaultId = ""
defaultPerm = "0700"
defaultCreated = ""
testPath = "path/to/new/directory"
// echo -n "path/to/new/directory" | sha256sum | awk '{print $1}' #.
testId = "2d020a0327fe0a114bf587a2b24894d67654203b0bd4428546ad5bf4ed7ed6a7"
testCreated = "path"
)
var booleanFields = []string{"fake"}
func TestLocalDirectoryResourceMetadata(t *testing.T) {
t.Run("Metadata function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryResource
want resource.MetadataResponse
}{
{"Basic test", LocalDirectoryResource{}, resource.MetadataResponse{TypeName: "file_local_directory"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
res := resource.MetadataResponse{}
tc.fit.Metadata(context.Background(), resource.MetadataRequest{ProviderTypeName: "file"}, &res)
got := res
if got != tc.want {
t.Errorf("%#v.Metadata() is %v; want %v", tc.fit, got, tc.want)
return
}
})
}
})
}
func TestLocalDirectorySchema(t *testing.T) {
t.Run("Schema function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryResource
want resource.SchemaResponse
}{
{"Basic test", LocalDirectoryResource{}, *getLocalDirectoryResourceSchema()},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := resource.SchemaResponse{}
tc.fit.Schema(context.Background(), resource.SchemaRequest{}, &r)
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Schema() mismatch (-want +got):\n%s", diff)
return
}
})
}
})
}
func TestLocalDirectoryResourceCreate(t *testing.T) {
t.Run("Create function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryResource
have resource.CreateRequest
want resource.CreateResponse
}{
{
"Basic",
LocalDirectoryResource{client: &c.MemoryDirectoryClient{}},
// have
getCreateRequest(t, map[string]string{
"path": testPath,
"permissions": defaultPerm,
"id": defaultId,
"created": defaultCreated,
}),
// want
getCreateResponse(t, map[string]string{
"path": testPath,
"permissions": defaultPerm,
"id": testId,
"created": testCreated,
}),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var plannedState LocalDirectoryResourceModel
if diags := tc.have.Plan.Get(context.Background(), &plannedState); diags.HasError() {
t.Errorf("Failed to get planned state: %v", diags)
return
}
plannedPath := plannedState.Path.ValueString()
r := getCreateResponseContainer()
tc.fit.Create(context.Background(), tc.have, &r)
defer func() {
if err := tc.fit.client.Delete(plannedPath); err != nil {
t.Errorf("Error cleaning up: %v", err)
return
}
}()
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Create() mismatch (-want +got):\n%s", diff)
return
}
})
}
})
}
func TestLocalDirectoryResourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryResource
have resource.ReadRequest
want resource.ReadResponse
setup map[string]string
}{
{
"Basic",
LocalDirectoryResource{client: &c.MemoryDirectoryClient{}},
// have
getReadRequest(t, map[string]string{
"id": testId,
"path": testPath,
"created": testCreated,
"permissions": defaultPerm,
}),
// want
getReadResponse(t, map[string]string{
"id": testId,
"path": testPath,
"created": testCreated,
"permissions": defaultPerm,
}),
// setup
map[string]string{
"path": testPath,
"permissions": defaultPerm,
},
},
{
"Updates permission",
LocalDirectoryResource{client: &c.MemoryDirectoryClient{}},
// have
getReadRequest(t, map[string]string{
"id": testId,
"path": testPath,
"created": testCreated,
"permissions": defaultPerm,
}),
// want
getReadResponse(t, map[string]string{
"id": testId,
"path": testPath,
"created": testCreated,
"permissions": "0777",
}),
// setup
map[string]string{
"path": testPath,
"permissions": "0777",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
created, err := tc.fit.client.Create(tc.setup["path"], tc.setup["permissions"])
if err != nil {
t.Errorf("Error setting up: %v", err)
return
}
defer func() {
if err := tc.fit.client.Delete(created); err != nil {
t.Errorf("Error tearing down: %v", err)
return
}
}()
r := getReadResponseContainer()
tc.fit.Read(context.Background(), tc.have, &r)
got := r
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Read() mismatch (-want +got):\n%s", diff)
return
}
})
}
})
}
func TestLocalDirectoryResourceUpdate(t *testing.T) {
t.Run("Update function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryResource
have resource.UpdateRequest
want resource.UpdateResponse
setup map[string]string
}{
{
"Basic",
LocalDirectoryResource{client: &c.MemoryDirectoryClient{}},
// have
getUpdateRequest(t, map[string]map[string]string{
"priorState": {
"id": testId,
"path": testPath,
"permissions": defaultPerm,
"created": testCreated,
},
"plan": {
"id": testId,
"path": testPath,
"permissions": defaultPerm,
"created": testCreated,
},
}),
// want
getUpdateResponse(t, map[string]string{
"id": testId,
"path": testPath,
"permissions": defaultPerm,
"created": testCreated,
}),
// setup
map[string]string{
"path": testPath,
"permissions": defaultPerm,
},
},
{
"Updates permissions",
LocalDirectoryResource{client: &c.MemoryDirectoryClient{}},
// have
getUpdateRequest(t, map[string]map[string]string{
"priorState": {
"id": testId,
"path": testPath,
"permissions": defaultPerm,
"created": testCreated,
},
"plan": {
"id": testId,
"path": testPath,
"permissions": "0755",
"created": testCreated,
},
}),
// want
getUpdateResponse(t, map[string]string{
"id": testId,
"path": testPath,
"permissions": "0755",
"created": testCreated,
}),
// setup
map[string]string{
"path": testPath,
"permissions": defaultPerm,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
created, err := tc.fit.client.Create(tc.setup["path"], tc.setup["permissions"])
if err != nil {
t.Errorf("Error setting up: %v", err)
return
}
defer func() {
if err := tc.fit.client.Delete(created); err != nil {
t.Errorf("Error tearing down: %v", err)
return
}
}()
r := getUpdateResponseContainer()
tc.fit.Update(context.Background(), tc.have, &r)
got := r
var plannedState LocalDirectoryResourceModel
if diags := tc.have.Plan.Get(context.Background(), &plannedState); diags.HasError() {
t.Errorf("Failed to get planned state: %v", diags)
return
}
plannedPath := plannedState.Path.ValueString()
plannedPermissions := plannedState.Permissions.ValueString()
permissionsAfterUpdate, _, err := tc.fit.client.Read(plannedPath)
if err != nil {
t.Errorf("Failed to read directory for update verification: %s", err)
return
}
if permissionsAfterUpdate != plannedPermissions {
t.Errorf("Directory permissions were not updated correctly. Got %q, want %q", permissionsAfterUpdate, plannedPermissions)
return
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Update() mismatch (-want +got):\n%s", diff)
return
}
})
}
})
}
func TestLocalDirectoryResourceDelete(t *testing.T) {
t.Run("Delete function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDirectoryResource
have resource.DeleteRequest
want resource.DeleteResponse
setup map[string]string
}{
{
"Basic test",
LocalDirectoryResource{client: &c.MemoryDirectoryClient{}},
// have
getDeleteRequest(t, map[string]string{
"id": testId,
"path": testPath,
"permissions": defaultPerm,
"created": testCreated,
}),
// want
getDeleteResponse(),
// setup
map[string]string{
"path": testPath,
"permissions": defaultPerm,
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := tc.fit.client.Create(tc.setup["path"], tc.setup["permissions"])
if err != nil {
t.Errorf("Error setting up: %v", err)
return
}
r := getDeleteResponseContainer()
tc.fit.Delete(context.Background(), tc.have, &r)
got := r
// Verify the directory was actually deleted
if _, _, err := tc.fit.client.Read(tc.setup["path"]); err == nil || err.Error() != "directory not found" {
if err == nil {
t.Errorf("Expected directory to be deleted, but it still exists.")
return
}
t.Errorf("Expected directory to be deleted, but it still exists. Error: %s", err.Error())
return
}
// verify that the directory was removed from state
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("Update() mismatch (-want +got):\n%s", diff)
return
}
})
}
})
}
// *** Test Helper Functions *** //
func getCreateRequest(t *testing.T, data map[string]string) resource.CreateRequest {
planMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue)
} else {
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.CreateRequest{}
}
planMap[key] = tftypes.NewValue(tftypes.Bool, v)
}
} else {
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
} else {
planMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
}
planValue := tftypes.NewValue(getObjectAttributeTypes(), planMap)
return resource.CreateRequest{
Plan: tfsdk.Plan{
Raw: planValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getCreateResponseContainer() resource.CreateResponse {
return resource.CreateResponse{
State: tfsdk.State{Schema: getLocalDirectoryResourceSchema().Schema},
}
}
func getCreateResponse(t *testing.T, data map[string]string) resource.CreateResponse {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.CreateResponse{}
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.CreateResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getReadRequest(t *testing.T, data map[string]string) resource.ReadRequest {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.ReadRequest{}
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.ReadRequest{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getReadResponseContainer() resource.ReadResponse {
return resource.ReadResponse{
State: tfsdk.State{Schema: getLocalDirectoryResourceSchema().Schema},
}
}
func getReadResponse(t *testing.T, data map[string]string) resource.ReadResponse {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.ReadResponse{}
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.ReadResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getUpdateRequest(t *testing.T, data map[string]map[string]string) resource.UpdateRequest {
stateMap := make(map[string]tftypes.Value)
for key, value := range data["priorState"] {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.UpdateRequest{}
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
priorStateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
planMap := make(map[string]tftypes.Value)
for key, value := range data["plan"] {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.Bool, tftypes.UnknownValue)
} else {
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.UpdateRequest{}
}
planMap[key] = tftypes.NewValue(tftypes.Bool, v)
}
} else {
if value == "" {
planMap[key] = tftypes.NewValue(tftypes.String, tftypes.UnknownValue)
} else {
planMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
}
planValue := tftypes.NewValue(getObjectAttributeTypes(), planMap)
return resource.UpdateRequest{
State: tfsdk.State{
Raw: priorStateValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
Plan: tfsdk.Plan{
Raw: planValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getUpdateResponseContainer() resource.UpdateResponse {
return resource.UpdateResponse{
State: tfsdk.State{Schema: getLocalDirectoryResourceSchema().Schema},
}
}
func getUpdateResponse(t *testing.T, data map[string]string) resource.UpdateResponse {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.UpdateResponse{}
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.UpdateResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getDeleteRequest(t *testing.T, data map[string]string) resource.DeleteRequest {
stateMap := make(map[string]tftypes.Value)
for key, value := range data {
if slices.Contains(booleanFields, key) { // booleanFields is a constant
v, err := strconv.ParseBool(value)
if err != nil {
t.Errorf("Error converting %s to bool %s: ", value, err.Error())
return resource.DeleteRequest{}
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getObjectAttributeTypes(), stateMap)
return resource.DeleteRequest{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalDirectoryResourceSchema().Schema,
},
}
}
func getDeleteResponseContainer() resource.DeleteResponse {
// A delete response does not need a schema as it results in a null state.
return resource.DeleteResponse{}
}
func getDeleteResponse() resource.DeleteResponse {
return resource.DeleteResponse{
State: tfsdk.State{
Raw: tftypes.Value{},
Schema: nil,
},
}
}
func getObjectAttributeTypes() tftypes.Object {
return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"path": tftypes.String,
"permissions": tftypes.String,
"created": tftypes.String,
"id": tftypes.String,
},
}
}
func getLocalDirectoryResourceSchema() *resource.SchemaResponse {
var testResource LocalDirectoryResource
r := &resource.SchemaResponse{}
testResource.Schema(context.Background(), resource.SchemaRequest{}, r)
return r
}

View File

@ -1,4 +1,4 @@
package file_snapshot package file_local_snapshot
import ( import (
"bytes" "bytes"
@ -19,29 +19,29 @@ import (
// The `var _` is a special Go construct that results in an unusable variable. // The `var _` is a special Go construct that results in an unusable variable.
// The purpose of these lines is to make sure our LocalFileDataSource correctly implements the `datasource.DataSource“ interface. // The purpose of these lines is to make sure our LocalFileDataSource correctly implements the `datasource.DataSource“ interface.
// These will fail at compilation time if the implementation is not satisfied. // These will fail at compilation time if the implementation is not satisfied.
var _ datasource.DataSource = &SnapshotDataSource{} var _ datasource.DataSource = &LocalSnapshotDataSource{}
func NewSnapshotDataSource() datasource.DataSource { func NewLocalSnapshotDataSource() datasource.DataSource {
return &SnapshotDataSource{} return &LocalSnapshotDataSource{}
} }
type SnapshotDataSource struct{} type LocalSnapshotDataSource struct{}
type SnapshotDataSourceModel struct { type LocalSnapshotDataSourceModel struct {
Id types.String `tfsdk:"id"` Id types.String `tfsdk:"id"`
Contents types.String `tfsdk:"contents"` Contents types.String `tfsdk:"contents"`
Data types.String `tfsdk:"data"` Data types.String `tfsdk:"data"`
Decompress types.Bool `tfsdk:"decompress"` Decompress types.Bool `tfsdk:"decompress"`
} }
func (r *SnapshotDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { func (r *LocalSnapshotDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_snapshot" // file_snapshot resp.TypeName = req.ProviderTypeName + "_local_snapshot" // _local_snapshot
} }
func (r *SnapshotDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { func (r *LocalSnapshotDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{ resp.Schema = schema.Schema{
MarkdownDescription: "File Snapshot data source. \n" + MarkdownDescription: "File LocalSnapshot data source. \n" +
"This data source retrieves the contents of a file from the output of a file_snapshot datasource." + "This data source retrieves the contents of a file from the output of a file_local_snapshot datasource." +
"Warning! Using this resource places the plain text contents of the snapshot in your state file.", "Warning! Using this resource places the plain text contents of the snapshot in your state file.",
Attributes: map[string]schema.Attribute{ Attributes: map[string]schema.Attribute{
@ -74,7 +74,7 @@ func (r *SnapshotDataSource) Schema(ctx context.Context, req datasource.SchemaRe
} }
} }
func (r *SnapshotDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { func (r *LocalSnapshotDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured. // Prevent panic if the provider has not been configured.
// This only configures the provider, so anything here must be available in the provider package to configure. // This only configures the provider, so anything here must be available in the provider package to configure.
// If you want to configure a client, do that in the Create/Read/Update/Delete functions. // If you want to configure a client, do that in the Create/Read/Update/Delete functions.
@ -83,10 +83,10 @@ func (r *SnapshotDataSource) Configure(ctx context.Context, req datasource.Confi
} }
} }
func (r *SnapshotDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { func (r *LocalSnapshotDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
tflog.Debug(ctx, fmt.Sprintf("Read Request Object: %+v", req)) tflog.Debug(ctx, fmt.Sprintf("Read Request Object: %+v", req))
var config SnapshotDataSourceModel var config LocalSnapshotDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return

View File

@ -1,4 +1,4 @@
package file_snapshot package file_local_snapshot
import ( import (
"context" "context"
@ -30,14 +30,14 @@ const (
var snapshotDataSourceBooleanFields = []string{"decompress"} var snapshotDataSourceBooleanFields = []string{"decompress"}
func TestSnapshotDataSourceMetadata(t *testing.T) { func TestLocalSnapshotDataSourceMetadata(t *testing.T) {
t.Run("Metadata function", func(t *testing.T) { t.Run("Metadata function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotDataSource fit LocalSnapshotDataSource
want datasource.MetadataResponse want datasource.MetadataResponse
}{ }{
{"Basic test", SnapshotDataSource{}, datasource.MetadataResponse{TypeName: "file_snapshot"}}, {"Basic test", LocalSnapshotDataSource{}, datasource.MetadataResponse{TypeName: "file_local_snapshot"}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@ -52,14 +52,14 @@ func TestSnapshotDataSourceMetadata(t *testing.T) {
}) })
} }
func TestSnapshotDataSourceSchema(t *testing.T) { func TestLocalSnapshotDataSourceSchema(t *testing.T) {
t.Run("Schema function", func(t *testing.T) { t.Run("Schema function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotDataSource fit LocalSnapshotDataSource
want datasource.SchemaResponse want datasource.SchemaResponse
}{ }{
{"Basic test", SnapshotDataSource{}, *getSnapshotDataSourceSchema()}, {"Basic test", LocalSnapshotDataSource{}, *getLocalSnapshotDataSourceSchema()},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@ -74,26 +74,26 @@ func TestSnapshotDataSourceSchema(t *testing.T) {
}) })
} }
func TestSnapshotDataSourceRead(t *testing.T) { func TestLocalSnapshotDataSourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) { t.Run("Read function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotDataSource fit LocalSnapshotDataSource
have datasource.ReadRequest have datasource.ReadRequest
want datasource.ReadResponse want datasource.ReadResponse
}{ }{
{ {
"Basic", "Basic",
SnapshotDataSource{}, LocalSnapshotDataSource{},
// have // have
getSnapshotDataSourceReadRequest(t, map[string]string{ getLocalSnapshotDataSourceReadRequest(t, map[string]string{
"id": "", // id is computed. "id": "", // id is computed.
"data": "", // data is computed. "data": "", // data is computed.
"contents": testDataEncoded, "contents": testDataEncoded,
"decompress": defaultDecompress, "decompress": defaultDecompress,
}), }),
// want // want
getSnapshotDataSourceReadResponse(t, map[string]string{ getLocalSnapshotDataSourceReadResponse(t, map[string]string{
"id": testDataEncodedId, "id": testDataEncodedId,
"data": testDataContents, "data": testDataContents,
"contents": testDataEncoded, "contents": testDataEncoded,
@ -102,16 +102,16 @@ func TestSnapshotDataSourceRead(t *testing.T) {
}, },
{ {
"Compressed", "Compressed",
SnapshotDataSource{}, LocalSnapshotDataSource{},
// have // have
getSnapshotDataSourceReadRequest(t, map[string]string{ getLocalSnapshotDataSourceReadRequest(t, map[string]string{
"id": "", // id is computed. "id": "", // id is computed.
"data": "", // data is computed. "data": "", // data is computed.
"contents": testDataCompressed, "contents": testDataCompressed,
"decompress": "true", "decompress": "true",
}), }),
// want // want
getSnapshotDataSourceReadResponse(t, map[string]string{ getLocalSnapshotDataSourceReadResponse(t, map[string]string{
"id": testDataCompressedId, "id": testDataCompressedId,
"data": testDataContents, "data": testDataContents,
"contents": testDataCompressed, "contents": testDataCompressed,
@ -121,7 +121,7 @@ func TestSnapshotDataSourceRead(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := getSnapshotDataSourceReadResponseContainer() r := getLocalSnapshotDataSourceReadResponseContainer()
tc.fit.Read(context.Background(), tc.have, &r) tc.fit.Read(context.Background(), tc.have, &r)
got := r got := r
if diff := cmp.Diff(tc.want, got); diff != "" { if diff := cmp.Diff(tc.want, got); diff != "" {
@ -135,7 +135,7 @@ func TestSnapshotDataSourceRead(t *testing.T) {
// *** Test Helper Functions *** // // *** Test Helper Functions *** //
// Read. // Read.
func getSnapshotDataSourceReadRequest(t *testing.T, data map[string]string) datasource.ReadRequest { func getLocalSnapshotDataSourceReadRequest(t *testing.T, data map[string]string) datasource.ReadRequest {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotDataSourceBooleanFields, key) { // snapshotDataSourceBooleanFields is a constant if slices.Contains(snapshotDataSourceBooleanFields, key) { // snapshotDataSourceBooleanFields is a constant
@ -148,22 +148,22 @@ func getSnapshotDataSourceReadRequest(t *testing.T, data map[string]string) data
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotDataSourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotDataSourceAttributeTypes(), stateMap)
return datasource.ReadRequest{ return datasource.ReadRequest{
Config: tfsdk.Config{ Config: tfsdk.Config{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotDataSourceSchema().Schema, Schema: getLocalSnapshotDataSourceSchema().Schema,
}, },
} }
} }
func getSnapshotDataSourceReadResponseContainer() datasource.ReadResponse { func getLocalSnapshotDataSourceReadResponseContainer() datasource.ReadResponse {
return datasource.ReadResponse{ return datasource.ReadResponse{
State: tfsdk.State{Schema: getSnapshotDataSourceSchema().Schema}, State: tfsdk.State{Schema: getLocalSnapshotDataSourceSchema().Schema},
} }
} }
func getSnapshotDataSourceReadResponse(t *testing.T, data map[string]string) datasource.ReadResponse { func getLocalSnapshotDataSourceReadResponse(t *testing.T, data map[string]string) datasource.ReadResponse {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotDataSourceBooleanFields, key) { // snapshotDataSourceBooleanFields is a constant if slices.Contains(snapshotDataSourceBooleanFields, key) { // snapshotDataSourceBooleanFields is a constant
@ -176,17 +176,17 @@ func getSnapshotDataSourceReadResponse(t *testing.T, data map[string]string) dat
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotDataSourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotDataSourceAttributeTypes(), stateMap)
return datasource.ReadResponse{ return datasource.ReadResponse{
State: tfsdk.State{ State: tfsdk.State{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotDataSourceSchema().Schema, Schema: getLocalSnapshotDataSourceSchema().Schema,
}, },
} }
} }
// The helpers helpers. // The helpers helpers.
func getSnapshotDataSourceAttributeTypes() tftypes.Object { func getLocalSnapshotDataSourceAttributeTypes() tftypes.Object {
return tftypes.Object{ return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{ AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String, "id": tftypes.String,
@ -197,8 +197,8 @@ func getSnapshotDataSourceAttributeTypes() tftypes.Object {
} }
} }
func getSnapshotDataSourceSchema() *datasource.SchemaResponse { func getLocalSnapshotDataSourceSchema() *datasource.SchemaResponse {
var testDataSource SnapshotDataSource var testDataSource LocalSnapshotDataSource
r := &datasource.SchemaResponse{} r := &datasource.SchemaResponse{}
testDataSource.Schema(context.Background(), datasource.SchemaRequest{}, r) testDataSource.Schema(context.Background(), datasource.SchemaRequest{}, r)
return r return r

View File

@ -1,4 +1,4 @@
package file_snapshot package file_local_snapshot
import ( import (
"context" "context"
@ -20,34 +20,34 @@ import (
// The `var _` is a special Go construct that results in an unusable variable. // The `var _` is a special Go construct that results in an unusable variable.
// The purpose of these lines is to make sure our LocalFileResource correctly implements the `resource.Resource“ interface. // The purpose of these lines is to make sure our LocalFileResource correctly implements the `resource.Resource“ interface.
// These will fail at compilation time if the implementation is not satisfied. // These will fail at compilation time if the implementation is not satisfied.
var _ resource.Resource = &SnapshotResource{} var _ resource.Resource = &LocalSnapshotResource{}
var _ resource.ResourceWithImportState = &SnapshotResource{} var _ resource.ResourceWithImportState = &LocalSnapshotResource{}
func NewSnapshotResource() resource.Resource { func NewLocalSnapshotResource() resource.Resource {
return &SnapshotResource{} return &LocalSnapshotResource{}
} }
type SnapshotResource struct { type LocalSnapshotResource struct {
client c.FileClient client c.FileClient
} }
// SnapshotResourceModel describes the resource data model. // LocalSnapshotResourceModel describes the resource data model.
type SnapshotResourceModel struct { type LocalSnapshotResourceModel struct {
Id types.String `tfsdk:"id"` Id types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"` Name types.String `tfsdk:"name"`
Directory types.String `tfsdk:"directory"` Directory types.String `tfsdk:"directory"`
Snapshot types.String `tfsdk:"snapshot"` LocalSnapshot types.String `tfsdk:"snapshot"`
UpdateTrigger types.String `tfsdk:"update_trigger"` UpdateTrigger types.String `tfsdk:"update_trigger"`
Compress types.Bool `tfsdk:"compress"` Compress types.Bool `tfsdk:"compress"`
} }
func (r *SnapshotResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { func (r *LocalSnapshotResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_snapshot" // file_snapshot resp.TypeName = req.ProviderTypeName + "_local_snapshot" // file_local_snapshot
} }
func (r *SnapshotResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { func (r *LocalSnapshotResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{ resp.Schema = schema.Schema{
MarkdownDescription: "File Snapshot resource. \n" + MarkdownDescription: "File LocalSnapshot resource. \n" +
"This resource saves some content in state and doesn't update it until the trigger argument changes. " + "This resource saves some content in state and doesn't update it until the trigger argument changes. " +
"The refresh phase doesn't update state, instead " + "The refresh phase doesn't update state, instead " +
"the state can only change on create or update and only when the update_trigger argument changes.", "the state can only change on create or update and only when the update_trigger argument changes.",
@ -100,7 +100,7 @@ func (r *SnapshotResource) Schema(ctx context.Context, req resource.SchemaReques
} }
} }
func (r *SnapshotResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { func (r *LocalSnapshotResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured. // Prevent panic if the provider has not been configured.
// This only configures the provider, so anything here must be available in the provider package to configure. // This only configures the provider, so anything here must be available in the provider package to configure.
// If you want to configure a client, do that in the Create/Read/Update/Delete functions. // If you want to configure a client, do that in the Create/Read/Update/Delete functions.
@ -115,7 +115,7 @@ func (r *SnapshotResource) Configure(ctx context.Context, req resource.Configure
// - update reality and state to match plan in the Update function (don't compare old state, just override) // - update reality and state to match plan in the Update function (don't compare old state, just override)
// - destroy reality in the Destroy function (state is handled automatically) // - destroy reality in the Destroy function (state is handled automatically)
func (r *SnapshotResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { func (r *LocalSnapshotResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Create Request Object: %+v", req)) tflog.Debug(ctx, fmt.Sprintf("Create Request Object: %+v", req))
if r.client == nil { if r.client == nil {
@ -123,7 +123,7 @@ func (r *SnapshotResource) Create(ctx context.Context, req resource.CreateReques
r.client = &c.OsFileClient{} r.client = &c.OsFileClient{}
} }
var plan SnapshotResourceModel var plan LocalSnapshotResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
@ -152,7 +152,7 @@ func (r *SnapshotResource) Create(ctx context.Context, req resource.CreateReques
resp.Diagnostics.AddError("Error reading encoded file: ", err.Error()) resp.Diagnostics.AddError("Error reading encoded file: ", err.Error())
return return
} }
plan.Snapshot = types.StringValue(encodedContents) plan.LocalSnapshot = types.StringValue(encodedContents)
hash, err := r.client.Hash(pDir, pName) hash, err := r.client.Hash(pDir, pName)
if err != nil { if err != nil {
@ -180,10 +180,10 @@ func (r *SnapshotResource) Create(ctx context.Context, req resource.CreateReques
tflog.Debug(ctx, fmt.Sprintf("Create Response Object: %+v", *resp)) tflog.Debug(ctx, fmt.Sprintf("Create Response Object: %+v", *resp))
} }
func (r *SnapshotResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { func (r *LocalSnapshotResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
tflog.Debug(ctx, fmt.Sprintf("Read Request Object: %+v", req)) tflog.Debug(ctx, fmt.Sprintf("Read Request Object: %+v", req))
var state SnapshotResourceModel var state LocalSnapshotResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
@ -198,7 +198,7 @@ func (r *SnapshotResource) Read(ctx context.Context, req resource.ReadRequest, r
// a difference between the plan and the state has been found. // a difference between the plan and the state has been found.
// we want to update reality and state to match the plan. // we want to update reality and state to match the plan.
// our snapshot will only update if the update trigger has changed. // our snapshot will only update if the update trigger has changed.
func (r *SnapshotResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { func (r *LocalSnapshotResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
tflog.Debug(ctx, fmt.Sprintf("Update Request Object: %+v", req)) tflog.Debug(ctx, fmt.Sprintf("Update Request Object: %+v", req))
if r.client == nil { if r.client == nil {
@ -206,7 +206,7 @@ func (r *SnapshotResource) Update(ctx context.Context, req resource.UpdateReques
r.client = &c.OsFileClient{} r.client = &c.OsFileClient{}
} }
var plan SnapshotResourceModel var plan LocalSnapshotResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
@ -216,14 +216,14 @@ func (r *SnapshotResource) Update(ctx context.Context, req resource.UpdateReques
pDir := plan.Directory.ValueString() pDir := plan.Directory.ValueString()
pUpdateTrigger := plan.UpdateTrigger.ValueString() pUpdateTrigger := plan.UpdateTrigger.ValueString()
var state SnapshotResourceModel var state LocalSnapshotResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() { if resp.Diagnostics.HasError() {
return return
} }
sUpdateTrigger := state.UpdateTrigger.ValueString() sUpdateTrigger := state.UpdateTrigger.ValueString()
sSnapshot := state.Snapshot.ValueString() sLocalSnapshot := state.LocalSnapshot.ValueString()
sCompress := state.Compress.ValueBool() sCompress := state.Compress.ValueBool()
sId := state.Id.ValueString() sId := state.Id.ValueString()
@ -252,10 +252,10 @@ func (r *SnapshotResource) Update(ctx context.Context, req resource.UpdateReques
resp.Diagnostics.AddError("Error reading encoded file: ", err.Error()) resp.Diagnostics.AddError("Error reading encoded file: ", err.Error())
return return
} }
plan.Snapshot = types.StringValue(encodedContents) plan.LocalSnapshot = types.StringValue(encodedContents)
} else { } else {
tflog.Debug(ctx, fmt.Sprintf("Update trigger hasn't changed, keeping previous snapshot (%s).", sSnapshot)) tflog.Debug(ctx, fmt.Sprintf("Update trigger hasn't changed, keeping previous snapshot (%s).", sLocalSnapshot))
plan.Snapshot = types.StringValue(sSnapshot) plan.LocalSnapshot = types.StringValue(sLocalSnapshot)
} }
tflog.Debug(ctx, fmt.Sprintf("Setting state to this plan: \n%+v", &plan)) tflog.Debug(ctx, fmt.Sprintf("Setting state to this plan: \n%+v", &plan))
@ -276,7 +276,7 @@ func (r *SnapshotResource) Update(ctx context.Context, req resource.UpdateReques
tflog.Debug(ctx, fmt.Sprintf("Update Response Object: %+v", *resp)) tflog.Debug(ctx, fmt.Sprintf("Update Response Object: %+v", *resp))
} }
func (r *SnapshotResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { func (r *LocalSnapshotResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
tflog.Debug(ctx, fmt.Sprintf("Delete Request Object: %+v", req)) tflog.Debug(ctx, fmt.Sprintf("Delete Request Object: %+v", req))
// delete is a no-op // delete is a no-op
@ -284,6 +284,6 @@ func (r *SnapshotResource) Delete(ctx context.Context, req resource.DeleteReques
tflog.Debug(ctx, fmt.Sprintf("Delete Response Object: %+v", *resp)) tflog.Debug(ctx, fmt.Sprintf("Delete Response Object: %+v", *resp))
} }
func (r *SnapshotResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { func (r *LocalSnapshotResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
} }

View File

@ -1,4 +1,4 @@
package file_snapshot package file_local_snapshot
import ( import (
"context" "context"
@ -34,14 +34,14 @@ const (
var snapshotResourceBooleanFields = []string{"compress"} var snapshotResourceBooleanFields = []string{"compress"}
func TestSnapshotResourceMetadata(t *testing.T) { func TestLocalSnapshotResourceMetadata(t *testing.T) {
t.Run("Metadata function", func(t *testing.T) { t.Run("Metadata function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotResource fit LocalSnapshotResource
want resource.MetadataResponse want resource.MetadataResponse
}{ }{
{"Basic test", SnapshotResource{}, resource.MetadataResponse{TypeName: "file_snapshot"}}, {"Basic test", LocalSnapshotResource{}, resource.MetadataResponse{TypeName: "file_local_snapshot"}},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@ -56,14 +56,14 @@ func TestSnapshotResourceMetadata(t *testing.T) {
}) })
} }
func TestSnapshotResourceSchema(t *testing.T) { func TestLocalSnapshotResourceSchema(t *testing.T) {
t.Run("Schema function", func(t *testing.T) { t.Run("Schema function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotResource fit LocalSnapshotResource
want resource.SchemaResponse want resource.SchemaResponse
}{ }{
{"Basic test", SnapshotResource{}, *getSnapshotResourceSchema()}, {"Basic test", LocalSnapshotResource{}, *getLocalSnapshotResourceSchema()},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@ -78,20 +78,20 @@ func TestSnapshotResourceSchema(t *testing.T) {
}) })
} }
func TestSnapshotResourceCreate(t *testing.T) { func TestLocalSnapshotResourceCreate(t *testing.T) {
t.Run("Create function", func(t *testing.T) { t.Run("Create function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotResource fit LocalSnapshotResource
have resource.CreateRequest have resource.CreateRequest
want resource.CreateResponse want resource.CreateResponse
setup map[string]string setup map[string]string
}{ }{
{ {
"Basic", "Basic",
SnapshotResource{client: &c.MemoryFileClient{}}, LocalSnapshotResource{client: &c.MemoryFileClient{}},
// have // have
getSnapshotResourceCreateRequest(t, map[string]string{ getLocalSnapshotResourceCreateRequest(t, map[string]string{
"id": "", "id": "",
"snapshot": "", "snapshot": "",
"name": testName, "name": testName,
@ -100,7 +100,7 @@ func TestSnapshotResourceCreate(t *testing.T) {
"compress": defaultCompress, "compress": defaultCompress,
}), }),
// want // want
getSnapshotResourceCreateResponse(t, map[string]string{ getLocalSnapshotResourceCreateResponse(t, map[string]string{
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
"name": testName, "name": testName,
@ -118,7 +118,7 @@ func TestSnapshotResourceCreate(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := getSnapshotResourceCreateResponseContainer() r := getLocalSnapshotResourceCreateResponseContainer()
if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], defaultPermissions); err != nil { if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], defaultPermissions); err != nil {
t.Errorf("Error setting up: %v", err) t.Errorf("Error setting up: %v", err)
} }
@ -137,19 +137,19 @@ func TestSnapshotResourceCreate(t *testing.T) {
} }
}) })
} }
func TestSnapshotResourceRead(t *testing.T) { func TestLocalSnapshotResourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) { t.Run("Read function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotResource fit LocalSnapshotResource
have resource.ReadRequest have resource.ReadRequest
want resource.ReadResponse want resource.ReadResponse
}{ }{
{ {
"Basic", "Basic",
SnapshotResource{}, LocalSnapshotResource{},
// have // have
getSnapshotResourceReadRequest(t, map[string]string{ getLocalSnapshotResourceReadRequest(t, map[string]string{
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
"name": testName, "name": testName,
@ -158,7 +158,7 @@ func TestSnapshotResourceRead(t *testing.T) {
"compress": defaultCompress, "compress": defaultCompress,
}), }),
// want // want
getSnapshotResourceReadResponse(t, map[string]string{ getLocalSnapshotResourceReadResponse(t, map[string]string{
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
"name": testName, "name": testName,
@ -170,7 +170,7 @@ func TestSnapshotResourceRead(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := getSnapshotResourceReadResponseContainer() r := getLocalSnapshotResourceReadResponseContainer()
tc.fit.Read(context.Background(), tc.have, &r) tc.fit.Read(context.Background(), tc.have, &r)
got := r got := r
if diff := cmp.Diff(tc.want, got); diff != "" { if diff := cmp.Diff(tc.want, got); diff != "" {
@ -181,20 +181,20 @@ func TestSnapshotResourceRead(t *testing.T) {
}) })
} }
func TestSnapshotResourceUpdate(t *testing.T) { func TestLocalSnapshotResourceUpdate(t *testing.T) {
t.Run("Update function", func(t *testing.T) { t.Run("Update function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotResource fit LocalSnapshotResource
have resource.UpdateRequest have resource.UpdateRequest
want resource.UpdateResponse want resource.UpdateResponse
setup map[string]string setup map[string]string
}{ }{
{ {
"Basic", "Basic",
SnapshotResource{client: &c.MemoryFileClient{}}, LocalSnapshotResource{client: &c.MemoryFileClient{}},
// have // have
getSnapshotResourceUpdateRequest(t, map[string]map[string]string{ getLocalSnapshotResourceUpdateRequest(t, map[string]map[string]string{
"priorState": { "priorState": {
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
@ -213,7 +213,7 @@ func TestSnapshotResourceUpdate(t *testing.T) {
}, },
}), }),
// want // want
getSnapshotResourceUpdateResponse(t, map[string]string{ getLocalSnapshotResourceUpdateResponse(t, map[string]string{
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
"name": testName, "name": testName,
@ -230,9 +230,9 @@ func TestSnapshotResourceUpdate(t *testing.T) {
}, },
{ {
"Updates when trigger changes", "Updates when trigger changes",
SnapshotResource{client: &c.MemoryFileClient{}}, LocalSnapshotResource{client: &c.MemoryFileClient{}},
// have // have
getSnapshotResourceUpdateRequest(t, map[string]map[string]string{ getLocalSnapshotResourceUpdateRequest(t, map[string]map[string]string{
"priorState": { "priorState": {
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
@ -251,7 +251,7 @@ func TestSnapshotResourceUpdate(t *testing.T) {
}, },
}), }),
// want // want
getSnapshotResourceUpdateResponse(t, map[string]string{ getLocalSnapshotResourceUpdateResponse(t, map[string]string{
"id": testId, // id shouldn't change "id": testId, // id shouldn't change
"snapshot": "dGhlc2UgY29udGVudHMgYXJlIHVwZGF0ZWQgZm9yIHRlc3Rpbmc=", // echo -n "these contents are updated for testing" | base64 -w 0 #. "snapshot": "dGhlc2UgY29udGVudHMgYXJlIHVwZGF0ZWQgZm9yIHRlc3Rpbmc=", // echo -n "these contents are updated for testing" | base64 -w 0 #.
"name": testName, "name": testName,
@ -268,9 +268,9 @@ func TestSnapshotResourceUpdate(t *testing.T) {
}, },
{ {
"Doesn't update when trigger stays the same", "Doesn't update when trigger stays the same",
SnapshotResource{client: &c.MemoryFileClient{}}, LocalSnapshotResource{client: &c.MemoryFileClient{}},
// have // have
getSnapshotResourceUpdateRequest(t, map[string]map[string]string{ getLocalSnapshotResourceUpdateRequest(t, map[string]map[string]string{
"priorState": { "priorState": {
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
@ -289,7 +289,7 @@ func TestSnapshotResourceUpdate(t *testing.T) {
}, },
}), }),
// want // want
getSnapshotResourceUpdateResponse(t, map[string]string{ getLocalSnapshotResourceUpdateResponse(t, map[string]string{
"id": testId, "id": testId,
"snapshot": testEncoded, "snapshot": testEncoded,
"name": testName, "name": testName,
@ -307,7 +307,7 @@ func TestSnapshotResourceUpdate(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := getSnapshotResourceUpdateResponseContainer() r := getLocalSnapshotResourceUpdateResponseContainer()
if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], defaultPermissions); err != nil { if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], defaultPermissions); err != nil {
t.Errorf("Error setting up: %v", err) t.Errorf("Error setting up: %v", err)
} }
@ -327,19 +327,19 @@ func TestSnapshotResourceUpdate(t *testing.T) {
}) })
} }
func TestSnapshotResourceDelete(t *testing.T) { func TestLocalSnapshotResourceDelete(t *testing.T) {
t.Run("Delete function", func(t *testing.T) { t.Run("Delete function", func(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
fit SnapshotResource fit LocalSnapshotResource
have resource.DeleteRequest have resource.DeleteRequest
want resource.DeleteResponse want resource.DeleteResponse
}{ }{
{ {
"Basic test", "Basic test",
SnapshotResource{client: &c.MemoryFileClient{}}, LocalSnapshotResource{client: &c.MemoryFileClient{}},
// have // have
getSnapshotResourceDeleteRequest(t, map[string]string{ getLocalSnapshotResourceDeleteRequest(t, map[string]string{
"id": testId, "id": testId,
"name": testName, "name": testName,
"directory": defaultDirectory, "directory": defaultDirectory,
@ -348,12 +348,12 @@ func TestSnapshotResourceDelete(t *testing.T) {
"compress": defaultCompress, "compress": defaultCompress,
}), }),
// want // want
getSnapshotResourceDeleteResponse(), getLocalSnapshotResourceDeleteResponse(),
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
r := getSnapshotResourceDeleteResponseContainer() r := getLocalSnapshotResourceDeleteResponseContainer()
tc.fit.Delete(context.Background(), tc.have, &r) tc.fit.Delete(context.Background(), tc.have, &r)
got := r got := r
if diff := cmp.Diff(tc.want, got); diff != "" { if diff := cmp.Diff(tc.want, got); diff != "" {
@ -366,7 +366,7 @@ func TestSnapshotResourceDelete(t *testing.T) {
// *** Test Helper Functions *** // // *** Test Helper Functions *** //
// Create. // Create.
func getSnapshotResourceCreateRequest(t *testing.T, data map[string]string) resource.CreateRequest { func getLocalSnapshotResourceCreateRequest(t *testing.T, data map[string]string) resource.CreateRequest {
planMap := make(map[string]tftypes.Value) planMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -387,22 +387,22 @@ func getSnapshotResourceCreateRequest(t *testing.T, data map[string]string) reso
} }
} }
} }
planValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), planMap) planValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), planMap)
return resource.CreateRequest{ return resource.CreateRequest{
Plan: tfsdk.Plan{ Plan: tfsdk.Plan{
Raw: planValue, Raw: planValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
func getSnapshotResourceCreateResponseContainer() resource.CreateResponse { func getLocalSnapshotResourceCreateResponseContainer() resource.CreateResponse {
return resource.CreateResponse{ return resource.CreateResponse{
State: tfsdk.State{Schema: getSnapshotResourceSchema().Schema}, State: tfsdk.State{Schema: getLocalSnapshotResourceSchema().Schema},
} }
} }
func getSnapshotResourceCreateResponse(t *testing.T, data map[string]string) resource.CreateResponse { func getLocalSnapshotResourceCreateResponse(t *testing.T, data map[string]string) resource.CreateResponse {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -415,17 +415,17 @@ func getSnapshotResourceCreateResponse(t *testing.T, data map[string]string) res
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), stateMap)
return resource.CreateResponse{ return resource.CreateResponse{
State: tfsdk.State{ State: tfsdk.State{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
// Read. // Read.
func getSnapshotResourceReadRequest(t *testing.T, data map[string]string) resource.ReadRequest { func getLocalSnapshotResourceReadRequest(t *testing.T, data map[string]string) resource.ReadRequest {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -438,22 +438,22 @@ func getSnapshotResourceReadRequest(t *testing.T, data map[string]string) resour
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), stateMap)
return resource.ReadRequest{ return resource.ReadRequest{
State: tfsdk.State{ State: tfsdk.State{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
func getSnapshotResourceReadResponseContainer() resource.ReadResponse { func getLocalSnapshotResourceReadResponseContainer() resource.ReadResponse {
return resource.ReadResponse{ return resource.ReadResponse{
State: tfsdk.State{Schema: getSnapshotResourceSchema().Schema}, State: tfsdk.State{Schema: getLocalSnapshotResourceSchema().Schema},
} }
} }
func getSnapshotResourceReadResponse(t *testing.T, data map[string]string) resource.ReadResponse { func getLocalSnapshotResourceReadResponse(t *testing.T, data map[string]string) resource.ReadResponse {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -466,17 +466,17 @@ func getSnapshotResourceReadResponse(t *testing.T, data map[string]string) resou
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), stateMap)
return resource.ReadResponse{ return resource.ReadResponse{
State: tfsdk.State{ State: tfsdk.State{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
// Update. // Update.
func getSnapshotResourceUpdateRequest(t *testing.T, data map[string]map[string]string) resource.UpdateRequest { func getLocalSnapshotResourceUpdateRequest(t *testing.T, data map[string]map[string]string) resource.UpdateRequest {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data["priorState"] { for key, value := range data["priorState"] {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -489,7 +489,7 @@ func getSnapshotResourceUpdateRequest(t *testing.T, data map[string]map[string]s
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
priorStateValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), stateMap) priorStateValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), stateMap)
planMap := make(map[string]tftypes.Value) planMap := make(map[string]tftypes.Value)
for key, value := range data["plan"] { for key, value := range data["plan"] {
@ -511,27 +511,27 @@ func getSnapshotResourceUpdateRequest(t *testing.T, data map[string]map[string]s
} }
} }
} }
planValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), planMap) planValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), planMap)
return resource.UpdateRequest{ return resource.UpdateRequest{
State: tfsdk.State{ State: tfsdk.State{
Raw: priorStateValue, Raw: priorStateValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
Plan: tfsdk.Plan{ Plan: tfsdk.Plan{
Raw: planValue, Raw: planValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
func getSnapshotResourceUpdateResponseContainer() resource.UpdateResponse { func getLocalSnapshotResourceUpdateResponseContainer() resource.UpdateResponse {
return resource.UpdateResponse{ return resource.UpdateResponse{
State: tfsdk.State{Schema: getSnapshotResourceSchema().Schema}, State: tfsdk.State{Schema: getLocalSnapshotResourceSchema().Schema},
} }
} }
func getSnapshotResourceUpdateResponse(t *testing.T, data map[string]string) resource.UpdateResponse { func getLocalSnapshotResourceUpdateResponse(t *testing.T, data map[string]string) resource.UpdateResponse {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -544,17 +544,17 @@ func getSnapshotResourceUpdateResponse(t *testing.T, data map[string]string) res
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), stateMap)
return resource.UpdateResponse{ return resource.UpdateResponse{
State: tfsdk.State{ State: tfsdk.State{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
// Delete. // Delete.
func getSnapshotResourceDeleteRequest(t *testing.T, data map[string]string) resource.DeleteRequest { func getLocalSnapshotResourceDeleteRequest(t *testing.T, data map[string]string) resource.DeleteRequest {
stateMap := make(map[string]tftypes.Value) stateMap := make(map[string]tftypes.Value)
for key, value := range data { for key, value := range data {
if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant if slices.Contains(snapshotResourceBooleanFields, key) { // snapshotResourceBooleanFields is a constant
@ -567,21 +567,21 @@ func getSnapshotResourceDeleteRequest(t *testing.T, data map[string]string) reso
stateMap[key] = tftypes.NewValue(tftypes.String, value) stateMap[key] = tftypes.NewValue(tftypes.String, value)
} }
} }
stateValue := tftypes.NewValue(getSnapshotResourceAttributeTypes(), stateMap) stateValue := tftypes.NewValue(getLocalSnapshotResourceAttributeTypes(), stateMap)
return resource.DeleteRequest{ return resource.DeleteRequest{
State: tfsdk.State{ State: tfsdk.State{
Raw: stateValue, Raw: stateValue,
Schema: getSnapshotResourceSchema().Schema, Schema: getLocalSnapshotResourceSchema().Schema,
}, },
} }
} }
func getSnapshotResourceDeleteResponseContainer() resource.DeleteResponse { func getLocalSnapshotResourceDeleteResponseContainer() resource.DeleteResponse {
// A delete response does not need a schema as it results in a null state. // A delete response does not need a schema as it results in a null state.
return resource.DeleteResponse{} return resource.DeleteResponse{}
} }
func getSnapshotResourceDeleteResponse() resource.DeleteResponse { func getLocalSnapshotResourceDeleteResponse() resource.DeleteResponse {
return resource.DeleteResponse{ return resource.DeleteResponse{
State: tfsdk.State{ State: tfsdk.State{
Raw: tftypes.Value{}, Raw: tftypes.Value{},
@ -591,7 +591,7 @@ func getSnapshotResourceDeleteResponse() resource.DeleteResponse {
} }
// The helpers helpers. // The helpers helpers.
func getSnapshotResourceAttributeTypes() tftypes.Object { func getLocalSnapshotResourceAttributeTypes() tftypes.Object {
return tftypes.Object{ return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{ AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String, "id": tftypes.String,
@ -604,8 +604,8 @@ func getSnapshotResourceAttributeTypes() tftypes.Object {
} }
} }
func getSnapshotResourceSchema() *resource.SchemaResponse { func getLocalSnapshotResourceSchema() *resource.SchemaResponse {
var testResource SnapshotResource var testResource LocalSnapshotResource
r := &resource.SchemaResponse{} r := &resource.SchemaResponse{}
testResource.Schema(context.Background(), resource.SchemaRequest{}, r) testResource.Schema(context.Background(), resource.SchemaRequest{}, r)
return r return r

View File

@ -10,7 +10,8 @@ import (
"github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/rancher/terraform-provider-file/internal/provider/file_local" "github.com/rancher/terraform-provider-file/internal/provider/file_local"
"github.com/rancher/terraform-provider-file/internal/provider/file_snapshot" "github.com/rancher/terraform-provider-file/internal/provider/file_local_directory"
"github.com/rancher/terraform-provider-file/internal/provider/file_local_snapshot"
) )
// The `var _` is a special Go construct that results in an unusable variable. // The `var _` is a special Go construct that results in an unusable variable.
@ -51,14 +52,16 @@ func (p *FileProvider) Configure(ctx context.Context, req provider.ConfigureRequ
func (p *FileProvider) Resources(ctx context.Context) []func() resource.Resource { func (p *FileProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{ return []func() resource.Resource{
file_local.NewLocalResource, file_local.NewLocalResource,
file_snapshot.NewSnapshotResource, file_local_snapshot.NewLocalSnapshotResource,
file_local_directory.NewLocalDirectoryResource,
} }
} }
func (p *FileProvider) DataSources(ctx context.Context) []func() datasource.DataSource { func (p *FileProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{ return []func() datasource.DataSource{
file_local.NewLocalDataSource, file_local.NewLocalDataSource,
file_snapshot.NewSnapshotDataSource, file_local_snapshot.NewLocalSnapshotDataSource,
file_local_directory.NewLocalDirectoryDataSource,
} }
} }

View File

@ -0,0 +1,83 @@
package basic
import (
"path/filepath"
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
util "github.com/rancher/terraform-provider-file/test"
)
func TestLocalDirectoryAdvanced(t *testing.T) {
t.Parallel()
id := util.GetId()
directory := "local_directory_advanced"
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)
newDir := filepath.Join(repoRoot, "test", "data", id, "newDirectory", "subDirectory")
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{}{
"path": newDir,
},
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)
}
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
directoryExists, err := util.CheckFileExists(filepath.Join(newDir))
if err != nil {
t.Log("Test failed, tearing down...")
util.TearDown(t, testDir, terraformOptions)
t.Fatalf("Error checking file: %s", err)
}
if !directoryExists {
t.Fail()
}
if t.Failed() {
t.Log("Test failed...")
} else {
t.Log("Test passed...")
}
t.Log("Test complete, tearing down...")
util.TearDown(t, testDir, terraformOptions)
}

View File

@ -0,0 +1,83 @@
package basic
import (
"path/filepath"
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
util "github.com/rancher/terraform-provider-file/test"
)
func TestLocalDirectoryBasic(t *testing.T) {
t.Parallel()
id := util.GetId()
directory := "local_directory_basic"
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)
newDir := filepath.Join(repoRoot, "test", "data", id, "newDirectory")
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{}{
"path": newDir,
},
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)
}
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
directoryExists, err := util.CheckFileExists(filepath.Join(newDir))
if err != nil {
t.Log("Test failed, tearing down...")
util.TearDown(t, testDir, terraformOptions)
t.Fatalf("Error checking file: %s", err)
}
if !directoryExists {
t.Fail()
}
if t.Failed() {
t.Log("Test failed...")
} else {
t.Log("Test passed...")
}
t.Log("Test complete, tearing down...")
util.TearDown(t, testDir, terraformOptions)
}

View File

@ -0,0 +1,70 @@
package basic
import (
"path/filepath"
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
util "github.com/rancher/terraform-provider-file/test"
)
func TestLocalDirectoryExample(t *testing.T) {
t.Parallel()
id := util.GetId()
directory := "file_local_directory"
repoRoot, err := util.GetRepoRoot(t)
if err != nil {
t.Fatalf("Error getting git root directory: %v", err)
}
exampleDir := filepath.Join(repoRoot, "examples", "resources", 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{}{},
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)
}
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
if t.Failed() {
t.Log("Test failed...")
} else {
t.Log("Test passed...")
}
t.Log("Test complete, tearing down...")
util.TearDown(t, testDir, terraformOptions)
}

View File

@ -0,0 +1,86 @@
package basic
import (
"path/filepath"
"testing"
"github.com/gruntwork-io/terratest/modules/terraform"
util "github.com/rancher/terraform-provider-file/test"
)
func TestLocalDirectorySubdirectory(t *testing.T) {
t.Parallel()
id := util.GetId()
directory := "local_directory_basic"
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)
newDir := filepath.Join(repoRoot, "test", "data", id, "newDirectory", "newSubdirectory")
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{}{
"path": newDir,
},
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)
}
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
directoryExists, err := util.CheckFileExists(newDir)
if err != nil {
t.Log("Test failed, tearing down...")
util.TearDown(t, testDir, terraformOptions)
t.Fatalf("Error checking file: %s", err)
}
if !directoryExists {
t.Log("Directory doesn't exist")
t.Fail()
} else {
t.Log("Directory exists")
}
if t.Failed() {
t.Log("Test failed...")
} else {
t.Log("Test passed...")
}
t.Log("Test complete, tearing down...")
util.TearDown(t, testDir, terraformOptions)
}

View File

@ -13,7 +13,7 @@ func TestSnapshotBasic(t *testing.T) {
t.Parallel() t.Parallel()
id := util.GetId() id := util.GetId()
directory := "snapshot_basic" directory := "local_snapshot_basic"
repoRoot, err := util.GetRepoRoot(t) repoRoot, err := util.GetRepoRoot(t)
if err != nil { if err != nil {
t.Fatalf("Error getting git root directory: %v", err) t.Fatalf("Error getting git root directory: %v", err)

View File

@ -13,7 +13,7 @@ func TestSnapshotExample(t *testing.T) {
t.Parallel() t.Parallel()
id := util.GetId() id := util.GetId()
directory := "file_snapshot" directory := "file_local_snapshot"
repoRoot, err := util.GetRepoRoot(t) repoRoot, err := util.GetRepoRoot(t)
if err != nil { if err != nil {
t.Fatalf("Error getting git root directory: %v", err) t.Fatalf("Error getting git root directory: %v", err)

View File

@ -13,7 +13,7 @@ func TestSnapshotCompressed(t *testing.T) {
t.Parallel() t.Parallel()
id := util.GetId() id := util.GetId()
directory := "snapshot_compressed" directory := "local_snapshot_compressed"
repoRoot, err := util.GetRepoRoot(t) repoRoot, err := util.GetRepoRoot(t)
if err != nil { if err != nil {
t.Fatalf("Error getting git root directory: %v", err) t.Fatalf("Error getting git root directory: %v", err)