Skip to content

Add support for the early evaluation feature in OpenTofu #361

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/test-early-eval.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Test OpenTofu early eval

on:
- pull_request

permissions:
contents: read

jobs:
s3-backend:
runs-on: ubuntu-24.04
name: Plan with early eval
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: tofu plan
uses: ./tofu-plan
with:
path: tests/workflows/test-early-eval/s3
add_github_comment: false
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"

remote-backend:
runs-on: ubuntu-24.04
name: Remote plan with early eval
env:
TERRAFORM_CLOUD_TOKENS: dflook.scalr.io=${{ secrets.SCALR_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Create workspace
uses: ./tofu-new-workspace
with:
path: tests/workflows/test-early-eval/scalr
workspace: ${{ github.head_ref }}-early-eval
variables: |
passphrase = "tofuqwertyuiopasdfgh"

- name: Create workspace again
uses: ./tofu-new-workspace
with:
path: tests/workflows/test-early-eval/scalr
workspace: ${{ github.head_ref }}-early-eval
variables: |
passphrase = "tofuqwertyuiopasdfgh"

- name: Destroy workspace
uses: ./tofu-destroy-workspace
with:
path: tests/workflows/test-early-eval/scalr
workspace: ${{ github.head_ref }}-early-eval
variables: |
passphrase = "tofuqwertyuiopasdfgh"
5 changes: 4 additions & 1 deletion docs-gen/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs-gen/actions/destroy_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,4 @@
workspace: ${{ github.head_ref }}
```
'''
)
)
12 changes: 10 additions & 2 deletions docs-gen/actions/new_workspace.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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(
Expand All @@ -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, 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.
''', available_in=[OpenTofu]),
dataclasses.replace(var_file, available_in=[OpenTofu]),
backend_config,
backend_config_file,
],
Expand Down Expand Up @@ -62,4 +70,4 @@
auto_approve: true
```
'''
)
)
15 changes: 13 additions & 2 deletions image/actions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,14 @@ function set-init-args() {
done
fi

if [[ -v OPENTOFU && $TERRAFORM_VER_MINOR -ge 8 ]]; then
debug_log "Preparing variables for early evaluation"
set-variable-args
INIT_ARGS="$INIT_ARGS $VARIABLE_ARGS"
else
VARIABLE_ARGS=""
fi

export INIT_ARGS
}

Expand Down Expand Up @@ -302,9 +310,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 $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 $VARIABLE_ARGS "$INPUT_WORKSPACE") >"$STEP_TMP_DIR/workspace_select" 2>&1
WORKSPACE_EXIT=$?
set -e

Expand Down
6 changes: 4 additions & 2 deletions image/entrypoints/destroy-workspace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 $VARIABLE_ARGS -no-color -lock-timeout=300s "$INPUT_WORKSPACE"
# shellcheck disable=SC2086
(cd "$INPUT_PATH" && $TOOL_COMMAND_NAME workspace delete $VARIABLE_ARGS -no-color -lock-timeout=300s "$INPUT_WORKSPACE")
fi
12 changes: 8 additions & 4 deletions image/entrypoints/new-workspace.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 $VARIABLE_ARGS -no-color) \
2>"$STEP_TMP_DIR/terraform_workspace_list.stderr" \
>"$STEP_TMP_DIR/terraform_workspace_list.stdout"

Expand All @@ -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 $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 $VARIABLE_ARGS -no-color -lock-timeout=300s "$INPUT_WORKSPACE") \
2>"$STEP_TMP_DIR/terraform_workspace_new.stderr" \
>"$STEP_TMP_DIR/terraform_workspace_new.stdout"

Expand All @@ -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 $VARIABLE_ARGS -no-color "$INPUT_WORKSPACE")
else
cat "$STEP_TMP_DIR/terraform_workspace_new.stderr"
cat "$STEP_TMP_DIR/terraform_workspace_new.stdout"
Expand Down
3 changes: 2 additions & 1 deletion image/entrypoints/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 17 additions & 1 deletion image/tools/convert_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
51 changes: 47 additions & 4 deletions tests/test_output.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
48 changes: 48 additions & 0 deletions tests/workflows/test-early-eval/s3/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 "acm_certificate_version" {
type = string
default = "4.3.0"
}

variable "passphrase" {
type = string
sensitive = true
}

module "s3-bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = var.acm_certificate_version
}

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.8.8"
}
1 change: 1 addition & 0 deletions tests/workflows/test-early-eval/s3/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
state_bucket = "terraform-github-actions"
Loading
Loading