diff --git a/cmd/root.go b/cmd/root.go index 0343d395..a7d2d695 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -109,7 +109,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) { func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalOptions) { fs.StringVarP(&globalOptions.HelmBinary, "helm-binary", "b", app.DefaultHelmBinary, "Path to the helm binary") - fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference") + fs.StringVarP(&globalOptions.File, "file", "f", "", "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference. Specify - to load the config from the standard input.") fs.StringVarP(&globalOptions.Environment, "environment", "e", "", `specify the environment name. defaults to "default"`) fs.StringArrayVar(&globalOptions.StateValuesSet, "state-values-set", nil, "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") fs.StringArrayVar(&globalOptions.StateValuesFile, "state-values-file", nil, "specify state values in a YAML file") diff --git a/docs/index.md b/docs/index.md index 35fb0ea8..e0aeb243 100644 --- a/docs/index.md +++ b/docs/index.md @@ -530,7 +530,7 @@ Flags: --enable-live-output Show live output from the Helm binary Stdout/Stderr into Helmfile own Stdout/Stderr. It only applies for the Helm CLI commands, Stdout/Stderr for Hooks are still displayed only when it's execution finishes. -e, --environment string specify the environment name. defaults to "default" - -f, --file helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference + -f, --file helmfile.yaml load config from file or directory. defaults to helmfile.yaml or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference. Specify - to load the config from the standard input. -b, --helm-binary string Path to the helm binary (default "helm") -h, --help help for helmfile -i, --interactive Request confirmation before attempting to modify clusters @@ -655,6 +655,7 @@ A few rules to clear up this ambiguity: * Absolute paths are always resolved as absolute paths * Relative paths referenced *in* the Helmfile manifest itself are relative to that manifest * Relative paths referenced on the command line are relative to the current working directory the user is in +- Relative paths referenced from within the helmfile loaded from the standard input using `helmfile -f -` are relative to the current working directory For additional context, take a look at [paths examples](paths.md). diff --git a/docs/paths.md b/docs/paths.md index 9d383d84..e0e4b62a 100644 --- a/docs/paths.md +++ b/docs/paths.md @@ -7,6 +7,8 @@ A few rules to clear up this ambiguity: - Absolute paths are always resolved as absolute paths - Relative paths referenced *in* the helmfile manifest itself are relative to that manifest - Relative paths referenced on the command line are relative to the current working directory the user is in +- Relative paths referenced from within the helmfile loaded from the standard input using `helmfile -f -` are relative to the current working directory + ### Examples diff --git a/pkg/filesystem/fs.go b/pkg/filesystem/fs.go index cc003954..2f7d8e55 100644 --- a/pkg/filesystem/fs.go +++ b/pkg/filesystem/fs.go @@ -1,11 +1,27 @@ package filesystem import ( + "io" "io/fs" "os" "path/filepath" + "time" ) +type fileStat struct { + name string + size int64 + mode fs.FileMode + modTime time.Time +} + +func (fs fileStat) Name() string { return fs.name } +func (fs fileStat) Size() int64 { return fs.size } +func (fs fileStat) Mode() fs.FileMode { return fs.mode } +func (fs fileStat) ModTime() time.Time { return fs.modTime } +func (fs fileStat) IsDir() bool { return fs.mode.IsDir() } +func (fs fileStat) Sys() any { return nil } + type FileSystem struct { ReadFile func(string) ([]byte, error) ReadDir func(string) ([]fs.DirEntry, error) @@ -22,7 +38,6 @@ type FileSystem struct { func DefaultFileSystem() *FileSystem { dfs := FileSystem{ - ReadFile: os.ReadFile, ReadDir: os.ReadDir, DeleteFile: os.Remove, Stat: os.Stat, @@ -32,6 +47,8 @@ func DefaultFileSystem() *FileSystem { Abs: filepath.Abs, } + dfs.Stat = dfs.stat + dfs.ReadFile = dfs.readFile dfs.FileExistsAt = dfs.fileExistsAtDefault dfs.DirectoryExistsAt = dfs.directoryExistsDefault dfs.FileExists = dfs.fileExistsDefault @@ -78,6 +95,20 @@ func FromFileSystem(params FileSystem) *FileSystem { return dfs } +func (filesystem *FileSystem) stat(name string) (os.FileInfo, error) { + if name == "-" { + return fileStat{mode: 0}, nil + } + return os.Stat(name) +} + +func (filesystem *FileSystem) readFile(name string) ([]byte, error) { + if name == "-" { + return io.ReadAll(os.Stdin) + } + return os.ReadFile(name) +} + func (filesystem *FileSystem) fileExistsAtDefault(path string) bool { fileInfo, err := filesystem.Stat(path) return err == nil && fileInfo.Mode().IsRegular() diff --git a/pkg/filesystem/fs_test.go b/pkg/filesystem/fs_test.go index 3193dc95..2431df90 100644 --- a/pkg/filesystem/fs_test.go +++ b/pkg/filesystem/fs_test.go @@ -4,30 +4,19 @@ import ( "errors" "io/fs" "os" + "path" "strings" "testing" - "time" ) -type TestFileInfo struct { - mode fs.FileMode -} - -func (tfi TestFileInfo) Name() string { return "" } -func (tfi TestFileInfo) Size() int64 { return 0 } -func (tfi TestFileInfo) Mode() fs.FileMode { return tfi.mode } -func (tfi TestFileInfo) ModTime() time.Time { return time.Time{} } -func (tfi TestFileInfo) IsDir() bool { return tfi.mode.IsDir() } -func (tfi TestFileInfo) Sys() any { return nil } - func NewTestFileSystem() FileSystem { replaceffs := FileSystem{ Stat: func(s string) (os.FileInfo, error) { if strings.HasPrefix(s, "existing_file") { - return TestFileInfo{mode: 0}, nil + return fileStat{mode: 0}, nil } if strings.HasPrefix(s, "existing_dir") { - return TestFileInfo{mode: fs.ModeDir}, nil + return fileStat{mode: fs.ModeDir}, nil } return nil, errors.New("Error") }, @@ -46,6 +35,12 @@ func TestFs_fileExistsDefault(t *testing.T) { if exists { t.Errorf("Not expected file %s, found", "non_existing_file.txt") } + + dfs := DefaultFileSystem() + exists, _ = dfs.FileExists("-") + if !exists { + t.Errorf("Not expected file %s, not found", "-") + } } func TestFs_fileExistsAtDefault(t *testing.T) { @@ -65,6 +60,12 @@ func TestFs_fileExistsAtDefault(t *testing.T) { if exists { t.Errorf("Not expected file %s, found", "existing_dir") } + + dfs := DefaultFileSystem() + exists = dfs.FileExistsAt("-") + if !exists { + t.Errorf("Not expected file %s, not found", "-") + } } func TestFs_directoryExistsDefault(t *testing.T) { @@ -80,6 +81,61 @@ func TestFs_directoryExistsDefault(t *testing.T) { } } +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 := path.Join(dir, c.path) + + dfs := DefaultFileSystem() + tmpfile, err := os.Create(yamlPath) + if err != nil { + t.Errorf("create file %s error: %v", yamlPath, err) + } + _, err = tmpfile.Write(c.content) + if err != nil { + t.Errorf(" 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 + } + if _, err = tmpfile.Seek(0, 0); err != nil { + t.Errorf("file %s seek error: %v", yamlPath, err) + } + + want, err := dfs.readFile(readPath) + if err != nil { + t.Errorf("read file %s error: %v", readPath, err) + } else { + if string(c.content) != string(want) { + t.Errorf("nexpected error: unexpected=%s, got=%v", string(c.content), string(want)) + } + } + }) + } +} + func TestFs_DefaultBuilder(t *testing.T) { ffs := DefaultFileSystem() if ffs.ReadFile == nil || diff --git a/test/integration/test-cases/happypath.sh b/test/integration/test-cases/happypath.sh index 707cc799..50b51bb2 100644 --- a/test/integration/test-cases/happypath.sh +++ b/test/integration/test-cases/happypath.sh @@ -27,6 +27,23 @@ for output in $(ls -d ${dir}/tmp/*); do done done +info "Templating ${dir}/happypath.yaml from stdin" +pushd ${dir} +rm -rf ./tmp +cat ./happypath.yaml | ../../${helmfile} -f - --debug template --output-dir tmp +code=$? +[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile template: ${code}" +for output in $(ls -d ./tmp/*); do + # e.g. test/integration/tmp/happypath-877c0dd4-helmx/helmx + for release_dir in $(ls -d ${output}/*); do + release_name=$(basename ${release_dir}) + golden_dir=./templates-golden/v${helm_major_version}/${release_name} + info "Comparing template output ${release_dir}/templates with ${golden_dir}" + ../../diff-yamls ${golden_dir} ${release_dir}/templates || fail "unexpected diff in template result for ${release_name}" + done +done +popd + info "Applying ${dir}/happypath.yaml" bash -c "${helmfile} -f ${dir}/happypath.yaml apply --detailed-exitcode; code="'$?'"; echo Code: "'$code'"; [ "'${code}'" -eq 2 ]" || fail "unexpected exit code returned by helmfile apply" @@ -41,6 +58,13 @@ ${helmfile} -f ${dir}/happypath.yaml apply --detailed-exitcode code=$? [ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: want 0, got ${code}" +info "Applying ${dir}/happypath.yaml from stdin" +pushd ${dir} +cat ./happypath.yaml | ../../${helmfile} -f - apply --detailed-exitcode +code=$? +[ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: want 0, got ${code}" +popd + info "Locking dependencies" ${helmfile} -f ${dir}/happypath.yaml deps code=$?