From b845dfdebe6b12c875279a628ba2cb4f69fee7d3 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Thu, 5 Sep 2024 21:19:23 -0500 Subject: [PATCH] Include manifest diff in PR description Naturally, the PR will include the diff of the configuration changes the update process makes, but that doesn't necessarily show what will actually change in the cluster. This is true of the `images` setting in Kustomize configuration, and will become even more important when we start updating remote manifest references. To get a better idea of what will actually change when the update is applied, we now try to run `kubectl diff` for each project after making all changes. The output is then included in the PR description. --- updatebot.py | 70 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/updatebot.py b/updatebot.py index eacac8c..edc5cc0 100644 --- a/updatebot.py +++ b/updatebot.py @@ -5,6 +5,7 @@ import logging import functools import os import re +import subprocess import tempfile import threading import urllib.parse @@ -130,6 +131,10 @@ class BaseProject(abc.ABC, pydantic.BaseModel): def apply_updates(self, basedir: Path) -> Iterable[tuple[ImageDef, str]]: raise NotImplementedError + @abc.abstractmethod + def manifest_diff(self, basedir: Path) -> Optional[str]: + raise NotImplementedError + class KustomizeProject(BaseProject): kind: Literal['kustomize'] @@ -169,6 +174,25 @@ class KustomizeProject(BaseProject): yaml.dump(kustomization, f) yield (image, version) + def manifest_diff(self, basedir: Path) -> Optional[str]: + path = basedir / self.path + cmd = ['kubectl', 'diff', '-k', path] + try: + p = subprocess.run( + cmd, + stdin=subprocess.DEVNULL, + capture_output=True, + check=False, + encoding='utf-8', + ) + except FileNotFoundError as e: + log.warning('Cannot generate namifest diff: %s', e) + return None + if p.returncode != 0 and not p.stdout: + log.error('Failed to generate manifest diff: %s', p.stderr) + return None + return p.stdout + Project = KustomizeProject @@ -204,7 +228,11 @@ class RepoConfig(pydantic.BaseModel): return data['clone_url'] def create_pr( - self, title: str, source_branch: str, target_branch: str + self, + title: str, + source_branch: str, + target_branch: str, + body: Optional[str] = None, ) -> None: session = _get_session() r = session.post( @@ -216,6 +244,7 @@ class RepoConfig(pydantic.BaseModel): 'title': title, 'base': target_branch, 'head': source_branch, + 'body': body, }, ) log.log(TRACE, '%r', r.content) @@ -244,10 +273,12 @@ class Arguments: projects: list[str] -def update_project(repo: git.Repo, project: Project) -> Optional[str]: +def update_project( + repo: git.Repo, project: Project +) -> tuple[Optional[str], Optional[str]]: basedir = Path(repo.working_dir) title = None - for (image, version) in project.apply_updates(basedir): + for image, version in project.apply_updates(basedir): log.info('Updating %s to %s', image.name, version) if repo.index.diff(None): log.debug('Committing changes to %s', project.path) @@ -261,7 +292,8 @@ def update_project(repo: git.Repo, project: Project) -> Optional[str]: title = c.summary else: log.info('No changes to commit') - return title + diff = project.manifest_diff(basedir) + return title, diff def parse_args() -> Arguments: @@ -322,29 +354,39 @@ def main() -> None: log.debug('Checking out new branch: %s', args.branch_name) repo.heads[0].checkout(force=True, B=args.branch_name) title = None + description = None if project.name not in projects: continue - title = update_project(repo, project) + title, diff = update_project(repo, project) if not title: log.info('No changes made') continue - repo.head.reference.set_tracking_branch( - git.RemoteReference( - repo, f'refs/remotes/origin/{args.branch_name}' + if diff: + description = ( + '
\nManifest diff\n\n' + f'```diff\n{diff}```\n' + '
' ) - ) if not args.dry_run: + repo.head.reference.set_tracking_branch( + git.RemoteReference( + repo, f'refs/remotes/origin/{args.branch_name}' + ) + ) repo.remote().push(force=True) config.repo.create_pr( - title, args.branch_name, config.repo.branch + title, + args.branch_name, + config.repo.branch, + description, ) else: - log.info( - 'Would create PR %s → %s: %s', - config.repo.branch, - args.branch_name, + print( + 'Would create PR', + f'{args.branch_name} → {config.repo.branch}:', title, ) + print(description or '') if __name__ == '__main__':