Files
hunfabric/tools/generate_plan_summary.py
Julio Castillo d8d66583f8 Bump GCP provider version to 7.33.0 (#4004)
* Bump provider version

* Fix inventories

* Ignore certificates in inventories

* Add header to cloud run recipe

* Optimize file copy for example-based tests

* Remove local references
2026-05-31 21:04:01 +00:00

255 lines
8.0 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright 2026 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.
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "click",
# "marko",
# "pytest>=7.2.1",
# "PyYAML>=6.0",
# "tftest>=1.8.1",
# ]
# ///
"""Generate plan summary for README examples or tftest.yaml tests.
This script unifies the functionality of generating inventory files from
either README code blocks or tftest.yaml test specifications.
"""
import collections
import datetime
import glob
import os
import re
import shutil
import sys
import tempfile
import click
import marko
import yaml
from pathlib import Path
BASEDIR = Path(__file__).parents[1]
sys.path.append(str(BASEDIR / 'tests'))
try:
import fixtures
from examples.utils import get_readme_examples, get_tftest_directive
except ImportError as e:
print(f"Error importing fixtures or utils: {e}")
sys.exit(1)
FILTERED_ATTRIBUTES = [
'filename',
'source_md5hash',
'pem_certificate',
]
HEADER = "".join(open(__file__).readlines()[2:15])
current_year = datetime.date.today().year
HEADER = re.sub(r"Copyright \d{4}", f"Copyright {current_year}", HEADER)
def output_summary(summary, inventory_path, save):
values = fixtures.filter_plan_values(summary.values, FILTERED_ATTRIBUTES)
outputs = {
k: v.get('value', '__missing__') for k, v in summary.outputs.items()
}
if save:
if not inventory_path:
print("Error: Cannot determine inventory path for saving.")
sys.exit(1)
inventory_path.parent.mkdir(parents=True, exist_ok=True)
with open(inventory_path, 'w') as f:
f.write(HEADER)
f.write('\n')
yaml.dump({'values': values}, f)
f.write('\n')
yaml.dump({'counts': summary.counts}, f)
f.write('\n')
yaml.dump({'outputs': outputs}, f)
print(f"Inventory saved to {inventory_path}")
else:
print(yaml.dump({'values': values}))
print(yaml.dump({'counts': summary.counts}))
print(yaml.dump({'outputs': outputs}))
def prepare_files(test_path, files, fixtures_dict, requested_files,
requested_fixtures):
if requested_files:
for f in requested_files.split(','):
if f in files:
destination = test_path / files[f].path
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(files[f].content)
if requested_fixtures:
for f in requested_fixtures.split(','):
if f.startswith('fixtures/'):
source = BASEDIR / 'tests' / f
destination = test_path / source.name
if not destination.exists():
destination.symlink_to(source)
elif f in fixtures_dict:
destination = test_path / f'{f}.tf'
destination.write_text(fixtures_dict[f])
def handle_readme(readme_path, target, index, save):
examples = get_readme_examples(readme_path, BASEDIR)
header = target
if not header:
headers = sorted(
set(exp_header
for exp, example_id, marks, exp_header, exp_index in examples
if exp_header))
if not headers:
print(f"No tests found in {readme_path}")
sys.exit(0)
print("Available headers with tests:")
for i, h in enumerate(headers, 1):
print(f" {i}. {h}")
choice = click.prompt("Select a header by number", type=int)
if 1 <= choice <= len(headers):
header = headers[choice - 1]
else:
print("Invalid selection")
sys.exit(1)
target_example = None
for exp, example_id, marks, exp_header, exp_index in examples:
if exp_header == header and exp_index == index:
target_example = exp
break
if not target_example:
print(f"Test not found for header '{header}' and index {index}")
sys.exit(1)
directive = target_example.directive
module_path = readme_path.parent
inventory_path = None
if save:
inventory_name = directive.kwargs.get('inventory')
if not inventory_name:
print("Error: No inventory file specified in the # tftest directive.")
print("Please add `inventory=filename.yaml` to the directive first.")
sys.exit(1)
module_str = str(target_example.module).replace('-', '_')
inventory_path = (BASEDIR / 'tests' / module_str / 'examples' /
inventory_name)
with tempfile.TemporaryDirectory(prefix='tftest-') as tmp_path:
tmp_path = Path(tmp_path)
if target_example.type == 'hcl':
(tmp_path / 'fabric').symlink_to(BASEDIR)
(tmp_path / 'variables.tf').symlink_to(BASEDIR / 'tests' / 'examples' /
'variables.tf')
(tmp_path / 'main.tf').write_text(target_example.code)
assets_path = module_path / 'assets'
if assets_path.exists():
(tmp_path / 'assets').symlink_to(assets_path.resolve())
prepare_files(tmp_path, target_example.files, target_example.fixtures,
directive.kwargs.get('files'),
directive.kwargs.get('fixtures'))
summary = fixtures.plan_summary(tmp_path, Path(), [])
elif target_example.type == 'tfvars':
(tmp_path / 'terraform.auto.tfvars').write_text(target_example.code)
shutil.copytree(module_path, tmp_path, dirs_exist_ok=True)
summary = fixtures.plan_summary(tmp_path, Path(),
[tmp_path / 'terraform.auto.tfvars'])
output_summary(summary, inventory_path, save)
def handle_tftest(test_file, target, save):
test_base_dir = Path(test_file).parent
with open(test_file) as f:
raw = yaml.safe_load(f)
module = raw.pop('module')
test_name = target
if not test_name:
tests = sorted(raw.get('tests', {}).keys())
if not tests:
print(f"No tests found in {test_file}")
sys.exit(0)
print("Available tests:")
for i, t in enumerate(tests, 1):
print(f" {i}. {t}")
choice = click.prompt("Select a test by number", type=int)
if 1 <= choice <= len(tests):
test_name = tests[choice - 1]
else:
print("Invalid selection")
sys.exit(1)
common = raw.pop('common_tfvars', [])
spec = raw.get('tests', {})[test_name] or {}
extra_dirs = spec.get('extra_dirs', [])
extra_files = spec.get('extra_files', [])
tf_var_files = common + [f'{test_name}.tfvars'] + spec.get('tfvars', [])
module_path = BASEDIR / module
summary = fixtures.plan_summary(module_path, test_base_dir, tf_var_files,
extra_files=extra_files,
extra_dirs=extra_dirs)
inventory_path = test_base_dir / f'{test_name}.yaml' if save else None
output_summary(summary, inventory_path, save)
@click.command()
@click.argument('file_path', type=click.Path(exists=True), nargs=1)
@click.argument('target', required=False)
@click.option('--index', default=1,
help='Index of the test under the header (README only)')
@click.option('--save', is_flag=True,
help='Automatically save inventory to the right location')
def main(file_path, target, index, save):
"""Generate plan summary for a README example or a tftest.yaml test.
FILE_PATH: Path to README.md or tftest.yaml.
TARGET: Header name (for README) or test name (for tftest.yaml).
"""
file_path = Path(file_path)
if file_path.suffix == '.md' or file_path.name == 'README.md':
handle_readme(file_path, target, index, save)
elif file_path.suffix in ('.yaml', '.yml') or file_path.name == 'tftest.yaml':
handle_tftest(file_path, target, save)
else:
print(f"Unsupported file type: {file_path.suffix}")
print("Please provide a README.md or a tftest.yaml file.")
sys.exit(1)
if __name__ == '__main__':
main()