Skip to content

Commit ddc2921

Browse files
authored
Add project repos command, fix type checking issues (#99)
* Upgrade harborapi, remove type ignores * Remove type ignores for ellipsis defaults * Remove certain strings from param help * Add `project repos` * Run linting and formatting on tests * Update changelog
1 parent 6125350 commit ddc2921

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+244
-156
lines changed

Diff for: CHANGELOG.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,23 @@ The **third number** is the patch version (bug fixes)
1212

1313
## Unreleased
1414

15+
### Added
16+
17+
- Command: `project repos` to list repositories in a project. A more intuitive way to list repositories for a project than going through `repository list`.
18+
- `--sbom-generation` option for `project create` and `project update` commands to enable automatic SBOM generation for the project.
19+
1520
### Changed
1621

1722
- Styling of multiline help text in commands.
1823

24+
### Removed
25+
26+
- Mentions of valid values from `project {create,update}` commands.
27+
1928
### Fixed
2029

2130
- REPL closing when certain errors are raised.
31+
- `artifact list` for artifacts with no extra attributes.
2232

2333
## [0.2.2](https://github.com/unioslo/harbor-cli/tree/harbor-cli-v0.2.2) - 2024-03-01
2434

@@ -72,7 +82,6 @@ The **third number** is the patch version (bug fixes)
7282

7383
- Double printing of messages in terminal if logging wasn't properly configured.
7484

75-
7685
## [0.1.0](https://github.com/unioslo/harbor-cli/tree/ca08e7e8830ff3a10e1be447b5555acd5ed672ed) - 2023-12-06
7786

7887
### Added

Diff for: harbor_cli/commands/api/artifact.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ def create_artifact_tag(
325325
"""Create a tag for an artifact."""
326326
an = parse_artifact_name(artifact)
327327
# NOTE: We might need to fetch repo and artifact IDs
328-
t = Tag(name=tag) # pyright: ignore[reportCallIssue]
328+
t = Tag(name=tag)
329329
location = state.run(
330330
state.client.create_artifact_tag(an.project, an.repository, an.reference, t),
331331
f"Creating tag {tag!r} for {artifact}...",
@@ -393,7 +393,7 @@ def add_artifact_label(
393393
description=description,
394394
color=color,
395395
scope=scope,
396-
) # pyright: ignore[reportCallIssue]
396+
)
397397
state.run(
398398
state.client.add_artifact_label(an.project, an.repository, an.reference, label),
399399
f"Adding label {label.name!r} to {artifact}...",

Diff for: harbor_cli/commands/api/auditlog.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def _get_schedule(
213213
return Schedule(
214214
parameters=params,
215215
schedule=ScheduleObj(**obj_kwargs),
216-
) # pyright: ignore[reportCallIssue]
216+
)
217217

218218

219219
# HarborAsyncClient.create_audit_log_rotation_schedule()

Diff for: harbor_cli/commands/api/cve_allowlist.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ def clear_allowlist(
8686
) -> None:
8787
"""Clear the current CVE allowlist of all CVEs, and optionally all metadata as well."""
8888
if full_clear:
89-
allowlist = CVEAllowlist(items=[]) # pyright: ignore[reportCallIssue] # create a whole new allowlist
89+
allowlist = CVEAllowlist(items=[]) # create a whole new allowlist
9090
else:
9191
# Fetch existing allowlist to preserve metadata
9292
allowlist = state.run(

Diff for: harbor_cli/commands/api/gc.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ def create_gc_schedule(
5959
schedule_obj = ScheduleObj(
6060
type=type,
6161
cron=cron,
62-
) # pyright: ignore[reportCallIssue]
62+
)
6363
# TODO: investigate which parameters the `parameters` field takes
64-
schedule = Schedule(schedule=schedule_obj) # pyright: ignore[reportCallIssue]
64+
schedule = Schedule(schedule=schedule_obj)
6565
state.run(
6666
state.client.create_gc_schedule(schedule),
6767
"Creating Garbage Collection schedule...",

Diff for: harbor_cli/commands/api/project.py

+65-16
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,39 @@ def get_project_info(
8787
render_result(p, ctx)
8888

8989

90+
# HarborAsyncClient.get_repositories()
91+
@app.command("repos")
92+
@inject_resource_options()
93+
def list_repos(
94+
ctx: typer.Context,
95+
project: Optional[str] = typer.Argument(
96+
help="Name of project to fetch repositories from.",
97+
),
98+
query: Optional[str] = None,
99+
sort: Optional[str] = None,
100+
page: int = 1,
101+
page_size: int = 10,
102+
limit: Optional[int] = ...,
103+
) -> None:
104+
"""List all repositories in a project.
105+
106+
Alternative to `repository list`"""
107+
repos = state.run(
108+
state.client.get_repositories(
109+
project,
110+
query=query,
111+
sort=sort,
112+
page=page,
113+
page_size=page_size,
114+
limit=limit,
115+
),
116+
"Fetching repositories...",
117+
)
118+
if not repos:
119+
info(f"{project!r} has no repositories.")
120+
render_result(repos, ctx)
121+
122+
90123
# HarborAsyncClient.get_project_logs()
91124
@app.command("logs")
92125
@inject_resource_options()
@@ -143,7 +176,8 @@ def project_exists(
143176
@app.command("create", no_args_is_help=True)
144177
@inject_help(ProjectReq)
145178
@inject_help(
146-
ProjectMetadata
179+
ProjectMetadata,
180+
remove=['The valid values are "true", "false".'],
147181
) # inject this first so its "public" field takes precedence
148182
def create_project(
149183
ctx: typer.Context,
@@ -196,23 +230,32 @@ def create_project(
196230
None,
197231
"--retention-id",
198232
),
233+
auto_sbom_generation: Optional[bool] = typer.Option(
234+
None,
235+
"--sbom-generation",
236+
is_flag=False,
237+
),
199238
# TODO: add support for adding CVE allowlist when creating a project
200239
) -> None:
201240
"""Create a new project."""
202241
project_req = ProjectReq(
203242
project_name=project_name,
204243
storage_limit=storage_limit,
205244
registry_id=registry_id,
206-
metadata=ProjectMetadata(
207-
# validator does bool -> str conversion for the string bool fields
208-
public=public, # type: ignore
209-
enable_content_trust=enable_content_trust,
210-
enable_content_trust_cosign=enable_content_trust_cosign,
211-
prevent_vul=prevent_vul,
212-
severity=severity,
213-
auto_scan=auto_scan,
214-
reuse_sys_cve_allowlist=reuse_sys_cve_allowlist,
215-
retention_id=retention_id,
245+
metadata=ProjectMetadata.model_validate(
246+
# NOTE: Constructing via a dict here to avoid type checking noise.
247+
# See tests/api/test_models.py for more information.
248+
{
249+
"public": public,
250+
"enable_content_trust": enable_content_trust,
251+
"enable_content_trust_cosign": enable_content_trust_cosign,
252+
"prevent_vul": prevent_vul,
253+
"severity": severity,
254+
"auto_scan": auto_scan,
255+
"reuse_sys_cve_allowlist": reuse_sys_cve_allowlist,
256+
"retention_id": retention_id,
257+
"auto_sbom_generation": auto_sbom_generation,
258+
}
216259
),
217260
)
218261
location = state.run(
@@ -283,7 +326,8 @@ def list_projects(
283326
@app.command("update", no_args_is_help=True)
284327
@inject_help(ProjectReq)
285328
@inject_help(
286-
ProjectMetadata
329+
ProjectMetadata,
330+
remove=['The valid values are "true", "false".'],
287331
) # inject this first so its "public" field takes precedence
288332
def update_project(
289333
ctx: typer.Context,
@@ -336,6 +380,11 @@ def update_project(
336380
None,
337381
"--retention-id",
338382
),
383+
auto_sbom_generation: Optional[bool] = typer.Option(
384+
None,
385+
"--sbom-generation",
386+
is_flag=False,
387+
),
339388
) -> None:
340389
"""Update project information."""
341390
req_params = model_params_from_ctx(ctx, ProjectReq)
@@ -346,7 +395,7 @@ def update_project(
346395
arg = get_project_arg(project_name_or_id)
347396
project = get_project(arg)
348397
if project.metadata is None:
349-
project.metadata = ProjectMetadata() # pyright: ignore[reportCallIssue] # mypy bug
398+
project.metadata = ProjectMetadata()
350399

351400
# Create updated models from params
352401
req = create_updated_model(
@@ -748,9 +797,9 @@ def list_project_members(
748797
entity_name: Optional[str] = typer.Option(
749798
None, "--entity", help="Entity name to search for."
750799
),
751-
page: int = ..., # type: ignore
752-
page_size: int = ..., # type: ignore
753-
limit: Optional[int] = ..., # type: ignore
800+
page: int = ...,
801+
page_size: int = ...,
802+
limit: Optional[int] = ...,
754803
) -> None:
755804
"""List all members of a project."""
756805
project_arg = get_project_arg(project_name_or_id)

Diff for: harbor_cli/commands/api/registry.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def create_registry(
9292
type=type,
9393
insecure=insecure,
9494
description=description,
95-
) # pyright: ignore[reportCallIssue]
95+
)
9696
location = state.run(state.client.create_registry(registry), "Creating registry...")
9797
render_result(location, ctx)
9898

Diff for: harbor_cli/commands/api/repository.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def list_repos(
109109
sort: Optional[str] = None,
110110
page: int = 1,
111111
page_size: int = 10,
112-
limit: Optional[int] = ..., # type: ignore
112+
limit: Optional[int] = ...,
113113
) -> None:
114114
"""List repositories in all projects or a specific project."""
115115
repos = state.run(

Diff for: harbor_cli/commands/api/retention.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -200,9 +200,9 @@ def list_retention_jobs(
200200
ctx: typer.Context,
201201
project_name_or_id: Optional[str] = ARG_PROJECT_NAME_OR_ID_OPTIONAL,
202202
policy_id: Optional[int] = OPTION_POLICY_ID,
203-
page: int = ..., # type: ignore
204-
page_size: int = ..., # type: ignore
205-
limit: Optional[int] = ..., # type: ignore
203+
page: int = ...,
204+
page_size: int = ...,
205+
limit: Optional[int] = ...,
206206
) -> None:
207207
"""List retention jobs."""
208208
policy_id = policy_id_from_args(project_name_or_id, policy_id)
@@ -266,9 +266,9 @@ def list_retention_tasks(
266266
project_name_or_id: Optional[str] = ARG_PROJECT_NAME_OR_ID_OPTIONAL,
267267
job_id: int = typer.Argument(help="ID of the job to list tasks for."),
268268
policy_id: Optional[int] = OPTION_POLICY_ID,
269-
page: int = ..., # type: ignore
270-
page_size: int = ..., # type: ignore
271-
limit: Optional[int] = ..., # type: ignore
269+
page: int = ...,
270+
page_size: int = ...,
271+
limit: Optional[int] = ...,
272272
) -> None:
273273
"""List retention tasks."""
274274
policy_id = policy_id_from_args(project_name_or_id, policy_id)

Diff for: harbor_cli/commands/api/scan.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ def start_scan_export(
176176
) -> None:
177177
# TODO: resolve label names to IDs (?)
178178

179-
req = ScanDataExportRequest() # pyright: ignore[reportCallIssue]
179+
req = ScanDataExportRequest()
180180
if job_name:
181181
req.job_name = job_name
182182
if cve:

Diff for: harbor_cli/commands/api/scanall.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def create_scanall_schedule(
6363
cron: Optional[str] = typer.Option(None),
6464
) -> None:
6565
params = model_params_from_ctx(ctx, ScheduleObj)
66-
schedule = Schedule(schedule=ScheduleObj(**params)) # pyright: ignore[reportCallIssue]
66+
schedule = Schedule(schedule=ScheduleObj(**params))
6767
state.run(
6868
state.client.create_scan_all_schedule(schedule),
6969
"Creating 'Scan All' schedule...",

Diff for: harbor_cli/commands/api/user.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -286,14 +286,14 @@ def unset_user_admin(
286286
def set_user_password(
287287
username_or_id: str = ARG_USERNAME_OR_ID,
288288
old_password: str = typer.Option(
289-
..., # type: ignore # pyright unable to infer type?
289+
..., # type: ignore # pyright unable to infer type?
290290
"--old-password",
291291
prompt="Enter old password",
292292
hide_input=True,
293293
help="Old password for user. Prompted if not provided.",
294294
),
295295
new_password: str = typer.Option(
296-
..., # type: ignore
296+
..., # type: ignore # pyright unable to infer type?
297297
"--new-password",
298298
prompt="Enter new password",
299299
hide_input=True,

Diff for: harbor_cli/commands/api/usergroup.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def create_usergroup(
6161
group_name=group_name,
6262
group_type=group_type.as_int(),
6363
ldap_group_dn=ldap_group_dn,
64-
) # pyright: ignore[reportCallIssue]
64+
)
6565
location = state.run(
6666
state.client.create_usergroup(usergroup),
6767
f"Creating user group {group_name}...",
@@ -77,7 +77,7 @@ def update_usergroup(
7777
# NOTE: make group_name optional if we can update other fields in the future
7878
) -> None:
7979
"""Update a user group. Only the name can be updated currently."""
80-
usergroup = UserGroup(group_name=group_name) # pyright: ignore[reportCallIssue]
80+
usergroup = UserGroup(group_name=group_name)
8181
state.run(
8282
state.client.update_usergroup(group_id, usergroup),
8383
f"Updating user group {group_id}...",
@@ -115,9 +115,9 @@ def get_usergroups(
115115
"--group-name",
116116
help="Group name to filter by (fuzzy matching).",
117117
),
118-
page: int = ..., # type: ignore
119-
page_size: int = ..., # type: ignore
120-
limit: Optional[int] = ..., # type: ignore
118+
page: int = ...,
119+
page_size: int = ...,
120+
limit: Optional[int] = ...,
121121
) -> None:
122122
"""List user groups."""
123123
usergroups = state.run(
@@ -141,7 +141,7 @@ def search_usergroups(
141141
group_name: str = typer.Argument(help="Name of group to search for."),
142142
page: int = 1,
143143
page_size: int = 10,
144-
# limit: Optional[int] = ..., # type: ignore # NYI in harborapi
144+
# limit: Optional[int] = ..., # NYI in harborapi
145145
) -> None:
146146
"""Search for user groups by name."""
147147
usergroups = state.run(

Diff for: harbor_cli/utils/commands.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from functools import lru_cache
66
from typing import Any
77
from typing import List
8+
from typing import Optional
89
from typing import Type
910

1011
import click
@@ -102,7 +103,10 @@ def get_app_callback_options(app: typer.Typer) -> list[typer.models.OptionInfo]:
102103

103104

104105
def inject_help(
105-
model: Type[BaseModel], strict: bool = False, **field_additions: str
106+
model: Type[BaseModel],
107+
strict: bool = False,
108+
remove: Optional[List[str]] = None,
109+
**field_additions: str,
106110
) -> Any:
107111
"""
108112
Injects a Pydantic model's field descriptions into the help attributes
@@ -139,6 +143,8 @@ def my_command(my_field: str = typer.Option(...)):
139143
strict : bool
140144
If True, fail if a field in the model does not correspond to a function
141145
parameter of the same name with a typer.OptionInfo as a default value.
146+
remove: Optional[List[str]]
147+
List of strings to remove from descriptions before injecting them.
142148
**field_additions
143149
Additional help text to add to the help attribute of a field.
144150
The parameter name should be the name of the field, and the value
@@ -164,7 +170,13 @@ def decorator(func: Any) -> Any:
164170
addition = field_additions.get(field_name, "")
165171
if addition:
166172
addition = f" {addition}" # add leading space
167-
param.default.help = f"{field.description or ''}{addition}"
173+
description = field.description or ""
174+
if remove:
175+
for to_remove in remove:
176+
# Could this be faster with a regex?
177+
description = description.replace(to_remove, "")
178+
description = description.strip()
179+
param.default.help = f"{description}{addition}"
168180
return func
169181

170182
return decorator

Diff for: pyproject.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ classifiers = [
2929
]
3030
dependencies = [
3131
"typer==0.9.0",
32-
"harborapi>=0.25.1",
32+
"harborapi>=0.25.2",
3333
"pydantic>=2.7.4",
3434
"trogon>=0.5.0",
3535
"platformdirs>=2.5.4",
@@ -134,7 +134,7 @@ typeCheckingMode = "strict"
134134

135135
[tool.ruff]
136136
src = ["harbor_cli"]
137-
extend-exclude = ["tests", "harbor_cli/__init__.py"]
137+
extend-exclude = ["harbor_cli/__init__.py"]
138138

139139
[tool.ruff.lint]
140140
extend-select = ["I"]

0 commit comments

Comments
 (0)