Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

function/direxists: Add function that checks if a directory exists #285

Merged
merged 14 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(),
austinvalle marked this conversation as resolved.
Show resolved Hide resolved
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)
austinvalle marked this conversation as resolved.
Show resolved Hide resolved
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
Loading