Skip to content

Commit f1f7c54

Browse files
committed
feat: first-time user DX — guided hints, friendly errors, "did you mean?"
- Friendly error when no kubeconfig found: links to quickstart guide instead of scary "Internal error: Invalid kube-config file" - "Did you mean?" suggestions when commands are mistyped (e.g. `logs` → `services:logs`, `scale` → `ps:scale`) - Next steps hints after apps:create, deploy, and services:expose:on guiding users through the full create → deploy → access flow - Log streaming Ctrl+C hint on its own line for visibility - SECURITY.md: full security architecture (secrets, RBAC, local storage) - README: added DigitalOcean, services:connect/expose:on in quickstart - Release workflow: native ARM64 runner replaces slow QEMU build - Test resilience: tighter polling, join timeouts, monkeypatch env vars - Bump to v0.2.0
1 parent b1efe72 commit f1f7c54

15 files changed

Lines changed: 222 additions & 120 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ jobs:
9191
- name: Publish to PyPI
9292
uses: pypa/gh-action-pypi-publish@release/v1
9393

94-
# ── Native binaries (4 targets) ─────────────────────────────────
94+
# ── Native binaries (5 targets) ─────────────────────────────────
9595

9696
binaries:
9797
name: Binary — ${{ matrix.os }}-${{ matrix.arch }}
@@ -105,6 +105,10 @@ jobs:
105105
arch: amd64
106106
runner: ubuntu-latest
107107
asset: kuberoku-linux-amd64
108+
- os: linux
109+
arch: arm64
110+
runner: ubuntu-24.04-arm
111+
asset: kuberoku-linux-arm64
108112
- os: darwin
109113
arch: arm64
110114
runner: macos-latest
@@ -159,44 +163,11 @@ jobs:
159163
name: ${{ matrix.asset }}
160164
path: dist/${{ matrix.asset }}
161165

162-
# ── Linux ARM64 via QEMU ────────────────────────────────────────
163-
164-
binary-linux-arm64:
165-
name: Binary — linux-arm64 (QEMU)
166-
needs: gate
167-
runs-on: ubuntu-latest
168-
steps:
169-
- uses: actions/checkout@v4
170-
171-
- name: Set up QEMU
172-
uses: docker/setup-qemu-action@v3
173-
174-
- name: Build in ARM64 container
175-
run: |
176-
docker run --rm --platform linux/arm64 \
177-
-v "${{ github.workspace }}:/work" \
178-
-w /work \
179-
python:3.12-slim \
180-
sh -c '
181-
apt-get update && apt-get install -y --no-install-recommends binutils &&
182-
pip install --upgrade pip &&
183-
pip install . &&
184-
pip install pyinstaller &&
185-
pyinstaller kuberoku.spec &&
186-
mv dist/kuberoku dist/kuberoku-linux-arm64
187-
'
188-
189-
- name: Upload artifact
190-
uses: actions/upload-artifact@v4
191-
with:
192-
name: kuberoku-linux-arm64
193-
path: dist/kuberoku-linux-arm64
194-
195166
# ── GitHub Release ──────────────────────────────────────────────
196167

197168
release:
198169
name: Create GitHub Release
199-
needs: [publish, binaries, binary-linux-arm64]
170+
needs: [publish, binaries]
200171
runs-on: ubuntu-latest
201172
permissions:
202173
contents: write

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,14 @@ Requires `kubectl` configured with a valid kubeconfig. The Python install requir
3939

4040
### Already have a cluster?
4141

42-
Five commands from zero to a running app:
42+
From zero to a running app accessible on the internet:
4343

4444
```bash
4545
kuberoku apps:create myapi
4646
kuberoku deploy --app myapi --image nginx:1.27 --port 80/tcp
47+
kuberoku services:connect --app myapi # test locally
48+
kuberoku services:expose:on --app myapi web # get a public IP
4749
kuberoku config:set --app myapi GREETING=hello SECRET_KEY=abc123
48-
kuberoku ps:scale --app myapi web=3
4950
kuberoku services:logs --app myapi --tail
5051
```
5152

@@ -132,6 +133,7 @@ Then run the five commands above.
132133
| **AWS** | `eksctl create cluster --name dev` ([docs](https://eksctl.io)) |
133134
| **GCP** | `gcloud container clusters create dev` ([docs](https://cloud.google.com/kubernetes-engine/docs/deploy-app-cluster)) |
134135
| **Azure** | `az aks create -g dev -n dev` ([docs](https://learn.microsoft.com/en-us/azure/aks/learn/quick-kubernetes-deploy-cli)) |
136+
| **DigitalOcean** | `doctl kubernetes cluster create dev` ([docs](https://docs.digitalocean.com/products/kubernetes/getting-started/quickstart/)) |
135137

136138
Kuberoku works on any conformant K8s cluster (1.33+). No special setup required.
137139

SECURITY.md

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,76 @@ Include:
1616

1717
You'll receive a response within 72 hours.
1818

19-
## Scope
19+
## Security architecture
2020

21-
Kuberoku is a CLI/SDK that talks to the Kubernetes API. It does not run server-side components. Security concerns include:
21+
Kuberoku is a **client-side only** tool. There are no server-side components, no daemons, no webhooks, and no admission controllers installed on your cluster. The CLI talks directly to the Kubernetes API using your existing kubeconfig credentials.
2222

23-
- Secret value leakage in CLI output, logs, or crash reports
24-
- Command injection via user-supplied app names, config values, or arguments
25-
- Unsafe handling of kubeconfig credentials
26-
- Dependency vulnerabilities
23+
### What is stored locally
2724

28-
## What Kuberoku does NOT protect
25+
| Location | Contents |
26+
|---|---|
27+
| `~/.kube/config` | Standard kubeconfig (not managed by Kuberoku) |
28+
| `~/.kuberoku/config.yaml` | Cluster registry: context names, namespaces, resource prefixes. **No credentials or secrets.** |
29+
| `.kuberoku` (project dir) | Optional project marker with default app name. No secrets. |
2930

30-
- Secrets at rest in K8s (cluster admin responsibility: EncryptionConfiguration)
31-
- RBAC enforcement within a namespace (K8s limitation)
32-
- Network-level encryption (cluster responsibility: mTLS, network policies)
31+
**That's it.** Kuberoku does not store credentials, tokens, secrets, or sensitive data on disk. It delegates all authentication to your kubeconfig and the Kubernetes client library.
3332

34-
See `docs/NORTHSTAR.txt` Section 1 for the full security model.
33+
### How secrets are handled
34+
35+
- **Config vars** set with `config:set --secret` are stored as [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) using `stringData` (not base64-encoded `data`).
36+
- Secrets are **never written to local disk**, logs, or crash reports.
37+
- Secret values are **masked in CLI output** — only key names are shown.
38+
- Addon credentials (database passwords, etc.) are stored in K8s Secrets and injected into pods via environment variables.
39+
- **Secrets at rest encryption** is your cluster's responsibility (`EncryptionConfiguration`). Kuberoku uses whatever protection your cluster provides.
40+
41+
### Network posture
42+
43+
- **Outbound only**: CLI → Kubernetes API server (HTTPS). No inbound connections.
44+
- No telemetry, analytics, or phone-home behavior.
45+
- Plugin install/search talks to PyPI (HTTPS) — only when explicitly invoked.
46+
47+
### RBAC permissions
48+
49+
Kuberoku follows least-privilege. Run `kuberoku clusters:doctor` to audit your permissions, or `clusters:doctor --fix` to generate minimal Role/RoleBinding YAML.
50+
51+
**Required** (core functionality):
52+
53+
| Resource | API Group | Verbs | Used by |
54+
|---|---|---|---|
55+
| namespaces | core | get, list | apps |
56+
| configmaps | core | get, list, create, update, patch, delete | apps, config, releases |
57+
| deployments | apps | get, list, create, update, patch, delete | deploy, ps |
58+
| pods | core | get, list, delete | ps, restart |
59+
| pods/log | core | get | logs |
60+
| pods/exec | core | create | exec |
61+
| services | core | get, list, create, update, delete | deploy, addons |
62+
| jobs | batch | get, list, create, delete | run |
63+
64+
**Elevated** (optional features — only needed if you use them):
65+
66+
| Resource | API Group | Verbs | Used by |
67+
|---|---|---|---|
68+
| secrets | core | get, list, create, update, patch, delete | config --secret |
69+
| ingresses | networking.k8s.io | get, list, create, update, delete | domains |
70+
| statefulsets | apps | get, list, create, update, patch, delete | addons |
71+
| persistentvolumeclaims | core | get, list, create, delete | addons |
72+
| networkpolicies | networking.k8s.io | get, list, create, update, delete | networking |
73+
74+
### Scope of responsibility
75+
76+
| Kuberoku handles | Cluster admin handles |
77+
|---|---|
78+
| Safe CLI input validation | Secrets at rest encryption (`EncryptionConfiguration`) |
79+
| No secrets in logs/output | RBAC enforcement between namespaces |
80+
| Namespace isolation of resources | Network-level encryption (mTLS) |
81+
| Minimal RBAC requirements | Cluster authentication (OIDC, certificates) |
82+
83+
### Dependencies
84+
85+
Kuberoku has 3 runtime dependencies, minimizing supply chain surface:
86+
87+
- `kubernetes` — official Kubernetes Python client
88+
- `click` — CLI framework
89+
- `pyyaml` — YAML parsing
90+
91+
All PyPI releases use [OIDC trusted publishing](https://docs.pypi.org/trusted-publishers/) (no long-lived API tokens).

src/kuberoku/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.0"
1+
__version__ = "0.2.0"

src/kuberoku/cli/apps.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import click
88

99
from kuberoku.cli.context import resolve_app
10-
from kuberoku.cli.output import info, render_detail, render_table, success
10+
from kuberoku.cli.output import hint, info, render_detail, render_table, success
1111
from kuberoku.config.project import (
1212
remove_alias,
1313
remove_project_file,
@@ -58,6 +58,10 @@ def create(ctx: click.Context, name: str, cluster: str | None) -> None:
5858
factory = ctx.obj["factory"]
5959
app = factory.apps.create(name, on_step=lambda msg: info(msg))
6060
success(f"Created app {app.name}.")
61+
hint("\nNext steps:")
62+
hint(f" kuberoku deploy --app {app.name} --image <your-image> --port 80/tcp")
63+
hint(f" kuberoku config:set --app {app.name} KEY=value")
64+
hint(f" kuberoku apps:info --app {app.name}")
6165

6266

6367
@apps.command()

src/kuberoku/cli/deploy.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import click
88

99
from kuberoku.cli.context import resolve_app
10-
from kuberoku.cli.output import info, render_detail, success
10+
from kuberoku.cli.output import hint, info, render_detail, success
1111
from kuberoku.sdk.deploy import parse_port
1212

1313

@@ -75,3 +75,7 @@ def deploy(
7575
"Processes": ", ".join(release.images.keys()),
7676
},
7777
)
78+
hint("\nNext steps:")
79+
hint(f" kuberoku services:connect --app {app_name} # access locally")
80+
hint(f" kuberoku services:expose:on --app {app_name} web # expose to internet")
81+
hint(f" kuberoku services:logs --app {app_name} --tail")

src/kuberoku/cli/main.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ class ColonCommandGroup(click.Group):
2929
Both `kuberoku apps:create` and `kuberoku apps create` work identically.
3030
This overrides resolve_command() — Click's intended extension point for
3131
custom command resolution.
32+
33+
When a bare command name isn't found (e.g. ``logs``), searches all
34+
registered groups for a matching subcommand and suggests the full
35+
colon syntax (e.g. ``services:logs``).
3236
"""
3337

3438
def resolve_command(
@@ -39,7 +43,26 @@ def resolve_command(
3943
if cmd_name and ":" in cmd_name:
4044
parts = cmd_name.split(":", 1)
4145
args = [parts[0], parts[1]] + args[1:]
42-
return super().resolve_command(ctx, args)
46+
try:
47+
return super().resolve_command(ctx, args)
48+
except click.UsageError:
49+
# Command not found — search subgroups for a match
50+
if cmd_name and ":" not in cmd_name:
51+
suggestions = self._find_in_groups(cmd_name)
52+
if suggestions:
53+
hint = ", ".join(f"'{s}'" for s in suggestions)
54+
raise click.UsageError(
55+
f"No such command '{cmd_name}'. Did you mean {hint}?"
56+
) from None
57+
raise
58+
59+
def _find_in_groups(self, name: str) -> list[str]:
60+
"""Search all registered groups for a subcommand matching name."""
61+
matches: list[str] = []
62+
for group_name, cmd in (self.commands or {}).items():
63+
if isinstance(cmd, click.Group) and cmd.get_command(None, name): # type: ignore[arg-type]
64+
matches.append(f"{group_name}:{name}")
65+
return matches
4366

4467

4568
@click.group(cls=ColonCommandGroup)

src/kuberoku/cli/output.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ def info(message: str) -> None:
3535
click.echo(message)
3636

3737

38+
def hint(message: str) -> None:
39+
"""Print a hint/next-step message."""
40+
click.echo(message, err=True)
41+
42+
3843
def render_table(
3944
items: list[Any],
4045
columns: list[str],

src/kuberoku/cli/services.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import click
1010

1111
from kuberoku.cli.context import resolve_app
12-
from kuberoku.cli.output import info, render_detail, success
12+
from kuberoku.cli.output import hint, info, render_detail, success
1313
from kuberoku.models import LogLine
1414

1515

@@ -125,6 +125,8 @@ def expose_on(
125125
if endpoint.ports:
126126
fields["Ports"] = ", ".join(str(p) for p in endpoint.ports)
127127
render_detail(f"{app_name} expose", fields)
128+
hint("\nMap a custom domain:")
129+
hint(f" kuberoku domains:add --app {app_name} myapp.example.com")
128130

129131

130132
@expose.command("off")
@@ -341,7 +343,8 @@ def on_line(line: LogLine) -> None:
341343
ts_prefix = f"{line.timestamp.isoformat()} " if timestamps else ""
342344
click.echo(f"{ts_prefix}{line.dyno} | {line.message}")
343345

344-
info(f"Streaming logs for app '{app_name}'... (Ctrl+C to stop)")
346+
info(f"Streaming logs for app '{app_name}'...")
347+
info("Press Ctrl+C to stop.\n")
345348
try:
346349
factory.services.stream_logs(
347350
app_name,

src/kuberoku/exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@ class ClusterUnreachableError(KuberokuError):
136136
"""Raised when the K8s API server can't be reached."""
137137

138138

139+
class NoKubeConfigError(KuberokuError):
140+
"""Raised when no kubeconfig file is found (first-time user)."""
141+
142+
def __init__(self) -> None:
143+
super().__init__(
144+
"No Kubernetes cluster connected.\n"
145+
"\n"
146+
"Kuberoku needs a Kubernetes cluster to run your apps.\n"
147+
"New to Kubernetes? Follow the quickstart guide:\n"
148+
"\n"
149+
" https://github.com/amanjain/kuberoku#need-a-cluster-first\n"
150+
"\n"
151+
"Already have a cluster? Make sure kubectl can reach it:\n"
152+
"\n"
153+
" kubectl cluster-info"
154+
)
155+
156+
139157
class NoCurrentClusterError(KuberokuError):
140158
"""Raised when no current cluster is set and none can be inferred."""
141159

0 commit comments

Comments
 (0)