Files
hunfabric/tools/tflint-fast.py
2025-10-27 15:42:37 +01:00

127 lines
4.3 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import click
import glob
import subprocess
import yaml
from pathlib import Path
import os
import shutil
import tempfile
BASEDIR = Path(__file__).parents[1]
# if we're copying the module, we might as well ignore files and
# directories that are automatically read by terraform. Useful
# to avoid surprises if, for example, you have an active fast
# deployment with links to configs)
ignore_patterns = shutil.ignore_patterns('*.auto.tfvars', '*.auto.tfvars.json',
'[0-9]-*-providers.tf',
'terraform.tfstate*',
'.terraform.lock.hcl',
'terraform.tfvars', '.terraform')
def tflint_module(module_path, var_path, extra_dirs, junit):
with tempfile.TemporaryDirectory(dir=module_path.parent) as tmp_path:
tmp_path = Path(tmp_path)
# Running tests in a copy made with symlinks=True makes them run
# ~20% slower than when run in a copy made with symlinks=False.
shutil.copytree(BASEDIR / module_path, tmp_path, dirs_exist_ok=True,
symlinks=False, ignore=ignore_patterns)
for extra_dir in extra_dirs:
os.symlink(extra_dir, tmp_path / extra_dir.name)
args = ['tflint']
if junit:
args += ['--format=junit']
args += [
'--chdir',
str(tmp_path.absolute()),
'--var-file',
str((BASEDIR / var_path).absolute()),
'--config',
str((BASEDIR / ".tflint.hcl").absolute()),
]
if junit:
with open(f'tflint-fast-{str(module_path).replace("/", "_")}.xml',
'w+') as output:
return subprocess.run(args, stderr=subprocess.STDOUT,
stdout=output).returncode
else:
return subprocess.run(args, stderr=subprocess.STDOUT).returncode
def is_affected(files, module_path, tftest_path, extra_dirs):
# no files provided, run all tftests
if not files:
return True
absolute_files = [Path(x).absolute() for x in files]
# check if the files modified the module
ret = any(x.is_relative_to(module_path.absolute()) for x in absolute_files)
if ret:
return ret
# check if the files modified the test definition
ret = any(x.is_relative_to(tftest_path.absolute()) for x in absolute_files)
if ret:
return ret
# check if the files modified extra dirs
for extra_dir in extra_dirs:
ret = any(x.is_relative_to(extra_dir.absolute()) for x in absolute_files)
if ret:
return ret
return False
@click.option('--junit', default=False, is_flag=True)
@click.argument('files', nargs=-1, type=click.Path(), required=False)
@click.command()
def main(junit, files):
ret = 0
for tftest_yaml in sorted(
glob.glob(f'{BASEDIR}/tests/fast/**/tftest.yaml', recursive=True)):
with open(tftest_yaml, 'r') as f:
tftest = yaml.safe_load(f)
module_path = Path(tftest['module'])
tftest_path = Path(tftest_yaml).parent
simple_test = tftest['tests'].get('simple', {})
if not simple_test:
simple_test = {}
relative_extra_dirs = simple_test.get('extra_dirs')
extra_dirs = [
(module_path.absolute() / Path(x)) for x in relative_extra_dirs
] if relative_extra_dirs else []
var_path = (tftest_path / 'simple.tfvars')
if not var_path.exists():
print(f'## {module_path}: skipping stage as no simple.tfvars found there')
continue
if not is_affected(files, module_path, tftest_path, extra_dirs):
print(
f'## {module_path}: skipping stage as it is not affected by provided files'
)
continue
click.echo(f'## {module_path}')
ret |= tflint_module(module_path, var_path, extra_dirs, junit)
# end for
exit(ret)
if __name__ == '__main__':
main()