feat(P13): ns subcommands (inherit, set-constraint) + deprecation warnings (REQ-068)

orca ns inherit <name> --parent (cycle detection), ns set-constraint
<key>=value>. Deprecation warnings on orca cert ca-init/gen/renew
(step-ca replaces) and .hcl jobspec (R-013). --no-deprecation-warnings
suppresses all.

---ci---
project: orca
phase: 13
milestone: v0.11
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-08-07 07:47:28 +00:00
parent 020aa01623
commit 97b88a703c
7 changed files with 581 additions and 12 deletions
+3 -4
View File
@@ -60,10 +60,6 @@ Deprecated: v0.9 re-architecture replaces the internal CA with step-ca
(D-101/REQ-076). The ` + "`orca cert`" + ` command tree is retained for the
dual-write window and scheduled for deletion in v0.10. See
.ciagent/PRD_v0.9.md.`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
warnDeprecated("orca cert is deprecated in v0.9: step-ca (D-101) now handles CA; orca cert will be removed in v0.10 — see .ciagent/PRD_v0.9.md")
return nil
},
}
certCmd.AddCommand(newCAInitCmd(log))
@@ -81,6 +77,7 @@ func newCAInitCmd(log *slog.Logger) *cobra.Command {
Short: "Initialize a local orca CA (ca.crt + ca.key) under ~/.orca",
Long: "Generates a new RSA CA cert and writes it to ~/.orca/ca.crt (0644) and ~/.orca/ca.key (0600) per REQ-033.",
RunE: func(cmd *cobra.Command, args []string) error {
warnDeprecated("orca cert ca-init is deprecated: use step-ca (R-006); the internal CA is replaced by step-ca (D-101) — see .ciagent/PRD_v0.9.md")
dir := CADir()
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("mkdir %s: %w", dir, err)
@@ -114,6 +111,7 @@ func newGenCmd(log *slog.Logger) *cobra.Command {
Short: "Generate a server cert (CSR + sign) under ~/.orca",
Long: "Builds a CSR with the requested SANs, signs it with the local CA, and writes server.crt + server.key.",
RunE: func(cmd *cobra.Command, args []string) error {
warnDeprecated("orca cert gen is deprecated: use step-ca + orca node join for cert generation (D-101) — see .ciagent/PRD_v0.9.md")
dir := CADir()
if cn == "" {
cn = "orca-server"
@@ -185,6 +183,7 @@ func newRenewCmd(log *slog.Logger) *cobra.Command {
Short: "Rotate the server cert (hot-swapped by the daemon; REQ-034)",
Long: "Re-runs `cert gen` and overwrites server.crt / server.key in place. The daemon's GetCertificate callback picks up the new cert on the next handshake — no restart required.",
RunE: func(cmd *cobra.Command, args []string) error {
warnDeprecated("orca cert renew is deprecated: use step-ca for cert rotation (D-101) — see .ciagent/PRD_v0.9.md")
dir := CADir()
if cn == "" {
cn = "orca-server"
+7 -7
View File
@@ -133,8 +133,8 @@ func TestNoDeprecationWarningsFlagRegistered(t *testing.T) {
}
}
// TestCertEmitsDeprecationWarning verifies REQ-068: `orca cert`
// subcommands emit a deprecation banner.
// TestCertEmitsDeprecationWarning verifies REQ-068: deprecated
// `orca cert ca-init` subcommand emits a deprecation banner.
func TestCertEmitsDeprecationWarning(t *testing.T) {
_, cleanup := initTestEnv(t)
defer cleanup()
@@ -146,12 +146,12 @@ func TestCertEmitsDeprecationWarning(t *testing.T) {
var out bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
rootCmd.SetArgs([]string{"cert", "fingerprint", "--which", "ca"})
rootCmd.SetArgs([]string{"cert", "ca-init", "--cn", "test-ca"})
_ = rootCmd.Execute()
logged := buf.String()
if !strings.Contains(logged, "orca cert is deprecated in v0.9") {
t.Errorf("expected cert deprecation warning, got:\n%s", logged)
if !strings.Contains(logged, "orca cert ca-init is deprecated") {
t.Errorf("expected cert ca-init deprecation warning, got:\n%s", logged)
}
if !strings.Contains(logged, "step-ca") {
t.Errorf("deprecation warning should mention step-ca, got:\n%s", logged)
@@ -172,10 +172,10 @@ func TestCertDeprecationWarningSuppressed(t *testing.T) {
var out bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
rootCmd.SetArgs([]string{"cert", "fingerprint", "--which", "ca"})
rootCmd.SetArgs([]string{"cert", "ca-init", "--cn", "test-ca"})
_ = rootCmd.Execute()
if strings.Contains(buf.String(), "orca cert is deprecated") {
if strings.Contains(buf.String(), "orca cert ca-init is deprecated") {
t.Errorf("--no-deprecation-warnings should suppress cert warning, got:\n%s", buf.String())
}
}
+177
View File
@@ -0,0 +1,177 @@
package cli
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
// runWithSlog executes the given args against rootCmd, capturing the
// slog output (where warnDeprecated writes). It returns the captured
// slog buffer and the command stdout buffer.
func runWithSlog(t *testing.T, args []string, suppressWarnings bool) (slogOut, stdOut string, err error) {
t.Helper()
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
buf, restore := captureSlog(t)
defer restore()
if suppressWarnings {
_ = rootCmd.PersistentFlags().Set("no-deprecation-warnings", "true")
}
var out bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
rootCmd.SetArgs(args)
err = rootCmd.Execute()
return buf.String(), out.String(), err
}
func TestDeprecationDaemonEmitsWarning(t *testing.T) {
slogOut, _ := runDaemonHermetic(t, false)
if !strings.Contains(slogOut, "orca daemon is deprecated in v0.9") {
t.Errorf("expected daemon deprecation warning, got:\n%s", slogOut)
}
}
func TestDeprecationCertCAInitEmitsWarning(t *testing.T) {
slogOut, _, err := runWithSlog(t, []string{"cert", "ca-init", "--cn", "dep-test"}, false)
if err != nil {
t.Fatalf("cert ca-init: %v", err)
}
if !strings.Contains(slogOut, "orca cert ca-init is deprecated") {
t.Errorf("expected cert ca-init deprecation warning, got:\n%s", slogOut)
}
if !strings.Contains(slogOut, "step-ca") {
t.Errorf("deprecation warning should mention step-ca, got:\n%s", slogOut)
}
}
func TestDeprecationCertGenEmitsWarning(t *testing.T) {
// gen requires a CA; we only assert the warning fires (before the
// error path).
slogOut, _, _ := runWithSlog(t, []string{"cert", "gen", "--cn", "dep-gen"}, false)
if !strings.Contains(slogOut, "orca cert gen is deprecated") {
t.Errorf("expected cert gen deprecation warning, got:\n%s", slogOut)
}
}
func TestDeprecationCertRenewEmitsWarning(t *testing.T) {
// renew requires a CA; we only assert the warning fires (before the
// error path).
slogOut, _, _ := runWithSlog(t, []string{"cert", "renew"}, false)
if !strings.Contains(slogOut, "orca cert renew is deprecated") {
t.Errorf("expected cert renew deprecation warning, got:\n%s", slogOut)
}
}
func TestDeprecationCertShowNoWarning(t *testing.T) {
slogOut, _, err := runWithSlog(t, []string{"cert", "show"}, false)
// show may fail if no cert exists; we only assert no deprecation.
_ = err
if strings.Contains(slogOut, "deprecated") {
t.Errorf("cert show must NOT emit deprecation warning, got:\n%s", slogOut)
}
}
func TestDeprecationCertFingerprintNoWarning(t *testing.T) {
// Need a CA first so fingerprint has something to read.
_, cleanup := initTestEnv(t)
defer cleanup()
resetRootFlags(t)
var b bytes.Buffer
rootCmd.SetOut(&b)
rootCmd.SetErr(&b)
rootCmd.SetArgs([]string{"cert", "ca-init", "--cn", "fp-test"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("cert ca-init: %v", err)
}
slogBuf, restore := captureSlog(t)
defer restore()
resetRootFlags(t)
var out bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&out)
rootCmd.SetArgs([]string{"cert", "fingerprint", "--which", "ca"})
_ = rootCmd.Execute()
if strings.Contains(slogBuf.String(), "deprecated") {
t.Errorf("cert fingerprint must NOT emit deprecation warning, got:\n%s", slogBuf.String())
}
}
func TestDeprecationJobRunHCLEmitsWarning(t *testing.T) {
dir := t.TempDir()
specPath := filepath.Join(dir, "old-spec.hcl")
if err := os.WriteFile(specPath, []byte(`job "true" {}
task "t" {
command = "/bin/true"
}
`), 0o644); err != nil {
t.Fatalf("write spec: %v", err)
}
slogOut, _, err := runWithSlog(t, []string{"job", "run", specPath}, false)
if err != nil {
t.Fatalf("job run: %v", err)
}
if !strings.Contains(slogOut, ".hcl jobspec is legacy") {
t.Errorf("expected .hcl deprecation warning, got:\n%s", slogOut)
}
if !strings.Contains(slogOut, "R-013") {
t.Errorf("deprecation warning should reference R-013, got:\n%s", slogOut)
}
}
func TestDeprecationJobRunMDNoWarning(t *testing.T) {
dir := t.TempDir()
specPath := filepath.Join(dir, "spec.md")
if err := os.WriteFile(specPath, []byte("---\nkind: Workload\nname: md-job\n---\n"), 0o644); err != nil {
t.Fatalf("write spec: %v", err)
}
slogOut, _, _ := runWithSlog(t, []string{"job", "run", specPath}, false)
if strings.Contains(slogOut, ".hcl jobspec is legacy") {
t.Errorf(".md jobspec must NOT emit .hcl deprecation warning, got:\n%s", slogOut)
}
}
func TestDeprecationWarningsSuppressedByFlag(t *testing.T) {
// daemon
slogOut, _ := runDaemonHermetic(t, true)
if strings.Contains(slogOut, "deprecated in v0.9") {
t.Errorf("--no-deprecation-warnings should suppress daemon warning, got:\n%s", slogOut)
}
// cert ca-init
slogOut2, _, err := runWithSlog(t, []string{"cert", "ca-init", "--cn", "sup-test"}, true)
if err != nil {
t.Fatalf("cert ca-init: %v", err)
}
if strings.Contains(slogOut2, "deprecated") {
t.Errorf("--no-deprecation-warnings should suppress cert warning, got:\n%s", slogOut2)
}
// job run .hcl
dir := t.TempDir()
specPath := filepath.Join(dir, "old-spec.hcl")
if err := os.WriteFile(specPath, []byte(`job "true" {}
task "t" {
command = "/bin/true"
}
`), 0o644); err != nil {
t.Fatalf("write spec: %v", err)
}
slogOut3, _, err := runWithSlog(t, []string{"job", "run", specPath}, true)
if err != nil {
t.Fatalf("job run: %v", err)
}
if strings.Contains(slogOut3, "deprecated") {
t.Errorf("--no-deprecation-warnings should suppress .hcl warning, got:\n%s", slogOut3)
}
}
+3
View File
@@ -49,6 +49,9 @@ var jobRunCmd = &cobra.Command{
Long: "Submit a job spec, execute its tasks, and persist the result. Use --target to pin to a specific node (overrides bin-packing); --idempotency-key for cross-node dispatch dedupe.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if strings.HasSuffix(args[0], ".hcl") {
warnDeprecated("orca job run <spec.hcl> is deprecated: .hcl jobspec is legacy (R-013); convert to .md format (REQ-064) — see .ciagent/PRD_v0.9.md")
}
spec, err := jobspec.ParseFile(args[0])
if err != nil {
return err
+144
View File
@@ -6,6 +6,8 @@
// 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
// orca ns inherit <name> — set the parent namespace (R-002)
// orca ns set-constraint <name> <key>=<value> — set a constraint
//
// All subcommands honor $ORCA_HOME via internal/paths. The inheritance
// resolver (internal/ns) is a pure function shared by inspect + validate.
@@ -291,6 +293,144 @@ set).`,
},
}
var (
nsInheritParent string
)
var nsInheritCmd = &cobra.Command{
Use: "inherit <name>",
Short: "Set the parent namespace for inheritance (R-002)",
Long: `Set the parent namespace for a namespace. Updates ns.md
frontmatter (parents) and validates the new chain has no cycles
(child cannot inherit from itself transitively). The implicit root
_defaults is always appended last (D-185).`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
if name == paths.DefaultNamespace() {
return fmt.Errorf("cannot set parent on the implicit root namespace %q", name)
}
if nsInheritParent == "" {
return fmt.Errorf("--parent is required")
}
if nsInheritParent == name {
return fmt.Errorf("namespace %q cannot inherit from itself", name)
}
nsMd := paths.NSMd(name)
cfg, nsBody, err := ns.ParseNSMdWithBody(nsMd)
if err != nil {
return fmt.Errorf("parse %s: %w", nsMd, err)
}
cfg.Parents = []string{nsInheritParent}
root := paths.Root()
cfgs, err := ns.ParseNSMdDir(root)
if err != nil {
return fmt.Errorf("load namespaces: %w", err)
}
cfgs[name] = cfg
if _, err := ns.Resolve(cfgs); err != nil {
return fmt.Errorf("cycle check: %w", err)
}
body := renderNSMdFull(cfg, nsBody)
if err := os.WriteFile(nsMd, []byte(body), 0o644); err != nil {
return fmt.Errorf("write %s: %w", nsMd, err)
}
if jsonOutput {
return printJSON(map[string]any{
"name": name,
"parents": cfg.Parents,
"ns_md": nsMd,
})
}
fmt.Fprintf(cmd.OutOrStdout(), "\u2713 Namespace %s now inherits from %s\n", name, nsInheritParent)
return nil
},
}
var nsSetConstraintCmd = &cobra.Command{
Use: "set-constraint <name> <key>=<value>",
Short: "Set a constraint on a namespace (stored in ns.md frontmatter)",
Long: `Set a constraint on a namespace. Constraints are key=value
strings (e.g. max-allocs=10) stored in ns.md frontmatter and unioned
across the inheritance chain by the resolver.`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
name := args[0]
kv := args[1]
if name == paths.DefaultNamespace() {
return fmt.Errorf("cannot set a constraint on the implicit root namespace %q with set-constraint; edit ns.md directly", name)
}
idx := strings.Index(kv, "=")
if idx <= 0 || idx == len(kv)-1 {
return fmt.Errorf("constraint must be <key>=<value>, got %q", kv)
}
constraint := kv
nsMd := paths.NSMd(name)
cfg, nsBody, err := ns.ParseNSMdWithBody(nsMd)
if err != nil {
return fmt.Errorf("parse %s: %w", nsMd, err)
}
for _, c := range cfg.Constraints {
if c == constraint {
return fmt.Errorf("constraint %q already set on namespace %q", constraint, name)
}
}
cfg.Constraints = append(cfg.Constraints, constraint)
body := renderNSMdFull(cfg, nsBody)
if err := os.WriteFile(nsMd, []byte(body), 0o644); err != nil {
return fmt.Errorf("write %s: %w", nsMd, err)
}
if jsonOutput {
return printJSON(map[string]any{
"name": name,
"constraints": cfg.Constraints,
"ns_md": nsMd,
})
}
fmt.Fprintf(cmd.OutOrStdout(), "\u2713 Constraint set on %s: %s\n", name, constraint)
return nil
},
}
// renderNSMdFull renders a complete ns.md from a parsed *ns.NSConfig
// plus an optional body (the markdown after the frontmatter). Used by
// the ns inherit / set-constraint editors to rewrite frontmatter while
// preserving the body.
func renderNSMdFull(cfg *ns.NSConfig, body string) string {
var b strings.Builder
b.WriteString("---\n")
b.WriteString("kind: Namespace\n")
b.WriteString("name: ")
b.WriteString(cfg.Name)
b.WriteString("\n")
if len(cfg.Parents) > 0 {
quoted := make([]string, len(cfg.Parents))
for i, p := range cfg.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", cfg.InheritsEnv)
fmt.Fprintf(&b, "inherits_secrets: %t\n", cfg.InheritsSecrets)
if len(cfg.Constraints) > 0 {
quoted := make([]string, len(cfg.Constraints))
for i, c := range cfg.Constraints {
quoted[i] = fmt.Sprintf("%q", c)
}
b.WriteString("constraints: [")
b.WriteString(strings.Join(quoted, ", "))
b.WriteString("]\n")
}
b.WriteString("---\n")
b.WriteString(body)
return b.String()
}
// 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
@@ -344,10 +484,14 @@ func init() {
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)")
nsInheritCmd.Flags().StringVar(&nsInheritParent, "parent", "", "parent namespace to inherit from (required)")
nsCmd.AddCommand(nsListCmd)
nsCmd.AddCommand(nsCreateCmd)
nsCmd.AddCommand(nsDeleteCmd)
nsCmd.AddCommand(nsInspectCmd)
nsCmd.AddCommand(nsValidateCmd)
nsCmd.AddCommand(nsInheritCmd)
nsCmd.AddCommand(nsSetConstraintCmd)
rootCmd.AddCommand(nsCmd)
}
+191 -1
View File
@@ -17,6 +17,7 @@ func resetNSFlags() {
nsCreateParent = ""
nsCreateInheritsEnv = true
nsCreateInheritsSecret = true
nsInheritParent = ""
}
func writeDefaultsNS(t *testing.T, root string) {
@@ -384,7 +385,7 @@ func TestNSRootRegistered(t *testing.T) {
}
}
}
for _, want := range []string{"list", "create <name>", "delete <name>", "inspect <name>", "validate <name>"} {
for _, want := range []string{"list", "create <name>", "delete <name>", "inspect <name>", "validate <name>", "inherit <name>", "set-constraint <name> <key>=<value>"} {
if !sub[want] {
t.Errorf("missing ns subcommand %q", want)
}
@@ -405,3 +406,192 @@ func TestNSListNoORCAHOME(t *testing.T) {
}
var _ = paths.DefaultNamespace // keep paths import alive
func TestNSInheritSetsParent(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", "inherit", "prod", "--parent", "_defaults"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("ns inherit: %v", err)
}
data, err := os.ReadFile(filepath.Join(root, "prod", "ns.md"))
if err != nil {
t.Fatalf("read ns.md: %v", err)
}
if !strings.Contains(string(data), "parents:") || !strings.Contains(string(data), "_defaults") {
t.Errorf("ns.md missing parents: %s", string(data))
}
}
func TestNSInheritCycleRefused(t *testing.T) {
root := t.TempDir()
t.Setenv("ORCA_HOME", root)
resetRootFlags(t)
resetNSFlags()
writeDefaultsNS(t, root)
// a -> b already; now set a's parent to b, then try b -> a.
writeCustomNS(t, root, "a", `["b"]`)
writeCustomNS(t, root, "b", "")
resetNSFlags()
rootCmd.SetArgs([]string{"ns", "inherit", "b", "--parent", "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 TestNSInheritSelfRefused(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", "inherit", "prod", "--parent", "prod"})
err := rootCmd.Execute()
if err == nil {
t.Fatal("expected self-inherit error, got nil")
}
}
func TestNSInheritDefaultsRefused(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", "inherit", "_defaults", "--parent", "prod"})
err := rootCmd.Execute()
if err == nil {
t.Fatal("expected error setting parent on _defaults, got nil")
}
}
func TestNSSetConstraint(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", "set-constraint", "prod", "max-allocs=10"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("ns set-constraint: %v", err)
}
data, err := os.ReadFile(filepath.Join(root, "prod", "ns.md"))
if err != nil {
t.Fatalf("read ns.md: %v", err)
}
if !strings.Contains(string(data), "constraints:") || !strings.Contains(string(data), "max-allocs=10") {
t.Errorf("ns.md missing constraints: %s", string(data))
}
}
func TestNSSetConstraintValidatePasses(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", "set-constraint", "prod", "max-allocs=10"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("ns set-constraint: %v", err)
}
resetNSFlags()
rootCmd.SetArgs([]string{"ns", "validate", "prod"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("ns validate after set-constraint: %v", err)
}
}
func TestNSSetConstraintInspectShowsConstraint(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", "set-constraint", "prod", "max-allocs=10"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("ns set-constraint: %v", err)
}
resetNSFlags()
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())
}
cons, _ := result["constraints"].([]any)
found := false
for _, c := range cons {
if c == "max-allocs=10" {
found = true
}
}
if !found {
t.Errorf("constraint max-allocs=10 not in inspect output: %v", cons)
}
}
func TestNSSetConstraintInvalidFormat(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", "set-constraint", "prod", "noequals"})
err := rootCmd.Execute()
if err == nil {
t.Fatal("expected error for malformed constraint, got nil")
}
if !strings.Contains(err.Error(), "<key>=<value>") {
t.Errorf("error = %q, want contains '<key>=<value>'", err.Error())
}
}
func TestNSSetConstraintDuplicate(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", "set-constraint", "prod", "max-allocs=10"})
if err := rootCmd.Execute(); err != nil {
t.Fatalf("ns set-constraint first: %v", err)
}
resetNSFlags()
rootCmd.SetArgs([]string{"ns", "set-constraint", "prod", "max-allocs=10"})
err := rootCmd.Execute()
if err == nil {
t.Fatal("expected duplicate error, got nil")
}
if !strings.Contains(err.Error(), "already set") {
t.Errorf("error = %q, want contains 'already set'", err.Error())
}
}
+56
View File
@@ -21,6 +21,7 @@ import (
// parents: ["a", "b"] (optional; default empty)
// inherits_env: true (optional; default true)
// inherits_secrets: true (optional; default true)
// constraints: ["k=v"] (optional; parsed into cfg.Constraints)
// quota: {...} (optional; parsed but not surfaced here)
// acl: {...} (optional; parsed but not surfaced here)
//
@@ -55,6 +56,55 @@ func ParseNSMd(path string) (*NSConfig, error) {
return cfg, nil
}
// ParseNSMdWithBody is like ParseNSMd but also returns the markdown
// body (the content after the closing `---` delimiter). Used by the
// ns inherit / set-constraint editors that rewrite frontmatter while
// preserving the body.
func ParseNSMdWithBody(path string) (*NSConfig, string, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, "", fmt.Errorf("read %s: %w", path, err)
}
content := string(data)
block, ok := extractFrontmatter(content)
if !ok {
return nil, "", fmt.Errorf("parse %s: missing frontmatter", path)
}
if strings.TrimSpace(block) == "" {
return nil, "", fmt.Errorf("parse %s: missing frontmatter", path)
}
cfg, err := parseNSFrontmatter(block, path)
if err != nil {
return nil, "", err
}
if cfg.Name == "" {
return nil, "", fmt.Errorf("parse %s: missing name", path)
}
body := bodyAfterFrontmatter(content)
return cfg, body, nil
}
// bodyAfterFrontmatter returns the content after the closing frontmatter
// delimiter of an ns.md file. If no frontmatter is present, the whole
// input is returned unchanged.
func bodyAfterFrontmatter(content string) string {
trimmed := strings.TrimLeft(content, "\r\n\t ")
if !strings.HasPrefix(trimmed, "---") {
return content
}
rest := trimmed[3:]
rest = strings.TrimLeft(rest, "\r\n")
idx := strings.Index(rest, "\n---")
if idx < 0 {
return content
}
after := rest[idx+4:]
after = strings.TrimLeft(after, "\r\n")
return after
}
// extractFrontmatter returns the YAML block between the first pair of
// `---` delimiters and whether a frontmatter block was present.
func extractFrontmatter(content string) (string, bool) {
@@ -110,6 +160,12 @@ func parseNSFrontmatter(block, path string) (*NSConfig, error) {
cfg.InheritsEnv = parseBool(val)
case "inherits_secrets":
cfg.InheritsSecrets = parseBool(val)
case "constraints":
cons, err := parseStringArray(val)
if err != nil {
return nil, fmt.Errorf("parse %s: line %d: constraints: %w", path, lineNo+1, err)
}
cfg.Constraints = cons
case "quota", "acl":
// Reserved nested-mapping keys; recognized, contents ignored.
default: