Skip to content
Draft
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
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,21 @@ dependencies = [
"confluent-kafka",
]

[build-system]
requires = ["setuptools>=61.0.0", "wheel"]
build-backend = "setuptools.build_meta"

[tool.setuptools]
packages = [
"release_service_utils",
"release_service_utils.helpers",
"release_service_utils.tasks",
"release_service_utils.tasks.internal",
Comment thread
midnightercz marked this conversation as resolved.
]

[tool.setuptools.package-dir]
"release_service_utils" = "scripts/python"

Comment thread
midnightercz marked this conversation as resolved.
[tool.black]
line-length = 95

Expand Down
1 change: 1 addition & 0 deletions scripts/python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Release Service Utils Python Package."""
11 changes: 11 additions & 0 deletions scripts/python/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,12 @@
"""Reusable Python helpers for release task scripts."""

__all__ = [
"authentication",
"file",
"http_client",
"image_ref",
"logger",
"osidb",
"retry",
"tekton",
]
2 changes: 1 addition & 1 deletion scripts/python/helpers/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from collections.abc import Sequence
from pathlib import Path

import retry
from release_service_utils.helpers import retry


def read_mounted_text(mount: Path, filename: str) -> str:
Expand Down
2 changes: 1 addition & 1 deletion scripts/python/helpers/osidb.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import json
from typing import Any

import http_client
from release_service_utils.helpers import http_client
from requests_kerberos import HTTPKerberosAuth, OPTIONAL


Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import pytest

import authentication
from release_service_utils.helpers import authentication


def test_read_mounted_text_strips_whitespace(tmp_path: Path) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

from pathlib import Path

import file
import pytest

from release_service_utils.helpers import file


def test_path_from_env_variable_uses_set_value(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import requests
from requests.adapters import HTTPAdapter

import http_client
from release_service_utils.helpers import http_client


def test_retries_policy_defaults() -> None:
Expand Down Expand Up @@ -43,7 +43,9 @@ def test_get_text_uses_session_and_headers() -> None:
r.text = "body"
r.raise_for_status = mock.MagicMock()
session.get.return_value = r
with mock.patch("http_client.get_session", return_value=session):
with mock.patch(
"release_service_utils.helpers.http_client.get_session", return_value=session
):
out = http_client.get_text("https://e/x", headers={"A": "b", "C": "d"})

assert out == "body"
Expand All @@ -63,7 +65,9 @@ def test_get_text_auth_passed() -> None:
r.text = "t"
session.get.return_value = r
ra = object()
with mock.patch("http_client.get_session", return_value=session):
with mock.patch(
"release_service_utils.helpers.http_client.get_session", return_value=session
):
assert http_client.get_text("https://u", auth=ra) == "t"
assert session.get.call_args[1]["auth"] is ra

Expand All @@ -75,7 +79,9 @@ def test_get_text_http_error() -> None:
r.status_code = 500
r.raise_for_status.side_effect = requests.HTTPError("nope", response=mock.MagicMock())
session.get.return_value = r
with mock.patch("http_client.get_session", return_value=session):
with mock.patch(
"release_service_utils.helpers.http_client.get_session", return_value=session
):
with pytest.raises(requests.HTTPError):
http_client.get_text("https://u/")

Expand All @@ -99,9 +105,11 @@ def test_get_text_retries_429_then_succeeds() -> None:
session.get.side_effect = [r1, r2, r3]

with (
mock.patch("http_client.get_session", return_value=session),
mock.patch("http_client.random.randint", return_value=0),
mock.patch("http_client.time.sleep") as sleep_mock,
mock.patch(
"release_service_utils.helpers.http_client.get_session", return_value=session
),
mock.patch("release_service_utils.helpers.http_client.random.randint", return_value=0),
mock.patch("release_service_utils.helpers.http_client.time.sleep") as sleep_mock,
):
assert http_client.get_text("https://u") == "ok"

Expand All @@ -125,9 +133,11 @@ def test_get_text_retries_404_when_enabled(monkeypatch: pytest.MonkeyPatch) -> N
session.get.side_effect = [r1, r2]

with (
mock.patch("http_client.get_session", return_value=session),
mock.patch("http_client.random.randint", return_value=0),
mock.patch("http_client.time.sleep") as sleep_mock,
mock.patch(
"release_service_utils.helpers.http_client.get_session", return_value=session
),
mock.patch("release_service_utils.helpers.http_client.random.randint", return_value=0),
mock.patch("release_service_utils.helpers.http_client.time.sleep") as sleep_mock,
):
assert http_client.get_text("https://u") == "ok"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from __future__ import annotations

import image_ref
from release_service_utils.helpers import image_ref

import pytest


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
import pytest
import requests_kerberos

import osidb
from release_service_utils.helpers import osidb


def test_get_access_token_returns_access_string() -> None:
"""
A JSON body with a non-empty string ``access`` is returned, and the token URL
is ``{base}/auth/token`` (trailing slash on the base is stripped before join).
"""
with mock.patch("http_client.get_text", return_value='{"access": "tok-abc"}') as m:
with mock.patch("osidb.http_client.get_text", return_value='{"access": "tok-abc"}') as m:
out = osidb.get_access_token("https://osidb.example.com/")
assert out == "tok-abc"
m.assert_called_once()
Expand All @@ -27,7 +27,7 @@ def test_get_access_token_returns_access_string() -> None:

def test_get_access_token_rejects_empty_body() -> None:
"""A blank HTTP body raises with a message about the token request."""
with mock.patch("http_client.get_text", return_value=""):
with mock.patch("osidb.http_client.get_text", return_value=""):
with pytest.raises(ValueError) as e:
osidb.get_access_token("https://u")
assert e.value
Expand All @@ -37,7 +37,7 @@ def test_get_access_token_rejects_empty_body() -> None:
@pytest.mark.parametrize("body", ['{"x": 1}', '{"access": ""}'])
def test_get_access_token_rejects_invalid_access(body: str) -> None:
"""``ValueError`` when JSON has no non-empty string ``access`` (missing or empty)."""
with mock.patch("http_client.get_text", return_value=body):
with mock.patch("osidb.http_client.get_text", return_value=body):
with pytest.raises(ValueError) as e:
osidb.get_access_token("https://u")
assert "access" in str(e.value) or e.value
1 change: 1 addition & 0 deletions scripts/python/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Release Service Utils Python Tasks."""
1 change: 1 addition & 0 deletions scripts/python/tasks/internal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Release Service Utils Python Internal Tasks."""
10 changes: 5 additions & 5 deletions scripts/python/tasks/internal/check_embargoed_cves.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@
import requests
import urllib.parse

import authentication
import file
import http_client
import osidb
import tekton
from release_service_utils.helpers import authentication
from release_service_utils.helpers import file
from release_service_utils.helpers import http_client
from release_service_utils.helpers import osidb
from release_service_utils.helpers import tekton

PROG = "check_embargoed_cves.py"

Expand Down
12 changes: 6 additions & 6 deletions scripts/python/tasks/internal/check_fbc_opt_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
from requests.auth import AuthBase
from requests_kerberos import HTTPKerberosAuth, OPTIONAL

import authentication
import file
import http_client
import image_ref
import tekton
from logger import logger
from release_service_utils.helpers import authentication
from release_service_utils.helpers import file
from release_service_utils.helpers import http_client
from release_service_utils.helpers import image_ref
from release_service_utils.helpers import tekton
from release_service_utils.helpers.logger import logger

PROG = "check_fbc_opt_in.py"

Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from pathlib import Path
from unittest import mock

import check_embargoed_cves
import file
import pytest
import tekton

from release_service_utils.tasks.internal import check_embargoed_cves
from release_service_utils.helpers import file
from release_service_utils.helpers import tekton


def _write_service_account(
Expand Down Expand Up @@ -80,7 +81,9 @@ def test_is_embargoed_flaw_response(payload: dict, exp: bool) -> None:

def test_fetch_flaw_state_empty_bodies() -> None:
"""An empty body from the flaws request is an empty result dict."""
with mock.patch("http_client.get_text", return_value="") as m:
with mock.patch(
"release_service_utils.helpers.http_client.get_text", return_value=""
) as m:
d = check_embargoed_cves.fetch_flaw_state("https://u", "t", "CVE-1")
m.assert_called()
assert d == {}
Expand All @@ -89,7 +92,7 @@ def test_fetch_flaw_state_empty_bodies() -> None:
def test_fetch_flaw_state_parses_json_body() -> None:
"""A non-empty flaws body is parsed with `json.loads` and returned as a dict."""
body = '{"results": [{"cve_id": "CVE-1", "embargoed": false}]}'
with mock.patch("http_client.get_text", return_value=body):
with mock.patch("release_service_utils.helpers.http_client.get_text", return_value=body):
out = check_embargoed_cves.fetch_flaw_state("https://u", "tok", "CVE-1")
assert out == {"results": [{"cve_id": "CVE-1", "embargoed": False}]}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
from pathlib import Path
from unittest import mock

import check_fbc_opt_in
import pytest
import requests
import tekton

import release_service_utils.helpers.tekton
import release_service_utils.tasks.internal.check_fbc_opt_in as check_fbc_opt_in


def _write_service_account(
Expand Down Expand Up @@ -45,18 +46,24 @@ def test_parse_container_images_invalid(raw: str) -> None:

def test_get_fbc_opt_in_true_false_and_missing() -> None:
"""Only explicit `fbc_opt_in: true` maps to `True`."""
with mock.patch("http_client.get_text", return_value='{"fbc_opt_in": true}'):
with mock.patch(
"release_service_utils.helpers.http_client.get_text",
return_value='{"fbc_opt_in": true}',
):
assert check_fbc_opt_in.get_fbc_opt_in("https://p", "r.io/repo/i:1", None) is True
with mock.patch("http_client.get_text", return_value='{"fbc_opt_in": false}'):
with mock.patch(
"release_service_utils.helpers.http_client.get_text",
return_value='{"fbc_opt_in": false}',
):
assert check_fbc_opt_in.get_fbc_opt_in("https://p", "r.io/repo/i:1", None) is False
with mock.patch("http_client.get_text", return_value="{}"):
with mock.patch("release_service_utils.helpers.http_client.get_text", return_value="{}"):
assert check_fbc_opt_in.get_fbc_opt_in("https://p", "r.io/repo/i:1", None) is False


def test_get_fbc_opt_in_http_error_returns_false() -> None:
"""HTTP exceptions are treated as opt-out."""
with mock.patch(
"http_client.get_text",
"release_service_utils.helpers.http_client.get_text",
side_effect=requests.HTTPError("boom", response=mock.MagicMock()),
):
assert check_fbc_opt_in.get_fbc_opt_in("https://p", "r.io/repo/i:1", None) is False
Expand Down Expand Up @@ -90,15 +97,20 @@ def test_run_check_wraps_service_account_errors(tmp_path: Path) -> None:
"""Missing principal/keytab files become `CheckStepError` with mount context."""
cfg = tmp_path / "cfg"
_write_krb5(cfg)
with pytest.raises(tekton.CheckStepError, match="mounted IIB service account"):
with pytest.raises(
release_service_utils.helpers.tekton.CheckStepError,
match="mounted IIB service account",
):
check_fbc_opt_in.run_check(["r/repo/i:1"], "https://pyxis/v1", tmp_path / "sa", cfg)


def test_run_check_wraps_krb5_errors(tmp_path: Path) -> None:
"""Missing `krb5.conf` becomes `CheckStepError` with Kerberos context."""
sa = tmp_path / "sa"
_write_service_account(sa)
with pytest.raises(tekton.CheckStepError, match="Kerberos configuration"):
with pytest.raises(
release_service_utils.helpers.tekton.CheckStepError, match="Kerberos configuration"
):
check_fbc_opt_in.run_check(["r/repo/i:1"], "https://pyxis/v1", sa, tmp_path / "cfg")


Expand All @@ -112,7 +124,9 @@ def test_run_check_wraps_kinit_error(tmp_path: Path) -> None:
def _fail_kinit(*_a: object, **_k: object) -> None:
raise subprocess.CalledProcessError(1, "kinit")

with pytest.raises(tekton.CheckStepError, match="logging in with Kerberos"):
with pytest.raises(
release_service_utils.helpers.tekton.CheckStepError, match="logging in with Kerberos"
):
check_fbc_opt_in.run_check(
["r/repo/i:1"],
"https://pyxis/v1",
Expand Down
14 changes: 13 additions & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading