def local_state_version(request): terraform_version = Version(request.param) terraform_path = get_executable(Version(request.param)) module_dir = Path(os.getcwd(), '.local_state_version', str(terraform_version)) os.makedirs(module_dir, exist_ok=True) with open(os.path.join(module_dir, 'main.tf'), 'w') as f: f.write(''' output "hello" { value = "hello" } ''') # Here we go result = subprocess.run([terraform_path, 'init'], env={'TF_INPUT': 'false'}, capture_output=True, cwd=module_dir) assert result.returncode == 0 result = subprocess.run([terraform_path, 'apply', '-auto-approve'], env={'TF_INPUT': 'false'}, capture_output=True, cwd=module_dir) assert result.returncode == 0 shutil.rmtree(os.path.join(module_dir, '.terraform'), ignore_errors=True) yield module_dir, terraform_version shutil.rmtree(module_dir, ignore_errors=True)
def state_version(request): terraform_version = Version(request.param) terraform_path = get_executable(terraform_version) module_dir = os.path.join(os.getcwd(), '.terraform-state', str(terraform_version)) os.makedirs(module_dir, exist_ok=True) with open(os.path.join(module_dir, 'main.tf'), 'w') as f: backend_tf = ''' terraform { backend "s3" { bucket = "terraform-github-actions" key = "test_remote_state_s3_''' + str(terraform_version) + '''" region = "eu-west-2" dynamodb_table = "terraform-github-actions" } } ''' f.write(backend_tf + ''' output "hello" { value = "hello" } ''') # Here we go result = subprocess.run([terraform_path, 'init'], env=os.environ | {'TF_INPUT': 'false'}, capture_output=True, cwd=module_dir) print(f'{result.args=}') print(f'{result.returncode=}') print(f'{result.stdout.decode()=}') print(f'{result.stderr.decode()=}') assert result.returncode == 0 result = subprocess.run( [terraform_path, 'apply'] + (['-auto-approve'] if terraform_version >= Version('0.10.0') else []), env=os.environ | {'TF_INPUT': 'false'}, capture_output=True, cwd=module_dir) print(f'{result.args=}') print(f'{result.returncode=}') print(f'{result.stdout.decode()=}') print(f'{result.stderr.decode()=}') assert result.returncode == 0 shutil.rmtree(os.path.join(module_dir, '.terraform'), ignore_errors=True) yield terraform_version, backend_tf shutil.rmtree(module_dir, ignore_errors=True)
def get_remote_workspace_version( inputs: InitInputs, module: TerraformModule, cli_config_path: Path, versions: Iterable[Version]) -> Optional[Version]: """Get the terraform version set in a terraform cloud/enterprise workspace.""" backend_config = get_remote_backend_config( module, backend_config_files=inputs.get('INPUT_BACKEND_CONFIG_FILE', ''), backend_config_vars=inputs.get('INPUT_BACKEND_CONFIG', ''), cli_config_path=cli_config_path) if backend_config is None: backend_config = get_cloud_config(module, cli_config_path=cli_config_path) if backend_config is None: return None if workspace_info := get_workspace(backend_config, inputs['INPUT_WORKSPACE']): version = str(workspace_info['attributes']['terraform-version']) if version == 'latest': return latest_non_prerelease_version(versions) else: return Version(version)
def parse_tfenv(terraform_version_file: str, versions: Iterable[Version]) -> Version: """ Return the version specified in the terraform version file :param terraform_version_file: The contents of a tfenv .terraform-version file. :param versions: The available terraform versions :return: The terraform version specified by the file """ version = terraform_version_file.strip() if version == 'latest': return latest_non_prerelease_version(v for v in versions if not v.pre_release) if version.startswith('latest:'): version_regex = version.split(':', maxsplit=1)[1] matched = [v for v in versions if re.search(version_regex, str(v))] if not matched: raise Exception( f'No terraform versions match regex {version_regex}') return latest_version(matched) return Version(version)
def parse_asdf(tool_versions: str, versions: Iterable[Version]) -> Version: """Return the version specified in an asdf .tool-versions file.""" for line in tool_versions.splitlines(): if match := re.match(r'^\s*terraform\s+([^\s#]+)', line.strip()): if match.group(1) == 'latest': return latest_non_prerelease_version(v for v in versions if not v.pre_release) return Version(match.group(1))
def parse_tfswitch(tfswitch: str) -> Version: """ Return the terraform version specified by a tfswitch file :param tfswitch: The contents of a .tfswitch file :return: The terraform version specified by the file """ return Version(tfswitch.strip())
def test_parse_asdf(): versions = [ Version('0.13.6'), Version('1.1.8'), Version('1.1.9'), Version('1.1.7'), Version('1.1.0-alpha20210811'), Version('1.2.0-alpha20225555') ] assert parse_asdf('terraform 0.13.6', versions) == Version('0.13.6') assert parse_asdf(''' # comment terraform 0.15.6 #basdasd ''', versions) == Version('0.15.6') assert parse_asdf('terraform 1.1.1-cool', versions) == Version('1.1.1-cool') try: parse_asdf('', versions) except Exception: pass else: assert False try: parse_asdf('blahblah', versions) except Exception: pass else: assert False try: parse_asdf('terraform blasdasf', versions) except Exception: pass else: assert False assert parse_asdf('terraform latest', versions) == Version('1.1.9')
def main() -> None: """Entrypoint for terraform-version.""" if len(sys.argv) > 1: switch(Version(sys.argv[1])) else: version = determine_version(cast(InitInputs, os.environ), Path('~/.terraformrc'), cast(ActionsEnv, os.environ), cast(GithubEnv, os.environ)) switch(version)
def test_version(): v0_1_1 = Version('0.1.1') assert v0_1_1.major == 0 and v0_1_1.minor == 1 and v0_1_1.patch == 1 and v0_1_1.pre_release == '' assert str(v0_1_1) == '0.1.1' v1_0_11 = Version('1.0.11') assert v1_0_11.major == 1 and v1_0_11.minor == 0 and v1_0_11.patch == 11 and v1_0_11.pre_release == '' assert str(v1_0_11) == '1.0.11' v0_15_0_rc2 = Version('0.15.0-rc2') assert v0_15_0_rc2.major == 0 and v0_15_0_rc2.minor == 15 and v0_15_0_rc2.patch == 0 and v0_15_0_rc2.pre_release == 'rc2' assert str(v0_15_0_rc2) == '0.15.0-rc2' v0_15_0 = Version('0.15.0') assert v0_15_0.major == 0 and v0_15_0.minor == 15 and v0_15_0.patch == 0 and v0_15_0.pre_release == '' assert str(v0_15_0) == '0.15.0' assert v0_1_1 == v0_1_1 assert v1_0_11 != v0_1_1 assert v0_15_0_rc2 < v0_15_0 assert v1_0_11 > v0_15_0 > v0_1_1
def test_parse_tfswitch(): assert parse_tfswitch('0.13.6') == Version('0.13.6') assert parse_tfswitch(''' 0.15.6 ''') == Version('0.15.6') assert parse_tfswitch('1.1.1-cool') == Version('1.1.1-cool') try: parse_tfswitch('') except ValueError: pass else: assert False try: parse_tfswitch('blahblah') except ValueError: pass else: assert False
def test_state(state_version): terraform_version, backend_tf = state_version module = hcl2.loads(backend_tf) assert try_guess_state_version( { 'INPUT_BACKEND_CONFIG': '', 'INPUT_BACKEND_CONFIG_FILE': '', 'INPUT_WORKSPACE': 'default' }, module, versions=apply_constraints( sorted(Version(v) for v in terraform_versions), get_backend_constraints(module, {}))) == terraform_version
def read_local_state(module_dir: Path) -> Optional[Version]: """Return the terraform version that wrote a local terraform.tfstate file.""" state_path = os.path.join(module_dir, 'terraform.tfstate') if not os.path.isfile(state_path): return None try: with open(state_path) as f: state = json.load(f) if state.get('serial') > 0: return Version(state.get('terraform_version')) except Exception as e: debug(str(e)) return None
def try_init(terraform: Version, init_args: list[str], workspace: str, backend_tf: str) -> Optional[Union[Version, Constraint]]: """ Try and initialize the specified backend using the specified terraform version. Returns the information discovered from doing the init. This could be: - Version: the version of terraform used to write the state - Constraint: a constraint to apply to the available versions, that further narrows down to the version used to write the state - None: There is no remote state """ terraform_path = get_executable(terraform) module_dir = tempfile.mkdtemp() with open(os.path.join(module_dir, 'terraform.tf'), 'w') as f: f.write(backend_tf) # Here we go result = subprocess.run([str(terraform_path), 'init'] + init_args, env=os.environ | { 'TF_INPUT': 'false', 'TF_WORKSPACE': workspace }, capture_output=True, cwd=module_dir) debug(f'{result.args[:2]=}') debug(f'{result.returncode=}') debug(result.stdout.decode()) debug(result.stderr.decode()) if result.returncode != 0: if match := re.search( rb'state snapshot was created by Terraform v(.*),', result.stderr): return Version(match.group(1).decode()) elif b'does not support state version 4' in result.stderr: return Constraint('>=0.12.0') elif b'Failed to select workspace' in result.stderr: return None else: debug(str(result.stderr)) return None
def test_parse_tfenv(): versions = [ Version('0.13.6'), Version('1.1.8'), Version('1.1.9'), Version('1.1.7'), Version('1.1.0-alpha20210811'), Version('1.2.0-alpha20225555') ] assert parse_tfenv('0.13.6', versions) == Version('0.13.6') assert parse_tfenv(''' 0.15.6 ''', versions) == Version('0.15.6') assert parse_tfenv('1.1.1-cool', versions) == Version('1.1.1-cool') try: parse_tfenv('', versions) except ValueError: pass else: assert False try: parse_tfenv('blahblah', versions) except ValueError: pass else: assert False assert parse_tfenv('latest', versions) == Version('1.1.9') assert parse_tfenv('latest:^1.1', versions) >= Version('1.1.8') assert parse_tfenv('latest:1.8', versions) >= Version('1.1.0-alpha20210811') try: parse_tfenv('latest:^1.8', versions) except Exception: pass else: assert False
def test_latest(): versions = [ Version('0.13.6-alpha-23'), Version('0.13.6'), Version('1.1.8'), Version('1.1.9'), Version('1.1.7'), Version('1.1.0-alpha20210811'), Version('1.2.0-alpha20225555') ] assert earliest_version(versions) == Version('0.13.6-alpha-23') assert latest_version(versions) == Version('1.2.0-alpha20225555') assert earliest_non_prerelease_version(versions) == Version('0.13.6') assert latest_non_prerelease_version(versions) == Version('1.1.9')
if result.returncode != 0: if b'does not support state version 4' in result.stderr: return Constraint('>=0.12.0') raise Exception(result.stderr) try: state = json.loads(result.stdout.decode()) if state['version'] == 4 and state['serial'] == 0 and not state.get( 'outputs', {}): return None # This workspace has no state if b'no state' in result.stderr: return None if terraform < Version('0.12.0'): # terraform_version is reported correctly in state output return Version(state['terraform_version']) # terraform_version is made up except Exception as e: debug(str(e)) # There is some state return terraform def guess_state_version(inputs: InitInputs, module: TerraformModule, versions: Iterable[Version]) -> Optional[Version]: """Try and guess the terraform version that wrote the remote state file of the specified module."""
def test_constraint(): constraint = Constraint('0.12.4-hello') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == 'hello' and constraint.operator == '=' assert str(constraint) == '=0.12.4-hello' assert constraint.is_allowed(Version('0.12.4-hello')) assert not constraint.is_allowed(Version('0.12.4')) constraint = Constraint('0.12.4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '=' assert str(constraint) == '=0.12.4' constraint = Constraint(' = 0 .1 2. 4-hello') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == 'hello' and constraint.operator == '=' assert str(constraint) == '=0.12.4-hello' constraint = Constraint(' = 0 .1 2. 4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '=' assert str(constraint) == '=0.12.4' constraint = Constraint(' >= 0 .1 2. 4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '>=' assert str(constraint) == '>=0.12.4' assert constraint.is_allowed(Version('0.12.4')) assert constraint.is_allowed(Version('0.12.8')) assert constraint.is_allowed(Version('0.13.0')) assert constraint.is_allowed(Version('1.1.1')) assert not constraint.is_allowed(Version('0.12.3')) assert not constraint.is_allowed(Version('0.10.0')) constraint = Constraint(' > 0 .1 2. 4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '>' assert str(constraint) == '>0.12.4' assert not constraint.is_allowed(Version('0.12.4')) assert constraint.is_allowed(Version('0.12.8')) assert constraint.is_allowed(Version('0.13.0')) assert constraint.is_allowed(Version('1.1.1')) assert not constraint.is_allowed(Version('0.12.3')) assert not constraint.is_allowed(Version('0.10.0')) constraint = Constraint(' < 0 .1 2. 4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '<' assert str(constraint) == '<0.12.4' assert not constraint.is_allowed(Version('0.12.4')) assert not constraint.is_allowed(Version('0.12.8')) assert not constraint.is_allowed(Version('0.13.0')) assert not constraint.is_allowed(Version('1.1.1')) assert constraint.is_allowed(Version('0.12.3')) assert constraint.is_allowed(Version('0.10.0')) constraint = Constraint(' <= 0 .1 2. 4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '<=' assert str(constraint) == '<=0.12.4' assert constraint.is_allowed(Version('0.12.4')) assert not constraint.is_allowed(Version('0.12.8')) assert not constraint.is_allowed(Version('0.13.0')) assert not constraint.is_allowed(Version('1.1.1')) assert constraint.is_allowed(Version('0.12.3')) assert constraint.is_allowed(Version('0.10.0')) constraint = Constraint(' != 0 .1 2. 4') assert constraint.major == 0 and constraint.minor == 12 and constraint.patch == 4 and constraint.pre_release == '' and constraint.operator == '!=' assert str(constraint) == '!=0.12.4' assert not constraint.is_allowed(Version('0.12.4')) assert constraint.is_allowed(Version('0.12.8')) assert constraint.is_allowed(Version('0.13.0')) assert constraint.is_allowed(Version('1.1.1')) assert constraint.is_allowed(Version('0.12.3')) assert constraint.is_allowed(Version('0.10.0')) constraint = Constraint('1') assert (constraint.major == 1 and constraint.minor is None and constraint.patch is None and constraint.pre_release == '' and constraint.operator == '=') assert str(constraint) == '=1' assert constraint.is_allowed(Version('1.0.0')) assert not constraint.is_allowed(Version('1.0.1')) assert not constraint.is_allowed(Version('0.0.9')) assert not constraint.is_allowed(Version('1.0.0-wooo')) constraint = Constraint('1.2') assert (constraint.major == 1 and constraint.minor == 2 and constraint.patch is None and constraint.pre_release == '' and constraint.operator == '=') assert str(constraint) == '=1.2' constraint = Constraint('~>1.2.3') assert constraint.major == 1 and constraint.minor == 2 and constraint.patch == 3 and constraint.pre_release == '' and constraint.operator == '~>' assert str(constraint) == '~>1.2.3' constraint = Constraint('~>1.2') assert (constraint.major == 1 and constraint.minor == 2 and constraint.patch is None and constraint.pre_release == '' and constraint.operator == '~>') assert str(constraint) == '~>1.2' assert Constraint('0.12.0') < Constraint('0.12.1') assert Constraint('0.12.0') == Constraint('0.12.0') assert Constraint('0.12.0') != Constraint('0.15.0') test_ordering = [ Constraint('0.11.0'), Constraint('<0.12.0'), Constraint('<=0.12.0'), Constraint('0.12.0'), Constraint('~>0.12.0'), Constraint('>=0.12.0'), Constraint('>0.12.0'), Constraint('0.12.5'), Constraint('0.13.0'), ] assert test_ordering == sorted(test_ordering)