package state import ( "errors" "fmt" "net/url" "path/filepath" "sort" "strings" "go.uber.org/zap" "github.com/helmfile/helmfile/pkg/filesystem" "github.com/helmfile/helmfile/pkg/remote" ) type Storage struct { logger *zap.SugaredLogger FilePath string basePath string fs *filesystem.FileSystem } func NewStorage(forFile string, logger *zap.SugaredLogger, fs *filesystem.FileSystem) *Storage { return &Storage{ FilePath: forFile, basePath: filepath.Dir(forFile), logger: logger, fs: fs, } } type resolveFileConfig struct { IgnoreMissingGitBranch bool } type resolveFileOption func(*resolveFileConfig) func ignoreMissingGitBranch(v bool) func(c *resolveFileConfig) { return func(c *resolveFileConfig) { c.IgnoreMissingGitBranch = v } } func (st *Storage) resolveFile(missingFileHandler *string, tpe, path string, opts ...resolveFileOption) ([]string, bool, error) { title := fmt.Sprintf("%s file", tpe) var ( files []string err error conf resolveFileConfig ) for _, o := range opts { o(&conf) } if remote.IsRemote(path) { r := remote.NewRemote(st.logger, "", st.fs) // Named fetchErr, not err: err is declared in the outer scope above and // checked again after this if-block. Reusing that name here would shadow // it with a new, block-local variable (fetchedFilePath is new, so ":=" // can't reuse the outer err), silently discarding any fetch error that // isn't returned or explicitly ignored below. fetchedFilePath, fetchErr := r.Fetch(path, "values") if fetchErr != nil { // https://github.com/helmfile/helmfile/issues/392 if conf.IgnoreMissingGitBranch && strings.Contains(fetchErr.Error(), "' did not match any file(s) known to git") { st.logger.Debugf("Ignored missing git branch error: %v", fetchErr) } else { return nil, false, fetchErr } } switch { case fetchedFilePath == "": // Fetch failed and the failure was ignored above (ignoreMissingGitBranch). // Leave files empty and let the missing file handler below decide. case st.fs.FileExistsAt(fetchedFilePath): // A literal, existing file. Checked before glob-expanding so that a // file name which happens to contain "[" or "?" (valid in // filepath.Match patterns but also valid in plain file names) still // resolves to itself when it exists. files = []string{fetchedFilePath} case remote.HasGlobPattern(path): // Fetch joins the "@" selector onto the local cache directory // verbatim (see Remote.Fetch), so a wildcard selector is expanded // here, against the fetched directory, using the same glob syntax as // local values files (st.ExpandPaths / filepath.Match). // // st.ExpandPaths itself is not reused: its normalizePath would // incorrectly prefix the helmfile's basePath onto this // already-absolute cache path whenever remote.CacheDir() falls back // to the relative ".helmfile" directory. matches, globErr := st.fs.Glob(fetchedFilePath) if globErr != nil { // filepath.Glob's only documented error is ErrBadPattern (e.g. an // unclosed "["). Before wildcard support, a selector containing "[" // was checked with FileExistsAt and simply treated as missing if it // didn't exist, so a malformed pattern should fall back to the same // missingFileHandler-driven "no matches" handling below rather than // becoming an unconditional hard error, which would be a regression // for existing Info/Warn/Debug users referencing such a file. if !errors.Is(globErr, filepath.ErrBadPattern) { return nil, false, fmt.Errorf("failed processing %s: %v", path, globErr) } st.logger.Debugf("Treating invalid glob pattern as no match for %s: %v", path, globErr) } sort.Strings(matches) for _, m := range matches { // Keep the same "regular files only" contract as the non-glob // case above: a glob can also match directories. if st.fs.FileExistsAt(m) { files = append(files, m) } } } } else { files, err = st.ExpandPaths(path) } if err != nil { return nil, false, err } var handlerId string if missingFileHandler != nil { handlerId = *missingFileHandler } else { handlerId = MissingFileHandlerError } if len(files) == 0 { switch handlerId { case MissingFileHandlerError: return nil, false, fmt.Errorf("%s matching \"%s\" does not exist in \"%s\"", title, path, st.basePath) case MissingFileHandlerWarn: st.logger.Warnf("skipping missing %s matching \"%s\"", title, path) return nil, true, nil case MissingFileHandlerInfo: st.logger.Infof("skipping missing %s matching \"%s\"", title, path) return nil, true, nil case MissingFileHandlerDebug: st.logger.Debugf("skipping missing %s matching \"%s\"", title, path) return nil, true, nil default: available := []string{ MissingFileHandlerError, MissingFileHandlerWarn, MissingFileHandlerInfo, MissingFileHandlerDebug, } return nil, false, fmt.Errorf("invalid missing file handler \"%s\" while processing \"%s\" in \"%s\": it must be one of %s", handlerId, path, st.FilePath, available) } } return files, false, nil } func (st *Storage) ExpandPaths(globPattern string) ([]string, error) { result := []string{} absPathPattern := st.normalizePath(globPattern) matches, err := st.fs.Glob(absPathPattern) if err != nil { return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) } sort.Strings(matches) result = append(result, matches...) return result, nil } // normalizes relative path to absolute one func (st *Storage) normalizePath(path string) string { u, _ := url.Parse(path) if u != nil && (u.Scheme != "" || filepath.IsAbs(path)) { return path } // Avoid double-prefixing when the path already starts with basePath. // This can happen when normalizePath is called multiple times on the same path // (e.g. once in generateVanillaValuesFiles and again in resolveFile/ExpandPaths). if st.basePath != "" && st.basePath != "." && strings.HasPrefix(path, st.basePath+string(filepath.Separator)) { return path } return st.JoinBase(path) } // JoinBase returns an absolute path in the form basePath/relative // Helm's setFiles command does not support unescaped filepath separators (\) on Windows. // Instead, it requires double backslashes (\\) as filepath separators. // See https://github.com/helm/helm/issues/9537 func (st *Storage) JoinBase(relPath string) string { path := filepath.Join(st.basePath, relPath) return path } func (st *Storage) normalizeSetFilePath(path, goos string) string { normalizedPath := st.normalizePath(path) if goos == "windows" { return strings.ReplaceAll(normalizedPath, "\\", "\\\\") } return normalizedPath }