From af0383108b93b1d79d9eeaf016ab626f384359d1 Mon Sep 17 00:00:00 2001 From: MermaidMan Date: Fri, 22 May 2026 23:56:56 +0000 Subject: [PATCH 1/3] Add check-go guard with friendly message for missing Go toolchain --- Makefile | 60 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index bfe215d..6fa8494 100644 --- a/Makefile +++ b/Makefile @@ -20,32 +20,60 @@ GOVET := $(GOCMD) vet # Cross-compilation targets TARGETS := linux-amd64 linux-arm64 darwin-arm64 windows-amd64 windows-arm64 -# Platform detection -GOOS := $(shell go env GOOS) -GOARCH := $(shell go env GOARCH) +# Platform detection (noiseless fallback — true check happens in check-go target) +GOOS := $(shell go env GOOS 2>/dev/null || echo "unknown") +GOARCH := $(shell go env GOARCH 2>/dev/null || echo "unknown") + +# ────────────────────────────────────────────── +# Friendly Go-not-found guard +check-go: + @if ! command -v go >/dev/null 2>&1; then \ + echo ""; \ + echo " ╭──────────────────────────────────────────────╮"; \ + echo " │ ⚠ Go is not installed on this machine │"; \ + echo " ├──────────────────────────────────────────────┤"; \ + echo " │ │"; \ + echo " │ obm is a Go project. You need the Go │"; \ + echo " │ toolchain to build from source. │"; \ + echo " │ │"; \ + echo " │ Install Go: │"; \ + echo " │ Linux: your package manager │"; \ + echo " │ or https://go.dev/dl/ │"; \ + echo " │ │"; \ + echo " │ macOS: brew install go │"; \ + echo " │ │"; \ + echo " │ Windows: https://go.dev/dl/ │"; \ + echo " │ │"; \ + echo " │ Or use the all-in-one script: │"; \ + echo " │ ./scripts/install.sh │"; \ + echo " │ │"; \ + echo " ╰──────────────────────────────────────────────╯"; \ + echo ""; \ + exit 1; \ + fi all: lint test build -build: +build: check-go @echo "Building $(BINARY) v$(VERSION) for $(GOOS)/$(GOARCH)..." $(GOBUILD) $(LDFLAGS) -o $(BINARY) ./cmd/obm -test: +test: check-go @echo "Running tests..." $(GOTEST) -v -race -coverprofile=coverage.out ./... lint: vet fmt @echo "✓ Lint pass" -vet: +vet: check-go @echo "Running go vet..." $(GOVET) ./... -fmt: +fmt: check-go @echo "Formatting code..." gofmt -l -s -w . -clean: +clean: check-go @echo "Cleaning..." rm -f $(BINARY) rm -f coverage.out @@ -67,7 +95,7 @@ version: @echo "$(VERSION)" # Cross-compilation -cross-compile: +cross-compile: check-go @echo "Cross-compiling for all platforms..." @mkdir -p bin @for target in $(TARGETS); do \ @@ -84,23 +112,23 @@ cross-compile: @echo "✓ Cross-compilation complete" # Build specific platform -build-linux-amd64: +build-linux-amd64: check-go @mkdir -p bin GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/linux-amd64/$(BINARY) ./cmd/obm -build-linux-arm64: +build-linux-arm64: check-go @mkdir -p bin GOOS=linux GOARCH=arm64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/linux-arm64/$(BINARY) ./cmd/obm -build-darwin-arm64: +build-darwin-arm64: check-go @mkdir -p bin GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/darwin-arm64/$(BINARY) ./cmd/obm -build-windows-amd64: +build-windows-amd64: check-go @mkdir -p bin GOOS=windows GOARCH=amd64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/windows-amd64/$(BINARY).exe ./cmd/obm -build-windows-arm64: +build-windows-arm64: check-go @mkdir -p bin GOOS=windows GOARCH=arm64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/windows-arm64/$(BINARY).exe ./cmd/obm @@ -140,10 +168,10 @@ dev: build test @echo "✓ Development build complete" # Quick test during development -quick-test: +quick-test: check-go $(GOTEST) -race ./... # Coverage report -coverage: test +coverage: check-go test $(GOCMD) tool cover -html=coverage.out -o coverage.html @echo "Coverage report: coverage.html" \ No newline at end of file From 119dbe14c489eb43a4bf72d906e1c449af01daf7 Mon Sep 17 00:00:00 2001 From: Jezza Hehn Date: Fri, 22 May 2026 20:51:28 -0400 Subject: [PATCH 2/3] Tweaking golang warning text --- Makefile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 6fa8494..f817798 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,11 @@ check-go: @if ! command -v go >/dev/null 2>&1; then \ echo ""; \ echo " ╭──────────────────────────────────────────────╮"; \ - echo " │ ⚠ Go is not installed on this machine │"; \ + echo " │ ⚠ Go is not installed on this machine! │"; \ echo " ├──────────────────────────────────────────────┤"; \ echo " │ │"; \ - echo " │ obm is a Go project. You need the Go │"; \ - echo " │ toolchain to build from source. │"; \ + echo " │ This is a Go project. You need the │"; \ + echo " │ Go language toolchain to build from source. │"; \ echo " │ │"; \ echo " │ Install Go: │"; \ echo " │ Linux: your package manager │"; \ @@ -44,7 +44,7 @@ check-go: echo " │ │"; \ echo " │ Windows: https://go.dev/dl/ │"; \ echo " │ │"; \ - echo " │ Or use the all-in-one script: │"; \ + echo " │ Or use the obm full install script: │"; \ echo " │ ./scripts/install.sh │"; \ echo " │ │"; \ echo " ╰──────────────────────────────────────────────╯"; \ From ab1de96168feda7273c0db9fe36cb9fa60669bff Mon Sep 17 00:00:00 2001 From: MermaidMan Date: Thu, 4 Jun 2026 17:46:40 +0000 Subject: [PATCH 3/3] migrate to OpenTofu with Terraform fallback Add binary lookup in both terraform.go and destroy.go: tofu preferred, terraform fallback. Update all docs to reflect the OpenTofu-first approach. --- DETAILS.md | 22 +++++++++++----------- LLMs.md | 4 ++-- README.md | 4 ++-- internal/destroy/destroy.go | 14 ++++++++++++-- internal/terraform/terraform.go | 30 +++++++++++++++++++++--------- 5 files changed, 48 insertions(+), 26 deletions(-) diff --git a/DETAILS.md b/DETAILS.md index 1a51171..90739e4 100644 --- a/DETAILS.md +++ b/DETAILS.md @@ -53,7 +53,7 @@ obm/ │ │ │ └── hetzner_test.go │ │ └── provider_test.go │ ├── terraform/ -│ │ └── terraform.go # Runner: Init, Plan, Apply, Destroy wrappers +│ │ └── terraform.go # Runner: Init, Plan, Apply, Destroy (OpenTofu/Terraform) │ └── validation/ │ ├── validation.go # Check interface, Runner, CheckResult, Status enum │ └── validation_test.go @@ -82,7 +82,7 @@ obm/ | `obm deploy` | `--config ` | Interactive walkthrough (default) or non-interactive from YAML | | `obm validate` | `--env-file ` | Load `.env`, check required vars, validate API keys | | `obm status` | — | Show deployment state (not yet implemented) | -| `obm destroy` | — | Confirmation prompt → `terraform destroy` → state cleanup | +| `obm destroy` | — | Confirmation prompt → `tofu destroy` → state cleanup | | `obm version` | — | Print version with commit hash and build time | | `obm help` | — | Print usage | @@ -111,7 +111,7 @@ The `obm deploy` interactive flow runs 8 steps in sequence: 7. **Tailscale** — Optional VPN setup. Auth key and tailnet domain. 8. **Discord** — Optional bot integration. Bot token, server ID, user IDs. -Final step: summary display with cost estimate → confirm → write `.env` → optionally run `terraform init && terraform apply`. +Final step: summary display with cost estimate → confirm → write `.env` → optionally run `tofu init && tofu apply` (or `terraform` if installed). ### Framework-specific defaults @@ -355,15 +355,15 @@ DigitalOcean prices: --- -## Terraform Integration +## Terraform / OpenTofu Integration -`obm` generates the `.env` file that Terraform expects. The actual Terraform configs live in the separate [openboatmobile-ai](https://github.com/openboatmobile/openboatmobile-ai) repo. +`obm` generates the `.env` file that OpenTofu (or Terraform) expects. The actual infrastructure configs live in the separate [openboatmobile-ai](https://github.com/openboatmobile/openboatmobile-ai) repo. -The `internal/terraform/terraform.go` wrapper provides: -- `Runner.Init()` — `terraform init -input=false` -- `Runner.Plan(destroy bool)` — `terraform plan` (with optional `-destroy` flag) -- `Runner.Apply()` — `terraform apply -auto-approve` -- `Runner.Destroy()` — `terraform destroy -auto-approve` +The `internal/terraform/terraform.go` wrapper provides (auto-detects OpenTofu first, Terraform fallback): +- `Runner.Init()` — `tofu init -input=false` +- `Runner.Plan(destroy bool)` — `tofu plan` (with optional `-destroy` flag) +- `Runner.Apply()` — `tofu apply -auto-approve` +- `Runner.Destroy()` — `tofu destroy -auto-approve` All commands run in the `WorkDir` and capture combined output. @@ -391,7 +391,7 @@ User sets `TF_VAR_*` env vars → sourced from `.env` → Terraform reads them ## Destroy Flow -`obm destroy` reads `terraform.tfstate` to list resources, shows them, asks for confirmation, runs `terraform destroy`, then cleans up state files unless `--keep-state` is set. +`obm destroy` reads `terraform.tfstate` to list resources, shows them, asks for confirmation, runs `tofu destroy`, then cleans up state files unless `--keep-state` is set. --- diff --git a/LLMs.md b/LLMs.md index 8b9365d..4c85d01 100644 --- a/LLMs.md +++ b/LLMs.md @@ -85,7 +85,7 @@ When your human runs `obm deploy`, they'll go through these in order: 7. **Tailscale** — Optional VPN (auth key + tailnet) 8. **Discord** — Optional bot integration (token, server ID, user IDs) -After all 8 steps: summary + cost estimate → confirm → `.env` written → optional `terraform init && apply`. +After all 8 steps: summary + cost estimate → confirm → `.env` written → optional `tofu init && tofu apply` (or `terraform` if installed). --- @@ -200,7 +200,7 @@ This loads `.env`, checks required variables, and validates inference API keys a obm destroy ``` -It asks for confirmation. It reads `terraform.tfstate` to show what will be destroyed. +It asks for confirmation. It reads the state file (`terraform.tfstate`) to show what will be destroyed. ### "Set up CI/CD deployment" diff --git a/README.md b/README.md index ba3c2f4..edea6c4 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,9 @@ The AI model API costs are separate, and depend on your usage. Generally that's ## What happens under the hood? -`obm` generates a `.env` file that [Terraform](https://terraform.io) reads to provision your server, install the agent software, and configure everything. You don't need to know Terraform — `obm` handles it. +`obm` generates a `.env` file that [OpenTofu](https://opentofu.org) (or [Terraform](https://terraform.io) as a fallback) reads to provision your server, install the agent software, and configure everything. You don't need to know either — `obm` handles it. -The Terraform configs live in the [openboatmobile-ai](https://github.com/openboatmobile/openboatmobile-ai) repo. `obm` is the friendly CLI wrapper around them. +The infrastructure configs live in the [openboatmobile-ai](https://github.com/openboatmobile/openboatmobile-ai) repo. `obm` is the friendly CLI wrapper around them. --- diff --git a/internal/destroy/destroy.go b/internal/destroy/destroy.go index a2b5222..8ece268 100644 --- a/internal/destroy/destroy.go +++ b/internal/destroy/destroy.go @@ -12,6 +12,16 @@ import ( "github.com/openboatmobile/obm/internal/prompt" ) +// tfBinary returns the path to the IaC binary: tofu preferred, terraform fallback. +func tfBinary() string { + for _, name := range []string{"tofu", "terraform"} { + if path, err := exec.LookPath(name); err == nil { + return path + } + } + return "tofu" +} + // Options configures the destroy operation. type Options struct { WorkDir string // Working directory (default: current) @@ -198,7 +208,7 @@ func displayDestroyPlan(resources []Resource) { fmt.Println() } -// runTerraformDestroy executes terraform destroy. +// runTerraformDestroy executes tofu/terraform destroy. func runTerraformDestroy(workDir string, opts *Options) error { args := []string{"destroy", "-auto-approve"} @@ -207,7 +217,7 @@ func runTerraformDestroy(workDir string, opts *Options) error { args = append(args, "-var-file", vf) } - cmd := exec.Command("terraform", args...) + cmd := exec.Command(tfBinary(), args...) cmd.Dir = workDir // Stream output to stdout/stderr diff --git a/internal/terraform/terraform.go b/internal/terraform/terraform.go index fa55b91..e4a8580 100644 --- a/internal/terraform/terraform.go +++ b/internal/terraform/terraform.go @@ -1,4 +1,5 @@ -// Package terraform wraps Terraform operations (init, plan, apply, destroy). +// Package terraform wraps IaC operations (init, plan, apply, destroy). +// Supports OpenTofu (preferred) and Terraform (fallback). package terraform import ( @@ -6,22 +7,32 @@ import ( "os/exec" ) -// Runner executes Terraform commands. +// binary returns the path to the IaC binary: tofu preferred, terraform fallback. +func binary() string { + for _, name := range []string{"tofu", "terraform"} { + if path, err := exec.LookPath(name); err == nil { + return path + } + } + return "tofu" // fallback — will produce a useful error at runtime +} + +// Runner executes infrastructure-as-code commands. type Runner struct { WorkDir string } -// NewRunner creates a Terraform runner for the given working directory. +// NewRunner creates a runner for the given working directory. func NewRunner(workDir string) *Runner { return &Runner{WorkDir: workDir} } -// Init runs terraform init. +// Init runs init. func (r *Runner) Init() error { return r.run("init", "-input=false") } -// Plan runs terraform plan. +// Plan runs plan. func (r *Runner) Plan(destroy bool) error { args := []string{"plan"} if destroy { @@ -30,22 +41,23 @@ func (r *Runner) Plan(destroy bool) error { return r.run(args...) } -// Apply runs terraform apply. +// Apply runs apply. func (r *Runner) Apply() error { return r.run("apply", "-auto-approve") } -// Destroy runs terraform destroy. +// Destroy runs destroy. func (r *Runner) Destroy() error { return r.run("destroy", "-auto-approve") } func (r *Runner) run(args ...string) error { - cmd := exec.Command("terraform", args...) + bin := binary() + cmd := exec.Command(bin, args...) cmd.Dir = r.WorkDir output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("terraform %v: %w\n%s", args, err, output) + return fmt.Errorf("%s %v: %w\n%s", bin, args, err, output) } return nil }