diff --git a/DETAILS.md b/DETAILS.md index 90739e4..1a51171 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 (OpenTofu/Terraform) +│ │ └── terraform.go # Runner: Init, Plan, Apply, Destroy wrappers │ └── 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 → `tofu destroy` → state cleanup | +| `obm destroy` | — | Confirmation prompt → `terraform 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 `tofu init && tofu apply` (or `terraform` if installed). +Final step: summary display with cost estimate → confirm → write `.env` → optionally run `terraform init && terraform apply`. ### Framework-specific defaults @@ -355,15 +355,15 @@ DigitalOcean prices: --- -## Terraform / OpenTofu Integration +## Terraform Integration -`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. +`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. -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` +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` 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 `tofu 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 `terraform destroy`, then cleans up state files unless `--keep-state` is set. --- diff --git a/LLMs.md b/LLMs.md index 4c85d01..8b9365d 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 `tofu init && tofu apply` (or `terraform` if installed). +After all 8 steps: summary + cost estimate → confirm → `.env` written → optional `terraform init && apply`. --- @@ -200,7 +200,7 @@ This loads `.env`, checks required variables, and validates inference API keys a obm destroy ``` -It asks for confirmation. It reads the state file (`terraform.tfstate`) to show what will be destroyed. +It asks for confirmation. It reads `terraform.tfstate` to show what will be destroyed. ### "Set up CI/CD deployment" diff --git a/Makefile b/Makefile index bfe215d..f817798 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 " │ 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 │"; \ + echo " │ or https://go.dev/dl/ │"; \ + echo " │ │"; \ + echo " │ macOS: brew install go │"; \ + echo " │ │"; \ + echo " │ Windows: https://go.dev/dl/ │"; \ + echo " │ │"; \ + echo " │ Or use the obm full install 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 diff --git a/README.md b/README.md index edea6c4..ba3c2f4 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 [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. +`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. -The infrastructure configs live in the [openboatmobile-ai](https://github.com/openboatmobile/openboatmobile-ai) repo. `obm` is the friendly CLI wrapper around them. +The Terraform 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 8ece268..a2b5222 100644 --- a/internal/destroy/destroy.go +++ b/internal/destroy/destroy.go @@ -12,16 +12,6 @@ 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) @@ -208,7 +198,7 @@ func displayDestroyPlan(resources []Resource) { fmt.Println() } -// runTerraformDestroy executes tofu/terraform destroy. +// runTerraformDestroy executes terraform destroy. func runTerraformDestroy(workDir string, opts *Options) error { args := []string{"destroy", "-auto-approve"} @@ -217,7 +207,7 @@ func runTerraformDestroy(workDir string, opts *Options) error { args = append(args, "-var-file", vf) } - cmd := exec.Command(tfBinary(), args...) + cmd := exec.Command("terraform", args...) cmd.Dir = workDir // Stream output to stdout/stderr diff --git a/internal/terraform/terraform.go b/internal/terraform/terraform.go index e4a8580..fa55b91 100644 --- a/internal/terraform/terraform.go +++ b/internal/terraform/terraform.go @@ -1,5 +1,4 @@ -// Package terraform wraps IaC operations (init, plan, apply, destroy). -// Supports OpenTofu (preferred) and Terraform (fallback). +// Package terraform wraps Terraform operations (init, plan, apply, destroy). package terraform import ( @@ -7,32 +6,22 @@ import ( "os/exec" ) -// 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. +// Runner executes Terraform commands. type Runner struct { WorkDir string } -// NewRunner creates a runner for the given working directory. +// NewRunner creates a Terraform runner for the given working directory. func NewRunner(workDir string) *Runner { return &Runner{WorkDir: workDir} } -// Init runs init. +// Init runs terraform init. func (r *Runner) Init() error { return r.run("init", "-input=false") } -// Plan runs plan. +// Plan runs terraform plan. func (r *Runner) Plan(destroy bool) error { args := []string{"plan"} if destroy { @@ -41,23 +30,22 @@ func (r *Runner) Plan(destroy bool) error { return r.run(args...) } -// Apply runs apply. +// Apply runs terraform apply. func (r *Runner) Apply() error { return r.run("apply", "-auto-approve") } -// Destroy runs destroy. +// Destroy runs terraform destroy. func (r *Runner) Destroy() error { return r.run("destroy", "-auto-approve") } func (r *Runner) run(args ...string) error { - bin := binary() - cmd := exec.Command(bin, args...) + cmd := exec.Command("terraform", args...) cmd.Dir = r.WorkDir output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("%s %v: %w\n%s", bin, args, err, output) + return fmt.Errorf("terraform %v: %w\n%s", args, err, output) } return nil }