├── .github └── workflows │ └── CICD.yml ├── .gitignore ├── LICENSE ├── README.md ├── main.tf ├── outputs.tf ├── run.ps1 ├── run.sh ├── tester ├── main.tf └── variables.tf ├── tests ├── command-exists.json ├── command-not-exists.json ├── complex-unix.json ├── complex-windows.json ├── echo-env-var-nonsensitive.json ├── echo-env-var-sensitive.json ├── echo-env-var-stderr-nonsensitive.json ├── echo-env-var-stderr-sensitive.json ├── echo-long.json ├── echo-stderr.json ├── echo-stdout-unsuppressed.json ├── echo-stdout.json ├── exit-nonzero.json ├── multiline-comment.json ├── multiline.json ├── special-characters-env.json ├── special-characters.json ├── timeout-fail.json └── timeout-succeed.json ├── tmpfiles └── .gitkeep ├── variables.tf └── versions.tf /.github/workflows/CICD.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Matrix: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Generate Matrix 11 | id: matrix 12 | uses: Invicton-Labs/terraform-module-testing/matrix@v0.1.0 13 | with: 14 | minimum_tf_version: '0.13.1' 15 | # 0.15.0 - 0.15.3 has a bug where it doesn't like sensitive-marked outputs 16 | # 0.13.0 and 1.3.0 have bugs where it doesn't find a map value properly 17 | excluded_tf_versions: '0.15.0,0.15.1,0.15.2,0.15.3,1.3.0' 18 | 19 | - name: Output Matrix 20 | run: | 21 | echo "Strategy: ${{ steps.matrix.outputs.strategy }}" 22 | 23 | outputs: 24 | strategy: ${{ steps.matrix.outputs.strategy }} 25 | 26 | Test: 27 | needs: [Matrix] 28 | strategy: ${{ fromJSON(needs.Matrix.outputs.strategy)}} 29 | runs-on: ${{ matrix.runs-on }} 30 | container: ${{ matrix.container }} 31 | steps: 32 | 33 | - name: Initialize 34 | id: init-pass 35 | uses: Invicton-Labs/terraform-module-testing/initialize@v0.1.0 36 | with: 37 | tf_path: tester 38 | 39 | # Do an apply for each shell we want to test, if it's a specific shell 40 | - name: Test (bash) 41 | if: matrix.shells == 'true' 42 | uses: Invicton-Labs/terraform-module-testing/apply-destroy@v0.1.0 43 | with: 44 | tf_path: tester 45 | tf_args: -var="unix_interpreter=/bin/bash" 46 | 47 | - name: Test (dash) 48 | if: matrix.shells == 'true' 49 | uses: Invicton-Labs/terraform-module-testing/apply-destroy@v0.1.0 50 | with: 51 | tf_path: tester 52 | tf_args: -var="unix_interpreter=/bin/dash" 53 | 54 | # Run the Terraform test without specifying a shell 55 | - name: Test (Default Shell) 56 | if: matrix.shells != 'true' 57 | uses: Invicton-Labs/terraform-module-testing/apply-destroy@v0.1.0 58 | with: 59 | tf_path: tester 60 | 61 | # This job just waits for all other jobs to pass. We have it here 62 | # so our branch protection rule can reference a single job, instead 63 | # of needing to list every matrix value of every job above. 64 | Passed: 65 | runs-on: ubuntu-latest 66 | needs: [Test] 67 | steps: 68 | - name: Mark tests as passed 69 | run: echo "🎉" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Modules shouldn't use a lock file, only root configs should 9 | .terraform.lock.hcl 10 | 11 | # Crash log files 12 | crash.log 13 | 14 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 15 | # .tfvars files are managed as part of configuration and so should be included in 16 | # version control. 17 | # 18 | # example.tfvars 19 | 20 | # Ignore override files as they are usually used to override resources locally and so 21 | # are not checked in 22 | override.tf 23 | override.tf.json 24 | *_override.tf 25 | *_override.tf.json 26 | 27 | # Include override files you do wish to add to version control using negated pattern 28 | # 29 | # !example_override.tf 30 | 31 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 32 | # example: *tfplan* 33 | 34 | # Ignore temporary output files 35 | /tmpfiles/* 36 | !/tmpfiles/.gitkeep 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Invicton Labs (https://invictonlabs.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build](https://github.com/Invicton-Labs/terraform-external-shell-data/actions/workflows/CICD.yml/badge.svg) 2 | 3 | # Terraform Shell (Data) 4 | 5 | On the Terraform Registry: [Invicton-Labs/shell-data/external](https://registry.terraform.io/modules/Invicton-Labs/shell-data/external/latest) 6 | 7 | This module provides a wrapper for running shell scripts as data sources (re-run on every plan/apply) and capturing the output. Unlike Terraform's standard [External Data Source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/data_source), this module supports: 8 | - Environment variables 9 | - Capturing `stdout`, `stderr`, and `exit_code` of the command 10 | - Built-in support for all major flavors of Linux, all POSIX-compatible shells, Windows, and MacOS 11 | - Optional Terraform failure when an error in the given command occurs 12 | - Optional timeouts for commands 13 | 14 | For Windows, this module should work on any system that supports a relatively modern version of PowerShell. For Unix (Linux and MacOS), this module should work on any POSIX-compatible shell that supports `echo`, `cat`, `cut`, `head`, and `base64` (which is the vast majority of out-of-the-box systems). 15 | 16 | For a similar module that **runs as a resource** (only re-runs the command on resource re-create or on a change in a trigger), see [this module](https://registry.terraform.io/modules/Invicton-Labs/shell-resource/external/latest) on the Terraform Registry. 17 | 18 | ## Notes: 19 | 20 | 1. If only one of `command_unix` or `command_windows` is provided, that one will be used on all operating systems. 21 | 2. Carriage returns (`\r`) are removed in all input commands. This is because Powershell doesn't require them to be present, but Unix shells don't support them, so removing them allows the same command to be used on both types of machine. 22 | 3. Carriage returns (`\r`) are removed from all outputs. This is to help ensure a consistent output across platforms. 23 | 4. Trailing newlines are trimmed for the `stdout` and `stderr` outputs. This is due to limitations with POSIX-compatible file reading and to ensure consistency between executions of the same Terraform configuration on Windows- and Unix-based machines. 24 | 25 | ## Usage 26 | 27 | ``` 28 | module "shell_data_hello" { 29 | source = "Invicton-Labs/shell-data/external" 30 | 31 | // This is the command that will be run on Unix-based systems 32 | // If you wanted to, you could use the file() function to read 33 | // this command from a local file instead of specifying it as 34 | // a string. 35 | command_unix = <&2 echo "This is an error" 39 | EOF 40 | 41 | // This is the command that will be run on Windows-based systems 42 | command_windows = < replace(replace(v, "\r", ""), "\r\n", "\n") 27 | } 28 | 29 | // Generate the environment variable file 30 | env_file_content = join(";", [ 31 | for k, v in local.env_vars : 32 | "${base64encode(k)}:${base64encode(v)}" 33 | ]) 34 | 35 | is_debug = local.var_execution_id != null 36 | execution_id = local.var_execution_id != null ? local.var_execution_id : " " 37 | 38 | query_windows = { 39 | // If it's Windows, use the query parameter normally since PowerShell can natively handle JSON decoding 40 | execution_id = base64encode(local.execution_id) 41 | directory = base64encode(local.temporary_dir) 42 | environment = base64encode(local.env_file_content) 43 | timeout = base64encode(local.var_timeout == null ? 0 : local.var_timeout) 44 | exit_on_nonzero = base64encode(local.var_fail_on_nonzero_exit_code ? "true" : "false") 45 | exit_on_stderr = base64encode(local.var_fail_on_stderr ? "true" : "false") 46 | exit_on_timeout = base64encode(local.var_fail_on_timeout ? "true" : "false") 47 | debug = base64encode(local.is_debug ? "true" : "false") 48 | command = base64encode(local.command) 49 | } 50 | 51 | query_unix = { 52 | // If it's Unix, use base64-encoded strings with a special separator that we can easily use to separate in shell, 53 | // without needing to install jq. The "|" character will never be found in base64-encoded content and it doesn't 54 | // need to be escaped, so it's a good option. 55 | "" = join("|", [ 56 | "", 57 | local.query_windows.execution_id, 58 | local.query_windows.directory, 59 | local.query_windows.environment, 60 | local.query_windows.timeout, 61 | local.query_windows.exit_on_nonzero, 62 | local.query_windows.exit_on_stderr, 63 | local.query_windows.exit_on_timeout, 64 | local.query_windows.debug, 65 | local.query_windows.command, 66 | base64encode(local.var_unix_interpreter), 67 | "" 68 | ]) 69 | } 70 | 71 | query = local.is_windows ? local.query_windows : local.query_unix 72 | } 73 | 74 | // Run the command 75 | data "external" "run" { 76 | program = local.is_windows ? ["powershell.exe", "${abspath(path.module)}/run.ps1"] : [local.var_unix_interpreter, "${abspath(path.module)}/run.sh"] 77 | // Mark the query as sensitive just so it doesn't show up in the plan output. 78 | // Since it's all base64-encoded anyways, showing it in the plan wouldn't be useful 79 | // We want to support older versions of TF that don't have the sensitive function though, 80 | // so fall back to not marking it as sensitive. 81 | query = local.var_suppress_console && !local.is_debug ? try(sensitive(local.query), local.query) : local.query 82 | working_dir = local.wait_for_apply == "" ? local.var_working_dir : local.var_working_dir 83 | } 84 | 85 | locals { 86 | // Replace all "\r\n" (considered by Terraform to be a single character) with "\n", and remove any extraneous "\r". 87 | // This helps ensure a consistent output across platforms. 88 | stderr = trimsuffix(replace(replace(base64decode(data.external.run.result.stderr), "\r", ""), "\r\n", "\n"), "\n") 89 | stdout = trimsuffix(replace(replace(base64decode(data.external.run.result.stdout), "\r", ""), "\r\n", "\n"), "\n") 90 | // This checks if the stderr/stdout contains any of the values of the `environment_sensitive` input variable. 91 | // We use `replace` to check for the presence, even though the recommended tool is `regexall`, because 92 | // we don't control what the search string is, so it could be a regex pattern, but we want to treat 93 | // it as a literal. 94 | stderr_contains_sensitive = length([ 95 | for k, v in local.var_environment_sensitive : 96 | true 97 | if length(replace(local.stderr, v, "")) != length(local.stderr) 98 | ]) > 0 99 | stdout_contains_sensitive = length([ 100 | for k, v in local.var_environment_sensitive : 101 | true 102 | if length(replace(local.stdout, v, "")) != length(local.stdout) 103 | ]) > 0 104 | // The `try` is to support versions of Terraform that don't support `sensitive`. 105 | stderr_censored = local.stderr_contains_sensitive ? try(sensitive(local.stderr), local.stderr) : local.stderr 106 | stdout_censored = local.stdout_contains_sensitive ? try(sensitive(local.stdout), local.stdout) : local.stdout 107 | exitcode_str = trimspace(replace(replace(base64decode(data.external.run.result.exitcode), "\r", ""), "\r\n", "\n")) 108 | exitcode = local.exitcode_str == "null" ? null : tonumber(local.exitcode_str) 109 | } 110 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | //================================================== 2 | // Outputs that match the input variables 3 | //================================================== 4 | output "dynamic_depends_on" { 5 | description = "The value of the `dynamic_depends_on` input variable." 6 | value = local.var_dynamic_depends_on 7 | } 8 | output "command_unix" { 9 | description = "The value of the `command_unix` input variable, or the default value if the input was `null`, with all carriage returns removed." 10 | value = local.command_unix 11 | } 12 | output "command_windows" { 13 | description = "The value of the `command_windows` input variable, or the default value if the input was `null`, with all carriage returns removed." 14 | value = local.command_windows 15 | } 16 | output "environment" { 17 | description = "The value of the `environment` input variable, or the default value if the input was `null`, with all carriage returns removed." 18 | value = local.env_vars 19 | } 20 | output "working_dir" { 21 | description = "The value of the `working_dir` input variable." 22 | value = local.var_working_dir 23 | } 24 | output "fail_on_nonzero_exit_code" { 25 | description = "The value of the `fail_on_nonzero_exit_code` input variable, or the default value if the input was `null`." 26 | value = local.var_fail_on_nonzero_exit_code 27 | } 28 | output "fail_on_stderr" { 29 | description = "The value of the `fail_on_stderr` input variable, or the default value if the input was `null`." 30 | value = local.var_fail_on_stderr 31 | } 32 | output "force_wait_for_apply" { 33 | description = "The value of the `force_wait_for_apply` input variable, or the default value if the input was `null`." 34 | value = local.var_force_wait_for_apply 35 | } 36 | output "timeout" { 37 | description = "The value of the `timeout` input variable." 38 | value = local.var_timeout 39 | } 40 | output "fail_on_timeout" { 41 | description = "The value of the `fail_on_timeout` input variable, or the default value if the input was `null`." 42 | value = local.var_fail_on_timeout 43 | } 44 | output "unix_interpreter" { 45 | description = "The value of the `unix_interpreter` input variable, or the default value if the input was `null`." 46 | value = local.var_unix_interpreter 47 | } 48 | 49 | //================================================== 50 | // Outputs generated by this module 51 | //================================================== 52 | output "stdout" { 53 | description = "The stdout output of the shell command, with all carriage returns and trailing newlines removed." 54 | value = local.stdout_censored 55 | } 56 | output "stderr" { 57 | description = "The stderr output of the shell command, with all carriage returns and trailing newlines removed." 58 | value = local.stderr_censored 59 | } 60 | output "exit_code" { 61 | description = "The exit status code of the shell command. If the `timeout` input variable was provided and the command timed out, this will be `null`." 62 | value = local.exitcode 63 | } 64 | output "null_command_windows" { 65 | description = "A command that does nothing on Windows systems. You can use this output as the value for the `command_windows` input if you don't want the module to do anything on Windows systems." 66 | value = local.null_command_windows 67 | } 68 | output "null_command_unix" { 69 | description = "A command that does nothing on Unix-based systems. You can use this output as the value for the `command_unix` input if you don't want the module to do anything on Unix-based systems." 70 | value = local.null_command_unix 71 | } 72 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | # Equivalent of set -e 2 | $ErrorActionPreference = "Stop" 3 | 4 | # Equivalent of set -u (https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode?view=powershell-7.1) 5 | set-strictmode -version 3.0 6 | 7 | $jsonpayload = [Console]::In.ReadLine() 8 | $json = ConvertFrom-Json $jsonpayload 9 | 10 | $_execution_id = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.execution_id)) 11 | $_directory = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.directory)) 12 | $_environment = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.environment)) 13 | $_timeout = [int][System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.timeout)) 14 | $_exit_on_nonzero = [System.Convert]::ToBoolean([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.exit_on_nonzero))) 15 | $_exit_on_stderr = [System.Convert]::ToBoolean([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.exit_on_stderr))) 16 | $_exit_on_timeout = [System.Convert]::ToBoolean([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.exit_on_timeout))) 17 | $_debug = [System.Convert]::ToBoolean([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($json.debug))) 18 | $_command_b64 = $json.command 19 | 20 | # Generate a random/unique ID 21 | if ( "$_execution_id" -eq " " ) { 22 | $_execution_id = [guid]::NewGuid().ToString() 23 | } 24 | $_cmdfile = "$_directory/$_execution_id.ps1" 25 | $_debugfile = "$_directory/$_execution_id.debug.txt" 26 | 27 | # Set the environment variables 28 | $_env_vars = $_environment.Split(";") 29 | foreach ($_env in $_env_vars) { 30 | if ( "$_env" -eq "" ) { 31 | continue 32 | } 33 | $_env_parts = $_env.Split(":") 34 | [Environment]::SetEnvironmentVariable([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_env_parts[0])), [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_env_parts[1])), "Process") 35 | } 36 | 37 | if ($_debug) { Write-Output "Environment variables set" | Out-File -FilePath "$_debugfile" } 38 | 39 | # Write the command to a file 40 | [System.IO.File]::WriteAllText("$_cmdfile", [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($_command_b64))) 41 | # Always force the command file to exit with the last exit code 42 | [System.IO.File]::AppendAllText("$_cmdfile", "`nExit `$LASTEXITCODE") 43 | 44 | if ($_debug) { Write-Output "Command file prepared" | Out-File -Append -FilePath "$_debugfile" } 45 | 46 | # This is a function that recursively kills all child processes of a process 47 | function TreeKill([int]$ProcessId) { 48 | if ($_debug) { Write-Output "Getting process children for $ProcessId" | Out-File -Append -FilePath "$_debugfile" } 49 | Get-CimInstance Win32_Process | Where-Object { $_.ParentProcessId -eq $ProcessId } | ForEach-Object { TreeKill -ProcessId $_.ProcessId } 50 | if ($_debug) { Write-Output "Killing process $ProcessId" | Out-File -Append -FilePath "$_debugfile" } 51 | $_p = Get-Process -ErrorAction SilentlyContinue -Id $ProcessId 52 | if ($_p) { 53 | Stop-Process -Force -Id $ProcessId 54 | $_p.WaitForExit(10000) | Out-Null 55 | if (!$_p.HasExited) { 56 | $_err = "Failed to kill the process after waiting for $_delay seconds:`n$_" 57 | if ($_debug) { Write-Output "$_err" | Out-File -Append -FilePath "$_debugfile" } 58 | Write-Error "$_err" 59 | Exit -1 60 | } 61 | } 62 | } 63 | 64 | $_pinfo = New-Object System.Diagnostics.ProcessStartInfo 65 | $_pinfo.FileName = "powershell.exe" 66 | # This allows capturing the output to a variable 67 | $_pinfo.RedirectStandardError = $true 68 | $_pinfo.RedirectStandardOutput = $true 69 | $_pinfo.UseShellExecute = $false 70 | $_pinfo.CreateNoWindow = $false 71 | $_pinfo.Arguments = "-NoProfile -File `"$_cmdfile`"" 72 | $_process = New-Object System.Diagnostics.Process 73 | $_process.StartInfo = $_pinfo 74 | 75 | if ($_debug) { Write-Output "Starting process" | Out-File -Append -FilePath "$_debugfile" } 76 | 77 | $ErrorActionPreference = "Continue" 78 | $_process.Start() | Out-Null 79 | $_out_task = $_process.StandardOutput.ReadToEndAsync(); 80 | $_err_task = $_process.StandardError.ReadToEndAsync(); 81 | $_timed_out = $false 82 | 83 | if ([int]$_timeout -eq 0) { 84 | $_process.WaitForExit() | Out-Null 85 | } 86 | else { 87 | $_process_result = $_process.WaitForExit($_timeout * 1000) 88 | if (-Not $_process_result) { 89 | if ($_debug) { Write-Output "Process timed out, killing..." | Out-File -Append -FilePath "$_debugfile" } 90 | TreeKill -ProcessId $_process.Id 91 | $_timed_out = $true 92 | } 93 | } 94 | $ErrorActionPreference = "Stop" 95 | 96 | if ($_debug) { Write-Output "Finished process" | Out-File -Append -FilePath "$_debugfile" } 97 | 98 | $_stdout = $_out_task.Result 99 | $_stderr = $_err_task.Result 100 | $_exitcode = $_process.ExitCode 101 | 102 | # Delete the command file, unless we're using debug mode, 103 | # in which case we might want to review it for debugging 104 | # purposes. 105 | if ( -not $_debug ) { 106 | Remove-Item "$_cmdfile" 107 | } 108 | 109 | # Check if the execution timed out 110 | if ($_timed_out) { 111 | # If it did, check if we're supposed to exit the script on a timeout 112 | if ( $_exit_on_timeout ) { 113 | $ErrorActionPreference = "Continue" 114 | Write-Error "Execution timed out after $_timeout seconds" 115 | $ErrorActionPreference = "Stop" 116 | Exit -1 117 | } 118 | else { 119 | $_exitcode = "null" 120 | } 121 | } 122 | 123 | # If we want to kill Terraform on a non-zero exit code and the exit code was non-zero, OR 124 | # we want to kill Terraform on a non-empty stderr and the stderr was non-empty 125 | if ((( $_exit_on_nonzero ) -and ($_exitcode -ne 0) -and ($_exitcode -ne "null")) -or (( $_exit_on_stderr ) -and "$_stderr")) { 126 | # If there was a stderr, write it out as an error 127 | if ("$_stderr") { 128 | # Set continue to not kill the process on writing an error, so we can exit with the desired exit code 129 | $ErrorActionPreference = "Continue" 130 | Write-Error "$_stderr" 131 | $ErrorActionPreference = "Stop" 132 | } 133 | # If a non-zero exit code was given, exit with it 134 | if (($_exitcode -ne 0) -and ($_exitcode -ne "null")) { 135 | exit $_exitcode 136 | } 137 | # Otherwise, exit with a default non-zero exit code 138 | exit 1 139 | } 140 | 141 | if ($_debug) { Write-Output "Done!" | Out-File -Append -FilePath "$_debugfile" } 142 | 143 | # Return the outputs as a JSON-encoded string for Terraform to parse 144 | @{ 145 | stderr = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($_stderr)) 146 | stdout = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($_stdout)) 147 | exitcode = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($_exitcode)) 148 | } | ConvertTo-Json -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | if ! [ -z "$BASH" ] ; then 3 | # Only Bash supports this feature 4 | set -o pipefail 5 | fi 6 | # Set this after checking for the BASH variable 7 | set -u 8 | 9 | # This checks if we're running on MacOS 10 | _kernel_name="$(uname -s)" 11 | case "${_kernel_name}" in 12 | darwin*|Darwin*) 13 | # It's MacOS. 14 | # Mac doesn't support the "-d" flag for base64 decoding, 15 | # so we have to use the full "--decode" flag instead. 16 | _decode_flag="--decode" 17 | # Mac doesn't support the "-w" flag for base64 wrapping, 18 | # and it isn't needed because by default it doens't break lines. 19 | _wrap_flag="" ;; 20 | *) 21 | # It's NOT MacOS. 22 | # Not all Linux base64 installs (e.g. BusyBox) support the full 23 | # "--decode" flag. So, we use "-d" here, since it's supported 24 | # by everything except MacOS. 25 | _decode_flag="-d" 26 | # All non-Mac installs need this to be specified to prevent line 27 | # wrapping, which adds newlines that we don't want. 28 | _wrap_flag="-w0" ;; 29 | esac 30 | 31 | # This checks if the "-n" flag is supported on this shell, and sets vars accordingly 32 | if [ "`echo -n`" = "-n" ] ; then 33 | _echo_n="" 34 | _echo_c="\c" 35 | else 36 | _echo_n="-n" 37 | _echo_c="" 38 | fi 39 | 40 | _raw_input="$(cat)" 41 | 42 | # We know that all of the inputs are base64-encoded, and "|" is not a valid base64 character, so therefore it 43 | # cannot possibly be included in the stdin. 44 | IFS="|" 45 | set -o noglob 46 | set -- $_raw_input"" 47 | _execution_id="$(echo "${2}" | base64 $_decode_flag)" 48 | _directory="$(echo "${3}" | base64 $_decode_flag)" 49 | _environment="$(echo "${4}" | base64 $_decode_flag)" 50 | _timeout="$(echo "${5}" | base64 $_decode_flag)" 51 | _exit_on_nonzero="$(echo "${6}" | base64 $_decode_flag)" 52 | _exit_on_stderr="$(echo "${7}" | base64 $_decode_flag)" 53 | _exit_on_timeout="$(echo "${8}" | base64 $_decode_flag)" 54 | _debug="$(echo "${9}" | base64 $_decode_flag)" 55 | _command_b64="${10}" 56 | _shell="$(echo "${11}" | base64 $_decode_flag)" 57 | 58 | # Generate a random/unique ID if an ID wasn't explicitly set 59 | if [ "$_execution_id" = " " ] ; then 60 | # We try many different strategies for generating a random number, hoping that at least one will succeed, 61 | # since each OS/shell supports a different combination. 62 | if [ -e /proc/sys/kernel/random/uuid ] ; then 63 | _execution_id="$(cat /proc/sys/kernel/random/uuid)" 64 | elif [ -e /dev/urandom ] ; then 65 | # Using head instead of cat here seems odd, and it is, but it deals with a pipefail 66 | # issue on MacOS that occurs if you use cat instead. 67 | _execution_id="$(head -c 10000 /dev/urandom | LC_ALL=C tr -dc '[:alnum:]' | head -c 40)" 68 | elif [ -e /dev/random ] ; then 69 | _execution_id="$(head -c 10000 /dev/random | LC_ALL=C tr -dc '[:alnum:]' | head -c 40)" 70 | else 71 | _execution_id="$RANDOM-$RANDOM-$RANDOM-$RANDOM" 72 | fi 73 | fi 74 | 75 | # The filenames to direct output to 76 | _stderrfile="$_directory/$_execution_id.stderr" 77 | _stdoutfile="$_directory/$_execution_id.stdout" 78 | _debugfile="$_directory/$_execution_id.debug" 79 | 80 | if [ $_debug = "true" ] ; then echo "Arguments loaded" > "$_debugfile"; fi 81 | 82 | # Split the env var input on semicolons. We use semicolons because we know 83 | # that neither the base64-encoded name or value will contain a semicolon. 84 | IFS=";" 85 | set -o noglob 86 | set -- $_environment"" 87 | for _env in "$@"; do 88 | if [ -z "$_env" ] ; then 89 | continue 90 | fi 91 | # For each env var, split it on a colon. We use colons because we know 92 | # that neither the env var name nor the base64-encoded value will contain 93 | # a colon. 94 | _key="$(echo $_echo_n "${_env}${_echo_c}" | cut -d':' -f1 | base64 $_decode_flag)" 95 | _val="$(echo $_echo_n "${_env}${_echo_c}" | cut -d':' -f2 | base64 $_decode_flag)" 96 | export "$_key"="$_val" 97 | done 98 | 99 | # A command to run at the very end of the input script. This forces the script to 100 | # always exit with the exit code of the last command that returned an exit code. 101 | _cmd_suffix=<> "$_debugfile"; fi 115 | set +e 116 | 2>"$_stderrfile" >"$_stdoutfile" $_shell -c "$(echo "${_command_b64}" | base64 $_decode_flag)${_cmd_suffix}" 117 | _exitcode=$? 118 | set -e 119 | else 120 | # Add a prefix to the command, which wraps the commands in a block 121 | _cmd_prefix=<&3 130 | EOF 131 | # Default to using the built-in timeout command 132 | _timeout_cmd="timeout" 133 | if ! command -v $_timeout_cmd >/dev/null 2>/dev/null; then 134 | # If it doesn't exist though, use the custom Perl one we created 135 | _timeout_cmd="perl_timeout" 136 | fi 137 | 138 | # There is a timeout set, so run the command with it 139 | if [ $_debug = "true" ] ; then echo "Starting process with a $_timeout second timeout" >> "$_debugfile"; fi 140 | set +e 141 | $_timeout_cmd $_timeout 3>"$_stderrfile" >"$_stdoutfile" $_shell -c "${_cmd_prefix}$(echo "${_command_b64}" | base64 $_decode_flag)${_cmd_suffix}" 142 | _exitcode=$? 143 | set -e 144 | # Check if it timed out. 142 is the exit code from a Perl alarm signal, 124 is the exit code from most built-in 145 | # "timeout" commands, and 143 is the exit code from the Busybox "timeout" command. 146 | if [ $_exitcode -eq 142 ] || [ $_exitcode -eq 124 ] || [ $_exitcode -eq 143 ] ; then 147 | if [ $_debug = "true" ] ; then echo "Process timed out after $_timeout seconds" >> "$_debugfile"; fi 148 | _timed_out="true" 149 | fi 150 | fi 151 | 152 | if [ $_debug = "true" ] ; then echo "Execution complete" >> "$_debugfile"; fi 153 | 154 | # Read the stderr and stdout files 155 | if [ $_debug = "true" ] ; then echo "Reading stdout file" >> "$_debugfile"; fi 156 | _stdout="$(cat "$_stdoutfile")" 157 | if [ $_debug = "true" ] ; then echo "Reading stderr file" >> "$_debugfile"; fi 158 | _stderr="$(cat "$_stderrfile")" 159 | if [ $_debug = "true" ] ; then echo "Finished reading output files" >> "$_debugfile"; fi 160 | 161 | # Delete the files, unless we're using debug mode 162 | if [ "$_debug" != "true" ] ; then 163 | if [ $_debug = "true" ] ; then echo "Deleting stdout and stderr files" >> "$_debugfile"; fi 164 | rm "$_stderrfile" 165 | rm "$_stdoutfile" 166 | fi 167 | 168 | # Check if the execution timed out 169 | if [ "$_timed_out" = "true" ] ; then 170 | if [ "$_exit_on_timeout" = "true" ] ; then 171 | if [ $_debug = "true" ] ; then echo "Failing due to a timeout error" >> "$_debugfile"; fi 172 | >&2 echo $_echo_n "Execution timed out after $_timeout seconds${_echo_c}" 173 | exit 1 174 | else 175 | _exitcode="null" 176 | fi 177 | fi 178 | 179 | # If we want to kill Terraform on a non-zero exit code and the exit code was non-zero, OR 180 | # we want to kill Terraform on a non-empty stderr and the stderr was non-empty 181 | if ( [ "$_exit_on_nonzero" = "true" ] && [ "$_exitcode" != "null" ] && [ $_exitcode -ne 0 ] ) || ( [ "$_exit_on_stderr" = "true" ] && ! [ -z "$_stderr" ] ) ; then 182 | # If there was a stderr, write it out as an error 183 | if ! [ -z "$_stderr" ] ; then 184 | if [ $_debug = "true" ] && [ "$_exit_on_stderr" = "true" ] ; then echo "Failing due to presence of stderr output" >> "$_debugfile"; fi 185 | >&2 echo $_echo_n "${_stderr}${_echo_c}" 186 | fi 187 | 188 | # If a non-zero exit code was given, exit with it 189 | if ( [ "$_exitcode" != "null" ] && [ "$_exitcode" -ne 0 ] ); then 190 | if [ $_debug = "true" ] && [ "$_exit_on_nonzero" = "true" ] ; then echo "Failing due to a non-zero exit code ($_exitcode)" >> "$_debugfile"; fi 191 | exit $_exitcode 192 | fi 193 | if [ $_debug = "true" ] ; then echo -e "\nStdout:\n$_stdout" >> "$_debugfile"; fi 194 | if [ $_debug = "true" ] ; then echo -e "\nStderr:\n$_stderr" >> "$_debugfile"; fi 195 | # Otherwise, exit with a default non-zero exit code 196 | exit 1 197 | fi 198 | 199 | # Base64-encode the stdout and stderr for transmission back to Terraform 200 | _stdout_b64=$(echo $_echo_n "${_stdout}${_echo_c}" | base64 $_wrap_flag) 201 | _stderr_b64=$(echo $_echo_n "${_stderr}${_echo_c}" | base64 $_wrap_flag) 202 | _exitcode_b64=$(echo $_echo_n "${_exitcode}${_echo_c}" | base64 $_wrap_flag) 203 | 204 | # Echo a JSON string that Terraform can parse as the result 205 | echo $_echo_n "{\"stdout\": \"$_stdout_b64\", \"stderr\": \"$_stderr_b64\", \"exitcode\": \"$_exitcode_b64\"}${_echo_c}" 206 | 207 | if [ $_debug = "true" ] ; then echo "Done!" >> "$_debugfile"; fi 208 | exit 0 209 | -------------------------------------------------------------------------------- /tester/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | test_file_path = "${path.module}/../tests/" 3 | // The ternary within the fileset just forces TF to wait for the delete_existing_files module before going any further 4 | test_files = fileset(local.test_file_path, "${local.test_file_path}**.json") 5 | platform = dirname("/") == "\\" ? "windows" : "unix" 6 | tests = { 7 | for test_file in local.test_files : 8 | replace(replace(trimsuffix(trimprefix(test_file, local.test_file_path), ".json"), "\\", "/"), "/", "_") => jsondecode(file("${local.test_file_path}${test_file}")) 9 | } 10 | 11 | tests_fields = { 12 | for name, config in local.tests : 13 | name => keys(config) 14 | } 15 | 16 | expected_output_fields = ["expected_stdout", "expected_stderr", "expected_exit_code", "expected_stdout_sensitive", "expected_stderr_sensitive"] 17 | 18 | tests_invalid_fields = { 19 | for name, keys in local.tests_fields : 20 | name => [ 21 | for field in keys : 22 | field 23 | if !contains(concat([ 24 | "command_unix", 25 | "command_windows", 26 | "environment", 27 | "environment_sensitive", 28 | "working_dir", 29 | "timeout", 30 | "fail_on_nonzero_exit_code", 31 | "fail_on_stderr", 32 | "fail_on_timeout", 33 | "suppress_console", 34 | "platforms", 35 | ], local.expected_output_fields), field) 36 | ] 37 | } 38 | 39 | invalid_fields_errs = [ 40 | for name, fields in local.tests_invalid_fields : 41 | "${name} (${join(", ", fields)})" 42 | if length(fields) > 0 43 | ] 44 | 45 | invalid_fields_err_msg = length(local.invalid_fields_errs) > 0 ? "The following tests have invalid fields: ${join("; ", local.invalid_fields_errs)}" : null 46 | 47 | tests_without_expected_output = [ 48 | for name, keys in local.tests_fields : 49 | name 50 | if length(setintersection(keys, local.expected_output_fields)) == 0 51 | ] 52 | 53 | missing_expected_output_err_msg = length(local.tests_without_expected_output) > 0 ? "The following tests are missing an expected output field (each test must have one of: ${join(", ", local.expected_output_fields)}): ${join(", ", local.tests_without_expected_output)}" : null 54 | 55 | tests_to_run = { 56 | for name, config in local.tests : 57 | name => config 58 | if contains(lookup(config, "platforms", ["windows", "unix"]), local.platform) 59 | } 60 | } 61 | 62 | module "assert_test_fields_valid" { 63 | source = "Invicton-Labs/assertion/null" 64 | version = "~>0.2.3" 65 | condition = local.invalid_fields_err_msg == null 66 | error_message = local.invalid_fields_err_msg 67 | } 68 | 69 | module "assert_expected_output_fields" { 70 | source = "Invicton-Labs/assertion/null" 71 | version = "~>0.2.3" 72 | condition = local.missing_expected_output_err_msg == null 73 | error_message = local.missing_expected_output_err_msg 74 | } 75 | 76 | module "tests" { 77 | source = "../" 78 | for_each = module.assert_test_fields_valid.checked && module.assert_expected_output_fields.checked ? local.tests_to_run : null 79 | command_unix = lookup(each.value, "command_unix", null) 80 | command_windows = lookup(each.value, "command_windows", null) 81 | environment = lookup(each.value, "environment", null) 82 | environment_sensitive = lookup(each.value, "environment_sensitive", null) 83 | working_dir = lookup(each.value, "working_dir", null) 84 | timeout = lookup(each.value, "timeout", null) 85 | fail_on_nonzero_exit_code = lookup(each.value, "fail_on_nonzero_exit_code", null) 86 | fail_on_stderr = lookup(each.value, "fail_on_stderr", null) 87 | fail_on_timeout = lookup(each.value, "fail_on_timeout", null) 88 | suppress_console = lookup(each.value, "suppress_console", null) 89 | unix_interpreter = var.unix_interpreter 90 | //force_wait_for_apply = true 91 | //execution_id = each.key 92 | } 93 | 94 | locals { 95 | tf_version_supports_nonsensitive = can(nonsensitive(sensitive("hello world"))) 96 | 97 | incorrect_outputs = { 98 | for name, config in local.tests_to_run : 99 | name => flatten([ 100 | contains(local.tests_fields[name], "expected_stdout") ? config.expected_stdout != module.tests[name].stdout ? ["Incorrect value for stdout: expected \"${config.expected_stdout}\", got \"${module.tests[name].stdout}\""] : [] : [], 101 | contains(local.tests_fields[name], "expected_stderr") ? config.expected_stderr != module.tests[name].stderr ? ["Incorrect value for stderr: expected \"${config.expected_stderr}\", got \"${module.tests[name].stderr}\""] : [] : [], 102 | contains(local.tests_fields[name], "expected_exit_code") ? config.expected_exit_code != module.tests[name].exit_code ? ["Incorrect value for exit code: expected \"${config.expected_exit_code == null ? "null" : config.expected_exit_code}\", got \"${module.tests[name].exit_code == null ? "null" : module.tests[name].exit_code}\""] : [] : [], 103 | local.tf_version_supports_nonsensitive ? [ 104 | contains(local.tests_fields[name], "expected_stdout_sensitive") ? ( 105 | config.expected_stdout_sensitive ? ( 106 | // If we expect it to be sensitive, make sure we can nonsensitive it 107 | !can(nonsensitive(module.tests[name].stdout)) ? ["Incorrect sensitivity for stdout: expected a sensitive value, but received a non-sensitive value."] : [] 108 | ) : ( 109 | // If we expect it to be nonsensitive, make sure we can NOT nonsensitive it 110 | can(nonsensitive(module.tests[name].stdout)) ? ["Incorrect sensitivity for stdout: expected a non-sensitive value, but received a sensitive value."] : [] 111 | ) 112 | ) : [], 113 | contains(local.tests_fields[name], "expected_stderr_sensitive") ? ( 114 | config.expected_stderr_sensitive ? ( 115 | // If we expect it to be sensitive, make sure we can nonsensitive it 116 | !can(nonsensitive(module.tests[name].stderr)) ? ["Incorrect sensitivity for stderr: expected a sensitive value, but received a non-sensitive value."] : [] 117 | ) : ( 118 | // If we expect it to be nonsensitive, make sure we can NOT nonsensitive it 119 | can(nonsensitive(module.tests[name].stderr)) ? ["Incorrect sensitivity for stderr: expected a non-sensitive value, but received a sensitive value."] : [] 120 | ) 121 | ) : [], 122 | ] : [] 123 | ]) 124 | } 125 | 126 | incorrect_output_err_msgs = { 127 | for name, incorrect_outputs in local.incorrect_outputs : 128 | name => join("\n", incorrect_outputs) 129 | if length(incorrect_outputs) > 0 130 | } 131 | 132 | incorrect_output_err_msg = length(local.incorrect_output_err_msgs) > 0 ? "At least one test has outputs that do not match the expected values.\n${join("\n\n", [for k, v in local.incorrect_output_err_msgs : "${k}:\n${v}"])}" : null 133 | } 134 | 135 | module "assert_expected_output" { 136 | source = "Invicton-Labs/assertion/null" 137 | version = "~>0.2.3" 138 | condition = local.incorrect_output_err_msg == null 139 | error_message = try(nonsensitive(local.incorrect_output_err_msg), local.incorrect_output_err_msg) 140 | } 141 | -------------------------------------------------------------------------------- /tester/variables.tf: -------------------------------------------------------------------------------- 1 | variable "unix_interpreter" { 2 | description = "The interpreter to use when running commands on a Unix-based system. This is primarily used for testing, and should usually be left to the default value." 3 | type = string 4 | default = null 5 | } 6 | -------------------------------------------------------------------------------- /tests/command-exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "if ! command -v \"$COMMAND\" >/dev/null 2>/dev/null; then\necho 'does not exist'\nexit $EXITCODE\nelse\necho 'exists'\nexit 0\nfi", 3 | "command_windows": "$ErrorActionPreference = 'stop'\ntry {\nif(Get-Command $Env:COMMAND){\nWrite-Output \"exists\"\nexit 0\n}\n}\nCatch {\nWrite-Output \"does not exist\"\nexit [int]$Env:EXITCODE\n}", 4 | "fail_on_nonzero_exit_code": false, 5 | "environment": { 6 | "COMMAND": "echo", 7 | "EXITCODE": 33 8 | }, 9 | "expected_stdout": "exists", 10 | "expected_stderr": "", 11 | "expected_exit_code": 0 12 | } -------------------------------------------------------------------------------- /tests/command-not-exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "if ! command -v \"$COMMAND\" >/dev/null 2>/dev/null; then\necho 'does not exist'\nexit $EXITCODE\nelse\necho 'exists'\nexit 0\nfi", 3 | "command_windows": "$ErrorActionPreference = 'stop'\ntry {\nif(Get-Command $Env:COMMAND){\nWrite-Output \"exists\"\nexit 0\n}\n}\nCatch {\nWrite-Output \"does not exist\"\nexit [int]$Env:EXITCODE\n}", 4 | "fail_on_nonzero_exit_code": false, 5 | "environment": { 6 | "COMMAND": "somerandomcommandthatshouldnotexist", 7 | "EXITCODE": 33 8 | }, 9 | "expected_stdout": "does not exist", 10 | "expected_stderr": "", 11 | "expected_exit_code": 33 12 | } -------------------------------------------------------------------------------- /tests/complex-unix.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": [ 3 | "unix" 4 | ], 5 | "command_unix": "myvar=$(cat <&2 echo \"$ERR\"\nexit 123", 6 | "fail_on_nonzero_exit_code": false, 7 | "expected_stdout": "hello world\n4321", 8 | "environment": { 9 | "INPUT1": "hello world", 10 | "INPUT2": 4321, 11 | "ERR": "goodbye world" 12 | }, 13 | "expected_stderr": "goodbye world", 14 | "expected_exit_code": 123 15 | } -------------------------------------------------------------------------------- /tests/complex-windows.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": [ 3 | "windows" 4 | ], 5 | "command_windows": "$myvar=@\"\n$Env:INPUT1\n$Env:INPUT2\n\"@\nWrite-Output \"$myvar\"\nexit 123", 6 | "fail_on_nonzero_exit_code": false, 7 | "expected_stdout": "hello world\n4321", 8 | "environment": { 9 | "INPUT1": "hello world", 10 | "INPUT2": 4321 11 | }, 12 | "expected_stderr": "", 13 | "expected_exit_code": 123 14 | } -------------------------------------------------------------------------------- /tests/echo-env-var-nonsensitive.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"${INPUT}-${lowerINPUT1234}-${MULTILINE}\"", 3 | "command_windows": "Write-Output \"$Env:INPUT-$Env:lowerINPUT1234-$Env:MULTILINE\"", 4 | "environment": { 5 | "INPUT": "hello world", 6 | "lowerINPUT1234": "goodbye world", 7 | "MULTILINE": "line1\nline2\nline3" 8 | }, 9 | "expected_stdout": "hello world-goodbye world-line1\nline2\nline3", 10 | "expected_stderr": "", 11 | "expected_exit_code": 0, 12 | "expected_stdout_sensitive": false 13 | } -------------------------------------------------------------------------------- /tests/echo-env-var-sensitive.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"${INPUT}-${lowerINPUT1234}-${MULTILINE}\"", 3 | "command_windows": "Write-Output \"$Env:INPUT-$Env:lowerINPUT1234-$Env:MULTILINE\"", 4 | "environment": { 5 | "INPUT": "hello world", 6 | "lowerINPUT1234": "goodbye world" 7 | }, 8 | "environment_sensitive": { 9 | "MULTILINE": "line1\nline2\nline3" 10 | }, 11 | "expected_stdout": "hello world-goodbye world-line1\nline2\nline3", 12 | "expected_stderr": "", 13 | "expected_exit_code": 0, 14 | "expected_stdout_sensitive": true 15 | } -------------------------------------------------------------------------------- /tests/echo-env-var-stderr-nonsensitive.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": [ 3 | "unix" 4 | ], 5 | "command_unix": ">&2 echo \"${INPUT}-${lowerINPUT1234}-${MULTILINE}\"", 6 | "environment": { 7 | "INPUT": "hello world", 8 | "lowerINPUT1234": "goodbye world", 9 | "MULTILINE": "line1\nline2\nline3" 10 | }, 11 | "expected_stdout": "", 12 | "expected_stderr": "hello world-goodbye world-line1\nline2\nline3", 13 | "expected_exit_code": 0, 14 | "expected_stdout_sensitive": false, 15 | "expected_stderr_sensitive": false 16 | } -------------------------------------------------------------------------------- /tests/echo-env-var-stderr-sensitive.json: -------------------------------------------------------------------------------- 1 | { 2 | "platforms": [ 3 | "unix" 4 | ], 5 | "command_unix": ">&2 echo \"${INPUT}-${lowerINPUT1234}-${MULTILINE}\"", 6 | "environment": { 7 | "INPUT": "hello world", 8 | "lowerINPUT1234": "goodbye world" 9 | }, 10 | "environment_sensitive": { 11 | "MULTILINE": "line1\nline2\nline3" 12 | }, 13 | "expected_stdout": "", 14 | "expected_stderr": "hello world-goodbye world-line1\nline2\nline3", 15 | "expected_exit_code": 0, 16 | "expected_stdout_sensitive": false, 17 | "expected_stderr_sensitive": true 18 | } -------------------------------------------------------------------------------- /tests/echo-long.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"${INPUT}\"", 3 | "command_windows": "Write-Output \"$Env:INPUT\"", 4 | "environment": { 5 | "INPUT": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vestibulum consectetur massa, sed varius orci venenatis vel. Ut vitae vestibulum elit. Nam non nisl vehicula dui lacinia sagittis. Sed maximus urna sem, id posuere felis semper sit amet. Aliquam nisi enim, pulvinar eget dui eu, tincidunt congue nulla. Aliquam non mi eu lorem porttitor eleifend id vitae augue. Aliquam nisl nibh, dignissim in dui sit amet, finibus eleifend orci. Vivamus tristique, justo vel molestie molestie, lectus augue finibus nisl, ac semper felis mauris ac mauris. Quisque ex nisi, vehicula sit amet risus sollicitudin, auctor molestie mauris. Sed condimentum nulla nibh, sed vehicula nisl rhoncus nec. Nullam turpis nisl, cursus quis fermentum nec, tempus eu elit. Pellentesque vitae bibendum ex, commodo tempus mi. Aenean dignissim condimentum dolor sed pulvinar. Aenean congue ligula nec vehicula scelerisque. Etiam non orci quis mauris posuere semper. Donec luctus libero eget nibh tincidunt dictum. Quisque cursus vehicula neque, eu vulputate ex scelerisque quis. Proin neque risus, elementum eget diam vulputate, consequat vestibulum dolor. Sed ac convallis elit, et maximus ligula. Aenean fringilla mauris a est dictum, sed gravida nulla convallis. Phasellus quam tellus, hendrerit et maximus sit amet, tristique eu turpis. Donec quis ligula euismod, rhoncus arcu eu, rhoncus massa. Donec vehicula pellentesque risus, eu venenatis sem tincidunt congue. Mauris vel sem in risus fermentum vulputate sed id dolor. Morbi at condimentum augue. Integer fringilla eleifend erat, eleifend aliquam nibh fermentum nec. Suspendisse in erat nec turpis finibus varius id eget velit. Nulla et enim lobortis libero imperdiet aliquam nec ut felis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur vel elit massa. Proin ut risus vitae mauris porta ultrices. Quisque elementum augue in ipsum tempus, sed congue magna pretium. Mauris ac tortor vitae lectus tincidunt hendrerit. Morbi sed sagittis velit. Nulla efficitur lacinia finibus. Curabitur condimentum nibh non rhoncus sollicitudin. Donec sem libero, euismod porta nunc a, sollicitudin luctus enim. Nam gravida tellus diam, eu placerat justo sagittis eget. Nam tincidunt, leo vitae mollis venenatis, nisi arcu ultrices neque, et hendrerit mi enim eu lacus. Nullam in nisl eu risus dignissim consequat. Aenean tincidunt pharetra sem, sit amet mattis sapien tempor vel. Donec commodo mi et sem volutpat porttitor. Ut varius orci odio, sed pellentesque risus molestie ut. Sed luctus, sapien et fermentum sodales, mauris enim semper dolor, ac finibus magna nunc id lorem. Nunc est nibh, gravida a volutpat ac, condimentum sit amet metus. Integer dignissim pellentesque arcu, at semper metus pretium sed. Aenean ut hendrerit dui. Donec condimentum, eros non congue venenatis, ex est pretium tortor, sed sodales mi mauris et magna. Integer bibendum ante quis egestas semper. Integer vel dignissim erat, vel vehicula magna. In ut arcu dui. Aenean nisi mi, tempor non sagittis id, fringilla non ipsum. In dapibus elit eros, id fringilla quam tempor at. Pellentesque aliquet urna finibus lorem accumsan, sit amet porta sem volutpat. Proin purus tellus, mollis vitae tempor in, congue sit amet sem. Vivamus risus nulla, iaculis non sodales a, eleifend id tellus. Sed neque lectus, pellentesque eget orci vitae, ornare pellentesque ex. Aenean sed lectus finibus, tempor eros sit amet, condimentum nisl. Nunc eget elementum quam. Nulla auctor leo non molestie congue. Maecenas vehicula turpis libero, mollis tincidunt dolor volutpat nec. Suspendisse in sem gravida, dapibus arcu non, interdum enim. Aenean rhoncus, nunc ut convallis malesuada, metus lectus semper mi, in viverra massa metus nec ipsum. Maecenas ipsum tellus, interdum sit amet arcu at, feugiat suscipit magna. Sed sed ligula molestie, finibus massa in, auctor felis. Praesent rhoncus dictum libero, eu fermentum turpis ullamcorper id. Etiam mattis lacus vitae tellus dignissim congue. Praesent nunc tellus, scelerisque at ornare id, elementum vel dolor. In porttitor sapien sit amet quam auctor congue. Duis dignissim id nulla in aliquet. Aliquam erat volutpat. Nam ut dignissim augue. Nulla vehicula dolor ex, id venenatis dolor iaculis varius. Nulla lacinia velit lorem, eu interdum enim accumsan quis. Sed a tellus eget nunc imperdiet dignissim ac id neque. Proin vestibulum sed sapien vel accumsan. Sed laoreet arcu in lectus congue posuere. Cras tincidunt, dolor a varius faucibus, mi arcu cursus felis, vel posuere felis tortor non dui. Vestibulum volutpat ipsum nec nibh euismod, eu elementum ipsum gravida. Vivamus erat nulla, fermentum non euismod quis, euismod vel felis. Nulla vitae velit ac erat suscipit tempor id eget diam. Nullam sagittis imperdiet risus non imperdiet. Cras laoreet vitae nibh ut condimentum. Quisque in purus tincidunt, laoreet metus ac, gravida mauris. Ut eu turpis sapien. Aliquam pulvinar non enim eget sodales. Morbi auctor diam quis felis scelerisque, quis maximus nibh dapibus. Aliquam ut aliquam mauris, id luctus eros. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut vel urna erat. In sit amet hendrerit enim. Vivamus maximus dignissim velit quis vestibulum. Nullam ut nisi neque. Cras semper mauris dui, vel ultrices augue finibus vel. Maecenas sed odio eget mi egestas viverra vitae gravida urna. Suspendisse bibendum nisl vitae felis posuere luctus. Fusce nec laoreet sem. Proin sit amet placerat sem. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus mollis orci vel mauris lacinia tempus. Nullam nulla eros, porttitor at posuere sit amet, pellentesque nec ex. Maecenas eget efficitur sem, sit amet faucibus nisi. Sed euismod sed est ac euismod. Sed gravida dolor venenatis, placerat dolor quis, semper turpis. Sed aliquam dui metus, nec accumsan erat hendrerit eget. Sed eu facilisis justo. Donec viverra ante in augue pharetra, aliquam malesuada est egestas. Maecenas ac enim feugiat, egestas eros sit amet, sollicitudin velit. Nunc faucibus at eros id euismod. Etiam viverra ut elit semper finibus. Praesent condimentum tincidunt dui quis elementum. Cras rhoncus, velit ac consequat vestibulum, nulla purus vulputate dui, at gravida quam nisi et elit. Suspendisse vitae felis eu felis lobortis porta ac aliquam eros. Sed et eleifend leo. Quisque a sagittis nibh, ac mollis tellus. Ut luctus rhoncus est. Nunc eu tempor odio. Donec consequat quam vitae purus cursus, sit amet tempor arcu mollis. Integer semper pharetra justo, sit amet pharetra." 6 | }, 7 | "expected_stdout": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vestibulum consectetur massa, sed varius orci venenatis vel. Ut vitae vestibulum elit. Nam non nisl vehicula dui lacinia sagittis. Sed maximus urna sem, id posuere felis semper sit amet. Aliquam nisi enim, pulvinar eget dui eu, tincidunt congue nulla. Aliquam non mi eu lorem porttitor eleifend id vitae augue. Aliquam nisl nibh, dignissim in dui sit amet, finibus eleifend orci. Vivamus tristique, justo vel molestie molestie, lectus augue finibus nisl, ac semper felis mauris ac mauris. Quisque ex nisi, vehicula sit amet risus sollicitudin, auctor molestie mauris. Sed condimentum nulla nibh, sed vehicula nisl rhoncus nec. Nullam turpis nisl, cursus quis fermentum nec, tempus eu elit. Pellentesque vitae bibendum ex, commodo tempus mi. Aenean dignissim condimentum dolor sed pulvinar. Aenean congue ligula nec vehicula scelerisque. Etiam non orci quis mauris posuere semper. Donec luctus libero eget nibh tincidunt dictum. Quisque cursus vehicula neque, eu vulputate ex scelerisque quis. Proin neque risus, elementum eget diam vulputate, consequat vestibulum dolor. Sed ac convallis elit, et maximus ligula. Aenean fringilla mauris a est dictum, sed gravida nulla convallis. Phasellus quam tellus, hendrerit et maximus sit amet, tristique eu turpis. Donec quis ligula euismod, rhoncus arcu eu, rhoncus massa. Donec vehicula pellentesque risus, eu venenatis sem tincidunt congue. Mauris vel sem in risus fermentum vulputate sed id dolor. Morbi at condimentum augue. Integer fringilla eleifend erat, eleifend aliquam nibh fermentum nec. Suspendisse in erat nec turpis finibus varius id eget velit. Nulla et enim lobortis libero imperdiet aliquam nec ut felis. Interdum et malesuada fames ac ante ipsum primis in faucibus. Curabitur vel elit massa. Proin ut risus vitae mauris porta ultrices. Quisque elementum augue in ipsum tempus, sed congue magna pretium. Mauris ac tortor vitae lectus tincidunt hendrerit. Morbi sed sagittis velit. Nulla efficitur lacinia finibus. Curabitur condimentum nibh non rhoncus sollicitudin. Donec sem libero, euismod porta nunc a, sollicitudin luctus enim. Nam gravida tellus diam, eu placerat justo sagittis eget. Nam tincidunt, leo vitae mollis venenatis, nisi arcu ultrices neque, et hendrerit mi enim eu lacus. Nullam in nisl eu risus dignissim consequat. Aenean tincidunt pharetra sem, sit amet mattis sapien tempor vel. Donec commodo mi et sem volutpat porttitor. Ut varius orci odio, sed pellentesque risus molestie ut. Sed luctus, sapien et fermentum sodales, mauris enim semper dolor, ac finibus magna nunc id lorem. Nunc est nibh, gravida a volutpat ac, condimentum sit amet metus. Integer dignissim pellentesque arcu, at semper metus pretium sed. Aenean ut hendrerit dui. Donec condimentum, eros non congue venenatis, ex est pretium tortor, sed sodales mi mauris et magna. Integer bibendum ante quis egestas semper. Integer vel dignissim erat, vel vehicula magna. In ut arcu dui. Aenean nisi mi, tempor non sagittis id, fringilla non ipsum. In dapibus elit eros, id fringilla quam tempor at. Pellentesque aliquet urna finibus lorem accumsan, sit amet porta sem volutpat. Proin purus tellus, mollis vitae tempor in, congue sit amet sem. Vivamus risus nulla, iaculis non sodales a, eleifend id tellus. Sed neque lectus, pellentesque eget orci vitae, ornare pellentesque ex. Aenean sed lectus finibus, tempor eros sit amet, condimentum nisl. Nunc eget elementum quam. Nulla auctor leo non molestie congue. Maecenas vehicula turpis libero, mollis tincidunt dolor volutpat nec. Suspendisse in sem gravida, dapibus arcu non, interdum enim. Aenean rhoncus, nunc ut convallis malesuada, metus lectus semper mi, in viverra massa metus nec ipsum. Maecenas ipsum tellus, interdum sit amet arcu at, feugiat suscipit magna. Sed sed ligula molestie, finibus massa in, auctor felis. Praesent rhoncus dictum libero, eu fermentum turpis ullamcorper id. Etiam mattis lacus vitae tellus dignissim congue. Praesent nunc tellus, scelerisque at ornare id, elementum vel dolor. In porttitor sapien sit amet quam auctor congue. Duis dignissim id nulla in aliquet. Aliquam erat volutpat. Nam ut dignissim augue. Nulla vehicula dolor ex, id venenatis dolor iaculis varius. Nulla lacinia velit lorem, eu interdum enim accumsan quis. Sed a tellus eget nunc imperdiet dignissim ac id neque. Proin vestibulum sed sapien vel accumsan. Sed laoreet arcu in lectus congue posuere. Cras tincidunt, dolor a varius faucibus, mi arcu cursus felis, vel posuere felis tortor non dui. Vestibulum volutpat ipsum nec nibh euismod, eu elementum ipsum gravida. Vivamus erat nulla, fermentum non euismod quis, euismod vel felis. Nulla vitae velit ac erat suscipit tempor id eget diam. Nullam sagittis imperdiet risus non imperdiet. Cras laoreet vitae nibh ut condimentum. Quisque in purus tincidunt, laoreet metus ac, gravida mauris. Ut eu turpis sapien. Aliquam pulvinar non enim eget sodales. Morbi auctor diam quis felis scelerisque, quis maximus nibh dapibus. Aliquam ut aliquam mauris, id luctus eros. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Ut vel urna erat. In sit amet hendrerit enim. Vivamus maximus dignissim velit quis vestibulum. Nullam ut nisi neque. Cras semper mauris dui, vel ultrices augue finibus vel. Maecenas sed odio eget mi egestas viverra vitae gravida urna. Suspendisse bibendum nisl vitae felis posuere luctus. Fusce nec laoreet sem. Proin sit amet placerat sem. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Phasellus mollis orci vel mauris lacinia tempus. Nullam nulla eros, porttitor at posuere sit amet, pellentesque nec ex. Maecenas eget efficitur sem, sit amet faucibus nisi. Sed euismod sed est ac euismod. Sed gravida dolor venenatis, placerat dolor quis, semper turpis. Sed aliquam dui metus, nec accumsan erat hendrerit eget. Sed eu facilisis justo. Donec viverra ante in augue pharetra, aliquam malesuada est egestas. Maecenas ac enim feugiat, egestas eros sit amet, sollicitudin velit. Nunc faucibus at eros id euismod. Etiam viverra ut elit semper finibus. Praesent condimentum tincidunt dui quis elementum. Cras rhoncus, velit ac consequat vestibulum, nulla purus vulputate dui, at gravida quam nisi et elit. Suspendisse vitae felis eu felis lobortis porta ac aliquam eros. Sed et eleifend leo. Quisque a sagittis nibh, ac mollis tellus. Ut luctus rhoncus est. Nunc eu tempor odio. Donec consequat quam vitae purus cursus, sit amet tempor arcu mollis. Integer semper pharetra justo, sit amet pharetra.", 8 | "expected_stderr": "", 9 | "expected_exit_code": 0 10 | } -------------------------------------------------------------------------------- /tests/echo-stderr.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": ">&2 echo \"hello world\"", 3 | "platforms": [ 4 | "unix" 5 | ], 6 | "expected_stdout": "", 7 | "expected_stderr": "hello world", 8 | "expected_exit_code": 0 9 | } -------------------------------------------------------------------------------- /tests/echo-stdout-unsuppressed.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"hello world\"", 3 | "command_windows": "Write-Output \"hello world\"", 4 | "expected_stdout": "hello world", 5 | "expected_stderr": "", 6 | "expected_exit_code": 0, 7 | "suppress_console": false 8 | } -------------------------------------------------------------------------------- /tests/echo-stdout.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"hello world\"", 3 | "command_windows": "Write-Output \"hello world\"", 4 | "expected_stdout": "hello world", 5 | "expected_stderr": "", 6 | "expected_exit_code": 0 7 | } -------------------------------------------------------------------------------- /tests/exit-nonzero.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "exit 123", 3 | "fail_on_nonzero_exit_code": false, 4 | "expected_stdout": "", 5 | "expected_stderr": "", 6 | "expected_exit_code": 123 7 | } -------------------------------------------------------------------------------- /tests/multiline-comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "a='hello world'\n# This checks if we're running on MacOS\n_kernel_name=\"$(uname -s)\"\ncase \"${_kernel_name}\" in\n darwin*|Darwin*) \n # It's MacOS.\n # Mac doesn't support the \"-d\" flag for base64 decoding, \n # so we have to use the full \"--decode\" flag instead.\n _decode_flag=\"--decode\" ;;\n *)\n # It's NOT MacOS.\n # Not all Linux base64 installs (e.g. BusyBox) support the full\n # \"--decode\" flag. So, we use \"-d\" here, since it's supported\n # by everything except MacOS.\n _decode_flag=\"-d\" ;;\nesac\necho \"$a\"", 3 | "command_windows": "$a='hello world'\n# This is a comment\nWrite-Output \"$a\"", 4 | "expected_stdout": "hello world", 5 | "expected_stderr": "", 6 | "expected_exit_code": 0 7 | } -------------------------------------------------------------------------------- /tests/multiline.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "a='hello world'\necho \"$a\"", 3 | "command_windows": "$a='hello world'\nWrite-Output \"$a\"", 4 | "expected_stdout": "hello world", 5 | "expected_stderr": "", 6 | "expected_exit_code": 0 7 | } -------------------------------------------------------------------------------- /tests/special-characters-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"$CHARACTERS\"", 3 | "command_windows": "Write-Output \"$Env:CHARACTERS\"", 4 | "environment": { 5 | "CHARACTERS": "~@#$%&*()-_=+{}[]<>?/.,'\"`;!" 6 | }, 7 | "expected_stdout": "~@#$%&*()-_=+{}[]<>?/.,'\"`;!", 8 | "expected_stderr": "", 9 | "expected_exit_code": 0 10 | } -------------------------------------------------------------------------------- /tests/special-characters.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo \"~@#$%&*()-_=+{}[]<>?/.,'\\\"\\`;!\"", 3 | "command_windows": "Write-Output \"~@#$%&*()-_=+{}[]<>?/.,'`\"``;!\"", 4 | "expected_stdout": "~@#$%&*()-_=+{}[]<>?/.,'\"`;!", 5 | "expected_stderr": "", 6 | "expected_exit_code": 0 7 | } -------------------------------------------------------------------------------- /tests/timeout-fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo 'hello world'\nsleep 10\necho 'goodbye world'", 3 | "command_windows": "Write-Output 'hello world'\nStart-Sleep -Seconds 10\nWrite-Output 'goodbye world'", 4 | "timeout": 3, 5 | "fail_on_timeout": false, 6 | "expected_stdout": "hello world", 7 | "expected_stderr": "", 8 | "expected_exit_code": null 9 | } -------------------------------------------------------------------------------- /tests/timeout-succeed.json: -------------------------------------------------------------------------------- 1 | { 2 | "command_unix": "echo 'hello'\nsleep 1\necho 'world'", 3 | "command_windows": "Write-Output 'hello'\nStart-Sleep -Seconds 1\nWrite-Output 'world'", 4 | "timeout": 30, 5 | "fail_on_timeout": true, 6 | "expected_stdout": "hello\nworld", 7 | "expected_stderr": "", 8 | "expected_exit_code": 0 9 | } -------------------------------------------------------------------------------- /tmpfiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Invicton-Labs/terraform-external-shell-data/8d09bb53ae872d3520ad2c73f9abc9bede321f20/tmpfiles/.gitkeep -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "dynamic_depends_on" { 2 | description = "This input variable has the same function as the `depends_on` built-in variable, but has no restrictions on what kind of content it can contain." 3 | type = any 4 | default = null 5 | } 6 | locals { 7 | var_dynamic_depends_on = var.dynamic_depends_on 8 | } 9 | 10 | variable "command_unix" { 11 | description = "The command to run on creation when the module is used on a Unix machine. If not specified, will default to be the same as the `command_windows` variable." 12 | type = string 13 | default = null 14 | } 15 | 16 | variable "command_windows" { 17 | description = "The command to run on creation when the module is used on a Windows machine. If not specified, will default to be the same as the `command_unix` variable." 18 | type = string 19 | default = null 20 | } 21 | 22 | variable "environment" { 23 | description = "Map of environment variables to pass to the command." 24 | type = map(string) 25 | default = {} 26 | validation { 27 | // Ensure that none of the variable names violate the env var naming rules 28 | condition = var.environment == null ? true : length([ 29 | for k in keys(var.environment) : 30 | true 31 | if length(regexall("^[a-zA-Z_]+[a-zA-Z0-9_]*$", k)) == 0 32 | ]) == 0 33 | error_message = "Environment variable names must consist solely of letters, digits, and underscores, and may not start with a digit." 34 | } 35 | } 36 | locals { 37 | var_environment = var.environment != null ? var.environment : {} 38 | } 39 | 40 | variable "environment_sensitive" { 41 | description = "Map of sensitive environment variables to pass to the command. If any of these values are detected in the `stdout` or `stderr` outputs, they will be marked as sensitive. These keys/values will be merged with the `environment` input variable (this overwrites those values with the same key)." 42 | type = map(string) 43 | default = {} 44 | validation { 45 | // Ensure that none of the variable names violate the env var naming rules 46 | condition = var.environment_sensitive == null ? true : length([ 47 | for k in keys(var.environment_sensitive) : 48 | true 49 | if length(regexall("^[a-zA-Z_]+[a-zA-Z0-9_]*$", k)) == 0 50 | ]) == 0 51 | error_message = "Environment variable names must consist solely of letters, digits, and underscores, and may not start with a digit." 52 | } 53 | } 54 | locals { 55 | var_environment_sensitive = var.environment_sensitive != null ? var.environment_sensitive : {} 56 | } 57 | 58 | variable "working_dir" { 59 | description = "The working directory where command will be executed. Defaults to this module's install directory (usually somewhere in the `.terraform` directory)." 60 | type = string 61 | default = null 62 | } 63 | locals { 64 | var_working_dir = var.working_dir 65 | } 66 | 67 | variable "fail_on_nonzero_exit_code" { 68 | description = "Whether a Terraform error should be thrown if the command exits with a non-zero exit code. If true, nothing will be returned from this module and Terraform will fail the plan/apply. If false, the error message will be returned in `stderr` and the error code will be returned in `exit_code`." 69 | type = bool 70 | default = true 71 | } 72 | locals { 73 | var_fail_on_nonzero_exit_code = var.fail_on_nonzero_exit_code != null ? var.fail_on_nonzero_exit_code : true 74 | } 75 | 76 | variable "fail_on_stderr" { 77 | description = "Whether a Terraform error should be thrown if the command outputs anything to stderr. If true, nothing will be returned from this module and Terraform will fail the plan/apply. If false, the error message will be returned in `stderr` and the exit code will be returned in `exit_code`." 78 | type = bool 79 | default = false 80 | } 81 | locals { 82 | var_fail_on_stderr = var.fail_on_stderr != null ? var.fail_on_stderr : false 83 | } 84 | 85 | variable "force_wait_for_apply" { 86 | description = "Whether to force this module to wait for apply-time to execute the shell command. Otherwise, it will run during plan-time if possible (i.e. if all inputs are known during plan time)." 87 | type = bool 88 | default = false 89 | } 90 | locals { 91 | var_force_wait_for_apply = var.force_wait_for_apply != null ? var.force_wait_for_apply : false 92 | } 93 | 94 | variable "timeout" { 95 | description = "The maximum number of seconds to allow the shell command to execute for If it exceeds this timeout, it will be killed and will fail. Leave as the default (`null`) or set as 0 for no timeout." 96 | type = number 97 | default = null 98 | validation { 99 | condition = var.timeout == null ? true : var.timeout >= 0 100 | error_message = "The `timeout` input variable, if provided, must be greater than or equal to 0." 101 | } 102 | } 103 | locals { 104 | var_timeout = var.timeout == 0 ? null : var.timeout 105 | } 106 | 107 | variable "fail_on_timeout" { 108 | description = "Whether a Terraform error should be thrown if the command times out. If true, nothing will be returned from this module and Terraform will fail the plan/apply. If false, any `stdout` and `stderr` output that was produced before timing out will be returned in their respective outputs, and the `exit_code` output will be `null`." 109 | type = bool 110 | default = true 111 | } 112 | locals { 113 | var_fail_on_timeout = var.fail_on_timeout != null ? var.fail_on_timeout : true 114 | } 115 | 116 | variable "unix_interpreter" { 117 | description = "The interpreter to use when running commands on a Unix-based system. This is primarily used for testing, and should usually be left to the default value." 118 | type = string 119 | default = "/bin/sh" 120 | } 121 | locals { 122 | var_unix_interpreter = var.unix_interpreter != null ? var.unix_interpreter : "/bin/sh" 123 | } 124 | 125 | variable "execution_id" { 126 | description = "A unique ID for the shell execution. USED FOR DEVELOPMENT ONLY and will default to a UUID." 127 | type = string 128 | default = null 129 | validation { 130 | // Ensure that if an execution ID is provided, it matches the regex 131 | condition = var.execution_id == null ? true : length(regexall("^[a-zA-Z0-9_. -]+$", trimspace(var.execution_id))) > 0 132 | error_message = "The `execution_id` input variable, if provided, must consist solely of letters, digits, hyphens, underscores, and spaces, and may not consist entirely of whitespace." 133 | } 134 | } 135 | locals { 136 | var_execution_id = var.execution_id 137 | } 138 | 139 | variable "suppress_console" { 140 | description = "Whether to suppress the Terraform console output (including plan content and shell execution status messages) for this module. If enabled, much of the content will be hidden by marking it as \"sensitive\"." 141 | type = bool 142 | default = true 143 | } 144 | locals { 145 | var_suppress_console = var.suppress_console != null ? var.suppress_console : false 146 | } 147 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | // 0.15.0 - 0.15.3 had a bug where it threw an error if an output 3 | // was marked as sensitive. 4 | required_version = ">= 0.13.1, !=0.15.0, !=0.15.1, !=0.15.2, !=0.15.3, !=1.3.0" 5 | required_providers { 6 | external = { 7 | source = "hashicorp/external" 8 | version = ">= 1.1.0" 9 | } 10 | } 11 | } 12 | --------------------------------------------------------------------------------