7bb31d4c09
P0a2 — Namespace inheritance resolver + orca ns CLI subcommands. Resolver (REQ-082, internal/ns/resolve.go): - Pure Resolve() function: DFS post-order chain assembly (most-specific first, _defaults implicit last D-185). Child-wins-scalar env merge, de-duped union constraints. Cycle detection with readable cycle path. Missing-parent + missing-_defaults + misordering (['_defaults','x']) rejection. Opt-out impossible (D-187). 89.6% coverage. Parser (internal/ns/parse.go): - ParseNSMd: hand-rolled YAML frontmatter (no yaml.v3 dep). Validates kind:Namespace + name, parses parents flow-array, inherits_env/secrets. - ParseNSMdDir: walks root/*/ns.md, skips cluster/, requires _defaults. CLI (internal/cli/ns.go, D-176): - orca ns list/create/delete/inspect/validate. Inspect + validate use the resolver. Create refuses _defaults/cluster; delete refuses _defaults + non-empty namespaces. JSON output support. 85.2% coverage. - Registered on rootCmd. Tests: resolve_test.go (11 tests), parse_test.go (14 tests), ns_test.go (21 tests). 18 packages pass, 20 bats pass, gofmt clean, verify-reqs 90 consistent. ---ci--- project: orca phase: P0a2 milestone: v0.9 status: execute ---/ci---
228 lines
5.9 KiB
Go
228 lines
5.9 KiB
Go
package ns
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func writeNSMd(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", path, err)
|
|
}
|
|
}
|
|
|
|
const validNSMd = `---
|
|
kind: Namespace
|
|
name: prod
|
|
parents: ["_defaults"]
|
|
inherits_env: true
|
|
inherits_secrets: true
|
|
quota:
|
|
cpu: 4
|
|
acl:
|
|
admin: ops
|
|
---
|
|
# Prod namespace
|
|
|
|
This body is ignored.
|
|
`
|
|
|
|
func TestParseNSMdValid(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, validNSMd)
|
|
cfg, err := ParseNSMd(path)
|
|
if err != nil {
|
|
t.Fatalf("ParseNSMd: %v", err)
|
|
}
|
|
if cfg.Name != "prod" {
|
|
t.Errorf("name = %q, want prod", cfg.Name)
|
|
}
|
|
if !eqSlice(cfg.Parents, []string{"_defaults"}) {
|
|
t.Errorf("parents = %v, want [_defaults]", cfg.Parents)
|
|
}
|
|
if !cfg.InheritsEnv || !cfg.InheritsSecrets {
|
|
t.Errorf("inherits_env=%v inherits_secrets=%v, want both true", cfg.InheritsEnv, cfg.InheritsSecrets)
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdMissingFrontmatter(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "# just a body, no frontmatter\n")
|
|
_, err := ParseNSMd(path)
|
|
if err == nil {
|
|
t.Fatal("expected missing frontmatter error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing frontmatter") {
|
|
t.Errorf("error = %q, want contains 'missing frontmatter'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdEmptyFrontmatter(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\n---\nbody\n")
|
|
_, err := ParseNSMd(path)
|
|
if err == nil {
|
|
t.Fatal("expected error for empty frontmatter, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdWrongKind(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\nkind: Job\nname: x\n---\n")
|
|
_, err := ParseNSMd(path)
|
|
if err == nil {
|
|
t.Fatal("expected wrong-kind error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "not \"Namespace\"") {
|
|
t.Errorf("error = %q, want contains 'is not \"Namespace\"'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdMissingKind(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\nname: x\n---\n")
|
|
_, err := ParseNSMd(path)
|
|
if err == nil {
|
|
t.Fatal("expected missing kind error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing kind") {
|
|
t.Errorf("error = %q, want contains 'missing kind'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdMissingName(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\nkind: Namespace\n---\n")
|
|
_, err := ParseNSMd(path)
|
|
if err == nil {
|
|
t.Fatal("expected missing name error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "missing name") {
|
|
t.Errorf("error = %q, want contains 'missing name'", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdParentsUnquoted(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\nkind: Namespace\nname: x\nparents: [a, b]\n---\n")
|
|
cfg, err := ParseNSMd(path)
|
|
if err != nil {
|
|
t.Fatalf("ParseNSMd: %v", err)
|
|
}
|
|
if !eqSlice(cfg.Parents, []string{"a", "b"}) {
|
|
t.Errorf("parents = %v, want [a b]", cfg.Parents)
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdParentsEmpty(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\nkind: Namespace\nname: x\nparents: []\n---\n")
|
|
cfg, err := ParseNSMd(path)
|
|
if err != nil {
|
|
t.Fatalf("ParseNSMd: %v", err)
|
|
}
|
|
if len(cfg.Parents) != 0 {
|
|
t.Errorf("parents = %v, want empty", cfg.Parents)
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdInheritsFalse(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
path := filepath.Join(tmp, "ns.md")
|
|
writeNSMd(t, path, "---\nkind: Namespace\nname: x\ninherits_env: false\ninherits_secrets: no\n---\n")
|
|
cfg, err := ParseNSMd(path)
|
|
if err != nil {
|
|
t.Fatalf("ParseNSMd: %v", err)
|
|
}
|
|
if cfg.InheritsEnv {
|
|
t.Errorf("inherits_env should be false")
|
|
}
|
|
if cfg.InheritsSecrets {
|
|
t.Errorf("inherits_secrets should be false")
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdMissingFile(t *testing.T) {
|
|
_, err := ParseNSMd(filepath.Join(t.TempDir(), "nope.md"))
|
|
if err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdDirHappy(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeNSMd(t, filepath.Join(root, "_defaults", "ns.md"), "---\nkind: Namespace\nname: _defaults\n---\n")
|
|
writeNSMd(t, filepath.Join(root, "prod", "ns.md"), validNSMd)
|
|
|
|
cfgs, err := ParseNSMdDir(root)
|
|
if err != nil {
|
|
t.Fatalf("ParseNSMdDir: %v", err)
|
|
}
|
|
if _, ok := cfgs["_defaults"]; !ok {
|
|
t.Errorf("missing _defaults in %v", cfgs)
|
|
}
|
|
if _, ok := cfgs["prod"]; !ok {
|
|
t.Errorf("missing prod in %v", cfgs)
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdDirMissingDefaults(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeNSMd(t, filepath.Join(root, "prod", "ns.md"), validNSMd)
|
|
_, err := ParseNSMdDir(root)
|
|
if err == nil {
|
|
t.Fatal("expected missing _defaults error, got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), "_defaults") {
|
|
t.Errorf("error = %q, want contains _defaults", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdDirSkipsCluster(t *testing.T) {
|
|
root := t.TempDir()
|
|
writeNSMd(t, filepath.Join(root, "_defaults", "ns.md"), "---\nkind: Namespace\nname: _defaults\n---\n")
|
|
// cluster/ contains a ns.md-shaped file but must be skipped.
|
|
writeNSMd(t, filepath.Join(root, "cluster", "ns.md"), "---\nkind: Namespace\nname: cluster\n---\n")
|
|
|
|
cfgs, err := ParseNSMdDir(root)
|
|
if err != nil {
|
|
t.Fatalf("ParseNSMdDir: %v", err)
|
|
}
|
|
if _, ok := cfgs["cluster"]; ok {
|
|
t.Errorf("cluster should be skipped, present in %v", cfgs)
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdDirNoFiles(t *testing.T) {
|
|
root := t.TempDir()
|
|
_, err := ParseNSMdDir(root)
|
|
if err == nil {
|
|
t.Fatal("expected missing _defaults error on empty dir, got nil")
|
|
}
|
|
}
|
|
|
|
func TestParseNSMdDirNotADir(t *testing.T) {
|
|
tmp := t.TempDir()
|
|
// Create a file with the same name as the expected root dir.
|
|
root := filepath.Join(tmp, "notadir")
|
|
writeNSMd(t, root, "x")
|
|
_, err := ParseNSMdDir(root)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-dir root")
|
|
}
|
|
}
|