From b8a293aea79a1a0adec817ecf7cca6d21566ada6 Mon Sep 17 00:00:00 2001 From: Sylvain Hellegouarch Date: Wed, 29 Nov 2023 22:35:16 +0100 Subject: [PATCH] add var substitution support Signed-off-by: Sylvain Hellegouarch --- CHANGELOG.md | 6 ++++++ chaosreport/__init__.py | 35 +++++++++++++++++++++++++----- chaosreport/cli.py | 40 ++++++++++++++++++++++++++++++++++- chaosreport/template/index.md | 6 +++--- 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed1daac..1ffcd4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ [Unreleased]: https://github.com/chaostoolkit/chaostoolkit-reporting/compare/0.14.3...HEAD +### Added + +- Support to substitute values in arguments so we know how activities were + called. You can now pass `--var` and `--var-file` to generate the report + as you do when you run the experiment + ## [0.14.3][] - 2023-06-09 [0.14.3]: https://github.com/chaostoolkit/chaostoolkit-reporting/compare/0.14.2...0.14.3 diff --git a/chaosreport/__init__.py b/chaosreport/__init__.py index 2bdef97..23d203f 100644 --- a/chaosreport/__init__.py +++ b/chaosreport/__init__.py @@ -10,7 +10,7 @@ import tempfile from base64 import b64encode from datetime import datetime, timedelta -from typing import List +from typing import Any, Dict, List import cairosvg import dateparser @@ -20,8 +20,11 @@ import pygal import pypandoc import semver +from chaoslib import merge_vars, substitute from chaoslib.caching import cache_activities, lookup_activity -from chaoslib.types import Experiment, Journal, Run +from chaoslib.configuration import load_configuration +from chaoslib.secret import load_secrets +from chaoslib.types import Configuration, Experiment, Journal, Run, Secrets from jinja2 import Environment, PackageLoader from logzero import logger from natural import date @@ -244,7 +247,12 @@ def generate_report_header( return header -def generate_report(journal_path: str, export_format: str = "markdown") -> str: +def generate_report( + journal_path: str, + export_format: str = "markdown", + var: Dict[str, Any] = None, + var_file: List[str] = None, +) -> str: """ Generate a report document from a chaostoolkit journal. @@ -267,7 +275,14 @@ def generate_report(journal_path: str, export_format: str = "markdown") -> str: generate_chart_from_metric_probes(journal, export_format) add_contribution_model(journal, export_format) - template = get_report_template(journal["chaoslib-version"]) + config_vars, secret_vars = merge_vars(var, var_file) or (None, None) + config = load_configuration( + experiment.get("configuration", {}), config_vars + ) + secrets = load_secrets(experiment.get("secrets", {}), secret_vars) + template = get_report_template( + journal["chaoslib-version"], configuration=config, secrets=secrets + ) report = template.render(journal) return report @@ -333,7 +348,10 @@ def save_report( def get_report_template( - report_version: str, default_template: str = "index.md" + report_version: str, + default_template: str = "index.md", + configuration: Configuration = None, + secrets: Secrets = None, ): """ Retrieve and return the most appropriate template based on the @@ -346,6 +364,13 @@ def get_report_template( env.globals["pretty_duration"] = lambda d0, d1: date.delta( dateparser.parse(d0), dateparser.parse(d1), words=False )[0] + env.globals["substitute"] = ( + lambda args: { + k: substitute(v, configuration, secrets) for k, v in args.items() + } + if isinstance(args, dict) + else args + ) if not report_version: return env.get_template(default_template) diff --git a/chaosreport/cli.py b/chaosreport/cli.py index 52745f0..6317e45 100644 --- a/chaosreport/cli.py +++ b/chaosreport/cli.py @@ -1,24 +1,62 @@ # -*- coding: utf-8 -*- import os from glob import glob +from typing import Any, Dict, List import click +from chaoslib import convert_vars from chaosreport import generate_report, generate_report_header, save_report __all__ = ["report"] +def validate_vars( + ctx: click.Context, param: click.Option, value: List[str] +) -> Dict[str, Any]: + """ + Process all `--var key=value` and return a dictionary of them with the + value converted to the appropriate type. + """ + try: + return convert_vars(value) + except ValueError as x: + raise click.BadParameter(str(x)) + + @click.command() @click.option( "--export-format", default="markdown", help="Format to export the report to: html, markdown, pdf.", ) +@click.option( + "--var", + multiple=True, + callback=validate_vars, + help="Specify substitution values for configuration only. Can " + "be provided multiple times. The pattern must be " + "key=value or key:type=value. In that latter case, the " + "value will be casted as the specified type. Supported " + "types are: int, float, bytes. No type specified means " + "a utf-8 decoded string.", +) +@click.option( + "--var-file", + multiple=True, + type=click.Path(exists=True), + help="Specify files that contain configuration and secret " + "substitution values. Either as a json/yaml payload where " + "each key has a value mapping to a configuration entry. " + "Or a .env file defining environment variables. " + "Can be provided multiple times.", +) @click.argument("journal", type=click.Path(exists=True), nargs=-1) @click.argument("report", type=click.Path(exists=False), nargs=1) def report( export_format: str = "markdown", + var: Dict[str, Any] = None, + var_file: List[str] = None, journal: str = "journal.json", report: str = "report.md", ): @@ -33,6 +71,6 @@ def report( journal = glob(journal) for journal in journal: - reports.append(generate_report(journal, export_format)) + reports.append(generate_report(journal, export_format, var, var_file)) save_report(header, reports, report_path, export_format) click.echo("Report generated as '{f}'".format(f=report)) diff --git a/chaosreport/template/index.md b/chaosreport/template/index.md index 91f0533..68d03d8 100644 --- a/chaosreport/template/index.md +++ b/chaosreport/template/index.md @@ -117,7 +117,7 @@ The {{item.activity.type}} provider that was executed: | **Type** | {{item.activity.provider.type}} | | **Path** | {{item.activity.provider.path}} | | **Timeout** | {{item.activity.provider.get("timeout", "N/A")}} | -| **Arguments** | {{item.activity.provider.get("arguments", "N/A")}} | +| **Arguments** | {{substitute(item.activity.provider.get("arguments", "N/A"))}} | {% elif item.activity.provider.type == "http" %} | | | | --------------- | ---------------------------------------------------------- | @@ -125,14 +125,14 @@ The {{item.activity.type}} provider that was executed: | **URL** | {{item.activity.provider.url}} | | **Method** | {{item.activity.provider.get("method", "GET")}} | | **Timeout** | {{item.activity.provider.get("timeout", "N/A")}} | -| **Arguments** | {{item.activity.provider.get("arguments", "N/A")}} | +| **Arguments** | {{substitute(item.activity.provider.get("arguments", "N/A"))}} | {% else %} | | | | --------------- | ---------------------------------------------------------- | | **Type** | {{item.activity.provider.type}} | | **Module** | {{item.activity.provider.module}} | | **Function** | {{item.activity.provider.func}} | -| **Arguments** | {{item.activity.provider.get("arguments", "N/A")}} | +| **Arguments** | {{substitute(item.activity.provider.get("arguments", "N/A"))}} | {% endif %} {% if item.exception %}