diff --git a/.woodpecker/helm-diff.yaml b/.woodpecker/helm-diff.yaml new file mode 100644 index 00000000..122486b7 --- /dev/null +++ b/.woodpecker/helm-diff.yaml @@ -0,0 +1,20 @@ +when: + branch: ${CI_REPO_DEFAULT_BRANCH} + +matrix: + STACK: + - system + - platform + - apps + +steps: + # TODO DRY with nix develop and custom entrypoint https://github.com/woodpecker-ci/woodpecker/pull/2985, + # but first we need a Nix cache. See the nix-cache branch for the WIP. + diff: + image: nixery.dev/shell/git/python3/kubernetes-helm/diffutils/dyff # TODO replace with nix develop + commands: + - ./scripts/helm-diff --repository "${CI_REPO_CLONE_URL}" --source "${CI_COMMIT_SOURCE_BRANCH}" --target "${CI_COMMIT_TARGET_BRANCH}" --subpath "${STACK}" + when: + - event: pull_request + path: '${STACK}/**' + depends_on: [] diff --git a/flake.nix b/flake.nix index ebcbc4c8..8fc06045 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ diffutils docker docker-compose_1 # TODO upgrade to version 2 + dyff git go gotestsum diff --git a/scripts/helm-diff b/scripts/helm-diff new file mode 100755 index 00000000..107cf2fc --- /dev/null +++ b/scripts/helm-diff @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +from argparse import ArgumentParser +from glob import glob +from os import path +from subprocess import run +from tempfile import mkdtemp, NamedTemporaryFile + + +def clone_repository(repo, branch, target_path): + run( + ['git', 'clone', repo, '--depth', '1', '--branch', branch, target_path], + check=True + ) + + +def render_helm_chart(chart_path, namespace, release_name, rendered_path): + # Even if there is no Helm chart at the specified chart path, do not raise an error. + # This accommodates cases where the entire chart is removed, or a new chart is added. + # In such cases, the rendered file will simply be empty. + if path.isdir(chart_path): + run( + ['helm', 'dependency', 'update', chart_path], + check=True + ) + + run( + ['helm', 'template', '--namespace', namespace, release_name, chart_path], + stdout=open(rendered_path, 'w'), + check=True + ) + + +def changed_charts(source_path, target_path, subpath): + changed_charts = [] + + # Convert to set for deduplication + all_charts = set( + glob(f"*", root_dir=f"{source_path}/{subpath}") + + glob(f"*", root_dir=f"{target_path}/{subpath}") + ) + + for chart in all_charts: + source_chart_path = path.join(source_path, subpath, chart) + target_chart_path = path.join(target_path, subpath, chart) + + if run(['diff', source_chart_path, target_chart_path], capture_output=True).returncode != 0: + changed_charts.append(chart) + + return changed_charts + + +def main(): + parser = ArgumentParser(description='Compare Helm charts in a directory between two Git revisions.') + parser.add_argument('--repository', required=True, help='Repository to clone') + parser.add_argument('--source', required=True, help='Source branch (e.g. pull request branch)') + parser.add_argument('--target', required=True, help='Target branch (e.g. master branch)') + parser.add_argument('--subpath', required=True, help='Subpath containing the charts (e.g. system)') + + args = parser.parse_args() + + source_path = mkdtemp() + target_path = mkdtemp() + + clone_repository(args.repository, args.source, source_path) + clone_repository(args.repository, args.target, target_path) + + for chart in changed_charts(source_path, target_path, args.subpath): + with NamedTemporaryFile(suffix='.yaml', mode='w+', delete=False) as f_source, NamedTemporaryFile(suffix='.yaml', mode='w+', delete=False) as f_target: + render_helm_chart(f"{source_path}/{args.subpath}/{chart}", chart, chart, f_source.name) + render_helm_chart(f"{target_path}/{args.subpath}/{chart}", chart, chart, f_target.name) + + diff_result = run( + ['dyff', 'between', '--omit-header', '--use-go-patch-style', '--color=on', '--truecolor=off', f_target.name, f_source.name], + capture_output=True, + text=True, + check=True + ) + + print(diff_result.stdout) + + +if __name__ == "__main__": + main()