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
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 alpha/beta 1.8 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-alpha20240228
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 alpha/beta versions of Terraform. This should be reverted once Terraform v1.8.0 is released.
# terraform_version: ${{ matrix.terraform }}.*
terraform_version: ${{ matrix.terraform }}
austinvalle marked this conversation as resolved.
Show resolved Hide resolved
terraform_wrapper: false

- name: Run acceptance test
Expand Down
45 changes: 45 additions & 0 deletions docs/functions/direxists.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
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 regular file, FIFO, or other special mode, it will return an error.

## Example 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")
}
```

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

Empty file.
16 changes: 16 additions & 0 deletions examples/functions/direxists/function.tf
austinvalle marked this conversation as resolved.
Show resolved Hide resolved
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")
austinvalle marked this conversation as resolved.
Show resolved Hide resolved
}
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
81 changes: 81 additions & 0 deletions internal/provider/function_direxists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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 regular file, FIFO, or other " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth mentioning/clarifying the behaviour with symlink'd dirs, I assume they're considered the same as regular dirs? What would constitute a FIFO in this context?

Copy link
Member Author

@austinvalle austinvalle Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally copied this over from fileexists, but I think it makes it more confusing because symlink is a special mode, but if it points to a directory it'll work fine.

What do you think of 275e921 ?

"special mode, it will return an error.",

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-alpha20240228"))),
},
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-alpha20240228"))),
},
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-alpha20240228"))),
},
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
23 changes: 23 additions & 0 deletions internal/provider/testdata/TestDirectoryExists_basic/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

terraform {
required_providers {
local = {
source = "hashicorp/local"
}
}
}

output "test_dir_exists" {
# Known issue where relative path is based on where the test working directory is located:
# https://github.com/hashicorp/terraform-plugin-testing/issues/277
value = provider::local::direxists("${path.module}/testdata/TestDirectoryExists_basic/test_dir")
}


output "test_dir_doesnt_exist" {
# Known issue where relative path is based on where the test working directory is located:
# https://github.com/hashicorp/terraform-plugin-testing/issues/277
value = provider::local::direxists("${path.module}/testdata/TestDirectoryExists_basic/nothing_here")
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright (c) HashiCorp, Inc.
# SPDX-License-Identifier: MPL-2.0

terraform {
required_providers {
local = {
source = "hashicorp/local"
}
}
}

output "test_not_a_dir" {
# Known issue where relative path is based on where the test working directory is located:
# https://github.com/hashicorp/terraform-plugin-testing/issues/277
value = provider::local::direxists("${path.module}/testdata/TestDirectoryExists_invalid_file/not_a_dir.txt")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a text file
Loading
Loading