Skip to content

Commit 8df1251

Browse files
committed
fix: Fixing symlink issue on pre-existing provider cache entries
1 parent d48f6ca commit 8df1251

4 files changed

Lines changed: 79 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
version: "v1.0.4"
3+
category: "bug-fixes"
4+
---
5+
6+
#### Fixed `failed to create directory ...: file exists` from the provider cache server
7+
8+
If a previous run had cached a provider by symlinking `~/.terraform.d/plugins/<provider>` into Terragrunt's own provider cache, and that user plugin directory was later moved or deleted, the symlink was left dangling. The next run failed with `failed to create directory ...: file exists` and refused to cache the provider.
9+
10+
Terragrunt now removes a dangling symlink at the cache path on the next run and proceeds to download the provider. A non-symlink at that path is left in place and surfaced as an error.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package services
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
// UnexpectedProviderCachePathError is returned when something other than a
9+
// Terragrunt-managed symlink occupies a provider's package path inside the
10+
// provider cache directory. Terragrunt only ever writes a directory (from a
11+
// fresh download) or a symlink (pointing at the user plugins directory) to
12+
// this path, so anything else is treated as user content and reported rather
13+
// than removed.
14+
type UnexpectedProviderCachePathError struct {
15+
Path string
16+
Mode os.FileMode
17+
}
18+
19+
func (e *UnexpectedProviderCachePathError) Error() string {
20+
return fmt.Sprintf("unexpected non-symlink at provider package path %q (mode %s); refusing to remove", e.Path, e.Mode)
21+
}

internal/tf/cache/services/provider_cache.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,15 @@ func (cache *ProviderCache) warmUp(ctx context.Context) error {
321321
return nil
322322
}
323323

324+
// vfs.FileExists uses Stat, which follows symlinks. A dangling symlink
325+
// (e.g. left over from a previous run that pointed at ~/.terraform.d/plugins
326+
// before that directory was moved or deleted) reports as non-existent here
327+
// but still trips MkdirAll downstream with "file exists". Remove only the
328+
// symlink itself before the download or user-plugin symlink path runs.
329+
if err := cache.removeStaleSymlink(fs); err != nil {
330+
return err
331+
}
332+
324333
if err := fs.MkdirAll(filepath.Dir(cache.packageDir), os.ModePerm); err != nil {
325334
return errors.New(err)
326335
}
@@ -411,6 +420,34 @@ func (cache *ProviderCache) newRequest(ctx context.Context, url string) (*http.R
411420
return req, nil
412421
}
413422

423+
// removeStaleSymlink removes a dangling symlink at cache.packageDir, if one is
424+
// present. The caller has already confirmed via vfs.FileExists (Stat-based)
425+
// that no real cached package is here. The only artifact this method deletes
426+
// is a symlink whose target has gone missing, typically because the user
427+
// moved or removed ~/.terraform.d/plugins after a prior run. A regular file
428+
// or directory at this path is unexpected and surfaces as an error rather
429+
// than a deletion.
430+
func (cache *ProviderCache) removeStaleSymlink(fs vfs.FS) error {
431+
info, err := vfs.Lstat(fs, cache.packageDir)
432+
if err != nil {
433+
if os.IsNotExist(err) {
434+
return nil
435+
}
436+
437+
return errors.Errorf("failed to inspect provider package path %q: %w", cache.packageDir, err)
438+
}
439+
440+
if info.Mode()&os.ModeSymlink == 0 {
441+
return &UnexpectedProviderCachePathError{Path: cache.packageDir, Mode: info.Mode()}
442+
}
443+
444+
if err := fs.Remove(cache.packageDir); err != nil {
445+
return errors.Errorf("failed to clear stale provider package symlink %q: %w", cache.packageDir, err)
446+
}
447+
448+
return nil
449+
}
450+
414451
func (cache *ProviderCache) removeArchive() error {
415452
fs := cache.ProviderService.FS()
416453

internal/vfs/vfs.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ func FileExists(vfs FS, path string) (bool, error) {
9595
return false, err
9696
}
9797

98+
// Lstat returns the FileInfo for the named path without following symlinks.
99+
// Filesystems that do not implement afero.Lstater fall back to Stat.
100+
func Lstat(fsys FS, path string) (os.FileInfo, error) {
101+
if lstater, ok := fsys.(afero.Lstater); ok {
102+
info, _, err := lstater.LstatIfPossible(path)
103+
return info, err
104+
}
105+
106+
return fsys.Stat(path)
107+
}
108+
98109
// WriteFile writes data to a file on the given filesystem.
99110
func WriteFile(fs FS, filename string, data []byte, perm os.FileMode) error {
100111
dir := filepath.Dir(filename)

0 commit comments

Comments
 (0)