Allow helmfile statefile to passed by stdin (#520)
* Allow helmfile statefile to passed by stdin Signed-off-by: xiaomudk <xiaomudk@gmail.com>
This commit is contained in:
		
							parent
							
								
									ce7f38ee96
								
							
						
					
					
						commit
						7844145ee3
					
				|  | @ -109,7 +109,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) { | ||||||
| 
 | 
 | ||||||
| func setGlobalOptionsForRootCmd(fs *pflag.FlagSet, globalOptions *config.GlobalOptions) { | 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.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.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.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") | 	fs.StringArrayVar(&globalOptions.StateValuesFile, "state-values-file", nil, "specify state values in a YAML file") | ||||||
|  |  | ||||||
|  | @ -530,7 +530,7 @@ Flags: | ||||||
|       --enable-live-output              Show live output from the Helm binary Stdout/Stderr into Helmfile own Stdout/Stderr. |       --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. |                                         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" |   -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") |   -b, --helm-binary string              Path to the helm binary (default "helm") | ||||||
|   -h, --help                            help for helmfile |   -h, --help                            help for helmfile | ||||||
|   -i, --interactive                     Request confirmation before attempting to modify clusters |   -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 | * Absolute paths are always resolved as absolute paths | ||||||
| * Relative paths referenced *in* the Helmfile manifest itself are relative to that manifest | * 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 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). | For additional context, take a look at [paths examples](paths.md). | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,8 @@ A few rules to clear up this ambiguity: | ||||||
| - Absolute paths are always resolved as absolute paths | - Absolute paths are always resolved as absolute paths | ||||||
| - Relative paths referenced *in* the helmfile manifest itself are relative to that manifest | - 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 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 | ### Examples | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,11 +1,27 @@ | ||||||
| package filesystem | package filesystem | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"io" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
| 	"os" | 	"os" | ||||||
| 	"path/filepath" | 	"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 { | type FileSystem struct { | ||||||
| 	ReadFile          func(string) ([]byte, error) | 	ReadFile          func(string) ([]byte, error) | ||||||
| 	ReadDir           func(string) ([]fs.DirEntry, error) | 	ReadDir           func(string) ([]fs.DirEntry, error) | ||||||
|  | @ -22,7 +38,6 @@ type FileSystem struct { | ||||||
| 
 | 
 | ||||||
| func DefaultFileSystem() *FileSystem { | func DefaultFileSystem() *FileSystem { | ||||||
| 	dfs := FileSystem{ | 	dfs := FileSystem{ | ||||||
| 		ReadFile:   os.ReadFile, |  | ||||||
| 		ReadDir:    os.ReadDir, | 		ReadDir:    os.ReadDir, | ||||||
| 		DeleteFile: os.Remove, | 		DeleteFile: os.Remove, | ||||||
| 		Stat:       os.Stat, | 		Stat:       os.Stat, | ||||||
|  | @ -32,6 +47,8 @@ func DefaultFileSystem() *FileSystem { | ||||||
| 		Abs:        filepath.Abs, | 		Abs:        filepath.Abs, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	dfs.Stat = dfs.stat | ||||||
|  | 	dfs.ReadFile = dfs.readFile | ||||||
| 	dfs.FileExistsAt = dfs.fileExistsAtDefault | 	dfs.FileExistsAt = dfs.fileExistsAtDefault | ||||||
| 	dfs.DirectoryExistsAt = dfs.directoryExistsDefault | 	dfs.DirectoryExistsAt = dfs.directoryExistsDefault | ||||||
| 	dfs.FileExists = dfs.fileExistsDefault | 	dfs.FileExists = dfs.fileExistsDefault | ||||||
|  | @ -78,6 +95,20 @@ func FromFileSystem(params FileSystem) *FileSystem { | ||||||
| 	return dfs | 	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 { | func (filesystem *FileSystem) fileExistsAtDefault(path string) bool { | ||||||
| 	fileInfo, err := filesystem.Stat(path) | 	fileInfo, err := filesystem.Stat(path) | ||||||
| 	return err == nil && fileInfo.Mode().IsRegular() | 	return err == nil && fileInfo.Mode().IsRegular() | ||||||
|  |  | ||||||
|  | @ -4,30 +4,19 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"io/fs" | 	"io/fs" | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"path" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"testing" | 	"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 { | func NewTestFileSystem() FileSystem { | ||||||
| 	replaceffs := FileSystem{ | 	replaceffs := FileSystem{ | ||||||
| 		Stat: func(s string) (os.FileInfo, error) { | 		Stat: func(s string) (os.FileInfo, error) { | ||||||
| 			if strings.HasPrefix(s, "existing_file") { | 			if strings.HasPrefix(s, "existing_file") { | ||||||
| 				return TestFileInfo{mode: 0}, nil | 				return fileStat{mode: 0}, nil | ||||||
| 			} | 			} | ||||||
| 			if strings.HasPrefix(s, "existing_dir") { | 			if strings.HasPrefix(s, "existing_dir") { | ||||||
| 				return TestFileInfo{mode: fs.ModeDir}, nil | 				return fileStat{mode: fs.ModeDir}, nil | ||||||
| 			} | 			} | ||||||
| 			return nil, errors.New("Error") | 			return nil, errors.New("Error") | ||||||
| 		}, | 		}, | ||||||
|  | @ -46,6 +35,12 @@ func TestFs_fileExistsDefault(t *testing.T) { | ||||||
| 	if exists { | 	if exists { | ||||||
| 		t.Errorf("Not expected file %s, found", "non_existing_file.txt") | 		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) { | func TestFs_fileExistsAtDefault(t *testing.T) { | ||||||
|  | @ -65,6 +60,12 @@ func TestFs_fileExistsAtDefault(t *testing.T) { | ||||||
| 	if exists { | 	if exists { | ||||||
| 		t.Errorf("Not expected file %s, found", "existing_dir") | 		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) { | 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) { | func TestFs_DefaultBuilder(t *testing.T) { | ||||||
| 	ffs := DefaultFileSystem() | 	ffs := DefaultFileSystem() | ||||||
| 	if ffs.ReadFile == nil || | 	if ffs.ReadFile == nil || | ||||||
|  |  | ||||||
|  | @ -27,6 +27,23 @@ for output in $(ls -d ${dir}/tmp/*); do | ||||||
|     done |     done | ||||||
| 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" | 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" | 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=$? | ||||||
| [ ${code} -eq 0 ] || fail "unexpected exit code returned by helmfile apply: want 0, got ${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" | info "Locking dependencies" | ||||||
| ${helmfile} -f ${dir}/happypath.yaml deps | ${helmfile} -f ${dir}/happypath.yaml deps | ||||||
| code=$? | code=$? | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue