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.
Dustin 2024-09-05 21:19:23 -05:00
parent 34fbdc6e02
commit b845dfdebe
1 changed files with 56 additions and 14 deletions

View File

@ -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
if diff:
description = (
'<details>\n<summary>Manifest diff</summary>\n\n'
f'```diff\n{diff}```\n'
'</details>'
)
if not args.dry_run:
repo.head.reference.set_tracking_branch(
git.RemoteReference(
repo, f'refs/remotes/origin/{args.branch_name}'
)
)
if not args.dry_run:
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__':