package app import ( "fmt" "github.com/roboll/helmfile/helmexec" "github.com/roboll/helmfile/state" "go.uber.org/zap" "io/ioutil" "log" "os" "os/signal" "path/filepath" "sort" "syscall" ) type App struct { KubeContext string Logger *zap.SugaredLogger Reverse bool Env string Namespace string Selectors []string readFile func(string) ([]byte, error) glob func(string) ([]string, error) abs func(string) (string, error) fileExistsAt func(string) bool directoryExistsAt func(string) bool getwd func() (string, error) chdir func(string) error } func Init(app *App) *App { app.readFile = ioutil.ReadFile app.glob = filepath.Glob app.abs = filepath.Abs app.getwd = os.Getwd app.chdir = os.Chdir app.fileExistsAt = fileExistsAt app.directoryExistsAt = directoryExistsAt return app } func (a *App) within(dir string, do func() error) error { if dir == "." { return do() } prev, err := a.getwd() if err != nil { return fmt.Errorf("failed getting current working direcotyr: %v", err) } absDir, err := a.abs(dir) if err != nil { return err } a.Logger.Debugf("changing working directory to \"%s\"", absDir) if err := a.chdir(absDir); err != nil { return fmt.Errorf("failed changing working directory to \"%s\": %v", absDir, err) } appErr := do() a.Logger.Debugf("changing working directory back to \"%s\"", prev) if chdirBackErr := a.chdir(prev); chdirBackErr != nil { if appErr != nil { a.Logger.Warnf("%v", appErr) } return fmt.Errorf("failed chaging working directory back to \"%s\": %v", prev, chdirBackErr) } return appErr } func (a *App) visitStateFiles(fileOrDir string, do func(string) error) error { desiredStateFiles, err := a.findDesiredStateFiles(fileOrDir) if err != nil { return err } for _, relPath := range desiredStateFiles { var file string var dir string if a.directoryExistsAt(relPath) { file = relPath dir = relPath } else { file = filepath.Base(relPath) dir = filepath.Dir(relPath) } a.Logger.Debugf("processing file \"%s\" in directory \"%s\"", file, dir) err := a.within(dir, func() error { return do(file) }) if err != nil { return err } } return nil } func (a *App) VisitDesiredStates(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) error { noMatchInHelmfiles := true err := a.visitStateFiles(fileOrDir, func(f string) error { content, err := a.readFile(f) if err != nil { return err } // render template, in two runs r := &twoPassRenderer{ reader: a.readFile, env: a.Env, namespace: a.Namespace, filename: f, logger: a.Logger, abs: a.abs, } yamlBuf, err := r.renderTemplate(content) if err != nil { return fmt.Errorf("error during %s parsing: %v", f, err) } st, err := a.loadDesiredStateFromYaml( yamlBuf.Bytes(), f, a.Namespace, a.Env, ) helm := helmexec.New(a.Logger, a.KubeContext) if err != nil { switch stateLoadErr := err.(type) { // Addresses https://github.com/roboll/helmfile/issues/279 case *state.StateLoadError: switch stateLoadErr.Cause.(type) { case *state.UndefinedEnvError: return nil default: return err } default: return err } } errs := []error{} if len(st.Helmfiles) > 0 { noMatchInSubHelmfiles := true for _, m := range st.Helmfiles { if err := a.VisitDesiredStates(m, converge); err != nil { switch err.(type) { case *NoMatchingHelmfileError: default: return fmt.Errorf("failed processing %s: %v", m, err) } } else { noMatchInSubHelmfiles = false } } noMatchInHelmfiles = noMatchInHelmfiles && noMatchInSubHelmfiles } else { var err error st, err = st.ExecuteTemplates() if err != nil { return fmt.Errorf("failed executing release templates in \"%s\": %v", f, err) } var processed bool processed, errs = converge(st, helm) noMatchInHelmfiles = noMatchInHelmfiles && !processed } return clean(st, errs) }) if err != nil { return err } if noMatchInHelmfiles { return &NoMatchingHelmfileError{selectors: a.Selectors, env: a.Env} } return nil } func (a *App) VisitDesiredStatesWithReleasesFiltered(fileOrDir string, converge func(*state.HelmState, helmexec.Interface) []error) error { selectors := a.Selectors err := a.VisitDesiredStates(fileOrDir, func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { if len(selectors) > 0 { err := st.FilterReleases(selectors) if err != nil { return false, []error{err} } } releaseNameCounts := map[string]int{} for _, r := range st.Releases { releaseNameCounts[r.Name]++ } for name, c := range releaseNameCounts { if c > 1 { return false, []error{fmt.Errorf("duplicate release \"%s\" found: there were %d releases named \"%s\" matching specified selector", name, c, name)} } } errs := converge(st, helm) processed := len(st.Releases) != 0 && len(errs) == 0 return processed, errs }) if err != nil { return err } return nil } func (a *App) findStateFilesInAbsPaths(specifiedPath string) ([]string, error) { rels, err := a.findDesiredStateFiles(specifiedPath) if err != nil { return rels, err } files := make([]string, len(rels)) for i := range rels { files[i], err = filepath.Abs(rels[i]) if err != nil { return []string{}, err } } return files, nil } func (a *App) findDesiredStateFiles(specifiedPath string) ([]string, error) { var helmfileDir string if specifiedPath != "" { if a.fileExistsAt(specifiedPath) { return []string{specifiedPath}, nil } else if a.directoryExistsAt(specifiedPath) { helmfileDir = specifiedPath } else { return []string{}, fmt.Errorf("specified state file %s is not found", specifiedPath) } } else { var defaultFile string if a.fileExistsAt(DefaultHelmfile) { defaultFile = DefaultHelmfile } else if a.fileExistsAt(DeprecatedHelmfile) { log.Printf( "warn: %s is being loaded: %s is deprecated in favor of %s. See https://github.com/roboll/helmfile/issues/25 for more information", DeprecatedHelmfile, DeprecatedHelmfile, DefaultHelmfile, ) defaultFile = DeprecatedHelmfile } if a.directoryExistsAt(DefaultHelmfileDirectory) { if defaultFile != "" { return []string{}, fmt.Errorf("configuration conlict error: you can have either %s or %s, but not both", defaultFile, DefaultHelmfileDirectory) } helmfileDir = DefaultHelmfileDirectory } else if defaultFile != "" { return []string{defaultFile}, nil } else { return []string{}, fmt.Errorf("no state file found. It must be named %s/*.yaml, %s, or %s, or otherwise specified with the --file flag", DefaultHelmfileDirectory, DefaultHelmfile, DeprecatedHelmfile) } } files, err := a.glob(filepath.Join(helmfileDir, "*.yaml")) if err != nil { return []string{}, err } sort.Slice(files, func(i, j int) bool { return files[i] < files[j] }) return files, nil } func fileExistsAt(path string) bool { fileInfo, err := os.Stat(path) return err == nil && fileInfo.Mode().IsRegular() } func directoryExistsAt(path string) bool { fileInfo, err := os.Stat(path) return err == nil && fileInfo.Mode().IsDir() } func (a *App) loadDesiredStateFromYaml(yaml []byte, file string, namespace string, env string) (*state.HelmState, error) { c := state.NewCreator(a.Logger, a.readFile, a.abs) st, err := c.CreateFromYaml(yaml, file, env) if err != nil { return nil, err } helmfiles := []string{} for _, globPattern := range st.Helmfiles { helmfileRelativePattern := st.JoinBase(globPattern) matches, err := a.glob(helmfileRelativePattern) if err != nil { return nil, fmt.Errorf("failed processing %s: %v", globPattern, err) } sort.Strings(matches) helmfiles = append(helmfiles, matches...) } st.Helmfiles = helmfiles if a.Reverse { rev := func(i, j int) bool { return j < i } sort.Slice(st.Releases, rev) sort.Slice(st.Helmfiles, rev) } if a.KubeContext != "" { if st.HelmDefaults.KubeContext != "" { log.Printf("err: Cannot use option --kube-context and set attribute helmDefaults.kubeContext.") os.Exit(1) } st.HelmDefaults.KubeContext = a.KubeContext } if namespace != "" { if st.Namespace != "" { log.Printf("err: Cannot use option --namespace and set attribute namespace.") os.Exit(1) } st.Namespace = namespace } sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigs errs := []error{fmt.Errorf("Received [%s] to shutdown ", sig)} _ = clean(st, errs) // See http://tldp.org/LDP/abs/html/exitcodes.html switch sig { case syscall.SIGINT: os.Exit(130) case syscall.SIGTERM: os.Exit(143) } }() return st, nil } func clean(st *state.HelmState, errs []error) error { if errs == nil { errs = []error{} } cleanErrs := st.Clean() if cleanErrs != nil { errs = append(errs, cleanErrs...) } if errs != nil && len(errs) > 0 { for _, err := range errs { switch e := err.(type) { case *state.ReleaseError: fmt.Printf("err: release \"%s\" in \"%s\" failed: %v\n", e.Name, st.FilePath, e) default: fmt.Printf("err: %v\n", e) } } return errs[0] } return nil }