Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ the right `-var-file` flags every time is tedious and error-prone. `tf` does it
pass through
- **plan, apply, etc.** auto-run `terraform init` with backend config, then inject
`-var-file` flags
- **force-unlock** auto-runs `terraform init` with backend config so the lock can be located,
but does not inject `-var-file` flags
- `tf plan` automatically saves a binary plan to `/tmp/<profile>.tfplan` (view with
`tf show /tmp/<profile>.tfplan`). Skipped if the user passes their own `-out` flag.
- `tf apply` automatically appends `-auto-approve`, since locally the plan has already been
Expand Down
27 changes: 16 additions & 11 deletions bin/tf
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,9 @@ SUBCOMMANDS_ACCEPTING_VAR_FILE = frozenset({
"plan", "apply", "destroy", "import", "refresh", "console",
})

SUBCOMMANDS_NEEDING_BACKEND = SUBCOMMANDS_ACCEPTING_VAR_FILE | {"init"}
SUBCOMMANDS_NEEDING_BACKEND = SUBCOMMANDS_ACCEPTING_VAR_FILE | {
"init", "force-unlock",
}

SUBCOMMANDS_NEEDING_DATA_DIR = frozenset({"show", "output", "state"})

Expand Down Expand Up @@ -252,15 +254,21 @@ class TfRunner:

self._run_init(backend)

tfvars = TfVarsFiles.find(context)
if self.subcommand in SUBCOMMANDS_ACCEPTING_VAR_FILE:
tfvars = TfVarsFiles.find(context)

if tfvars.is_using_deprecated_filenames():
print(
f"tf: using account-ID tfvars (deprecated). Rename to {context.profile}.tfvars.",
file=sys.stderr,
)
if tfvars.is_using_deprecated_filenames():
print(
f"tf: using account-ID tfvars (deprecated). Rename to {context.profile}.tfvars.",
file=sys.stderr,
)

command = self.build_command(tfvars)

command = self.build_command(tfvars)
if list(tfvars):
print(f"tf: var-files: {', '.join(tfvars)}", file=sys.stderr)
else:
command = ["terraform"] + self.args

if self.subcommand == "plan" and not self._has_out_flag():
out_path = f"/tmp/{context.profile}.tfplan"
Expand All @@ -271,9 +279,6 @@ class TfRunner:
command.append("-auto-approve")
print("tf: auto-approving apply", file=sys.stderr)

if list(tfvars):
print(f"tf: var-files: {', '.join(tfvars)}", file=sys.stderr)

os.execvp("terraform", command)

def _has_out_flag(self) -> bool:
Expand Down
100 changes: 100 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,103 @@ def test_apply_does_not_add_out_flag(self, monkeypatch):

command = mock_execvp.call_args[0][1]
assert not any(arg.startswith("-out=") for arg in command)


class TestTfRunnerForceUnlock:
def test_force_unlock_runs_init_before_command(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.subprocess.run") as mock_run, \
patch("tf.os.execvp"):
mock_run.return_value.returncode = 0
tf.TfRunner(["force-unlock", "abc123"]).call()

init_command = mock_run.call_args[0][0]
assert init_command[0] == "terraform"
assert init_command[1] == "init"
assert any(arg.startswith("-backend-config=bucket=") for arg in init_command)

def test_force_unlock_sets_tf_data_dir(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")
monkeypatch.delenv("TF_DATA_DIR", raising=False)

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.subprocess.run", return_value=type("R", (), {"returncode": 0})()), \
patch("tf.os.execvp"):
tf.TfRunner(["force-unlock", "abc123"]).call()

assert os.environ["TF_DATA_DIR"] == ".terraform.123456789012-eu-west-1"

def test_force_unlock_does_not_inject_var_files(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.TfVarsFiles.find",
return_value=tf.TfVarsFiles(["staging.tfvars"])), \
patch("tf.subprocess.run", return_value=type("R", (), {"returncode": 0})()), \
patch("tf.os.execvp") as mock_execvp:
tf.TfRunner(["force-unlock", "abc123"]).call()

command = mock_execvp.call_args[0][1]
assert "-var-file" not in command
assert "staging.tfvars" not in command

def test_force_unlock_does_not_search_for_tfvars(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.TfVarsFiles.find") as mock_find, \
patch("tf.subprocess.run", return_value=type("R", (), {"returncode": 0})()), \
patch("tf.os.execvp"):
tf.TfRunner(["force-unlock", "abc123"]).call()

mock_find.assert_not_called()

def test_force_unlock_does_not_add_auto_approve(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.subprocess.run", return_value=type("R", (), {"returncode": 0})()), \
patch("tf.os.execvp") as mock_execvp:
tf.TfRunner(["force-unlock", "abc123"]).call()

command = mock_execvp.call_args[0][1]
assert "-auto-approve" not in command

def test_force_unlock_does_not_add_out_flag(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.subprocess.run", return_value=type("R", (), {"returncode": 0})()), \
patch("tf.os.execvp") as mock_execvp:
tf.TfRunner(["force-unlock", "abc123"]).call()

command = mock_execvp.call_args[0][1]
assert not any(arg.startswith("-out=") for arg in command)

def test_force_unlock_passes_lock_id_through(self, monkeypatch):
monkeypatch.setenv("AWS_VAULT", "staging")
monkeypatch.setenv("AWS_DEFAULT_REGION", "eu-west-1")

with patch("tf.TfBackend._get_repo_name", return_value="my-repo"), \
patch("tf.TfVarsFiles._get_account_id", return_value="123456789012"), \
patch("tf.subprocess.run", return_value=type("R", (), {"returncode": 0})()), \
patch("tf.os.execvp") as mock_execvp:
tf.TfRunner(["force-unlock", "abc123"]).call()

mock_execvp.assert_called_once_with(
"terraform", ["terraform", "force-unlock", "abc123"]
)