From 8e38aaabe5700009bfe1eb26e4e606593445344e Mon Sep 17 00:00:00 2001 From: Grant Gainey Date: Wed, 17 Jun 2026 09:34:30 -0400 Subject: [PATCH] DRAFT versioned-api support --- pulp-glue/src/pulp_glue/common/context.py | 14 ++++++++++---- pulp-glue/src/pulp_glue/common/openapi.py | 4 +++- src/pulp_cli/__init__.py | 2 ++ src/pulp_cli/config.py | 8 ++++++++ src/pulp_cli/generic.py | 3 +++ 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pulp-glue/src/pulp_glue/common/context.py b/pulp-glue/src/pulp_glue/common/context.py index be6f74e9c..01ed19bfb 100644 --- a/pulp-glue/src/pulp_glue/common/context.py +++ b/pulp-glue/src/pulp_glue/common/context.py @@ -287,7 +287,7 @@ class PulpContext: It is an abstraction layer for api access and output handling. Parameters: - api_root: The base url (excluding "api/v3/") to the servers api. + api_root: The base url (excluding "api/{version}/") to the servers api. api_kwargs: Extra arguments to pass to the wrapped `OpenAPI` object. background_tasks: Whether to wait for tasks. If `True`, all tasks triggered will immediately raise `PulpNoWait`. @@ -297,6 +297,7 @@ class PulpContext: Where possible, instead of failing, the requested result will be faked. This implies `dry_run=True` on the `api_kwargs`. verify_ssl: A boolean or a path to the CA bundle. + api-version: Version of the Pulp API to talk to (e.g., "v3") """ def echo(self, message: str, nl: bool = True, err: bool = False) -> None: @@ -328,8 +329,10 @@ def __init__( verify_ssl: bool | str | None = None, verify: bool | str | None = None, # Deprecated chunk_size: int | None = None, + api_version: str | None = "v3", ) -> None: self._api: OpenAPI | None = None + self._api_version = api_version self._api_root: str = api_root self._api_kwargs = api_kwargs self.verify_ssl = verify_ssl @@ -353,6 +356,7 @@ def __init__( self.fake_mode: bool = fake_mode if self.fake_mode: self._api_kwargs["dry_run"] = True + self._api_kwargs["api_version"] = self._api_version self.chunk_size = chunk_size @classmethod @@ -435,6 +439,7 @@ def from_config(cls, config: dict[str, t.Any]) -> "t.Self": api_root=config.get("api_root", "/pulp/"), domain=config.get("domain", "default"), verify_ssl=config.get("verify_ssl", True), + api_version=config.get("api_version", "v3"), api_kwargs=api_kwargs, ) @@ -453,8 +458,8 @@ def domain_enabled(self) -> bool: @property def api_path(self) -> str: if self.domain_enabled: - return self._api_root + self.pulp_domain + "/api/v3/" - return self._api_root + "api/v3/" + return f"{self._api_root}{self.pulp_domain}/api/{self._api_version}/" + return f"{self._api_root}api/{self._api_version}/" @property def api(self) -> OpenAPI: @@ -481,7 +486,7 @@ def api(self) -> OpenAPI: ) try: self._api = OpenAPI( - doc_path=f"{self._api_root}api/v3/docs/api.json", + doc_path=f"{self._api_root}api/{self._api_version}/docs/api.json", verify_ssl=self.verify_ssl, patch_api_hook=_patch_api_hook, **self._api_kwargs, @@ -535,6 +540,7 @@ def call( if "pulp_domain" in self.api.param_spec(operation_id, "path", required=True): parameters["pulp_domain"] = self.pulp_domain parameters = preprocess_payload(parameters) + if body is not None: body = preprocess_payload(body) try: diff --git a/pulp-glue/src/pulp_glue/common/openapi.py b/pulp-glue/src/pulp_glue/common/openapi.py index 77cad6c80..a92946041 100644 --- a/pulp-glue/src/pulp_glue/common/openapi.py +++ b/pulp-glue/src/pulp_glue/common/openapi.py @@ -99,6 +99,7 @@ class OpenAPI: cid: Correlation ID to send with all requests. validate_certs: DEPRECATED use verify_ssl instead. safe_calls_only: DEPRECATED use dry_run instead. + api_version: Version of the Pulp APi to contact (e.g., "v3") """ _api_spec: oas.OpenAPISpec @@ -121,6 +122,7 @@ def __init__( validate_certs: bool | None = None, safe_calls_only: bool | None = None, patch_api_hook: t.Callable[[t.Any], t.Any] | None = None, + api_version: str | None = "v3", ): if validate_certs is not None: warnings.warn( @@ -167,6 +169,7 @@ def __init__( self._oauth2_expires: datetime = datetime.now() self._patch_api_hook: t.Callable[[t.Any], t.Any] = patch_api_hook or (lambda data: data) + self._api_version = api_version self.load_api(refresh_cache=refresh_cache) def _setup_session(self) -> None: @@ -728,7 +731,6 @@ def call( rel_url = path for name, value in rendered_parameters["path"].items(): rel_url = path.replace("{" + name + "}", value) - query_params = rendered_parameters["query"] url = urljoin(self._base_url, rel_url) diff --git a/src/pulp_cli/__init__.py b/src/pulp_cli/__init__.py index d5fc022a4..e1401b529 100644 --- a/src/pulp_cli/__init__.py +++ b/src/pulp_cli/__init__.py @@ -225,6 +225,7 @@ def main( dry_run: bool, timeout: int, cid: str, + api_version: str, ) -> None: if verbose: logging.basicConfig(level=logging.DEBUG + 4 - verbose, format="%(message)s") @@ -252,6 +253,7 @@ def main( oauth2_client_id=client_id, oauth2_client_secret=client_secret, chunk_size=chunk_size, + api_version=api_version, ) diff --git a/src/pulp_cli/config.py b/src/pulp_cli/config.py index a465e7847..e0b587fee 100644 --- a/src/pulp_cli/config.py +++ b/src/pulp_cli/config.py @@ -37,6 +37,7 @@ str(Path(click.utils.get_app_dir("pulp"), "cli.toml")), ] FORMAT_CHOICES = list(REGISTERED_OUTPUT_FORMATTERS.keys()) +VERSIONS = ["v3", "v4", "v5"] REQUIRED_SETTINGS = { "base_url", "api_root", @@ -57,6 +58,7 @@ "key", "chunk_size", "plugins", + "api_version", } SETTINGS = REQUIRED_SETTINGS | OPTIONAL_SETTINGS @@ -137,6 +139,12 @@ def headers_callback( count=True, help=_("Increase verbosity; explain api calls as they are made"), ), + click.option( + "--api-version", + type=click.Choice(VERSIONS, case_sensitive=True), + default="v3", + help=_("API version to talk to (e.g., 'v3')"), + ), ] diff --git a/src/pulp_cli/generic.py b/src/pulp_cli/generic.py index 402e7155d..e438f7352 100644 --- a/src/pulp_cli/generic.py +++ b/src/pulp_cli/generic.py @@ -143,6 +143,7 @@ class PulpCLIContext(PulpContext): Parameters: api_root: The base url (excluding "api/v3/") to the server's api. + api_version: The version of Pulp's API to talk to (e.g. "v3"). api_kwargs: Extra arguments to pass to the wrapped `OpenAPI` object. background_tasks: Whether to wait for tasks. If `True`, all tasks triggered will immediately raise `PulpNoWait`. @@ -166,6 +167,7 @@ def __init__( oauth2_client_id: str | None = None, oauth2_client_secret: str | None = None, chunk_size: int | None = None, + api_version: str | None = "v3", ) -> None: self.username = username self.password = password @@ -184,6 +186,7 @@ def __init__( timeout=timeout, domain=domain, chunk_size=chunk_size, + api_version=api_version, ) self.format = format