Skip to content

Commit e871e49

Browse files
Kevin Allioliclaude
andcommitted
release: v1.0.1 — Infomaniak prod test fixes
Bugs and UX issues found during end-to-end testing on the Infomaniak public cloud (dc3-a, non-admin user). All items validated live. P1 (prod blockers): - server ssh: drop permissive orca-* glob fallback, recognise .pem/.key variants, reuse _find_ssh_key in port-forward - backup: clarify docstring — this group is Freezer (DR); Cinder volume backups live under `orca volume backup-*` P2 (UX): - client: distinguish 401 (AuthenticationError, re-auth) from 403 (PermissionDeniedError — valid token, insufficient role) across all HTTP helpers (core + image/container/object_store) - client: sniff HTML error pages (text/html or <!doctype/<html prefix) and surface "endpoint advertised in catalogue but not exposed" - event list: drop columns whose values are all empty; switch long UUID columns from no_wrap to overflow=fold to avoid Rich zero-width edges - server ssh: boot-from-volume distro detection via the attached volume's volume_image_metadata.os_distro (fixes Debian 12 BFV → root) P3 (polish): - server shelve/unshelve: add --wait (SHELVED_OFFLOADED / ACTIVE) - flavor list: --limit is now optional; auto-paginate via Nova marker - audit: ICMPv6 open to 0.0.0.0/0 downgraded CRITICAL → MEDIUM (ND/MLD per RFC 4861 is expected baseline) - recordset list: split SOA single-record on whitespace, render NS/MX/TXT multi-values one-per-line, ID column overflow=fold Infrastructure: - main.py: auto-discover commands in orca_cli/commands/*.py (modules can now expose multiple top-level groups with no main.py churn) - new `orca find` top-level command for cross-service resource lookup - pyproject: add pytest-cov + [tool.coverage.*]; coverage gate at 85% (current 85.47%); add optional docs group (mkdocs-material, mkdocs-click) for GitHub Pages build - tests: +7 new modules (cache, completions, context, exceptions, find, main, output, server_ssh, validators); expand client, config, event, image, setup, wait, watch, wizard coverage Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent c610044 commit e871e49

39 files changed

Lines changed: 3523 additions & 274 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
strategy:
3232
fail-fast: false
3333
matrix:
34-
python-version: ["3.9", "3.10", "3.11", "3.12"]
34+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
3535

3636
steps:
3737
- uses: actions/checkout@v4
@@ -46,4 +46,6 @@ jobs:
4646
poetry install --with dev
4747
4848
- name: Run tests
49-
run: poetry run pytest tests/ -v --tb=short
49+
run: >
50+
poetry run pytest tests/ -v --tb=short
51+
--cov=orca_cli --cov-report=term --cov-fail-under=85

.github/workflows/deploy-docs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ jobs:
2323

2424
- name: Install dependencies
2525
run: |
26-
pip install mkdocs-material
27-
pip install -r requirements.txt || true
26+
pip install poetry
27+
poetry install --with docs
2828
2929
- name: Build documentation
30-
run: mkdocs build
30+
run: poetry run mkdocs build
3131

3232
- name: Deploy to GitHub Pages (gh-pages branch)
3333
uses: peaceiris/actions-gh-pages@v3

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@ build/
1111
.mypy_cache/
1212
.pytest_cache/
1313
.ruff_cache/
14+
.coverage
15+
htmlcov/
1416
site/
1517
CLAUDE.md

docs/reference.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CLI Reference
2+
3+
This page is **auto-generated from the running CLI** via [mkdocs-click][mkc] — it never goes stale. Every command, option, flag, and environment variable shown here is introspected directly from the `orca` binary at build time.
4+
5+
For tutorial-style content, see the curated [command pages](commands/index.md).
6+
7+
[mkc]: https://mkdocs-click.readthedocs.io/
8+
9+
---
10+
11+
::: mkdocs-click
12+
:module: orca_cli.main
13+
:command: cli
14+
:prog_name: orca
15+
:depth: 1
16+
:style: table

mkdocs.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ markdown_extensions:
4242
permalink: true
4343
- attr_list
4444
- md_in_html
45+
- mkdocs-click
4546

4647
nav:
4748
- Home: index.md
4849
- Getting Started: getting-started.md
50+
- CLI Reference: reference.md
4951
- Commands:
5052
- Overview: commands/index.md
5153
- Orca-exclusive:

orca_cli/commands/audit.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,19 @@ def audit(ctx: click.Context) -> None:
5353

5454
# Fully open (all ports)
5555
if port_min is None and port_max is None:
56-
findings.append(("CRITICAL", f"SG: {sg_name}",
57-
sg["id"],
58-
f"All {proto or 'any'} ports open to {remote}"))
56+
# ICMPv6 is required for IPv6 Neighbor Discovery (RFC 4861)
57+
# and ICMPv4 for path-MTU / basic reachability — don't flag
58+
# these as CRITICAL; surface as MEDIUM so users see the
59+
# rule but aren't alarmed by an expected baseline.
60+
if proto in ("icmp", "ipv6-icmp", "icmpv6"):
61+
findings.append(("MEDIUM", f"SG: {sg_name}",
62+
sg["id"],
63+
f"All {proto} types open to {remote} "
64+
f"(expected for ND/ping — consider restricting)"))
65+
else:
66+
findings.append(("CRITICAL", f"SG: {sg_name}",
67+
sg["id"],
68+
f"All {proto or 'any'} ports open to {remote}"))
5969
continue
6070

6171
# Check for dangerous ports

orca_cli/commands/backup.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ def _freezer(client) -> str:
1919
@click.group()
2020
@click.pass_context
2121
def backup(ctx: click.Context) -> None:
22-
"""Manage backups, jobs, sessions & clients (Freezer)."""
22+
"""Manage Freezer backups, jobs, sessions & clients.
23+
24+
For Cinder volume backups see ``orca volume backup-list`` etc.
25+
"""
2326
pass
2427

2528

orca_cli/commands/container.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ def _human_size(num_bytes: int | float | str | None) -> str:
2727
def _head(client, url: str) -> dict[str, str]:
2828
"""Perform a HEAD request and return the response headers as a dict."""
2929
resp = client._http.head(url, headers=client._headers())
30-
if resp.status_code in (401, 403):
30+
if resp.status_code == 401:
3131
from orca_cli.core.exceptions import AuthenticationError
3232
raise AuthenticationError()
33+
if resp.status_code == 403:
34+
from orca_cli.core.exceptions import PermissionDeniedError
35+
raise PermissionDeniedError()
3336
if not resp.is_success:
3437
from orca_cli.core.exceptions import APIError
3538
raise APIError(resp.status_code, resp.text[:300])
@@ -42,9 +45,12 @@ def _post_no_body(client, url: str, extra_headers: dict[str, str] | None = None)
4245
if extra_headers:
4346
headers.update(extra_headers)
4447
resp = client._http.post(url, headers=headers)
45-
if resp.status_code in (401, 403):
48+
if resp.status_code == 401:
4649
from orca_cli.core.exceptions import AuthenticationError
4750
raise AuthenticationError()
51+
if resp.status_code == 403:
52+
from orca_cli.core.exceptions import PermissionDeniedError
53+
raise PermissionDeniedError()
4854
if not resp.is_success:
4955
from orca_cli.core.exceptions import APIError
5056
raise APIError(resp.status_code, resp.text[:300])
@@ -129,9 +135,12 @@ def container_create(ctx: click.Context, container_name: str) -> None:
129135
url = f"{base}/{container_name}"
130136
headers = client._headers()
131137
resp = client._http.put(url, headers=headers)
132-
if resp.status_code in (401, 403):
138+
if resp.status_code == 401:
133139
from orca_cli.core.exceptions import AuthenticationError
134140
raise AuthenticationError()
141+
if resp.status_code == 403:
142+
from orca_cli.core.exceptions import PermissionDeniedError
143+
raise PermissionDeniedError()
135144
if not resp.is_success:
136145
from orca_cli.core.exceptions import APIError
137146
raise APIError(resp.status_code, resp.text[:300])

orca_cli/commands/event.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,22 @@ def _colored_action(action: str) -> str:
5959
return f"[{color}]{action}[/{color}]"
6060

6161

62+
def _drop_empty_columns(items: list[dict], column_defs: list[tuple]) -> list[tuple]:
63+
"""Return only columns that have at least one non-empty value across all rows."""
64+
keep: list[tuple] = []
65+
for cd in column_defs:
66+
key = cd[1]
67+
has_value = False
68+
for item in items:
69+
val = key(item) if callable(key) else item.get(key, "")
70+
if val not in (None, "", "—"):
71+
has_value = True
72+
break
73+
if has_value:
74+
keep.append(cd)
75+
return keep or column_defs # never strip everything — at least show headers
76+
77+
6278
def _colored_result(result: str | None) -> str:
6379
if not result:
6480
return ""
@@ -109,13 +125,17 @@ def event_list(
109125
def _action_styled(item: dict) -> str:
110126
return _colored_action(item.get("action", ""))
111127

112-
column_defs = [
128+
column_defs: list[tuple] = [
113129
("Action", _action_styled),
114-
("Request ID", "request_id", {"no_wrap": True}),
130+
("Request ID", "request_id", {"overflow": "fold"}),
115131
("Start Time", lambda i: _format_ts(i.get("start_time"))),
116-
("User ID", "user_id", {"no_wrap": True}),
132+
("User ID", "user_id", {"overflow": "fold"}),
117133
("Message", lambda i: i.get("message") or ""),
118134
]
135+
# Drop columns where every value is empty — avoids the "empty column at
136+
# start/end" rendering artifact Rich produces when wide no-wrap columns
137+
# force siblings to zero width on a narrow terminal.
138+
column_defs = _drop_empty_columns(actions, column_defs)
119139

120140
print_list(
121141
actions,

0 commit comments

Comments
 (0)