Skip to content

Commit b600a3f

Browse files
authored
Merge branch 'main' into fix-5810-feature-default-isolation
2 parents d3da69f + 45fe7da commit b600a3f

87 files changed

Lines changed: 5457 additions & 1175 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.golangci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,19 @@ linters:
109109
- linters:
110110
- dupl
111111
path: cli/flags/shared
112+
# BubbleTea's tea.Model expects value-receiver methods (Init/Update/View),
113+
# so Model and WelcomeModel in the TUI packages trip gocritic's hugeParam
114+
# check. Silence it for these packages only; hugeParam still runs everywhere else.
115+
- linters:
116+
- gocritic
117+
text: "hugeParam:"
118+
path: internal/cli/commands/catalog/tui/
112119
# Incrementally linting lines that are too long to ensure that
113120
# we don't have conflicts on every file in the codebase while
114121
# trying to get this merged in.
115122
- linters:
116123
- lll
117-
path-except: '^(internal/awshelper/|internal/cas/|internal/cli/commands/(backend/(delete|migrate)|catalog/tui/command|exec|find|help|list|stack)/|internal/cloner/|internal/configbridge/|internal/engine/|internal/errorconfig/|internal/errors/|internal/experiment/|internal/gcphelper/|internal/git/|internal/prepare/|internal/runner/(common|graph|run/creds/providers/(amazonsts|externalcmd))/|internal/stacks/(generate|output)/|internal/tf/cache/(controllers|middleware)/|internal/tips/|internal/vfs/|pkg/log/(format/placeholders|writer)/)'
124+
path-except: '^(internal/awshelper/|internal/cas/|internal/cli/commands/(backend/(delete|migrate)|catalog/tui/command|exec|find|help|list|stack)/|internal/cloner/|internal/configbridge/|internal/engine/|internal/errorconfig/|internal/errors/|internal/experiment/|internal/gcphelper/|internal/git/|internal/os/exec/|internal/prepare/|internal/queue/|internal/runner/(common|graph|run/creds)/|internal/stacks/(generate|output)/|internal/tf/cache/(controllers|middleware)/|internal/tflint/|internal/tips/|internal/vfs/|internal/worktrees/|pkg/log/(format/placeholders|writer)/)'
118125
paths:
119126
- docs
120127
- _ci

docs/src/assets/logo-dark-2.svg

Lines changed: 46 additions & 0 deletions
Loading

docs/src/assets/logo-ko.png

8.59 KB
Loading

docs/src/components/Header.astro

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Search from 'virtual:starlight/components/Search';
77
import HeadphonesIcon from '@assets/headset-icon.svg';
88
import PipelineIcon from '@assets/pipelines.svg';
99
import TerragruntLogo from '@assets/horizontal-logo-light.svg';
10+
import TerragruntIcon from '@assets/logo-dark-2.svg';
1011
import ThemeToggle from '@components/ThemeToggle.astro';
1112
import ButtonLink from '@components/ui/ButtonLink';
1213
import { getGitHubRepo, formatStarCount } from '@lib/github';
@@ -152,13 +153,14 @@ if (!process.env.GITHUB_STARS) {
152153

153154
<!-- Buttons -->
154155
<ButtonLink
155-
href="/terragrunt-scale"
156-
variant="secondary"
157156
className="hidden md:flex whitespace-nowrap"
157+
href="https://www.gruntwork.io/services/terragrunt"
158158
size={'lg'}
159+
variant="secondary"
159160
>
160-
Automate Deployments
161+
Enterprise Support
161162
</ButtonLink>
163+
162164
<ButtonLink
163165
href="/terragrunt-scale"
164166
variant="secondary"
@@ -173,18 +175,26 @@ if (!process.env.GITHUB_STARS) {
173175
/>
174176
</ButtonLink>
175177

178+
176179
<ButtonLink
177-
href="https://www.gruntwork.io/services/terragrunt"
178-
variant="primary"
180+
buttonClassName="flex items-center justify-center gap-2"
179181
className="hidden md:flex whitespace-nowrap"
180-
isExternalLink={true}
182+
href="https://terragrunt.com/terragrunt-scale"
181183
size={'lg'}
184+
variant="primary"
182185
>
183-
Enterprise Support
186+
Terragrunt Scale
187+
<Image
188+
alt="Terragrunt Scale Icon"
189+
height={16}
190+
src={TerragruntIcon}
191+
quality="max"
192+
width={16}
193+
/>
184194
</ButtonLink>
185195

186196
<ButtonLink
187-
href="https://www.gruntwork.io/services/terragrunt"
197+
href="https://terragrunt.com/terragrunt-scale"
188198
variant="primary"
189199
size="icon"
190200
className="md:hidden"

docs/src/content/docs/03-features/02-stacks/03-explicit.mdx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,23 @@ Requirements:
474474
- The `--no-cas` flag must not be set.
475475
- Sources using `update_source_with_cas` must be relative paths within the same repository.
476476

477+
### Local catalog sources
478+
479+
The consumer stack's `source` can be a local filesystem path in addition to a remote Git URL. Both go through the same CAS-backed rewrite pipeline:
480+
481+
```hcl
482+
# live/terragrunt.stack.hcl
483+
stack "service" {
484+
source = "../catalog//stacks/service"
485+
486+
path = "service"
487+
}
488+
```
489+
490+
Terragrunt copies the referenced directory into a temporary directory, computes a content-addressed root hash over `(relative path, mode, file-content hash)` triples using SHA-256, and rewrites nested `source` attributes against that copy. The original directory is never modified, so the catalog on disk stays clean even though the rewritten `source` values need somewhere to live.
491+
492+
This is useful when iterating on a catalog before tagging a release: the same `update_source_with_cas = true` attributes in the catalog's `terragrunt.stack.hcl` and `terragrunt.hcl` files apply unchanged, whether consumers pull the catalog over Git or point at a local checkout.
493+
477494
</Since>
478495

479496
## Known Limitations of Explicit Stacks

docs/src/content/docs/03-features/07-caching/04-cas.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ terraform {
9292

9393
During stack generation, Terragrunt rewrites these relative sources to `cas::` references that point to content stored in the CAS. The repository is cloned once, and subsequent stack generations resolve content from the local store without network access. Generated `.terragrunt-stack` files contain deterministic CAS references instead of version variables, so they do not produce diffs on regeneration.
9494

95+
The catalog source can be either a remote Git URL or a local filesystem path (absolute, or relative to the current working directory). Local sources are copied into a temporary directory before rewriting, so the original catalog directory is never modified. This makes the same catalog layout usable against a published Git ref or a local checkout, which is useful when iterating on a catalog before tagging a release.
96+
9597
For more details on using this with stacks, see [Explicit Stacks: CAS Integration](/features/stacks/explicit#cas-integration).
9698

9799
<Aside type="caution">
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
version: "v1.0.3"
3+
category: "experiments-updated"
4+
---
5+
6+
#### `cas` — Local paths supported as stack component sources
7+
8+
CAS-backed stack generation now accepts a local filesystem path as the `source` of a consumer `stack` or `unit` block, in addition to a remote Git URL. Terragrunt copies the referenced directory into a temporary directory, computes a content-addressed root hash over the copy, and applies the same `update_source_with_cas` rewriting as the remote flow. The original directory is left untouched.
9+
10+
```hcl
11+
# live/terragrunt.stack.hcl
12+
stack "service" {
13+
source = "../catalog//stacks/service"
14+
15+
path = "service"
16+
}
17+
```
18+
19+
This makes a catalog usable against a local checkout under the same `update_source_with_cas = true` attributes that already work for Git URLs, which is helpful when iterating on a catalog before tagging a release.
20+
21+
See the [CAS documentation](/features/caching/cas) and [Explicit Stacks: Local catalog sources](/features/stacks/explicit#local-catalog-sources) for details.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
version: "v1.0.4"
3+
category: "experiments-updated"
4+
---
5+
6+
#### Cache and plugin directories follow platform conventions
7+
8+
Terragrunt's global cache directory now resolves to the platform's user cache location instead of a hard-coded `~/.cache/terragrunt`. On Linux this honors `XDG_CACHE_HOME` (still `~/.cache/terragrunt` by default), on macOS it resolves to `~/Library/Caches/terragrunt`, and on Windows it resolves under `%LocalAppData%`. The CAS content store, the auto provider cache, and the IaC engine plugin directory all move with it.
9+
10+
Existing caches at the previous locations are not migrated. They become orphaned and continue to consume disk space until removed.
11+
12+
Consider deleting the old paths to reclaim that space if you are on macOS or Windows, or have configured a custom `XDG_CACHE_HOME`:
13+
14+
```bash
15+
# CAS store and engine plugins under the legacy ~/.cache layout
16+
rm -rf ~/.cache/terragrunt
17+
```
18+

internal/cache/cache_test.go

Lines changed: 143 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package cache_test
22

33
import (
4+
"context"
5+
"path/filepath"
46
"testing"
7+
"testing/synctest"
58
"time"
69

710
"github.com/gruntwork-io/terragrunt/internal/cache"
811
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
913
)
1014

1115
func TestCacheCreation(t *testing.T) {
@@ -52,32 +56,155 @@ func TestExpiringCacheCreation(t *testing.T) {
5256
func TestExpiringCacheOperation(t *testing.T) {
5357
t.Parallel()
5458

55-
ctx := t.Context()
56-
cache := cache.NewExpiringCache[string]("test")
59+
synctest.Test(t, func(t *testing.T) {
60+
ctx := t.Context()
61+
cache := cache.NewExpiringCache[string]("test")
5762

58-
value, found := cache.Get(ctx, "potato")
63+
value, found := cache.Get(ctx, "potato")
5964

60-
assert.False(t, found)
61-
assert.Empty(t, value)
65+
assert.False(t, found)
66+
assert.Empty(t, value)
6267

63-
cache.Put(ctx, "potato", "carrot", time.Now().Add(1*time.Second))
64-
value, found = cache.Get(ctx, "potato")
68+
cache.Put(ctx, "potato", "carrot", time.Now().Add(1*time.Second))
69+
value, found = cache.Get(ctx, "potato")
6570

66-
assert.True(t, found)
67-
assert.NotEmpty(t, value)
68-
assert.Equal(t, "carrot", value)
71+
assert.True(t, found)
72+
assert.NotEmpty(t, value)
73+
assert.Equal(t, "carrot", value)
74+
})
6975
}
7076

7177
func TestExpiringCacheExpiration(t *testing.T) {
7278
t.Parallel()
7379

80+
synctest.Test(t, func(t *testing.T) {
81+
ctx := t.Context()
82+
cache := cache.NewExpiringCache[string]("test")
83+
84+
cache.Put(ctx, "potato", "carrot", time.Now().Add(time.Second))
85+
86+
// Move the bubble's virtual clock past the expiration so the Get below
87+
// observes the entry as expired without any real wallclock wait.
88+
time.Sleep(2 * time.Second)
89+
90+
value, found := cache.Get(ctx, "potato")
91+
92+
assert.False(t, found)
93+
assert.NotEmpty(t, value)
94+
assert.Equal(t, "carrot", value)
95+
})
96+
}
97+
98+
func TestContextCache(t *testing.T) {
99+
t.Parallel()
100+
74101
ctx := t.Context()
75-
cache := cache.NewExpiringCache[string]("test")
76102

77-
cache.Put(ctx, "potato", "carrot", time.Now().Add(-1*time.Second))
78-
value, found := cache.Get(ctx, "potato")
103+
// Missing entry returns a fresh detached instance.
104+
c := cache.ContextCache[int](ctx, "not-installed")
105+
require.NotNil(t, c)
106+
c.Put(ctx, "k", 7)
79107

80-
assert.False(t, found)
81-
assert.NotEmpty(t, value)
82-
assert.Equal(t, "carrot", value)
108+
// Installed entry round-trips.
109+
installed := cache.NewCache[int]("installed")
110+
ctxWith := context.WithValue(ctx, cache.RunCmdCacheContextKey, installed)
111+
112+
got := cache.ContextCache[int](ctxWith, cache.RunCmdCacheContextKey)
113+
assert.Same(t, installed, got)
114+
}
115+
116+
func TestContextWithCacheInstallsBoth(t *testing.T) {
117+
t.Parallel()
118+
119+
ctx := cache.ContextWithCache(t.Context())
120+
121+
runCmd, ok := ctx.Value(cache.RunCmdCacheContextKey).(*cache.Cache[string])
122+
require.True(t, ok)
123+
require.NotNil(t, runCmd)
124+
125+
repoRoots, ok := ctx.Value(cache.RepoRootCacheContextKey).(*cache.RepoRootCache)
126+
require.True(t, ok)
127+
require.NotNil(t, repoRoots)
128+
assert.Equal(t, 0, repoRoots.Len())
129+
}
130+
131+
func TestRepoRootCacheLookupAndAdd(t *testing.T) {
132+
t.Parallel()
133+
134+
ctx := t.Context()
135+
c := cache.NewRepoRootCache("repo")
136+
137+
// Empty cache misses.
138+
_, ok := c.Lookup(ctx, filepath.FromSlash("/a/b"))
139+
assert.False(t, ok)
140+
141+
outer := filepath.FromSlash("/repo")
142+
inner := filepath.FromSlash("/repo/sub/nested")
143+
144+
// Add deepest-first, then a shallower root, to exercise insertion ordering.
145+
c.Add(ctx, outer)
146+
c.Add(ctx, inner)
147+
// Duplicate Add is a no-op.
148+
c.Add(ctx, outer)
149+
// Empty Add is a no-op.
150+
c.Add(ctx, "")
151+
assert.Equal(t, 2, c.Len())
152+
153+
// Adding a shallower-still root exercises the "no insertAt found" branch
154+
// where the new root is shorter than every existing one and is appended.
155+
shallow := filepath.FromSlash("/r")
156+
c.Add(ctx, shallow)
157+
assert.Equal(t, 3, c.Len())
158+
159+
// Exact-path hit returns the matching root.
160+
got, ok := c.Lookup(ctx, outer)
161+
assert.True(t, ok)
162+
assert.Equal(t, outer, got)
163+
164+
// Descendant of the deeper root prefers it over the shallow one.
165+
got, ok = c.Lookup(ctx, filepath.Join(inner, "x"))
166+
assert.True(t, ok)
167+
assert.Equal(t, inner, got)
168+
169+
// Sibling that prefix-matches lexically but not on a separator boundary
170+
// is not a hit (e.g. /repobar should not match /repo).
171+
_, ok = c.Lookup(ctx, filepath.FromSlash("/repobar/x"))
172+
assert.False(t, ok)
173+
174+
// Path outside any cached root misses entirely.
175+
_, ok = c.Lookup(ctx, filepath.FromSlash("/elsewhere"))
176+
assert.False(t, ok)
177+
}
178+
179+
func TestRepoRootCacheBeginEndResolve(t *testing.T) {
180+
t.Parallel()
181+
182+
c := cache.NewRepoRootCache("repo")
183+
184+
// Round-trip the lock twice to confirm BeginResolve/EndResolve pair up
185+
// (a missing Unlock would deadlock the second BeginResolve). The lock's
186+
// mutual-exclusion semantics are the stdlib's responsibility, not this
187+
// test's.
188+
c.BeginResolve()
189+
c.EndResolve()
190+
191+
c.BeginResolve()
192+
c.EndResolve()
193+
}
194+
195+
func TestContextRepoRootCache(t *testing.T) {
196+
t.Parallel()
197+
198+
ctx := t.Context()
199+
200+
// Missing key returns a fresh detached instance, never nil.
201+
c := cache.ContextRepoRootCache(ctx, "missing")
202+
require.NotNil(t, c)
203+
assert.Equal(t, 0, c.Len())
204+
205+
installed := cache.NewRepoRootCache("installed")
206+
ctxWith := context.WithValue(ctx, cache.RepoRootCacheContextKey, installed)
207+
208+
got := cache.ContextRepoRootCache(ctxWith, cache.RepoRootCacheContextKey)
209+
assert.Same(t, installed, got)
83210
}

0 commit comments

Comments
 (0)