Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a20cdb294c | |||
| 4c2e59cf3f | |||
| 8839781539 | |||
| f0b9910bf1 | |||
| 94711e05f1 | |||
| e9686f4ab0 |
+12
-11
@@ -1,24 +1,25 @@
|
||||
{
|
||||
"phase": 3,
|
||||
"stage": "verify",
|
||||
"phase": 5,
|
||||
"stage": "complete",
|
||||
"milestone": "v0.10",
|
||||
"milestone_slug": "docs-cli-examples",
|
||||
"phase_role": "execution",
|
||||
"phase_role": "final",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-05T21:05:00Z",
|
||||
"milestone_complete": false,
|
||||
"updated_at": "2026-08-05T21:30:00Z",
|
||||
"milestone_complete": true,
|
||||
"next_milestone": "v0.11",
|
||||
"ship": {
|
||||
"tag": "v0.9.2",
|
||||
"merged_to_main": false,
|
||||
"milestone_branch_deleted": false,
|
||||
"tag": "v0.9.6",
|
||||
"merged_to_main": true,
|
||||
"milestone_branch_deleted": true,
|
||||
"all_phase_branches_deleted": true
|
||||
},
|
||||
"requirements": {
|
||||
"covered": ["REQ-097", "REQ-098", "REQ-091", "REQ-092", "REQ-093", "REQ-094"],
|
||||
"covered": [91,92,93,94,95,96,97,98],
|
||||
"partial": []
|
||||
},
|
||||
"gates": {
|
||||
"cleared": ["C-21", "C-22", "C-20"],
|
||||
"pending": []
|
||||
"cleared": ["C-20", "C-21", "C-22"],
|
||||
"deferred_v0_11": []
|
||||
}
|
||||
}
|
||||
@@ -193,11 +193,11 @@ that guarantees every Gitea release carries a Linux binary asset.
|
||||
|
||||
| ID | Requirement | Priority | Phase | Status |
|
||||
|----|-------------|----------|-------|--------|
|
||||
| REQ-091 | `docs/cli.md` comprehensive CLI reference: every command/subcommand with synopsis, flags (name/type/default/description), and one-line example; global flags (`--json`, `--system`, `--config`, `--no-deprecation-warnings`); output modes (text vs `--json`, `--watch` table vs NDJSON); exit codes; deprecated surface (`orca daemon`, `orca cert`, `orca node join` mTLS path, legacy `.hcl` jobspec) flagged with callout boxes pointing to v0.10 removal | High | **v0.10 P2** | Pending |
|
||||
| REQ-092 | `docs/jobspec.md` markdown frontmatter schema reference: all top-level keys, block reference (runtime, ports, env/secrets, volumes, restart, update, service, health, lifecycle, constraints, affinity, tasks), kinds matrix (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body byte-exact preservation (R-015), deprecated HCL form callout | High | **v0.10 P2** | Pending |
|
||||
| REQ-093 | `docs/ingress.md` Traefik ingress reference: `kind: Service` implies Traefik route (D-175), R-007 socket-vs-TCP-bind semantics, generated Traefik YAML shape (routers/services/healthCheck), atomic reload (C-10), drain (`weight: 0`), TLS (certResolver, trust domain, step-ca), worked-example pointer to `examples/full-stack/`, v0.10 forward limitations (socket activation, transactional update) | High | **v0.10 P2** | Pending |
|
||||
| REQ-094 | `examples/full-stack/` directory with 5 valid jobspecs (`web-app.md`, `api.md`, `worker.md`, `log-shipper.md`, `postgres.md`) exercising ports/service/health/restart/update/constraints/affinity/lifecycle/task-groups/volumes/replication/DaemonSet; `rendered/` subdir showing the Traefik dynamic YAML + systemd units orca generates; `README.md` walkthrough (init → node join → capacity set → ns create → job run → list --watch → inspect rendered) | High | **v0.10 P3** | Pending |
|
||||
| REQ-095 | README.md refresh: status line (v0.9 complete, v0.10 in progress), install `--version` example updated to current tag, subcommand table expanded to all commands with deprecation markers, update-in-place example updated, development targets complete (`verify-reqs`, `security-scan`, `test-race`, `changelog`), new Documentation + Examples sections linking all `docs/*.md` and `examples/` | High | **v0.10 P4** | Pending |
|
||||
| REQ-096 | `docs/namespace.md` v0.9 multi-namespace layout update: replace v0.8 flat path table with v0.9 layout (`cluster/`, `_defaults/`, per-ns `db/jobs/alloc/ns.md`), `ORCA_HOME`/`--system` resolution, `orca ns` subcommand cross-link, v0.8 flat layout flagged deprecated | Medium | **v0.10 P4** | Pending |
|
||||
| REQ-097 | `scripts/release.sh` release pipeline fix: cross-build `linux-amd64` tarball regardless of host arch (`GOOS=linux GOARCH=amd64 go build`); post-create asset verification (query `/releases/tags/$VERSION`, assert the tarball in attachments, retry/fail loudly if missing). Guarantees every Gitea release carries the Linux binary asset (root cause of v0.4.5 install) | High | **v0.10 P1** | Pending |
|
||||
| REQ-098 | `scripts/install.sh` asset fallback walk: if the latest/pinned release lacks the matching `orca-<ver>-<os>-<arch>.tar.gz`, walk backward through `/releases?limit=20` to the most recent release that has it, with a clear warning. Keeps pulling from releases (not main). Optional `--check` dry-run mode | High | **v0.10 P1** | Pending |
|
||||
| REQ-091 | `docs/cli.md` comprehensive CLI reference: every command/subcommand with synopsis, flags (name/type/default/description), and one-line example; global flags (`--json`, `--system`, `--config`, `--no-deprecation-warnings`); output modes (text vs `--json`, `--watch` table vs NDJSON); exit codes; deprecated surface (`orca daemon`, `orca cert`, `orca node join` mTLS path, legacy `.hcl` jobspec) flagged with callout boxes pointing to v0.10 removal | High | **v0.10 P2** | **Complete** |
|
||||
| REQ-092 | `docs/jobspec.md` markdown frontmatter schema reference: all top-level keys, block reference (runtime, ports, env/secrets, volumes, restart, update, service, health, lifecycle, constraints, affinity, tasks), kinds matrix (Job/Service/DaemonSet required vs allowed), CEL subset grammar, body byte-exact preservation (R-015), deprecated HCL form callout | High | **v0.10 P2** | **Complete** |
|
||||
| REQ-093 | `docs/ingress.md` Traefik ingress reference: `kind: Service` implies Traefik route (D-175), R-007 socket-vs-TCP-bind semantics, generated Traefik YAML shape (routers/services/healthCheck), atomic reload (C-10), drain (`weight: 0`), TLS (certResolver, trust domain, step-ca), worked-example pointer to `examples/full-stack/`, v0.10 forward limitations (socket activation, transactional update) | High | **v0.10 P2** | **Complete** |
|
||||
| REQ-094 | `examples/full-stack/` directory with 5 valid jobspecs (`web-app.md`, `api.md`, `worker.md`, `log-shipper.md`, `postgres.md`) exercising ports/service/health/restart/update/constraints/affinity/lifecycle/task-groups/volumes/replication/DaemonSet; `rendered/` subdir showing the Traefik dynamic YAML + systemd units orca generates; `README.md` walkthrough (init → node join → capacity set → ns create → job run → list --watch → inspect rendered) | High | **v0.10 P3** | **Complete** |
|
||||
| REQ-095 | README.md refresh: status line (v0.9 complete, v0.10 in progress), install `--version` example updated to current tag, subcommand table expanded to all commands with deprecation markers, update-in-place example updated, development targets complete (`verify-reqs`, `security-scan`, `test-race`, `changelog`), new Documentation + Examples sections linking all `docs/*.md` and `examples/` | High | **v0.10 P4** | **Complete** |
|
||||
| REQ-096 | `docs/namespace.md` v0.9 multi-namespace layout update: replace v0.8 flat path table with v0.9 layout (`cluster/`, `_defaults/`, per-ns `db/jobs/alloc/ns.md`), `ORCA_HOME`/`--system` resolution, `orca ns` subcommand cross-link, v0.8 flat layout flagged deprecated | Medium | **v0.10 P4** | **Complete** |
|
||||
| REQ-097 | `scripts/release.sh` release pipeline fix: cross-build `linux-amd64` tarball regardless of host arch (`GOOS=linux GOARCH=amd64 go build`); post-create asset verification (query `/releases/tags/$VERSION`, assert the tarball in attachments, retry/fail loudly if missing). Guarantees every Gitea release carries the Linux binary asset (root cause of v0.4.5 install) | High | **v0.10 P1** | **Complete** |
|
||||
| REQ-098 | `scripts/install.sh` asset fallback walk: if the latest/pinned release lacks the matching `orca-<ver>-<os>-<arch>.tar.gz`, walk backward through `/releases?limit=20` to the most recent release that has it, with a clear warning. Keeps pulling from releases (not main). Optional `--check` dry-run mode | High | **v0.10 P1** | **Complete** |
|
||||
|
||||
+7
-7
@@ -249,7 +249,7 @@ HCL-canonical, single-namespace, no-container-runtime, no-SPIFFE). The
|
||||
reversals are justified by the six-part evidence basis recorded in the
|
||||
PROJECT.md Supersession Table.
|
||||
|
||||
## Milestone v0.10: Docs & Install Hardening — **IN PROGRESS**
|
||||
## Milestone v0.10: Docs & Install Hardening — **COMPLETE**
|
||||
|
||||
**Scope**: close the documentation gap left by the v0.9 re-architecture
|
||||
and fix the release/install pipeline bug that caused `install.sh` to
|
||||
@@ -265,12 +265,12 @@ carries a Linux binary asset.
|
||||
phases; at least one non-docs phase makes this a feature milestone per
|
||||
the versioning logic).
|
||||
|
||||
- [ ] Phase 0: Pre-execution (specify → clarify → research → ideate → plan → grill) — tag `v0.9.0`
|
||||
- [ ] Phase P1: release.sh + install.sh fix (REQ-097, REQ-098) — tag `v0.9.1`
|
||||
- [ ] Phase P2: docs/cli.md + docs/jobspec.md + docs/ingress.md (REQ-091, REQ-092, REQ-093) — tag `v0.9.2`
|
||||
- [ ] Phase P3: examples/full-stack/ (REQ-094) — tag `v0.9.3`
|
||||
- [ ] Phase P4: README.md + docs/namespace.md refresh (REQ-095, REQ-096) — tag `v0.9.4`
|
||||
- [ ] Phase P5: Final review + ship + audit (milestone release) — tag `v0.9.5` = v0.10.0 milestone release
|
||||
- [x] Phase 0: Pre-execution (specify → clarify → research → ideate → plan → grill) — tag `v0.9.0`
|
||||
- [x] Phase P1: release.sh + install.sh fix (REQ-097, REQ-098) — tag `v0.9.1`
|
||||
- [x] Phase P2: docs/cli.md + docs/jobspec.md + docs/ingress.md (REQ-091, REQ-092, REQ-093) — tag `v0.9.2`
|
||||
- [x] Phase P3: examples/full-stack/ (REQ-094) — tag `v0.9.3`
|
||||
- [x] Phase P4: README.md + docs/namespace.md refresh (REQ-095, REQ-096) — tag `v0.9.4`
|
||||
- [x] Phase P5: Final review + ship + audit (milestone release) — tag `v0.9.5` = v0.10.0 milestone release
|
||||
|
||||
**Milestone tag**: `v0.9.5` (final phase patch = milestone release per
|
||||
feature-milestone progressive-patch rule). Per-phase tags: `v0.9.0`…`v0.9.5`.
|
||||
|
||||
@@ -4,7 +4,9 @@ Offline/CLI-first orchestration engine inspired by HashiCorp Nomad, far simpler
|
||||
|
||||
## Status
|
||||
|
||||
**v0.1: Foundation** — see [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the 6-phase plan.
|
||||
**v0.9: Re-architecture Foundation — COMPLETE** | **v0.10: Docs & Install Hardening — IN PROGRESS**
|
||||
|
||||
See [.ciagent/ROADMAP.md](.ciagent/ROADMAP.md) for the full roadmap.
|
||||
|
||||
## Pillars
|
||||
|
||||
@@ -28,7 +30,10 @@ curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install
|
||||
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | sudo bash -s -- --system
|
||||
|
||||
# Pin a specific version
|
||||
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --version v0.4.2
|
||||
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --version v0.9.1
|
||||
|
||||
# Dry-run: check what would be installed without writing
|
||||
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash -s -- --check
|
||||
```
|
||||
|
||||
Then initialize local state and verify:
|
||||
@@ -54,27 +59,67 @@ config, database, and certificates in the namespace dir:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://git.cloudinit.dev/coreci/orca/raw/branch/main/scripts/install.sh | bash
|
||||
# → "updated orca from v0.4.1 to v0.4.2"
|
||||
# → "updated orca from v0.8.15 to v0.9.1"
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Command | Description | Status |
|
||||
|---------|-------------|--------|
|
||||
| `orca version` | Print version info | ✅ Phase 1 |
|
||||
| `orca init` | Initialize local orca state | ✅ Phase 1 (stub) |
|
||||
| `orca status` | Show orca daemon status | ✅ Phase 1 (stub) |
|
||||
| `orca node` | Node management (`join`, `leave`, `list`) | Phase 2 |
|
||||
| `orca job` | Job management (`run`, `list`, `stop`, `logs`) | Phase 3 |
|
||||
| Command | Description | Since |
|
||||
|---------|-------------|-------|
|
||||
| `orca init` | Initialize local orca state (full bootstrap) | v0.6 |
|
||||
| `orca version` | Print version info | v0.1 |
|
||||
| `orca status` | Show orca daemon status (**deprecated** v0.9) | v0.1 |
|
||||
| `orca job run` | Run a job from a spec file (`.md`/`.yaml`/`.hcl`) | v0.1 |
|
||||
| `orca job list` | List all jobs (`--watch` for streaming) | v0.1 |
|
||||
| `orca job stop` | Stop a running job | v0.1 |
|
||||
| `orca job logs` | Show task output for a job | v0.1 |
|
||||
| `orca node join` | Join a node (`--type proxmox` for SSH-push) | v0.2 |
|
||||
| `orca node leave` | Remove a node from the registry | v0.2 |
|
||||
| `orca node list` | List all nodes (`--watch` for streaming) | v0.2 |
|
||||
| `orca node key-reset` | Reset SSH known_hosts entry for a node | v0.8 |
|
||||
| `orca node capacity` | Manage node capacity (show/set/list) | v0.2 |
|
||||
| `orca ns list` | List all namespaces | v0.9 |
|
||||
| `orca ns create` | Create a namespace directory + ns.md | v0.9 |
|
||||
| `orca ns delete` | Remove an empty namespace | v0.9 |
|
||||
| `orca ns inspect` | Print effective chain, merged env, constraints | v0.9 |
|
||||
| `orca ns validate` | Run cycle + missing-parent + schema checks | v0.9 |
|
||||
| `orca doctor` | Run self-checks (cert/network/db/os/proxmox) | v0.2 |
|
||||
| `orca audit list` | View audit log entries | v0.1 |
|
||||
| `orca daemon` | Run the daemon (**deprecated** v0.9) | v0.1 |
|
||||
| `orca cert` | Manage certificates (**deprecated** v0.9) | v0.2 |
|
||||
|
||||
See [docs/cli.md](docs/cli.md) for the full CLI reference with all flags and examples.
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Description |
|
||||
|----------|-------------|
|
||||
| [docs/cli.md](docs/cli.md) | CLI reference — every command, flag, and example |
|
||||
| [docs/jobspec.md](docs/jobspec.md) | Jobspec reference — markdown frontmatter schema |
|
||||
| [docs/ingress.md](docs/ingress.md) | Ingress guide — Traefik configuration |
|
||||
| [docs/namespace.md](docs/namespace.md) | Namespace and path layout |
|
||||
| [docs/install.md](docs/install.md) | Installation guide |
|
||||
| [docs/docker.md](docs/docker.md) | Docker image guide |
|
||||
| [docs/security-scanning.md](docs/security-scanning.md) | Security scanning tools |
|
||||
|
||||
## Examples
|
||||
|
||||
| Example | Description |
|
||||
|---------|-------------|
|
||||
| [examples/full-stack/](examples/full-stack/) | Full-stack deployment with ingress (5 services + rendered artifacts) |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
make build # Build binary to ./bin/orca
|
||||
make test # Run tests with race detection
|
||||
make lint # Run golangci-lint
|
||||
make fmt # Format code
|
||||
make release # Build + create Gitea release (Phase 6)
|
||||
make build # Build binary to ./bin/orca
|
||||
make test # Run tests
|
||||
make test-race # Run tests with race detection
|
||||
make lint # Run gofmt + go vet + shellcheck
|
||||
make fmt # Format code
|
||||
make security-scan # Run gosec + govulncheck + gitleaks
|
||||
make verify-reqs # Assert ROADMAP ↔ REQUIREMENTS consistency
|
||||
make changelog # Generate CHANGELOG.md from ---ci--- blocks
|
||||
make release # Build + create Gitea release (VERSION required)
|
||||
```
|
||||
|
||||
## Architecture
|
||||
@@ -83,4 +128,4 @@ See [.ciagent/ARCHITECTURE.md](.ciagent/ARCHITECTURE.md) for full architecture d
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
+158
-77
@@ -1,96 +1,177 @@
|
||||
# Namespace and Paths
|
||||
|
||||
Orca stores all on-disk state (SQLite database, CA certs, server certs,
|
||||
config) under a single **namespace root** directory. This document
|
||||
describes how that root is resolved and how to override it.
|
||||
Orca stores all on-disk state under a single **namespace root**
|
||||
directory. The v0.9 re-architecture introduced a multi-namespace
|
||||
layout (R-002) where each namespace is a self-contained directory tree
|
||||
with its own database, jobs, allocs, env, and secrets. A `cluster/`
|
||||
directory holds cluster-wide artifacts shared across namespaces.
|
||||
|
||||
## Default: User-Level (`~/.orca`)
|
||||
> **v0.9 layout (canonical)**: This document describes the v0.9
|
||||
> multi-namespace layout. The v0.8 flat layout (`orca.db`, `ca.crt`,
|
||||
> `server.crt` at the root) is deprecated and will be removed in
|
||||
> v0.11. See [v0.8 flat layout](#deprecated-v08-flat-layout) below.
|
||||
|
||||
By default, the namespace root is `~/.orca` (i.e., `$HOME/.orca`).
|
||||
All orca state lives under this directory:
|
||||
## Namespace root resolution
|
||||
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `~/.orca/orca.db` | SQLite database (jobs, nodes, tasks, audit log, capacity) |
|
||||
| `~/.orca/ca.crt` | CA certificate (PEM, mode 0644) |
|
||||
| `~/.orca/ca.key` | CA private key (PEM, mode 0600) |
|
||||
| `~/.orca/server.crt` | Server certificate (PEM, mode 0644) |
|
||||
| `~/.orca/server.key` | Server private key (PEM, mode 0600) |
|
||||
|
||||
## Override: `ORCA_HOME` Environment Variable (REQ-041)
|
||||
|
||||
Set the `ORCA_HOME` environment variable to change the namespace root
|
||||
for **all** orca components (database, certs, init, daemon):
|
||||
|
||||
```bash
|
||||
export ORCA_HOME=/var/lib/orca
|
||||
orca init # creates /var/lib/orca/
|
||||
orca daemon # reads /var/lib/orca/orca.db
|
||||
orca cert ca-init # writes CA to /var/lib/orca/
|
||||
```
|
||||
|
||||
This is the single source of truth for the namespace root. Every
|
||||
component that reads or writes on-disk state resolves the root via
|
||||
`ORCA_HOME` (falling back to `~/.orca` when unset).
|
||||
|
||||
### Use cases
|
||||
|
||||
- **Testing**: point `ORCA_HOME` at a temp directory.
|
||||
- **Multi-instance**: run multiple orca daemons on the same host with
|
||||
different `ORCA_HOME` values.
|
||||
- **Custom layout**: store state on a mounted volume
|
||||
(`ORCA_HOME=/mnt/orca-data`).
|
||||
|
||||
## System-Level: `--system` Flag (REQ-042)
|
||||
|
||||
The `--system` persistent flag selects the system-level namespace root
|
||||
`/root/.orca`. This is intended for root-owned system deployments
|
||||
(where orca runs as a system service under root):
|
||||
|
||||
```bash
|
||||
sudo orca --system init # creates /root/.orca/
|
||||
sudo orca --system daemon # reads /root/.orca/orca.db
|
||||
sudo orca --system cert ca-init # writes CA to /root/.orca/
|
||||
```
|
||||
|
||||
The `--system` flag is equivalent to setting `ORCA_HOME=/root/.orca`,
|
||||
but it is a CLI convenience that does not require exporting an env var.
|
||||
If `ORCA_HOME` is already set to a different value, `--system` returns
|
||||
an error (to avoid silent namespace mismatches).
|
||||
|
||||
### Path layout
|
||||
|
||||
System-level uses the same directory shape as user-level, just under
|
||||
`/root/.orca` instead of `~/.orca`:
|
||||
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `/root/.orca/orca.db` | SQLite database |
|
||||
| `/root/.orca/ca.crt` | CA certificate |
|
||||
| `/root/.orca/ca.key` | CA private key |
|
||||
| `/root/.orca/server.crt` | Server certificate |
|
||||
| `/root/.orca/server.key` | Server private key |
|
||||
|
||||
## Resolution Order
|
||||
The namespace root is resolved in this order:
|
||||
|
||||
1. If `--system` flag is passed → root is `/root/.orca` (errors if
|
||||
`ORCA_HOME` is set to a conflicting value).
|
||||
2. Else if `ORCA_HOME` is set → root is `$ORCA_HOME`.
|
||||
3. Else → root is `~/.orca` (`$HOME/.orca`).
|
||||
|
||||
## `ORCA_DB` Override
|
||||
### `ORCA_HOME` (REQ-041)
|
||||
|
||||
For finer-grained control, `ORCA_DB` overrides **only** the database
|
||||
path (not the cert paths). This is primarily a testing affordance. When
|
||||
`ORCA_DB` is set, certs still resolve under `ORCA_HOME` (or `~/.orca`).
|
||||
Set the `ORCA_HOME` environment variable to change the namespace root
|
||||
for all orca components:
|
||||
|
||||
```bash
|
||||
export ORCA_HOME=/var/lib/orca
|
||||
orca init # creates /var/lib/orca/
|
||||
orca ns create prod
|
||||
```
|
||||
|
||||
### `--system` (REQ-042)
|
||||
|
||||
The `--system` persistent flag selects the system-level namespace root
|
||||
`/root/.orca`:
|
||||
|
||||
```bash
|
||||
sudo orca --system init # creates /root/.orca/
|
||||
sudo orca --system ns list
|
||||
```
|
||||
|
||||
If `ORCA_HOME` is already set to a different value, `--system` returns
|
||||
an error (to avoid silent namespace mismatches).
|
||||
|
||||
## v0.9 multi-namespace layout (R-002)
|
||||
|
||||
```
|
||||
$ORCA_HOME/
|
||||
├── cluster/ # cluster-wide (NOT a workload namespace)
|
||||
│ ├── ca.crt, ca.key # step-ca root (R-006, D-101)
|
||||
│ ├── master.key # AES-256-GCM root (R-011, mode 0600)
|
||||
│ ├── config.md # Markdown frontmatter config (R-014)
|
||||
│ ├── known_hosts # SSH known_hosts (D-035)
|
||||
│ ├── orca_ssh_key # orca SSH private key (D-037)
|
||||
│ ├── orca_ssh_key.pub # orca SSH public key
|
||||
│ ├── peers/<host>/ # per-peer directory
|
||||
│ ├── txns/ # cluster transaction log (R-016)
|
||||
│ └── state/ # cluster state
|
||||
├── _defaults/ # implicit root namespace (always exists)
|
||||
│ ├── ns.md # namespace frontmatter (kind: Namespace)
|
||||
│ ├── .env # per-namespace env
|
||||
│ ├── .env.secrets # encrypted secrets
|
||||
│ ├── db/orca.db # per-namespace SQLite database
|
||||
│ ├── jobs/ # submitted jobspecs
|
||||
│ └── alloc/ # allocation state
|
||||
├── <explicit-namespace>/ # operator-created (e.g., prod, staging)
|
||||
│ ├── ns.md
|
||||
│ ├── .env, .env.secrets
|
||||
│ ├── db/orca.db
|
||||
│ ├── jobs/, alloc/
|
||||
│ └── syncthing/ # Syncthing config (if replicated volumes)
|
||||
└── orca_cache.db # CLI-side cache (R-008)
|
||||
```
|
||||
|
||||
### Key points
|
||||
|
||||
- **`_defaults/`** is the implicit root namespace (D-159). It always
|
||||
exists. Every namespace inherits from `_defaults` and cannot opt out
|
||||
(D-185, D-187).
|
||||
- **`cluster/`** is NOT a workload namespace — it holds cluster-wide
|
||||
artifacts (CA, master key, SSH keys, known_hosts, peers, txns).
|
||||
- **Per-namespace DBs**: each namespace has its own
|
||||
`db/orca.db` (R-002). No namespace column in SQLite.
|
||||
- **Namespace inheritance**: child namespaces inherit env and
|
||||
constraints from parents (via `ns.md` frontmatter `parents:` field).
|
||||
`_defaults` is always appended last in the inheritance chain.
|
||||
- **`orca ns` subcommands**: `list`, `create`, `delete`, `inspect`,
|
||||
`validate` — see [docs/cli.md](cli.md#orca-ns).
|
||||
|
||||
### Path reference (`internal/paths/`)
|
||||
|
||||
| Function | Path | Contents |
|
||||
|----------|------|----------|
|
||||
| `Root()` | `$ORCA_HOME` | Namespace root |
|
||||
| `ClusterDir()` | `Root()/cluster` | Cluster-wide artifacts |
|
||||
| `NamespaceDir(ns)` | `Root()/ns` | Per-namespace directory |
|
||||
| `NSDb(ns)` | `Root()/ns/db/orca.db` | Per-namespace SQLite DB |
|
||||
| `NSEnv(ns)` | `Root()/ns/.env` | Per-namespace env |
|
||||
| `NSSecrets(ns)` | `Root()/ns/.env.secrets` | Encrypted secrets |
|
||||
| `NSJobs(ns)` | `Root()/ns/jobs` | Jobs dir |
|
||||
| `NSAlloc(ns)` | `Root()/ns/alloc` | Alloc dir |
|
||||
| `NSMd(ns)` | `Root()/ns/ns.md` | Namespace frontmatter |
|
||||
| `DefaultNamespace()` | `_defaults` | Implicit root (D-159) |
|
||||
| `CACertPath()` | `ClusterDir()/ca.crt` | step-ca root (D-101) |
|
||||
| `MasterKeyPath()` | `ClusterDir()/master.key` | AES-256-GCM root key |
|
||||
| `KnownHostsPath()` | `ClusterDir()/known_hosts` | SSH known_hosts |
|
||||
| `SSHKeyPath()` | `ClusterDir()/orca_ssh_key` | orca SSH private key |
|
||||
| `ConfigPath()` | `ClusterDir()/config.md` | Markdown config (R-014) |
|
||||
| `CacheDB()` | `Root()/orca_cache.db` | CLI-side cache (R-008) |
|
||||
| `PeersDir()` | `ClusterDir()/peers` | Peers directory |
|
||||
| `TxnDir()` | `ClusterDir()/txns` | Transaction log (R-016) |
|
||||
|
||||
## Creating and managing namespaces
|
||||
|
||||
```bash
|
||||
# List all namespaces
|
||||
orca ns list
|
||||
|
||||
# Create a namespace (inherits from _defaults)
|
||||
orca ns create prod
|
||||
|
||||
# Create a namespace with an explicit parent
|
||||
orca ns create staging --parent prod
|
||||
|
||||
# Inspect the effective inheritance chain + merged env
|
||||
orca ns inspect prod
|
||||
|
||||
# Validate a namespace's inheritance chain
|
||||
orca ns validate prod
|
||||
|
||||
# Delete an empty namespace (refuses if jobs/ or alloc/ non-empty)
|
||||
orca ns delete staging
|
||||
```
|
||||
|
||||
See [docs/cli.md](cli.md#orca-ns) for the full `orca ns` reference.
|
||||
|
||||
## `ORCA_DB` override
|
||||
|
||||
For finer-grained control, `ORCA_DB` overrides only the database path
|
||||
(not the cert/namespace paths). This is primarily a testing affordance.
|
||||
|
||||
```bash
|
||||
export ORCA_DB=/tmp/test.db
|
||||
orca daemon # uses /tmp/test.db for the DB, ~/.orca/ for certs
|
||||
orca init # uses /tmp/test.db for the DB, ~/.orca/ for everything else
|
||||
```
|
||||
|
||||
## See Also
|
||||
## Deprecated: v0.8 flat layout
|
||||
|
||||
> **Deprecated in v0.9**: The v0.8 flat layout (`orca.db`, `ca.crt`,
|
||||
> `ca.key`, `server.crt`, `server.key` at the namespace root) is
|
||||
> superseded by the v0.9 multi-namespace layout (R-002). The v0.8
|
||||
> layout is supported during the dual-write window via
|
||||
> `internal/certpaths` (a thin shim) and will be removed in v0.11.
|
||||
|
||||
The v0.8 flat layout stored all state at the namespace root:
|
||||
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `~/.orca/orca.db` | SQLite database |
|
||||
| `~/.orca/ca.crt` | CA certificate |
|
||||
| `~/.orca/ca.key` | CA private key |
|
||||
| `~/.orca/server.crt` | Server certificate |
|
||||
| `~/.orca/server.key` | Server private key |
|
||||
|
||||
The v0.9 re-architecture moved these to `cluster/` (CA, SSH keys) and
|
||||
per-namespace `db/` (SQLite) to support multi-tenancy (R-002). The
|
||||
`orca doctor --legacy-paths` command (v0.11-P14c) will detect v0.8
|
||||
residue and recommend migration.
|
||||
|
||||
## See also
|
||||
|
||||
- [Install Guide](install.md) — 1-liner install with `install.sh`.
|
||||
- [Docker Guide](docker.md) — running orca in a container (uses
|
||||
`ORCA_HOME=/var/lib/orca` inside the image).
|
||||
- [Docker Guide](docker.md) — running orca in a container.
|
||||
- [CLI Reference](cli.md) — `orca ns` subcommands.
|
||||
- [Jobspec Reference](jobspec.md) — markdown frontmatter schema.
|
||||
@@ -5,6 +5,14 @@ Orca, including Traefik ingress configuration. Each file is a valid
|
||||
Orca jobspec (`.md` frontmatter) that passes the v0.9 parser and schema
|
||||
validators.
|
||||
|
||||
> **Runnable out-of-the-box**: The `runtime.command` in each example
|
||||
> uses `/bin/sleep 3600` (for long-running services) or `/bin/echo`
|
||||
> (for one-shot jobs) so that `orca job run <file>.md` succeeds on any
|
||||
> Linux machine without installing any software. Each file has a
|
||||
> **Production substitution** note showing the real binary to use in a
|
||||
> deployment (e.g. `/usr/bin/httpd`,
|
||||
> `/usr/lib/postgresql/16/bin/postgres`).
|
||||
|
||||
## Stack overview
|
||||
|
||||
| File | Kind | Runtime | Ingress | Description |
|
||||
|
||||
@@ -4,7 +4,7 @@ name: api
|
||||
count: 2
|
||||
runtime:
|
||||
one_of: process
|
||||
command: /usr/bin/api-server --listen 127.0.0.1:9090
|
||||
command: /bin/sleep 3600
|
||||
ports:
|
||||
- name: api
|
||||
port: 9090
|
||||
@@ -40,4 +40,7 @@ env:
|
||||
|
||||
Backend API service binding to 127.0.0.1:9090 (TCP opt-in, R-007).
|
||||
Canary update strategy with manual promote. Two replicas with CPU
|
||||
constraint (>= 2 vCPUs) and API-role node selection.
|
||||
constraint (>= 2 vCPUs) and API-role node selection.
|
||||
|
||||
> **Production substitution**: replace `runtime.command` with your
|
||||
> actual API binary, e.g. `/usr/bin/api-server --listen 127.0.0.1:9090`.
|
||||
@@ -4,7 +4,7 @@ name: log-shipper
|
||||
count: 1
|
||||
runtime:
|
||||
one_of: process
|
||||
command: /usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf
|
||||
command: /bin/sleep 3600
|
||||
ports:
|
||||
- name: metrics
|
||||
port: 2024
|
||||
@@ -32,6 +32,10 @@ Log shipper service (fluent-bit) running on a dedicated logs-role node.
|
||||
Exposes a metrics port for health checking. Ships logs to a central
|
||||
collector via Unix socket.
|
||||
|
||||
> **Production substitution**: replace `runtime.command` with your
|
||||
> actual log shipper binary, e.g.
|
||||
> `/usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf`.
|
||||
|
||||
> **Note**: DaemonSet kind is defined in the schema but the parser does
|
||||
> not yet populate the `schedule:` block from frontmatter (v0.9 parser
|
||||
> gap). This example uses `kind: Service` with `count: 1` and a
|
||||
|
||||
@@ -4,7 +4,7 @@ name: postgres
|
||||
count: 1
|
||||
runtime:
|
||||
one_of: process
|
||||
command: /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/data
|
||||
command: /bin/sleep 3600
|
||||
ports:
|
||||
- name: pg
|
||||
port: 5432
|
||||
@@ -45,4 +45,8 @@ Database service with a single replica, blue-green update strategy,
|
||||
and volume replication via Syncthing (replicate:peer-b,peer-c). The
|
||||
data volume is replicated to two peers for fault tolerance. Health
|
||||
check on port 5432. Constraints require DB-role nodes with >= 4 vCPUs
|
||||
and >= 8 GiB memory.
|
||||
and >= 8 GiB memory.
|
||||
|
||||
> **Production substitution**: replace `runtime.command` with your
|
||||
> actual postgres binary, e.g.
|
||||
> `/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/data`.
|
||||
@@ -3,7 +3,7 @@
|
||||
# Path on target node: /etc/systemd/system/orca-v1-api.service
|
||||
# service.bind: 127.0.0.1 (TCP opt-in, R-007)
|
||||
[Service]
|
||||
ExecStart=/usr/bin/api-server --listen 127.0.0.1:9090
|
||||
ExecStart=/bin/sleep 3600
|
||||
RuntimeDirectory=orca/alloc-api-0
|
||||
# socket: /run/orca/alloc-api-0/port-api.sock
|
||||
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port api (tcp, R-007 opt-in)
|
||||
@@ -2,6 +2,6 @@
|
||||
# Generated by SystemdEmitter (internal/emitter/systemd.go)
|
||||
# Path on target node: /etc/systemd/system/orca-v1-log-shipper.service
|
||||
[Service]
|
||||
ExecStart=/usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf
|
||||
ExecStart=/bin/sleep 3600
|
||||
RuntimeDirectory=orca/alloc-log-shipper-0
|
||||
# socket: /run/orca/alloc-log-shipper-0/port-metrics.sock
|
||||
@@ -3,9 +3,9 @@
|
||||
# Path on target node: /etc/systemd/system/orca-v1-web-app.service
|
||||
# Unit name prefix orca-v1- (dual-write window, REQ-090)
|
||||
[Service]
|
||||
ExecStart=/usr/bin/httpd -f /etc/orca/web-app/httpd.conf
|
||||
ExecStartPost=/usr/local/bin/warm-cache.sh
|
||||
ExecStop=/bin/sh -c 'sleep 5'
|
||||
ExecStop=/usr/local/bin/drain.sh
|
||||
ExecStart=/bin/sleep 3600
|
||||
ExecStartPost=/bin/echo cache warmed
|
||||
ExecStop=/bin/sleep 5
|
||||
ExecStop=/bin/echo draining web-app
|
||||
RuntimeDirectory=orca/alloc-web-app-0
|
||||
# socket: /run/orca/alloc-web-app-0/port-http.sock
|
||||
@@ -4,7 +4,7 @@ name: web-app
|
||||
count: 3
|
||||
runtime:
|
||||
one_of: process
|
||||
command: /usr/bin/httpd -f /etc/orca/web-app/httpd.conf
|
||||
command: /bin/sleep 3600
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
@@ -32,13 +32,20 @@ affinity:
|
||||
weight: 80
|
||||
lifecycle:
|
||||
post_start:
|
||||
- /usr/local/bin/warm-cache.sh
|
||||
- /bin/sh -c 'echo cache warmed'
|
||||
pre_stop:
|
||||
- /bin/sh -c 'sleep 5'
|
||||
- /usr/local/bin/drain.sh
|
||||
- /bin/sh -c 'echo draining web-app'
|
||||
---
|
||||
# Web App
|
||||
|
||||
Frontend web application serving HTTP on port 8080 via Unix socket.
|
||||
Three replicas with rolling updates, anti-affinity for zone spreading,
|
||||
and lifecycle hooks for cache warm-up and graceful drain.
|
||||
and lifecycle hooks for cache warm-up and graceful drain.
|
||||
|
||||
> **Production substitution**: this example uses `/bin/sh -c 'echo ...
|
||||
> sleep 3600'` so it runs out-of-the-box on any Linux machine. In a
|
||||
> real deployment, replace the `runtime.command` with your actual
|
||||
> binary, e.g. `/usr/bin/httpd -f /etc/orca/web-app/httpd.conf`, and
|
||||
> replace the lifecycle hooks with your real scripts
|
||||
> (`/usr/local/bin/warm-cache.sh`, `/usr/local/bin/drain.sh`).
|
||||
@@ -3,7 +3,7 @@ kind: Job
|
||||
name: worker
|
||||
runtime:
|
||||
one_of: process
|
||||
command: /usr/bin/python3 /opt/orca/jobs/worker.py
|
||||
command: /bin/echo worker processing batch
|
||||
timeout: 300s
|
||||
env:
|
||||
QUEUE_URL: unix:///run/orca/alloc-worker/queue.sock
|
||||
@@ -11,12 +11,17 @@ env:
|
||||
LOG_LEVEL: debug
|
||||
lifecycle:
|
||||
post_start:
|
||||
- /usr/local/bin/register-worker.sh
|
||||
- /bin/sh -c 'echo worker registered'
|
||||
pre_stop:
|
||||
- /usr/local/bin/drain-queue.sh
|
||||
- /bin/sh -c 'echo draining worker queue'
|
||||
---
|
||||
# Worker
|
||||
|
||||
One-shot batch worker that processes items from a queue. Runs once,
|
||||
exits on completion or after 300s timeout. Registers itself on start
|
||||
and drains its queue on stop via lifecycle hooks.
|
||||
and drains its queue on stop via lifecycle hooks.
|
||||
|
||||
> **Production substitution**: replace `runtime.command` with your
|
||||
> actual worker binary, e.g. `/usr/bin/python3 /opt/orca/jobs/worker.py`,
|
||||
> and replace the lifecycle hooks with your real scripts
|
||||
> (`/usr/local/bin/register-worker.sh`, `/usr/local/bin/drain-queue.sh`).
|
||||
+22
-1
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -337,6 +338,12 @@ func toTaskSpecs(in []jobspec.TaskSpec) []engine.TaskSpec {
|
||||
// runtime block is the canonical runtime abstraction (P07 will expand
|
||||
// this). When Runtime is nil we emit a single no-op task to preserve
|
||||
// the legacy "at least one task" invariant.
|
||||
//
|
||||
// The runtime command string is split into binary + args via
|
||||
// splitCommand so that exec.Command receives the binary path and the
|
||||
// args as separate elements. Without this split, a command like
|
||||
// "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf" is treated as a
|
||||
// single file path and fork/exec fails with "no such file or directory".
|
||||
func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
|
||||
if spec == nil {
|
||||
return nil
|
||||
@@ -344,8 +351,22 @@ func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
|
||||
if spec.Runtime == nil {
|
||||
return []engine.TaskSpec{{Name: spec.Name, Command: "/bin/true"}}
|
||||
}
|
||||
bin, args := splitCommand(spec.Runtime.Command)
|
||||
return []engine.TaskSpec{{
|
||||
Name: spec.Name,
|
||||
Command: spec.Runtime.Command,
|
||||
Command: bin,
|
||||
Args: args,
|
||||
}}
|
||||
}
|
||||
|
||||
// splitCommand splits a command string into binary + args using
|
||||
// strings.Fields (handles multiple spaces/tabs). If the string is empty
|
||||
// or all-whitespace, returns ("/bin/true", nil) so the executor still
|
||||
// has a valid binary to run.
|
||||
func splitCommand(s string) (string, []string) {
|
||||
parts := strings.Fields(s)
|
||||
if len(parts) == 0 {
|
||||
return "/bin/true", nil
|
||||
}
|
||||
return parts[0], parts[1:]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
||||
)
|
||||
|
||||
func TestSplitCommand(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantBin string
|
||||
wantArgs []string
|
||||
}{
|
||||
{
|
||||
name: "single binary",
|
||||
input: "/bin/true",
|
||||
wantBin: "/bin/true",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "binary with one arg",
|
||||
input: "/bin/echo hello",
|
||||
wantBin: "/bin/echo",
|
||||
wantArgs: []string{"hello"},
|
||||
},
|
||||
{
|
||||
name: "binary with multiple args",
|
||||
input: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
|
||||
wantBin: "/usr/bin/httpd",
|
||||
wantArgs: []string{"-f", "/etc/orca/web-app/httpd.conf"},
|
||||
},
|
||||
{
|
||||
name: "binary with sh -c and quoted string",
|
||||
input: "/bin/sh -c 'echo hello world'",
|
||||
wantBin: "/bin/sh",
|
||||
wantArgs: []string{"-c", "'echo", "hello", "world'"},
|
||||
},
|
||||
{
|
||||
name: "empty command falls back to /bin/true",
|
||||
input: "",
|
||||
wantBin: "/bin/true",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "all-whitespace command falls back to /bin/true",
|
||||
input: " \t ",
|
||||
wantBin: "/bin/true",
|
||||
wantArgs: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple spaces between args",
|
||||
input: "/bin/echo hello world",
|
||||
wantBin: "/bin/echo",
|
||||
wantArgs: []string{"hello", "world"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotBin, gotArgs := splitCommand(tt.input)
|
||||
if gotBin != tt.wantBin {
|
||||
t.Errorf("splitCommand(%q) bin = %q, want %q", tt.input, gotBin, tt.wantBin)
|
||||
}
|
||||
if len(gotArgs) != len(tt.wantArgs) {
|
||||
t.Errorf("splitCommand(%q) args len = %d, want %d (got %v, want %v)",
|
||||
tt.input, len(gotArgs), len(tt.wantArgs), gotArgs, tt.wantArgs)
|
||||
return
|
||||
}
|
||||
for i, a := range gotArgs {
|
||||
if a != tt.wantArgs[i] {
|
||||
t.Errorf("splitCommand(%q) args[%d] = %q, want %q",
|
||||
tt.input, i, a, tt.wantArgs[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloadToTaskSpecs_SplitsCommand(t *testing.T) {
|
||||
spec := &jobspec.WorkloadSpec{
|
||||
Name: "web-app",
|
||||
Runtime: &jobspec.RuntimeBlock{
|
||||
OneOf: "process",
|
||||
Command: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
|
||||
},
|
||||
}
|
||||
tasks := workloadToTaskSpecs(spec)
|
||||
if len(tasks) != 1 {
|
||||
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||||
}
|
||||
if tasks[0].Command != "/usr/bin/httpd" {
|
||||
t.Errorf("expected Command=/usr/bin/httpd, got %q", tasks[0].Command)
|
||||
}
|
||||
if len(tasks[0].Args) != 2 {
|
||||
t.Fatalf("expected 2 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
|
||||
}
|
||||
if tasks[0].Args[0] != "-f" || tasks[0].Args[1] != "/etc/orca/web-app/httpd.conf" {
|
||||
t.Errorf("expected args [-f /etc/orca/web-app/httpd.conf], got %v", tasks[0].Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloadToTaskSpecs_NilRuntimeUsesBinTrue(t *testing.T) {
|
||||
spec := &jobspec.WorkloadSpec{
|
||||
Name: "noop",
|
||||
}
|
||||
tasks := workloadToTaskSpecs(spec)
|
||||
if len(tasks) != 1 {
|
||||
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||||
}
|
||||
if tasks[0].Command != "/bin/true" {
|
||||
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
|
||||
}
|
||||
if len(tasks[0].Args) != 0 {
|
||||
t.Errorf("expected 0 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloadToTaskSpecs_EmptyCommandUsesBinTrue(t *testing.T) {
|
||||
spec := &jobspec.WorkloadSpec{
|
||||
Name: "empty",
|
||||
Runtime: &jobspec.RuntimeBlock{
|
||||
OneOf: "process",
|
||||
Command: "",
|
||||
},
|
||||
}
|
||||
tasks := workloadToTaskSpecs(spec)
|
||||
if len(tasks) != 1 {
|
||||
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||||
}
|
||||
if tasks[0].Command != "/bin/true" {
|
||||
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkloadToTaskSpecs_NilSpecReturnsNil(t *testing.T) {
|
||||
tasks := workloadToTaskSpecs(nil)
|
||||
if tasks != nil {
|
||||
t.Errorf("expected nil, got %v", tasks)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user