diff --git a/.changes/unreleased/FEATURES-20251103-153952.yaml b/.changes/unreleased/FEATURES-20251103-153952.yaml new file mode 100644 index 00000000..6b6cc9f9 --- /dev/null +++ b/.changes/unreleased/FEATURES-20251103-153952.yaml @@ -0,0 +1,5 @@ +kind: FEATURES +body: 'action/local_command: New action that invokes an executable on the local machine.' +time: 2025-11-03T15:39:52.741799-05:00 +custom: + Issue: "450" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1094f110..1a8b686c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,6 +37,8 @@ jobs: - name: Set up Terraform uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 with: + # TODO: Remove once Terraform 1.14 GA is released + terraform_version: 1.14.0-rc2 terraform_wrapper: false - name: Generate diff --git a/.gitignore b/.gitignore index 5982d2c6..98c77a08 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ website/vendor # Test exclusions !command/test-fixtures/**/*.tfstate !command/test-fixtures/**/.terraform/ + +.vscode \ No newline at end of file diff --git a/docs/actions/command.md b/docs/actions/command.md new file mode 100644 index 00000000..3be7d075 --- /dev/null +++ b/docs/actions/command.md @@ -0,0 +1,79 @@ +page_title: "local_command Action - terraform-provider-local" +subcategory: "" +description: |- + Invokes an executable on the local machine. All environment variables visible to the Terraform process are passed through to the child process. After the child process successfully executes, the stdout will be returned for Terraform to display to the user. + Any non-zero exit code will be treated as an error and will return a diagnostic to Terraform containing the stderr message if available. +--- + +# local_command (Action) + +Invokes an executable on the local machine. All environment variables visible to the Terraform process are passed through to the child process. After the child process successfully executes, the `stdout` will be returned for Terraform to display to the user. + +Any non-zero exit code will be treated as an error and will return a diagnostic to Terraform containing the `stderr` message if available. + +## Example Usage + +For the following bash script (`example_script.sh`): +```bash +#!/bin/bash + +DATA=$( +## Schema + +### Required + +- `command` (String) Executable name to be discovered on the PATH or absolute path to executable. + +### Optional + +- `arguments` (List of String) Arguments to be passed to the given command. Any `null` arguments will be removed from the list. +- `stdin` (String) Data to be passed to the given command's standard input. +- `working_directory` (String) The directory path where the command should be executed, either an absolute path or relative to the Terraform working directory. If not provided, defaults to the Terraform working directory. diff --git a/examples/actions/local_command/action.tf b/examples/actions/local_command/action.tf new file mode 100644 index 00000000..0accb220 --- /dev/null +++ b/examples/actions/local_command/action.tf @@ -0,0 +1,19 @@ +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_example] + } + } +} + +action "local_command" "bash_example" { + config { + command = "bash" + arguments = ["example_script.sh", "arg1", "arg2"] + stdin = jsonencode({ + "key1" : "value1" + "key2" : "value2" + }) + } +} diff --git a/examples/actions/local_command/example_script.sh b/examples/actions/local_command/example_script.sh new file mode 100644 index 00000000..afe4b63b --- /dev/null +++ b/examples/actions/local_command/example_script.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +DATA=$( 0 { + resp.Diagnostics.AddAttributeError( + path.Root("command"), + "Command Execution Failed", + "The action received an unexpected error while attempting to execute the command."+ + "\n\n"+ + fmt.Sprintf("Command: %s\n", cmd.String())+ + fmt.Sprintf("Command Error: %s\n", stderrStr)+ + fmt.Sprintf("State: %s", err), + ) + return + } + + resp.Diagnostics.AddAttributeError( + path.Root("command"), + "Command Execution Failed", + "The action received an unexpected error while attempting to execute the command."+ + "\n\n"+ + fmt.Sprintf("Command: %s\n", cmd.Path)+ + fmt.Sprintf("Error: %s", err), + ) + return + } + + tflog.Trace(ctx, "Executed local command", map[string]interface{}{"command": cmd.String(), "stdout": stdoutStr, "stderr": stderrStr}) + + // Send the STDOUT to Terraform to display to the practitioner. The underlying action protocol supports streaming the + // STDOUT line-by-line in real-time, although each progress message gets a prefix per line, so it'd be difficult + // to read without batching lines together with an arbitrary time interval (this can be improved later if needed). + resp.SendProgress(action.InvokeProgressEvent{ + Message: fmt.Sprintf("\n\n%s\n", stdoutStr), + }) +} + +func findCommand(command string) diag.Diagnostic { + if _, err := exec.LookPath(command); err != nil { + return diag.NewAttributeErrorDiagnostic( + path.Root("command"), + "Command Lookup Failed", + "The action received an unexpected error while attempting to find the command."+ + "\n\n"+ + "The command must be accessible according to the platform where Terraform is running."+ + "\n\n"+ + "If the expected command should be automatically found on the platform where Terraform is running, "+ + "ensure that the command is in an expected directory. On Unix-based platforms, these directories are "+ + "typically searched based on the '$PATH' environment variable. On Windows-based platforms, these directories "+ + "are typically searched based on the '%PATH%' environment variable."+ + "\n\n"+ + "If the expected command is relative to the Terraform configuration, it is recommended that the command name includes "+ + "the interpolated value of 'path.module' before the command name to ensure that it is compatible with varying module usage. For example: \"${path.module}/my-command\""+ + "\n\n"+ + "The command must also be executable according to the platform where Terraform is running. On Unix-based platforms, the file on the filesystem must have the executable bit set. "+ + "On Windows-based platforms, no action is typically necessary."+ + "\n\n"+ + fmt.Sprintf("Platform: %s\n", runtime.GOOS)+ + fmt.Sprintf("Command: %s\n", command)+ + fmt.Sprintf("Error: %s", err), + ) + } + + return nil +} diff --git a/internal/provider/action_local_command_test.go b/internal/provider/action_local_command_test.go new file mode 100644 index 00000000..88dd0858 --- /dev/null +++ b/internal/provider/action_local_command_test.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "math/rand" + "os" + "os/exec" + "path/filepath" + "regexp" + "testing" + + "github.com/google/go-cmp/cmp" + "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/terraform" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +var ( + bashTestDirectory = filepath.Join("testdata", "TestLocalCommandAction_bash") +) + +func TestLocalCommandAction_bash(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + expectedFileContent := "stdin: , args: \n" + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_bash_stdin(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + stdin := "John" + expectedFileContent := fmt.Sprintf("stdin: %s, args: \n", stdin) + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "stdin": config.StringVariable(stdin), + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_bash_all(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + stdin := "John" + randomNumber1 := rand.Intn(100) + randomNumber2 := rand.Intn(100) + randomNumber3 := rand.Intn(100) + expectedFileContent := fmt.Sprintf("stdin: %s, args: %d %d %d\n", stdin, randomNumber1, randomNumber2, randomNumber3) + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "stdin": config.StringVariable(stdin), + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + "arguments": config.ListVariable( + config.IntegerVariable(randomNumber1), + config.IntegerVariable(randomNumber2), + config.IntegerVariable(randomNumber3), + ), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_bash_null_args(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + tempDir := t.TempDir() + randomNumber1 := rand.Intn(100) + randomNumber2 := rand.Intn(100) + randomNumber3 := rand.Intn(100) + expectedFileContent := fmt.Sprintf("stdin: , args: %d %d %d\n", randomNumber1, randomNumber2, randomNumber3) + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + "arguments": config.ListVariable( // Null arguments will be appended to this list in the test config, then filtered by the action code. + config.IntegerVariable(randomNumber1), + config.IntegerVariable(randomNumber2), + config.IntegerVariable(randomNumber3), + ), + }, + ConfigDirectory: config.TestNameDirectory(), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_absolute_path_bash(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, bashTestDirectory, "scripts") + tempDir := t.TempDir() + expectedFileContent := "stdin: , args: \n" + + bashAbsPath, err := exec.LookPath("bash") + if err != nil { + t.Fatalf("Failed to find bash executable: %v", err) + } + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "bash_path": config.StringVariable(bashAbsPath), + "working_directory": config.StringVariable(tempDir), + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.StaticDirectory(bashTestDirectory), + Check: func(s *terraform.State) error { + return assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent) + }, + // TODO: use this when PostApplyFunc is released in terraform-plugin-testing + // PostApplyFunc: assertTestFile(t, filepath.Join(tempDir, "test_file.txt"), expectedFileContent), + }, + }, + }) +} + +func TestLocalCommandAction_not_found(t *testing.T) { + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: ` +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.test] + } + } +} + +action "local_command" "test" { + config { + command = "notarealcommand" + } +}`, + ExpectError: regexp.MustCompile(`Error: exec: "notarealcommand": executable file not found`), + }, + }, + }) +} + +func TestLocalCommandAction_stderr(t *testing.T) { + wd, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get working directory: %v", err) + } + + testScriptsDir := filepath.Join(wd, "testdata", t.Name(), "scripts") + + resource.UnitTest(t, resource.TestCase{ + // Actions are only available in 1.14 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion("1.14.0"))), // TODO: replace with tfversion.Version1_14_0 when new plugin-testing version is released + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "scripts_folder_path": config.StringVariable(testScriptsDir), + }, + ConfigDirectory: config.TestNameDirectory(), + ExpectError: regexp.MustCompile(`Command Error: ru roh, an error occurred in the bash script!\n\nState: exit status 1`), + }, + }, + }) +} + +// TODO: use this function when PostApplyFunc is released in terraform-plugin-testing +// +// func assertTestFile(t *testing.T, filePath, expectedContent string) func() { +// return func() { +// t.Helper() + +// testFile, err := os.ReadFile(filePath) +// if err != nil { +// t.Fatalf("error trying to read created test file: %s", err) +// } + +// if diff := cmp.Diff(expectedContent, string(testFile)); diff != "" { +// t.Fatalf("unexpected file diff (-expected, +got): %s", diff) +// } +// } +// } + +func assertTestFile(t *testing.T, filePath, expectedContent string) error { + t.Helper() + + testFile, err := os.ReadFile(filePath) + if err != nil { + return fmt.Errorf("error trying to read created test file: %s", err) + } + + if diff := cmp.Diff(expectedContent, string(testFile)); diff != "" { + return fmt.Errorf("unexpected file diff (-expected, +got): %s", diff) + } + + return nil +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 907ce2f3..69294a52 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -12,6 +12,7 @@ import ( "encoding/base64" "encoding/hex" + "github.com/hashicorp/terraform-plugin-framework/action" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/provider" @@ -20,7 +21,7 @@ import ( ) var ( - _ provider.ProviderWithFunctions = (*localProvider)(nil) + _ provider.Provider = (*localProvider)(nil) ) func New() provider.Provider { @@ -57,6 +58,12 @@ func (p *localProvider) Functions(ctx context.Context) []func() function.Functio } } +func (p *localProvider) Actions(ctx context.Context) []func() action.Action { + return []func() action.Action{ + NewLocalCommandAction, + } +} + func (p *localProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{} } diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/main.tf b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf new file mode 100644 index 00000000..25915cc0 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash/main.tf @@ -0,0 +1,32 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +locals { + test_script = ( + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the folder path from the Go test environment via a variable. + # If running manually, there is no need to provide the scripts_folder_path. + var.scripts_folder_path != null ? + "${var.scripts_folder_path}/example_script.sh" : + "${abspath(path.module)}/scripts/example_script.sh" + ) +} + +action "local_command" "bash_test" { + config { + command = var.bash_path + arguments = concat([local.test_script], var.arguments) + stdin = var.stdin + + working_directory = var.working_directory + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh new file mode 100644 index 00000000..064e4084 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash/scripts/example_script.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + + +NAME=$(cat) +echo "Hello $NAME!" + +echo "stdin: $NAME, args: $@" >> test_file.txt diff --git a/internal/provider/testdata/TestLocalCommandAction_bash/variables.tf b/internal/provider/testdata/TestLocalCommandAction_bash/variables.tf new file mode 100644 index 00000000..c5b16261 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash/variables.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "bash_path" { + type = string + default = "bash" +} + +variable "stdin" { + type = string + default = null +} + +variable "arguments" { + type = list(string) + default = [] +} + +variable "working_directory" { + type = string + default = null +} + +variable "scripts_folder_path" { + type = string + default = null +} diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf new file mode 100644 index 00000000..163e5be1 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/main.tf @@ -0,0 +1,32 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +locals { + test_script = ( + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the folder path from the Go test environment via a variable. + # If running manually, there is no need to provide the scripts_folder_path. + var.scripts_folder_path != null ? + "${var.scripts_folder_path}/example_script.sh" : + "${abspath(path.module)}/scripts/example_script.sh" + ) +} + +action "local_command" "bash_test" { + config { + command = var.bash_path + arguments = concat([local.test_script], [null, null], var.arguments, [null, null, null]) # null arguments will be removed, empty strings preserved + stdin = var.stdin + + working_directory = var.working_directory + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh new file mode 100644 index 00000000..3ac5fb31 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/scripts/example_script.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + + +if [ "$#" -ne 3 ]; then + echo "You provided $# arguments, expected exactly 3 random number arguments (the 5 null arguments should be removed)." >&2 + exit 1 +fi + +NAME=$(> test_file.txt \ No newline at end of file diff --git a/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf new file mode 100644 index 00000000..c5b16261 --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_bash_null_args/variables.tf @@ -0,0 +1,27 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "bash_path" { + type = string + default = "bash" +} + +variable "stdin" { + type = string + default = null +} + +variable "arguments" { + type = list(string) + default = [] +} + +variable "working_directory" { + type = string + default = null +} + +variable "scripts_folder_path" { + type = string + default = null +} diff --git a/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf b/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf new file mode 100644 index 00000000..a6284fce --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_stderr/main.tf @@ -0,0 +1,34 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +variable "scripts_folder_path" { + type = string + default = null +} + +resource "terraform_data" "test" { + lifecycle { + action_trigger { + events = [after_create] + actions = [action.local_command.bash_test] + } + } +} + +locals { + test_script = ( + # This configuration will get copied to a temporary location without the scripts folder, so for + # acceptance tests we pass the folder path from the Go test environment via a variable. + # If running manually, there is no need to provide the scripts_folder_path. + var.scripts_folder_path != null ? + "${var.scripts_folder_path}/example_script.sh" : + "${abspath(path.module)}/scripts/example_script.sh" + ) +} + +action "local_command" "bash_test" { + config { + command = "bash" + arguments = [local.test_script] + } +} diff --git a/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh b/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh new file mode 100644 index 00000000..c0d72bad --- /dev/null +++ b/internal/provider/testdata/TestLocalCommandAction_stderr/scripts/example_script.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + + +echo "ru roh, an error occurred in the bash script!" >&2 +exit 1 \ No newline at end of file diff --git a/templates/actions/command.md.tmpl b/templates/actions/command.md.tmpl new file mode 100644 index 00000000..6621b0ee --- /dev/null +++ b/templates/actions/command.md.tmpl @@ -0,0 +1,45 @@ +page_title: "{{.Name}} {{.Type}} - {{.RenderedProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +For the following bash script (`example_script.sh`): +```bash +#!/bin/bash + +DATA=$(