Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions cli/cmd/bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"os"
"path/filepath"
"testing"
)

func TestBootstrapInventory(t *testing.T) {
t.Run("copies example when inventory missing", func(t *testing.T) {
dir := t.TempDir()
invPath := filepath.Join(dir, "dev-inventory")
examplePath := invPath + ".example"

exampleContent := []byte("[all:vars]\nenv=dev\nregion=us-west-2\n")
if err := os.WriteFile(examplePath, exampleContent, 0o644); err != nil {
t.Fatalf("write example: %v", err)
}

if err := bootstrapInventory(invPath); err != nil {
t.Fatalf("bootstrapInventory() error: %v", err)
}

got, err := os.ReadFile(invPath)
if err != nil {
t.Fatalf("read bootstrapped inventory: %v", err)
}
if string(got) != string(exampleContent) {
t.Errorf("content mismatch:\ngot: %q\nwant: %q", got, exampleContent)
}
})

t.Run("no-op when inventory exists", func(t *testing.T) {
dir := t.TempDir()
invPath := filepath.Join(dir, "dev-inventory")

existing := []byte("[all:vars]\nenv=dev\ninstance=i-abc123\n")
if err := os.WriteFile(invPath, existing, 0o644); err != nil {
t.Fatalf("write existing: %v", err)
}

if err := bootstrapInventory(invPath); err != nil {
t.Fatalf("bootstrapInventory() error: %v", err)
}

got, err := os.ReadFile(invPath)
if err != nil {
t.Fatalf("read inventory: %v", err)
}
if string(got) != string(existing) {
t.Errorf("existing inventory was overwritten")
}
})

t.Run("errors when neither file exists", func(t *testing.T) {
dir := t.TempDir()
invPath := filepath.Join(dir, "dev-inventory")

err := bootstrapInventory(invPath)
if err == nil {
t.Fatal("expected error when no inventory or example exists")
}
})
}
4 changes: 2 additions & 2 deletions cli/cmd/inventory.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ func runInventorySync(cmd *cobra.Command, args []string) error {
}
invPath := cfg.InventoryPath()

if _, err := os.Stat(invPath); os.IsNotExist(err) {
return fmt.Errorf("inventory file not found: %s", invPath)
if err := bootstrapInventory(invPath); err != nil {
return err
}

backup, _ := cmd.Flags().GetBool("backup")
Expand Down
25 changes: 25 additions & 0 deletions cli/cmd/provision.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,36 @@ func preflightChecks(ctx context.Context, cfg *config.Config) error {
return nil
}

// bootstrapInventory copies the example inventory file to the target path if
// the target does not exist. This allows provision and inventory commands to
// work on a fresh environment without a manual copy step.
func bootstrapInventory(invPath string) error {
if _, err := os.Stat(invPath); err == nil {
return nil
}
examplePath := invPath + ".example"
if _, err := os.Stat(examplePath); err != nil {
return fmt.Errorf("inventory file not found: %s (no .example template either)", invPath)
}
data, err := os.ReadFile(examplePath)
if err != nil {
return fmt.Errorf("read example inventory: %w", err)
}
if err := os.WriteFile(invPath, data, 0o644); err != nil {
return fmt.Errorf("write inventory from example: %w", err)
}
slog.Info("bootstrapped inventory from example template", "path", invPath)
return nil
}

// ensureInventorySynced compares inventory instance IDs against live EC2
// state and auto-syncs if they diverge. This prevents provisioning against
// stale instance IDs after an infra destroy/apply cycle.
func ensureInventorySynced(ctx context.Context, cfg *config.Config) error {
invPath := cfg.InventoryPath()
if err := bootstrapInventory(invPath); err != nil {
return err
}
parsed, err := inv.Parse(invPath)
if err != nil {
return fmt.Errorf("parse inventory: %w", err)
Expand Down
Loading