Compare commits
1 commit
main
...
migrate-to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab1de96168 |
5 changed files with 48 additions and 26 deletions
22
DETAILS.md
22
DETAILS.md
|
|
@ -53,7 +53,7 @@ obm/
|
||||||
│ │ │ └── hetzner_test.go
|
│ │ │ └── hetzner_test.go
|
||||||
│ │ └── provider_test.go
|
│ │ └── provider_test.go
|
||||||
│ ├── terraform/
|
│ ├── terraform/
|
||||||
│ │ └── terraform.go # Runner: Init, Plan, Apply, Destroy wrappers
|
│ │ └── terraform.go # Runner: Init, Plan, Apply, Destroy (OpenTofu/Terraform)
|
||||||
│ └── validation/
|
│ └── validation/
|
||||||
│ ├── validation.go # Check interface, Runner, CheckResult, Status enum
|
│ ├── validation.go # Check interface, Runner, CheckResult, Status enum
|
||||||
│ └── validation_test.go
|
│ └── validation_test.go
|
||||||
|
|
@ -82,7 +82,7 @@ obm/
|
||||||
| `obm deploy` | `--config <path>` | Interactive walkthrough (default) or non-interactive from YAML |
|
| `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 validate` | `--env-file <path>` | Load `.env`, check required vars, validate API keys |
|
||||||
| `obm status` | — | Show deployment state (not yet implemented) |
|
| `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 version` | — | Print version with commit hash and build time |
|
||||||
| `obm help` | — | Print usage |
|
| `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.
|
7. **Tailscale** — Optional VPN setup. Auth key and tailnet domain.
|
||||||
8. **Discord** — Optional bot integration. Bot token, server ID, user IDs.
|
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
|
### 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:
|
The `internal/terraform/terraform.go` wrapper provides (auto-detects OpenTofu first, Terraform fallback):
|
||||||
- `Runner.Init()` — `terraform init -input=false`
|
- `Runner.Init()` — `tofu init -input=false`
|
||||||
- `Runner.Plan(destroy bool)` — `terraform plan` (with optional `-destroy` flag)
|
- `Runner.Plan(destroy bool)` — `tofu plan` (with optional `-destroy` flag)
|
||||||
- `Runner.Apply()` — `terraform apply -auto-approve`
|
- `Runner.Apply()` — `tofu apply -auto-approve`
|
||||||
- `Runner.Destroy()` — `terraform destroy -auto-approve`
|
- `Runner.Destroy()` — `tofu destroy -auto-approve`
|
||||||
|
|
||||||
All commands run in the `WorkDir` and capture combined output.
|
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
|
## 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
4
LLMs.md
4
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)
|
7. **Tailscale** — Optional VPN (auth key + tailnet)
|
||||||
8. **Discord** — Optional bot integration (token, server ID, user IDs)
|
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
|
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"
|
### "Set up CI/CD deployment"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,9 +115,9 @@ The AI model API costs are separate, and depend on your usage. Generally that's
|
||||||
|
|
||||||
## What happens under the hood?
|
## 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,16 @@ import (
|
||||||
"github.com/openboatmobile/obm/internal/prompt"
|
"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.
|
// Options configures the destroy operation.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
WorkDir string // Working directory (default: current)
|
WorkDir string // Working directory (default: current)
|
||||||
|
|
@ -198,7 +208,7 @@ func displayDestroyPlan(resources []Resource) {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
// runTerraformDestroy executes terraform destroy.
|
// runTerraformDestroy executes tofu/terraform destroy.
|
||||||
func runTerraformDestroy(workDir string, opts *Options) error {
|
func runTerraformDestroy(workDir string, opts *Options) error {
|
||||||
args := []string{"destroy", "-auto-approve"}
|
args := []string{"destroy", "-auto-approve"}
|
||||||
|
|
||||||
|
|
@ -207,7 +217,7 @@ func runTerraformDestroy(workDir string, opts *Options) error {
|
||||||
args = append(args, "-var-file", vf)
|
args = append(args, "-var-file", vf)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("terraform", args...)
|
cmd := exec.Command(tfBinary(), args...)
|
||||||
cmd.Dir = workDir
|
cmd.Dir = workDir
|
||||||
|
|
||||||
// Stream output to stdout/stderr
|
// Stream output to stdout/stderr
|
||||||
|
|
|
||||||
|
|
@ -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
|
package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|
@ -6,22 +7,32 @@ import (
|
||||||
"os/exec"
|
"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 {
|
type Runner struct {
|
||||||
WorkDir string
|
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 {
|
func NewRunner(workDir string) *Runner {
|
||||||
return &Runner{WorkDir: workDir}
|
return &Runner{WorkDir: workDir}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init runs terraform init.
|
// Init runs init.
|
||||||
func (r *Runner) Init() error {
|
func (r *Runner) Init() error {
|
||||||
return r.run("init", "-input=false")
|
return r.run("init", "-input=false")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan runs terraform plan.
|
// Plan runs plan.
|
||||||
func (r *Runner) Plan(destroy bool) error {
|
func (r *Runner) Plan(destroy bool) error {
|
||||||
args := []string{"plan"}
|
args := []string{"plan"}
|
||||||
if destroy {
|
if destroy {
|
||||||
|
|
@ -30,22 +41,23 @@ func (r *Runner) Plan(destroy bool) error {
|
||||||
return r.run(args...)
|
return r.run(args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply runs terraform apply.
|
// Apply runs apply.
|
||||||
func (r *Runner) Apply() error {
|
func (r *Runner) Apply() error {
|
||||||
return r.run("apply", "-auto-approve")
|
return r.run("apply", "-auto-approve")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy runs terraform destroy.
|
// Destroy runs destroy.
|
||||||
func (r *Runner) Destroy() error {
|
func (r *Runner) Destroy() error {
|
||||||
return r.run("destroy", "-auto-approve")
|
return r.run("destroy", "-auto-approve")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Runner) run(args ...string) error {
|
func (r *Runner) run(args ...string) error {
|
||||||
cmd := exec.Command("terraform", args...)
|
bin := binary()
|
||||||
|
cmd := exec.Command(bin, args...)
|
||||||
cmd.Dir = r.WorkDir
|
cmd.Dir = r.WorkDir
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue