From 7f1f0b5350d3f6556b719d5e462bab4d6222e35c Mon Sep 17 00:00:00 2001 From: Adam Dyess Date: Thu, 12 Sep 2024 13:45:30 -0500 Subject: [PATCH] Introduce tests to ensure branches and lp-recipes exist (#663) --- .github/workflows/integration.yaml | 16 ++++ tests/branch_management/.copyright.tmpl | 1 + tests/branch_management/requirements-dev.txt | 5 + tests/branch_management/requirements-test.txt | 7 ++ tests/branch_management/tests/conftest.py | 27 ++++++ .../branch_management/tests/test_branches.py | 93 +++++++++++++++++++ tests/branch_management/tox.ini | 52 +++++++++++ 7 files changed, 201 insertions(+) create mode 100644 tests/branch_management/.copyright.tmpl create mode 100644 tests/branch_management/requirements-dev.txt create mode 100644 tests/branch_management/requirements-test.txt create mode 100644 tests/branch_management/tests/conftest.py create mode 100644 tests/branch_management/tests/test_branches.py create mode 100644 tests/branch_management/tox.ini diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 8f5b0eb40..6a58c1194 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -45,6 +45,22 @@ jobs: name: k8s.snap path: k8s.snap + test-branches: + name: Test Branch Management + runs-on: ubuntu-20.04 + steps: + - name: Check out code + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.8' + - name: Install tox + run: pip install tox + - name: Run branch_management tests + run: | + tox -c tests/branch_management -e integration + test-integration: name: Test ${{ matrix.os }} strategy: diff --git a/tests/branch_management/.copyright.tmpl b/tests/branch_management/.copyright.tmpl new file mode 100644 index 000000000..ecbed6c7a --- /dev/null +++ b/tests/branch_management/.copyright.tmpl @@ -0,0 +1 @@ +Copyright ${years} ${owner}. diff --git a/tests/branch_management/requirements-dev.txt b/tests/branch_management/requirements-dev.txt new file mode 100644 index 000000000..a66721ae0 --- /dev/null +++ b/tests/branch_management/requirements-dev.txt @@ -0,0 +1,5 @@ +black==24.3.0 +codespell==2.2.4 +flake8==6.0.0 +isort==5.12.0 +licenseheaders==0.8.8 diff --git a/tests/branch_management/requirements-test.txt b/tests/branch_management/requirements-test.txt new file mode 100644 index 000000000..b702086cc --- /dev/null +++ b/tests/branch_management/requirements-test.txt @@ -0,0 +1,7 @@ +coverage[toml]==7.2.5 +pytest==7.3.1 +PyYAML==6.0.1 +tenacity==8.2.3 +pylint==3.2.5 +requests==2.32.3 +semver==3.0.2 \ No newline at end of file diff --git a/tests/branch_management/tests/conftest.py b/tests/branch_management/tests/conftest.py new file mode 100644 index 000000000..f745828fb --- /dev/null +++ b/tests/branch_management/tests/conftest.py @@ -0,0 +1,27 @@ +# +# Copyright 2024 Canonical, Ltd. +# +from pathlib import Path + +import pytest +import requests +import semver + + +@pytest.fixture +def upstream_release() -> semver.VersionInfo: + """Return the latest stable k8s in the release series""" + release_url = "https://dl.k8s.io/release/stable.txt" + r = requests.get(release_url) + r.raise_for_status() + return semver.Version.parse(r.content.decode().lstrip("v")) + + +@pytest.fixture +def current_release() -> semver.VersionInfo: + """Return the current branch k8s version""" + ver_file = ( + Path(__file__).parent / "../../../build-scripts/components/kubernetes/version" + ) + version = ver_file.read_text().strip() + return semver.Version.parse(version.lstrip("v")) diff --git a/tests/branch_management/tests/test_branches.py b/tests/branch_management/tests/test_branches.py new file mode 100644 index 000000000..426fde599 --- /dev/null +++ b/tests/branch_management/tests/test_branches.py @@ -0,0 +1,93 @@ +# +# Copyright 2024 Canonical, Ltd. +# +from pathlib import Path +from subprocess import check_output + +import requests + + +def _get_max_minor(major): + """Get the latest minor release of the provided major. + For example if you use 1 as major you will get back X where X gives you latest 1.X release. + """ + minor = 0 + while _upstream_release_exists(major, minor): + minor += 1 + return minor - 1 + + +def _upstream_release_exists(major, minor): + """Return true if the major.minor release exists""" + release_url = "https://dl.k8s.io/release/stable-{}.{}.txt".format(major, minor) + r = requests.get(release_url) + return r.status_code == 200 + + +def _confirm_branch_exists(branch): + cmd = f"git ls-remote --heads https://github.com/canonical/k8s-snap.git/ {branch}" + output = check_output(cmd.split()).decode("utf-8") + assert branch in output, f"Branch {branch} does not exist" + + +def _branch_flavours(branch: str = None): + patch_dir = Path("build-scripts/patches") + branch = "HEAD" if branch is None else branch + cmd = f"git ls-tree --full-tree -r --name-only {branch} {patch_dir}" + output = check_output(cmd.split()).decode("utf-8") + patches = set( + Path(f).relative_to(patch_dir).parents[0] for f in output.splitlines() + ) + return [p.name for p in patches] + + +def _confirm_recipe(track, flavour): + recipe = f"https://launchpad.net/~containers/k8s/+snap/k8s-snap-{track}-{flavour}" + r = requests.get(recipe) + return r.status_code == 200 + + +def test_branches(upstream_release): + """Ensures git branches exist for prior releases. + + We need to make sure the LP builders pointing to the main github branch are only pushing + to the latest and current k8s edge snap tracks. An indication that this is not enforced is + that we do not have a branch for the k8s release for the previous stable release. Let me + clarify with an example. + + Assuming upstream stable k8s release is v1.12.x, there has to be a 1.11 github branch used + by the respective LP builders for building the v1.11.y. + """ + if upstream_release.minor != 0: + major = upstream_release.major + minor = upstream_release.minor - 1 + else: + major = int(upstream_release.major) - 1 + minor = _get_max_minor(major) + + prior_branch = f"release-{major}.{minor}" + print(f"Current stable is {upstream_release}") + print(f"Checking {prior_branch} branch exists") + _confirm_branch_exists(prior_branch) + flavours = _branch_flavours(prior_branch) + for flavour in flavours: + prior_branch = f"autoupdate/{prior_branch}-{flavour}" + print(f"Checking {prior_branch} branch exists") + _confirm_branch_exists(prior_branch) + + +def test_launchpad_recipe(current_release): + """Ensures the current recipes are available. + + We should ensure that a launchpad recipe exists for this release to be build with + """ + track = f"{current_release.major}.{current_release.minor}" + print(f"Checking {track} recipe exists") + flavours = ["classic"] + _branch_flavours() + recipe_exists = {flavour: _confirm_recipe(track, flavour) for flavour in flavours} + if missing_recipes := [ + flavour for flavour, exists in recipe_exists.items() if not exists + ]: + assert ( + not missing_recipes + ), f"LP Recipes do not exist for {track} {missing_recipes}" diff --git a/tests/branch_management/tox.ini b/tests/branch_management/tox.ini new file mode 100644 index 000000000..371ad51e4 --- /dev/null +++ b/tests/branch_management/tox.ini @@ -0,0 +1,52 @@ +[tox] +no_package = True +skip_missing_interpreters = True +env_list = format, lint, integration +min_version = 4.0.0 + +[testenv] +set_env = + PYTHONBREAKPOINT=pdb.set_trace + PY_COLORS=1 +pass_env = + PYTHONPATH + +[testenv:format] +description = Apply coding style standards to code +deps = -r {tox_root}/requirements-dev.txt +commands = + licenseheaders -t {tox_root}/.copyright.tmpl -cy -o 'Canonical, Ltd' -d {tox_root}/tests + isort {tox_root}/tests --profile=black + black {tox_root}/tests + +[testenv:lint] +description = Check code against coding style standards +deps = -r {tox_root}/requirements-dev.txt +commands = + codespell {tox_root}/tests + flake8 {tox_root}/tests + licenseheaders -t {tox_root}/.copyright.tmpl -cy -o 'Canonical, Ltd' -d {tox_root}/tests --dry + isort {tox_root}/tests --profile=black --check + black {tox_root}/tests --check --diff + +[testenv:test] +description = Run integration tests +deps = + -r {tox_root}/requirements-test.txt +commands = + pytest -v \ + --maxfail 1 \ + --tb native \ + --log-cli-level DEBUG \ + --disable-warnings \ + {posargs} \ + {tox_root}/tests +pass_env = + TEST_* + +[flake8] +max-line-length = 120 +select = E,W,F,C,N +ignore = W503 +exclude = venv,.git,.tox,.tox_env,.venv,build,dist,*.egg_info +show-source = true