Skip to content

Bootstrap without -e crashes with "environment: unbound variable" under set -u #50

Description

@Zordrak

Summary

In --bootstrap mode, when -e/--environment is omitted (as the README's
bootstrapping usage explicitly instructs), the environment variable is never
declared, so the first bare ${environment} reference aborts the script with
environment: unbound variable under set -u (enabled in v2.3.0).

Root Cause

set -uo pipefail is enabled at bin/terraform.sh:8.

In the bootstrap branch, environment is only declared readonly when
environment_arg is non-empty
:

bin/terraform.sh:316-321

if [ "${bootstrap}" == 'true' ]; then
  ...
  [ -n "${environment_arg}" ] && readonly environment="${environment_arg}";
else
  ...
  readonly environment="${environment_arg}";   # always declared in non-bootstrap
fi;

When bootstrapping without -e, environment_arg is empty, the && branch is
skipped, and environment is never created. Every subsequent bare
${environment} reference then violates set -u.

Notably, the v2.3.0 hardening commit (1932b57b) already anticipated this and
defensively guarded exactly one line:

bin/terraform.sh:357

[ -n "${environment:-}" ] && export TF_VAR_environment="${environment}";

…but every other reference was left unguarded, so the fix is incomplete. The
comment at bin/terraform.sh:584 even documents the
intent — "Environment is normally expected, but in bootstrapping it may not be
provided"
— yet the guard on the very next line uses a bare ${environment},
which is itself the crash.

Unguarded ${environment} references reachable in bootstrap mode

All of the following use bare ${environment} (not ${environment:-}) and are
reachable during a --bootstrap run, in execution order:

Line Context Reachability
437 Global pre.sh sourcing Only if a pre.sh exists
474 Component pre.sh sourcing Only if component pre.sh exists
487 S3 secrets aws s3 ls path Unconditional — first guaranteed crash
490-491 S3 secrets aws s3 cp path If secrets file present
520 remote_vars_path construction Unconditional
537 versions_file_name construction Unconditional
541 Env file [ -n "${environment}" ] guard Unconditional
585 "may not be provided" guard Unconditional

The first guaranteed abort is bin/terraform.sh:487
(the S3 secrets existence check runs regardless of any hook files). If a global
pre.sh exists, bin/terraform.sh:437 aborts even
earlier.

Evidence

Minimal reproduction of the exact mechanism (mirrors lines 316-321 then a later
bare reference):

$ bash -c 'set -uo pipefail
declare environment_arg=""
bootstrap="true"
[ -n "${environment_arg}" ] && readonly environment="${environment_arg}"
echo "reached line before use"
echo "env is: ${environment}"'
reached line before use
bash: line 1: environment: unbound variable
# exit code 127

Expected: bootstrapping succeeds without -e, because environment is
genuinely irrelevant to creating the state bucket (the bootstrap backend prefix
is hard-coded to .../bootstrap at
bin/terraform.sh:676, not .../${environment}).

Actual: script aborts with environment: unbound variable.

Reproduction Steps

Follow the README bootstrapping usage verbatim (no -e). Because of #49 you
must currently also pass a -- argument to get past line 284:

bin/terraform.sh \
  -p myproject \
  -b myproject-tfscaffold \
  -r eu-west-2 \
  --bootstrap \
  -a apply \
  -- -input=false

With valid AWS credentials this proceeds past argument parsing and account
lookup, then aborts at the first bare ${environment} reference
(line 487, or line 437 if a
global pre.sh exists) with line NNN: environment: unbound variable.

Suggested Fix

The documented behaviour is that environment is optional for bootstrapping, so
the intent is for an unset environment to be valid — not to error. Two viable
directions:

  1. Declare a safe default once (preferred, matches documented intent):
    after the bootstrap/non-bootstrap block, ensure environment always exists,
    e.g. declare it as empty in the bootstrap branch when environment_arg is
    empty. Then audit every bare ${environment} reference and make the ones
    that are merely presence checks tolerant of empty (the [ -n ... ] guards
    at 541 and 585 already do
    the right thing once the variable simply exists). The S3 path
    constructions (487, 520)
    would then build .../<account>/<region>// — which is harmless for the
    secrets/remote-vars lookups, but the fixer should confirm the trailing-slash
    behaviour is acceptable or skip those lookups entirely when bootstrapping.

  2. Use ${environment:-} consistently at every reference reachable in
    bootstrap mode (mirroring the existing guard at
    line 357).

Whichever approach is chosen, the fixer should also confirm the interaction
with the secrets and remote-tfvars S3 lookups during bootstrap — these arguably
should be skipped entirely when bootstrap == 'true', since a not-yet-created
bucket cannot contain them, which would sidestep several of the unguarded
references at their source.

Note: this must remain compatible with the readonly declarations — do not
declare environment twice.

Affected Platforms

All platforms (Linux, macOS, any bash with set -u). The bug is in
shell-level variable handling, not platform-specific tooling.

Related

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions