diff --git a/.github/workflows/test-early-eval.yaml b/.github/workflows/test-early-eval.yaml new file mode 100644 index 00000000..cfe10274 --- /dev/null +++ b/.github/workflows/test-early-eval.yaml @@ -0,0 +1,72 @@ +name: Test OpenTofu early eval + +on: + - pull_request + +permissions: + contents: read + +jobs: + s3-backend: + runs-on: ubuntu-24.04 + name: Plan with early eval + permissions: + contents: read + pull-requests: write + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: tofu plan + uses: ./tofu-plan + id: plan + with: + path: tests/workflows/test-early-eval/s3 + variables: | + passphrase = "tofuqwertyuiopasdfgh" + + - name: Verify outputs + env: + JSON_PLAN_PATH: ${{ steps.plan.outputs.json_plan_path }} + run: | + if [[ ! -f "$JSON_PLAN_PATH" ]]; then + echo "::error:: json_plan_path not set correctly" + exit 1 + fi + + - name: tofu apply + uses: ./tofu-apply + with: + path: tests/workflows/test-early-eval/s3 + variables: | + passphrase = "tofuqwertyuiopasdfgh" + + - name: Create workspace + uses: ./tofu-new-workspace + with: + path: tests/workflows/test-early-eval/s3 + workspace: test-workspace + variables: | + passphrase = "tofuqwertyuiopasdfgh" + + - name: Create workspace again + uses: ./tofu-new-workspace + with: + path: tests/workflows/test-early-eval/s3 + workspace: test-workspace + variables: | + passphrase = "tofuqwertyuiopasdfgh" + + - name: Destroy workspace + uses: ./tofu-destroy-workspace + with: + path: tests/workflows/test-early-eval/s3 + workspace: test-workspace + variables: | + passphrase = "tofuqwertyuiopasdfgh" diff --git a/.github/workflows/test-version.yaml b/.github/workflows/test-version.yaml index 8acf716a..0ac87029 100644 --- a/.github/workflows/test-version.yaml +++ b/.github/workflows/test-version.yaml @@ -611,7 +611,7 @@ jobs: run: | echo "The terraform version was $DETECTED_TERRAFORM_VERSION" - if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.11"* ]]; then + if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.12"* ]]; then echo "::error:: Latest version was not used" exit 1 fi @@ -632,7 +632,7 @@ jobs: run: | echo "The terraform version was $DETECTED_TERRAFORM_VERSION" - if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.11"* ]]; then + if [[ "$DETECTED_TERRAFORM_VERSION" != *"1.12"* ]]; then echo "::error:: Latest version was not used" exit 1 fi diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9d2b1e23..38a307c8 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -119,13 +119,6 @@ jobs: docs/*.md **/README.md - - name: ensure-sha-pinned-actions - uses: zgosalvez/github-actions-ensure-sha-pinned-actions@25ed13d0628a1601b4b44048e63cc4328ed03633 # v3 - with: - allowlist: | - actions/ - dflook/ - - name: Lint Dockerfile uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 with: diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..81a51310 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,7 @@ +rules: + unpinned-uses: + config: + policies: + dflook/terraform-apply: ref-pin + dflook/terraform-plan: ref-pin + actions/*: ref-pin diff --git a/docs-gen/action.py b/docs-gen/action.py index 0dbf45aa..d7f8105b 100644 --- a/docs-gen/action.py +++ b/docs-gen/action.py @@ -45,6 +45,7 @@ class Input: deprecation_message: str = None show_in_docs: bool = True example: str = None + available_in: list[Type[Terraform] | Type[OpenTofu]] = dataclasses.field(default_factory=lambda: [Terraform, OpenTofu]) def markdown(self, tool: Tool) -> str: if self.deprecation_message is None: @@ -226,6 +227,8 @@ def markdown(self, tool: Tool) -> str: for input in self.inputs: if not input.show_in_docs: continue + if tool not in input.available_in: + continue s += text_chunk(input.markdown(tool)) if self.outputs: @@ -264,7 +267,7 @@ def action_yaml(self, tool: Tool) -> str: if self.inputs: s += 'inputs:\n' - for input in self.inputs: + for input in (input for input in self.inputs if tool in input.available_in): s += f' {input.name}:\n' description = input.meta_description or input.description diff --git a/docs-gen/actions/destroy_workspace.py b/docs-gen/actions/destroy_workspace.py index 35ef297f..48975825 100644 --- a/docs-gen/actions/destroy_workspace.py +++ b/docs-gen/actions/destroy_workspace.py @@ -114,4 +114,4 @@ workspace: ${{ github.head_ref }} ``` ''' -) \ No newline at end of file +) diff --git a/docs-gen/actions/fmt.py b/docs-gen/actions/fmt.py index 41739ccb..eadf3807 100644 --- a/docs-gen/actions/fmt.py +++ b/docs-gen/actions/fmt.py @@ -1,11 +1,13 @@ import dataclasses -from action import Action +from action import Action, OpenTofu from environment_variables.GITHUB_DOT_COM_TOKEN import GITHUB_DOT_COM_TOKEN from environment_variables.TERRAFORM_CLOUD_TOKENS import TERRAFORM_CLOUD_TOKENS from inputs.backend_config import backend_config from inputs.backend_config_file import backend_config_file from inputs.path import path +from inputs.var_file import var_file +from inputs.variables import variables from inputs.workspace import workspace fmt = Action( @@ -20,6 +22,11 @@ $ProductName workspace to inspect when discovering the $ProductName version to use, if the version is not otherwise specified. See [dflook/$ToolName-version](https://github.com/dflook/terraform-github-actions/tree/main/$ToolName-version#$ToolName-version-action) for details. '''), + dataclasses.replace(variables, available_in=[OpenTofu], description=''' + Variables to set when initializing $ProductName. This should be valid $ProductName syntax - like a [variable definition file]($VariableDefinitionUrl). + Variables set here override any given in `var_file`s. + '''), + dataclasses.replace(var_file, available_in=[OpenTofu]), dataclasses.replace(backend_config, description=''' List of $ProductName backend config values, one per line. This is used for discovering the $ProductName version to use, if the version is not otherwise specified. See [dflook/$ToolName-version](https://github.com/dflook/terraform-github-actions/tree/main/$ToolName-version#$ToolName-version-action) for details. @@ -70,4 +77,4 @@ branch: automated-$ToolName-fmt ``` ''' -) \ No newline at end of file +) diff --git a/docs-gen/actions/fmt_check.py b/docs-gen/actions/fmt_check.py index 447dc564..e2b5e34a 100644 --- a/docs-gen/actions/fmt_check.py +++ b/docs-gen/actions/fmt_check.py @@ -1,11 +1,13 @@ import dataclasses -from action import Action +from action import Action, OpenTofu from environment_variables.GITHUB_DOT_COM_TOKEN import GITHUB_DOT_COM_TOKEN from environment_variables.TERRAFORM_CLOUD_TOKENS import TERRAFORM_CLOUD_TOKENS from inputs.backend_config import backend_config from inputs.backend_config_file import backend_config_file from inputs.path import path +from inputs.var_file import var_file +from inputs.variables import variables from inputs.workspace import workspace from outputs.failure_reason import failure_reason @@ -24,6 +26,11 @@ $ProductName workspace to inspect when discovering the $ProductName version to use, if the version is not otherwise specified. See [dflook/$ToolName-version](https://github.com/dflook/terraform-github-actions/tree/main/$ToolName-version#$ToolName-version-action) for details. '''), + dataclasses.replace(variables, available_in=[OpenTofu], description=''' + Variables to set when initializing $ProductName. This should be valid $ProductName syntax - like a [variable definition file]($VariableDefinitionUrl). + Variables set here override any given in `var_file`s. + '''), + dataclasses.replace(var_file, available_in=[OpenTofu]), dataclasses.replace(backend_config, description=''' List of $ProductName backend config values, one per line. This is used for discovering the $ProductName version to use, if the version is not otherwise specified. See [dflook/$ToolName-version](https://github.com/dflook/terraform-github-actions/tree/main/$ToolName-version#$ToolName-version-action) for details. @@ -96,4 +103,4 @@ run: echo "formatting check failed" ``` ''' -) \ No newline at end of file +) diff --git a/docs-gen/actions/new_workspace.py b/docs-gen/actions/new_workspace.py index b917a82c..bffefe1f 100644 --- a/docs-gen/actions/new_workspace.py +++ b/docs-gen/actions/new_workspace.py @@ -1,6 +1,6 @@ import dataclasses -from action import Action +from action import Action, OpenTofu from environment_variables.GITHUB_DOT_COM_TOKEN import GITHUB_DOT_COM_TOKEN from environment_variables.TERRAFORM_CLOUD_TOKENS import TERRAFORM_CLOUD_TOKENS from environment_variables.TERRAFORM_HTTP_CREDENTIALS import TERRAFORM_HTTP_CREDENTIALS @@ -9,6 +9,8 @@ from inputs.backend_config import backend_config from inputs.backend_config_file import backend_config_file from inputs.path import path +from inputs.var_file import var_file +from inputs.variables import variables from inputs.workspace import workspace new_workspace = Action( @@ -19,6 +21,12 @@ inputs=[ path, dataclasses.replace(workspace, description='The name of the $ProductName workspace to create.', required=True, default=None), + dataclasses.replace(variables, available_in=[OpenTofu], description=''' + Variables to set when initializing $ProductName. This should be valid $ProductName syntax - like a [variable definition file]($VariableDefinitionUrl). + + Variables set here override any given in `var_file`s. + '''), + dataclasses.replace(var_file, available_in=[OpenTofu]), backend_config, backend_config_file, ], @@ -62,4 +70,4 @@ auto_approve: true ``` ''' -) \ No newline at end of file +) diff --git a/docs-gen/actions/output.py b/docs-gen/actions/output.py index bba5542f..3431db64 100644 --- a/docs-gen/actions/output.py +++ b/docs-gen/actions/output.py @@ -1,6 +1,6 @@ import dataclasses -from action import Action +from action import Action, OpenTofu from environment_variables.GITHUB_DOT_COM_TOKEN import GITHUB_DOT_COM_TOKEN from environment_variables.TERRAFORM_CLOUD_TOKENS import TERRAFORM_CLOUD_TOKENS from environment_variables.TERRAFORM_HTTP_CREDENTIALS import TERRAFORM_HTTP_CREDENTIALS @@ -9,6 +9,8 @@ from inputs.backend_config import backend_config from inputs.backend_config_file import backend_config_file from inputs.path import path +from inputs.var_file import var_file +from inputs.variables import variables from inputs.workspace import workspace from outputs.terraform_outputs import terraform_outputs @@ -20,8 +22,14 @@ inputs=[ path, dataclasses.replace(workspace, description='$ProductName workspace to get outputs from'), + dataclasses.replace(variables, available_in=[OpenTofu], description=''' + Variables to set when initializing $ProductName. This should be valid $ProductName syntax - like a [variable definition file]($VariableDefinitionUrl). + + Variables set here override any given in `var_file`s. + '''), + dataclasses.replace(var_file, available_in=[OpenTofu]), backend_config, - backend_config_file, + backend_config_file ], environment_variables=[ GITHUB_DOT_COM_TOKEN, @@ -106,4 +114,4 @@ The subnet-ids are subnet-053008016a2c1768c,subnet-07d4ce437c43eba2f,subnet-0a5f8c3a20023b8c0 ``` ''' -) \ No newline at end of file +) diff --git a/image/actions.sh b/image/actions.sh index b0e6f93d..f7bb4cb4 100644 --- a/image/actions.sh +++ b/image/actions.sh @@ -170,9 +170,13 @@ function relative_to() { function init() { start_group "Initializing $TOOL_PRODUCT_NAME" + set-init-args + rm -rf "$TF_DATA_DIR" - debug_log "$TOOL_COMMAND_NAME" init -input=false -backend=false - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false -backend=false) + # shellcheck disable=SC2086 + debug_log "$TOOL_COMMAND_NAME" init -input=false -backend=false $EARLY_VARIABLE_ARGS + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false -backend=false $EARLY_VARIABLE_ARGS) end_group } @@ -184,14 +188,20 @@ function init() { function init-test() { start_group "Initializing $TOOL_PRODUCT_NAME" + set-init-args + rm -rf "$TF_DATA_DIR" if [[ -n "$INPUT_TEST_DIRECTORY" ]]; then - debug_log "$TOOL_COMMAND_NAME" init -input=false -backend=false -test-directory "$INPUT_TEST_DIRECTORY" - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false -backend=false -test-directory "$INPUT_TEST_DIRECTORY") + # shellcheck disable=SC2086 + debug_log "$TOOL_COMMAND_NAME" init -input=false -backend=false $EARLY_VARIABLE_ARGS -test-directory "$INPUT_TEST_DIRECTORY" + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false -backend=false $EARLY_VARIABLE_ARGS -test-directory "$INPUT_TEST_DIRECTORY") else - debug_log "$TOOL_COMMAND_NAME" init -input=false -backend=false - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false -backend=false) + # shellcheck disable=SC2086 + debug_log "$TOOL_COMMAND_NAME" init -input=false -backend=false $EARLY_VARIABLE_ARGS + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false -backend=false $EARLY_VARIABLE_ARGS) fi end_group @@ -200,7 +210,7 @@ function init-test() { function set-init-args() { INIT_ARGS="" - if [[ -n "$INPUT_BACKEND_CONFIG_FILE" ]]; then + if [[ -n "${INPUT_BACKEND_CONFIG_FILE:-}" ]]; then for file in $(echo "$INPUT_BACKEND_CONFIG_FILE" | tr ',' '\n'); do if [[ ! -f "$file" ]]; then @@ -212,12 +222,20 @@ function set-init-args() { done fi - if [[ -n "$INPUT_BACKEND_CONFIG" ]]; then + if [[ -n "${INPUT_BACKEND_CONFIG:-}" ]]; then for config in $(echo "$INPUT_BACKEND_CONFIG" | tr ',' '\n'); do INIT_ARGS="$INIT_ARGS -backend-config=$config" done fi + if [[ -v OPENTOFU && $TERRAFORM_VER_MINOR -ge 8 ]]; then + debug_log "Preparing variables for early evaluation" + set-variable-args + EARLY_VARIABLE_ARGS=$VARIABLE_ARGS + else + EARLY_VARIABLE_ARGS="" + fi + export INIT_ARGS } @@ -232,12 +250,12 @@ function init-backend-workspace() { rm -rf "$TF_DATA_DIR" - # shellcheck disable=SC2016 - debug_log TF_WORKSPACE="$INPUT_WORKSPACE" "$TOOL_COMMAND_NAME" init -input=false '$INIT_ARGS' # don't expand INIT_ARGS + # shellcheck disable=SC2016,SC2086 + debug_log TF_WORKSPACE="$INPUT_WORKSPACE" "$TOOL_COMMAND_NAME" init -input=false '$INIT_ARGS' $EARLY_VARIABLE_ARGS # don't expand INIT_ARGS set +e # shellcheck disable=SC2086 - (cd "$INPUT_PATH" && TF_WORKSPACE=$INPUT_WORKSPACE $TOOL_COMMAND_NAME init -input=false $INIT_ARGS \ + (cd "$INPUT_PATH" && TF_WORKSPACE=$INPUT_WORKSPACE $TOOL_COMMAND_NAME init -input=false $INIT_ARGS $EARLY_VARIABLE_ARGS \ 2>"$STEP_TMP_DIR/terraform_init.stderr") local INIT_EXIT=$? @@ -273,11 +291,11 @@ function init-backend-default-workspace() { rm -rf "$TF_DATA_DIR" - # shellcheck disable=SC2016 - debug_log "$TOOL_COMMAND_NAME" init -input=false '$INIT_ARGS' # don't expand INIT_ARGS + # shellcheck disable=SC2016,SC2086 + debug_log "$TOOL_COMMAND_NAME" init -input=false '$INIT_ARGS' $EARLY_VARIABLE_ARGS # don't expand INIT_ARGS set +e # shellcheck disable=SC2086 - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false $INIT_ARGS \ + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME init -input=false $INIT_ARGS $EARLY_VARIABLE_ARGS \ 2>"$STEP_TMP_DIR/terraform_init.stderr") local INIT_EXIT=$? @@ -302,9 +320,12 @@ function init-backend-default-workspace() { function select-workspace() { local WORKSPACE_EXIT - debug_log "$TOOL_COMMAND_NAME" workspace select "$INPUT_WORKSPACE" + # shellcheck disable=SC2086 + debug_log "$TOOL_COMMAND_NAME" workspace select $EARLY_VARIABLE_ARGS "$INPUT_WORKSPACE" + set +e - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace select "$INPUT_WORKSPACE") >"$STEP_TMP_DIR/workspace_select" 2>&1 + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && "$TOOL_COMMAND_NAME" workspace select $EARLY_VARIABLE_ARGS "$INPUT_WORKSPACE") >"$STEP_TMP_DIR/workspace_select" 2>&1 WORKSPACE_EXIT=$? set -e @@ -371,7 +392,7 @@ function set-common-plan-args() { function set-variable-args() { VARIABLE_ARGS="" - if [[ -n "$INPUT_VAR_FILE" ]]; then + if [[ -n "${INPUT_VAR_FILE:-}" ]]; then for file in $(echo "$INPUT_VAR_FILE" | tr ',' '\n'); do if [[ ! -f "$file" ]]; then @@ -383,7 +404,7 @@ function set-variable-args() { done fi - if [[ -n "$INPUT_VARIABLES" ]]; then + if [[ -n "${INPUT_VARIABLES:-}" ]]; then echo "$INPUT_VARIABLES" >"$STEP_TMP_DIR/variables.tfvars" VARIABLE_ARGS="$VARIABLE_ARGS -var-file=$STEP_TMP_DIR/variables.tfvars" fi @@ -415,11 +436,7 @@ function set-plan-args() { export PLAN_ARGS } -function set-remote-plan-args() { - set-common-plan-args - VARIABLE_ARGS="" - DEPRECATED_VAR_ARGS="" - +function create-auto-tfvars() { local AUTO_TFVARS_COUNTER=0 if [[ -n "$INPUT_VAR_FILE" ]]; then @@ -430,16 +447,29 @@ function set-remote-plan-args() { fi if [[ -n "$INPUT_VARIABLES" ]]; then - echo "$INPUT_VARIABLES" >"$STEP_TMP_DIR/variables.tfvars" cp "$STEP_TMP_DIR/variables.tfvars" "$INPUT_PATH/zzzz-dflook-terraform-github-actions-$AUTO_TFVARS_COUNTER.auto.tfvars" fi +} + +function delete-auto-tfvars() { + debug_cmd find "$INPUT_PATH" -regex '.*/zzzz-dflook-terraform-github-actions-[0-9]+\.auto\.tfvars' -print -delete || true +} + +function set-remote-plan-args() { + set-common-plan-args + VARIABLE_ARGS="" + DEPRECATED_VAR_ARGS="" + + create-auto-tfvars export PLAN_ARGS } function output() { - debug_log "$TOOL_COMMAND_NAME" output -json - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME output -json | tee "$STEP_TMP_DIR/terraform_output.json" | convert_output) + # shellcheck disable=SC2086 + debug_log "$TOOL_COMMAND_NAME" output -json $EARLY_VARIABLE_ARGS + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME output -json $EARLY_VARIABLE_ARGS | tee "$STEP_TMP_DIR/terraform_output.json" | convert_output) } function random_string() { @@ -535,8 +565,10 @@ function destroy() { function force_unlock() { echo "Unlocking state with ID: $INPUT_LOCK_ID" - debug_log "$TOOL_COMMAND_NAME" force-unlock -force "$INPUT_LOCK_ID" - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME force-unlock -force "$INPUT_LOCK_ID") + # shellcheck disable=SC2086 + debug_log "$TOOL_COMMAND_NAME" force-unlock -force $EARLY_VARIABLE_ARGS "$INPUT_LOCK_ID" + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME force-unlock -force $EARLY_VARIABLE_ARGS "$INPUT_LOCK_ID") } # Every file written to disk should use one of these directories @@ -560,7 +592,7 @@ function fix_owners() { fi if [[ -d "$INPUT_PATH" ]]; then - debug_cmd find "$INPUT_PATH" -regex '.*/zzzz-dflook-terraform-github-actions-[0-9]+\.auto\.tfvars' -print -delete || true + delete-auto-tfvars fi if [[ -f "$HOME/.terraformrc" ]]; then diff --git a/image/entrypoints/apply.sh b/image/entrypoints/apply.sh index 2fa20664..6cd33f56 100755 --- a/image/entrypoints/apply.sh +++ b/image/entrypoints/apply.sh @@ -29,7 +29,7 @@ exec 3>&1 function apply() { local APPLY_EXIT - set +e + if [[ -n "$PLAN_OUT" ]]; then # With Terrraform >= 1.10 Ephemeral variables must be specified again in the apply command. @@ -41,6 +41,15 @@ function apply() { SAVED_PLAN_VARIABLES="$VARIABLE_ARGS" fi + # With OpenTofu >= 1.8.0 Early variable initialization any variables used by the encryption block + # must be available for the apply command, but you can not use the -var or -var-file arguments with a saved plan + # We have to put them in an auto tfvars file as a workaround. + + if [[ "$TOOL_PRODUCT_NAME" == "OpenTofu" && -n "$EARLY_VARIABLE_ARGS" ]]; then + create-auto-tfvars + fi + + set +e # shellcheck disable=SC2086 debug_log $TOOL_COMMAND_NAME apply -input=false -no-color -lock-timeout=300s $PARALLEL_ARG $SAVED_PLAN_VARIABLES $PLAN_OUT # shellcheck disable=SC2086 @@ -50,11 +59,17 @@ function apply() { | tee "$STEP_TMP_DIR/terraform_apply.stdout" APPLY_EXIT=${PIPESTATUS[0]} >&2 cat "$STEP_TMP_DIR/terraform_apply.stderr" + set -e + + if [[ "$TOOL_PRODUCT_NAME" == "OpenTofu" && -n "$EARLY_VARIABLE_ARGS" ]]; then + delete-auto-tfvars + fi else # There is no plan file to apply, since the remote backend can't produce them. # Instead we need to do an auto approved apply using the arguments we would normally use for the plan + set +e # shellcheck disable=SC2086,SC2016 debug_log $TOOL_COMMAND_NAME apply -input=false -no-color -auto-approve -lock-timeout=300s $PARALLEL_ARG $PLAN_ARGS "$(masked-deprecated-vars)" $VARIABLE_ARGS # shellcheck disable=SC2086 @@ -64,9 +79,9 @@ function apply() { | tee "$STEP_TMP_DIR/terraform_apply.stdout" APPLY_EXIT=${PIPESTATUS[0]} >&2 cat "$STEP_TMP_DIR/terraform_apply.stderr" + set -e fi - set -e if [[ "$TERRAFORM_BACKEND_TYPE" == "cloud" || "$TERRAFORM_BACKEND_TYPE" == "remote" ]]; then if remote-run-id "$STEP_TMP_DIR/terraform_apply.stdout" "$STEP_TMP_DIR/terraform_apply.stderr" >"$STEP_TMP_DIR/remote-run-id.stdout" 2>"$STEP_TMP_DIR/remote-run-id.stderr"; then @@ -146,7 +161,8 @@ else fi if [[ -n "$PLAN_OUT" ]]; then - if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json "$PLAN_OUT") >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then + # shellcheck disable=SC2086 + if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json $EARLY_VARIABLE_ARGS "$PLAN_OUT" ) >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json" else debug_file "$STEP_TMP_DIR/terraform_show.stderr" diff --git a/image/entrypoints/destroy-workspace.sh b/image/entrypoints/destroy-workspace.sh index 59d76bc7..bb800861 100755 --- a/image/entrypoints/destroy-workspace.sh +++ b/image/entrypoints/destroy-workspace.sh @@ -35,6 +35,8 @@ else # We can't delete an active workspace, so re-initialize with a 'default' workspace (which may not exist) init-backend-default-workspace - debug_log terraform workspace delete -no-color -lock-timeout=300s "$INPUT_WORKSPACE" - (cd "$INPUT_PATH" && terraform workspace delete -no-color -lock-timeout=300s "$INPUT_WORKSPACE") + # shellcheck disable=SC2086 + debug_log $TOOL_COMMAND_NAME workspace delete $EARLY_VARIABLE_ARGS -no-color -lock-timeout=300s "$INPUT_WORKSPACE" + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace delete $EARLY_VARIABLE_ARGS -no-color -lock-timeout=300s "$INPUT_WORKSPACE") fi diff --git a/image/entrypoints/new-workspace.sh b/image/entrypoints/new-workspace.sh index e3ee676e..6774036f 100755 --- a/image/entrypoints/new-workspace.sh +++ b/image/entrypoints/new-workspace.sh @@ -14,7 +14,8 @@ fi init-backend-default-workspace set +e -(cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace list -no-color) \ +# shellcheck disable=SC2086 +(cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace list $EARLY_VARIABLE_ARGS -no-color) \ 2>"$STEP_TMP_DIR/terraform_workspace_list.stderr" \ >"$STEP_TMP_DIR/terraform_workspace_list.stdout" @@ -32,12 +33,14 @@ fi if workspace_exists "$INPUT_WORKSPACE" <"$STEP_TMP_DIR/terraform_workspace_list.stdout"; then echo "Workspace appears to exist, selecting it" - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace select -no-color "$INPUT_WORKSPACE") + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace select $EARLY_VARIABLE_ARGS -no-color "$INPUT_WORKSPACE") else echo "Workspace does not appear to exist, attempting to create it" set +e - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace new -no-color -lock-timeout=300s "$INPUT_WORKSPACE") \ + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace new $EARLY_VARIABLE_ARGS -no-color -lock-timeout=300s "$INPUT_WORKSPACE") \ 2>"$STEP_TMP_DIR/terraform_workspace_new.stderr" \ >"$STEP_TMP_DIR/terraform_workspace_new.stdout" @@ -52,7 +55,8 @@ else if grep -Fq "already exists" "$STEP_TMP_DIR/terraform_workspace_new.stderr"; then echo "Workspace does exist, selecting it" - (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace select -no-color "$INPUT_WORKSPACE") + # shellcheck disable=SC2086 + (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace select $EARLY_VARIABLE_ARGS -no-color "$INPUT_WORKSPACE") else cat "$STEP_TMP_DIR/terraform_workspace_new.stderr" cat "$STEP_TMP_DIR/terraform_workspace_new.stdout" diff --git a/image/entrypoints/plan.sh b/image/entrypoints/plan.sh index 12c8897b..778244b1 100755 --- a/image/entrypoints/plan.sh +++ b/image/entrypoints/plan.sh @@ -84,7 +84,8 @@ if [[ -n "$PLAN_OUT" ]]; then cp "$PLAN_OUT" "$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.tfplan" set_output plan_path "$WORKSPACE_TMP_DIR/plan.tfplan" - if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json "$PLAN_OUT") >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then + # shellcheck disable=SC2086 + if (cd "$INPUT_PATH" && $TOOL_COMMAND_NAME show -json $EARLY_VARIABLE_ARGS "$PLAN_OUT") >"$GITHUB_WORKSPACE/$WORKSPACE_TMP_DIR/plan.json" 2>"$STEP_TMP_DIR/terraform_show.stderr"; then set_output json_plan_path "$WORKSPACE_TMP_DIR/plan.json" else debug_file "$STEP_TMP_DIR/terraform_show.stderr" diff --git a/image/entrypoints/test.sh b/image/entrypoints/test.sh index badd1c4f..05b24c02 100755 --- a/image/entrypoints/test.sh +++ b/image/entrypoints/test.sh @@ -34,7 +34,8 @@ function set-test-args() { function test() { - debug_log $TOOL_COMMAND_NAME test -no-color "$TEST_ARGS" "$VARIABLE_ARGS" + # shellcheck disable=SC2086 + debug_log $TOOL_COMMAND_NAME test -no-color $TEST_ARGS $VARIABLE_ARGS set +e # shellcheck disable=SC2086 diff --git a/image/entrypoints/validate.sh b/image/entrypoints/validate.sh index 218604f5..da679761 100755 --- a/image/entrypoints/validate.sh +++ b/image/entrypoints/validate.sh @@ -21,7 +21,8 @@ fi init || true -if ! (cd "$INPUT_PATH" && TF_WORKSPACE="$TF_WORKSPACE" $TOOL_COMMAND_NAME validate -json | convert_validate_report "$INPUT_PATH"); then +# shellcheck disable=SC2086 +if ! (cd "$INPUT_PATH" && TF_WORKSPACE="$TF_WORKSPACE" $TOOL_COMMAND_NAME validate -json $EARLY_VARIABLE_ARGS | convert_validate_report "$INPUT_PATH"); then (cd "$INPUT_PATH" && TF_WORKSPACE="$TF_WORKSPACE" $TOOL_COMMAND_NAME validate) else echo -e "\033[1;32mSuccess!\033[0m The configuration is valid" diff --git a/image/src/github_pr_comment/backend_fingerprint.py b/image/src/github_pr_comment/backend_fingerprint.py index f7b7d8a3..448e33e2 100644 --- a/image/src/github_pr_comment/backend_fingerprint.py +++ b/image/src/github_pr_comment/backend_fingerprint.py @@ -7,6 +7,10 @@ Combined with the backend type and workspace name, this should uniquely identify a remote state file. """ +import json +import os +from pathlib import Path + import canonicaljson from github_actions.debug import debug @@ -194,7 +198,40 @@ def fingerprint(backend_type: BackendType, backend_config: BackendConfig, env) - } fingerprint_inputs = backends.get(backend_type, lambda c, e: c)(backend_config, env) + fingerprint_inputs = initialised_backend_config(backend_type, fingerprint_inputs) debug(f'Backend fingerprint includes {fingerprint_inputs.keys()}') return canonicaljson.encode_canonical_json(fingerprint_inputs) + +def initialised_backend_config(backend_type: BackendType, config: dict[str, str]) -> dict[str, str]: + """ + Get backend config from an initialized data directory + """ + + statefile_path = Path(os.environ.get('TF_DATA_DIR')) / 'terraform.tfstate' + if not statefile_path.exists(): + debug(f'No state file found at {statefile_path}') + return config + + with open(statefile_path) as f: + statefile = json.load(f) + + backend = statefile.get('backend', {}) + if backend.get('type') != backend_type: + debug(f'Backend type {backend.get("type")} from statefile does not match {backend_type}') + return config + + if 'config' not in backend: + debug('No backend config found in statefile') + return config + + for k in config: + v = backend['config'].get(k) + if v is not None and v != config[k]: + # The backend config in the statefile is different from the one in the .tf file + # We should use the one in the statefile + debug(f'Backend config {k} is {v} (tfstate) instead of {config[k]} (tf file)') + config[k] = v + + return config diff --git a/image/src/terraform/module.py b/image/src/terraform/module.py index 00766159..d280300c 100644 --- a/image/src/terraform/module.py +++ b/image/src/terraform/module.py @@ -3,7 +3,7 @@ from __future__ import annotations import os -from typing import Any, cast, NewType, Optional, TYPE_CHECKING, TypedDict, List +from typing import Any, cast, NewType, Optional, TYPE_CHECKING, TypedDict, List, Iterable import terraform.hcl @@ -51,6 +51,26 @@ def merge(a: TerraformModule, b: TerraformModule) -> TerraformModule: return merged +def files_in_module(path: Path) -> Iterable[Path]: + + if os.environ.get('OPENTOFU') == 'true': + files = {} + + for filename in path.iterdir(): + stem = filename.stem + + if filename.suffix == '.tf' and stem not in files: + files[stem] = filename + elif filename.suffix == '.tofu': + files[stem] = filename + + yield from files.values() + + else: + for filename in path.iterdir(): + if filename.suffix == '.tf': + yield filename + def load_module(path: Path) -> TerraformModule: """ @@ -62,12 +82,10 @@ def load_module(path: Path) -> TerraformModule: module = cast(TerraformModule, {}) - for file in os.listdir(path): - if not file.endswith('.tf'): - continue + for file in files_in_module(path): try: - tf_file = cast(TerraformModule, terraform.hcl.load(os.path.join(path, file))) + tf_file = cast(TerraformModule, terraform.hcl.load(file)) module = merge(module, tf_file) except Exception as e: # ignore tf files that don't parse diff --git a/image/tools/convert_output.py b/image/tools/convert_output.py index 361a3f9f..bcfa67d3 100755 --- a/image/tools/convert_output.py +++ b/image/tools/convert_output.py @@ -40,11 +40,27 @@ def convert_to_github(outputs: Dict) -> Iterable[Union[Mask, Output]]: yield Output(name, str(value)) +def read_input(s: str) -> dict: + """ + If there is a problem connecting to terraform, the output contains junk lines we need to skip over + """ + + # Remove any lines that don't start with a { + # This is because terraform sometimes outputs junk lines + # before the JSON output + lines = s.splitlines() + while lines and not lines[0].startswith('{'): + lines.pop(0) + + jstr = '\n'.join(lines) + return json.loads(jstr) + + if __name__ == '__main__': input_string = sys.stdin.read() try: - outputs = json.loads(input_string) + outputs = read_input(input_string) if not isinstance(outputs, dict): raise Exception('Unable to parse outputs') except: diff --git a/tests/test_module.py b/tests/test_module.py index 06a9b636..f0154045 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -1,5 +1,7 @@ +from pathlib import Path + from terraform.hcl import loads -from terraform.module import get_sensitive_variables +from terraform.module import get_sensitive_variables, files_in_module def test_get_sensitive_variables(): @@ -26,3 +28,17 @@ def test_get_sensitive_variables(): ''') assert get_sensitive_variables(module) == ['secret', 'super_secret'] + +def test_load_terraform_module(): + assert set(s.name for s in files_in_module(Path('tests/tofu-module'))) == { + 'blah.tf', + 'hello.tf', + } + +def test_load_tofu_module(monkeypatch): + monkeypatch.setenv('OPENTOFU', 'true') + assert set(s.name for s in files_in_module(Path('tests/tofu-module'))) == { + 'blah.tf', + 'hello.tofu', + 'tofu-only.tofu' + } diff --git a/tests/test_output.py b/tests/test_output.py index 8c89dd10..cf395d0d 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -1,5 +1,5 @@ import json -from convert_output import convert_to_github, Mask, Output +from convert_output import convert_to_github, Mask, Output, read_input def test_string(): @@ -40,9 +40,11 @@ def test_number(): } } - expected_output = [Output(name='int', value='123'), - Mask(value='123'), - Output(name='sensitive_int', value='123')] + expected_output = [ + Output(name='int', value='123'), + Mask(value='123'), + Output(name='sensitive_int', value='123') + ] output = list(convert_to_github(input)) assert output == expected_output @@ -305,3 +307,44 @@ def test_compound(): output = list(convert_to_github(input)) assert output == expected_output + + +def test_read_input_with_junk_lines(): + input_string = ''' There was an error connecting to Terraform Cloud. Please do not exit +Terraform to prevent data loss! Trying to restore the connection... + +Still trying to restore the connection... (3s elapsed) +Still trying to restore the connection... (5s elapsed) +{ + "output1": {"type": "string", "value": "value1", "sensitive": false} +}''' + result = read_input(input_string) + assert result == { + "output1": {"type": "string", "value": "value1", "sensitive": False} + } + +def test_read_input_without_junk_lines(): + input_string = '''{ + "output1": {"type": "string", "value": "value1", "sensitive": false} +}''' + result = read_input(input_string) + assert result == { + "output1": {"type": "string", "value": "value1", "sensitive": False} + } + +def test_read_input_empty_string(): + input_string = '' + try: + read_input(input_string) + assert False, "Expected an exception" + except json.JSONDecodeError: + pass + +def test_read_input_invalid_json(): + input_string = '''{ + "output1": {"type": "string", "value": "value1", "sensitive": false''' + try: + read_input(input_string) + assert False, "Expected an exception" + except json.JSONDecodeError: + pass diff --git a/tests/tofu-module/blah.tf b/tests/tofu-module/blah.tf new file mode 100644 index 00000000..e69de29b diff --git a/tests/tofu-module/hello.tf b/tests/tofu-module/hello.tf new file mode 100644 index 00000000..e69de29b diff --git a/tests/tofu-module/hello.tofu b/tests/tofu-module/hello.tofu new file mode 100644 index 00000000..e69de29b diff --git a/tests/tofu-module/nope b/tests/tofu-module/nope new file mode 100644 index 00000000..e69de29b diff --git a/tests/tofu-module/tofu-only.tofu b/tests/tofu-module/tofu-only.tofu new file mode 100644 index 00000000..e69de29b diff --git a/tests/workflows/test-early-eval/s3/main.tf b/tests/workflows/test-early-eval/s3/main.tf new file mode 100644 index 00000000..b05cfa22 --- /dev/null +++ b/tests/workflows/test-early-eval/s3/main.tf @@ -0,0 +1,54 @@ +terraform { + backend "s3" { + bucket = var.state_bucket + key = "test-plan-early-eval" + region = "eu-west-2" + } +} + +provider "aws" { + region = "eu-west-2" +} + +variable "state_bucket" { + type = string +} + +variable "module_version" { + type = string + default = "0.25.0" +} + +variable "passphrase" { + type = string + sensitive = true +} + +module "label" { + source = "cloudposse/label/null" + version = var.module_version + + name = "hello" +} + +resource "random_string" "my_String" { + length = 10 +} + +terraform { + encryption { + key_provider "pbkdf2" "my_passphrase" { + passphrase = var.passphrase + } + + method "aes_gcm" "my_method" { + keys = key_provider.pbkdf2.my_passphrase + } + + state { + method = method.aes_gcm.my_method + } + } + + required_version = "1.9.0" +} diff --git a/tests/workflows/test-early-eval/s3/terraform.tfvars b/tests/workflows/test-early-eval/s3/terraform.tfvars new file mode 100644 index 00000000..7f3ce5bc --- /dev/null +++ b/tests/workflows/test-early-eval/s3/terraform.tfvars @@ -0,0 +1 @@ +state_bucket = "terraform-github-actions" \ No newline at end of file diff --git a/tofu-fmt-check/README.md b/tofu-fmt-check/README.md index f75fe34d..b1c59795 100644 --- a/tofu-fmt-check/README.md +++ b/tofu-fmt-check/README.md @@ -26,6 +26,39 @@ If any files are not correctly formatted a failing GitHub check will be added fo - Optional - Default: `default` +* `variables` + + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + Variables set here override any given in `var_file`s. + + ```yaml + with: + variables: | + image_id = "${{ secrets.AMI_ID }}" + availability_zone_names = [ + "us-east-1a", + "us-west-1c", + ] + ``` + + - Type: string + - Optional + +* `var_file` + + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + var_file: | + common.tfvars + prod.tfvars + ``` + + - Type: string + - Optional + * `backend_config` List of OpenTofu backend config values, one per line. This is used for discovering the OpenTofu version to use, if the version is not otherwise specified. diff --git a/tofu-fmt-check/action.yaml b/tofu-fmt-check/action.yaml index 1631ef8a..1fa24661 100644 --- a/tofu-fmt-check/action.yaml +++ b/tofu-fmt-check/action.yaml @@ -13,6 +13,16 @@ inputs: See [dflook/tofu-version](https://github.com/dflook/terraform-github-actions/tree/main/tofu-version#tofu-version-action) for details. required: false default: "default" + variables: + description: | + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + Variables set here override any given in `var_file`s. + required: false + var_file: + description: | + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false backend_config: description: | List of OpenTofu backend config values, one per line. This is used for discovering the OpenTofu version to use, if the version is not otherwise specified. diff --git a/tofu-fmt/README.md b/tofu-fmt/README.md index 75702cec..cca13211 100644 --- a/tofu-fmt/README.md +++ b/tofu-fmt/README.md @@ -23,6 +23,39 @@ This action uses the `tofu fmt -recursive` command to reformat files in a direct - Optional - Default: `default` +* `variables` + + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + Variables set here override any given in `var_file`s. + + ```yaml + with: + variables: | + image_id = "${{ secrets.AMI_ID }}" + availability_zone_names = [ + "us-east-1a", + "us-west-1c", + ] + ``` + + - Type: string + - Optional + +* `var_file` + + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + var_file: | + common.tfvars + prod.tfvars + ``` + + - Type: string + - Optional + * `backend_config` List of OpenTofu backend config values, one per line. This is used for discovering the OpenTofu version to use, if the version is not otherwise specified. diff --git a/tofu-fmt/action.yaml b/tofu-fmt/action.yaml index 74d53d58..7c9398b1 100644 --- a/tofu-fmt/action.yaml +++ b/tofu-fmt/action.yaml @@ -13,6 +13,16 @@ inputs: See [dflook/tofu-version](https://github.com/dflook/terraform-github-actions/tree/main/tofu-version#tofu-version-action) for details. required: false default: "default" + variables: + description: | + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + Variables set here override any given in `var_file`s. + required: false + var_file: + description: | + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false backend_config: description: | List of OpenTofu backend config values, one per line. This is used for discovering the OpenTofu version to use, if the version is not otherwise specified. diff --git a/tofu-new-workspace/README.md b/tofu-new-workspace/README.md index a872f7fb..6b6efee0 100644 --- a/tofu-new-workspace/README.md +++ b/tofu-new-workspace/README.md @@ -21,6 +21,40 @@ Creates a new OpenTofu workspace. If the workspace already exists, succeeds with - Type: string - Required +* `variables` + + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + + ```yaml + with: + variables: | + image_id = "${{ secrets.AMI_ID }}" + availability_zone_names = [ + "us-east-1a", + "us-west-1c", + ] + ``` + + - Type: string + - Optional + +* `var_file` + + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + var_file: | + common.tfvars + prod.tfvars + ``` + + - Type: string + - Optional + * `backend_config` List of OpenTofu backend config values, one per line. diff --git a/tofu-new-workspace/action.yaml b/tofu-new-workspace/action.yaml index 789cd3cd..28d68659 100644 --- a/tofu-new-workspace/action.yaml +++ b/tofu-new-workspace/action.yaml @@ -10,6 +10,17 @@ inputs: workspace: description: The name of the OpenTofu workspace to create. required: true + variables: + description: | + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + required: false + var_file: + description: | + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false backend_config: description: List of OpenTofu backend config values, one per line. required: false diff --git a/tofu-output/README.md b/tofu-output/README.md index 600245da..da12564e 100644 --- a/tofu-output/README.md +++ b/tofu-output/README.md @@ -22,6 +22,40 @@ Retrieve the root-level outputs from an OpenTofu configuration. - Optional - Default: `default` +* `variables` + + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + + ```yaml + with: + variables: | + image_id = "${{ secrets.AMI_ID }}" + availability_zone_names = [ + "us-east-1a", + "us-west-1c", + ] + ``` + + - Type: string + - Optional + +* `var_file` + + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + + ```yaml + with: + var_file: | + common.tfvars + prod.tfvars + ``` + + - Type: string + - Optional + * `backend_config` List of OpenTofu backend config values, one per line. diff --git a/tofu-output/action.yaml b/tofu-output/action.yaml index 445bca99..05f87420 100644 --- a/tofu-output/action.yaml +++ b/tofu-output/action.yaml @@ -11,6 +11,17 @@ inputs: description: OpenTofu workspace to get outputs from required: false default: "default" + variables: + description: | + Variables to set when initializing OpenTofu. This should be valid OpenTofu syntax - like a [variable definition file](https://opentofu.org/docs/language/values/variables/#variable-definitions-tfvars-files). + + Variables set here override any given in `var_file`s. + required: false + var_file: + description: | + List of tfvars files to use, one per line. + Paths should be relative to the GitHub Actions workspace + required: false backend_config: description: List of OpenTofu backend config values, one per line. required: false