feat: `apply` run `sync` against only affected releases (#291)
Enhance the `diff` functionality to be able to return affected releases that has any changes, so that the succeeding `sync` can be run against only the affected releases. This provides us extra idempotency. Resolves #277
This commit is contained in:
parent
60843cc224
commit
f1ac50bf44
70
main.go
70
main.go
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/urfave/cli"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -183,7 +184,8 @@ func main() {
|
|||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
|
||||
return executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets"))
|
||||
_, errs := executeDiffCommand(c, state, helm, c.Bool("detailed-exitcode"), c.Bool("suppress-secrets"))
|
||||
return errs
|
||||
})
|
||||
},
|
||||
},
|
||||
|
|
@ -297,40 +299,56 @@ func main() {
|
|||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return findAndIterateOverDesiredStatesUsingFlags(c, func(state *state.HelmState, helm helmexec.Interface) []error {
|
||||
return findAndIterateOverDesiredStatesUsingFlags(c, func(st *state.HelmState, helm helmexec.Interface) []error {
|
||||
if !c.Bool("skip-repo-update") {
|
||||
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
|
||||
if errs := st.SyncRepos(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
}
|
||||
if errs := state.UpdateDeps(helm); errs != nil && len(errs) > 0 {
|
||||
if errs := st.UpdateDeps(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
errs := executeDiffCommand(c, state, helm, true, c.Bool("suppress-secrets"))
|
||||
releases, errs := executeDiffCommand(c, st, helm, true, c.Bool("suppress-secrets"))
|
||||
|
||||
noError := true
|
||||
for _, e := range errs {
|
||||
switch err := e.(type) {
|
||||
case *state.DiffError:
|
||||
noError = noError && err.Code == 2
|
||||
default:
|
||||
noError = false
|
||||
}
|
||||
}
|
||||
|
||||
// sync only when there are changes
|
||||
if len(errs) > 0 {
|
||||
allErrsIndicateChanges := true
|
||||
for _, err := range errs {
|
||||
switch e := err.(type) {
|
||||
case *exec.ExitError:
|
||||
status := e.Sys().(syscall.WaitStatus)
|
||||
// `helm diff --detailed-exitcode` returns 2 when there are changes
|
||||
allErrsIndicateChanges = allErrsIndicateChanges && status.ExitStatus() == 2
|
||||
default:
|
||||
allErrsIndicateChanges = false
|
||||
}
|
||||
if noError {
|
||||
if len(releases) == 0 {
|
||||
// TODO better way to get the logger
|
||||
logger := c.App.Metadata["logger"].(*zap.SugaredLogger)
|
||||
logger.Infof("")
|
||||
logger.Infof("No affected releases")
|
||||
} else {
|
||||
names := make([]string, len(releases))
|
||||
for i, r := range releases {
|
||||
names[i] = fmt.Sprintf(" %s (%s)", r.Name, r.Chart)
|
||||
}
|
||||
|
||||
msg := `Do you really want to apply?
|
||||
msg := fmt.Sprintf(`Affected releases are:
|
||||
%s
|
||||
|
||||
Do you really want to apply?
|
||||
Helmfile will apply all your changes, as shown above.
|
||||
|
||||
`
|
||||
if allErrsIndicateChanges {
|
||||
`, strings.Join(names, "\n"))
|
||||
autoApprove := c.Bool("auto-approve")
|
||||
if autoApprove || !autoApprove && askForConfirmation(msg) {
|
||||
return executeSyncCommand(c, state, helm)
|
||||
rs := make([]state.ReleaseSpec, len(releases))
|
||||
for i, r := range releases {
|
||||
rs[i] = *r
|
||||
}
|
||||
st.Releases = rs
|
||||
return executeSyncCommand(c, st, helm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -480,8 +498,8 @@ func executeTemplateCommand(c *cli.Context, state *state.HelmState, helm helmexe
|
|||
return state.TemplateReleases(helm, values, args)
|
||||
}
|
||||
|
||||
func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) []error {
|
||||
args := args.GetArgs(c.String("args"), state)
|
||||
func executeDiffCommand(c *cli.Context, st *state.HelmState, helm helmexec.Interface, detailedExitCode, suppressSecrets bool) ([]*state.ReleaseSpec, []error) {
|
||||
args := args.GetArgs(c.String("args"), st)
|
||||
if len(args) > 0 {
|
||||
helm.SetExtraArgs(args...)
|
||||
}
|
||||
|
|
@ -490,15 +508,15 @@ func executeDiffCommand(c *cli.Context, state *state.HelmState, helm helmexec.In
|
|||
}
|
||||
|
||||
if c.Bool("sync-repos") {
|
||||
if errs := state.SyncRepos(helm); errs != nil && len(errs) > 0 {
|
||||
return errs
|
||||
if errs := st.SyncRepos(helm); errs != nil && len(errs) > 0 {
|
||||
return []*state.ReleaseSpec{}, errs
|
||||
}
|
||||
}
|
||||
|
||||
values := c.StringSlice("values")
|
||||
workers := c.Int("concurrency")
|
||||
|
||||
return state.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets)
|
||||
return st.DiffReleases(helm, values, workers, detailedExitCode, suppressSecrets)
|
||||
}
|
||||
|
||||
func findAndIterateOverDesiredStatesUsingFlags(c *cli.Context, converge func(*state.HelmState, helmexec.Interface) []error) error {
|
||||
|
|
@ -706,6 +724,8 @@ func clean(st *state.HelmState, errs []error) error {
|
|||
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
|
||||
status := e.Sys().(syscall.WaitStatus)
|
||||
os.Exit(status.ExitStatus())
|
||||
case *state.DiffError:
|
||||
os.Exit(e.Code)
|
||||
default:
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
|||
147
state/state.go
147
state/state.go
|
|
@ -19,6 +19,8 @@ import (
|
|||
"github.com/roboll/helmfile/valuesfile"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v2"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// HelmState structure for the helmfile
|
||||
|
|
@ -405,23 +407,37 @@ func (state *HelmState) LintReleases(helm helmexec.Interface, additionalValues [
|
|||
return nil
|
||||
}
|
||||
|
||||
// DiffReleases wrapper for executing helm diff on the releases
|
||||
func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool) []error {
|
||||
var wgRelease sync.WaitGroup
|
||||
var wgError sync.WaitGroup
|
||||
errs := []error{}
|
||||
jobQueue := make(chan *ReleaseSpec, len(state.Releases))
|
||||
errQueue := make(chan error)
|
||||
type DiffError struct {
|
||||
*ReleaseSpec
|
||||
err error
|
||||
Code int
|
||||
}
|
||||
|
||||
if workerLimit < 1 {
|
||||
workerLimit = len(state.Releases)
|
||||
func (e *DiffError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
type diffResult struct {
|
||||
err *DiffError
|
||||
}
|
||||
|
||||
type diffPrepareResult struct {
|
||||
release *ReleaseSpec
|
||||
flags []string
|
||||
errors []*ReleaseError
|
||||
}
|
||||
|
||||
func (state *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, suppressSecrets bool) ([]diffPrepareResult, []error) {
|
||||
jobs := make(chan *ReleaseSpec, len(state.Releases))
|
||||
results := make(chan diffPrepareResult)
|
||||
|
||||
if concurrency < 1 {
|
||||
concurrency = len(state.Releases)
|
||||
}
|
||||
|
||||
wgRelease.Add(len(state.Releases))
|
||||
|
||||
for w := 1; w <= workerLimit; w++ {
|
||||
for w := 1; w <= concurrency; w++ {
|
||||
go func() {
|
||||
for release := range jobQueue {
|
||||
for release := range jobs {
|
||||
errs := []error{}
|
||||
|
||||
state.applyDefaultsTo(release)
|
||||
|
|
@ -451,41 +467,100 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues [
|
|||
flags = append(flags, "--suppress-secrets")
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
if err := helm.DiffRelease(release.Name, normalizeChart(state.basePath, release.Chart), flags...); err != nil {
|
||||
errs = append(errs, err)
|
||||
if len(errs) > 0 {
|
||||
rsErrs := make([]*ReleaseError, len(errs))
|
||||
for i, e := range errs {
|
||||
rsErrs[i] = &ReleaseError{release, e}
|
||||
}
|
||||
results <- diffPrepareResult{errors: rsErrs}
|
||||
} else {
|
||||
results <- diffPrepareResult{release: release, flags: flags, errors: []*ReleaseError{}}
|
||||
}
|
||||
for _, err := range errs {
|
||||
errQueue <- err
|
||||
}
|
||||
wgRelease.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wgError.Add(1)
|
||||
go func() {
|
||||
for err := range errQueue {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
wgError.Done()
|
||||
}()
|
||||
|
||||
for i := 0; i < len(state.Releases); i++ {
|
||||
jobQueue <- &state.Releases[i]
|
||||
jobs <- &state.Releases[i]
|
||||
}
|
||||
close(jobs)
|
||||
|
||||
rs := []diffPrepareResult{}
|
||||
errs := []error{}
|
||||
for i := 0; i < len(state.Releases); {
|
||||
select {
|
||||
case res := <-results:
|
||||
if res.errors != nil && len(res.errors) > 0 {
|
||||
for _, e := range res.errors {
|
||||
errs = append(errs, e)
|
||||
}
|
||||
} else if res.release != nil {
|
||||
rs = append(rs, res)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
return rs, errs
|
||||
}
|
||||
|
||||
// DiffReleases wrapper for executing helm diff on the releases
|
||||
// It returns releases that had any changes
|
||||
func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, suppressSecrets bool) ([]*ReleaseSpec, []error) {
|
||||
preps, prepErrs := state.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, suppressSecrets)
|
||||
if len(prepErrs) > 0 {
|
||||
return []*ReleaseSpec{}, prepErrs
|
||||
}
|
||||
|
||||
jobQueue := make(chan *diffPrepareResult, len(preps))
|
||||
results := make(chan diffResult, len(preps))
|
||||
|
||||
if workerLimit < 1 {
|
||||
workerLimit = len(state.Releases)
|
||||
}
|
||||
|
||||
for w := 1; w <= workerLimit; w++ {
|
||||
go func() {
|
||||
for prep := range jobQueue {
|
||||
flags := prep.flags
|
||||
release := prep.release
|
||||
if err := helm.DiffRelease(release.Name, normalizeChart(state.basePath, release.Chart), flags...); err != nil {
|
||||
switch e := err.(type) {
|
||||
case *exec.ExitError:
|
||||
// Propagate any non-zero exit status from the external command like `helm` that is failed under the hood
|
||||
status := e.Sys().(syscall.WaitStatus)
|
||||
results <- diffResult{&DiffError{release, err, status.ExitStatus()}}
|
||||
default:
|
||||
results <- diffResult{&DiffError{release, err, 0}}
|
||||
}
|
||||
} else {
|
||||
// diff succeeded, found no changes
|
||||
results <- diffResult{}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < len(preps); i++ {
|
||||
jobQueue <- &preps[i]
|
||||
}
|
||||
close(jobQueue)
|
||||
wgRelease.Wait()
|
||||
|
||||
close(errQueue)
|
||||
wgError.Wait()
|
||||
|
||||
if len(errs) != 0 {
|
||||
return errs
|
||||
rs := []*ReleaseSpec{}
|
||||
errs := []error{}
|
||||
for i := 0; i < len(preps); {
|
||||
select {
|
||||
case res := <-results:
|
||||
if res.err != nil {
|
||||
errs = append(errs, res.err)
|
||||
if res.err.Code == 2 {
|
||||
rs = append(rs, res.err.ReleaseSpec)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
close(results)
|
||||
return rs, errs
|
||||
}
|
||||
|
||||
func (state *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error {
|
||||
|
|
|
|||
Loading…
Reference in New Issue