Skip to content

Commit efb96c9

Browse files
committed
Release v0.6.0: Bootstrap command for fast secret setup
BREAKING CHANGE: Removed op:// and sops:// support from env section Added: - comet bootstrap command for one-time secret setup - comet bootstrap status to show configuration state - comet bootstrap clear to reset state - Bootstrap step types: secret, command, check - State tracking in .comet/bootstrap.state - Idempotent execution with --force flag override Changed: - env section now only supports plain values (fast startup) - Secret resolution moved to bootstrap for 30x performance improvement Migration: - Move secret references from env to bootstrap configuration - Run 'comet bootstrap' once for setup - All subsequent commands are now fast (100ms vs 4s) See CHANGELOG.md for full migration guide.
1 parent 5de272c commit efb96c9

10 files changed

Lines changed: 892 additions & 94 deletions

File tree

CHANGELOG.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.6.0] - 2025-10-16
11+
12+
### Added
13+
- **`comet bootstrap` command** - One-time setup for secrets and dependencies. Fetches secrets from 1Password/SOPS and caches them locally, making all subsequent commands fast. No more 3-5 second delays on every command!
14+
- `comet bootstrap` - Run bootstrap steps
15+
- `comet bootstrap status` - Show what's been set up
16+
- `comet bootstrap clear` - Reset state
17+
- Bootstrap configuration in `comet.yaml` with support for secret fetching, command execution, and dependency checks
18+
- State tracking in `.comet/bootstrap.state`
19+
- Idempotent by default with `--force` flag to re-run
20+
21+
### Changed
22+
- **BREAKING: Removed `op://` and `sops://` support from `env` section** - The `env` section now only supports plain values for fast startup. Use `comet bootstrap` instead for secret management.
23+
- **`env` section is now fast** - No more slow secret resolution on every command. Plain environment variables only.
24+
25+
### Migration Guide
26+
If you were using `op://` or `sops://` in your `env` section:
27+
28+
**Before (v0.5.0):**
29+
```yaml
30+
env:
31+
SOPS_AGE_KEY: op://vault/sops-key/private # Slow on every command
32+
```
33+
34+
**After (v0.6.0):**
35+
```yaml
36+
bootstrap:
37+
- name: sops-key
38+
type: secret
39+
source: op://vault/sops-key/private
40+
target: ~/.config/sops/age/keys.txt
41+
mode: "0600"
42+
43+
# Then run once: comet bootstrap
44+
# All commands are now fast!
45+
```
46+
1047
## [0.5.0] - 2025-10-10
1148

1249
### Added
@@ -63,6 +100,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
63100
- Support for Terraform and OpenTofu
64101
- CLI commands: plan, apply, destroy, list, output, clean
65102

66-
[Unreleased]: https://github.com/moonwalker/comet/compare/v0.5.0...HEAD
103+
[Unreleased]: https://github.com/moonwalker/comet/compare/v0.6.0...HEAD
104+
[0.6.0]: https://github.com/moonwalker/comet/releases/tag/v0.6.0
67105
[0.5.0]: https://github.com/moonwalker/comet/releases/tag/v0.5.0
68106
[0.1.0]: https://github.com/moonwalker/comet/releases/tag/v0.1.0

README.md

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,27 +376,98 @@ tf_command: tofu # Use 'tofu' or 'terraform'
376376
377377
### Environment Variables
378378
379-
Pre-load environment variables before any command runs. Perfect for secrets needed during stack parsing (like SOPS_AGE_KEY):
379+
Set plain environment variables that are loaded before commands run:
380380
381381
```yaml
382382
# comet.yaml
383383
env:
384-
# Plain values - fast and simple
385384
TF_LOG: DEBUG
386385
AWS_REGION: us-west-2
387-
388-
# Secret references - convenient but SLOW (3-5s per secret on every command)
389-
# SOPS_AGE_KEY: op://ci-cd/sops-age-key/private # ⚠️ Adds ~4s to EVERY command
386+
PROJECT_ID: my-project
390387
```
391388
392389
**Features:**
393-
- Supports `op://` (1Password) and `sops://` secret resolution
390+
- Plain values only (fast startup)
394391
- Shell environment variables take precedence
395392
- Loaded before stack parsing begins
396393
397-
**⚠️ Performance Warning:**
394+
**Note:** The `env` section only supports plain values. For secrets, use the `bootstrap` feature below.
395+
396+
### Bootstrap: One-Time Secret Setup
397+
398+
Bootstrap fetches secrets from 1Password/SOPS and caches them locally. Run once, then all commands are fast!
399+
400+
```yaml
401+
# comet.yaml
402+
bootstrap:
403+
- name: sops-age-key
404+
type: secret
405+
source: op://vault/infrastructure/sops-age-key # Your 1Password path
406+
target: ~/.config/sops/age/keys.txt
407+
mode: "0600"
408+
```
409+
410+
**Usage:**
411+
412+
```bash
413+
# One-time setup (takes ~4s)
414+
comet bootstrap
415+
416+
# Check status
417+
comet bootstrap status
418+
419+
# Now all commands are fast!
420+
comet plan dev # 100ms instead of 4s!
421+
comet apply dev
422+
```
423+
424+
**Features:**
425+
- ✅ **One-time cost** - Slow fetch only happens once
426+
- ✅ **Fast commands** - All subsequent commands use cached secrets
427+
- ✅ **Idempotent** - Safe to run multiple times
428+
- ✅ **State tracking** - Knows what's been set up
429+
- ✅ **Standard location** - Uses SOPS's default key path
430+
431+
**Bootstrap Types:**
398432

399-
Secret references (`op://`, `sops://`) are resolved on **EVERY** comet command (plan, apply, list, etc.), which can add 3-5 seconds due to CLI overhead.
433+
```yaml
434+
bootstrap:
435+
# Fetch and cache secrets
436+
- name: sops-key
437+
type: secret
438+
source: op://vault/item/field
439+
target: ~/.config/sops/age/keys.txt
440+
mode: "0600"
441+
442+
# Check required tools exist
443+
- name: check-tools
444+
type: check
445+
command: op,sops,tofu # Comma-separated
446+
447+
# Run custom commands
448+
- name: gcloud-auth
449+
type: command
450+
command: gcloud auth application-default login
451+
optional: true
452+
```
453+
454+
**Migrating from v0.5.0:**
455+
456+
If you were using `op://` or `sops://` in the `env` section, migrate to `bootstrap`:
457+
458+
```yaml
459+
# OLD (v0.5.0) - Slow on every command
460+
env:
461+
SOPS_AGE_KEY: op://vault/key/private
462+
463+
# NEW (v0.6.0) - Fast after bootstrap
464+
bootstrap:
465+
- name: sops-key
466+
type: secret
467+
source: op://vault/key/private
468+
target: ~/.config/sops/age/keys.txt
469+
mode: "0600"
470+
```
400471

401472
**Recommended approach for frequently-used secrets:**
402473

cmd/bootstrap.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"time"
7+
8+
"github.com/spf13/cobra"
9+
10+
"github.com/moonwalker/comet/internal/bootstrap"
11+
"github.com/moonwalker/comet/internal/log"
12+
)
13+
14+
var (
15+
bootstrapForce bool
16+
17+
bootstrapCmd = &cobra.Command{
18+
Use: "bootstrap",
19+
Short: "Bootstrap secrets and dependencies",
20+
Long: `Bootstrap secrets and dependencies from configured sources.
21+
22+
This command fetches secrets (like SOPS keys) from remote sources (1Password, etc.)
23+
and saves them locally for fast access. This is a one-time setup step.
24+
25+
After bootstrapping, your comet commands will be fast since secrets are cached locally.`,
26+
Run: runBootstrap,
27+
}
28+
29+
bootstrapStatusCmd = &cobra.Command{
30+
Use: "status",
31+
Short: "Show bootstrap status",
32+
Run: bootstrapStatus,
33+
}
34+
35+
bootstrapClearCmd = &cobra.Command{
36+
Use: "clear",
37+
Short: "Clear bootstrap state",
38+
Run: bootstrapClear,
39+
}
40+
)
41+
42+
func init() {
43+
rootCmd.AddCommand(bootstrapCmd)
44+
bootstrapCmd.AddCommand(bootstrapStatusCmd)
45+
bootstrapCmd.AddCommand(bootstrapClearCmd)
46+
47+
bootstrapCmd.Flags().BoolVarP(&bootstrapForce, "force", "f", false, "Force re-run all steps")
48+
}
49+
50+
func runBootstrap(cmd *cobra.Command, args []string) {
51+
if len(config.Bootstrap) == 0 {
52+
log.Info("No bootstrap configuration found in comet.yaml")
53+
log.Info("\nTo configure bootstrap, add a 'bootstrap' section:")
54+
log.Info(`
55+
bootstrap:
56+
- name: sops-key
57+
type: secret
58+
source: op://vault/item/field
59+
target: ~/.config/sops/age/keys.txt
60+
mode: "0600"
61+
`)
62+
return
63+
}
64+
65+
log.Info(fmt.Sprintf("Bootstrap configuration: %d step(s)", len(config.Bootstrap)))
66+
67+
runner, err := bootstrap.NewRunner(config, bootstrapForce)
68+
if err != nil {
69+
log.Fatal(err)
70+
}
71+
72+
if err := runner.Run(); err != nil {
73+
log.Fatal(err)
74+
}
75+
}
76+
77+
func bootstrapStatus(cmd *cobra.Command, args []string) {
78+
if len(config.Bootstrap) == 0 {
79+
log.Info("No bootstrap configuration found")
80+
return
81+
}
82+
83+
state, err := bootstrap.LoadState()
84+
if err != nil {
85+
log.Fatal(err)
86+
}
87+
88+
fmt.Println("\nBootstrap Configuration:")
89+
fmt.Printf(" Steps: %d\n", len(config.Bootstrap))
90+
if !state.LastRun.IsZero() {
91+
fmt.Printf(" Last run: %s\n", formatTime(state.LastRun))
92+
}
93+
94+
fmt.Println("\nStep Status:")
95+
96+
completed := 0
97+
for _, step := range config.Bootstrap {
98+
stepState := state.GetStep(step.Name)
99+
100+
if stepState == nil {
101+
fmt.Printf(" ⚪ %-20s Not run yet\n", step.Name)
102+
continue
103+
}
104+
105+
switch stepState.Status {
106+
case "completed":
107+
completed++
108+
fmt.Printf(" ✅ %-20s Completed (%s)\n", step.Name, formatTime(stepState.CompletedAt))
109+
if step.Target != "" {
110+
targetExists := fileExists(expandPath(step.Target))
111+
if targetExists {
112+
fmt.Printf(" %-20s Target: %s (exists)\n", "", step.Target)
113+
} else {
114+
fmt.Printf(" %-20s Target: %s (missing!)\n", "", step.Target)
115+
}
116+
}
117+
case "failed":
118+
fmt.Printf(" ❌ %-20s Failed (%s)\n", step.Name, formatTime(stepState.LastAttempt))
119+
fmt.Printf(" %-20s Error: %s\n", "", stepState.Error)
120+
case "skipped":
121+
fmt.Printf(" ⏭️ %-20s Skipped\n", step.Name)
122+
default:
123+
fmt.Printf(" ⚪ %-20s Unknown status: %s\n", step.Name, stepState.Status)
124+
}
125+
}
126+
127+
fmt.Printf("\nOverall: %d/%d steps completed\n", completed, len(config.Bootstrap))
128+
129+
if completed < len(config.Bootstrap) {
130+
fmt.Println("\nRun 'comet bootstrap' to complete setup")
131+
fmt.Println("Run 'comet bootstrap --force' to re-run all steps")
132+
}
133+
}
134+
135+
func bootstrapClear(cmd *cobra.Command, args []string) {
136+
if err := bootstrap.Clear(); err != nil {
137+
log.Fatal(err)
138+
}
139+
log.Info("✅ Bootstrap state cleared")
140+
log.Info("Run 'comet bootstrap' to set up again")
141+
}
142+
143+
func fileExists(path string) bool {
144+
_, err := os.Stat(path)
145+
return err == nil
146+
}
147+
148+
func expandPath(path string) string {
149+
if path == "" {
150+
return ""
151+
}
152+
if path[0] == '~' {
153+
home, _ := os.UserHomeDir()
154+
return home + path[1:]
155+
}
156+
return os.ExpandEnv(path)
157+
}
158+
159+
func formatTime(t time.Time) string {
160+
if t.IsZero() {
161+
return "never"
162+
}
163+
164+
duration := time.Since(t)
165+
166+
if duration < time.Minute {
167+
return "just now"
168+
} else if duration < time.Hour {
169+
mins := int(duration.Minutes())
170+
return fmt.Sprintf("%dm ago", mins)
171+
} else if duration < 24*time.Hour {
172+
hours := int(duration.Hours())
173+
return fmt.Sprintf("%dh ago", hours)
174+
} else {
175+
days := int(duration.Hours() / 24)
176+
return fmt.Sprintf("%dd ago", days)
177+
}
178+
}

cmd/root.go

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"os"
66
"strings"
7-
"time"
87

98
"github.com/spf13/cobra"
109

@@ -13,7 +12,6 @@ import (
1312
"github.com/moonwalker/comet/internal/env"
1413
"github.com/moonwalker/comet/internal/log"
1514
"github.com/moonwalker/comet/internal/schema"
16-
"github.com/moonwalker/comet/internal/secrets"
1715
)
1816

1917
const (
@@ -55,26 +53,16 @@ func loadConfigEnv(config *schema.Config) {
5553
continue
5654
}
5755

58-
// Resolve secrets if value starts with op:// or sops://
56+
// Only plain values supported
57+
// For secrets, use 'comet bootstrap' to set them up locally
5958
if strings.HasPrefix(value, "op://") || strings.HasPrefix(value, "sops://") {
60-
start := time.Now()
61-
log.Debug("Resolving secret for env var", "key", key, "ref", value)
62-
log.Warn(fmt.Sprintf("Loading secret for %s from config - this runs on EVERY command and may be slow (consider setting in shell instead)", key))
63-
64-
resolved, err := secrets.Get(value)
65-
duration := time.Since(start)
66-
log.Debug("Secret resolution completed", "key", key, "duration", duration)
67-
68-
if err != nil {
69-
log.Error(fmt.Sprintf("failed to resolve env var %s: %v", key, err))
70-
continue
71-
}
72-
os.Setenv(key, resolved)
73-
} else {
74-
// Plain value
75-
log.Debug("Setting env var from config", "key", key)
76-
os.Setenv(key, value)
59+
log.Error(fmt.Sprintf("Secret references (op://, sops://) are no longer supported in env section for %s", key))
60+
log.Error(fmt.Sprintf("Use 'comet bootstrap' to set up secrets locally. See docs for migration guide."))
61+
continue
7762
}
63+
64+
log.Debug("Setting env var from config", "key", key)
65+
os.Setenv(key, value)
7866
}
7967
}
8068

0 commit comments

Comments
 (0)