Example #1
0
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)
Example #4
0
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)
Example #5
0
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)
Example #9
0
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
Example #10
0
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
Example #12
0
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
Example #15
0
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."""
Example #17
0
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)