mirror of
https://github.com/helmfile/helmfile.git
synced 2026-09-30 14:01:46 +02:00
* fix: eliminate race condition in rewriteChartDependencies by copying chart before modifying Instead of modifying the original Chart.yaml in-place (which causes race conditions when multiple releases reference the same local chart), copy the chart to a temporary directory and rewrite the copy's dependencies. This eliminates the need for per-chart mutex locks and prevents file corruption when concurrent goroutines process releases sharing the same local chart. Fixes #2502 Signed-off-by: yxxhero <aiopsclub@163.com> * fix: address PR review comments for rewriteChartDependencies - Handle non-NotExist errors from st.fs.Stat to surface permission/IO failures - Reword function doc to clarify temp copy is conditional on rewrite being needed - Assert rewrittenPath vs tempDir based on expectModified in test table Signed-off-by: yxxhero <aiopsclub@163.com> * test: add integration test for issue #2502 race condition with shared local chart Signed-off-by: yxxhero <aiopsclub@163.com> * fix: separate environments and releases with --- in helmfile.yaml Signed-off-by: yxxhero <aiopsclub@163.com> * fix: correct file:// path and remove --skip-deps for dependency build Signed-off-by: yxxhero <aiopsclub@163.com> * fix: correct file:// dependency path (5 levels up to test/integration/) Signed-off-by: yxxhero <aiopsclub@163.com> * fix: remove output validation from race condition test Signed-off-by: yxxhero <aiopsclub@163.com> * fix: assert WriteFile/MkdirTemp/RemoveAll/CopyDir in DefaultFileSystem test Signed-off-by: yxxhero <aiopsclub@163.com> * fix: add strategicMergePatches to trigger chartify in race condition test Signed-off-by: yxxhero <aiopsclub@163.com> * fix: scope test values under raw subchart and align ConfigMap name with strategic merge patches The race condition test values.yaml had templates at the top level instead of scoped under the raw subchart key, causing helm template to produce no output and chartify's ReplaceWithRendered to fail with an empty helmx.1.rendered directory. Also align the ConfigMap name to match the strategicMergePatches target. Signed-off-by: yxxhero <aiopsclub@163.com> --------- Signed-off-by: yxxhero <aiopsclub@163.com>
260 lines
5.5 KiB
Go
260 lines
5.5 KiB
Go
package filesystem
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func NewTestFileSystem() FileSystem {
|
|
replaceffs := FileSystem{
|
|
Stat: func(s string) (os.FileInfo, error) {
|
|
if strings.HasSuffix(s, "existing_file.txt") {
|
|
return fileStat{mode: 0}, nil
|
|
}
|
|
if strings.HasSuffix(s, "existing_dir") {
|
|
return fileStat{mode: fs.ModeDir}, nil
|
|
}
|
|
return nil, os.ErrExist
|
|
},
|
|
Getwd: func() (string, error) {
|
|
return "/test/dir", nil
|
|
},
|
|
EvalSymlinks: func(s string) (string, error) {
|
|
if s == "/test/dir" {
|
|
return "/real/dir", nil
|
|
} else {
|
|
if strings.Contains(s, "missing") {
|
|
return "", os.ErrExist
|
|
}
|
|
return s, nil
|
|
}
|
|
},
|
|
}
|
|
return *FromFileSystem(replaceffs)
|
|
}
|
|
|
|
func TestFs_resolveSymlinks(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
symlinkPath string
|
|
expected string
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "existing file symlink",
|
|
symlinkPath: "../existing_file.txt",
|
|
expected: "/real/existing_file.txt",
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "missing file symlink",
|
|
symlinkPath: "../missing_file.txt",
|
|
expected: "",
|
|
expectedErr: os.ErrExist,
|
|
},
|
|
{
|
|
name: "local existing file symlink",
|
|
symlinkPath: "./existing_file.txt",
|
|
expected: "./existing_file.txt",
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "absolute existing file symlink",
|
|
symlinkPath: "/a/b/c/existing_file.txt",
|
|
expected: "/a/b/c/existing_file.txt",
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "current directory existing file symlink",
|
|
symlinkPath: "existing_file.txt",
|
|
expected: "existing_file.txt",
|
|
expectedErr: nil,
|
|
},
|
|
}
|
|
|
|
ffs := NewTestFileSystem()
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
path, err := ffs.resolveSymlinks(c.symlinkPath)
|
|
if c.expectedErr != nil {
|
|
require.ErrorIs(t, err, c.expectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
assert.Equal(t, c.expected, path)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFs_fileExistsDefault(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
filePath string
|
|
expected bool
|
|
expectedErr error
|
|
}{
|
|
{
|
|
name: "existing file",
|
|
filePath: "existing_file.txt",
|
|
expected: true,
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
name: "missing file",
|
|
filePath: "missing_file.txt",
|
|
expected: false,
|
|
expectedErr: os.ErrExist,
|
|
},
|
|
}
|
|
|
|
ffs := NewTestFileSystem()
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
exists, err := ffs.FileExists(c.filePath)
|
|
if c.expectedErr != nil {
|
|
require.ErrorIs(t, err, c.expectedErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
assert.Equal(t, c.expected, exists)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFs_fileExistsAtDefault(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
filePath string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "existing file",
|
|
filePath: "existing_file.txt",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "missing file",
|
|
filePath: "missing_file.txt",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "existing dir",
|
|
filePath: "existing_dir",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
ffs := NewTestFileSystem()
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
exists := ffs.FileExistsAt(c.filePath)
|
|
assert.Equal(t, c.expected, exists)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFs_directoryExistsDefault(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
dirPath string
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "existing dir",
|
|
dirPath: "existing_dir",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "missing dir",
|
|
dirPath: "missing_dir",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
ffs := NewTestFileSystem()
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
exists := ffs.DirectoryExistsAt(c.dirPath)
|
|
assert.Equal(t, c.expected, exists)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFsTeadFile(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
content []byte
|
|
path string
|
|
wantError string
|
|
}{
|
|
{
|
|
name: "read file",
|
|
content: []byte("hello helmfile"),
|
|
path: "helmfile.yaml",
|
|
},
|
|
{
|
|
name: "read file from stdin",
|
|
content: []byte("hello helmfile"),
|
|
path: "-",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
yamlPath := filepath.Join(dir, c.path)
|
|
|
|
dfs := DefaultFileSystem()
|
|
tmpfile, err := os.Create(yamlPath)
|
|
require.NoErrorf(t, err, "create file %s error: %v", yamlPath, err)
|
|
|
|
_, err = tmpfile.Write(c.content)
|
|
require.NoErrorf(t, err, "write to file %s error: %v", yamlPath, err)
|
|
|
|
readPath := yamlPath
|
|
if c.path == "-" {
|
|
readPath = c.path
|
|
oldOsStdin := os.Stdin
|
|
defer func() { os.Stdin = oldOsStdin }()
|
|
os.Stdin = tmpfile
|
|
}
|
|
_, err = tmpfile.Seek(0, 0)
|
|
require.NoErrorf(t, err, "file %s seek error: %v", yamlPath, err)
|
|
|
|
want, err := dfs.readFile(readPath)
|
|
require.NoErrorf(t, err, "read file %s error: %v", readPath, err)
|
|
require.Equalf(t, string(c.content), string(want), "unexpected error: unexpected=%s, got=%v", string(c.content), string(want))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFs_DefaultBuilder(t *testing.T) {
|
|
ffs := DefaultFileSystem()
|
|
assert.NotNil(t, ffs.ReadFile)
|
|
assert.NotNil(t, ffs.ReadDir)
|
|
assert.NotNil(t, ffs.DeleteFile)
|
|
assert.NotNil(t, ffs.FileExists)
|
|
assert.NotNil(t, ffs.Glob)
|
|
assert.NotNil(t, ffs.FileExistsAt)
|
|
assert.NotNil(t, ffs.DirectoryExistsAt)
|
|
assert.NotNil(t, ffs.Stat)
|
|
assert.NotNil(t, ffs.Getwd)
|
|
assert.NotNil(t, ffs.Chdir)
|
|
assert.NotNil(t, ffs.Abs)
|
|
assert.NotNil(t, ffs.EvalSymlinks)
|
|
assert.NotNil(t, ffs.WriteFile)
|
|
assert.NotNil(t, ffs.MkdirTemp)
|
|
assert.NotNil(t, ffs.RemoveAll)
|
|
assert.NotNil(t, ffs.CopyDir)
|
|
}
|