diff --git a/cli/cmd_claim.go b/cli/cmd_claim.go deleted file mode 100644 index e11a37db..00000000 --- a/cli/cmd_claim.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "fmt" - "strings" -) - -func claimCommands() []Command { - return []Command{ - {Path: []string{"claim"}, Tier: TierWrite, - Summary: "claim a shared infra resource on the presence board", - Run: runClaim}, - {Path: []string{"release"}, Tier: TierWrite, - Summary: "release a presence claim", - Run: runRelease}, - } -} - -// runClaim parses `: --purpose "..."` in either order (the presence -// script takes the label first, so we can't rely on Go's flag package which -// stops at the first positional). -func runClaim(args []string) error { - var label, purpose string - for i := 0; i < len(args); i++ { - a := args[i] - switch { - case a == "--purpose" || a == "-purpose": - if i+1 < len(args) { - purpose = args[i+1] - i++ - } - case strings.HasPrefix(a, "--purpose="): - purpose = strings.TrimPrefix(a, "--purpose=") - case !strings.HasPrefix(a, "-") && label == "": - label = a - } - } - if label == "" { - return fmt.Errorf(`usage: homelab claim : --purpose "what + why"`) - } - return presenceClaim(label, purpose) -} - -func runRelease(args []string) error { - var label string - for _, a := range args { - if !strings.HasPrefix(a, "-") { - label = a - break - } - } - if label == "" { - return fmt.Errorf("usage: homelab release :") - } - return presenceRelease(label) -} diff --git a/cli/cmd_tf.go b/cli/cmd_tf.go deleted file mode 100644 index 95e0260b..00000000 --- a/cli/cmd_tf.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "path/filepath" - "strings" - "sync" - "syscall" -) - -func tfCommands() []Command { - return []Command{ - {Path: []string{"tf", "plan"}, Tier: TierRead, - Summary: "terragrunt plan a stack (via scripts/tg)", Run: tfPassthrough("plan")}, - {Path: []string{"tf", "validate"}, Tier: TierRead, - Summary: "terragrunt validate a stack", Run: tfPassthrough("validate")}, - {Path: []string{"tf", "fmt"}, Tier: TierRead, - Summary: "terraform fmt a stack's files", Run: tfFmt}, - {Path: []string{"tf", "force-unlock"}, Tier: TierWrite, - Summary: "release a stuck terraform state lock (needs )", Run: tfForceUnlock}, - {Path: []string{"tf", "apply"}, Tier: TierWrite, - Summary: "terragrunt apply a stack — presence-coupled, out-of-band", Run: tfApply}, - } -} - -// firstPositional returns the first non-flag arg and the remaining args with it removed. -func firstPositional(args []string) (string, []string) { - for i, a := range args { - if !strings.HasPrefix(a, "-") { - rest := append(append([]string{}, args[:i]...), args[i+1:]...) - return a, rest - } - } - return "", args -} - -// resolveTfStack finds the infra root (from cwd) and the stack directory named -// by the first positional arg, returning the remaining args. -func resolveTfStack(args []string) (infraRoot, stackName, stackDir string, rest []string, err error) { - stackName, rest = firstPositional(args) - if stackName == "" { - err = fmt.Errorf("missing argument") - return - } - cwd, e := os.Getwd() - if e != nil { - err = e - return - } - infraRoot, err = findInfraRoot(cwd) - if err != nil { - return - } - stackDir, err = resolveStack(infraRoot, stackName) - return -} - -func tgPath(infraRoot string) string { return filepath.Join(infraRoot, "scripts", "tg") } - -// tfPassthrough runs `scripts/tg [extra]` in the stack directory. -func tfPassthrough(verb string) func([]string) error { - return func(args []string) error { - infraRoot, _, stackDir, rest, err := resolveTfStack(args) - if err != nil { - return err - } - return runStreamingIn(stackDir, tgPath(infraRoot), append([]string{verb}, rest...)...) - } -} - -func tfFmt(args []string) error { - _, _, stackDir, _, err := resolveTfStack(args) - if err != nil { - return err - } - return runStreamingIn(stackDir, "terraform", "fmt", "-recursive", ".") -} - -func tfForceUnlock(args []string) error { - infraRoot, _, stackDir, rest, err := resolveTfStack(args) - if err != nil { - return err - } - if len(rest) < 1 { - return fmt.Errorf("usage: homelab tf force-unlock ") - } - return runStreamingIn(stackDir, tgPath(infraRoot), "force-unlock", "-force", rest[0]) -} - -// tfApply applies a stack out-of-band: claim the stack on the presence board, -// ALWAYS release on exit (normal, error, or signal — fixing the claim leak), -// and warn that CI applies canonically on push. -func tfApply(args []string) error { - infraRoot, stackName, stackDir, _, err := resolveTfStack(args) - if err != nil { - return err - } - label := "stack:" + stackName - fmt.Fprintf(os.Stderr, - "homelab: out-of-band apply of %q — CI applies canonically on push to master.\n", stackName) - - if err := presenceClaim(label, "homelab tf apply "+stackName); err != nil { - return fmt.Errorf("presence claim failed (run `vault login -method=oidc`?): %w", err) - } - // Release exactly once, whether we exit normally, on error, or on signal — - // sync.Once makes the defer and the signal goroutine safe to both call it. - var once sync.Once - release := func() { once.Do(func() { _ = presenceRelease(label) }) } - defer release() - - sig := make(chan os.Signal, 1) - signal.Notify(sig, os.Interrupt, syscall.SIGTERM) - go func() { - <-sig - release() - os.Exit(130) - }() - - return runStreamingIn(stackDir, tgPath(infraRoot), "apply", "--non-interactive") -} diff --git a/cli/cmd_tf_test.go b/cli/cmd_tf_test.go deleted file mode 100644 index 74f5b9bd..00000000 --- a/cli/cmd_tf_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "reflect" - "testing" -) - -func TestFirstPositional(t *testing.T) { - cases := []struct { - args []string - wantName string - wantRest []string - }{ - {[]string{"vault"}, "vault", []string{}}, - {[]string{"--json", "vault"}, "vault", []string{"--json"}}, - {[]string{"vault", "abc-123"}, "vault", []string{"abc-123"}}, - {[]string{"--foo", "monitoring", "extra"}, "monitoring", []string{"--foo", "extra"}}, - {[]string{"--only-flags"}, "", []string{"--only-flags"}}, - } - for _, c := range cases { - gotName, gotRest := firstPositional(c.args) - if gotName != c.wantName || !reflect.DeepEqual(gotRest, c.wantRest) { - t.Errorf("firstPositional(%v) = (%q, %v), want (%q, %v)", - c.args, gotName, gotRest, c.wantName, c.wantRest) - } - } -} diff --git a/cli/command.go b/cli/command.go deleted file mode 100644 index fd7f4812..00000000 --- a/cli/command.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "sort" - "strings" -) - -// Tier classifies whether a command observes (read) or mutates (write) state. -// v0.1 allows everything; the tier is recorded so a classifier hook can gate -// writes later without restructuring (see docs/adr/0005). -type Tier string - -const ( - TierRead Tier = "read" - TierWrite Tier = "write" -) - -// Command is one homelab verb. Path is the token sequence that selects it, -// e.g. ["claim"] or ["tf", "plan"]. Run receives the args after the path. -type Command struct { - Path []string - Tier Tier - Summary string - Run func(args []string) error -} - -// dispatch routes args to the command whose Path is the longest matching prefix -// of args, passing the remaining args to its Run. -func dispatch(reg []Command, args []string) error { - best := -1 - bestLen := 0 - for i, c := range reg { - if len(c.Path) > len(args) { - continue - } - match := true - for j, p := range c.Path { - if args[j] != p { - match = false - break - } - } - if match && len(c.Path) >= bestLen { - best = i - bestLen = len(c.Path) - } - } - if best < 0 { - return fmt.Errorf("unknown command: %q", strings.Join(args, " ")) - } - return reg[best].Run(args[bestLen:]) -} - -// name is the space-joined verb path, e.g. "tf plan". -func (c Command) name() string { return strings.Join(c.Path, " ") } - -// sortedByName returns a copy of reg ordered by verb path for stable output. -func sortedByName(reg []Command) []Command { - out := make([]Command, len(reg)) - copy(out, reg) - sort.Slice(out, func(i, j int) bool { return out[i].name() < out[j].name() }) - return out -} - -// manifestText renders one aligned line per command: " ". -// This is the cheap progressive-discovery entrypoint (see docs/adr/0004). -func manifestText(reg []Command) string { - cmds := sortedByName(reg) - width := 0 - for _, c := range cmds { - if n := len(c.name()); n > width { - width = n - } - } - var b strings.Builder - for _, c := range cmds { - fmt.Fprintf(&b, "%-*s %-5s %s\n", width, c.name(), c.Tier, c.Summary) - } - return b.String() -} - -// manifestJSON renders the registry as a JSON array of {command, tier, summary} -// so agents can parse the full surface in one call. -func manifestJSON(reg []Command) (string, error) { - type entry struct { - Command string `json:"command"` - Tier string `json:"tier"` - Summary string `json:"summary"` - } - entries := make([]entry, 0, len(reg)) - for _, c := range sortedByName(reg) { - entries = append(entries, entry{Command: c.name(), Tier: string(c.Tier), Summary: c.Summary}) - } - b, err := json.MarshalIndent(entries, "", " ") - if err != nil { - return "", err - } - return string(b), nil -} diff --git a/cli/command_test.go b/cli/command_test.go deleted file mode 100644 index e686622d..00000000 --- a/cli/command_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package main - -import ( - "encoding/json" - "reflect" - "strings" - "testing" -) - -// Tracer bullet: the dispatcher must route `homelab ` to the -// command whose Path is the longest matching prefix of the input tokens, and -// hand the command the remaining args. -func TestDispatchRoutesToLongestPrefixMatch(t *testing.T) { - var gotArgs []string - ran := "" - reg := []Command{ - {Path: []string{"claim"}, Tier: TierWrite, Summary: "claim a resource", - Run: func(a []string) error { ran = "claim"; gotArgs = a; return nil }}, - {Path: []string{"tf", "plan"}, Tier: TierRead, Summary: "plan a stack", - Run: func(a []string) error { ran = "tf plan"; gotArgs = a; return nil }}, - } - - if err := dispatch(reg, []string{"tf", "plan", "vault", "--json"}); err != nil { - t.Fatalf("dispatch returned error: %v", err) - } - if ran != "tf plan" { - t.Fatalf("routed to %q, want %q", ran, "tf plan") - } - if want := []string{"vault", "--json"}; !reflect.DeepEqual(gotArgs, want) { - t.Fatalf("command got args %v, want %v", gotArgs, want) - } -} - -func TestDispatchUnknownCommandErrors(t *testing.T) { - reg := []Command{{Path: []string{"claim"}, Run: func(a []string) error { return nil }}} - if err := dispatch(reg, []string{"bogus"}); err == nil { - t.Fatal("expected error for unknown command, got nil") - } -} - -// The manifest is the progressive-discovery entrypoint: one line per command -// showing the full verb path, its tier, and summary, sorted for stable output. -func TestManifestTextListsEveryCommandWithTier(t *testing.T) { - reg := []Command{ - {Path: []string{"tf", "plan"}, Tier: TierRead, Summary: "plan a stack"}, - {Path: []string{"claim"}, Tier: TierWrite, Summary: "claim a resource"}, - } - out := manifestText(reg) - for _, want := range []string{"claim", "tf plan", "read", "write", "plan a stack", "claim a resource"} { - if !strings.Contains(out, want) { - t.Errorf("manifest text missing %q\n---\n%s", want, out) - } - } - // sorted: claim (c) must appear before tf plan (t) - if strings.Index(out, "claim") > strings.Index(out, "tf plan") { - t.Errorf("manifest not sorted by path:\n%s", out) - } -} - -func TestManifestJSONIsParsableAndTagged(t *testing.T) { - reg := []Command{{Path: []string{"tf", "apply"}, Tier: TierWrite, Summary: "apply a stack"}} - out, err := manifestJSON(reg) - if err != nil { - t.Fatalf("manifestJSON error: %v", err) - } - var got []map[string]string - if err := json.Unmarshal([]byte(out), &got); err != nil { - t.Fatalf("manifest JSON not parsable: %v\n%s", err, out) - } - if len(got) != 1 || got[0]["command"] != "tf apply" || got[0]["tier"] != "write" { - t.Fatalf("unexpected manifest JSON: %v", got) - } -} diff --git a/cli/homelab.go b/cli/homelab.go deleted file mode 100644 index f3ad5f4b..00000000 --- a/cli/homelab.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "fmt" - "strings" -) - -// version is stamped at build time via -ldflags "-X main.version=vX.Y.Z". -var version = "dev" - -// buildRegistry returns every homelab verb. New verb-groups append here. -func buildRegistry() []Command { - var reg []Command - reg = append(reg, claimCommands()...) - reg = append(reg, tfCommands()...) - return reg -} - -// dispatchTop handles the homelab verb surface. handled=false means the args are -// not a homelab verb, so main() falls back to the legacy -use-case path. -func dispatchTop(args []string) (handled bool, err error) { - if len(args) == 0 { - fmt.Print(usage()) - return true, nil - } - switch args[0] { - case "help", "-h", "--help": - fmt.Print(usage()) - return true, nil - case "version", "--version": - fmt.Println("homelab " + version) - return true, nil - case "manifest": - reg := buildRegistry() - if containsArg(args[1:], "--json") { - out, err := manifestJSON(reg) - if err != nil { - return true, err - } - fmt.Println(out) - return true, nil - } - fmt.Print(manifestText(reg)) - return true, nil - } - if strings.HasPrefix(args[0], "-") { - return false, nil - } - reg := buildRegistry() - if !isCommandGroup(reg, args[0]) { - return false, nil - } - return true, dispatch(reg, args) -} - -func isCommandGroup(reg []Command, group string) bool { - for _, c := range reg { - if len(c.Path) > 0 && c.Path[0] == group { - return true - } - } - return false -} - -func containsArg(args []string, want string) bool { - for _, a := range args { - if a == want { - return true - } - } - return false -} - -func usage() string { - var b strings.Builder - fmt.Fprintf(&b, "homelab %s — unified homelab operations CLI\n\n", version) - b.WriteString("Usage:\n homelab [args]\n\nCommands:\n") - for _, line := range strings.Split(strings.TrimRight(manifestText(buildRegistry()), "\n"), "\n") { - if line != "" { - b.WriteString(" " + line + "\n") - } - } - b.WriteString("\n manifest [--json] list all commands (machine-readable with --json)\n") - b.WriteString(" version print version\n") - b.WriteString("\nLegacy webhook use-cases remain available via -use-case=.\n") - return b.String() -} diff --git a/cli/main.go b/cli/main.go index a53f7672..3b9fee1c 100644 --- a/cli/main.go +++ b/cli/main.go @@ -26,16 +26,8 @@ var ( ) func main() { - // homelab verb surface (work/tf/claim/...) is tried first; if the args are - // not a homelab verb, fall through to the legacy webhook -use-case path. - if handled, err := dispatchTop(os.Args[1:]); handled { - if err != nil { - fmt.Fprintln(os.Stderr, "homelab: "+err.Error()) - os.Exit(1) - } - return - } - if err := run(); err != nil { + err := run() + if err != nil { glog.Errorf("run failed: %s", err.Error()) os.Exit(255) } diff --git a/cli/presence.go b/cli/presence.go deleted file mode 100644 index bcf054d7..00000000 --- a/cli/presence.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "strings" -) - -// validPresenceKinds is the fixed label taxonomy accepted by the presence board. -var validPresenceKinds = []string{"node", "host", "stack", "service", "db", "pvc", "infra"} - -// presenceScript locates the presence CLI — homelab WRAPS it, it does not -// reimplement it. Override with HOMELAB_PRESENCE; defaults to ~/code/scripts/presence. -func presenceScript() string { - if p := os.Getenv("HOMELAB_PRESENCE"); p != "" { - return p - } - home, err := os.UserHomeDir() - if err != nil { - return "presence" - } - return filepath.Join(home, "code", "scripts", "presence") -} - -// validateLabel checks a presence label is : with a known kind. -func validateLabel(label string) error { - parts := strings.SplitN(label, ":", 2) - if len(parts) != 2 || parts[0] == "" || parts[1] == "" { - return fmt.Errorf("label must be : (e.g. stack:vault), got %q", label) - } - for _, k := range validPresenceKinds { - if parts[0] == k { - return nil - } - } - return fmt.Errorf("invalid label kind %q; valid kinds: %s", parts[0], strings.Join(validPresenceKinds, ", ")) -} - -// presenceClaim claims label on the board with a purpose note. -func presenceClaim(label, purpose string) error { - if err := validateLabel(label); err != nil { - return err - } - args := []string{"claim", label} - if purpose != "" { - args = append(args, "--purpose", purpose) - } - return runStreaming(presenceScript(), args...) -} - -// presenceRelease releases a prior claim on label. -func presenceRelease(label string) error { - if err := validateLabel(label); err != nil { - return err - } - return runStreaming(presenceScript(), "release", label) -} diff --git a/cli/presence_test.go b/cli/presence_test.go deleted file mode 100644 index 3d1596e1..00000000 --- a/cli/presence_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -import "testing" - -func TestValidateLabelAcceptsTaxonomy(t *testing.T) { - good := []string{ - "stack:vault", "service:health", "node:k8s-node1", "db:pg-cluster", - "infra:gpu-operator", "host:proxmox-1", "pvc:dbaas/data", - } - for _, l := range good { - if err := validateLabel(l); err != nil { - t.Errorf("validateLabel(%q) = %v, want nil", l, err) - } - } -} - -func TestValidateLabelRejectsBadLabels(t *testing.T) { - bad := []string{"vault", "stack:", "bogus:x", ":x", "stack", ""} - for _, l := range bad { - if err := validateLabel(l); err == nil { - t.Errorf("validateLabel(%q) = nil, want error", l) - } - } -} diff --git a/cli/repo.go b/cli/repo.go deleted file mode 100644 index ff65e8a4..00000000 --- a/cli/repo.go +++ /dev/null @@ -1,63 +0,0 @@ -package main - -import ( - "os/exec" - "strings" -) - -// preferRemote picks the canonical remote: forgejo if present, else origin, -// else the first listed. (For infra, origin and forgejo both point at Forgejo.) -func preferRemote(remotes []string) string { - has := map[string]bool{} - for _, r := range remotes { - has[r] = true - } - switch { - case has["forgejo"]: - return "forgejo" - case has["origin"]: - return "origin" - case len(remotes) > 0: - return remotes[0] - default: - return "" - } -} - -// hasGitCryptAttr reports whether .gitattributes content enables git-crypt. -func hasGitCryptAttr(gitattributes string) bool { - return strings.Contains(gitattributes, "filter=git-crypt") -} - -// gitCryptFlags are the per-command flags that disable smudge/clean so git -// operations in a git-crypt repo don't try to decrypt (NEVER persisted to config). -func gitCryptFlags() []string { - return []string{ - "-c", "filter.git-crypt.smudge=cat", - "-c", "filter.git-crypt.clean=cat", - "-c", "filter.git-crypt.required=false", - } -} - -// gitOutput runs `git -C dir ` and returns trimmed stdout. -func gitOutput(dir string, args ...string) (string, error) { - cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) - out, err := cmd.Output() - return strings.TrimSpace(string(out)), err -} - -func gitRepoRoot(dir string) (string, error) { - return gitOutput(dir, "rev-parse", "--show-toplevel") -} - -// gitRemotes lists configured remote names for the repo at dir. -func gitRemotes(dir string) ([]string, error) { - out, err := gitOutput(dir, "remote") - if err != nil { - return nil, err - } - if out == "" { - return nil, nil - } - return strings.Split(out, "\n"), nil -} diff --git a/cli/repo_test.go b/cli/repo_test.go deleted file mode 100644 index 76cf21a7..00000000 --- a/cli/repo_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import "testing" - -func TestPreferRemote(t *testing.T) { - cases := []struct { - in []string - want string - }{ - {[]string{"origin", "forgejo"}, "forgejo"}, - {[]string{"forgejo"}, "forgejo"}, - {[]string{"origin"}, "origin"}, - {[]string{"upstream"}, "upstream"}, - {nil, ""}, - } - for _, c := range cases { - if got := preferRemote(c.in); got != c.want { - t.Errorf("preferRemote(%v) = %q, want %q", c.in, got, c.want) - } - } -} - -func TestHasGitCryptAttr(t *testing.T) { - if !hasGitCryptAttr("*.tfvars filter=git-crypt diff=git-crypt") { - t.Error("expected git-crypt detected") - } - if hasGitCryptAttr("*.md text\n*.png binary") { - t.Error("expected no git-crypt") - } -} - -func TestGitCryptFlagsShape(t *testing.T) { - f := gitCryptFlags() - if len(f) != 6 || f[0] != "-c" || f[1] != "filter.git-crypt.smudge=cat" { - t.Fatalf("unexpected git-crypt flags: %v", f) - } -} diff --git a/cli/run.go b/cli/run.go deleted file mode 100644 index 22e7f17a..00000000 --- a/cli/run.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "os" - "os/exec" -) - -// runStreaming executes name with args, wiring std streams to this process so -// the caller sees live output, and returns the command's error (non-nil on -// non-zero exit — preserved so homelab's own exit code reflects the child's). -func runStreaming(name string, args ...string) error { - return runStreamingIn("", name, args...) -} - -// runStreamingIn is runStreaming with a working directory (empty = inherit). -func runStreamingIn(dir, name string, args ...string) error { - cmd := exec.Command(name, args...) - cmd.Dir = dir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Stdin = os.Stdin - return cmd.Run() -} diff --git a/cli/stack.go b/cli/stack.go deleted file mode 100644 index 1cfdd8d0..00000000 --- a/cli/stack.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" -) - -// findInfraRoot walks up from start to the infra repo root — the directory -// holding both terragrunt.hcl and a stacks/ directory. -func findInfraRoot(start string) (string, error) { - dir := start - for { - if isFile(filepath.Join(dir, "terragrunt.hcl")) && isDir(filepath.Join(dir, "stacks")) { - return dir, nil - } - parent := filepath.Dir(dir) - if parent == dir { - return "", fmt.Errorf("not inside an infra checkout (no terragrunt.hcl + stacks/ found above %s)", start) - } - dir = parent - } -} - -// resolveStack maps a bare stack name to its directory under /stacks. -func resolveStack(infraRoot, name string) (string, error) { - dir := filepath.Join(infraRoot, "stacks", name) - if isDir(dir) { - return dir, nil - } - avail := listStacks(infraRoot) - return "", fmt.Errorf("stack %q not found under stacks/; available: %s", name, strings.Join(avail, ", ")) -} - -// listStacks returns the sorted names of every directory under /stacks. -func listStacks(infraRoot string) []string { - entries, err := os.ReadDir(filepath.Join(infraRoot, "stacks")) - if err != nil { - return nil - } - var out []string - for _, e := range entries { - if e.IsDir() { - out = append(out, e.Name()) - } - } - sort.Strings(out) - return out -} - -func isFile(p string) bool { fi, err := os.Stat(p); return err == nil && !fi.IsDir() } -func isDir(p string) bool { fi, err := os.Stat(p); return err == nil && fi.IsDir() } diff --git a/cli/stack_test.go b/cli/stack_test.go deleted file mode 100644 index 2967dc18..00000000 --- a/cli/stack_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "testing" -) - -func newInfraTree(t *testing.T, stacks ...string) string { - t.Helper() - root := t.TempDir() - if err := os.WriteFile(filepath.Join(root, "terragrunt.hcl"), []byte("# root"), 0o644); err != nil { - t.Fatal(err) - } - for _, s := range stacks { - if err := os.MkdirAll(filepath.Join(root, "stacks", s), 0o755); err != nil { - t.Fatal(err) - } - } - return root -} - -func TestFindInfraRootWalksUp(t *testing.T) { - root := newInfraTree(t, "vault") - got, err := findInfraRoot(filepath.Join(root, "stacks", "vault")) - if err != nil { - t.Fatalf("findInfraRoot error: %v", err) - } - if got != root { - t.Fatalf("findInfraRoot = %q, want %q", got, root) - } -} - -func TestFindInfraRootErrorsOutsideInfra(t *testing.T) { - if _, err := findInfraRoot(t.TempDir()); err == nil { - t.Fatal("expected error outside an infra checkout") - } -} - -func TestResolveStack(t *testing.T) { - root := newInfraTree(t, "vault", "monitoring") - dir, err := resolveStack(root, "vault") - if err != nil { - t.Fatalf("resolveStack error: %v", err) - } - if want := filepath.Join(root, "stacks", "vault"); dir != want { - t.Fatalf("resolveStack = %q, want %q", dir, want) - } - if _, err := resolveStack(root, "nonesuch"); err == nil { - t.Fatal("expected error for unknown stack") - } -} diff --git a/cli/update_viktorbarzin_me.go b/cli/update_viktorbarzin_me.go index c2c1d3f4..1a693a25 100644 --- a/cli/update_viktorbarzin_me.go +++ b/cli/update_viktorbarzin_me.go @@ -103,6 +103,6 @@ func notifyForIPChange(oldIP, newIP net.IP) error { if err != nil { return errors.Wrapf(err, "Error reading response") } - glog.Infof("Response: %s", string(responseBody)) + glog.Infof("Response:", string(responseBody)) return nil }