feat: add data sources and organize code (#148)

* feat: add data sources and organize code
* fix: ignore unused data source example
* fix: add ignore to docs
* fix: update CI test to new location
* fix: update CI to run acceptance tests
* fix: run tests in nix
* fix: switch terraform version based on matrix
* fix: remove equal sign
---------

Signed-off-by: matttrach <matt.trachier@suse.com>
This commit is contained in:
Matt Trachier 2025-09-04 00:02:33 -05:00 committed by GitHub
parent 6a43cc60fc
commit b0b84c1409
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 674 additions and 311 deletions

View File

@ -98,12 +98,23 @@ jobs:
matrix: matrix:
# list whatever Terraform versions here you would like to support # list whatever Terraform versions here you would like to support
terraform: terraform:
- '1.8.*' # the latest patch of each version will be used
- '1.9.*' - '1.5.0'
- '1.10.*' - '1.6.0'
- '1.11.*' - '1.7.0'
- '1.12.*' - '1.8.0'
- '1.9.0'
- '1.10.0'
- '1.11.0'
- '1.12.0'
- '1.13.0'
steps: steps:
- name: install-nix
run: |
curl -L https://nixos.org/nix/install | sh
source /home/runner/.nix-profile/etc/profile.d/nix.sh
nix --version
which nix
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 https://github.com/actions/checkout - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 https://github.com/actions/checkout
- uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 https://github.com/actions/setup-go - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 https://github.com/actions/setup-go
with: with:
@ -113,9 +124,12 @@ jobs:
with: with:
terraform_version: ${{ matrix.terraform }} terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false terraform_wrapper: false
- run: go mod download - name: run tests
- run: go install gotest.tools/gotestsum@ddd0b05a6878e2e8257a2abe6e7df66cebc53d0e # v1.12.3 shell: /home/runner/.nix-profile/bin/nix develop --ignore-environment --extra-experimental-features nix-command --extra-experimental-features flakes --keep HOME --keep NIX_SSL_CERT_FILE --keep NIX_ENV_LOADED --keep TERM --command bash -e {0}
- run: make test run: |
- run: terraform fmt -check -recursive go mod download
- run: pushd ./examples/use-cases/basic; terraform init -input=false; popd go install gotest.tools/gotestsum@ddd0b05a6878e2e8257a2abe6e7df66cebc53d0e # v1.12.3
- run: pushd ./examples/use-cases/basic; terraform validate -no-color; popd terraform fmt -check -recursive
tfswitch -s ${{ matrix.terraform }}
make test
make testacc

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
version: "2" version: "2"
linters: linters:

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
# https://goreleaser.com for documentation # https://goreleaser.com for documentation

View File

@ -20,14 +20,12 @@ test:
testacc: build testacc: build
export REPO_ROOT="../../../."; \ export REPO_ROOT="../../../."; \
export TF_CLI_CONFIG_FILE="../../../test/.terraformrc"; \
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; \
popd; popd;
debug: build debug: build
export REPO_ROOT="../../../."; \ export REPO_ROOT="../../../."; \
export TF_CLI_CONFIG_FILE="../../../test/.terraformrc"; \
export TF_LOG=DEBUG; \ 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; \

View File

@ -0,0 +1,39 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "file_local Data Source - file"
subcategory: ""
description: |-
Local File DataSource
---
# file_local (Data Source)
Local File DataSource
## Example Usage
```terraform
# tflint-ignore: terraform_unused_declarations
data "file_local" "basic_example" {
name = "example.txt"
directory = "."
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `directory` (String) The directory where the file exists.
- `name` (String) File name, required.
### Optional
- `hmac_secret_key` (String, Sensitive) A string used to generate the file identifier, you can pass this value in the environment variable `TF_FILE_HMAC_SECRET_KEY`.
### Read-Only
- `contents` (String) The file contents.
- `id` (String) Identifier derived from sha256+HMAC hash of file contents.
- `permissions` (String) The file permissions.

View File

@ -12,8 +12,6 @@ description: |-
## Example Usage ## Example Usage
```terraform ```terraform
# Copyright (c) HashiCorp, Inc.
# this provider has no configuration currently # this provider has no configuration currently
provider "file" {} provider "file" {}
``` ```

View File

@ -13,8 +13,6 @@ Local File resource
## Example Usage ## Example Usage
```terraform ```terraform
# Copyright (c) HashiCorp, Inc.
resource "file_local" "basic_example" { resource "file_local" "basic_example" {
name = "example.txt" name = "example.txt"
contents = "An example implementation writing a local file." contents = "An example implementation writing a local file."
@ -59,8 +57,6 @@ Import is supported using the following syntax:
The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example:
```shell ```shell
# Copyright (c) HashiCorp, Inc.
# echo "Test data" > data.txt # echo "Test data" > data.txt
# FILEPATH="./data.txt" # FILEPATH="./data.txt"
# TF_FILE_HMAC_SECRET_KEY="super-secret-key" # TF_FILE_HMAC_SECRET_KEY="super-secret-key"

View File

@ -1,6 +0,0 @@
# Copyright (c) HashiCorp, Inc.
data "file_local" "example" {
configurable_attribute = "some-value"
}

View File

@ -0,0 +1,6 @@
# tflint-ignore: terraform_unused_declarations
data "file_local" "basic_example" {
name = "example.txt"
directory = "."
}

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
terraform { terraform {
required_version = ">= 1.5.0" required_version = ">= 1.5.0"

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
# this provider has no configuration currently # this provider has no configuration currently
provider "file" {} provider "file" {}

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
terraform { terraform {
required_version = ">= 1.5.0" required_version = ">= 1.5.0"

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
# echo "Test data" > data.txt # echo "Test data" > data.txt
# FILEPATH="./data.txt" # FILEPATH="./data.txt"

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
resource "file_local" "basic_example" { resource "file_local" "basic_example" {
name = "example.txt" name = "example.txt"

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
terraform { terraform {
required_version = ">= 1.5.0" required_version = ">= 1.5.0"

View File

@ -1,6 +1,6 @@
provider_installation { provider_installation {
dev_overrides { dev_overrides {
"rancher/file" = "../../../bin" "rancher/file" = "../../../../bin"
} }
direct { direct {
exclude = [] exclude = []

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
terraform { terraform {

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
provider "file" {} provider "file" {}
@ -12,3 +12,8 @@ resource "file_local" "basic" {
directory = local.directory directory = local.directory
contents = "An example of the \"most basic\" implementation writing a local file." contents = "An example of the \"most basic\" implementation writing a local file."
} }
data "file_local" "basic" {
name = file_local.basic.name
directory = file_local.basic.directory
}

View File

@ -0,0 +1,11 @@
output "file_resource" {
value = jsonencode(file_local.basic)
sensitive = true
}
output "file_data_source" {
value = jsonencode(data.file_local.basic)
sensitive = true
}

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
variable "directory" { variable "directory" {
type = string type = string

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
terraform { terraform {
required_version = ">= 1.5.0" required_version = ">= 1.5.0"

View File

@ -1,6 +1,6 @@
provider_installation { provider_installation {
dev_overrides { dev_overrides {
"rancher/file" = "../../../bin" "rancher/file" = "../../../../bin"
} }
direct { direct {
exclude = [] exclude = []

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
terraform { terraform {

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
provider "file" {} provider "file" {}
@ -26,3 +26,8 @@ resource "file_local" "protected_env" {
directory = local.directory directory = local.directory
contents = "An example implementation of a protected file." contents = "An example implementation of a protected file."
} }
data "file_local" "protected" {
name = file_local.protected.name
directory = file_local.protected.directory
}

View File

@ -0,0 +1,16 @@
output "file_resource" {
value = jsonencode(file_local.protected)
sensitive = true
}
output "file_resource_env" {
value = jsonencode(file_local.protected_env)
sensitive = true
}
output "file_data_source" {
value = jsonencode(data.file_local.protected)
sensitive = true
}

View File

@ -1,4 +1,4 @@
# Copyright (c) HashiCorp, Inc.
variable "directory" { variable "directory" {
type = string 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

@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1756128520, "lastModified": 1756819007,
"narHash": "sha256-R94HxJBi+RK1iCm8Y4Q9pdrHZl0GZoDPIaYwjxRNPh4=", "narHash": "sha256-12V64nKG/O/guxSYnr5/nq1EfqwJCdD2+cIGmhz3nrE=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "c53baa6685261e5253a1c355a1b322f82674a824", "rev": "aaff8c16d7fc04991cac6245bee1baa31f72b1e1",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -1,105 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"context"
"fmt"
"net/http"
"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"
)
// Ensure provider defined types fully satisfy framework interfaces.
var _ datasource.DataSource = &ExampleDataSource{}
func NewExampleDataSource() datasource.DataSource {
return &ExampleDataSource{}
}
// ExampleDataSource defines the data source implementation.
type ExampleDataSource struct {
client *http.Client
}
// ExampleDataSourceModel describes the data source data model.
type ExampleDataSourceModel struct {
ConfigurableAttribute types.String `tfsdk:"configurable_attribute"`
Id types.String `tfsdk:"id"`
}
func (d *ExampleDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_example"
}
func (d *ExampleDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: "Example data source",
Attributes: map[string]schema.Attribute{
"configurable_attribute": schema.StringAttribute{
MarkdownDescription: "Example configurable attribute",
Optional: true,
},
"id": schema.StringAttribute{
MarkdownDescription: "Example identifier",
Computed: true,
},
},
}
}
func (d *ExampleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*http.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
d.client = client
}
func (d *ExampleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var data ExampleDataSourceModel
// Read Terraform configuration data into the model
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}
// If applicable, this is a great opportunity to initialize any necessary
// provider client data and make a call using it.
// httpResp, err := d.client.Do(httpReq)
// if err != nil {
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read example, got error: %s", err))
// return
// }
// For the purposes of this example code, hardcoding a response value to
// save into the Terraform state.
data.Id = types.StringValue("example-id")
// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "read a data source")
// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

View File

@ -1,39 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package provider
import (
"testing"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)
func TestAccExampleDataSource(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
// Read testing
{
Config: testAccExampleDataSourceConfig,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"data.file_example.test",
tfjsonpath.New("id"),
knownvalue.StringExact("example-id"),
),
},
},
},
})
}
const testAccExampleDataSourceConfig = `
data "file_example" "test" {
configurable_attribute = "example"
}
`

View File

@ -0,0 +1,143 @@
// SPDX-License-Identifier: MPL-2.0
package local
import (
"context"
"fmt"
"os"
"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"
)
// 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.
// These will fail at compilation time if the implementation is not satisfied.
var _ datasource.DataSource = &LocalDataSource{}
func NewLocalDataSource() datasource.DataSource {
return &LocalDataSource{}
}
type LocalDataSource struct {
client fileClient
}
type LocalDataSourceModel struct {
Id types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
Directory types.String `tfsdk:"directory"`
Contents types.String `tfsdk:"contents"`
Permissions types.String `tfsdk:"permissions"`
HmacSecretKey types.String `tfsdk:"hmac_secret_key"`
}
func (r *LocalDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_local" // file_local datasource
}
func (r *LocalDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "Local File DataSource",
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
MarkdownDescription: "File name, required.",
Required: true,
},
"directory": schema.StringAttribute{
MarkdownDescription: "The directory where the file exists.",
Required: true,
},
"hmac_secret_key": schema.StringAttribute{
MarkdownDescription: "A string used to generate the file identifier, " +
"you can pass this value in the environment variable `TF_FILE_HMAC_SECRET_KEY`. ",
Optional: true,
Computed: true,
Sensitive: true,
},
"contents": schema.StringAttribute{
MarkdownDescription: "The file contents.",
Computed: true,
},
"permissions": schema.StringAttribute{
MarkdownDescription: "The file permissions.",
Computed: true,
},
"id": schema.StringAttribute{
MarkdownDescription: "Identifier derived from sha256+HMAC hash of file contents. ",
Computed: true,
},
},
}
}
func (r *LocalDataSource) 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 *LocalDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
tflog.Debug(ctx, fmt.Sprintf("Request Object: %#v", req))
// Allow the ability to inject a file client, but use the osFileClient by default.
if r.client == nil {
tflog.Debug(ctx, "Configuring client with default osFileClient.")
r.client = &osFileClient{}
}
var config LocalDataSourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
cName := config.Name.ValueString()
cDirectory := config.Directory.ValueString()
cPerm := config.Permissions.ValueString()
cHmacSecretKey := config.HmacSecretKey.ValueString()
cKey := cHmacSecretKey
if cKey == "" {
tflog.Debug(ctx, "Checking for secret key in environment variable TF_FILE_HMAC_SECRET_KEY.")
cKey = os.Getenv("TF_FILE_HMAC_SECRET_KEY")
}
if cKey == "" {
tflog.Debug(ctx, "Key not found, attempting to use constant.")
cKey = unprotectedHmacSecret // this is a constant defined in file_local_resource.go
}
perm, contents, err := r.client.Read(cDirectory, cName)
if err != nil && err.Error() == "File not found." {
resp.State.RemoveResource(ctx)
return
}
if err != nil {
resp.Diagnostics.AddError("Error reading file: ", err.Error())
return
}
// update state with actual contents
config.Contents = types.StringValue(contents)
id, err := calculateId(contents, cKey)
if err != nil {
resp.Diagnostics.AddError("Error reading file: ", "Problem calculating id from key: "+err.Error())
return
}
config.Id = types.StringValue(id)
if perm != cPerm {
// update the state with the actual mode
config.Permissions = types.StringValue(perm)
}
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
tflog.Debug(ctx, fmt.Sprintf("Response Object: %#v", *resp))
}

View File

@ -0,0 +1,255 @@
// SPDX-License-Identifier: MPL-2.0
package local
import (
"context"
"slices"
"strconv"
"testing"
"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"
)
func TestLocalDataSourceMetadata(t *testing.T) {
t.Run("Metadata function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDataSource
want datasource.MetadataResponse
}{
{"Basic test", LocalDataSource{}, datasource.MetadataResponse{TypeName: "file_local"}},
}
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 TestLocalDataSourceRead(t *testing.T) {
t.Run("Read function", func(t *testing.T) {
testCases := []struct {
name string
fit LocalDataSource
have datasource.ReadRequest
want datasource.ReadResponse
setup map[string]string
}{
{
"Unprotected",
LocalDataSource{client: &memoryFileClient{}},
// have
getDataSourceReadRequest(t, map[string]string{
"id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2",
"name": "read.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is an unprotected read test",
"hmac_secret_key": defaultHmacSecretKey,
}),
// want
getDataSourceReadResponse(t, map[string]string{
"id": "60cef95046105ff4522c0c1f1aeeeba43d0d729dbcabdd8846c317c98cac60a2",
"name": "read.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is an unprotected read test",
"hmac_secret_key": defaultHmacSecretKey,
}),
map[string]string{
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "read.tmp",
"contents": "this is an unprotected read test",
},
},
{
"Protected",
LocalDataSource{client: &memoryFileClient{}},
// have
getDataSourceReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getDataSourceReadResponse(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "read_protected.tmp",
"contents": "this is a protected read test",
},
},
{
"Protected with content update",
LocalDataSource{client: &memoryFileClient{}},
// have
getDataSourceReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_content.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getDataSourceReadResponse(t, map[string]string{
"id": "84326116e261654e44ca3cb73fa026580853794062d472bc817b7ec2c82ff648",
"name": "read_protected_content.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is a change in contents in the real file",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": defaultPerm,
"directory": defaultDirectory,
"name": "read_protected_content.tmp",
"contents": "this is a change in contents in the real file",
},
},
{
"Protected with mode update",
LocalDataSource{client: &memoryFileClient{}},
// have
getDataSourceReadRequest(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_mode.tmp",
"directory": defaultDirectory,
"permissions": defaultPerm,
"contents": "this is a protected read test",
"hmac_secret_key": "this-is-a-test-key",
}),
// want
getDataSourceReadResponse(t, map[string]string{
"id": "ec4407ba53b2c40ac2ac18ff7372a6fe6e4f7f8aa04f340503aefc7d9a5fa4e1",
"name": "read_protected_mode.tmp",
"directory": defaultDirectory,
"permissions": "0755",
"contents": "this is a protected read test",
"hmac_secret_key": "this-is-a-test-key",
}),
// reality
map[string]string{
"mode": "0755",
"directory": defaultDirectory,
"name": "read_protected_mode.tmp",
"contents": "this is a protected read test",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := tc.fit.client.Create(tc.setup["directory"], tc.setup["name"], tc.setup["contents"], tc.setup["mode"]); err != nil {
t.Errorf("Error setting up: %v", err)
}
defer func() {
if err := tc.fit.client.Delete(tc.setup["directory"], tc.setup["name"]); err != nil {
t.Errorf("Error tearing down: %v", err)
}
}()
r := getDataSourceReadResponseContainer()
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)
}
})
}
})
}
// *** Test Helper Functions *** //
func getDataSourceReadRequest(t *testing.T, data map[string]string) datasource.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())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getDataSourceObjectAttributeTypes(), stateMap)
return datasource.ReadRequest{
Config: tfsdk.Config{
Raw: stateValue,
Schema: getLocalDataSourceSchema().Schema,
},
}
}
func getDataSourceReadResponseContainer() datasource.ReadResponse {
return datasource.ReadResponse{
State: tfsdk.State{Schema: getLocalDataSourceSchema().Schema},
}
}
func getDataSourceReadResponse(t *testing.T, data map[string]string) datasource.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())
}
stateMap[key] = tftypes.NewValue(tftypes.Bool, v)
} else {
stateMap[key] = tftypes.NewValue(tftypes.String, value)
}
}
stateValue := tftypes.NewValue(getDataSourceObjectAttributeTypes(), stateMap)
return datasource.ReadResponse{
State: tfsdk.State{
Raw: stateValue,
Schema: getLocalDataSourceSchema().Schema,
},
}
}
func getDataSourceObjectAttributeTypes() tftypes.Object {
return tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
"name": tftypes.String,
"directory": tftypes.String,
"permissions": tftypes.String,
"contents": tftypes.String,
"hmac_secret_key": tftypes.String,
},
}
}
func getLocalDataSourceSchema() *datasource.SchemaResponse {
var testResource LocalDataSource
r := &datasource.SchemaResponse{}
testResource.Schema(context.Background(), datasource.SchemaRequest{}, r)
return r
}

View File

@ -1,6 +1,6 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
package provider package local
import ( import (
"context" "context"
@ -10,8 +10,6 @@ import (
"fmt" "fmt"
"io" "io"
"os" "os"
"path/filepath"
"strconv"
"strings" "strings"
"github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
@ -34,6 +32,8 @@ import (
var _ resource.Resource = &LocalResource{} var _ resource.Resource = &LocalResource{}
var _ resource.ResourceWithImportState = &LocalResource{} var _ resource.ResourceWithImportState = &LocalResource{}
const unprotectedHmacSecret = "this-is-the-hmac-secret-key-that-will-be-used-to-calculate-the-hash-of-unprotected-files"
// An interface for defining custom file managers. // An interface for defining custom file managers.
type fileClient interface { type fileClient interface {
Create(directory string, name string, data string, permissions string) error Create(directory string, name string, data string, permissions string) error
@ -43,58 +43,6 @@ type fileClient interface {
Delete(directory string, name string) error Delete(directory string, name string) error
} }
// The default fileClient, using the os package.
type osFileClient struct{}
var _ fileClient = &osFileClient{} // make sure the osFileClient implements the fileClient
func (c *osFileClient) Create(directory string, name string, data string, permissions string) error {
path := filepath.Join(directory, name)
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
return os.WriteFile(path, []byte(data), os.FileMode(modeInt))
}
func (c *osFileClient) Read(directory string, name string) (string, string, error) {
path := filepath.Join(directory, name)
info, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
return "", "", fmt.Errorf("file not found")
}
if err != nil {
return "", "", err
}
mode := fmt.Sprintf("%#o", info.Mode().Perm())
contents, err := os.ReadFile(path)
if err != nil {
return "", "", err
}
return mode, string(contents), nil
}
func (c *osFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error {
currentPath := filepath.Join(currentDirectory, currentName)
newPath := filepath.Join(newDirectory, newName)
if currentPath != newPath {
err := os.Rename(currentPath, newPath)
if err != nil {
return err
}
}
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
if err = os.WriteFile(newPath, []byte(data), os.FileMode(modeInt)); err != nil {
return err
}
return nil
}
func (c *osFileClient) Delete(directory string, name string) error {
path := filepath.Join(directory, name)
return os.Remove(path)
}
func NewLocalResource() resource.Resource { func NewLocalResource() resource.Resource {
return &LocalResource{} return &LocalResource{}
} }
@ -212,6 +160,7 @@ func (r *LocalResource) Create(ctx context.Context, req resource.CreateRequest,
var err error var err error
// Allow the ability to inject a file client, but use the osFileClient by default. // Allow the ability to inject a file client, but use the osFileClient by default.
// see file_os_client.go
if r.client == nil { if r.client == nil {
tflog.Debug(ctx, "Configuring client with default osFileClient.") tflog.Debug(ctx, "Configuring client with default osFileClient.")
r.client = &osFileClient{} r.client = &osFileClient{}
@ -245,7 +194,7 @@ func (r *LocalResource) Create(ctx context.Context, req resource.CreateRequest,
return return
} // at this point we have an id, key, contents, protected is true, and our calculated id matches what was provided } // at this point we have an id, key, contents, protected is true, and our calculated id matches what was provided
} else { } else {
id, err = calculateId(contents, "this-is-the-hmac-secret-key-that-will-be-used-to-calculate-the-hash-of-unprotected-files") id, err = calculateId(contents, unprotectedHmacSecret)
if err != nil { if err != nil {
resp.Diagnostics.AddError("Error creating file: ", "Problem calculating id from hard coded key: "+err.Error()) resp.Diagnostics.AddError("Error creating file: ", "Problem calculating id from hard coded key: "+err.Error())
return return
@ -369,7 +318,7 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest,
return return
} }
} else { } else {
id, err := calculateId(cContents, "this-is-the-hmac-secret-key-that-will-be-used-to-calculate-the-hash-of-unprotected-files") id, err := calculateId(cContents, unprotectedHmacSecret)
if err != nil { if err != nil {
resp.Diagnostics.AddError("Error updating file: ", "Problem calculating id from hard coded key: "+err.Error()) resp.Diagnostics.AddError("Error updating file: ", "Problem calculating id from hard coded key: "+err.Error())
return return
@ -405,7 +354,7 @@ func (r *LocalResource) Update(ctx context.Context, req resource.UpdateRequest,
return return
} }
} else { } else {
_, err := calculateId(rContents, "this-is-the-hmac-secret-key-that-will-be-used-to-calculate-the-hash-of-unprotected-files") _, err := calculateId(rContents, unprotectedHmacSecret)
if err != nil { if err != nil {
resp.Diagnostics.AddError("Error updating file: ", "Problem calculating id from hard coded key: "+err.Error()) resp.Diagnostics.AddError("Error updating file: ", "Problem calculating id from hard coded key: "+err.Error())
return return

View File

@ -1,10 +1,9 @@
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
package provider package local
import ( import (
"context" "context"
"fmt"
"slices" "slices"
"strconv" "strconv"
"testing" "testing"
@ -709,43 +708,3 @@ func getLocalResourceSchema() *resource.SchemaResponse {
testResource.Schema(context.Background(), resource.SchemaRequest{}, r) testResource.Schema(context.Background(), resource.SchemaRequest{}, r)
return r return r
} }
// type fileClient interface {
// Create(directory string, name string, data string, permissions string) error
// // If file isn't found the error message must have err.Error() == "File not found."
// 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
// Delete(directory string, name string) error
// }
type memoryFileClient struct {
file map[string]string
}
var _ fileClient = &memoryFileClient{} // make sure the memoryFileClient implements the fileClient
func (c *memoryFileClient) Create(directory string, name string, data string, permissions string) error {
c.file = make(map[string]string)
c.file["directory"] = directory
c.file["name"] = name
c.file["contents"] = data
c.file["permissions"] = permissions
return nil
}
func (c *memoryFileClient) Read(directory string, name string) (string, string, error) {
if c.file["directory"] == "" || c.file["name"] == "" {
return "", "", fmt.Errorf("file not found")
}
return c.file["permissions"], c.file["contents"], nil
}
func (c *memoryFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error {
c.file["directory"] = newDirectory
c.file["name"] = newName
c.file["contents"] = data
c.file["permissions"] = permissions
return nil
}
func (c *memoryFileClient) Delete(directory string, name string) error {
c.file = nil
return nil
}

View File

@ -0,0 +1,41 @@
package local
import (
"fmt"
)
type memoryFileClient struct {
file map[string]string
}
var _ fileClient = &memoryFileClient{} // make sure the memoryFileClient implements the fileClient
func (c *memoryFileClient) Create(directory string, name string, data string, permissions string) error {
c.file = make(map[string]string)
c.file["directory"] = directory
c.file["name"] = name
c.file["contents"] = data
c.file["permissions"] = permissions
return nil
}
func (c *memoryFileClient) Read(directory string, name string) (string, string, error) {
if c.file["directory"] == "" || c.file["name"] == "" {
return "", "", fmt.Errorf("file not found")
}
return c.file["permissions"], c.file["contents"], nil
}
func (c *memoryFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error {
c.file["directory"] = newDirectory
c.file["name"] = newName
c.file["contents"] = data
c.file["permissions"] = permissions
return nil
}
func (c *memoryFileClient) Delete(directory string, name string) error {
c.file = nil
return nil
}

View File

@ -0,0 +1,63 @@
package local
import (
"fmt"
"os"
"path/filepath"
"strconv"
)
// The default fileClient, using the os package.
type osFileClient struct{}
var _ fileClient = &osFileClient{} // make sure the osFileClient implements the fileClient
func (c *osFileClient) Create(directory string, name string, data string, permissions string) error {
path := filepath.Join(directory, name)
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
return os.WriteFile(path, []byte(data), os.FileMode(modeInt))
}
func (c *osFileClient) Read(directory string, name string) (string, string, error) {
path := filepath.Join(directory, name)
info, err := os.Stat(path)
if err != nil && os.IsNotExist(err) {
return "", "", fmt.Errorf("file not found")
}
if err != nil {
return "", "", err
}
mode := fmt.Sprintf("%#o", info.Mode().Perm())
contents, err := os.ReadFile(path)
if err != nil {
return "", "", err
}
return mode, string(contents), nil
}
func (c *osFileClient) Update(currentDirectory string, currentName string, newDirectory string, newName string, data string, permissions string) error {
currentPath := filepath.Join(currentDirectory, currentName)
newPath := filepath.Join(newDirectory, newName)
if currentPath != newPath {
err := os.Rename(currentPath, newPath)
if err != nil {
return err
}
}
modeInt, err := strconv.ParseUint(permissions, 8, 32)
if err != nil {
return err
}
if err = os.WriteFile(newPath, []byte(data), os.FileMode(modeInt)); err != nil {
return err
}
return nil
}
func (c *osFileClient) Delete(directory string, name string) error {
path := filepath.Join(directory, name)
return os.Remove(path)
}

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider"
"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/local"
) )
// 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.
@ -48,13 +49,13 @@ 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{
NewLocalResource, local.NewLocalResource,
} }
} }
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{
// NewExampleDataSource, local.NewLocalDataSource,
} }
} }

View File

@ -1,5 +1,3 @@
// Copyright (c) HashiCorp, Inc.
package main package main
import ( import (

View File

@ -1,6 +1,6 @@
provider_installation { provider_installation {
dev_overrides { dev_overrides {
"rancher/file" = "../../../bin" "rancher/file" = "../../../../bin"
} }
direct { direct {
exclude = [] exclude = []

View File

@ -1,5 +1,3 @@
// Copyright (c) HashiCorp, Inc.
package basic package basic
import ( import (
@ -14,7 +12,7 @@ func TestBasic(t *testing.T) {
t.Parallel() t.Parallel()
id := util.GetId() id := util.GetId()
directory := "basic" directory := "local/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)
@ -40,6 +38,7 @@ func TestBasic(t *testing.T) {
}, },
EnvVars: map[string]string{ EnvVars: map[string]string{
"TF_DATA_DIR": testDir, "TF_DATA_DIR": testDir,
"TF_CLI_CONFIG_FILE": filepath.Join(repoRoot, "test", ".terraformrc"),
"TF_IN_AUTOMATION": "1", "TF_IN_AUTOMATION": "1",
"TF_CLI_ARGS_init": "-no-color", "TF_CLI_ARGS_init": "-no-color",
"TF_CLI_ARGS_plan": "-no-color", "TF_CLI_ARGS_plan": "-no-color",
@ -50,6 +49,7 @@ func TestBasic(t *testing.T) {
RetryableTerraformErrors: util.GetRetryableTerraformErrors(), RetryableTerraformErrors: util.GetRetryableTerraformErrors(),
NoColor: true, NoColor: true,
Upgrade: true, Upgrade: true,
// ExtraArgs: terraform.ExtraArgs{ Output: []string{"-json"} },
}) })
_, err = terraform.InitAndApplyE(t, terraformOptions) _, err = terraform.InitAndApplyE(t, terraformOptions)
@ -58,6 +58,10 @@ func TestBasic(t *testing.T) {
util.TearDown(t, testDir, terraformOptions) util.TearDown(t, testDir, terraformOptions)
t.Fatalf("Error creating file: %s", err) t.Fatalf("Error creating file: %s", err)
} }
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
fileExists, err := util.CheckFileExists(filepath.Join(testDir, "basic_test.txt")) fileExists, err := util.CheckFileExists(filepath.Join(testDir, "basic_test.txt"))
if err != nil { if err != nil {

View File

@ -1,5 +1,3 @@
// Copyright (c) HashiCorp, Inc.
package protected package protected
import ( import (
@ -13,7 +11,7 @@ import (
func TestProtectedBasic(t *testing.T) { func TestProtectedBasic(t *testing.T) {
t.Parallel() t.Parallel()
id := util.GetId() id := util.GetId()
directory := "protected" directory := "local/protected"
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)
@ -41,6 +39,7 @@ func TestProtectedBasic(t *testing.T) {
EnvVars: map[string]string{ EnvVars: map[string]string{
"TF_DATA_DIR": testDir, "TF_DATA_DIR": testDir,
"TF_FILE_HMAC_SECRET_KEY": "thisisasupersecretkey", "TF_FILE_HMAC_SECRET_KEY": "thisisasupersecretkey",
"TF_CLI_CONFIG_FILE": filepath.Join(repoRoot, "test", ".terraformrc"),
"TF_IN_AUTOMATION": "1", "TF_IN_AUTOMATION": "1",
"TF_CLI_ARGS_init": "-no-color", "TF_CLI_ARGS_init": "-no-color",
"TF_CLI_ARGS_plan": "-no-color", "TF_CLI_ARGS_plan": "-no-color",
@ -51,6 +50,7 @@ func TestProtectedBasic(t *testing.T) {
RetryableTerraformErrors: util.GetRetryableTerraformErrors(), RetryableTerraformErrors: util.GetRetryableTerraformErrors(),
NoColor: true, NoColor: true,
Upgrade: true, Upgrade: true,
// ExtraArgs: terraform.ExtraArgs{ Output: []string{"-json"} },
}) })
_, err = terraform.InitAndApplyE(t, terraformOptions) _, err = terraform.InitAndApplyE(t, terraformOptions)
@ -59,6 +59,10 @@ func TestProtectedBasic(t *testing.T) {
util.TearDown(t, testDir, terraformOptions) util.TearDown(t, testDir, terraformOptions)
t.Fatalf("Error creating file: %s", err) t.Fatalf("Error creating file: %s", err)
} }
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
fileAExists, err := util.CheckFileExists(filepath.Join(testDir, "a_protected_test.txt")) fileAExists, err := util.CheckFileExists(filepath.Join(testDir, "a_protected_test.txt"))
if err != nil { if err != nil {

View File

@ -1,5 +1,3 @@
// Copyright (c) HashiCorp, Inc.
package protected package protected
import ( import (
@ -13,7 +11,7 @@ import (
func TestProtectedProtects(t *testing.T) { func TestProtectedProtects(t *testing.T) {
t.Parallel() t.Parallel()
id := util.GetId() id := util.GetId()
directory := "protected" directory := "local/protected"
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)
@ -41,6 +39,7 @@ func TestProtectedProtects(t *testing.T) {
EnvVars: map[string]string{ EnvVars: map[string]string{
"TF_DATA_DIR": testDir, "TF_DATA_DIR": testDir,
"TF_FILE_HMAC_SECRET_KEY": "thisisasupersecretkey", "TF_FILE_HMAC_SECRET_KEY": "thisisasupersecretkey",
"TF_CLI_CONFIG_FILE": filepath.Join(repoRoot, "test", ".terraformrc"),
"TF_IN_AUTOMATION": "1", "TF_IN_AUTOMATION": "1",
"TF_CLI_ARGS_init": "-no-color", "TF_CLI_ARGS_init": "-no-color",
"TF_CLI_ARGS_plan": "-no-color", "TF_CLI_ARGS_plan": "-no-color",
@ -51,6 +50,7 @@ func TestProtectedProtects(t *testing.T) {
RetryableTerraformErrors: util.GetRetryableTerraformErrors(), RetryableTerraformErrors: util.GetRetryableTerraformErrors(),
NoColor: true, NoColor: true,
Upgrade: true, Upgrade: true,
// ExtraArgs: terraform.ExtraArgs{ Output: []string{"-json"} },
}) })
_, err = terraform.InitAndApplyE(t, terraformOptions) _, err = terraform.InitAndApplyE(t, terraformOptions)
@ -59,6 +59,10 @@ func TestProtectedProtects(t *testing.T) {
util.TearDown(t, testDir, terraformOptions) util.TearDown(t, testDir, terraformOptions)
t.Fatalf("Error creating file: %s", err) t.Fatalf("Error creating file: %s", err)
} }
_, err = terraform.OutputAllE(t, terraformOptions)
if err != nil {
t.Log("Output failed, moving along...")
}
fileAExists, err := util.CheckFileExists(filepath.Join(testDir, "a_protected_test.txt")) fileAExists, err := util.CheckFileExists(filepath.Join(testDir, "a_protected_test.txt"))
if err != nil { if err != nil {

View File

@ -1,5 +1,5 @@
#!/bin/sh #!/bin/sh
# Copyright (c) HashiCorp, Inc.
# summarize.sh - reads report.json and prints a summary # summarize.sh - reads report.json and prints a summary

View File

@ -1,5 +1,3 @@
// Copyright (c) HashiCorp, Inc.
package test package test
import ( import (

View File

@ -1,4 +1,3 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//go:build generate //go:build generate
@ -6,13 +5,9 @@
package tools package tools
import ( import (
_ "github.com/hashicorp/copywrite"
_ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs" _ "github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs"
) )
// Generate copyright headers
//go:generate go run github.com/hashicorp/copywrite headers -d .. --config ../.copywrite.hcl
// Format Terraform code for use in documentation. // Format Terraform code for use in documentation.
// If you do not have Terraform installed, you can remove the formatting command, but it is suggested // If you do not have Terraform installed, you can remove the formatting command, but it is suggested
// to ensure the documentation is formatted properly. // to ensure the documentation is formatted properly.