feat(P01): unified namespace root via ORCA_HOME + --system flag
REQ-041: ORCA_HOME is now the single namespace root for all components
(db, certs, init, daemon). store.Open("") and init command both
route through certpaths.Dir()/DBPath() instead of hardcoding ~/.orca.
Backward compatible: empty ORCA_HOME -> ~/.orca.
REQ-042: --system persistent flag on rootCmd sets ORCA_HOME=/root/.orca
via PersistentPreRunE. Errors on conflict with pre-set ORCA_HOME.
Tests: 7 new tests in namespace_test.go (default, ORCA_HOME override,
--system sets root, conflict detection, init --json, flag registered).
Full suite passes (no regressions).
Docs: docs/namespace.md covers default, ORCA_HOME, --system, ORCA_DB,
resolution order, and path layout tables.
---ci---
project: orca
phase: 1
milestone: v0.5
status: verify
---/ci---
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "complete",
|
||||
"phase": 1,
|
||||
"stage": "verify",
|
||||
"milestone": "v0.5",
|
||||
"milestone_slug": "distribution",
|
||||
"phase_role": "pre_execution",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-03T18:30:00Z",
|
||||
"updated_at": "2026-08-03T18:40:00Z",
|
||||
"milestone_complete": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
# Phase 1 Verification: Namespace Unification (v0.5 P1)
|
||||
|
||||
**Phase**: 1 (namespace unification)
|
||||
**Milestone**: v0.5 Distribution
|
||||
**Requirements covered**: REQ-041, REQ-042
|
||||
**Date**: 2026-08-03
|
||||
|
||||
## Structural Layer
|
||||
|
||||
- `gofmt -l .` → clean (no files need formatting).
|
||||
- `go vet ./...` → clean (no warnings).
|
||||
- `go build ./...` → succeeds.
|
||||
- New files: `internal/cli/namespace_test.go`, `docs/namespace.md`.
|
||||
- Modified files: `internal/cli/root.go`, `internal/cli/init.go`, `internal/store/store.go`.
|
||||
|
||||
## Behavioral Layer
|
||||
|
||||
### Unit tests (new)
|
||||
- `TestNamespaceDefaultsToUserHome` ✓ — empty `ORCA_HOME` → `~/.orca`.
|
||||
- `TestNamespaceHonorsORCAHOME` ✓ — `ORCA_HOME=/tmp/x` → `Dir()=/tmp/x`, `DBPath()=/tmp/x/orca.db`.
|
||||
- `TestInitHonorsORCAHOME` ✓ — `init` creates `$ORCA_HOME` dir.
|
||||
- `TestSystemFlagSetsORCAHOME` ✓ — `--system` sets `ORCA_HOME=/root/.orca`.
|
||||
- `TestSystemFlagConflictsWithORCAHOME` ✓ — `--system` + `ORCA_HOME=/custom` → error.
|
||||
- `TestInitJSONOutput` ✓ — `init --json` returns `{"path":"...","status":"initialized"}`.
|
||||
- `TestSystemFlagIsPersistent` ✓ — `--system` registered as persistent flag on `rootCmd`.
|
||||
|
||||
### Unit tests (regression — all pass)
|
||||
- `internal/cli/` (9.8s) ✓
|
||||
- `internal/store/` ✓
|
||||
- `internal/doctor/` ✓
|
||||
- `internal/daemon/` ✓
|
||||
- `internal/security/` ✓
|
||||
- `internal/engine/` ✓
|
||||
- `internal/jobspec/` ✓
|
||||
- `internal/transport/` ✓
|
||||
|
||||
### Manual e2e
|
||||
- `ORCA_HOME=/tmp/orca-test-user ./bin/orca init` → creates `/tmp/orca-test-user` ✓
|
||||
- `./bin/orca --system init` → creates `/root/.orca` ✓
|
||||
- `ORCA_HOME=/custom ./bin/orca --system init` → error "conflicts with ORCA_HOME" ✓
|
||||
- `./bin/orca version --json` → `{"version":"v0.4.1",...}` ✓
|
||||
|
||||
## Security Layer
|
||||
|
||||
- No new secret handling. The namespace unification moves path resolution
|
||||
but does not change cert/key file modes (0600/0644 per REQ-033 unchanged).
|
||||
- `--system` flag does not escalate privileges — it only changes the
|
||||
namespace root path. Running as non-root with `--system` will fail at
|
||||
`os.MkdirAll("/root/.orca")` with a permission error (expected).
|
||||
- No new network surface.
|
||||
|
||||
## Quality Layer
|
||||
|
||||
- **Backward compatibility**: empty `ORCA_HOME` + no `--system` → `~/.orca`
|
||||
(identical to pre-v0.5 behavior). All existing tests pass unmodified.
|
||||
- **Single source of truth**: `certpaths.Dir()` is the only namespace root
|
||||
resolver. `store.Open("")` and `init` both route through it.
|
||||
- **No redundant implementations**: the `--system` flag maps to `ORCA_HOME`
|
||||
rather than introducing a parallel path mechanism.
|
||||
- **Documentation**: `docs/namespace.md` covers default, `ORCA_HOME`, and
|
||||
`--system` with examples and resolution order.
|
||||
|
||||
## Must-Haves Checklist
|
||||
|
||||
- [x] `go test ./...` passes (including new namespace_test.go).
|
||||
- [x] `ORCA_HOME=/tmp/x orca init` creates `/tmp/x` (not `~/.orca`).
|
||||
- [x] `orca --system init` creates `/root/.orca` (when run as root).
|
||||
- [x] Empty `ORCA_HOME` + no `--system` → `~/.orca` (backward compat).
|
||||
- [x] `orca version --json` works (needed by install.sh in P2).
|
||||
|
||||
## Verdict
|
||||
|
||||
**PASS** — all 4 verification layers pass. REQ-041 and REQ-042 are
|
||||
satisfied. Ready to ship as `v0.4.2`.
|
||||
@@ -0,0 +1,96 @@
|
||||
# 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.
|
||||
|
||||
## Default: User-Level (`~/.orca`)
|
||||
|
||||
By default, the namespace root is `~/.orca` (i.e., `$HOME/.orca`).
|
||||
All orca state lives under this directory:
|
||||
|
||||
| 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
|
||||
|
||||
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
|
||||
|
||||
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`).
|
||||
|
||||
```bash
|
||||
export ORCA_DB=/tmp/test.db
|
||||
orca daemon # uses /tmp/test.db for the DB, ~/.orca/ for certs
|
||||
```
|
||||
|
||||
## 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).
|
||||
@@ -3,21 +3,18 @@ package cli
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
)
|
||||
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize local orca state directory",
|
||||
Long: "Create the local orca state directory at ~/.orca/ and write a default config file.",
|
||||
Long: "Create the local orca state directory (honors $ORCA_HOME; defaults to ~/.orca) and write a default config file.",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get home dir: %w", err)
|
||||
}
|
||||
orcaDir := filepath.Join(home, ".orca")
|
||||
orcaDir := certpaths.Dir()
|
||||
if err := os.MkdirAll(orcaDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create orca dir: %w", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
)
|
||||
|
||||
func resetRootFlags(t *testing.T) {
|
||||
t.Helper()
|
||||
rootCmd.SetArgs(nil)
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetErr(&buf)
|
||||
_ = rootCmd.PersistentFlags().Set("system", "false")
|
||||
_ = rootCmd.PersistentFlags().Set("json", "false")
|
||||
}
|
||||
|
||||
func TestNamespaceDefaultsToUserHome(t *testing.T) {
|
||||
t.Setenv("ORCA_HOME", "")
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Fatalf("UserHomeDir: %v", err)
|
||||
}
|
||||
want := filepath.Join(home, ".orca")
|
||||
if got := certpaths.Dir(); got != want {
|
||||
t.Errorf("certpaths.Dir() = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNamespaceHonorsORCAHOME(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", tmp)
|
||||
if got := certpaths.Dir(); got != tmp {
|
||||
t.Errorf("certpaths.Dir() = %q, want %q", got, tmp)
|
||||
}
|
||||
if got := certpaths.DBPath(); got != filepath.Join(tmp, "orca.db") {
|
||||
t.Errorf("certpaths.DBPath() = %q, want %q", got, filepath.Join(tmp, "orca.db"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitHonorsORCAHOME(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", tmp)
|
||||
resetRootFlags(t)
|
||||
|
||||
rootCmd.SetArgs([]string{"init"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("init: %v", err)
|
||||
}
|
||||
|
||||
info, err := os.Stat(tmp)
|
||||
if err != nil {
|
||||
t.Fatalf("stat %s: %v", tmp, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Errorf("%s is not a directory", tmp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemFlagSetsORCAHOME(t *testing.T) {
|
||||
t.Setenv("ORCA_HOME", "")
|
||||
resetRootFlags(t)
|
||||
|
||||
rootCmd.SetArgs([]string{"--system", "init"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("--system init: %v", err)
|
||||
}
|
||||
if got := os.Getenv("ORCA_HOME"); got != systemNamespaceRoot {
|
||||
t.Errorf("ORCA_HOME = %q, want %q", got, systemNamespaceRoot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemFlagConflictsWithORCAHOME(t *testing.T) {
|
||||
t.Setenv("ORCA_HOME", "/custom/path")
|
||||
resetRootFlags(t)
|
||||
|
||||
rootCmd.SetArgs([]string{"--system", "init"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for --system + ORCA_HOME conflict, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitJSONOutput(t *testing.T) {
|
||||
tmp := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", tmp)
|
||||
resetRootFlags(t)
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetArgs([]string{"init", "--json"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("init --json: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]string
|
||||
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
||||
t.Fatalf("unmarshal init output: %v\noutput: %s", err, buf.String())
|
||||
}
|
||||
if result["path"] != tmp {
|
||||
t.Errorf("init --json path = %q, want %q", result["path"], tmp)
|
||||
}
|
||||
if result["status"] != "initialized" {
|
||||
t.Errorf("init --json status = %q, want %q", result["status"], "initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemFlagIsPersistent(t *testing.T) {
|
||||
for _, name := range []string{"system", "json"} {
|
||||
f := rootCmd.PersistentFlags().Lookup(name)
|
||||
if f == nil {
|
||||
t.Errorf("persistent flag %q not found", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-1
@@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@@ -13,6 +14,8 @@ var (
|
||||
buildTime = "unknown"
|
||||
)
|
||||
|
||||
const systemNamespaceRoot = "/root/.orca"
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "orca",
|
||||
Short: "Orca — offline/CLI-first orchestration engine",
|
||||
@@ -21,12 +24,27 @@ inspired by HashiCorp Nomad, prioritizing stability, security, and simplicity
|
||||
over feature richness.`,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if systemNamespace {
|
||||
if existing := os.Getenv("ORCA_HOME"); existing != "" && existing != systemNamespaceRoot {
|
||||
return fmt.Errorf("--system conflicts with ORCA_HOME=%q (already set); unset ORCA_HOME or drop --system", existing)
|
||||
}
|
||||
if err := os.Setenv("ORCA_HOME", systemNamespaceRoot); err != nil {
|
||||
return fmt.Errorf("set ORCA_HOME for --system: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var jsonOutput bool
|
||||
var (
|
||||
jsonOutput bool
|
||||
systemNamespace bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().BoolVar(&jsonOutput, "json", false, "output in JSON format")
|
||||
rootCmd.PersistentFlags().BoolVar(&systemNamespace, "system", false, "use system-level namespace root (/root/.orca) instead of user-level (~/.orca)")
|
||||
}
|
||||
|
||||
func Execute() error {
|
||||
|
||||
@@ -7,15 +7,13 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
||||
)
|
||||
|
||||
func Open(path string) (*sql.DB, error) {
|
||||
if path == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get home dir: %w", err)
|
||||
}
|
||||
path = filepath.Join(home, ".orca", "orca.db")
|
||||
path = certpaths.DBPath()
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create db dir: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user