Add PULP_MANIFEST support

This commit is contained in:
Rohan McGovern 2022-08-08 13:19:32 +10:00
parent 254e0f7cd9
commit 90b746caee
8 changed files with 166 additions and 27 deletions

View file

@ -1,21 +1,28 @@
import gzip import gzip
import logging import logging
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from typing import Optional from typing import Optional, Type
import aiohttp import aiohttp
from .base import Fetcher, GeneratedIndex from .base import Fetcher, GeneratedIndex, Repo
from .yum import YumRepo from .yum import YumRepo
from .pulp import PulpFileRepo
LOG = logging.getLogger("repo-autoindex") LOG = logging.getLogger("repo-autoindex")
REPO_TYPES = [YumRepo] REPO_TYPES: list[Type[Repo]] = [YumRepo, PulpFileRepo]
def http_fetcher(session: aiohttp.ClientSession) -> Fetcher: def http_fetcher(session: aiohttp.ClientSession) -> Fetcher:
async def get_content_with_session(url: str) -> str: async def get_content_with_session(url: str) -> Optional[str]:
LOG.info("Fetching: %s", url) LOG.info("Fetching: %s", url)
async with session.get(url) as resp: async with session.get(url) as resp:
if resp.status == 404:
# This error status means we successfully determined that
# no content exists
return None
# Any other error status is fatal
resp.raise_for_status() resp.raise_for_status()
# Deal with the non-ideal content negotiation # Deal with the non-ideal content negotiation

View file

@ -10,6 +10,8 @@ Fetcher = Callable[[str], Awaitable[Optional[str]]]
ICON_FOLDER = "📂" ICON_FOLDER = "📂"
ICON_PACKAGE = "📦" ICON_PACKAGE = "📦"
ICON_OPTICAL = "📀"
ICON_QCOW = "🐮"
ICON_OTHER = " " ICON_OTHER = " "

View file

@ -11,12 +11,17 @@ LOG = logging.getLogger("repo-autoindex")
async def dump_autoindices(args: argparse.Namespace) -> None: async def dump_autoindices(args: argparse.Namespace) -> None:
index_filename = args.index_filename index_filename = args.index_filename
wrote_any = False
async for index in autoindex(args.url, index_href_suffix=index_filename): async for index in autoindex(args.url, index_href_suffix=index_filename):
os.makedirs(index.relative_dir or ".", exist_ok=True) os.makedirs(index.relative_dir or ".", exist_ok=True)
output = os.path.join(index.relative_dir or ".", index_filename) output = os.path.join(index.relative_dir or ".", index_filename)
with open(output, "w") as f: with open(output, "w") as f:
f.write(index.content) f.write(index.content)
LOG.info("Wrote %s", output) LOG.info("Wrote %s", output)
wrote_any = True
if not wrote_any:
LOG.info("No indexable content found at %s", args.url)
def argparser() -> argparse.ArgumentParser: def argparser() -> argparse.ArgumentParser:

View file

@ -0,0 +1,64 @@
from typing import Optional, Type
from collections.abc import AsyncGenerator
import logging
from .base import Repo, GeneratedIndex, Fetcher, IndexEntry, ICON_OPTICAL, ICON_QCOW
from .template import TemplateContext
from .tree import treeify
LOG = logging.getLogger("repo-autoindex")
class PulpFileRepo(Repo):
async def render_index(
self, index_href_suffix: str
) -> AsyncGenerator[GeneratedIndex, None]:
all_entries: list[IndexEntry] = [
IndexEntry(
href="PULP_MANIFEST",
text="PULP_MANIFEST",
size=str(len(self.entry_point_content)),
)
]
# PULP_MANIFEST is a series of lines like this:
# rhel-workstation-7.2-snapshot-2-x86_64-boot.iso,fa687b8f847b5301b6da817fdbe612558aa69c65584ec5781f3feb0c19ff8f24,379584512
# rhel-workstation-7.3-rc-2-x86_64-dvd.iso,eab749310c95b4751ef9df7d7906ae0b8021c8e0dbc280c3efc8e967d5e60e71,4324327424
# rhel-workstation-7.3-rc-1-x86_64-dvd.iso,e165919d6977e02e493605dda6a30d2d80c3f16ee3f4c3ab946d256b815dd5db,4323278848
# rhel-server-7.3-rc-1-x86_64-boot.iso,f760611401fd928c2840eba85a7a80653fe2dc9dc94f3cef8ec1f3e7880d4102,427819008
for line in sorted(self.entry_point_content.splitlines()):
components = line.split(",")
if len(components) != 3:
LOG.warning("Ignoring bad line in PULP_MANIFEST: %s", line)
continue
entry = IndexEntry(
href=components[0], text=components[0], size=components[2]
)
if entry.href.endswith(".iso"):
entry.icon = ICON_OPTICAL
elif entry.href.endswith(".qcow2"):
entry.icon = ICON_QCOW
all_entries.append(entry)
ctx = TemplateContext()
nodes = [treeify(all_entries, index_href_suffix=index_href_suffix)]
while nodes:
node = nodes.pop()
yield GeneratedIndex(
content=ctx.render_index(index_entries=node.entries),
relative_dir=node.relative_dir,
)
nodes.extend(node.children)
@classmethod
async def probe(
cls: Type["PulpFileRepo"], fetcher: Fetcher, url: str
) -> Optional["PulpFileRepo"]:
manifest_url = f"{url}/PULP_MANIFEST"
manifest_content = await fetcher(manifest_url)
if manifest_content is None:
return None
return cls(url, manifest_content, fetcher)

View file

@ -0,0 +1,5 @@
file1.iso,abc123,100
file2.qcow2,abc123,200
oops, this line ain't so valid
file3,abc234,300

View file

@ -1,5 +1,7 @@
import pathlib import pathlib
import asyncio import asyncio
import logging
from collections.abc import Callable, Awaitable
import pytest import pytest
@ -11,30 +13,44 @@ from repo_autoindex._impl.cmd import entrypoint
THIS_DIR = pathlib.Path(__file__).parent THIS_DIR = pathlib.Path(__file__).parent
async def test_command(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path): class CommandTester:
"""Run the repo-autoindex command against a sample repo and check the generated index.""" def __init__(self, monkeypatch: pytest.MonkeyPatch):
self.monkeypatch = monkeypatch
async def __call__(self, url: str):
entrypoint_coro = []
def fake_run(coro):
assert asyncio.iscoroutine(coro)
entrypoint_coro.append(coro)
self.monkeypatch.setattr("asyncio.run", fake_run)
app = web.Application()
app.add_routes([web.static("/", THIS_DIR)])
async with test_utils.TestServer(app) as server:
repo_url = server.make_url(url + "//")
self.monkeypatch.setattr("sys.argv", ["repo-autoindex", str(repo_url)])
entrypoint()
assert entrypoint_coro
await entrypoint_coro[0]
@pytest.fixture
def tester(monkeypatch: pytest.MonkeyPatch) -> CommandTester:
return CommandTester(monkeypatch)
async def test_command_yum(
monkeypatch: pytest.MonkeyPatch, tester: CommandTester, tmp_path: pathlib.Path
):
"""Run the repo-autoindex command against a sample yum repo and check the generated index."""
monkeypatch.chdir(tmp_path) monkeypatch.chdir(tmp_path)
entrypoint_coro = [] await tester("/sample_repo")
def fake_run(coro):
assert asyncio.iscoroutine(coro)
entrypoint_coro.append(coro)
monkeypatch.setattr("asyncio.run", fake_run)
app = web.Application()
app.add_routes([web.static("/", THIS_DIR)])
async with test_utils.TestServer(app) as server:
repo_url = server.make_url("/sample_repo///")
monkeypatch.setattr("sys.argv", ["repo-autoindex", str(repo_url)])
entrypoint()
assert entrypoint_coro
await entrypoint_coro[0]
# It should have written index files reproducing the structure # It should have written index files reproducing the structure
index_toplevel = tmp_path.joinpath("index.html") index_toplevel = tmp_path.joinpath("index.html")
@ -56,3 +72,43 @@ async def test_command(monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path):
assert '<a href="w/index.html">w/</a>' in pkgs assert '<a href="w/index.html">w/</a>' in pkgs
assert '<a href="walrus-5.21-1.noarch.rpm">walrus-5.21-1.noarch.rpm</a>' in w assert '<a href="walrus-5.21-1.noarch.rpm">walrus-5.21-1.noarch.rpm</a>' in w
async def test_command_pulp(
monkeypatch: pytest.MonkeyPatch, tester: CommandTester, tmp_path: pathlib.Path
):
"""Run the repo-autoindex command against a sample pulp file repo and check the generated index."""
monkeypatch.chdir(tmp_path)
await tester("/sample_pulp_repo")
# It should have written an index file
index_toplevel = tmp_path.joinpath("index.html")
assert index_toplevel.exists()
# Simple sanity check of some expected content
toplevel = index_toplevel.read_text()
assert '<a href="file2.qcow2">file2.qcow2</a>' in toplevel
async def test_command_no_content(
monkeypatch: pytest.MonkeyPatch,
tmp_path: pathlib.Path,
tester: CommandTester,
caplog: pytest.LogCaptureFixture,
):
"""Run the repo-autoindex command against an empty repo and verify nothing is written."""
caplog.set_level(logging.INFO)
monkeypatch.chdir(tmp_path)
await tester("/some-nonexistent-dir")
# It should not have written any index.html
index_toplevel = tmp_path.joinpath("index.html")
assert not index_toplevel.exists()
# It should have mentioned that there was no content
assert "No indexable content found" in caplog.text

View file

@ -16,6 +16,7 @@ class FakeResponse:
def __init__(self, body: bytes, content_type: str): def __init__(self, body: bytes, content_type: str):
self.body = body self.body = body
self.content_type = content_type self.content_type = content_type
self.status = 200
async def __aenter__(self): async def __aenter__(self):
return self return self

View file

@ -1,6 +1,5 @@
from typing import Optional from typing import Optional
import textwrap import textwrap
import pytest
from repo_autoindex import autoindex from repo_autoindex import autoindex
from repo_autoindex._impl.base import GeneratedIndex from repo_autoindex._impl.base import GeneratedIndex
@ -395,7 +394,7 @@ class StaticFetcher:
self.content: dict[str, str] = {} self.content: dict[str, str] = {}
async def __call__(self, url: str) -> Optional[str]: async def __call__(self, url: str) -> Optional[str]:
return self.content[url] return self.content.get(url)
async def test_typical_index(): async def test_typical_index():