Skip to content

Commit

Permalink
Add project repos command, fix type checking issues (#99)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pederhan authored Jun 26, 2024
1 parent 6125350 commit ddc2921
Show file tree
Hide file tree
Showing 44 changed files with 244 additions and 156 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@ The **third number** is the patch version (bug fixes)

## Unreleased

### Added

- Command: `project repos` to list repositories in a project. A more intuitive way to list repositories for a project than going through `repository list`.
- `--sbom-generation` option for `project create` and `project update` commands to enable automatic SBOM generation for the project.

### Changed

- Styling of multiline help text in commands.

### Removed

- Mentions of valid values from `project {create,update}` commands.

### Fixed

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

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

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

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


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

### Added
Expand Down
4 changes: 2 additions & 2 deletions harbor_cli/commands/api/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ def create_artifact_tag(
"""Create a tag for an artifact."""
an = parse_artifact_name(artifact)
# NOTE: We might need to fetch repo and artifact IDs
t = Tag(name=tag) # pyright: ignore[reportCallIssue]
t = Tag(name=tag)
location = state.run(
state.client.create_artifact_tag(an.project, an.repository, an.reference, t),
f"Creating tag {tag!r} for {artifact}...",
Expand Down Expand Up @@ -393,7 +393,7 @@ def add_artifact_label(
description=description,
color=color,
scope=scope,
) # pyright: ignore[reportCallIssue]
)
state.run(
state.client.add_artifact_label(an.project, an.repository, an.reference, label),
f"Adding label {label.name!r} to {artifact}...",
Expand Down
2 changes: 1 addition & 1 deletion harbor_cli/commands/api/auditlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def _get_schedule(
return Schedule(
parameters=params,
schedule=ScheduleObj(**obj_kwargs),
) # pyright: ignore[reportCallIssue]
)


# HarborAsyncClient.create_audit_log_rotation_schedule()
Expand Down
2 changes: 1 addition & 1 deletion harbor_cli/commands/api/cve_allowlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def clear_allowlist(
) -> None:
"""Clear the current CVE allowlist of all CVEs, and optionally all metadata as well."""
if full_clear:
allowlist = CVEAllowlist(items=[]) # pyright: ignore[reportCallIssue] # create a whole new allowlist
allowlist = CVEAllowlist(items=[]) # create a whole new allowlist
else:
# Fetch existing allowlist to preserve metadata
allowlist = state.run(
Expand Down
4 changes: 2 additions & 2 deletions harbor_cli/commands/api/gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ def create_gc_schedule(
schedule_obj = ScheduleObj(
type=type,
cron=cron,
) # pyright: ignore[reportCallIssue]
)
# TODO: investigate which parameters the `parameters` field takes
schedule = Schedule(schedule=schedule_obj) # pyright: ignore[reportCallIssue]
schedule = Schedule(schedule=schedule_obj)
state.run(
state.client.create_gc_schedule(schedule),
"Creating Garbage Collection schedule...",
Expand Down
81 changes: 65 additions & 16 deletions harbor_cli/commands/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,39 @@ def get_project_info(
render_result(p, ctx)


# HarborAsyncClient.get_repositories()
@app.command("repos")
@inject_resource_options()
def list_repos(
ctx: typer.Context,
project: Optional[str] = typer.Argument(
help="Name of project to fetch repositories from.",
),
query: Optional[str] = None,
sort: Optional[str] = None,
page: int = 1,
page_size: int = 10,
limit: Optional[int] = ...,
) -> None:
"""List all repositories in a project.
Alternative to `repository list`"""
repos = state.run(
state.client.get_repositories(
project,
query=query,
sort=sort,
page=page,
page_size=page_size,
limit=limit,
),
"Fetching repositories...",
)
if not repos:
info(f"{project!r} has no repositories.")
render_result(repos, ctx)


# HarborAsyncClient.get_project_logs()
@app.command("logs")
@inject_resource_options()
Expand Down Expand Up @@ -143,7 +176,8 @@ def project_exists(
@app.command("create", no_args_is_help=True)
@inject_help(ProjectReq)
@inject_help(
ProjectMetadata
ProjectMetadata,
remove=['The valid values are "true", "false".'],
) # inject this first so its "public" field takes precedence
def create_project(
ctx: typer.Context,
Expand Down Expand Up @@ -196,23 +230,32 @@ def create_project(
None,
"--retention-id",
),
auto_sbom_generation: Optional[bool] = typer.Option(
None,
"--sbom-generation",
is_flag=False,
),
# TODO: add support for adding CVE allowlist when creating a project
) -> None:
"""Create a new project."""
project_req = ProjectReq(
project_name=project_name,
storage_limit=storage_limit,
registry_id=registry_id,
metadata=ProjectMetadata(
# validator does bool -> str conversion for the string bool fields
public=public, # type: ignore
enable_content_trust=enable_content_trust,
enable_content_trust_cosign=enable_content_trust_cosign,
prevent_vul=prevent_vul,
severity=severity,
auto_scan=auto_scan,
reuse_sys_cve_allowlist=reuse_sys_cve_allowlist,
retention_id=retention_id,
metadata=ProjectMetadata.model_validate(
# NOTE: Constructing via a dict here to avoid type checking noise.
# See tests/api/test_models.py for more information.
{
"public": public,
"enable_content_trust": enable_content_trust,
"enable_content_trust_cosign": enable_content_trust_cosign,
"prevent_vul": prevent_vul,
"severity": severity,
"auto_scan": auto_scan,
"reuse_sys_cve_allowlist": reuse_sys_cve_allowlist,
"retention_id": retention_id,
"auto_sbom_generation": auto_sbom_generation,
}
),
)
location = state.run(
Expand Down Expand Up @@ -283,7 +326,8 @@ def list_projects(
@app.command("update", no_args_is_help=True)
@inject_help(ProjectReq)
@inject_help(
ProjectMetadata
ProjectMetadata,
remove=['The valid values are "true", "false".'],
) # inject this first so its "public" field takes precedence
def update_project(
ctx: typer.Context,
Expand Down Expand Up @@ -336,6 +380,11 @@ def update_project(
None,
"--retention-id",
),
auto_sbom_generation: Optional[bool] = typer.Option(
None,
"--sbom-generation",
is_flag=False,
),
) -> None:
"""Update project information."""
req_params = model_params_from_ctx(ctx, ProjectReq)
Expand All @@ -346,7 +395,7 @@ def update_project(
arg = get_project_arg(project_name_or_id)
project = get_project(arg)
if project.metadata is None:
project.metadata = ProjectMetadata() # pyright: ignore[reportCallIssue] # mypy bug
project.metadata = ProjectMetadata()

# Create updated models from params
req = create_updated_model(
Expand Down Expand Up @@ -748,9 +797,9 @@ def list_project_members(
entity_name: Optional[str] = typer.Option(
None, "--entity", help="Entity name to search for."
),
page: int = ..., # type: ignore
page_size: int = ..., # type: ignore
limit: Optional[int] = ..., # type: ignore
page: int = ...,
page_size: int = ...,
limit: Optional[int] = ...,
) -> None:
"""List all members of a project."""
project_arg = get_project_arg(project_name_or_id)
Expand Down
2 changes: 1 addition & 1 deletion harbor_cli/commands/api/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def create_registry(
type=type,
insecure=insecure,
description=description,
) # pyright: ignore[reportCallIssue]
)
location = state.run(state.client.create_registry(registry), "Creating registry...")
render_result(location, ctx)

Expand Down
2 changes: 1 addition & 1 deletion harbor_cli/commands/api/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def list_repos(
sort: Optional[str] = None,
page: int = 1,
page_size: int = 10,
limit: Optional[int] = ..., # type: ignore
limit: Optional[int] = ...,
) -> None:
"""List repositories in all projects or a specific project."""
repos = state.run(
Expand Down
12 changes: 6 additions & 6 deletions harbor_cli/commands/api/retention.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ def list_retention_jobs(
ctx: typer.Context,
project_name_or_id: Optional[str] = ARG_PROJECT_NAME_OR_ID_OPTIONAL,
policy_id: Optional[int] = OPTION_POLICY_ID,
page: int = ..., # type: ignore
page_size: int = ..., # type: ignore
limit: Optional[int] = ..., # type: ignore
page: int = ...,
page_size: int = ...,
limit: Optional[int] = ...,
) -> None:
"""List retention jobs."""
policy_id = policy_id_from_args(project_name_or_id, policy_id)
Expand Down Expand Up @@ -266,9 +266,9 @@ def list_retention_tasks(
project_name_or_id: Optional[str] = ARG_PROJECT_NAME_OR_ID_OPTIONAL,
job_id: int = typer.Argument(help="ID of the job to list tasks for."),
policy_id: Optional[int] = OPTION_POLICY_ID,
page: int = ..., # type: ignore
page_size: int = ..., # type: ignore
limit: Optional[int] = ..., # type: ignore
page: int = ...,
page_size: int = ...,
limit: Optional[int] = ...,
) -> None:
"""List retention tasks."""
policy_id = policy_id_from_args(project_name_or_id, policy_id)
Expand Down
2 changes: 1 addition & 1 deletion harbor_cli/commands/api/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def start_scan_export(
) -> None:
# TODO: resolve label names to IDs (?)

req = ScanDataExportRequest() # pyright: ignore[reportCallIssue]
req = ScanDataExportRequest()
if job_name:
req.job_name = job_name
if cve:
Expand Down
2 changes: 1 addition & 1 deletion harbor_cli/commands/api/scanall.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def create_scanall_schedule(
cron: Optional[str] = typer.Option(None),
) -> None:
params = model_params_from_ctx(ctx, ScheduleObj)
schedule = Schedule(schedule=ScheduleObj(**params)) # pyright: ignore[reportCallIssue]
schedule = Schedule(schedule=ScheduleObj(**params))
state.run(
state.client.create_scan_all_schedule(schedule),
"Creating 'Scan All' schedule...",
Expand Down
4 changes: 2 additions & 2 deletions harbor_cli/commands/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,14 @@ def unset_user_admin(
def set_user_password(
username_or_id: str = ARG_USERNAME_OR_ID,
old_password: str = typer.Option(
..., # type: ignore # pyright unable to infer type?
..., # type: ignore # pyright unable to infer type?
"--old-password",
prompt="Enter old password",
hide_input=True,
help="Old password for user. Prompted if not provided.",
),
new_password: str = typer.Option(
..., # type: ignore
..., # type: ignore # pyright unable to infer type?
"--new-password",
prompt="Enter new password",
hide_input=True,
Expand Down
12 changes: 6 additions & 6 deletions harbor_cli/commands/api/usergroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def create_usergroup(
group_name=group_name,
group_type=group_type.as_int(),
ldap_group_dn=ldap_group_dn,
) # pyright: ignore[reportCallIssue]
)
location = state.run(
state.client.create_usergroup(usergroup),
f"Creating user group {group_name}...",
Expand All @@ -77,7 +77,7 @@ def update_usergroup(
# NOTE: make group_name optional if we can update other fields in the future
) -> None:
"""Update a user group. Only the name can be updated currently."""
usergroup = UserGroup(group_name=group_name) # pyright: ignore[reportCallIssue]
usergroup = UserGroup(group_name=group_name)
state.run(
state.client.update_usergroup(group_id, usergroup),
f"Updating user group {group_id}...",
Expand Down Expand Up @@ -115,9 +115,9 @@ def get_usergroups(
"--group-name",
help="Group name to filter by (fuzzy matching).",
),
page: int = ..., # type: ignore
page_size: int = ..., # type: ignore
limit: Optional[int] = ..., # type: ignore
page: int = ...,
page_size: int = ...,
limit: Optional[int] = ...,
) -> None:
"""List user groups."""
usergroups = state.run(
Expand All @@ -141,7 +141,7 @@ def search_usergroups(
group_name: str = typer.Argument(help="Name of group to search for."),
page: int = 1,
page_size: int = 10,
# limit: Optional[int] = ..., # type: ignore # NYI in harborapi
# limit: Optional[int] = ..., # NYI in harborapi
) -> None:
"""Search for user groups by name."""
usergroups = state.run(
Expand Down
16 changes: 14 additions & 2 deletions harbor_cli/utils/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from functools import lru_cache
from typing import Any
from typing import List
from typing import Optional
from typing import Type

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


def inject_help(
model: Type[BaseModel], strict: bool = False, **field_additions: str
model: Type[BaseModel],
strict: bool = False,
remove: Optional[List[str]] = None,
**field_additions: str,
) -> Any:
"""
Injects a Pydantic model's field descriptions into the help attributes
Expand Down Expand Up @@ -139,6 +143,8 @@ def my_command(my_field: str = typer.Option(...)):
strict : bool
If True, fail if a field in the model does not correspond to a function
parameter of the same name with a typer.OptionInfo as a default value.
remove: Optional[List[str]]
List of strings to remove from descriptions before injecting them.
**field_additions
Additional help text to add to the help attribute of a field.
The parameter name should be the name of the field, and the value
Expand All @@ -164,7 +170,13 @@ def decorator(func: Any) -> Any:
addition = field_additions.get(field_name, "")
if addition:
addition = f" {addition}" # add leading space
param.default.help = f"{field.description or ''}{addition}"
description = field.description or ""
if remove:
for to_remove in remove:
# Could this be faster with a regex?
description = description.replace(to_remove, "")
description = description.strip()
param.default.help = f"{description}{addition}"
return func

return decorator
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ classifiers = [
]
dependencies = [
"typer==0.9.0",
"harborapi>=0.25.1",
"harborapi>=0.25.2",
"pydantic>=2.7.4",
"trogon>=0.5.0",
"platformdirs>=2.5.4",
Expand Down Expand Up @@ -134,7 +134,7 @@ typeCheckingMode = "strict"

[tool.ruff]
src = ["harbor_cli"]
extend-exclude = ["tests", "harbor_cli/__init__.py"]
extend-exclude = ["harbor_cli/__init__.py"]

[tool.ruff.lint]
extend-select = ["I"]
Expand Down
Loading

0 comments on commit ddc2921

Please sign in to comment.