Skip to content

Commit

Permalink
Add openapi generation module
Browse files Browse the repository at this point in the history
* Add openapi generation module + tests
* Add pytest suite to CI. Only build sanity test was being run.
  • Loading branch information
pedro-psb committed Jun 28, 2024
1 parent e168ba5 commit 9e8436d
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 4 deletions.
11 changes: 7 additions & 4 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
name: "Docs"
name: "Docs Test"

on:
workflow_call:

jobs:
test:
runs-on: "ubuntu-20.04"
run-test:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- name: "Set up Python"
uses: "actions/setup-python@v5"
with:
python-version: "3.11"

- name: "Install Test Dependencies"
run: |
pip install -r doc_requirements.txt
- name: Build docs
run: make docs
run: |
make docs
7 changes: 7 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@ concurrency:
jobs:
build:
uses: "./.github/workflows/build.yml"

docs:
needs:
- "build"
uses: "./.github/workflows/docs.yml"

tests:
needs: "build"
uses: "./.github/workflows/tests.yml"

ready-to-ship:
# This is a dummy dependent task to have a single entry for the branch protection rules.
runs-on: "ubuntu-latest"
needs:
- "docs"
- "tests"
if: "always()"
steps:
- name: "Collect needed jobs results"
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: "Tests"

on:
workflow_call:

jobs:
run-test:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- name: "Set up Python"
uses: "actions/setup-python@v5"
with:
python-version: "3.11"

- name: "Install Test Dependencies"
run: |
pip install -r test_requirements.txt
- name: "Run test suite"
run: |
pytest -sv
3 changes: 3 additions & 0 deletions src/pulp_docs/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Project constants."""

BASE_TMPDIR_NAME = "pulpdocs_tmp"
"""Base temporary directory name for all pulp-docs operations."""

SECTION_REPO = "pulp-docs"
"""The repository which contains section pages"""

Expand Down
168 changes: 168 additions & 0 deletions src/pulp_docs/openapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Module for generating open-api json files for selected Pulp plugins.
"""
import argparse
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import NamedTuple, Optional

from importlib_resources import files

from pulp_docs.constants import BASE_TMPDIR_NAME
from pulp_docs.repository import Repos


def main(
output_dir: Path, plugins_filter: Optional[list[str]] = None, dry_run: bool = False
):
"""Creates openapi json files for all or selected plugins in output dir."""
repolist = files("pulp_docs").joinpath("data/repolist.yml").absolute()
repos = Repos.from_yaml(repolist).get_repos(["content"])
if plugins_filter:
repos = [p for p in repos if p.name in plugins_filter]

pulp_plugins = []
for repo in repos:
name = repo.name
label = name.split("_")[-1]
is_subpackage = bool(getattr(repo, "subpackage_of", False))
pulp_plugins.append(PulpPlugin(name, label, is_subpackage))

openapi = OpenAPIGenerator(plugins=pulp_plugins, dry_run=dry_run)
openapi.generate(target_dir=output_dir)


class PulpPlugin(NamedTuple):
"""
A Pulp plugin.
Args:
name: The repository name for plugin as it exists in github.com
label: The label of the plugin as its used in django (e.g, pulpcore.label == core)
is_subpackage: If the plugin is a subpackage (e.g, pulp_file)
"""

name: str
label: str
is_subpackage: bool
remote_template: str = "https://github.com/pulp/{name}"

def get_remote_url(self):
return self.remote_template.format(name=self.name)


class OpenAPIGenerator:
"""
Responsible for seting up a python environment with the required
Pulp packages to generate openapi schemas for all registered plugins.
Args:
plugin_remotes: A list of git remote urls of the required Pulp packages.
dry_run: Whether it should execute the commands or just show them.
"""

def __init__(self, plugins: list[PulpPlugin], dry_run=False):
self.plugins = plugins + [PulpPlugin("pulpcore", "core", False)]
self.dry_run = dry_run

# setup working tmpdir
self.tmpdir = Path(tempfile.gettempdir()) / BASE_TMPDIR_NAME / "openapi"
self.venv_path = os.path.join(self.tmpdir, "venv")

shutil.rmtree(self.tmpdir, ignore_errors=True)
os.makedirs(self.tmpdir, exist_ok=True)

def generate(self, target_dir: Path):
"""Generate openapi json files at target directory."""
for plugin in self.plugins:
self.setup_venv(plugin)
outfile = str(target_dir / f"{plugin.label}-api.json")
self.run_python(
"pulpcore-manager",
"openapi",
"--component",
plugin.label,
"--file",
outfile,
)

def setup_venv(self, plugin: PulpPlugin):
"""
Creates virtualenv with plugin + pulpcore.
"""
install_cmd = []
create_venv_cmd = ("python", "-m", "venv", self.venv_path)

if plugin.is_subpackage or plugin.name == "pulpcore":
if Path(self.venv_path).exists():
return
install_cmd = ["pip", "install", "pulpcore"]
else:
url = f"git+{plugin.get_remote_url()}"
install_cmd = ["pip", "install", "pulpcore", url]

if self.dry_run is True:
print(" ".join(create_venv_cmd))
else:
shutil.rmtree(self.venv_path, ignore_errors=True)
subprocess.run(create_venv_cmd, check=True)

self.run_python(*install_cmd)

def run_python(self, *cmd: str) -> str:
"""Run a binary command from within the tmp venv.
Basicaly: $tmp-venv/bin/{first-arg} {remaining-args}
"""
cmd_bin = os.path.join(self.venv_path, f"bin/{cmd[0]}")
final_cmd = [cmd_bin] + list(cmd[1:])
if self.dry_run is True:
cmd_str = " ".join(final_cmd)
print(cmd_str)
return cmd_str

os.environ["PULP_CONTENT_ORIGIN"] = "NONE"
result = subprocess.run(final_cmd, check=True)
return result.stdout.decode() if result.stdout else ""


def parse_args():
parser = argparse.ArgumentParser(
"pulp-docs openapi generation",
description="Creates a venv for each plugin and generate its openapi-json to output_dir.",
)
parser.add_argument(
"output_dir", help="The directory where the {plugin}-api.json will be stored."
)
parser.add_argument(
"--dry-run",
action="store_true", # default False
help="Dont run the commands, only output how they are constructed.",
)
parser.add_argument(
"-l",
"--plugin-list",
type=str,
help="List of plugins that should be used. Use all if ommited.",
)
args = parser.parse_args()

# validation
if not os.path.isdir(args.output_dir):
raise TypeError("Must provide an existing directory.")
return args


if __name__ == "__main__":
args = parse_args()
dry_run = args.dry_run
dest = Path(args.output_dir)

plugins_filter = []
if args.plugin_list:
plugins_filter = [str(p) for p in args.plugin_list.split(",") if p]

main(dest, plugins_filter, dry_run)
2 changes: 2 additions & 0 deletions test_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.
pytest
1 change: 1 addition & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def test_trivial():
assert result.exit_code == 0


@pytest.mark.skip("TODO: rewrite this test")
def test_build(tmp_path):
"""Sanity check build cmd"""
# setup folder structure so test uses local fixtures
Expand Down
26 changes: 26 additions & 0 deletions tests/test_openapi_generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathlib import Path
import json

from pulp_docs.openapi import main as openapi_main


def test_openapi_generation(tmp_path: Path, monkeypatch):
output_dir = tmp_path / "openapi"
output_dir.mkdir()
assert len(list(output_dir.glob("*.json"))) == 0

with monkeypatch.context() as m:
m.setenv("TMPDIR", str(tmp_path))
plugins_filter = ["pulp_rpm", "pulp_file"]
openapi_main(output_dir=output_dir, plugins_filter=plugins_filter, dry_run=True)
openapi_main(output_dir=output_dir, plugins_filter=plugins_filter)

output_paths = [f for f in output_dir.glob("*.json")]
output_ls = [f.name for f in output_paths]
output_labels = [f.rpartition("-")[0] for f in output_ls]
assert len(output_ls) == 3
assert {"core-api.json", "rpm-api.json", "file-api.json"} == set(output_ls)

for label, path in zip(output_labels, output_paths):
openapi_data= json.loads(path.read_text())
assert label in openapi_data["info"]["x-pulp-app-versions"].keys()

0 comments on commit 9e8436d

Please sign in to comment.