Skip to content

Commit f84675a

Browse files
authored
SOPS decode change porting to v0.99 (#5549)
1 parent 4bf9b26 commit f84675a

6 files changed

Lines changed: 581 additions & 53 deletions

File tree

internal/runner/graph/graph.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/gruntwork-io/terragrunt/internal/runner"
1111
"github.com/gruntwork-io/terragrunt/internal/runner/common"
12+
"github.com/gruntwork-io/terragrunt/internal/runner/run/creds"
1213
"github.com/gruntwork-io/terragrunt/internal/runner/runall"
1314

1415
"github.com/gruntwork-io/terragrunt/internal/os/stdout"
@@ -21,6 +22,16 @@ import (
2122
)
2223

2324
func Run(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) error {
25+
// Get credentials BEFORE config parsing — sops_decrypt_file() and
26+
// get_aws_account_id() in locals need auth-provider credentials
27+
// available in opts.Env during HCL evaluation.
28+
// *Getter discarded: graph.Run only needs creds in opts.Env for initial config parse.
29+
// Per-unit creds are re-fetched in runnerpool task (intentional: each unit may have
30+
// different opts after clone).
31+
if _, err := creds.ObtainCredsForParsing(ctx, l, opts); err != nil {
32+
return err
33+
}
34+
2435
cfg, err := config.ReadTerragruntConfig(ctx, l, opts, config.DefaultParserOptions(l, opts))
2536
if err != nil {
2637
return err

internal/runner/run/creds/getter.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"maps"
77

88
"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers"
9+
"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd"
910
"github.com/gruntwork-io/terragrunt/pkg/log"
1011
"github.com/gruntwork-io/terragrunt/pkg/options"
1112
)
@@ -45,3 +46,16 @@ func (getter *Getter) ObtainAndUpdateEnvIfNecessary(ctx context.Context, l log.L
4546

4647
return nil
4748
}
49+
50+
// ObtainCredsForParsing creates a new Getter, obtains external-command
51+
// credentials, and populates opts.Env before HCL parsing.
52+
// Use when sops_decrypt_file() or get_aws_account_id() may appear in locals.
53+
// See https://github.com/gruntwork-io/terragrunt/issues/5515
54+
func ObtainCredsForParsing(ctx context.Context, l log.Logger, opts *options.TerragruntOptions) (*Getter, error) {
55+
g := NewGetter()
56+
if err := g.ObtainAndUpdateEnvIfNecessary(ctx, l, opts, externalcmd.NewProvider(l, opts)); err != nil {
57+
return nil, err
58+
}
59+
60+
return g, nil
61+
}

internal/runner/runnerpool/runner.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"github.com/gruntwork-io/terragrunt/internal/report"
2323
"github.com/gruntwork-io/terragrunt/internal/runner/common"
2424
"github.com/gruntwork-io/terragrunt/internal/runner/run/creds"
25-
"github.com/gruntwork-io/terragrunt/internal/runner/run/creds/providers/externalcmd"
2625
"github.com/gruntwork-io/terragrunt/internal/telemetry"
2726
"github.com/gruntwork-io/terragrunt/pkg/config"
2827
"github.com/gruntwork-io/terragrunt/pkg/log"
@@ -491,6 +490,15 @@ func (r *Runner) Run(ctx context.Context, l log.Logger, opts *options.Terragrunt
491490
unitLogger = l
492491
}
493492

493+
// Get credentials BEFORE config parsing — sops_decrypt_file() and
494+
// get_aws_account_id() in locals need auth-provider credentials
495+
// available in opts.Env during HCL evaluation.
496+
// See https://github.com/gruntwork-io/terragrunt/issues/5515
497+
credsGetter, err := creds.ObtainCredsForParsing(childCtx, unitLogger, u.Execution.TerragruntOptions)
498+
if err != nil {
499+
return err
500+
}
501+
494502
cfg, err := config.ReadTerragruntConfig(
495503
childCtx,
496504
unitLogger,
@@ -503,16 +511,6 @@ func (r *Runner) Run(ctx context.Context, l log.Logger, opts *options.Terragrunt
503511

504512
runCfg := cfg.ToRunConfig(unitLogger)
505513

506-
credsGetter := creds.NewGetter()
507-
if err = credsGetter.ObtainAndUpdateEnvIfNecessary(
508-
childCtx,
509-
unitLogger,
510-
u.Execution.TerragruntOptions,
511-
externalcmd.NewProvider(unitLogger, u.Execution.TerragruntOptions),
512-
); err != nil {
513-
return err
514-
}
515-
516514
err = unitRunner.Run(
517515
childCtx,
518516
u.Execution.TerragruntOptions,

pkg/config/config_helpers.go

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1258,9 +1258,27 @@ func sopsDecryptFile(ctx context.Context, pctx *ParsingContext, l log.Logger, pa
12581258
var result string
12591259

12601260
err := telemetry.TelemeterFromContext(ctx).Collect(ctx, "hcl_fn_sops_decrypt_file", attrs, func(childCtx context.Context) error {
1261+
if len(params) != 1 {
1262+
return errors.New(WrongNumberOfParamsError{Func: "sops_decrypt_file", Expected: "1", Actual: len(params)})
1263+
}
1264+
1265+
format, err := getSopsFileFormat(sourceFile)
1266+
if err != nil {
1267+
return errors.New(err)
1268+
}
1269+
1270+
path := sourceFile
1271+
1272+
if !filepath.IsAbs(path) {
1273+
path = filepath.Join(pctx.TerragruntOptions.WorkingDir, path)
1274+
path = filepath.Clean(path)
1275+
}
1276+
1277+
trackFileRead(pctx.FilesRead, path)
1278+
12611279
var innerErr error
12621280

1263-
result, innerErr = sopsDecryptFileImpl(childCtx, pctx, l, params)
1281+
result, innerErr = sopsDecryptFileImpl(childCtx, pctx, l, path, format, decrypt.File)
12641282

12651283
return innerErr
12661284
})
@@ -1269,60 +1287,59 @@ func sopsDecryptFile(ctx context.Context, pctx *ParsingContext, l log.Logger, pa
12691287
}
12701288

12711289
// sopsDecryptFileImpl contains the actual implementation of sopsDecryptFile
1272-
func sopsDecryptFileImpl(ctx context.Context, pctx *ParsingContext, _ log.Logger, params []string) (string, error) {
1273-
numParams := len(params)
1274-
1275-
var sourceFile string
1276-
1277-
if numParams > 0 {
1278-
sourceFile = params[0]
1279-
}
1290+
func sopsDecryptFileImpl(ctx context.Context, pctx *ParsingContext, l log.Logger, path string, format string, decryptFn func(string, string) ([]byte, error)) (string, error) {
1291+
// Fast path: check cache before acquiring lock.
1292+
// Cache has its own sync.RWMutex, safe for concurrent reads.
1293+
if val, ok := sopsCache.Get(ctx, path); ok {
1294+
l.Debugf("sops decrypt: cache hit for %s (len=%d)", path, len(val))
12801295

1281-
if numParams != 1 {
1282-
return "", errors.New(WrongNumberOfParamsError{Func: "sops_decrypt_file", Expected: "1", Actual: numParams})
1296+
return val, nil
12831297
}
12841298

1285-
format, err := getSopsFileFormat(sourceFile)
1286-
if err != nil {
1287-
return "", errors.New(err)
1288-
}
1299+
// Cache miss: acquire lock for env mutation + decrypt.
1300+
// The lock serializes os.Setenv/os.Unsetenv to prevent race conditions
1301+
// when multiple units decrypt concurrently with different auth credentials.
1302+
// See https://github.com/gruntwork-io/terragrunt/issues/5515
1303+
l.Debugf("sops decrypt: cache miss, acquiring lock for %s (format=%s)", path, format)
12891304

1290-
path := sourceFile
1305+
locks.EnvLock.Lock()
1306+
defer locks.EnvLock.Unlock()
12911307

1292-
if !filepath.IsAbs(path) {
1293-
path = filepath.Join(pctx.TerragruntOptions.WorkingDir, path)
1294-
path = filepath.Clean(path)
1295-
}
1308+
// Set env vars from opts.Env that are missing from process env.
1309+
// Auth-provider credentials (e.g., AWS_SESSION_TOKEN) may not exist
1310+
// in process env yet — SOPS needs them for KMS auth.
1311+
// Existing process env vars are preserved to avoid overriding real
1312+
// credentials with empty auth-provider values.
1313+
env := pctx.TerragruntOptions.Env
12961314

1297-
// Track that this file was read during parsing
1298-
trackFileRead(pctx.FilesRead, path)
1315+
var setKeys []string
12991316

1300-
// Set environment variables from the TerragruntOptions.Env map.
1301-
// This is especially useful for integrations with things like the `auth-provider` flag,
1302-
// which can set environment variables that are used for decryption.
1303-
//
1304-
// Due to the fact that sops doesn't expose a way of explicitly setting authentication configurations
1305-
// for decryption, we have to rely on environment variables to pass these configurations.
1306-
// This can cause a race condition, so we have to be careful to avoid having anything else
1307-
// running concurrently that might interfere with the environment variables.
1308-
env := pctx.TerragruntOptions.Env
1309-
if len(env) > 0 {
1310-
locks.EnvLock.Lock()
1311-
defer locks.EnvLock.Unlock()
1312-
1313-
for k, v := range env {
1314-
if os.Getenv(k) == "" {
1315-
os.Setenv(k, v) //nolint:errcheck
1316-
defer os.Unsetenv(k) //nolint:errcheck
1317-
}
1317+
for k, v := range env {
1318+
if _, exists := os.LookupEnv(k); exists {
1319+
continue
13181320
}
1321+
1322+
os.Setenv(k, v) //nolint:errcheck
1323+
1324+
setKeys = append(setKeys, k)
13191325
}
13201326

1327+
defer func() {
1328+
for _, k := range setKeys {
1329+
os.Unsetenv(k) //nolint:errcheck
1330+
}
1331+
}()
1332+
1333+
// Double-check: another goroutine may have populated cache while we waited for the lock.
13211334
if val, ok := sopsCache.Get(ctx, path); ok {
1335+
l.Debugf("sops decrypt: cache hit after lock for %s (len=%d)", path, len(val))
1336+
13221337
return val, nil
13231338
}
13241339

1325-
rawData, err := decrypt.File(path, format)
1340+
l.Debugf("sops decrypt: decrypting %s", path)
1341+
1342+
rawData, err := decryptFn(path, format)
13261343
if err != nil {
13271344
return "", errors.New(extractSopsErrors(err))
13281345
}
@@ -1334,7 +1351,7 @@ func sopsDecryptFileImpl(ctx context.Context, pctx *ParsingContext, _ log.Logger
13341351
return value, nil
13351352
}
13361353

1337-
return "", errors.New(InvalidSopsFormatError{SourceFilePath: sourceFile})
1354+
return "", errors.New(InvalidSopsFormatError{SourceFilePath: path})
13381355
}
13391356

13401357
// Mapping of SOPS format to string

0 commit comments

Comments
 (0)