Compare commits

..

1 commit

Author SHA1 Message Date
MermaidMan
ab1de96168 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.
2026-06-04 17:46:40 +00:00
6 changed files with 64 additions and 70 deletions

View file

@ -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 <path>` | Interactive walkthrough (default) or non-interactive from YAML |
| `obm validate` | `--env-file <path>` | 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.
---

View file

@ -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"

View file

@ -20,60 +20,32 @@ GOVET := $(GOCMD) vet
# Cross-compilation targets
TARGETS := linux-amd64 linux-arm64 darwin-arm64 windows-amd64 windows-arm64
# 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
# Platform detection
GOOS := $(shell go env GOOS)
GOARCH := $(shell go env GOARCH)
all: lint test build
build: check-go
build:
@echo "Building $(BINARY) v$(VERSION) for $(GOOS)/$(GOARCH)..."
$(GOBUILD) $(LDFLAGS) -o $(BINARY) ./cmd/obm
test: check-go
test:
@echo "Running tests..."
$(GOTEST) -v -race -coverprofile=coverage.out ./...
lint: vet fmt
@echo "✓ Lint pass"
vet: check-go
vet:
@echo "Running go vet..."
$(GOVET) ./...
fmt: check-go
fmt:
@echo "Formatting code..."
gofmt -l -s -w .
clean: check-go
clean:
@echo "Cleaning..."
rm -f $(BINARY)
rm -f coverage.out
@ -95,7 +67,7 @@ version:
@echo "$(VERSION)"
# Cross-compilation
cross-compile: check-go
cross-compile:
@echo "Cross-compiling for all platforms..."
@mkdir -p bin
@for target in $(TARGETS); do \
@ -112,23 +84,23 @@ cross-compile: check-go
@echo "✓ Cross-compilation complete"
# Build specific platform
build-linux-amd64: check-go
build-linux-amd64:
@mkdir -p bin
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/linux-amd64/$(BINARY) ./cmd/obm
build-linux-arm64: check-go
build-linux-arm64:
@mkdir -p bin
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/linux-arm64/$(BINARY) ./cmd/obm
build-darwin-arm64: check-go
build-darwin-arm64:
@mkdir -p bin
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/darwin-arm64/$(BINARY) ./cmd/obm
build-windows-amd64: check-go
build-windows-amd64:
@mkdir -p bin
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/windows-amd64/$(BINARY).exe ./cmd/obm
build-windows-arm64: check-go
build-windows-arm64:
@mkdir -p bin
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 $(GOBUILD) $(LDFLAGS) -o bin/windows-arm64/$(BINARY).exe ./cmd/obm
@ -168,10 +140,10 @@ dev: build test
@echo "✓ Development build complete"
# Quick test during development
quick-test: check-go
quick-test:
$(GOTEST) -race ./...
# Coverage report
coverage: check-go test
coverage: test
$(GOCMD) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report: coverage.html"

View file

@ -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.
---

View file

@ -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

View file

@ -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
}