feat(P0a2): namespace CRUD + inheritance engine (REQ-082)
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---
This commit is contained in:
@@ -34,6 +34,7 @@ func resetCommandFlags() {
|
||||
stopID, runTarget, runIDKey, jobWatch = "", "", "", false
|
||||
capSetCPU, capSetMem, capSetDisk, capNodeID = 0, 0, 0, ""
|
||||
auditLimit = 50
|
||||
resetNSFlags()
|
||||
}
|
||||
|
||||
func TestNamespaceDefaultsToUserHome(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
// Package cli: ns.go implements the `orca ns` subcommand family
|
||||
// (REQ-082, D-176). Subcommands:
|
||||
//
|
||||
// orca ns list — list all namespaces under ORCA_HOME
|
||||
// orca ns create <name> — create a namespace dir + ns.md
|
||||
// orca ns delete <name> — remove an empty namespace dir
|
||||
// orca ns inspect <name> — print effective chain + merged env
|
||||
// orca ns validate <name> — cycle + missing-parent + schema checks
|
||||
//
|
||||
// All subcommands honor $ORCA_HOME via internal/paths. The inheritance
|
||||
// resolver (internal/ns) is a pure function shared by inspect + validate.
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/ns"
|
||||
"git.cloudinit.dev/coreci/orca/internal/paths"
|
||||
)
|
||||
|
||||
var nsCmd = &cobra.Command{
|
||||
Use: "ns",
|
||||
Short: "Manage orca namespaces",
|
||||
Long: `Manage orca namespaces under ORCA_HOME (R-002).
|
||||
|
||||
Each namespace is a directory with ns.md, .env, .env.secrets, db/,
|
||||
jobs/, alloc/. The implicit root namespace _defaults always exists
|
||||
(D-159); every namespace inherits from _defaults (D-185) and cannot
|
||||
opt out (D-187).`,
|
||||
}
|
||||
|
||||
var (
|
||||
nsCreateParent string
|
||||
nsCreateInheritsEnv bool
|
||||
nsCreateInheritsSecret bool
|
||||
)
|
||||
|
||||
var nsListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all namespaces under ORCA_HOME",
|
||||
Long: `List all namespaces under ORCA_HOME (directories containing ns.md, plus the implicit _defaults).`,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
root := paths.Root()
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read ORCA_HOME %s: %w", root, err)
|
||||
}
|
||||
type nsRow struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Default bool `json:"default"`
|
||||
}
|
||||
var rows []nsRow
|
||||
for _, ent := range entries {
|
||||
if !ent.IsDir() {
|
||||
continue
|
||||
}
|
||||
if ent.Name() == "cluster" {
|
||||
continue
|
||||
}
|
||||
nsMd := filepath.Join(root, ent.Name(), "ns.md")
|
||||
if _, err := os.Stat(nsMd); err != nil {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, nsRow{
|
||||
Name: ent.Name(),
|
||||
Path: filepath.Join(root, ent.Name()),
|
||||
Default: ent.Name() == paths.DefaultNamespace(),
|
||||
})
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
if rows[i].Name == paths.DefaultNamespace() {
|
||||
return true
|
||||
}
|
||||
if rows[j].Name == paths.DefaultNamespace() {
|
||||
return false
|
||||
}
|
||||
return rows[i].Name < rows[j].Name
|
||||
})
|
||||
if jsonOutput {
|
||||
return printJSON(rows)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "No namespaces found. Run 'orca init' first.")
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-10s %s\n", "NAME", "DEFAULT", "PATH")
|
||||
for _, r := range rows {
|
||||
def := ""
|
||||
if r.Default {
|
||||
def = "*"
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "%-20s %-10s %s\n", r.Name, def, r.Path)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nsCreateCmd = &cobra.Command{
|
||||
Use: "create <name>",
|
||||
Short: "Create a namespace directory + ns.md",
|
||||
Long: `Create a namespace under ORCA_HOME. Builds the dir structure
|
||||
(db/, jobs/, alloc/) and writes ns.md frontmatter. --parent may be
|
||||
repeated to declare inheritance; _defaults is always appended last.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if name == paths.DefaultNamespace() {
|
||||
return fmt.Errorf("cannot create the implicit root namespace %q with `ns create` (it is auto-managed)", name)
|
||||
}
|
||||
if name == "cluster" {
|
||||
return fmt.Errorf("name %q is reserved for the cluster-wide dir", name)
|
||||
}
|
||||
if nsCreateParent == "" {
|
||||
nsCreateParent = paths.DefaultNamespace()
|
||||
}
|
||||
nsDir := paths.NamespaceDir(name)
|
||||
if _, err := os.Stat(nsDir); err == nil {
|
||||
if _, statErr := os.Stat(paths.NSMd(name)); statErr == nil {
|
||||
return fmt.Errorf("namespace %q already exists at %s", name, nsDir)
|
||||
}
|
||||
}
|
||||
for _, sub := range []string{"db", "jobs", "alloc"} {
|
||||
if err := os.MkdirAll(filepath.Join(nsDir, sub), 0o755); err != nil {
|
||||
return fmt.Errorf("create %s/%s: %w", nsDir, sub, err)
|
||||
}
|
||||
}
|
||||
parents := []string{nsCreateParent}
|
||||
if nsCreateParent == paths.DefaultNamespace() {
|
||||
// Explicit _defaults listing is allowed (de-duped silently).
|
||||
}
|
||||
body := renderNSMd(name, parents, nsCreateInheritsEnv, nsCreateInheritsSecret)
|
||||
if err := os.WriteFile(paths.NSMd(name), []byte(body), 0o644); err != nil {
|
||||
return fmt.Errorf("write ns.md: %w", err)
|
||||
}
|
||||
if jsonOutput {
|
||||
return printJSON(map[string]any{
|
||||
"name": name,
|
||||
"path": nsDir,
|
||||
"parents": parents,
|
||||
"ns_md": paths.NSMd(name),
|
||||
"inherits_env": nsCreateInheritsEnv,
|
||||
"inherits_secrets": nsCreateInheritsSecret,
|
||||
})
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "✓ Namespace created: %s (%s)\n", name, nsDir)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nsDeleteCmd = &cobra.Command{
|
||||
Use: "delete <name>",
|
||||
Short: "Remove an empty namespace directory",
|
||||
Long: `Remove a namespace directory. Refuses if jobs/ or alloc/
|
||||
contain any files (non-empty namespace). The implicit root _defaults
|
||||
cannot be deleted.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
if name == paths.DefaultNamespace() {
|
||||
return fmt.Errorf("cannot delete the implicit root namespace %q", name)
|
||||
}
|
||||
nsDir := paths.NamespaceDir(name)
|
||||
if _, err := os.Stat(nsDir); err != nil {
|
||||
return fmt.Errorf("namespace %q not found: %w", name, err)
|
||||
}
|
||||
for _, sub := range []string{"jobs", "alloc"} {
|
||||
dir := filepath.Join(nsDir, sub)
|
||||
if err := dirNonEmpty(dir); err != nil {
|
||||
return fmt.Errorf("refusing to delete %q: %s is non-empty (%w); clear it first", name, sub, err)
|
||||
}
|
||||
}
|
||||
if err := os.RemoveAll(nsDir); err != nil {
|
||||
return fmt.Errorf("delete %s: %w", nsDir, err)
|
||||
}
|
||||
if jsonOutput {
|
||||
return printJSON(map[string]string{"name": name, "deleted": nsDir})
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "✓ Namespace deleted: %s (%s)\n", name, nsDir)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nsInspectCmd = &cobra.Command{
|
||||
Use: "inspect <name>",
|
||||
Short: "Print the effective chain, merged env, and constraints",
|
||||
Long: `Resolve a namespace's inheritance chain and print the merged env and unioned constraints (uses the resolver).`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
root := paths.Root()
|
||||
cfgs, err := ns.ParseNSMdDir(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load namespaces: %w", err)
|
||||
}
|
||||
if _, ok := cfgs[name]; !ok {
|
||||
return fmt.Errorf("namespace %q not found under %s", name, root)
|
||||
}
|
||||
resolved, err := ns.Resolve(cfgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve: %w", err)
|
||||
}
|
||||
r := resolved[name]
|
||||
if r == nil {
|
||||
return fmt.Errorf("namespace %q resolved to nil", name)
|
||||
}
|
||||
if jsonOutput {
|
||||
return printJSON(map[string]any{
|
||||
"name": r.Name,
|
||||
"chain": r.Chain,
|
||||
"env": r.Env,
|
||||
"constraints": r.Constraints,
|
||||
})
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Namespace: %s\n", r.Name)
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "Chain: %s\n", strings.Join(r.Chain, " -> "))
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Env:")
|
||||
keys := sortedKeys(r.Env)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), " %s = %s\n", k, r.Env[k])
|
||||
}
|
||||
fmt.Fprintln(cmd.OutOrStdout(), "Constraints:")
|
||||
if len(r.Constraints) == 0 {
|
||||
fmt.Fprintln(cmd.OutOrStdout(), " (none)")
|
||||
} else {
|
||||
for _, c := range r.Constraints {
|
||||
fmt.Fprintf(cmd.OutOrStdout(), " - %s\n", c)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var nsValidateCmd = &cobra.Command{
|
||||
Use: "validate <name>",
|
||||
Short: "Run cycle + missing-parent + schema checks on a namespace",
|
||||
Long: `Validate a namespace's inheritance chain and ns.md frontmatter.
|
||||
Exits 0 if valid, 1 on error. Runs over ALL namespaces under ORCA_HOME
|
||||
(parsing + resolving validates cycles and missing parents across the
|
||||
set).`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
name := args[0]
|
||||
root := paths.Root()
|
||||
cfgs, err := ns.ParseNSMdDir(root)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load namespaces: %w", err)
|
||||
}
|
||||
if _, ok := cfgs[name]; !ok {
|
||||
return fmt.Errorf("namespace %q not found under %s", name, root)
|
||||
}
|
||||
resolved, err := ns.Resolve(cfgs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate: %w", err)
|
||||
}
|
||||
r := resolved[name]
|
||||
if r == nil {
|
||||
return fmt.Errorf("namespace %q resolved to nil", name)
|
||||
}
|
||||
if jsonOutput {
|
||||
return printJSON(map[string]any{
|
||||
"name": r.Name,
|
||||
"valid": true,
|
||||
"chain": r.Chain,
|
||||
})
|
||||
}
|
||||
fmt.Fprintf(cmd.OutOrStdout(), "✓ %s valid\n chain: %s\n", name, strings.Join(r.Chain, " -> "))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// renderNSMd writes a minimal ns.md frontmatter for `orca ns create`.
|
||||
func renderNSMd(name string, parents []string, inheritsEnv, inheritsSecrets bool) string {
|
||||
var b strings.Builder
|
||||
b.WriteString("---\n")
|
||||
b.WriteString("kind: Namespace\n")
|
||||
b.WriteString("name: ")
|
||||
b.WriteString(name)
|
||||
b.WriteString("\n")
|
||||
if len(parents) > 0 {
|
||||
quoted := make([]string, len(parents))
|
||||
for i, p := range parents {
|
||||
quoted[i] = fmt.Sprintf("%q", p)
|
||||
}
|
||||
b.WriteString("parents: [")
|
||||
b.WriteString(strings.Join(quoted, ", "))
|
||||
b.WriteString("]\n")
|
||||
}
|
||||
fmt.Fprintf(&b, "inherits_env: %t\n", inheritsEnv)
|
||||
fmt.Fprintf(&b, "inherits_secrets: %t\n", inheritsSecrets)
|
||||
b.WriteString("---\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dirNonEmpty returns an error wrapping the offending entry if dir
|
||||
// contains any entries.
|
||||
func dirNonEmpty(dir string) error {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
return fmt.Errorf("contains %s", e.Name())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortedKeys(m map[string]string) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func init() {
|
||||
nsCreateCmd.Flags().StringVar(&nsCreateParent, "parent", "", "parent namespace (default _defaults; the implicit root is always appended last)")
|
||||
nsCreateCmd.Flags().BoolVar(&nsCreateInheritsEnv, "inherits-env", true, "inherit env from parents (default true)")
|
||||
nsCreateCmd.Flags().BoolVar(&nsCreateInheritsSecret, "inherits-secrets", true, "inherit secrets from parents (default true)")
|
||||
|
||||
nsCmd.AddCommand(nsListCmd)
|
||||
nsCmd.AddCommand(nsCreateCmd)
|
||||
nsCmd.AddCommand(nsDeleteCmd)
|
||||
nsCmd.AddCommand(nsInspectCmd)
|
||||
nsCmd.AddCommand(nsValidateCmd)
|
||||
rootCmd.AddCommand(nsCmd)
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.cloudinit.dev/coreci/orca/internal/paths"
|
||||
)
|
||||
|
||||
// resetNSFlags zeroes the ns subcommand flag-bound vars so tests don't
|
||||
// leak state.
|
||||
func resetNSFlags() {
|
||||
nsCreateParent = ""
|
||||
nsCreateInheritsEnv = true
|
||||
nsCreateInheritsSecret = true
|
||||
}
|
||||
|
||||
func writeDefaultsNS(t *testing.T, root string) {
|
||||
t.Helper()
|
||||
nsDir := filepath.Join(root, "_defaults")
|
||||
if err := os.MkdirAll(nsDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir _defaults: %v", err)
|
||||
}
|
||||
body := "---\nkind: Namespace\nname: _defaults\ninherits_env: true\ninherits_secrets: true\n---\n# defaults\n"
|
||||
if err := os.WriteFile(filepath.Join(nsDir, "ns.md"), []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("write _defaults ns.md: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func writeCustomNS(t *testing.T, root, name, parentsList string) {
|
||||
t.Helper()
|
||||
nsDir := filepath.Join(root, name)
|
||||
if err := os.MkdirAll(nsDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", name, err)
|
||||
}
|
||||
body := "---\nkind: Namespace\nname: " + name + "\n"
|
||||
if parentsList != "" {
|
||||
body += "parents: " + parentsList + "\n"
|
||||
}
|
||||
body += "inherits_env: true\ninherits_secrets: true\n---\n# " + name + "\n"
|
||||
if err := os.WriteFile(filepath.Join(nsDir, "ns.md"), []byte(body), 0o644); err != nil {
|
||||
t.Fatalf("write %s ns.md: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSListEmpty(t *testing.T) {
|
||||
t.Setenv("ORCA_HOME", t.TempDir())
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "list"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns list: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSListWithNamespaces(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "list"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns list: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSCreateHappy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "create", "prod"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns create: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "prod", "ns.md")); err != nil {
|
||||
t.Fatalf("ns.md not created: %v", err)
|
||||
}
|
||||
for _, sub := range []string{"db", "jobs", "alloc"} {
|
||||
if _, err := os.Stat(filepath.Join(root, "prod", sub)); err != nil {
|
||||
t.Errorf("subdir %s not created: %v", sub, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSCreateWithParent(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "base", "")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "create", "child", "--parent", "base"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns create: %v", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(root, "child", "ns.md"))
|
||||
if err != nil {
|
||||
t.Fatalf("read ns.md: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "parents: [") || !strings.Contains(string(data), "\"base\"") {
|
||||
t.Errorf("ns.md missing parents: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSCreateExisting(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "create", "prod"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error creating existing namespace, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "already exists") {
|
||||
t.Errorf("error = %q, want contains 'already exists'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSCreateDefaultsRefused(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "create", "_defaults"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error creating _defaults, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSCreateClusterRefused(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "create", "cluster"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error creating cluster, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSDeleteHappy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "delete", "prod"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns delete: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, "prod")); !os.IsNotExist(err) {
|
||||
t.Errorf("prod dir still exists after delete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSDeleteDefaultsRefused(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "delete", "_defaults"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error deleting _defaults, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSDeleteNonEmpty(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
// put a job in jobs/
|
||||
if err := os.MkdirAll(filepath.Join(root, "prod", "jobs"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir jobs: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(root, "prod", "jobs", "j1.md"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatalf("write job: %v", err)
|
||||
}
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "delete", "prod"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error deleting non-empty namespace, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "refusing to delete") {
|
||||
t.Errorf("error = %q, want contains 'refusing to delete'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSDeleteMissing(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "delete", "ghost"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error deleting missing namespace, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSInspectHappy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "inspect", "prod"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns inspect: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSInspectJSON(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
|
||||
var buf bytes.Buffer
|
||||
rootCmd.SetOut(&buf)
|
||||
rootCmd.SetArgs([]string{"ns", "inspect", "prod", "--json"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns inspect --json: %v", err)
|
||||
}
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
||||
t.Fatalf("unmarshal: %v\n%s", err, buf.String())
|
||||
}
|
||||
if result["name"] != "prod" {
|
||||
t.Errorf("name = %v, want prod", result["name"])
|
||||
}
|
||||
chain, _ := result["chain"].([]any)
|
||||
if len(chain) < 2 {
|
||||
t.Errorf("chain too short: %v", chain)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSInspectMissingNamespace(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "inspect", "ghost"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing namespace, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSValidateHappy(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "prod", "")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "validate", "prod"})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("ns validate: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSValidateCycle(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
// a -> b, b -> a (cycle)
|
||||
writeCustomNS(t, root, "a", "[\"b\"]")
|
||||
writeCustomNS(t, root, "b", "[\"a\"]")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "validate", "a"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected cycle error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "cycle") {
|
||||
t.Errorf("error = %q, want contains 'cycle'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSValidateMissingParent(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
writeCustomNS(t, root, "x", "[\"ghost\"]")
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "validate", "x"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected missing-parent error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "ghost") || !strings.Contains(err.Error(), "not found") {
|
||||
t.Errorf("error = %q, want contains ghost + not found", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSValidateMissingNamespace(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
t.Setenv("ORCA_HOME", root)
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
writeDefaultsNS(t, root)
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "validate", "ghost"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing namespace, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderNSMd(t *testing.T) {
|
||||
body := renderNSMd("foo", []string{"_defaults"}, true, false)
|
||||
if !strings.Contains(body, "kind: Namespace") {
|
||||
t.Errorf("missing kind: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "name: foo") {
|
||||
t.Errorf("missing name: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "inherits_env: true") {
|
||||
t.Errorf("missing inherits_env true: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "inherits_secrets: false") {
|
||||
t.Errorf("missing inherits_secrets false: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSRootRegistered(t *testing.T) {
|
||||
found := false
|
||||
for _, c := range rootCmd.Commands() {
|
||||
if c.Use == "ns" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("ns command not registered on root")
|
||||
}
|
||||
// ensure subcommands present
|
||||
sub := map[string]bool{}
|
||||
for _, c := range rootCmd.Commands() {
|
||||
if c.Use == "ns" {
|
||||
for _, sc := range c.Commands() {
|
||||
sub[sc.Use] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, want := range []string{"list", "create <name>", "delete <name>", "inspect <name>", "validate <name>"} {
|
||||
if !sub[want] {
|
||||
t.Errorf("missing ns subcommand %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNSListNoORCAHOME(t *testing.T) {
|
||||
// ORCA_HOME points at a nonexistent dir; list should error.
|
||||
t.Setenv("ORCA_HOME", filepath.Join(t.TempDir(), "nope"))
|
||||
resetRootFlags(t)
|
||||
resetNSFlags()
|
||||
|
||||
rootCmd.SetArgs([]string{"ns", "list"})
|
||||
err := rootCmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing ORCA_HOME, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
var _ = paths.DefaultNamespace // keep paths import alive
|
||||
Reference in New Issue
Block a user