Skip to content

Commit

Permalink
function/direxists: Add function that checks if a directory exists (#…
Browse files Browse the repository at this point in the history
…285)

* move local_file to test directory

* add initial implementation

* add changelog

* add license headers

* fixed to skipbelow and added symlink test

* resolve to absolute path

* generated docs

* update to use new function errors

* update workflow, tests, and add an example

* update to use beta release

* add template with pathexpands example

* remove special mode
  • Loading branch information
austinvalle authored Mar 11, 2024
1 parent 5712e33 commit f15ab07
Show file tree
Hide file tree
Showing 24 changed files with 387 additions and 18 deletions.
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240116-180627.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'functions/direxists: Added a new `direxists` function that checks for the existence
of a directory, similar to the built-in `fileexists` function.'
time: 2024-01-16T18:06:27.665639-05:00
custom:
Issue: "285"
12 changes: 11 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ jobs:
with:
version: latest

# TODO: Temporary addition to ensure plugin-docs uses the v1.8.0-beta1 version of Terraform. This should be reverted once Terraform v1.8.0 is released.
- name: Setup Terraform (supporting provider functions)
uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v3.0.0
with:
terraform_version: v1.8.0-beta1
terraform_wrapper: false

- name: Generate
run: make generate

Expand Down Expand Up @@ -73,7 +80,10 @@ jobs:
- name: Setup Terraform ${{ matrix.terraform }}
uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v3.0.0
with:
terraform_version: ${{ matrix.terraform }}.*
# TODO: Temporary change has been made to `vars.TF_VERSIONS_PROTOCOL_V5` to include the `.*` to enable us
# to utilize the v1.8.0-beta1 of Terraform. This should be reverted once Terraform v1.8.0 is released.
# terraform_version: ${{ matrix.terraform }}.*
terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false

- name: Run acceptance test
Expand Down
67 changes: 67 additions & 0 deletions docs/functions/direxists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
page_title: "direxists function - terraform-provider-local"
subcategory: ""
description: |-
Determines whether a directory exists at a given path.
---

# function: direxists

Given a path string, will return true if the directory exists. This function works only with directories. If used with a file, the function will return an error.

This function behaves similar to the built-in [`fileexists`](https://developer.hashicorp.com/terraform/language/functions/fileexists) function, however, `direxists` will not replace filesystem paths including `~` with the current user's home directory path. This functionality can be achieved by using the built-in [`pathexpand`](https://developer.hashicorp.com/terraform/language/functions/pathexpand) function with `direxists`, see example below.

## Example Usage

### Basic Usage

```terraform
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
local = {
source = "hashicorp/local"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}
output "example_output" {
value = provider::local::direxists("${path.module}/example-directory")
}
```

### Usage with home directory

```terraform
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
local = {
source = "hashicorp/local"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}
output "example_output_homedir" {
value = provider::local::direxists(pathexpand("~/.ssh"))
}
```

## Signature

<!-- signature generated by tfplugindocs -->
```text
direxists(path string) bool
```

## Arguments

<!-- arguments generated by tfplugindocs -->
1. `path` (String) Relative or absolute path to check for the existence of a directory
16 changes: 16 additions & 0 deletions examples/functions/direxists/basic.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
local = {
source = "hashicorp/local"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}

output "example_output" {
value = provider::local::direxists("${path.module}/example-directory")
}
Empty file.
16 changes: 16 additions & 0 deletions examples/functions/direxists/homedir.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
local = {
source = "hashicorp/local"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}

output "example_output_homedir" {
value = provider::local::direxists(pathexpand("~/.ssh"))
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.21
toolchain go1.21.6

require (
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/terraform-plugin-framework v1.6.1
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.22.0
Expand All @@ -27,7 +28,6 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.6.3 // indirect
github.com/hashicorp/hcl/v2 v2.20.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
Expand Down
9 changes: 2 additions & 7 deletions internal/provider/data_source_local_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,19 @@ import (
"encoding/base64"
"testing"

"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestLocalFileDataSource(t *testing.T) {
content := "This is some content"
checkSums := genFileChecksums([]byte(content))

config := `
data "local_file" "file" {
filename = "./testdata/local_file"
}
`

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: config,
ConfigDirectory: config.TestNameDirectory(),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.local_file.file", "content", content),
resource.TestCheckResourceAttr("data.local_file.file", "content_base64", base64.StdEncoding.EncodeToString([]byte(content))),
Expand Down
11 changes: 3 additions & 8 deletions internal/provider/data_source_local_sensitive_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,19 @@ import (
"testing"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func TestLocalFileSensitiveDataSource(t *testing.T) {
testFileContent := "This is some content"
testFileContent := "This is some sensitive content"
checkSums := genFileChecksums([]byte(testFileContent))

config := `
data "local_sensitive_file" "file" {
filename = "./testdata/local_file"
}
`

resource.UnitTest(t, resource.TestCase{
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: config,
ConfigDirectory: config.TestNameDirectory(),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("data.local_sensitive_file.file", "content", testFileContent),
resource.TestCheckResourceAttr("data.local_sensitive_file.file", "content_base64", base64.StdEncoding.EncodeToString([]byte(testFileContent))),
Expand Down
83 changes: 83 additions & 0 deletions internal/provider/function_direxists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"fmt"
"os"
"path/filepath"

"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var _ function.Function = &DirectoryExistsFunction{}

type DirectoryExistsFunction struct{}

func NewDirectoryExistsFunction() function.Function {
return &DirectoryExistsFunction{}
}

func (f *DirectoryExistsFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "direxists"
}

func (f *DirectoryExistsFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Determines whether a directory exists at a given path.",
Description: "Given a path string, will return true if the directory exists. " +
"This function works only with directories. If used with a file, the function will return an error.\n\n" +
"This function behaves similar to the built-in [`fileexists`](https://developer.hashicorp.com/terraform/language/functions/fileexists) function, " +
"however, `direxists` will not replace filesystem paths including `~` with the current user's home directory path. This functionality can be achieved by using the built-in " +
"[`pathexpand`](https://developer.hashicorp.com/terraform/language/functions/pathexpand) function with `direxists`, see example below.",

Parameters: []function.Parameter{
function.StringParameter{
Name: "path",
Description: "Relative or absolute path to check for the existence of a directory",
},
},
Return: function.BoolReturn{},
}
}

func (f *DirectoryExistsFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var inputPath string

resp.Error = req.Arguments.Get(ctx, &inputPath)
if resp.Error != nil {
return
}

directoryPath := inputPath
if !filepath.IsAbs(directoryPath) {
var err error
directoryPath, err = filepath.Abs(directoryPath)
if err != nil {
resp.Error = function.NewArgumentFuncError(0, fmt.Sprintf("Error expanding relative path to absolute path: %s", err))
return
}
}

directoryPath = filepath.Clean(directoryPath)

fi, err := os.Stat(directoryPath)
if err != nil {
if os.IsNotExist(err) {
resp.Error = resp.Result.Set(ctx, types.BoolValue(false))
return
} else {
resp.Error = function.NewArgumentFuncError(0, fmt.Sprintf("Error checking for directory: %s", err))
return
}
}

if fi.IsDir() {
resp.Error = resp.Result.Set(ctx, types.BoolValue(true))
return
}
resp.Error = function.NewArgumentFuncError(0, fmt.Sprintf("Invalid file mode detected: %q was found, but is not a directory", inputPath))
}
69 changes: 69 additions & 0 deletions internal/provider/function_direxists_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"regexp"
"testing"

"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-testing/config"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestDirectoryExists_basic(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
// TODO: Replace with the stable v1.8.0 release when available
tfversion.SkipBelow(version.Must(version.NewVersion("v1.8.0-beta1"))),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
ConfigDirectory: config.TestNameDirectory(),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectKnownOutputValue("test_dir_exists", knownvalue.Bool(true)),
plancheck.ExpectKnownOutputValue("test_dir_doesnt_exist", knownvalue.Bool(false)),
},
},
},
},
})
}

func TestDirectoryExists_invalid_file(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
// TODO: Replace with the stable v1.8.0 release when available
tfversion.SkipBelow(version.Must(version.NewVersion("v1.8.0-beta1"))),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
ConfigDirectory: config.TestNameDirectory(),
ExpectError: regexp.MustCompile("\"./testdata/TestDirectoryExists_invalid_file/not_a_dir.txt\" was found, but is\nnot a directory."),
},
},
})
}

func TestDirectoryExists_invalid_symlink(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
// TODO: Replace with the stable v1.8.0 release when available
tfversion.SkipBelow(version.Must(version.NewVersion("v1.8.0-beta1"))),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
ConfigDirectory: config.TestNameDirectory(),
ExpectError: regexp.MustCompile("\"./testdata/TestDirectoryExists_invalid_symlink/not_a_dir_symlink\" was found,\nbut is not a directory."),
},
},
})
}
9 changes: 8 additions & 1 deletion internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import (
"encoding/hex"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
)

var (
_ provider.Provider = (*localProvider)(nil)
_ provider.ProviderWithFunctions = (*localProvider)(nil)
)

func New() provider.Provider {
Expand Down Expand Up @@ -50,6 +51,12 @@ func (p *localProvider) Resources(ctx context.Context) []func() resource.Resourc
}
}

func (p *localProvider) Functions(ctx context.Context) []func() function.Function {
return []func() function.Function{
NewDirectoryExistsFunction,
}
}

func (p *localProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{}
}
Expand Down
Loading

0 comments on commit f15ab07

Please sign in to comment.