Coverage for harbor_cli/commands/api/artifact.py: 38%
105 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 typing import Optional
5import typer
6from harborapi.exceptions import NotFound
7from harborapi.ext.api import get_artifact
8from harborapi.ext.api import get_artifacts
9from harborapi.models import Label
10from harborapi.models import Tag
12from ...harbor.artifact import parse_artifact_name
13from ...logs import logger
14from ...output.console import console
15from ...output.console import exit
16from ...output.console import exit_err
17from ...output.render import render_result
18from ...state import state
19from ...utils.commands import inject_resource_options
20from ..help import ARTIFACT_HELP_STRING
23# Create a command group
24app = typer.Typer(
25 name="artifact",
26 help="Manage artifacts.",
27 no_args_is_help=True,
28)
30# Artifact subcommands
31tag_cmd = typer.Typer(
32 name="tag",
33 help="Artifact tag commands",
34 no_args_is_help=True,
35)
36label_cmd = typer.Typer(
37 name="label",
38 help="Artifact label commands",
39 no_args_is_help=True,
40)
42app.add_typer(tag_cmd)
43app.add_typer(label_cmd)
45# get_artifacts()
46@app.command("list")
47@inject_resource_options()
48def list_artifacts(
49 ctx: typer.Context,
50 query: Optional[str],
51 project: str = typer.Argument(
52 ...,
53 help="Name of project to fetch artifacts from.",
54 ),
55 repo: Optional[str] = typer.Argument(
56 None,
57 help="Specific repository in project to fetch artifacts from.",
58 ),
59 tag: Optional[str] = typer.Option(
60 None,
61 "--tag",
62 help="Limit to artifacts with this tag (e.g. 'latest').",
63 ),
64 with_report: bool = typer.Option(
65 False,
66 "--with-report",
67 help="Include vulnerability report in output.",
68 ),
69 max_connections: int = typer.Option(
70 5,
71 "--max-connections",
72 help=(
73 "Maximum number of concurrent connections to use. "
74 "Setting this too high can lead to severe performance degradation."
75 ),
76 ),
77 # TODO: add ArtifactReport filtering options here
78) -> None:
79 """List artifacts in a project or in a specific repository."""
80 if "/" in project:
81 logger.debug("Interpreting argument format as <project>/<repo>.")
82 project, repo = project.split("/", 1)
84 # TODO: add pagination to output
86 if project and repo:
87 repositories = [repo]
88 else:
89 repositories = None # all repos
91 artifacts = state.run(
92 get_artifacts(
93 state.client,
94 projects=[project],
95 repositories=repositories,
96 tag=tag,
97 query=query,
98 ),
99 "Fetching artifacts...",
100 )
101 render_result(artifacts, ctx)
102 logger.info(f"Fetched {len(artifacts)} artifact(s).")
105# delete_artifact()
106@app.command("delete", no_args_is_help=True)
107def delete_artifact(
108 ctx: typer.Context,
109 artifact: str = typer.Argument(
110 ...,
111 help=ARTIFACT_HELP_STRING,
112 ),
113 force: bool = typer.Option(
114 False,
115 "--force",
116 "-f",
117 help="Force deletion without confirmation.",
118 ),
119) -> None:
120 """Delete an artifact."""
121 an = parse_artifact_name(artifact)
122 if not force:
123 if not typer.confirm(
124 f"Are you sure you want to delete {artifact}?",
125 abort=True,
126 ):
127 exit()
129 try:
130 state.run(
131 state.client.delete_artifact(an.project, an.repository, an.reference),
132 f"Deleting artifact {artifact}...",
133 no_handle=NotFound,
134 )
135 except NotFound:
136 exit_err(f"Artifact {artifact} not found.")
137 else:
138 logger.info(f"Artifact {artifact} deleted.")
141# copy_artifact()
142@app.command("copy", no_args_is_help=True)
143def copy_artifact(
144 ctx: typer.Context,
145 artifact: str = typer.Argument(
146 ...,
147 help=ARTIFACT_HELP_STRING,
148 ),
149 project: str = typer.Argument(
150 ...,
151 help="Destination project.",
152 ),
153 repository: str = typer.Argument(
154 ...,
155 help="Destination repository (without project name).",
156 ),
157) -> None:
158 """Copy an artifact."""
160 # Warn user if they pass a project name in the repository name
161 # e.g. project="foo", repository="foo/bar"
162 # When it should be project="foo", repository="bar"
163 if project in repository:
164 logger.warning(
165 "Project name is part of the repository name, you likely don't want this."
166 )
168 try:
169 resp = state.run(
170 state.client.copy_artifact(project, repository, artifact),
171 f"Copying artifact {artifact} to {project}/{repository}...",
172 no_handle=NotFound,
173 )
174 except NotFound:
175 exit_err(f"Artifact {artifact} not found.")
176 else:
177 logger.info(f"Artifact {artifact} copied to {resp}.")
180# HarborAsyncClient.get_artifact()
181@app.command("get")
182def get(
183 ctx: typer.Context,
184 artifact: str = typer.Argument(
185 ...,
186 help=ARTIFACT_HELP_STRING,
187 ),
188 with_vulnerabilities: bool = typer.Option(
189 False,
190 "--with-vuln",
191 "-v",
192 ),
193 # TODO: --tag
194) -> None:
195 """Get information about a specific artifact."""
197 an = parse_artifact_name(artifact)
198 # Just use normal endpoint method for a single artifact
199 artifact = state.run(
200 state.client.get_artifact(an.project, an.repository, an.reference), # type: ignore
201 f"Fetching artifact(s)...",
202 )
203 render_result(artifact, ctx)
206# HarborAsyncClient.get_artifact_tags()
207@tag_cmd.command("list", no_args_is_help=True)
208def list_artifact_tags(
209 ctx: typer.Context,
210 artifact: str = typer.Argument(
211 ...,
212 help=ARTIFACT_HELP_STRING,
213 ),
214) -> None:
215 """List tags for an artifact."""
216 an = parse_artifact_name(artifact)
217 tags = state.run(
218 state.client.get_artifact_tags(an.project, an.repository, an.reference),
219 f"Fetching tags for {an!r}...",
220 )
222 if not tags:
223 exit_err(f"No tags found for {an!r}")
225 for tag in tags:
226 console.print(tag)
229# create_artifact_tag()
230@tag_cmd.command("create", no_args_is_help=True)
231def create_artifact_tag(
232 ctx: typer.Context,
233 artifact: str = typer.Argument(
234 ...,
235 help=ARTIFACT_HELP_STRING,
236 ),
237 tag: str = typer.Argument(..., help="Name of the tag to create."),
238 # signed
239 # immutable
240) -> None:
241 """Create a tag for an artifact."""
242 an = parse_artifact_name(artifact)
243 # NOTE: We might need to fetch repo and artifact IDs
244 t = Tag(name=tag)
245 location = state.run(
246 state.client.create_artifact_tag(an.project, an.repository, an.reference, t),
247 f"Creating tag {tag!r} for {artifact}...",
248 )
249 logger.info(f"Created {tag!r} for {artifact}: {location}")
252# delete_artifact_tag()
253@tag_cmd.command("delete", no_args_is_help=True)
254def delete_artifact_tag(
255 ctx: typer.Context,
256 artifact: str = typer.Argument(
257 ...,
258 help=ARTIFACT_HELP_STRING,
259 ),
260 tag: str = typer.Argument(..., help="Name of the tag to delete."),
261 force: bool = typer.Option(
262 False,
263 "--force",
264 "-f",
265 help="Force deletion without confirmation.",
266 ),
267) -> None:
268 """Delete a tag for an artifact."""
269 an = parse_artifact_name(artifact)
270 # NOTE: We might need to fetch repo and artifact IDs
271 if not force:
272 if not typer.confirm(
273 f"Are you sure you want to delete the tag {tag!r}?",
274 abort=True,
275 ):
276 exit()
278 state.run(
279 state.client.delete_artifact_tag(an.project, an.repository, an.reference, tag),
280 f"Deleting tag {tag!r} for {artifact}...",
281 )
284# add_artifact_label()
285@label_cmd.command("add", no_args_is_help=True)
286def add_artifact_label(
287 ctx: typer.Context,
288 artifact: str = typer.Argument(
289 ...,
290 help=ARTIFACT_HELP_STRING,
291 ),
292 name: Optional[str] = typer.Option(
293 None,
294 "--name",
295 help="Name of the label.",
296 ),
297 description: Optional[str] = typer.Option(
298 None,
299 "--description",
300 help="Description of the label.",
301 ),
302 color: Optional[str] = typer.Option(
303 None,
304 "--color",
305 help="Label color.",
306 ),
307 scope: Optional[str] = typer.Option(
308 None,
309 "--scope",
310 help="Scope of the label.",
311 ),
312) -> None:
313 """Add a label to an artifact."""
314 # TODO: add parameter validation. Name is probably required?
315 # Otherwise, we can just leave validation to the API, and
316 # print a default error message.
317 an = parse_artifact_name(artifact)
318 label = Label(
319 name=name,
320 description=description,
321 color=color,
322 scope=scope,
323 )
324 state.run(
325 state.client.add_artifact_label(an.project, an.repository, an.reference, label),
326 f"Adding label {label.name!r} to {artifact}...",
327 )
328 logger.info(f"Added label {label.name!r} to {artifact}.")
331# HarborAsyncClient.delete_artifact_label()
332@label_cmd.command("delete", no_args_is_help=True)
333def delete_artifact_label(
334 ctx: typer.Context,
335 artifact: str = typer.Argument(
336 ...,
337 help=ARTIFACT_HELP_STRING,
338 ),
339 label_id: int = typer.Argument(
340 ...,
341 help="ID of the label to delete.",
342 ),
343) -> None:
344 """Add a label to an artifact."""
345 an = parse_artifact_name(artifact)
346 state.run(
347 state.client.delete_artifact_label(
348 an.project, an.repository, an.reference, label_id
349 ),
350 f"Deleting label {label_id} from {artifact}...",
351 )
354# HarborAsyncClient.get_artifact_vulnerabilities()
355# HarborAsyncClient.get_artifact_accessories()
356@app.command("accessories")
357def get_accessories(
358 ctx: typer.Context,
359 artifact: str = typer.Argument(
360 ...,
361 help=ARTIFACT_HELP_STRING,
362 ),
363) -> None:
364 """Get accessories for an artifact."""
365 an = parse_artifact_name(artifact)
366 accessories = state.run(
367 state.client.get_artifact_accessories(an.project, an.repository, an.reference),
368 f"Getting accessories for {artifact}...",
369 )
370 render_result(accessories, ctx)
373# HarborAsyncClient.get_artifact_build_history()
374@app.command("buildhistory", no_args_is_help=True)
375def get_buildhistory(
376 ctx: typer.Context,
377 artifact: str = typer.Argument(
378 ...,
379 help=ARTIFACT_HELP_STRING,
380 ),
381) -> None:
382 """Get build history for an artifact."""
383 an = parse_artifact_name(artifact)
384 history = state.run(
385 state.client.get_artifact_build_history(
386 an.project, an.repository, an.reference
387 ),
388 f"Getting build history for {artifact}...",
389 )
390 render_result(history, ctx)
393# harborapi.ext.api.get_artifact
394@app.command("vulnerabilities")
395def get_vulnerabilities(
396 ctx: typer.Context,
397 artifact: str = typer.Argument(
398 ...,
399 help=ARTIFACT_HELP_STRING,
400 ),
401) -> None:
402 """Get vulnerabilities for an artifact."""
403 an = parse_artifact_name(artifact)
404 a = state.run(
405 get_artifact(state.client, an.project, an.repository, an.reference),
406 f"Getting vulnerabilities for {artifact}...",
407 )
408 render_result(a, ctx)