Skip to content

Commit d05d8ca

Browse files
committed
feat: Add support for pre-loading environment variables from comet.yaml and enhance documentation
1 parent e216bfb commit d05d8ca

9 files changed

Lines changed: 186 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **Config-based environment variables** - Pre-load environment variables from `comet.yaml` before any command runs. Perfect for setting `SOPS_AGE_KEY` and other secrets needed during stack parsing. Supports secret resolution via `op://` and `sops://` prefixes. Shell environment variables take precedence.
1112
- **`comet init` command** - Initialize backends and providers without running plan/apply operations. Useful for read-only operations like `comet output` or troubleshooting provider/backend initialization issues.
1213
- **DSL Improvements** - Two core enhancements to reduce boilerplate by ~30%:
1314
- Bulk environment variables: `envs({})` accepts objects to set multiple vars at once

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION=v0.4.2
1+
VERSION=v0.4.3
22

33
tag:
44
@git tag -a ${VERSION} -m "version ${VERSION}" && git push origin ${VERSION}

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,40 @@ For more examples, see the [docs](https://github.com/moonwalker/comet/tree/main/
345345

346346
## Configuration
347347

348-
Comet can be configured using `comet.yaml` in your project directory.
348+
Comet can be configured using `comet.yaml` in your project directory.
349+
350+
### Basic Configuration
351+
352+
```yaml
353+
# comet.yaml
354+
stacks_dir: stacks # Directory containing stack files
355+
work_dir: stacks/_components # Working directory for components
356+
generate_backend: false # Auto-generate backend.tf.json
357+
log_level: INFO # Log verbosity
358+
tf_command: tofu # Use 'tofu' or 'terraform'
359+
```
360+
361+
### Environment Variables
362+
363+
Pre-load environment variables before any command runs. Perfect for secrets needed during stack parsing (like SOPS_AGE_KEY):
364+
365+
```yaml
366+
# comet.yaml
367+
env:
368+
# Load SOPS AGE key from 1Password (or other secret manager)
369+
SOPS_AGE_KEY: op://ci-cd/sops-age-key/private
370+
371+
# Plain values work too
372+
TF_LOG: DEBUG
373+
AWS_REGION: us-west-2
374+
```
375+
376+
**Features:**
377+
- Supports `op://` (1Password) and `sops://` secret resolution
378+
- Shell environment variables take precedence
379+
- Loaded before stack parsing begins
380+
381+
See [Best Practices](docs/best-practices.md) for more configuration examples.
349382

350383
## Development
351384

cmd/root.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cmd
33
import (
44
"fmt"
55
"os"
6+
"strings"
67

78
"github.com/spf13/cobra"
89

@@ -11,6 +12,7 @@ import (
1112
"github.com/moonwalker/comet/internal/env"
1213
"github.com/moonwalker/comet/internal/log"
1314
"github.com/moonwalker/comet/internal/schema"
15+
"github.com/moonwalker/comet/internal/secrets"
1416
)
1517

1618
const (
@@ -32,6 +34,7 @@ var (
3234
func init() {
3335
env.Load()
3436
cfg.Read(cfgFile, config)
37+
loadConfigEnv(config)
3538
log.SetLevel(config.LogLevel)
3639

3740
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", cfgFile, "config file")
@@ -43,6 +46,28 @@ func init() {
4346
})
4447
}
4548

49+
func loadConfigEnv(config *schema.Config) {
50+
for key, value := range config.Env {
51+
// Skip if already set in shell environment (shell wins)
52+
if os.Getenv(key) != "" {
53+
continue
54+
}
55+
56+
// Resolve secrets if value starts with op:// or sops://
57+
if strings.HasPrefix(value, "op://") || strings.HasPrefix(value, "sops://") {
58+
resolved, err := secrets.Get(value)
59+
if err != nil {
60+
log.Error(fmt.Sprintf("failed to resolve env var %s: %v", key, err))
61+
continue
62+
}
63+
os.Setenv(key, resolved)
64+
} else {
65+
// Plain value
66+
os.Setenv(key, value)
67+
}
68+
}
69+
}
70+
4671
func Execute() {
4772
if len(os.Args) == 1 {
4873
cli.PrintStyledText(name)

comet.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
stacks_dir: stacks
22
work_dir: stacks/_components
33
generate_backend: false
4+
5+
# Pre-load environment variables before any command runs
6+
# Perfect for SOPS AGE key which is needed during stack parsing
7+
env:
8+
SOPS_AGE_KEY: op://ci-cd/sops-age-key/private

docs/best-practices.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,30 @@ const db = component('database', 'modules/db', {
153153
})
154154
```
155155

156+
### SOPS AGE Key Setup
157+
158+
SOPS requires the `SOPS_AGE_KEY` to be set **before** stack parsing begins. Use `comet.yaml` to pre-load it:
159+
160+
**DO:** Configure in comet.yaml (recommended)
161+
```yaml
162+
# comet.yaml
163+
env:
164+
# Load SOPS AGE key from 1Password before stack parsing
165+
SOPS_AGE_KEY: op://ci-cd/sops-age-key/private
166+
```
167+
168+
**Alternative:** Export in shell
169+
```bash
170+
# In your shell profile or CI/CD
171+
export SOPS_AGE_KEY="AGE-SECRET-KEY-1..."
172+
```
173+
174+
**Why use comet.yaml?**
175+
- ✅ Automatic - no manual shell setup needed
176+
- ✅ Team-consistent - everyone uses the same config
177+
- ✅ CI/CD friendly - works seamlessly in pipelines
178+
- ✅ Secure - secrets fetched from 1Password on-demand
179+
156180
### Use Path-Based Organization
157181

158182
```yaml
@@ -371,7 +395,12 @@ comet apply app webapp # Now vpc outputs are available
371395

372396
**Issue:** SOPS decryption fails
373397
```bash
374-
# Solution: Ensure SOPS_AGE_KEY is set
398+
# Solution 1: Set SOPS_AGE_KEY in comet.yaml (recommended)
399+
# comet.yaml:
400+
# env:
401+
# SOPS_AGE_KEY: op://ci-cd/sops-age-key/private
402+
403+
# Solution 2: Export in shell
375404
export SOPS_AGE_KEY="your-age-key"
376405
comet apply dev
377406
```

internal/schema/config.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package schema
22

33
type Config struct {
4-
LogLevel string `mapstructure:"log_level"`
5-
Command string `mapstructure:"tf_command"`
6-
StacksDir string `mapstructure:"stacks_dir"`
7-
WorkDir string `mapstructure:"work_dir"`
8-
GenerateBackend bool `mapstructure:"generate_backend"`
4+
LogLevel string `mapstructure:"log_level"`
5+
Command string `mapstructure:"tf_command"`
6+
StacksDir string `mapstructure:"stacks_dir"`
7+
WorkDir string `mapstructure:"work_dir"`
8+
GenerateBackend bool `mapstructure:"generate_backend"`
9+
Env map[string]string `mapstructure:"env"`
910
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Example: Complete secrets workflow
2+
//
3+
// This demonstrates the difference between:
4+
// 1. Pre-loaded env vars (comet.yaml) - needed BEFORE parsing
5+
// 2. Stack-level secrets - loaded DURING parsing, used by Terraform
6+
7+
// ============================================================
8+
// In comet.yaml:
9+
// ============================================================
10+
// env:
11+
// # SOPS AGE key must be available before stack parsing
12+
// SOPS_AGE_KEY: op://ci-cd/sops-age-key/private
13+
//
14+
// # Any other early-stage environment variables
15+
// TF_LOG: DEBUG
16+
// ============================================================
17+
18+
stack({
19+
name: 'complete-secrets-example',
20+
backend: {
21+
type: 'gcs',
22+
bucket: 'my-terraform-state',
23+
prefix: 'complete-example'
24+
}
25+
})
26+
27+
// Now that SOPS_AGE_KEY is set, we can use sops:// references
28+
component('database', {
29+
source: './modules/database',
30+
vars: {
31+
// SOPS secret (requires SOPS_AGE_KEY from comet.yaml)
32+
admin_password: secret('sops://secrets/db.yaml#admin_password'),
33+
34+
// 1Password secret (loaded on-demand during stack parsing)
35+
backup_credentials: secret('op://production/database/backup-key'),
36+
37+
// Plain values work too
38+
database_name: 'myapp_production'
39+
}
40+
})
41+
42+
component('app', {
43+
source: './modules/app',
44+
vars: {
45+
// Mix and match secret sources
46+
api_key: secret('sops://secrets/app.yaml#api_key'),
47+
oauth_client_secret: secret('op://production/oauth/client-secret'),
48+
49+
// Reference outputs from other components
50+
database_host: state('database', 'host'),
51+
database_port: state('database', 'port')
52+
},
53+
envs: {
54+
// Environment variables for Terraform execution
55+
TF_VAR_region: 'us-west-2'
56+
}
57+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Example: Using SOPS with AGE encryption
2+
//
3+
// The SOPS_AGE_KEY is pre-loaded from comet.yaml before this stack is parsed,
4+
// allowing sops:// references to work immediately.
5+
//
6+
// In comet.yaml:
7+
// env:
8+
// SOPS_AGE_KEY: op://ci-cd/sops-age-key/private
9+
10+
stack({
11+
name: 'sops-example',
12+
backend: {
13+
type: 'gcs',
14+
bucket: 'my-terraform-state',
15+
prefix: 'sops-example'
16+
}
17+
})
18+
19+
// SOPS_AGE_KEY is already available, so this works:
20+
component('app', {
21+
source: './modules/app',
22+
vars: {
23+
// These secrets are decrypted using the AGE key set in comet.yaml
24+
database_password: secret('sops://secrets/prod.yaml#database_password'),
25+
api_key: secret('sops://secrets/prod.yaml#api_key')
26+
}
27+
})

0 commit comments

Comments
 (0)