Skip to content

Commit 07769cd

Browse files
committed
chore: PR simplification
1 parent df88646 commit 07769cd

21 files changed

Lines changed: 617 additions & 271 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
version: "v1.1.4"
3+
category: "enhancements"
4+
---
5+
6+
#### CLI config parsing no longer depends on the Terraform module
7+
8+
Terragrunt reads your OpenTofu/Terraform CLI config (`.tofurc`, `.terraformrc`, and the
9+
`*.tfrc` fragments under the CLI config directory) to pick up registry credentials, host
10+
overrides, and `provider_installation` settings.
11+
12+
That reading used to go through a loader vendored from the `hashicorp/terraform` module,
13+
which reached the invoking user's home directory directly and pulled a deprecated OpenPGP
14+
code path into the build. Terragrunt now parses these files itself, through the same
15+
filesystem and environment handles as the rest of a run.
16+
17+
The file format is unchanged, and so is precedence:
18+
19+
- `TF_CLI_CONFIG_FILE` or `TERRAFORM_CONFIG` names the config file outright, and suppresses the CLI config directory.
20+
- Otherwise the first existing default candidate is read, then every `*.tfrc` and `*.tfrc.json` fragment in the CLI config directory, in name order.
21+
- Fragments override the main file for `credentials`, `host`, and `credentials_helper`; `provider_installation` methods are appended; `plugin_cache_dir` keeps the first value set.
22+
- `TF_PLUGIN_CACHE_DIR` overrides `plugin_cache_dir` from every file, and is not expanded.
23+
24+
Two things that used to pass silently are now reported:
25+
26+
- A `credentials` or `host` block whose label is not a valid hostname is rejected, instead of being dropped and sending an unauthenticated request to the registry.
27+
- An unsupported `provider_installation` method is rejected by name.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
version: "v1.1.4"
3+
category: "process-updates"
4+
---
5+
6+
#### Go bumped to `v1.27`
7+
8+
The version of Golang used to compile the Terragrunt binary has been updated from `v1.26.6` to `v1.27.0`.
9+
10+
If you build Terragrunt from source, or import it as a Go module, you now need a Go 1.27 toolchain.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ require (
4444
github.com/hashicorp/go-hclog v1.6.3
4545
github.com/hashicorp/go-plugin v1.8.0
4646
github.com/hashicorp/go-version v1.9.0
47+
github.com/hashicorp/hcl v1.0.1-vault-7
4748
github.com/hashicorp/hcl/v2 v2.24.0
4849

4950
// Many functions of terraform was converted to internal to avoid use as a library after v0.15.3. This means that we
@@ -219,7 +220,6 @@ require (
219220
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
220221
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
221222
github.com/hashicorp/go-uuid v1.0.3 // indirect
222-
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
223223
github.com/hashicorp/vault/api v1.23.0 // indirect
224224
github.com/hashicorp/yamux v0.1.2 // indirect
225225
github.com/huandu/xstrings v1.5.0 // indirect

internal/gcphelper/config.go

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
package gcphelper
33

44
import (
5+
"bytes"
56
"context"
67
"encoding/json"
7-
"errors"
88
"fmt"
99

1010
"net/http"
@@ -143,7 +143,9 @@ func (b *GCPConfigBuilder) Build(
143143
return nil, err
144144
}
145145

146-
clientOpts = append(clientOpts, credOpt)
146+
if credOpt != nil {
147+
clientOpts = append(clientOpts, credOpt)
148+
}
147149
} else if gcpCfg != nil && gcpCfg.AccessToken != "" {
148150
// Use access token from config
149151
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{
@@ -213,37 +215,39 @@ func credentialsFileOption(v *venv.Venv, filename string) (option.ClientOption,
213215
return credentialsJSONOption(v, data)
214216
}
215217

216-
// credentialsJSONOption authenticates with a credentials JSON payload.
218+
// credentialsJSONOption authenticates with a credentials JSON payload, or returns a nil
219+
// option when the payload is empty so the caller falls through to the ADC chain the way
220+
// the SDK's own detection does.
217221
//
218-
// The credential type is selected explicitly because the SDK's generic JSON
219-
// detection is deprecated. Terragrunt retains support for every credential type
220-
// the SDK supports, but rejects unknown values before constructing credentials.
222+
// The payload's type is read here rather than by the SDK's generic JSON detection, whose
223+
// CredentialsJSON option is deprecated. The credentials are still built on v.HTTP, because
224+
// the SDK would otherwise put the token exchange on a client of its own making
225+
// (google.golang.org/api/internal.creds) that the venv never sees.
221226
func credentialsJSONOption(v *venv.Venv, data []byte) (option.ClientOption, error) {
227+
if len(bytes.TrimSpace(data)) == 0 {
228+
return nil, nil
229+
}
230+
222231
var metadata struct {
223232
Type credentials.CredType `json:"type"`
224233
}
225234

226235
if err := json.Unmarshal(data, &metadata); err != nil {
227-
return nil, fmt.Errorf("error parsing GCP credentials: %w", err)
236+
return nil, fmt.Errorf("%w: %w", ErrParsingCredentials, err)
228237
}
229238

230-
switch metadata.Type {
231-
case credentials.ServiceAccount,
232-
credentials.AuthorizedUser,
233-
credentials.ExternalAccount,
234-
credentials.ExternalAccountAuthorizedUser,
235-
credentials.ImpersonatedServiceAccount,
236-
credentials.GDCHServiceAccount:
237-
default:
238-
return nil, fmt.Errorf("unsupported GCP credentials type %q", metadata.Type)
239+
// The SDK reports a missing type as an unsupported filetype, which does not say what is wrong.
240+
if metadata.Type == "" {
241+
return nil, fmt.Errorf("%w: the payload has no \"type\" field", ErrParsingCredentials)
239242
}
240243

244+
// Every credential type the SDK accepts is passed through; it rejects the rest itself.
241245
creds, err := credentials.NewCredentialsFromJSON(metadata.Type, data, &credentials.DetectOptions{
242246
Scopes: gcsScopes(),
243247
Client: v.HTTP,
244248
})
245249
if err != nil {
246-
return nil, fmt.Errorf("error detecting GCP credentials: %w", err)
250+
return nil, fmt.Errorf("%w of type %q: %w", ErrBuildingCredentials, metadata.Type, err)
247251
}
248252

249253
return option.WithAuthCredentials(creds), nil
@@ -271,7 +275,7 @@ func createGCPCredentialsFromGoogleCredentialsEnv(
271275
}
272276

273277
if err := json.Unmarshal([]byte(contents), &account); err != nil {
274-
return nil, errors.New("error parsing GCP credentials")
278+
return nil, fmt.Errorf("%w from GOOGLE_CREDENTIALS: %w", ErrParsingCredentials, err)
275279
}
276280

277281
conf := jwt.Config{

internal/gcphelper/config_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,47 @@ func TestGcpConfigWithGoogleCredentialsFile(t *testing.T) {
248248
require.NoError(t, err)
249249
assert.NotEmpty(t, clientOpts)
250250
}
251+
252+
func TestGcpConfigCredentialsPayloads(t *testing.T) {
253+
t.Parallel()
254+
255+
testCases := []struct {
256+
expected error
257+
name string
258+
payload string
259+
}{
260+
{name: "unsupported type", payload: `{"type":"gce_metadata"}`, expected: gcphelper.ErrBuildingCredentials},
261+
{name: "missing type", payload: `{"client_email":"a@b.com"}`, expected: gcphelper.ErrParsingCredentials},
262+
{name: "not json", payload: `not-json`, expected: gcphelper.ErrParsingCredentials},
263+
{name: "json array", payload: `["a"]`, expected: gcphelper.ErrParsingCredentials},
264+
}
265+
266+
for _, tc := range testCases {
267+
t.Run(tc.name, func(t *testing.T) {
268+
t.Parallel()
269+
270+
credsFile := filepath.Join(t.TempDir(), "credentials.json")
271+
require.NoError(t, os.WriteFile(credsFile, []byte(tc.payload), 0o600))
272+
273+
_, err := gcphelper.NewGCPConfigBuilder().
274+
WithSessionConfig(&gcphelper.GCPSessionConfig{Credentials: credsFile}).
275+
Build(context.Background(), venvtest.NewWithOSFS().WithEnv(map[string]string{}))
276+
require.ErrorIs(t, err, tc.expected)
277+
})
278+
}
279+
}
280+
281+
// TestGcpConfigEmptyCredentialsFileFallsBackToADC pins the behaviour an unpopulated secret
282+
// volume depends on: an empty file contributes no option rather than failing the run.
283+
func TestGcpConfigEmptyCredentialsFileFallsBackToADC(t *testing.T) {
284+
t.Parallel()
285+
286+
credsFile := filepath.Join(t.TempDir(), "credentials.json")
287+
require.NoError(t, os.WriteFile(credsFile, nil, 0o600))
288+
289+
clientOpts, err := gcphelper.NewGCPConfigBuilder().
290+
WithSessionConfig(&gcphelper.GCPSessionConfig{Credentials: credsFile}).
291+
Build(context.Background(), venvtest.NewWithOSFS().WithEnv(map[string]string{}))
292+
require.NoError(t, err)
293+
assert.Empty(t, clientOpts)
294+
}

internal/gcphelper/errors.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package gcphelper
2+
3+
import "errors"
4+
5+
var (
6+
// ErrParsingCredentials reports a GCP credentials payload that is not usable JSON.
7+
ErrParsingCredentials = errors.New("error parsing GCP credentials")
8+
// ErrBuildingCredentials reports a credentials payload the SDK refused to turn into credentials.
9+
ErrBuildingCredentials = errors.New("error building GCP credentials")
10+
)

internal/getter/defaults.go

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

33
import (
4+
"errors"
45
"net/http"
56
"sync"
67

@@ -12,6 +13,9 @@ import (
1213
getter "github.com/hashicorp/go-getter/v2"
1314
)
1415

16+
// ErrNilVenv reports a nil venv handed to a constructor that authenticates through one.
17+
var ErrNilVenv = errors.New("getter: venv must not be nil")
18+
1519
// Registry keys for the non-git fetcher and resolver maps. They match
1620
// the lowercased scheme strings CASGetter.Detect produces. Exported so
1721
// callers can extend or replace specific entries in
@@ -95,16 +99,14 @@ func WithDispatchFS(fsys vfs.FS) GenericFetcherOption {
9599
// authenticate through, so the fetcher and the resolver read the registry
96100
// token and the user's CLI config from the same handles.
97101
func WithDispatchVenv(v *venv.Venv) GenericFetcherOption {
98-
return func(c *genericFetcherConfig) {
99-
if v == nil {
100-
panic("getter: WithDispatchVenv requires a non-nil venv")
101-
}
102-
103-
c.venv = v
102+
if v == nil {
103+
panic(ErrNilVenv)
104104
}
105+
106+
return func(c *genericFetcherConfig) { c.venv = v }
105107
}
106108

107-
// dispatchVenv returns the environment the tfr dispatch entries ride: the one
109+
// dispatchVenv returns the venv the tfr dispatch entries ride: the one
108110
// [WithDispatchVenv] persisted, or v when the caller left it unset.
109111
func dispatchVenv(v *venv.Venv, c *genericFetcherConfig) *venv.Venv {
110112
if c.venv != nil {

internal/getter/resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func DefaultSourceResolvers(
4141

4242
tfr := NewTFRResolver().
4343
WithHTTPClient(vhttp.WithTimeout(v.HTTP, tfrResolverTimeout)).
44-
WithAuth(RegistryAuth{Venv: dispatchVenv(v, &cfg)})
44+
WithAuth(NewRegistryAuth(dispatchVenv(v, &cfg)))
4545

4646
if cfg.tfrEnabled {
4747
requireLoggerFS(&cfg, SchemeTFR)

internal/getter/resolver_tfr.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func (r *TFRResolver) Probe(ctx context.Context, rawURL string) (string, error)
9292

9393
registryDomain := srcURL.Host
9494
if registryDomain == "" {
95-
registryDomain = tfimpl.DefaultRegistryDomain(r.Auth.env(), r.TofuImplementation)
95+
registryDomain = tfimpl.DefaultRegistryDomain(r.Auth.registryEnv(), r.TofuImplementation)
9696
}
9797

9898
versionList, hasVersion := srcURL.Query()[versionQueryKey]

internal/getter/tfr_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,15 +294,14 @@ credentials %q {
294294
require.NoError(t, err)
295295
}
296296

297-
func TestRegistryGetterNilEnvDoesNotPanic(t *testing.T) {
297+
func TestRegistryGetterEmptyEnvSendsNoAuth(t *testing.T) {
298298
t.Parallel()
299299

300300
server := newRegistryTestServerWithRequestHook(t, func(r *http.Request) {
301301
assert.Empty(t, r.Header.Get("Authorization"))
302302
})
303303

304-
v := venvtest.NewWithOSFS().WithHTTP(server.Client())
305-
v.Env = nil
304+
v := venvtest.NewWithOSFS().WithHTTP(server.Client()).WithEnv(map[string]string{})
306305

307306
client := newRegistryTestClientWithVenv(t, v, tfimpl.Terraform)
308307

0 commit comments

Comments
 (0)