Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/gardenlinux/apt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"""

from .debsource import Debsrc, DebsrcFile
from .package_repo_info import GardenLinuxRepo

__all__ = ["Debsrc", "DebsrcFile"]
__all__ = ["Debsrc", "DebsrcFile", "GardenLinuxRepo"]
10 changes: 9 additions & 1 deletion src/gardenlinux/constants.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import os
from pathlib import Path

ARCHS = ["amd64", "arm64"]

Expand Down Expand Up @@ -147,3 +148,10 @@
OCI_IMAGE_INDEX_MEDIA_TYPE = "application/vnd.oci.image.index.v1+json"

RELEASE_ID_FILE = ".github_release_id"

REQUESTS_TIMEOUTS = (5, 30) # connect, read

S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads"

GLVD_BASE_URL = "https://glvd.ingress.glvd.gardnlinux.shoot.canary.k8s-hana.ondemand.com/v1"
GL_DEB_REPO_BASE_URL = "https://packages.gardenlinux.io/gardenlinux"
29 changes: 27 additions & 2 deletions src/gardenlinux/github/__main__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import argparse

from .release import upload_to_github_release_page
from gardenlinux.logger import LoggerSetup

from .release import create_github_release, upload_to_github_release_page, write_to_release_id_file
from .release_notes import create_github_release_notes

LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO")


def main():
parser = argparse.ArgumentParser(description="GitHub Release Script")
subparsers = parser.add_subparsers(dest="command")

create_parser = subparsers.add_parser("create")
create_parser.add_argument("--owner", default="gardenlinux")
create_parser.add_argument("--repo", default="gardenlinux")
create_parser.add_argument("--tag", required=True)
create_parser.add_argument("--commit", required=True)
create_parser.add_argument('--latest', action='store_true', default=False)
create_parser.add_argument("--dry-run", action="store_true", default=False)

upload_parser = subparsers.add_parser("upload")
upload_parser.add_argument("--owner", default="gardenlinux")
upload_parser.add_argument("--repo", default="gardenlinux")
Expand All @@ -16,7 +29,19 @@ def main():

args = parser.parse_args()

if args.command == "upload":
if args.command == "create":
body = create_github_release_notes(args.tag, args.commit)
if args.dry_run:
print("Dry Run ...")
print("This release would be created:")
print(body)
else:
release_id = create_github_release(
args.owner, args.repo, args.tag, args.commit, args.latest, body
)
write_to_release_id_file(f"{release_id}")
LOGGER.info(f"Release created with ID: {release_id}")
elif args.command == "upload":
upload_to_github_release_page(
args.owner, args.repo, args.release_id, args.file_path, args.dry_run
)
Expand Down
6 changes: 2 additions & 4 deletions src/gardenlinux/github/release/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

import requests

from gardenlinux.constants import RELEASE_ID_FILE
from gardenlinux.constants import RELEASE_ID_FILE, REQUESTS_TIMEOUTS
from gardenlinux.logger import LoggerSetup

LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO")

REQUESTS_TIMEOUTS = (5, 30) # connect, read
LOGGER = LoggerSetup.get_logger("gardenlinux.github.release", "INFO")


def create_github_release(owner, repo, tag, commitish, latest, body):
Expand Down
33 changes: 33 additions & 0 deletions src/gardenlinux/github/release_notes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from .helpers import get_package_list
from .sections import (
release_notes_changes_section,
release_notes_compare_package_versions_section,
release_notes_software_components_section,
)


def create_github_release_notes(gardenlinux_version, commitish):
package_list = get_package_list(gardenlinux_version)

output = ""

output += release_notes_changes_section(gardenlinux_version)

output += release_notes_software_components_section(package_list)

output += release_notes_compare_package_versions_section(
gardenlinux_version, package_list
)

# TODO: image ids

output += "\n"
output += "## Kernel Module Build Container (kmodbuild)"
output += "\n"
output += "```"
output += "\n"
output += f"ghcr.io/gardenlinux/gardenlinux/kmodbuild:{gardenlinux_version}"
output += "\n"
output += "```"
output += "\n"
return output
35 changes: 35 additions & 0 deletions src/gardenlinux/github/release_notes/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import gzip
import io

import requests

from gardenlinux.apt import DebsrcFile, GardenLinuxRepo
from gardenlinux.apt.package_repo_info import compare_repo
from gardenlinux.constants import GL_DEB_REPO_BASE_URL, REQUESTS_TIMEOUTS


def get_package_list(gardenlinux_version):
url = f"{GL_DEB_REPO_BASE_URL}/dists/{gardenlinux_version}/main/binary-amd64/Packages.gz"
response = requests.get(url, timeout=REQUESTS_TIMEOUTS)
response.raise_for_status()

d = DebsrcFile()

with io.BytesIO(response.content) as buf:
with gzip.open(buf, "rt") as f:
d.read(f)

return d


def compare_apt_repo_versions(previous_version, current_version):
previous_repo = GardenLinuxRepo(previous_version)
current_repo = GardenLinuxRepo(current_version)
pkg_diffs = sorted(compare_repo(previous_repo, current_repo), key=lambda t: t[0])

output = f"| Package | {previous_version} | {current_version} |\n"
output += "|---------|--------------------|-------------------|\n"

for pkg in pkg_diffs:
output += f"|{pkg[0]} | {pkg[1] if pkg[1] is not None else '-'} | {pkg[2] if pkg[2] is not None else '-'} |\n"
return output
112 changes: 112 additions & 0 deletions src/gardenlinux/github/release_notes/sections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import re
import textwrap

import requests

from gardenlinux.constants import GLVD_BASE_URL, REQUESTS_TIMEOUTS
from gardenlinux.logger import LoggerSetup

from .helpers import compare_apt_repo_versions

LOGGER = LoggerSetup.get_logger("gardenlinux.github.release_notes", "INFO")


def release_notes_changes_section(gardenlinux_version):
"""
Get list of fixed CVEs, grouped by upgraded package.
Note: This result is not perfect, feel free to edit the generated release notes and
file issues in glvd for improvement suggestions https://github.com/gardenlinux/glvd/issues
"""
try:
url = f"{GLVD_BASE_URL}/patchReleaseNotes/{gardenlinux_version}"
response = requests.get(url, timeout=REQUESTS_TIMEOUTS)
response.raise_for_status()
data = response.json()

if len(data["packageList"]) == 0:
return ""

output = [
"## Changes",
"The following packages have been upgraded, to address the mentioned CVEs:",
]
for package in data["packageList"]:
upgrade_line = (
f"- upgrade '{package['sourcePackageName']}' from `{package['oldVersion']}` "
f"to `{package['newVersion']}`"
)
output.append(upgrade_line)

if package["fixedCves"]:
for fixedCve in package["fixedCves"]:
output.append(f" - {fixedCve}")

return "\n".join(output) + "\n\n"
except Exception as exn:
# There are expected error cases,
# for example with versions not supported by glvd (1443.x)
# or when the api is not available
# Fail gracefully by adding the placeholder we previously used,
# so that the release note generation does not fail.
LOGGER.error(f"Failed to process GLVD API output: {exn}")
return textwrap.dedent(
"""
## Changes
The following packages have been upgraded, to address the mentioned CVEs:
**todo release facilitator: fill this in**
"""
)


def release_notes_software_components_section(package_list):
output = "## Software Component Versions\n"
output += "```"
output += "\n"
packages_regex = re.compile(
r"^linux-image-amd64$|^systemd$|^containerd$|^runc$|^curl$|^openssl$|^openssh-server$|^libc-bin$"
)
for entry in package_list.values():
if packages_regex.match(entry.deb_source):
output += f"{entry!r}\n"
output += "```"
output += "\n\n"
return output


def release_notes_compare_package_versions_section(gardenlinux_version, package_list):
output = ""
version_components = gardenlinux_version.split(".")
# Assumes we always have version numbers like 1443.2
if len(version_components) == 2:
try:
major = int(version_components[0])
patch = int(version_components[1])

if patch > 0:
previous_version = f"{major}.{patch - 1}"

output += (
f"## Changes in Package Versions Compared to {previous_version}\n"
)
output += compare_apt_repo_versions(
previous_version, gardenlinux_version
)
elif patch == 0:
output += f"## Full List of Packages in Garden Linux version {major}\n"
output += "<details><summary>Expand to see full list</summary>\n"
output += "<pre>"
output += "\n"
for entry in package_list.values():
output += f"{entry!r}\n"
output += "</pre>"
output += "\n</details>\n\n"

except ValueError:
LOGGER.error(
f"Could not parse {gardenlinux_version} as the Garden Linux version, skipping version compare section"
)
else:
LOGGER.error(
f"Unexpected version number format {gardenlinux_version}, expected format (major is int).(patch is int)"
)
return output
9 changes: 0 additions & 9 deletions tests/constants.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
# -*- coding: utf-8 -*-

import os
from pathlib import Path

from gardenlinux.git import Repository

TEST_DATA_DIR = "test-data"
Expand All @@ -26,7 +21,3 @@

TEST_GARDENLINUX_RELEASE = "1877.3"
TEST_GARDENLINUX_COMMIT = "75df9f401a842914563f312899ec3ce34b24515c"

RELEASE_ID_FILE = ".github_release_id"

S3_DOWNLOADS_DIR = Path(os.path.dirname(__file__)) / ".." / "s3_downloads"
2 changes: 1 addition & 1 deletion tests/github/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from ..constants import RELEASE_ID_FILE, S3_DOWNLOADS_DIR
from gardenlinux.constants import RELEASE_ID_FILE, S3_DOWNLOADS_DIR


@pytest.fixture
Expand Down
39 changes: 39 additions & 0 deletions tests/github/test_create_github_release_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import requests_mock

from gardenlinux.constants import GLVD_BASE_URL
from gardenlinux.github.release_notes import (
release_notes_changes_section,
release_notes_compare_package_versions_section,
)

from ..constants import TEST_GARDENLINUX_RELEASE


def test_release_notes_changes_section_empty_packagelist():
with requests_mock.Mocker() as m:
m.get(
f"{GLVD_BASE_URL}/patchReleaseNotes/{TEST_GARDENLINUX_RELEASE}",
text='{"packageList": []}',
status_code=200
)
assert release_notes_changes_section(TEST_GARDENLINUX_RELEASE) == "", \
"Expected an empty result if GLVD returns an empty package list"


def test_release_notes_changes_section_broken_glvd_response():
with requests_mock.Mocker() as m:
m.get(
f"{GLVD_BASE_URL}/patchReleaseNotes/{TEST_GARDENLINUX_RELEASE}",
text="<html><body><h1>Personal Home Page</h1></body></html>",
status_code=200
)
assert "fill this in" in release_notes_changes_section(TEST_GARDENLINUX_RELEASE), \
"Expected a placeholder message to be generated if GVLD response is not valid"


def test_release_notes_compare_package_versions_section_semver_is_not_recognized():
assert release_notes_compare_package_versions_section("1.2.0", []) == "", "Semver is not supported"


def test_release_notes_compare_package_versions_section_unrecognizable_version():
assert release_notes_compare_package_versions_section("garden.linux", []) == ""
Loading