Skip to content

Commit e9f931f

Browse files
committed
Add Client.create_repository() [RHELDST-22483]
`Client` is now capable of creating a repository on pulp server. The repository is initialized with proper `Importer` in order to enable sync/upload of content to new repositories. Another notable changes: * 409 status is not retried in requests as it is used as indicator of existing repository. * Added create_repository() method to FakeClient as well. * Small changes in the repository.yaml schema as some of the fields don't have to be necessarily initialized when creating repo but can be updated later. * Added `Importer` pulp object mapping class and related subclasses that encapsulate minimal required fields for this object with valid defaults. * added `"importers": True` to search options for repos * fixed converter of `notes.signatures` on `Repository` - empty string is now converted to empty list, not to list with empty string
1 parent dfe6dd8 commit e9f931f

File tree

22 files changed

+436
-30
lines changed

22 files changed

+436
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- n/a
10+
- Added `Client.create_repository()` method
1111

1212
## [2.38.1] - 2023-12-14
1313

docs/api/files.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Repository
1414
.. autoclass:: pubtools.pulplib.FileSyncOptions()
1515
:members:
1616

17+
.. autoclass:: pubtools.pulplib.FileImporter()
18+
:members:
1719

1820
Units
1921
-----

docs/api/pulpcore.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ Repository
2727
.. autoclass:: pubtools.pulplib.SyncOptions()
2828
:members:
2929

30+
.. autoclass:: pubtools.pulplib.Importer()
31+
:members:
3032

3133
Units
3234
-----

docs/api/yum.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Repository
1414
.. autoclass:: pubtools.pulplib.YumSyncOptions()
1515
:members:
1616

17+
.. autoclass:: pubtools.pulplib.YumImporter()
18+
:members:
1719

1820
Units: RPM
1921
----------

pubtools/pulplib/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,8 @@
3131
Task,
3232
MaintenanceReport,
3333
MaintenanceEntry,
34+
Importer,
35+
YumImporter,
36+
FileImporter,
3437
)
3538
from ._impl.fake import FakeController

pubtools/pulplib/_impl/client/client.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ def search_repository(self, criteria=None):
278278
Each page will contain a collection of
279279
:class:`~pubtools.pulplib.Repository` objects.
280280
"""
281-
search_options = {"distributors": True}
281+
search_options = {"distributors": True, "importers": True}
282282
return self._search(
283283
Repository, "repositories", criteria=criteria, search_options=search_options
284284
)
@@ -1054,3 +1054,80 @@ def _do_sync(self, repo_id, sync_options):
10541054
return self._task_executor.submit(
10551055
self._do_request, method="POST", url=url, json=body
10561056
)
1057+
1058+
def create_repository(self, repo):
1059+
"""Create a repository with initial data provided in the
1060+
argument. Importer for repository is automatically associated
1061+
if available. If repository already exists, warning is logged.
1062+
After repository has been successfully created or if repository already exists,
1063+
it is checked if expected and current repository configuration values are
1064+
correct.
1065+
1066+
Args:
1067+
repo (:class:`~pubtools.pulplib.Repository`)
1068+
A repository object used for creation.
1069+
1070+
Returns:
1071+
Future[Repository]
1072+
A future which is resolved with a value of ``Repository`` once the
1073+
repository has been created.
1074+
1075+
.. versionadded:: 2.39.0
1076+
"""
1077+
url = os.path.join(self._url, "pulp/api/v2/repositories/")
1078+
1079+
body = repo._to_data()
1080+
repo_id = body["id"]
1081+
1082+
importer = body.pop("importers", [])
1083+
body["importer_type_id"] = importer[0]["importer_type_id"] if importer else None
1084+
body["importer_config"] = importer[0]["config"] if importer else None
1085+
1086+
def log_existing_repo(exception):
1087+
if (
1088+
getattr(exception, "response", None) is not None
1089+
and exception.response.status_code == 409
1090+
):
1091+
LOG.warning("Repository %s already exists", repo_id)
1092+
return None
1093+
1094+
raise exception
1095+
1096+
def get_and_check_repo(_):
1097+
repo_on_server = self.get_repository(repo_id)
1098+
try:
1099+
assert (
1100+
repo_on_server.result() == repo
1101+
), "Repo exists on server with unexpected values"
1102+
except AssertionError:
1103+
if importer:
1104+
for attr in ["type_id", "config"]:
1105+
expected = getattr(repo.importer, attr)
1106+
current = getattr(repo_on_server.importer, attr)
1107+
if expected != current:
1108+
LOG.error(
1109+
"Repository %s contains wrong importer %s\n"
1110+
"\t expected: %s\n"
1111+
"\t current: %s\n",
1112+
repo_id,
1113+
attr,
1114+
expected,
1115+
current,
1116+
)
1117+
LOG.exception(
1118+
"Repository %s exists on server and contains unexpected values",
1119+
repo_id,
1120+
)
1121+
raise
1122+
1123+
return repo_on_server
1124+
1125+
LOG.debug("Creating repository %s", repo_id)
1126+
out = self._request_executor.submit(
1127+
self._do_request, method="POST", url=url, json=body
1128+
)
1129+
1130+
out = f_map(out, error_fn=log_existing_repo)
1131+
out = f_flat_map(out, get_and_check_repo)
1132+
1133+
return f_proxy(out)

pubtools/pulplib/_impl/client/retry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def should_retry(self, attempt, future):
2929

3030
exception = future.exception()
3131
if exception and getattr(exception, "response", None) is not None:
32-
# if returned status code is 404, never retry on that
33-
if exception.response.status_code == 404:
32+
# if returned status code is 404 or 409, never retry on that
33+
if exception.response.status_code in (404, 409):
3434
return False
3535

3636
if exception and retry:

pubtools/pulplib/_impl/fake/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,12 @@ def _do_sync(self, repo_id, sync_config): # pylint:disable = unused-argument
651651
self._state.sync_history.append(Sync(repo_f.result(), [task], sync_config))
652652

653653
return f_return([task])
654+
655+
def create_repository(self, repo):
656+
with self._state.lock:
657+
if repo.id not in [
658+
existing_repo.id for existing_repo in self._state.repositories
659+
]:
660+
self._state.repositories.append(repo)
661+
662+
return self.get_repository(repo.id)

pubtools/pulplib/_impl/model/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
FileSyncOptions,
1010
ContainerSyncOptions,
1111
YumSyncOptions,
12+
Importer,
13+
FileImporter,
14+
YumImporter,
1215
)
1316
from .unit import (
1417
Unit,

pubtools/pulplib/_impl/model/common.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,15 @@ def _delete(self, resource_type, resource_id):
234234
delete_f = client._delete_resource(resource_type, resource_id)
235235
delete_f = f_map(delete_f, self.__detach)
236236
return f_proxy(delete_f)
237+
238+
239+
def schemaless_init(cls, data):
240+
# Construct and return an instance of (attrs-using) cls from
241+
# pulp data, where data in pulp has no schema at all (and hence
242+
# every field could possibly be missing).
243+
kwargs = {}
244+
for key in [fld.name for fld in attr.fields(cls)]:
245+
if key in data:
246+
kwargs[key] = data[key]
247+
248+
return cls(**kwargs)
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .base import Repository, PublishOptions, SyncOptions
1+
from .base import Repository, PublishOptions, SyncOptions, Importer
22
from .container import ContainerImageRepository, ContainerSyncOptions
3-
from .yum import YumRepository, YumSyncOptions
4-
from .file import FileRepository, FileSyncOptions
3+
from .yum import YumRepository, YumSyncOptions, YumImporter
4+
from .file import FileRepository, FileSyncOptions, FileImporter

pubtools/pulplib/_impl/model/repository/base.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,19 @@
88
from frozenlist2 import frozenlist
99
from more_executors.futures import f_proxy, f_map, f_flat_map
1010

11+
from frozendict.core import frozendict # pylint: disable=no-name-in-module
1112
from .repo_lock import RepoLock
1213
from ..attr import pulp_attrib, PULP2_FIELD, PULP2_MUTABLE
1314
from ..common import PulpObject, Deletable, DetachedException
14-
from ..convert import frozenlist_or_none_converter
15+
from ..convert import frozenlist_or_none_converter, frozendict_or_none_converter
1516
from ..distributor import Distributor
1617
from ...criteria import Criteria, Matcher
1718
from ...schema import load_schema
1819
from ... import compat_attr as attr
1920
from ...hooks import pm
2021
from ...util import dict_put, lookup, ABSENT
2122

23+
from ..common import schemaless_init
2224

2325
LOG = logging.getLogger("pubtools.pulplib")
2426

@@ -35,6 +37,44 @@ def decorate(klass):
3537
return decorate
3638

3739

40+
@attr.s(kw_only=True, frozen=True)
41+
class Importer(PulpObject):
42+
"""
43+
Importer is a pulp object that needs to be associated with repository
44+
in order to successfully sync or upload content to it.
45+
"""
46+
47+
type_id = pulp_attrib(default=None, type=str, pulp_field="importer_type_id")
48+
"""
49+
Type id of the importer.
50+
"""
51+
config = pulp_attrib(
52+
default=attr.Factory(frozendict),
53+
type=frozendict,
54+
converter=frozendict_or_none_converter,
55+
pulp_field="config",
56+
)
57+
"""
58+
Configuration dictionary of the importer.
59+
"""
60+
61+
@classmethod
62+
def _from_data(cls, data):
63+
# Convert from raw list/dict as provided in Pulp responses into model.
64+
if isinstance(data, list):
65+
return cls._from_data(data[0]) if data else schemaless_init(cls, data)
66+
67+
return schemaless_init(cls, data)
68+
69+
def _to_data(self):
70+
return [
71+
{
72+
"importer_type_id": self.type_id,
73+
"config": self.config,
74+
}
75+
]
76+
77+
3878
@attr.s(kw_only=True, frozen=True)
3979
class PublishOptions(object):
4080
"""Options controlling a repository
@@ -249,7 +289,7 @@ class Repository(PulpObject, Deletable):
249289
default=attr.Factory(frozenlist),
250290
type=list,
251291
pulp_field="notes.signatures",
252-
pulp_py_converter=lambda sigs: sigs.split(","),
292+
pulp_py_converter=lambda sigs: sigs.split(",") if sigs else [],
253293
py_pulp_converter=",".join,
254294
converter=lambda keys: frozenlist([k.strip() for k in keys]),
255295
)
@@ -339,6 +379,19 @@ class Repository(PulpObject, Deletable):
339379
.. versionadded:: 2.37.0
340380
"""
341381

382+
importer = pulp_attrib(
383+
default=Importer(),
384+
type=Importer,
385+
pulp_field="importers",
386+
pulp_py_converter=Importer._from_data,
387+
py_pulp_converter=Importer._to_data,
388+
)
389+
"""
390+
An object of :class:`~pubtools.pulplib.Importer` that is associated with the repository.
391+
392+
.. versionadded:: 2.39.0
393+
"""
394+
342395
@distributors.validator
343396
def _check_repo_id(self, _, value):
344397
# checks if distributor's repository id is same as the repository it

pubtools/pulplib/_impl/model/repository/file.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from attr import validators
55
from frozenlist2 import frozenlist
66

7-
from .base import Repository, SyncOptions, repo_type
7+
from .base import Repository, SyncOptions, repo_type, Importer
88
from ...model.unit import FileUnit
99
from ..attr import pulp_attrib
1010
from ... import compat_attr as attr
@@ -13,6 +13,16 @@
1313
LOG = logging.getLogger("pubtools.pulplib")
1414

1515

16+
@attr.s(kw_only=True, frozen=True)
17+
class FileImporter(Importer):
18+
type_id = pulp_attrib(
19+
default="iso_importer", type=str, pulp_field="importer_type_id"
20+
)
21+
"""
22+
Specific importer_type_id for File repositories.
23+
"""
24+
25+
1626
@attr.s(kw_only=True, frozen=True)
1727
class FileSyncOptions(SyncOptions):
1828
"""Options controlling a file repository
@@ -49,6 +59,19 @@ class FileRepository(Repository):
4959
converter=frozenlist,
5060
)
5161

62+
importer = pulp_attrib(
63+
default=FileImporter(),
64+
type=FileImporter,
65+
pulp_field="importers",
66+
pulp_py_converter=FileImporter._from_data,
67+
py_pulp_converter=FileImporter._to_data,
68+
)
69+
"""
70+
An object of :class:`~pubtools.pulplib.FileImporter` that is associated with the repository.
71+
72+
.. versionadded:: 2.39.0
73+
"""
74+
5275
def upload_file(self, file_obj, relative_url=None, **kwargs):
5376
"""Upload a file to this repository.
5477

pubtools/pulplib/_impl/model/repository/yum.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,24 @@
33
from frozenlist2 import frozenlist
44

55
from more_executors.futures import f_map, f_proxy, f_return, f_zip, f_flat_map
6-
from .base import Repository, SyncOptions, repo_type
6+
from .base import Repository, SyncOptions, repo_type, Importer
77
from ..attr import pulp_attrib
88
from ..common import DetachedException
99
from ...model.unit import RpmUnit
1010
from ... import compat_attr as attr, comps
1111
from ...criteria import Criteria, Matcher
1212

1313

14+
@attr.s(kw_only=True, frozen=True)
15+
class YumImporter(Importer):
16+
type_id = pulp_attrib(
17+
default="yum_importer", type=str, pulp_field="importer_type_id"
18+
)
19+
"""
20+
Specific importer_type_id for Yum repositories.
21+
"""
22+
23+
1424
@attr.s(kw_only=True, frozen=True)
1525
class YumSyncOptions(SyncOptions):
1626
"""Options controlling a yum repository
@@ -103,6 +113,19 @@ class YumRepository(Repository):
103113
)
104114
"""Version of UBI config that should be used for population of this repository."""
105115

116+
importer = pulp_attrib(
117+
default=YumImporter(),
118+
type=YumImporter,
119+
pulp_field="importers",
120+
pulp_py_converter=YumImporter._from_data,
121+
py_pulp_converter=YumImporter._to_data,
122+
)
123+
"""
124+
An object of :class:`~pubtools.pulplib.YumImporter` that is associated with the repository.
125+
126+
.. versionadded:: 2.39.0
127+
"""
128+
106129
def get_binary_repository(self):
107130
"""Find and return the binary repository relating to this repository.
108131

pubtools/pulplib/_impl/model/unit/base.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -139,15 +139,3 @@ def _usermeta_from_kwargs(cls, **kwargs):
139139
)
140140

141141
return out
142-
143-
144-
def schemaless_init(cls, data):
145-
# Construct and return an instance of (attrs-using) cls from
146-
# pulp data, where data in pulp has no schema at all (and hence
147-
# every field could possibly be missing).
148-
kwargs = {}
149-
for key in [fld.name for fld in attr.fields(cls)]:
150-
if key in data:
151-
kwargs[key] = data[key]
152-
153-
return cls(**kwargs)

0 commit comments

Comments
 (0)