Coverage for harbor_cli/commands/api/replication.py: 56%
83 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
« prev ^ index » next coverage.py v6.5.0, created at 2023-02-09 12:09 +0100
1from __future__ import annotations
3from enum import Enum
4from typing import Optional
6import typer
7from harborapi.models.models import ReplicationFilter
8from harborapi.models.models import ReplicationPolicy
9from harborapi.models.models import ReplicationTrigger
10from harborapi.models.models import ReplicationTriggerSettings
12from ...logs import logger
13from ...output.render import render_result
14from ...state import state
15from ...utils.args import model_params_from_ctx
16from ...utils.commands import inject_help
17from ...utils.commands import inject_resource_options
19# Create a command group
20app = typer.Typer(
21 name="replication",
22 help="Registry replication execution and policy.",
23 no_args_is_help=True,
24)
25policy_cmd = typer.Typer(
26 name="policy",
27 help="Manage replication policies.",
28 no_args_is_help=True,
29)
30task_cmd = typer.Typer(
31 name="task",
32 help="Manage replication execution tasks.",
33 no_args_is_help=True,
34)
35app.add_typer(task_cmd)
36app.add_typer(policy_cmd)
39# HarborAsyncClient.start_replication()
40@app.command("start")
41def start_replication_execution(
42 ctx: typer.Context,
43 policy_id: int = typer.Option(
44 ...,
45 help="The ID of the policy to start a execution for.",
46 ),
47) -> None:
48 """Start a replication execution."""
49 replication_url = state.run(
50 state.client.start_replication(policy_id), "Starting replication..."
51 )
52 render_result(replication_url, ctx)
53 logger.info(f"Replication started for policy {policy_id}.")
56# HarborAsyncClient.stop_replication()
57@app.command("stop")
58def stop_replication_execution(
59 ctx: typer.Context,
60 execution_id: int = typer.Option(
61 ...,
62 help="The ID of the replication execution.",
63 ),
64) -> None:
65 """Stop a replication execution."""
66 state.run(
67 state.client.stop_replication(execution_id),
68 f"Stopping replication execution...",
69 )
70 logger.info(f"Stopped replication execution with ID {execution_id}.")
73# HarborAsyncClient.get_replication()
74@app.command("get")
75def get_replication_execution(
76 ctx: typer.Context,
77 execution_id: int = typer.Argument(
78 ...,
79 help="The ID of the replication execution.",
80 ),
81) -> None:
82 """Get information about a replication execution."""
83 execution = state.run(
84 state.client.get_replication(execution_id), "Fetching replication execution..."
85 )
86 render_result(execution, ctx)
89# HarborAsyncClient.get_replications()
90@app.command("list")
91@inject_resource_options(use_defaults=False)
92def list_replication_executions(
93 ctx: typer.Context,
94 sort: Optional[str],
95 policy_id: Optional[int] = typer.Option(
96 None,
97 help="The ID of the policy to list executions for.",
98 ),
99 status: Optional[str] = typer.Option(
100 None,
101 help="The status of the executions to list.",
102 ),
103 trigger: Optional[str] = typer.Option(
104 None,
105 help="The trigger of the executions to list.",
106 ),
107 page: int = 1,
108 page_size: int = 10,
109) -> None:
110 """List replication executions."""
111 executions = state.run(
112 state.client.get_replications(
113 sort=sort,
114 trigger=trigger,
115 policy_id=policy_id,
116 status=status,
117 page=page,
118 page_size=page_size,
119 ),
120 "Fetching replication executions...",
121 )
122 render_result(executions, ctx)
125# HarborAsyncClient.get_replication_policy()
126@policy_cmd.command("get")
127def get_replication_policy(
128 ctx: typer.Context,
129 policy_id: int = typer.Option(
130 ...,
131 help="The ID of the replication policy.",
132 ),
133) -> None:
134 """Get information about a replication policy."""
135 policy = state.run(
136 state.client.get_replication_policy(policy_id), "Fetching replication policy..."
137 )
138 render_result(policy, ctx)
141class FilterMatchMode(Enum):
142 """Filter match modes."""
144 # The Web UI calls it "matching" and "excluding",
145 # but the API uses the values "matches" and "excludes".
146 # We follow the API naming scheme for simplicity.
147 MATCHES = "matches"
148 EXCLUDES = "excludes"
151class FilterResourceMode(Enum):
152 """Filter resource modes."""
154 ALL = "all"
155 IMAGE = "image"
156 ARTIFACT = "artifact"
159# HarborAsyncClient.create_replication_policy()
160@policy_cmd.command("create")
161@inject_help(ReplicationPolicy)
162def create_replication_policy(
163 ctx: typer.Context,
164 name: str = typer.Argument(...),
165 # Difference from spec: we take in the ID of registries instead of
166 # the full registry definitions. In the Web UI, you can only select
167 # existing registries, so this is more consistent.
168 src_registry_id: int = typer.Argument(
169 ...,
170 help=(
171 "The ID of registry to replicate from. "
172 "Typically an external registry such as [green]'hub.docker.com'[/], [green]'ghcr.io'[/], etc."
173 ),
174 ),
175 dest_registry_id: int = typer.Argument(
176 ...,
177 help="The ID of the registry to replicate to.",
178 ),
179 description: Optional[str] = typer.Option(
180 None,
181 "--description",
182 ),
183 dest_namespace: Optional[str] = typer.Option(
184 None,
185 "--dest-namespace",
186 ),
187 dest_namespace_replace_count: Optional[int] = typer.Option(
188 None,
189 "--dest-namespace-replace-count",
190 ),
191 replication_trigger_type: Optional[str] = typer.Option(
192 None,
193 "--trigger-type",
194 help=ReplicationTrigger.__fields__["type"].field_info.description,
195 ),
196 replication_trigger_cron: Optional[str] = typer.Option(
197 None,
198 "--trigger-cron",
199 help=ReplicationTriggerSettings.__fields__["cron"].field_info.description,
200 ),
201 filter_name: Optional[str] = typer.Option(
202 None,
203 "--filter-name",
204 help=(
205 "Filter the name of the resource. "
206 "Leave empty or use [green]'**'[/] to match all. "
207 "[green]'library/**'[/] only matches resources under [cyan]'library'[/]. "
208 "For more patterns, please refer to the offical Harbor user guide."
209 ),
210 ),
211 filter_tag: Optional[str] = typer.Option(
212 None, "--filter-tag", help=("Filter the tag of the resource. ")
213 ),
214 filter_tag_mode: FilterMatchMode = typer.Option(
215 FilterMatchMode.MATCHES.value,
216 "--filter-tag-mode",
217 help="Match or exclude the given tag",
218 ),
219 filter_label: Optional[str] = typer.Option(
220 None, "--filter-label", help=("Filter the label of the resource. ")
221 ),
222 filter_label_mode: FilterMatchMode = typer.Option(
223 FilterMatchMode.MATCHES.value,
224 "--filter-label-mode",
225 help="Match or exclude the given label",
226 ),
227 filter_resource: FilterResourceMode = typer.Option(
228 FilterResourceMode.ALL.value,
229 "--filter-resource",
230 help="Filter the resource type to replicate.",
231 ),
232 replicate_deletion: Optional[bool] = typer.Option(
233 None,
234 "--replicate-deletion",
235 is_flag=False,
236 ),
237 override: Optional[bool] = typer.Option(
238 None,
239 "--override",
240 is_flag=False,
241 ),
242 enabled: Optional[bool] = typer.Option(
243 None,
244 "--enabled",
245 is_flag=False,
246 ),
247 speed: Optional[int] = typer.Option(None, "--speed-limit"),
248) -> None:
249 """Create a replication policy."""
250 params = model_params_from_ctx(ctx, ReplicationPolicy)
252 # Get registries from API
253 src_registry = state.run(state.client.get_registry(src_registry_id))
254 dest_registry = state.run(state.client.get_registry(dest_registry_id))
256 # Create the filter objects
257 filters = []
258 if filter_name:
259 filters.append(
260 ReplicationFilter(
261 type="name",
262 value=filter_name,
263 decoration=None,
264 )
265 )
266 if filter_tag:
267 filters.append(
268 ReplicationFilter(
269 type="tag",
270 value=filter_tag,
271 decoration=filter_tag_mode.value,
272 )
273 )
274 if filter_label:
275 filters.append(
276 ReplicationFilter(
277 type="label",
278 value=filter_label,
279 decoration=filter_label_mode.value,
280 )
281 )
283 # create the replication policy
284 policy = ReplicationPolicy(
285 **params,
286 trigger=ReplicationTrigger(
287 type=replication_trigger_type,
288 trigger_settings=ReplicationTriggerSettings(
289 cron=replication_trigger_cron,
290 ),
291 ),
292 src_registry=src_registry,
293 dest_registry=dest_registry,
294 filters=filters if filters else None,
295 )
297 policy_url = state.run(
298 state.client.create_replication_policy(policy),
299 f"Creating replication policy {policy}...",
300 )
301 logger.info(f"Created replication policy: {policy_url}.")
304# HarborAsyncClient.update_replication_policy()
305# TODO: replication policy update
306# It would be nice to expose the same API as create, but without the
307# requirement of the source and destination registries.
308# 3 options come to mind here:
309# 1. Duplicate most of the code from create, but without the registry
310# arguments. This is the easiest, plays best with static analysis,
311# but is prone to errors and spec drift, since we have to maintain
312# two copies of largely the same function definitions.
313# 2. Use a decorator to inject the registry arguments to a function
314# shared by both create and update. This is a bit more complex,
315# but would allow us to maintain a single function instead of two.
316# However, it would probably be a bit too "magical", and would break
317# static analysis.
318# 3. Have a single monolithic function that handles both create and
319# update. This plays well with static analysis, but would also
320# require a lot of conditional logic to handle the different, and
321# sometimes conflicting, arguments. Furthermore, it would break the
322# current pattern of having a separate function for each command.
323# Users would expect `policy update`, but it would instead be
324# `policy create --update`
326# HarborAsyncClient.delete_replication_policy()
327@policy_cmd.command("delete")
328def delete_replication_policy(
329 ctx: typer.Context,
330 policy_id: int = typer.Option(
331 ...,
332 help="The ID of the replication policy.",
333 ),
334) -> None:
335 """Delete a replication policy."""
336 state.run(
337 state.client.delete_replication_policy(policy_id),
338 f"Deleting replication policy with ID {policy_id}...",
339 )
340 logger.info(f"Deleted replication policy with ID {policy_id}.")
343# HarborAsyncClient.get_replication_policies()
344@policy_cmd.command("list")
345@inject_resource_options(use_defaults=False)
346def list_replication_policies(
347 ctx: typer.Context,
348 query: Optional[str],
349 sort: Optional[str],
350 page: int,
351 page_size: int,
352) -> None:
353 """List replication policies."""
354 policies = state.run(
355 state.client.get_replication_policies(
356 query=query,
357 sort=sort,
358 page=page,
359 page_size=page_size,
360 ),
361 f"Fetching replication policies...",
362 )
363 render_result(policies, ctx)
366# Tasks
368# HarborAsyncClient.get_replication_tasks()
369@task_cmd.command("list")
370@inject_resource_options(use_defaults=False)
371def list_execution_tasks(
372 ctx: typer.Context,
373 sort: Optional[str],
374 status: Optional[str] = typer.Option(
375 None, "--status", help="Task status to filter by."
376 ),
377 resource_type: Optional[str] = typer.Option(
378 None, "--resource-type", help="Task resource type to filter by."
379 ),
380 page: int = 1,
381 page_size: int = 10,
382 execution_id: int = typer.Argument(
383 ..., help="The ID of the replication execution to list tasks for."
384 ),
385) -> None:
386 """List replication tasks."""
387 tasks = state.run(
388 state.client.get_replication_tasks(
389 execution_id,
390 sort=sort,
391 page=page,
392 page_size=page_size,
393 status=status,
394 resource_type=resource_type,
395 ),
396 f"Fetching replication tasks for execution {execution_id}...",
397 )
398 render_result(tasks, ctx)
401# HarborAsyncClient.get_replication_task_log()
402@task_cmd.command("log")
403def get_task_log(
404 ctx: typer.Context,
405 execution_id: int = typer.Argument(
406 ..., help="The ID of the execution the task belongs to."
407 ),
408 task_id: int = typer.Argument(..., help="The ID of the task to get the log for."),
409) -> None:
410 """Get the log for a replication task."""
411 log = state.run(
412 state.client.get_replication_task_log(execution_id, task_id),
413 "Fetching replication logs...",
414 )
415 render_result(log, ctx)
416 logger.info(
417 f"Fetched log for replication task {task_id} of replication execution {execution_id}"
418 )