387 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			387 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
| 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
 | |
| }
 |