lockss-turtles 0.6.0.dev24__tar.gz → 0.6.0.dev25__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/PKG-INFO +4 -3
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/README.rst +1 -1
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/pyproject.toml +1 -1
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/__init__.py +1 -1
- lockss_turtles-0.6.0.dev25/src/lockss/turtles/app.py +598 -0
- lockss_turtles-0.6.0.dev25/src/lockss/turtles/cli.py +653 -0
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/plugin.py +2 -3
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/plugin_registry.py +5 -6
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/plugin_set.py +24 -24
- lockss_turtles-0.6.0.dev25/src/lockss/turtles/plugin_signing_credentials.py +80 -0
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/util.py +2 -3
- lockss_turtles-0.6.0.dev25/tests/unittest/lockss/turtles/__init__.py +106 -0
- lockss_turtles-0.6.0.dev25/tests/unittest/lockss/turtles/test_plugin_registry.py +411 -0
- lockss_turtles-0.6.0.dev25/tests/unittest/lockss/turtles/test_plugin_set.py +272 -0
- lockss_turtles-0.6.0.dev24/src/lockss/turtles/app.py +0 -305
- lockss_turtles-0.6.0.dev24/src/lockss/turtles/cli.py +0 -382
- lockss_turtles-0.6.0.dev24/tests/unittest/lockss/turtles/__init__.py +0 -65
- lockss_turtles-0.6.0.dev24/tests/unittest/lockss/turtles/test_plugin_set.py +0 -77
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/CHANGELOG.rst +0 -0
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/LICENSE +0 -0
- {lockss_turtles-0.6.0.dev24 → lockss_turtles-0.6.0.dev25}/src/lockss/turtles/__main__.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: lockss-turtles
|
|
3
|
-
Version: 0.6.0.
|
|
3
|
+
Version: 0.6.0.dev25
|
|
4
4
|
Summary: Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries
|
|
5
5
|
License: BSD-3-Clause
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Author: Thib Guicherd-Callin
|
|
7
8
|
Author-email: thib@cs.stanford.edu
|
|
8
9
|
Maintainer: Thib Guicherd-Callin
|
|
@@ -34,7 +35,7 @@ Description-Content-Type: text/x-rst
|
|
|
34
35
|
Turtles
|
|
35
36
|
=======
|
|
36
37
|
|
|
37
|
-
.. |RELEASE| replace:: 0.6.0-
|
|
38
|
+
.. |RELEASE| replace:: 0.6.0-dev25 NOT YET RELEASED
|
|
38
39
|
.. |RELEASE_DATE| replace:: NOT YET RELEASED
|
|
39
40
|
.. |TURTLES| replace:: **Turtles**
|
|
40
41
|
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
[project]
|
|
30
30
|
name = "lockss-turtles"
|
|
31
|
-
version = "0.6.0-
|
|
31
|
+
version = "0.6.0-dev25" # Always change in __init__.py, and at release time in README.rst and CHANGELOG.rst
|
|
32
32
|
description = "Library and command line tool to manage LOCKSS plugin sets and LOCKSS plugin registries"
|
|
33
33
|
license = { text = "BSD-3-Clause" }
|
|
34
34
|
readme = "README.rst"
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2000-2025, Board of Trustees of Leland Stanford Jr. University
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without
|
|
6
|
+
# modification, are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# 3. Neither the name of the copyright holder nor the names of its contributors
|
|
16
|
+
# may be used to endorse or promote products derived from this software without
|
|
17
|
+
# specific prior written permission.
|
|
18
|
+
#
|
|
19
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
22
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
23
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
24
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
25
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
26
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
27
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
28
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
29
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
30
|
+
|
|
31
|
+
"""
|
|
32
|
+
Module to represent Turtles operations.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Remove in Python 3.14; see https://stackoverflow.com/a/33533514
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
# Remove in Python 3.11; see https://docs.python.org/3.11/library/exceptions.html#exception-groups
|
|
39
|
+
from exceptiongroup import ExceptionGroup
|
|
40
|
+
|
|
41
|
+
from collections.abc import Callable, Iterable
|
|
42
|
+
from pathlib import Path
|
|
43
|
+
from typing import ClassVar, Optional, Union
|
|
44
|
+
|
|
45
|
+
from lockss.pybasic.fileutil import path
|
|
46
|
+
from pydantic import ValidationError
|
|
47
|
+
import xdg
|
|
48
|
+
import yaml
|
|
49
|
+
|
|
50
|
+
from .plugin import Plugin, PluginIdentifier
|
|
51
|
+
from .plugin_registry import PluginRegistry, PluginRegistryCatalog, PluginRegistryCatalogKind, PluginRegistryIdentifier, PluginRegistryKind, PluginRegistryLayerIdentifier
|
|
52
|
+
from .plugin_set import PluginSet, PluginSetCatalog, PluginSetCatalogKind, PluginSetKind
|
|
53
|
+
from .plugin_signing_credentials import PluginSigningCredentials, PluginSigningCredentialsKind
|
|
54
|
+
from .util import PathOrStr
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
#: Type alias for the result of a single plugin building operation.
|
|
58
|
+
#: First item (index 0): identifier of the plugin set that had the given plugin.
|
|
59
|
+
#: Second item (index 1): plugin JAR file path (or None if not built).
|
|
60
|
+
#: Third item (index 2): plugin object (or None if not built).
|
|
61
|
+
BuildPluginResult = tuple[str, Optional[Path], Optional[Plugin]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
#: Type alias for the result of a single plugin deployment operation to a given
|
|
65
|
+
#: plugin registry layer.
|
|
66
|
+
#: First item (index 0):
|
|
67
|
+
#: Second item (index 1):
|
|
68
|
+
#: Third item (index 2): deployed JAR file path (or None if not deployed).
|
|
69
|
+
#: Fourth item (index 3): plugin object (or None if not deployed).
|
|
70
|
+
DeployPluginResult = tuple[PluginRegistryIdentifier, PluginRegistryLayerIdentifier, Optional[Path], Optional[Plugin]]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Turtles(object):
|
|
74
|
+
"""
|
|
75
|
+
A Turtles command object, which can be used to execute Turtles operations.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
#: The name of a Turtles configuration directory.
|
|
79
|
+
CONFIG_DIR_NAME: ClassVar[str] = 'lockss-turtles'
|
|
80
|
+
|
|
81
|
+
#: The Turtles configuration directory under ``$XDG_CONFIG_HOME`` (by
|
|
82
|
+
# default ``$HOME/.config``, which is typically ``/home/$USER/.config``).
|
|
83
|
+
XDG_CONFIG_DIR: ClassVar[Path] = Path(xdg.xdg_config_home(), CONFIG_DIR_NAME)
|
|
84
|
+
|
|
85
|
+
#: The Turtles configuration directory under ``/etc``.
|
|
86
|
+
ETC_CONFIG_DIR: ClassVar[Path] = Path('/etc', CONFIG_DIR_NAME)
|
|
87
|
+
|
|
88
|
+
#: The Turtles configuration directory under ``/usr/local/share``.
|
|
89
|
+
USR_CONFIG_DIR: ClassVar[Path] = Path('/usr/local/share', CONFIG_DIR_NAME)
|
|
90
|
+
|
|
91
|
+
#: The Turtles configuration directories in order of preference:
|
|
92
|
+
#: ``XDG_CONFIG_DIR``, ``ETC_CONFIG_DIR``, ``USR_CONFIG_DIR``
|
|
93
|
+
CONFIG_DIRS: ClassVar[tuple[Path, ...]] = (XDG_CONFIG_DIR, ETC_CONFIG_DIR, USR_CONFIG_DIR)
|
|
94
|
+
|
|
95
|
+
#: The default plugin registry catalog file name.
|
|
96
|
+
PLUGIN_REGISTRY_CATALOG: ClassVar[str] = 'plugin-registry-catalog.yaml'
|
|
97
|
+
|
|
98
|
+
#: The default plugin set catalog file name.
|
|
99
|
+
PLUGIN_SET_CATALOG: ClassVar[str] = 'plugin-set-catalog.yaml'
|
|
100
|
+
|
|
101
|
+
#: The default plugin signing credentials file name.
|
|
102
|
+
PLUGIN_SIGNING_CREDENTIALS: ClassVar[str] = 'plugin-signing-credentials.yaml'
|
|
103
|
+
|
|
104
|
+
def __init__(self) -> None:
|
|
105
|
+
"""
|
|
106
|
+
Constructor.
|
|
107
|
+
"""
|
|
108
|
+
super().__init__()
|
|
109
|
+
self._plugin_signing_password_callable: Optional[Callable[[], str]] = None
|
|
110
|
+
self._plugin_registries: list[PluginRegistry] = list()
|
|
111
|
+
self._plugin_registry_catalogs: list[PluginRegistryCatalog] = list()
|
|
112
|
+
self._plugin_set_catalogs: list[PluginSetCatalog] = list()
|
|
113
|
+
self._plugin_sets: list[PluginSet] = list()
|
|
114
|
+
self._plugin_signing_credentials: Optional[PluginSigningCredentials] = None
|
|
115
|
+
|
|
116
|
+
def build_plugin(self,
|
|
117
|
+
plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]]) -> dict[str, BuildPluginResult]:
|
|
118
|
+
"""
|
|
119
|
+
Builds zero or more plugins.
|
|
120
|
+
|
|
121
|
+
:param plugin_id_or_plugin_ids: Either one plugin identifier, or a list
|
|
122
|
+
of plugin identifiers.
|
|
123
|
+
:type plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]]
|
|
124
|
+
:return: A mapping from plugin identifier to build plugin result; if no
|
|
125
|
+
plugin identifiers were given, the result is an empty mapping.
|
|
126
|
+
:rtype: dict[str, BuildPluginResult]
|
|
127
|
+
:raises Exception: If a given plugin identifier is not found in any
|
|
128
|
+
loaded plugin set.
|
|
129
|
+
"""
|
|
130
|
+
plugin_ids: list[PluginIdentifier] = plugin_id_or_plugin_ids if isinstance(plugin_id_or_plugin_ids, list) else [plugin_id_or_plugin_ids]
|
|
131
|
+
return {plugin_id: self._build_one_plugin(plugin_id) for plugin_id in plugin_ids}
|
|
132
|
+
|
|
133
|
+
def deploy_plugin(self,
|
|
134
|
+
src_path_or_src_paths: Union[Path, list[Path]],
|
|
135
|
+
layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]],
|
|
136
|
+
interactive: bool=False) -> dict[tuple[Path, PluginIdentifier], list[DeployPluginResult]]:
|
|
137
|
+
"""
|
|
138
|
+
Deploys zero or more plugins.
|
|
139
|
+
|
|
140
|
+
:param src_path_or_src_paths: Either one signed JAR file paths or a list
|
|
141
|
+
of signed JAR file paths.
|
|
142
|
+
:type src_path_or_src_paths: Union[Path, list[Path]]
|
|
143
|
+
:param layer_id_or_layer_ids: Either one plugin registry layer
|
|
144
|
+
identifier or a list of plugin registry
|
|
145
|
+
layer identifiers.
|
|
146
|
+
:type layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]]
|
|
147
|
+
:param interactive: Whether interactive prompts are allowed (default
|
|
148
|
+
False).
|
|
149
|
+
:type interactive: bool
|
|
150
|
+
:return: A mapping from tuples of signed JAR file path and corresponding
|
|
151
|
+
plugin identifier to a list of build deployment results (one
|
|
152
|
+
per plugin registry layer); if no signed JAR file paths were
|
|
153
|
+
given, the result is an empty mapping.
|
|
154
|
+
:rtype: dict[tuple[Path, PluginIdentifier], list[DeployPluginResult]]
|
|
155
|
+
:raises Exception: If a given plugin is not declared in any loaded
|
|
156
|
+
plugin registry.
|
|
157
|
+
"""
|
|
158
|
+
src_paths: list[Path] = src_path_or_src_paths if isinstance(src_path_or_src_paths, list) else [src_path_or_src_paths]
|
|
159
|
+
layer_ids: list[PluginRegistryLayerIdentifier] = layer_id_or_layer_ids if isinstance(layer_id_or_layer_ids, list) else [layer_id_or_layer_ids]
|
|
160
|
+
plugin_ids = [Plugin.id_from_jar(src_path) for src_path in src_paths] # FIXME: should go down to _deploy_one_plugin?
|
|
161
|
+
return {(src_path, plugin_id): self._deploy_one_plugin(src_path,
|
|
162
|
+
plugin_id,
|
|
163
|
+
layer_ids,
|
|
164
|
+
interactive=interactive) for src_path, plugin_id in zip(src_paths, plugin_ids)}
|
|
165
|
+
|
|
166
|
+
def load_plugin_registries(self,
|
|
167
|
+
plugin_registry_path_or_str: PathOrStr) -> Turtles:
|
|
168
|
+
"""
|
|
169
|
+
Processes the given YAML file, loading all plugin registry definitions
|
|
170
|
+
it contains, ignoring other YAML objects.
|
|
171
|
+
|
|
172
|
+
:param plugin_registry_path_or_str: A file path (or string).
|
|
173
|
+
:type plugin_registry_path_or_str: PathOrStr
|
|
174
|
+
:return: This Turtles object (for chaining).
|
|
175
|
+
:rtype: Turtles
|
|
176
|
+
:raises ExceptionGroup: If one or more errors occur while loading plugin
|
|
177
|
+
registry definitions.
|
|
178
|
+
:raises ValueError: If the given file has already been processed or if
|
|
179
|
+
it contains no plugin registry definitions.
|
|
180
|
+
"""
|
|
181
|
+
plugin_registry_path = path(plugin_registry_path_or_str)
|
|
182
|
+
if plugin_registry_path in map(lambda pr: pr.get_root(), self._plugin_registries):
|
|
183
|
+
raise ValueError(f'Plugin registries already loaded from: {plugin_registry_path!s}')
|
|
184
|
+
errs, at_least_one = [], False
|
|
185
|
+
with plugin_registry_path.open('r') as fpr:
|
|
186
|
+
for yaml_obj in yaml.safe_load_all(fpr):
|
|
187
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryKind.__args__:
|
|
188
|
+
try:
|
|
189
|
+
plugin_registry = PluginRegistry(**yaml_obj).initialize(plugin_registry_path.parent)
|
|
190
|
+
self._plugin_registries.append(plugin_registry)
|
|
191
|
+
at_least_one = True
|
|
192
|
+
except ValidationError as ve:
|
|
193
|
+
errs.append(ve)
|
|
194
|
+
if errs:
|
|
195
|
+
raise ExceptionGroup(f'Errors while loading plugin registries from: {plugin_registry_path!s}', errs)
|
|
196
|
+
if not at_least_one:
|
|
197
|
+
raise ValueError(f'No plugin registries found in: {plugin_registry_path!s}')
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
def load_plugin_registry_catalogs(self,
|
|
201
|
+
plugin_registry_catalog_path_or_str: PathOrStr) -> Turtles:
|
|
202
|
+
"""
|
|
203
|
+
Processes the given YAML file, loading all plugin registry catalog
|
|
204
|
+
definitions it contains and in turn all plugin registry definitions they
|
|
205
|
+
reference, ignoring other YAML objects.
|
|
206
|
+
|
|
207
|
+
:param plugin_registry_catalog_path_or_str: A file path (or string).
|
|
208
|
+
:type plugin_registry_catalog_path_or_str: PathOrStr
|
|
209
|
+
:return: This Turtles object (for chaining).
|
|
210
|
+
:rtype: Turtles
|
|
211
|
+
:raises ExceptionGroup: If one or more errors occur while loading plugin
|
|
212
|
+
registry catalog definitions or the plugin
|
|
213
|
+
registry definitions they reference.
|
|
214
|
+
:raises ValueError: If the given file has already been processed or if
|
|
215
|
+
it contains no plugin registry catalog definitions.
|
|
216
|
+
"""
|
|
217
|
+
plugin_registry_catalog_path = path(plugin_registry_catalog_path_or_str)
|
|
218
|
+
if plugin_registry_catalog_path in map(lambda prc: prc.get_root(), self._plugin_registry_catalogs):
|
|
219
|
+
raise ValueError(f'Plugin registry catalogs already loaded from: {plugin_registry_catalog_path!s}')
|
|
220
|
+
errs, at_least_one = [], False
|
|
221
|
+
with plugin_registry_catalog_path.open('r') as fprc:
|
|
222
|
+
for yaml_obj in yaml.safe_load_all(fprc):
|
|
223
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginRegistryCatalogKind.__args__:
|
|
224
|
+
try:
|
|
225
|
+
plugin_registry_catalog = PluginRegistryCatalog(**yaml_obj).initialize(plugin_registry_catalog_path.parent)
|
|
226
|
+
self._plugin_registry_catalogs.append(plugin_registry_catalog)
|
|
227
|
+
at_least_one = True
|
|
228
|
+
for plugin_registry_file in plugin_registry_catalog.get_plugin_registry_files():
|
|
229
|
+
try:
|
|
230
|
+
self.load_plugin_registries(plugin_registry_catalog_path.joinpath(plugin_registry_file))
|
|
231
|
+
except ValueError as ve:
|
|
232
|
+
errs.append(ve)
|
|
233
|
+
except ExceptionGroup as eg:
|
|
234
|
+
errs.extend(eg.exceptions)
|
|
235
|
+
except ValidationError as ve:
|
|
236
|
+
errs.append(ve)
|
|
237
|
+
if errs:
|
|
238
|
+
raise ExceptionGroup(f'Errors while loading plugin registry catalogs from: {plugin_registry_catalog_path!s}', errs)
|
|
239
|
+
if not at_least_one:
|
|
240
|
+
raise ValueError(f'No plugin registry catalogs found in: {plugin_registry_catalog_path!s}')
|
|
241
|
+
return self
|
|
242
|
+
|
|
243
|
+
def load_plugin_set_catalogs(self,
|
|
244
|
+
plugin_set_catalog_path_or_str: PathOrStr) -> Turtles:
|
|
245
|
+
"""
|
|
246
|
+
Processes the given YAML file, loading all plugin set catalog
|
|
247
|
+
definitions it contains and in turn all plugin set definitions they
|
|
248
|
+
reference, ignoring other YAML objects.
|
|
249
|
+
|
|
250
|
+
:param plugin_set_catalog_path_or_str: A file path (or string).
|
|
251
|
+
:type plugin_set_catalog_path_or_str: PathOrStr
|
|
252
|
+
:return: This Turtles object (for chaining).
|
|
253
|
+
:rtype: Turtles
|
|
254
|
+
:raises ExceptionGroup: If one or more errors occur while loading plugin
|
|
255
|
+
set catalog definitions or the plugin set
|
|
256
|
+
definitions they reference.
|
|
257
|
+
:raises ValueError: If the given file has already been processed or if
|
|
258
|
+
it contains no plugin set catalog definitions.
|
|
259
|
+
"""
|
|
260
|
+
plugin_set_catalog_path = path(plugin_set_catalog_path_or_str)
|
|
261
|
+
if plugin_set_catalog_path in map(lambda psc: psc.get_root(), self._plugin_set_catalogs):
|
|
262
|
+
raise ValueError(f'Plugin set catalogs already loaded from: {plugin_set_catalog_path!s}')
|
|
263
|
+
errs, at_least_one = [], False
|
|
264
|
+
with plugin_set_catalog_path.open('r') as fpsc:
|
|
265
|
+
for yaml_obj in yaml.safe_load_all(fpsc):
|
|
266
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetCatalogKind.__args__:
|
|
267
|
+
try:
|
|
268
|
+
plugin_set_catalog = PluginSetCatalog(**yaml_obj).initialize(plugin_set_catalog_path.parent)
|
|
269
|
+
self._plugin_set_catalogs.append(plugin_set_catalog)
|
|
270
|
+
at_least_one = True
|
|
271
|
+
for plugin_set_file in plugin_set_catalog.get_plugin_set_files():
|
|
272
|
+
try:
|
|
273
|
+
self.load_plugin_sets(plugin_set_catalog_path.joinpath(plugin_set_file))
|
|
274
|
+
except ValueError as ve:
|
|
275
|
+
errs.append(ve)
|
|
276
|
+
except ExceptionGroup as eg:
|
|
277
|
+
errs.extend(eg.exceptions)
|
|
278
|
+
except ValidationError as ve:
|
|
279
|
+
errs.append(ve)
|
|
280
|
+
if errs:
|
|
281
|
+
raise ExceptionGroup(f'Errors while loading plugin set catalogs from: {plugin_set_catalog_path!s}', errs)
|
|
282
|
+
if not at_least_one:
|
|
283
|
+
raise ValueError(f'No plugin set catalogs found in: {plugin_set_catalog_path!s}')
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
def load_plugin_sets(self,
|
|
287
|
+
plugin_set_path_or_str: PathOrStr) -> Turtles:
|
|
288
|
+
"""
|
|
289
|
+
Processes the given YAML file, loading all plugin set definitions it
|
|
290
|
+
contains, ignoring other YAML objects.
|
|
291
|
+
|
|
292
|
+
:param plugin_set_path_or_str: A file path (or string).
|
|
293
|
+
:type plugin_set_path_or_str: PathOrStr
|
|
294
|
+
:return: This Turtles object (for chaining).
|
|
295
|
+
:rtype: Turtles
|
|
296
|
+
:raises ExceptionGroup: If one or more errors occur while loading plugin
|
|
297
|
+
set definitions.
|
|
298
|
+
:raises ValueError: If the given file has already been processed or if
|
|
299
|
+
it contains no plugin set definitions.
|
|
300
|
+
"""
|
|
301
|
+
plugin_set_path = path(plugin_set_path_or_str)
|
|
302
|
+
if plugin_set_path in map(lambda ps: ps.get_root(), self._plugin_sets):
|
|
303
|
+
raise ValueError(f'Plugin sets already loaded from: {plugin_set_path!s}')
|
|
304
|
+
errs, at_least_one = [], False
|
|
305
|
+
with plugin_set_path.open('r') as fps:
|
|
306
|
+
for yaml_obj in yaml.safe_load_all(fps):
|
|
307
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSetKind.__args__:
|
|
308
|
+
try:
|
|
309
|
+
plugin_set = PluginSet(**yaml_obj).initialize(plugin_set_path.parent)
|
|
310
|
+
self._plugin_sets.append(plugin_set)
|
|
311
|
+
at_least_one = True
|
|
312
|
+
except ValidationError as ve:
|
|
313
|
+
errs.append(ve)
|
|
314
|
+
if errs:
|
|
315
|
+
raise ExceptionGroup(f'Errors while loading plugin sets from: {plugin_set_path!s}', errs)
|
|
316
|
+
if not at_least_one:
|
|
317
|
+
raise ValueError(f'No plugin sets found in: {plugin_set_path!s}')
|
|
318
|
+
return self
|
|
319
|
+
|
|
320
|
+
def load_plugin_signing_credentials(self,
|
|
321
|
+
plugin_signing_credentials_path_or_str: PathOrStr) -> Turtles:
|
|
322
|
+
"""
|
|
323
|
+
Processes the given YAML file, loading all plugin set definitions it
|
|
324
|
+
contains in search of exactly one, ignoring YAML objects of other kinds.
|
|
325
|
+
|
|
326
|
+
:param plugin_signing_credentials_path_or_str: A file path (or string).
|
|
327
|
+
:type plugin_signing_credentials_path_or_str: PathOrStr
|
|
328
|
+
:return: This Turtles object (for chaining).
|
|
329
|
+
:rtype: Turtles
|
|
330
|
+
:raises ExceptionGroup: If one or more errors occur while loading plugin
|
|
331
|
+
signing credentials definitions.
|
|
332
|
+
:raises ValueError: If the given file has already been processed, if it
|
|
333
|
+
contains no plugin signing credentials definitions,
|
|
334
|
+
or if it contains more than one plugin signing
|
|
335
|
+
credentials definitions.
|
|
336
|
+
"""
|
|
337
|
+
plugin_signing_credentials_path = path(plugin_signing_credentials_path_or_str)
|
|
338
|
+
if self._plugin_signing_credentials:
|
|
339
|
+
raise ValueError(f'Plugin signing credentials already loaded from: {self._plugin_signing_credentials.get_root()!s}')
|
|
340
|
+
found = 0
|
|
341
|
+
with plugin_signing_credentials_path.open('r') as fpsc:
|
|
342
|
+
for yaml_obj in yaml.safe_load_all(fpsc):
|
|
343
|
+
if isinstance(yaml_obj, dict) and yaml_obj.get('kind') in PluginSigningCredentialsKind.__args__:
|
|
344
|
+
found = found + 1
|
|
345
|
+
if not self._plugin_signing_credentials:
|
|
346
|
+
try:
|
|
347
|
+
plugin_signing_credentials = PluginSigningCredentials(**yaml_obj).initialize(plugin_signing_credentials_path.parent)
|
|
348
|
+
self._plugin_signing_credentials = plugin_signing_credentials
|
|
349
|
+
except ValidationError as ve:
|
|
350
|
+
raise ExceptionGroup(f'Errors while loading plugin signing credentials from: {plugin_signing_credentials_path!s}', [ve])
|
|
351
|
+
if found == 0:
|
|
352
|
+
raise ValueError(f'No plugin signing credentials found in: {plugin_signing_credentials_path!s}')
|
|
353
|
+
if found > 1:
|
|
354
|
+
raise ValueError(f'Multiple plugin signing credentials found in: {plugin_signing_credentials_path!s}')
|
|
355
|
+
return self
|
|
356
|
+
|
|
357
|
+
def release_plugin(self,
|
|
358
|
+
plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]],
|
|
359
|
+
layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]],
|
|
360
|
+
interactive: bool=False) -> dict[PluginIdentifier, list[DeployPluginResult]]:
|
|
361
|
+
"""
|
|
362
|
+
Releases (builds then deploys) zero or more plugins.
|
|
363
|
+
|
|
364
|
+
:param plugin_id_or_plugin_ids: Either one plugin identifier, or a list
|
|
365
|
+
of plugin identifiers.
|
|
366
|
+
:type plugin_id_or_plugin_ids: Union[PluginIdentifier, list[PluginIdentifier]]
|
|
367
|
+
:param layer_id_or_layer_ids: Either one plugin registry layer
|
|
368
|
+
identifier or a list of plugin registry
|
|
369
|
+
layer identifiers.
|
|
370
|
+
:type layer_id_or_layer_ids: Union[PluginRegistryLayerIdentifier, list[PluginRegistryLayerIdentifier]]
|
|
371
|
+
:param interactive: Whether interactive prompts are allowed (default
|
|
372
|
+
False).
|
|
373
|
+
:type interactive: bool
|
|
374
|
+
:return: A mapping from plugin identifier to plugin deployment result;
|
|
375
|
+
if no plugins were given, the result is an empty mapping.
|
|
376
|
+
:rtype: dict[PluginIdentifier, list[DeployPluginResult]]
|
|
377
|
+
:raises Exception: If a given plugin is not found in any plugin set or
|
|
378
|
+
is not declared in any loaded plugin registry.
|
|
379
|
+
"""
|
|
380
|
+
plugin_ids: list[PluginIdentifier] = plugin_id_or_plugin_ids if isinstance(plugin_id_or_plugin_ids, list) else [plugin_id_or_plugin_ids]
|
|
381
|
+
layer_ids: list[PluginRegistryLayerIdentifier] = layer_id_or_layer_ids if isinstance(layer_id_or_layer_ids, list) else [layer_id_or_layer_ids]
|
|
382
|
+
# ... plugin_id -> (set_id, jar_path, plugin)
|
|
383
|
+
ret1 = self.build_plugin(plugin_ids)
|
|
384
|
+
jar_paths = [jar_path for set_id, jar_path, plugin in ret1.values()]
|
|
385
|
+
# ... (src_path, plugin_id) -> list of (registry_id, layer_id, dst_path, plugin)
|
|
386
|
+
ret2 = self.deploy_plugin(jar_paths,
|
|
387
|
+
layer_ids,
|
|
388
|
+
interactive=interactive)
|
|
389
|
+
return {plugin_id: val for (jar_path, plugin_id), val in ret2.items()}
|
|
390
|
+
|
|
391
|
+
def set_plugin_signing_password(self,
|
|
392
|
+
callable_or_password: Union[str, Callable[[], str]]) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Sets the plugin signing password callable.
|
|
395
|
+
|
|
396
|
+
:param callable_or_password: A callable returning a string (or simply a
|
|
397
|
+
string).
|
|
398
|
+
:type callable_or_password: Union[str, Callable[[], str]]
|
|
399
|
+
"""
|
|
400
|
+
self._plugin_signing_password_callable = callable_or_password if callable(callable_or_password) else lambda: callable_or_password
|
|
401
|
+
|
|
402
|
+
def _build_one_plugin(self,
|
|
403
|
+
plugin_id: PluginIdentifier) -> BuildPluginResult:
|
|
404
|
+
"""
|
|
405
|
+
Builds one plugin.
|
|
406
|
+
|
|
407
|
+
:param plugin_id: A plugin identifier.
|
|
408
|
+
:type plugin_id: PluginIdentifier
|
|
409
|
+
:return: A plugin build result object; if the plugin set returned None,
|
|
410
|
+
the second and third items (index 1 and 2) are None.
|
|
411
|
+
:rtype: BuildPluginResult
|
|
412
|
+
:raises Exception: If the given plugin identifier is not found in any
|
|
413
|
+
loaded plugin set.
|
|
414
|
+
"""
|
|
415
|
+
for plugin_set in self._plugin_sets:
|
|
416
|
+
if plugin_set.has_plugin(plugin_id):
|
|
417
|
+
bp = plugin_set.build_plugin(plugin_id,
|
|
418
|
+
self._get_plugin_signing_keystore(),
|
|
419
|
+
self._get_plugin_signing_alias(),
|
|
420
|
+
self._get_plugin_signing_password())
|
|
421
|
+
return plugin_set.get_id(), bp[0] if bp else None, bp[1] if bp else None
|
|
422
|
+
raise Exception(f'plugin identifier not found in any loaded plugin set: {plugin_id}')
|
|
423
|
+
|
|
424
|
+
def _deploy_one_plugin(self,
|
|
425
|
+
src_jar: Path,
|
|
426
|
+
plugin_id: PluginIdentifier,
|
|
427
|
+
layer_ids: list[PluginRegistryLayerIdentifier],
|
|
428
|
+
interactive: bool=False) -> list[DeployPluginResult]:
|
|
429
|
+
"""
|
|
430
|
+
Deploys a single plugin to the provided plugin registry layers of all
|
|
431
|
+
loaded plugin registries that declare the given plugin.
|
|
432
|
+
|
|
433
|
+
:param src_jar: File path of the signed JAR.
|
|
434
|
+
:type src_jar: Path
|
|
435
|
+
:param plugin_id: The corresponding plugin identifier.
|
|
436
|
+
:type plugin_id: PluginIdentifier
|
|
437
|
+
:param layer_ids: A list of plugin layer identifiers.
|
|
438
|
+
:type layer_ids: list[PluginRegistryLayerIdentifier]
|
|
439
|
+
:param interactive: Whether interactive prompts are allowed (default
|
|
440
|
+
False).
|
|
441
|
+
:type interactive: bool
|
|
442
|
+
:return: A non-empty list of plugin deployment results; if for any, the
|
|
443
|
+
plugin registry returned None, the third and fourth items
|
|
444
|
+
(index 2 and 3) are None.
|
|
445
|
+
:rtype: list[DeployPluginResult]
|
|
446
|
+
:raises Exception: If the given plugin identifier is not declared in any
|
|
447
|
+
loaded plugin registry.
|
|
448
|
+
"""
|
|
449
|
+
ret = list()
|
|
450
|
+
for plugin_registry in self._plugin_registries:
|
|
451
|
+
if plugin_registry.has_plugin(plugin_id):
|
|
452
|
+
for layer_id in layer_ids:
|
|
453
|
+
if layer := plugin_registry.get_layer(layer_id):
|
|
454
|
+
dp = layer.deploy_plugin(plugin_id,
|
|
455
|
+
src_jar,
|
|
456
|
+
interactive=interactive)
|
|
457
|
+
ret.append((plugin_registry.get_id(),
|
|
458
|
+
layer.get_id(),
|
|
459
|
+
dp[0] if dp else None,
|
|
460
|
+
dp[1] if dp else None))
|
|
461
|
+
if len(ret) == 0:
|
|
462
|
+
raise Exception(f'{src_jar}: {plugin_id} not declared in any plugin registry')
|
|
463
|
+
return ret
|
|
464
|
+
|
|
465
|
+
def _get_plugin_signing_alias(self) -> str:
|
|
466
|
+
"""
|
|
467
|
+
Returns the plugin signing alias from the loaded plugin signing
|
|
468
|
+
credentials.
|
|
469
|
+
|
|
470
|
+
:return: The plugin signing alias.
|
|
471
|
+
:rtype: str
|
|
472
|
+
"""
|
|
473
|
+
return self._plugin_signing_credentials.get_plugin_signing_alias()
|
|
474
|
+
|
|
475
|
+
def _get_plugin_signing_keystore(self) -> Path:
|
|
476
|
+
"""
|
|
477
|
+
Returns the plugin signing keystore file path from the loaded plugin
|
|
478
|
+
signing credentials.
|
|
479
|
+
|
|
480
|
+
:return: The plugin signing keystore file path.
|
|
481
|
+
:rtype: Path
|
|
482
|
+
"""
|
|
483
|
+
return self._plugin_signing_credentials.get_plugin_signing_keystore()
|
|
484
|
+
|
|
485
|
+
def _get_plugin_signing_password(self) -> Optional[Callable[[], str]]:
|
|
486
|
+
"""
|
|
487
|
+
Returns the plugin signing password.
|
|
488
|
+
|
|
489
|
+
:return: The plugin signing password callable.
|
|
490
|
+
:rtype: Optional[Callable[[], str]]
|
|
491
|
+
"""
|
|
492
|
+
return self._plugin_signing_password_callable
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def default_plugin_registry_catalog_choices() -> tuple[Path, ...]:
|
|
496
|
+
"""
|
|
497
|
+
Returns the tuple of default plugin registry catalog file choices.
|
|
498
|
+
|
|
499
|
+
See ``CONFIG_DIRS`` and ``PLUGIN_REGISTRY_CATALOG``.
|
|
500
|
+
|
|
501
|
+
:return: A tuple of default plugin registry catalog file choices.
|
|
502
|
+
:rtype: tuple[Path, ...]
|
|
503
|
+
"""
|
|
504
|
+
return Turtles._default_files(Turtles.PLUGIN_REGISTRY_CATALOG)
|
|
505
|
+
|
|
506
|
+
@staticmethod
|
|
507
|
+
def default_plugin_set_catalog_choices() -> tuple[Path, ...]:
|
|
508
|
+
"""
|
|
509
|
+
Returns the tuple of default plugin set catalog file choices.
|
|
510
|
+
|
|
511
|
+
See ``CONFIG_DIRS`` and ``PLUGIN_SET_CATALOG``.
|
|
512
|
+
|
|
513
|
+
:return: A tuple of default plugin set catalog file choices.
|
|
514
|
+
:rtype: tuple[Path, ...]
|
|
515
|
+
"""
|
|
516
|
+
return Turtles._default_files(Turtles.PLUGIN_SET_CATALOG)
|
|
517
|
+
|
|
518
|
+
@staticmethod
|
|
519
|
+
def default_plugin_signing_credentials_choices() -> tuple[Path, ...]:
|
|
520
|
+
"""
|
|
521
|
+
Returns the tuple of default plugin signing credentials file choices.
|
|
522
|
+
|
|
523
|
+
See ``CONFIG_DIRS`` and ``PLUGIN_SIGNING_CREDENTIALS``.
|
|
524
|
+
|
|
525
|
+
:return: A tuple of default plugin signing credentials file choices.
|
|
526
|
+
:rtype: tuple[Path, ...]
|
|
527
|
+
"""
|
|
528
|
+
return Turtles._default_files(Turtles.PLUGIN_SIGNING_CREDENTIALS)
|
|
529
|
+
|
|
530
|
+
@staticmethod
|
|
531
|
+
def select_default_plugin_registry_catalog() -> Optional[Path]:
|
|
532
|
+
"""
|
|
533
|
+
Of the default plugin registry catalog file choices, select the first
|
|
534
|
+
one that exists.
|
|
535
|
+
|
|
536
|
+
See ``default_plugin_registry_catalog_choices`` and ``_select_file``.
|
|
537
|
+
|
|
538
|
+
:return: The first of the default plugin registry catalog file choices
|
|
539
|
+
that exists, or None if none do.
|
|
540
|
+
:rtype: Optional[Path]
|
|
541
|
+
"""
|
|
542
|
+
return Turtles._select_file(Turtles.default_plugin_registry_catalog_choices())
|
|
543
|
+
|
|
544
|
+
@staticmethod
|
|
545
|
+
def select_default_plugin_set_catalog() -> Optional[Path]:
|
|
546
|
+
"""
|
|
547
|
+
Of the default plugin set catalog file choices, select the first one
|
|
548
|
+
that exists.
|
|
549
|
+
|
|
550
|
+
See ``default_plugin_registry_set_choices`` and ``_select_file``.
|
|
551
|
+
|
|
552
|
+
:return: The first of the default plugin set catalog file choices that
|
|
553
|
+
exists, or None if none do.
|
|
554
|
+
:rtype: Optional[Path]
|
|
555
|
+
"""
|
|
556
|
+
return Turtles._select_file(Turtles.default_plugin_set_catalog_choices())
|
|
557
|
+
|
|
558
|
+
@staticmethod
|
|
559
|
+
def select_default_plugin_signing_credentials() -> Optional[Path]:
|
|
560
|
+
"""
|
|
561
|
+
Of the default plugin signing credentials file choices, select the first
|
|
562
|
+
one that exists.
|
|
563
|
+
|
|
564
|
+
See ``default_plugin_registry_set_choices`` and ``_select_file``.
|
|
565
|
+
|
|
566
|
+
:return: The first of the default plugin signing credentials file
|
|
567
|
+
choices that exists, or None if none do.
|
|
568
|
+
:rtype: Optional[Path]
|
|
569
|
+
"""
|
|
570
|
+
return Turtles._select_file(Turtles.default_plugin_signing_credentials_choices())
|
|
571
|
+
|
|
572
|
+
@staticmethod
|
|
573
|
+
def _default_files(file_str) -> tuple[Path, ...]:
|
|
574
|
+
"""
|
|
575
|
+
Given a file base name, returns a tuple of this file in the various
|
|
576
|
+
Turtles configuration directories (``CONFIG_DIRS``).
|
|
577
|
+
|
|
578
|
+
:param file_str: A file base name.
|
|
579
|
+
:type file_str: str
|
|
580
|
+
:return: The file in the various Turtles configuration directories.
|
|
581
|
+
:rtype: tuple[Path, ...]
|
|
582
|
+
"""
|
|
583
|
+
return tuple(dir_path.joinpath(file_str) for dir_path in Turtles.CONFIG_DIRS)
|
|
584
|
+
|
|
585
|
+
@staticmethod
|
|
586
|
+
def _select_file(choices: Iterable[Path]) -> Optional[Path]:
|
|
587
|
+
"""
|
|
588
|
+
Of the given files, returns the first one that exists.
|
|
589
|
+
|
|
590
|
+
:param choices: An iterable of file paths.
|
|
591
|
+
:type choices: Iterable[Path]
|
|
592
|
+
:return: The first choice that exists, or None if none do.
|
|
593
|
+
:rtype: Optional[Path]
|
|
594
|
+
"""
|
|
595
|
+
for p in choices:
|
|
596
|
+
if p.is_file():
|
|
597
|
+
return p
|
|
598
|
+
return None
|