Add support for transitive dependencies. (#1983)
Co-authored-by: Peter Aichinger <petera@topdesk.com>
This commit is contained in:
		
							parent
							
								
									9a0ce53608
								
							
						
					
					
						commit
						77e6268bcb
					
				
							
								
								
									
										24
									
								
								main.go
								
								
								
								
							
							
						
						
									
										24
									
								
								main.go
								
								
								
								
							|  | @ -201,7 +201,7 @@ func main() { | ||||||
| 				}, | 				}, | ||||||
| 				cli.BoolTFlag{ | 				cli.BoolTFlag{ | ||||||
| 					Name:  "skip-needs", | 					Name:  "skip-needs", | ||||||
| 					Usage: `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs is not provided`, | 					Usage: `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`, | ||||||
| 				}, | 				}, | ||||||
| 				cli.BoolFlag{ | 				cli.BoolFlag{ | ||||||
| 					Name:  "include-needs", | 					Name:  "include-needs", | ||||||
|  | @ -286,6 +286,10 @@ func main() { | ||||||
| 					Name:  "include-needs", | 					Name:  "include-needs", | ||||||
| 					Usage: `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`, | 					Usage: `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`, | ||||||
| 				}, | 				}, | ||||||
|  | 				cli.BoolFlag{ | ||||||
|  | 					Name:  "include-transitive-needs", | ||||||
|  | 					Usage: `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`, | ||||||
|  | 				}, | ||||||
| 				cli.BoolFlag{ | 				cli.BoolFlag{ | ||||||
| 					Name:  "skip-deps", | 					Name:  "skip-deps", | ||||||
| 					Usage: `skip running "helm repo update" and "helm dependency build"`, | 					Usage: `skip running "helm repo update" and "helm dependency build"`, | ||||||
|  | @ -414,12 +418,16 @@ func main() { | ||||||
| 				}, | 				}, | ||||||
| 				cli.BoolTFlag{ | 				cli.BoolTFlag{ | ||||||
| 					Name:  "skip-needs", | 					Name:  "skip-needs", | ||||||
| 					Usage: `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs is not provided`, | 					Usage: `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`, | ||||||
| 				}, | 				}, | ||||||
| 				cli.BoolFlag{ | 				cli.BoolFlag{ | ||||||
| 					Name:  "include-needs", | 					Name:  "include-needs", | ||||||
| 					Usage: `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`, | 					Usage: `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`, | ||||||
| 				}, | 				}, | ||||||
|  | 				cli.BoolFlag{ | ||||||
|  | 					Name:  "include-transitive-needs", | ||||||
|  | 					Usage: `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`, | ||||||
|  | 				}, | ||||||
| 				cli.BoolFlag{ | 				cli.BoolFlag{ | ||||||
| 					Name:  "wait", | 					Name:  "wait", | ||||||
| 					Usage: `Override helmDefaults.wait setting "helm upgrade --install --wait"`, | 					Usage: `Override helmDefaults.wait setting "helm upgrade --install --wait"`, | ||||||
|  | @ -487,12 +495,16 @@ func main() { | ||||||
| 				}, | 				}, | ||||||
| 				cli.BoolTFlag{ | 				cli.BoolTFlag{ | ||||||
| 					Name:  "skip-needs", | 					Name:  "skip-needs", | ||||||
| 					Usage: `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs is not provided`, | 					Usage: `do not automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided. Defaults to true when --include-needs or --include-transitive-needs is not provided`, | ||||||
| 				}, | 				}, | ||||||
| 				cli.BoolFlag{ | 				cli.BoolFlag{ | ||||||
| 					Name:  "include-needs", | 					Name:  "include-needs", | ||||||
| 					Usage: `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`, | 					Usage: `automatically include releases from the target release's "needs" when --selector/-l flag is provided. Does nothing when when --selector/-l flag is not provided`, | ||||||
| 				}, | 				}, | ||||||
|  | 				cli.BoolFlag{ | ||||||
|  | 					Name:  "include-transitive-needs", | ||||||
|  | 					Usage: `like --include-needs, but also includes transitive needs (needs of needs). Does nothing when when --selector/-l flag is not provided. Overrides exclusions of other selectors and conditions.`, | ||||||
|  | 				}, | ||||||
| 				cli.BoolFlag{ | 				cli.BoolFlag{ | ||||||
| 					Name:  "skip-diff-on-install", | 					Name:  "skip-diff-on-install", | ||||||
| 					Usage: "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all", | 					Usage: "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all", | ||||||
|  | @ -782,7 +794,11 @@ func (c configImpl) SkipNeeds() bool { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (c configImpl) IncludeNeeds() bool { | func (c configImpl) IncludeNeeds() bool { | ||||||
| 	return c.c.Bool("include-needs") | 	return c.c.Bool("include-needs") || c.IncludeTransitiveNeeds() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (c configImpl) IncludeTransitiveNeeds() bool { | ||||||
|  | 	return c.c.Bool("include-transitive-needs") | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DiffConfig
 | // DiffConfig
 | ||||||
|  |  | ||||||
							
								
								
									
										126
									
								
								pkg/app/app.go
								
								
								
								
							
							
						
						
									
										126
									
								
								pkg/app/app.go
								
								
								
								
							|  | @ -120,7 +120,7 @@ func (a *App) Deps(c DepsConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, c.IncludeTransitiveNeeds(), SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Repos(c ReposConfigProvider) error { | func (a *App) Repos(c ReposConfigProvider) error { | ||||||
|  | @ -132,7 +132,7 @@ func (a *App) Repos(c ReposConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, c.IncludeTransitiveNeeds(), SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { | func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { | ||||||
|  | @ -149,7 +149,7 @@ func (a *App) DeprecatedSyncCharts(c DeprecatedChartsConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, c.IncludeTransitiveNeeds(), SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Diff(c DiffConfigProvider) error { | func (a *App) Diff(c DiffConfigProvider) error { | ||||||
|  | @ -203,7 +203,7 @@ func (a *App) Diff(c DiffConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return matched, criticalErrs | 		return matched, criticalErrs | ||||||
| 	}) | 	}, false) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -225,9 +225,6 @@ func (a *App) Diff(c DiffConfigProvider) error { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Template(c TemplateConfigProvider) error { | func (a *App) Template(c TemplateConfigProvider) error { | ||||||
| 
 |  | ||||||
| 	opts := []LoadOption{SetRetainValuesFiles(c.SkipCleanup())} |  | ||||||
| 
 |  | ||||||
| 	return a.ForEachState(func(run *Run) (ok bool, errs []error) { | 	return a.ForEachState(func(run *Run) (ok bool, errs []error) { | ||||||
| 		includeCRDs := c.IncludeCRDs() | 		includeCRDs := c.IncludeCRDs() | ||||||
| 
 | 
 | ||||||
|  | @ -249,7 +246,7 @@ func (a *App) Template(c TemplateConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, opts...) | 	}, c.IncludeTransitiveNeeds()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) WriteValues(c WriteValuesConfigProvider) error { | func (a *App) WriteValues(c WriteValuesConfigProvider) error { | ||||||
|  | @ -269,7 +266,7 @@ func (a *App) WriteValues(c WriteValuesConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, c.IncludeTransitiveNeeds(), SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type MultiError struct { | type MultiError struct { | ||||||
|  | @ -322,7 +319,7 @@ func (a *App) Lint(c LintConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, false, SetFilter(true)) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -350,7 +347,7 @@ func (a *App) Fetch(c FetchConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, false, SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Sync(c SyncConfigProvider) error { | func (a *App) Sync(c SyncConfigProvider) error { | ||||||
|  | @ -358,11 +355,12 @@ func (a *App) Sync(c SyncConfigProvider) error { | ||||||
| 		includeCRDs := !c.SkipCRDs() | 		includeCRDs := !c.SkipCRDs() | ||||||
| 
 | 
 | ||||||
| 		prepErr := run.withPreparedCharts("sync", state.ChartPrepareOptions{ | 		prepErr := run.withPreparedCharts("sync", state.ChartPrepareOptions{ | ||||||
| 			SkipRepos:   c.SkipDeps(), | 			SkipRepos:              c.SkipDeps(), | ||||||
| 			SkipDeps:    c.SkipDeps(), | 			SkipDeps:               c.SkipDeps(), | ||||||
| 			Wait:        c.Wait(), | 			Wait:                   c.Wait(), | ||||||
| 			WaitForJobs: c.WaitForJobs(), | 			WaitForJobs:            c.WaitForJobs(), | ||||||
| 			IncludeCRDs: &includeCRDs, | 			IncludeCRDs:            &includeCRDs, | ||||||
|  | 			IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), | ||||||
| 		}, func() { | 		}, func() { | ||||||
| 			ok, errs = a.sync(run, c) | 			ok, errs = a.sync(run, c) | ||||||
| 		}) | 		}) | ||||||
|  | @ -372,7 +370,7 @@ func (a *App) Sync(c SyncConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}) | 	}, c.IncludeTransitiveNeeds()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Apply(c ApplyConfigProvider) error { | func (a *App) Apply(c ApplyConfigProvider) error { | ||||||
|  | @ -410,7 +408,7 @@ func (a *App) Apply(c ApplyConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, opts...) | 	}, c.IncludeTransitiveNeeds(), opts...) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -439,7 +437,7 @@ func (a *App) Status(c StatusesConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, false, SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Delete(c DeleteConfigProvider) error { | func (a *App) Delete(c DeleteConfigProvider) error { | ||||||
|  | @ -456,7 +454,7 @@ func (a *App) Delete(c DeleteConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetReverse(true)) | 	}, false, SetReverse(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Destroy(c DestroyConfigProvider) error { | func (a *App) Destroy(c DestroyConfigProvider) error { | ||||||
|  | @ -473,7 +471,7 @@ func (a *App) Destroy(c DestroyConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetReverse(true)) | 	}, false, SetReverse(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Test(c TestConfigProvider) error { | func (a *App) Test(c TestConfigProvider) error { | ||||||
|  | @ -496,7 +494,7 @@ func (a *App) Test(c TestConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, false, SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) PrintState(c StateConfigProvider) error { | func (a *App) PrintState(c StateConfigProvider) error { | ||||||
|  | @ -543,7 +541,7 @@ func (a *App) PrintState(c StateConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, false, SetFilter(true)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) ListReleases(c ListConfigProvider) error { | func (a *App) ListReleases(c ListConfigProvider) error { | ||||||
|  | @ -594,7 +592,7 @@ func (a *App) ListReleases(c ListConfigProvider) error { | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return | 		return | ||||||
| 	}, SetFilter(true)) | 	}, false, SetFilter(true)) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
|  | @ -883,14 +881,14 @@ var ( | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (a *App) ForEachState(do func(*Run) (bool, []error), o ...LoadOption) error { | func (a *App) ForEachState(do func(*Run) (bool, []error), includeTransitiveNeeds bool, o ...LoadOption) error { | ||||||
| 	ctx := NewContext() | 	ctx := NewContext() | ||||||
| 	err := a.visitStatesWithSelectorsAndRemoteSupport(a.FileOrDir, func(st *state.HelmState) (bool, []error) { | 	err := a.visitStatesWithSelectorsAndRemoteSupport(a.FileOrDir, func(st *state.HelmState) (bool, []error) { | ||||||
| 		helm := a.getHelm(st) | 		helm := a.getHelm(st) | ||||||
| 
 | 
 | ||||||
| 		run := NewRun(st, helm, ctx) | 		run := NewRun(st, helm, ctx) | ||||||
| 		return do(run) | 		return do(run) | ||||||
| 	}, o...) | 	}, includeTransitiveNeeds, o...) | ||||||
| 
 | 
 | ||||||
| 	return err | 	return err | ||||||
| } | } | ||||||
|  | @ -966,7 +964,7 @@ type Opts struct { | ||||||
| 	DAGEnabled bool | 	DAGEnabled bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) visitStatesWithSelectorsAndRemoteSupport(fileOrDir string, converge func(*state.HelmState) (bool, []error), opt ...LoadOption) error { | func (a *App) visitStatesWithSelectorsAndRemoteSupport(fileOrDir string, converge func(*state.HelmState) (bool, []error), includeTransitiveNeeds bool, opt ...LoadOption) error { | ||||||
| 	opts := LoadOpts{ | 	opts := LoadOpts{ | ||||||
| 		Selectors: a.Selectors, | 		Selectors: a.Selectors, | ||||||
| 	} | 	} | ||||||
|  | @ -1004,16 +1002,17 @@ func (a *App) visitStatesWithSelectorsAndRemoteSupport(fileOrDir string, converg | ||||||
| 			return processFilteredReleases(st, a.getHelm(st), func(st *state.HelmState) []error { | 			return processFilteredReleases(st, a.getHelm(st), func(st *state.HelmState) []error { | ||||||
| 				_, err := converge(st) | 				_, err := converge(st) | ||||||
| 				return err | 				return err | ||||||
| 			}) | 			}, | ||||||
|  | 				includeTransitiveNeeds) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return a.visitStates(fileOrDir, opts, f) | 	return a.visitStates(fileOrDir, opts, f) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func processFilteredReleases(st *state.HelmState, helm helmexec.Interface, converge func(st *state.HelmState) []error) (bool, []error) { | func processFilteredReleases(st *state.HelmState, helm helmexec.Interface, converge func(st *state.HelmState) []error, includeTransitiveNeeds bool) (bool, []error) { | ||||||
| 	if len(st.Selectors) > 0 { | 	if len(st.Selectors) > 0 { | ||||||
| 		err := st.FilterReleases() | 		err := st.FilterReleases(includeTransitiveNeeds) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return false, []error{err} | 			return false, []error{err} | ||||||
| 		} | 		} | ||||||
|  | @ -1066,11 +1065,11 @@ func checkDuplicates(helm helmexec.Interface, st *state.HelmState, releases []st | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) Wrap(converge func(*state.HelmState, helmexec.Interface) []error) func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { | func (a *App) Wrap(converge func(*state.HelmState, helmexec.Interface) []error) func(st *state.HelmState, helm helmexec.Interface, includeTransitiveNeeds bool) (bool, []error) { | ||||||
| 	return func(st *state.HelmState, helm helmexec.Interface) (bool, []error) { | 	return func(st *state.HelmState, helm helmexec.Interface, includeTransitiveNeeds bool) (bool, []error) { | ||||||
| 		return processFilteredReleases(st, helm, func(st *state.HelmState) []error { | 		return processFilteredReleases(st, helm, func(st *state.HelmState) []error { | ||||||
| 			return converge(st, helm) | 			return converge(st, helm) | ||||||
| 		}) | 		}, includeTransitiveNeeds) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1144,8 +1143,8 @@ func (a *App) findDesiredStateFiles(specifiedPath string, opts LoadOpts) ([]stri | ||||||
| 	return files, nil | 	return files, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *App) getSelectedReleases(r *Run) ([]state.ReleaseSpec, []state.ReleaseSpec, error) { | func (a *App) getSelectedReleases(r *Run, includeTransitiveNeeds bool) ([]state.ReleaseSpec, []state.ReleaseSpec, error) { | ||||||
| 	selected, err := r.state.GetSelectedReleasesWithOverrides() | 	selected, err := r.state.GetSelectedReleasesWithOverrides(includeTransitiveNeeds) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, nil, err | 		return nil, nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -1159,12 +1158,7 @@ func (a *App) getSelectedReleases(r *Run) ([]state.ReleaseSpec, []state.ReleaseS | ||||||
| 
 | 
 | ||||||
| 	needed := map[string]struct{}{} | 	needed := map[string]struct{}{} | ||||||
| 	for _, r := range selected { | 	for _, r := range selected { | ||||||
| 		for _, id := range r.Needs { | 		collectNeeds(r, selectedIds, needed, allReleases, includeTransitiveNeeds) | ||||||
| 			// Avoids duplicating a release that is selected AND also needed by another selected release
 |  | ||||||
| 			if _, ok := selectedIds[id]; !ok { |  | ||||||
| 				needed[id] = struct{}{} |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var releases []state.ReleaseSpec | 	var releases []state.ReleaseSpec | ||||||
|  | @ -1192,11 +1186,29 @@ func (a *App) getSelectedReleases(r *Run) ([]state.ReleaseSpec, []state.ReleaseS | ||||||
| 	return selected, releases, nil | 	return selected, releases, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func collectNeeds(release state.ReleaseSpec, selectedIds map[string]struct{}, needed map[string]struct{}, allReleases []state.ReleaseSpec, includeTransitiveNeeds bool) { | ||||||
|  | 	for _, id := range release.Needs { | ||||||
|  | 		// Avoids duplicating a release that is selected AND also needed by another selected release
 | ||||||
|  | 		if _, ok := selectedIds[id]; !ok { | ||||||
|  | 			needed[id] = struct{}{} | ||||||
|  | 			if includeTransitiveNeeds { | ||||||
|  | 				releaseParts := strings.Split(id, "/") | ||||||
|  | 				releaseName := releaseParts[len(releaseParts)-1] | ||||||
|  | 				for _, r := range allReleases { | ||||||
|  | 					if r.Name == releaseName { | ||||||
|  | 						collectNeeds(r, selectedIds, needed, allReleases, includeTransitiveNeeds) | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { | func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { | ||||||
| 	st := r.state | 	st := r.state | ||||||
| 	helm := r.helm | 	helm := r.helm | ||||||
| 
 | 
 | ||||||
| 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r) | 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeTransitiveNeeds()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, false, []error{err} | 		return false, false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1210,7 +1222,7 @@ func (a *App) apply(r *Run, c ApplyConfigProvider) (bool, bool, []error) { | ||||||
| 	// See https://github.com/roboll/helmfile/issues/1818 for more context.
 | 	// See https://github.com/roboll/helmfile/issues/1818 for more context.
 | ||||||
| 	st.Releases = selectedAndNeededReleases | 	st.Releases = selectedAndNeededReleases | ||||||
| 
 | 
 | ||||||
| 	plan, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds()}) | 	plan, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, false, []error{err} | 		return false, false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1320,7 +1332,7 @@ Do you really want to apply? | ||||||
| 
 | 
 | ||||||
| 		// We upgrade releases by traversing the DAG
 | 		// We upgrade releases by traversing the DAG
 | ||||||
| 		if len(releasesToBeUpdated) > 0 { | 		if len(releasesToBeUpdated) > 0 { | ||||||
| 			_, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, Reverse: false, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { | 			_, updateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, Reverse: false, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { | ||||||
| 				var rs []state.ReleaseSpec | 				var rs []state.ReleaseSpec | ||||||
| 
 | 
 | ||||||
| 				for _, r := range subst.Releases { | 				for _, r := range subst.Releases { | ||||||
|  | @ -1357,7 +1369,7 @@ func (a *App) delete(r *Run, purge bool, c DestroyConfigProvider) (bool, []error | ||||||
| 
 | 
 | ||||||
| 	affectedReleases := state.AffectedReleases{} | 	affectedReleases := state.AffectedReleases{} | ||||||
| 
 | 
 | ||||||
| 	toSync, _, err := a.getSelectedReleases(r) | 	toSync, _, err := a.getSelectedReleases(r, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1429,7 +1441,7 @@ Do you really want to delete? | ||||||
| func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) { | func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) { | ||||||
| 	st := r.state | 	st := r.state | ||||||
| 
 | 
 | ||||||
| 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r) | 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, false, false, []error{err} | 		return nil, false, false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1450,7 +1462,7 @@ func (a *App) diff(r *Run, c DiffConfigProvider) (*string, bool, bool, []error) | ||||||
| 
 | 
 | ||||||
| 	st.Releases = selectedAndNeededReleases | 	st.Releases = selectedAndNeededReleases | ||||||
| 
 | 
 | ||||||
| 	plan, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds()}) | 	plan, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, SkipNeeds: c.SkipNeeds(), IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: false}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, false, false, []error{err} | 		return nil, false, false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1485,7 +1497,7 @@ func (a *App) lint(r *Run, c LintConfigProvider) (bool, []error, []error) { | ||||||
| 
 | 
 | ||||||
| 	allReleases := st.GetReleasesWithOverrides() | 	allReleases := st.GetReleasesWithOverrides() | ||||||
| 
 | 
 | ||||||
| 	selectedReleases, _, err := a.getSelectedReleases(r) | 	selectedReleases, _, err := a.getSelectedReleases(r, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, nil, []error{err} | 		return false, nil, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1553,7 +1565,7 @@ func (a *App) status(r *Run, c StatusesConfigProvider) (bool, []error) { | ||||||
| 
 | 
 | ||||||
| 	allReleases := st.GetReleasesWithOverrides() | 	allReleases := st.GetReleasesWithOverrides() | ||||||
| 
 | 
 | ||||||
| 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r) | 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1603,7 +1615,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { | ||||||
| 	st := r.state | 	st := r.state | ||||||
| 	helm := r.helm | 	helm := r.helm | ||||||
| 
 | 
 | ||||||
| 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r) | 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeTransitiveNeeds()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1617,7 +1629,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { | ||||||
| 	// See https://github.com/roboll/helmfile/issues/1818 for more context.
 | 	// See https://github.com/roboll/helmfile/issues/1818 for more context.
 | ||||||
| 	st.Releases = selectedAndNeededReleases | 	st.Releases = selectedAndNeededReleases | ||||||
| 
 | 
 | ||||||
| 	batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: c.IncludeNeeds(), SkipNeeds: c.SkipNeeds()}) | 	batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), SkipNeeds: c.SkipNeeds()}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1727,7 +1739,7 @@ func (a *App) sync(r *Run, c SyncConfigProvider) (bool, []error) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(releasesToUpdate) > 0 { | 	if len(releasesToUpdate) > 0 { | ||||||
| 		_, syncErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { | 		_, syncErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toUpdate, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { | ||||||
| 			var rs []state.ReleaseSpec | 			var rs []state.ReleaseSpec | ||||||
| 
 | 
 | ||||||
| 			for _, r := range subst.Releases { | 			for _, r := range subst.Releases { | ||||||
|  | @ -1759,7 +1771,7 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) { | ||||||
| 	st := r.state | 	st := r.state | ||||||
| 	helm := r.helm | 	helm := r.helm | ||||||
| 
 | 
 | ||||||
| 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r) | 	selectedReleases, selectedAndNeededReleases, err := a.getSelectedReleases(r, c.IncludeTransitiveNeeds()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1773,7 +1785,7 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) { | ||||||
| 	// See https://github.com/roboll/helmfile/issues/1818 for more context.
 | 	// See https://github.com/roboll/helmfile/issues/1818 for more context.
 | ||||||
| 	st.Releases = selectedAndNeededReleases | 	st.Releases = selectedAndNeededReleases | ||||||
| 
 | 
 | ||||||
| 	batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: c.IncludeNeeds(), SkipNeeds: !c.IncludeNeeds()}) | 	batches, err := st.PlanReleases(state.PlanOptions{Reverse: false, SelectedReleases: selectedReleases, IncludeNeeds: c.IncludeNeeds(), IncludeTransitiveNeeds: c.IncludeTransitiveNeeds(), SkipNeeds: !c.IncludeNeeds()}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1813,7 +1825,7 @@ func (a *App) template(r *Run, c TemplateConfigProvider) (bool, []error) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(toRender) > 0 { | 	if len(toRender) > 0 { | ||||||
| 		_, templateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toRender, Reverse: false, SkipNeeds: true}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { | 		_, templateErrs := withDAG(st, helm, a.Logger, state.PlanOptions{SelectedReleases: toRender, Reverse: false, SkipNeeds: true, IncludeTransitiveNeeds: c.IncludeTransitiveNeeds()}, a.WrapWithoutSelector(func(subst *state.HelmState, helm helmexec.Interface) []error { | ||||||
| 			opts := &state.TemplateOpts{ | 			opts := &state.TemplateOpts{ | ||||||
| 				Set:               c.Set(), | 				Set:               c.Set(), | ||||||
| 				IncludeCRDs:       c.IncludeCRDs(), | 				IncludeCRDs:       c.IncludeCRDs(), | ||||||
|  | @ -1837,7 +1849,7 @@ func (a *App) test(r *Run, c TestConfigProvider) []error { | ||||||
| 
 | 
 | ||||||
| 	st := r.state | 	st := r.state | ||||||
| 
 | 
 | ||||||
| 	toTest, _, err := a.getSelectedReleases(r) | 	toTest, _, err := a.getSelectedReleases(r, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return []error{err} | 		return []error{err} | ||||||
| 	} | 	} | ||||||
|  | @ -1859,7 +1871,7 @@ func (a *App) writeValues(r *Run, c WriteValuesConfigProvider) (bool, []error) { | ||||||
| 	st := r.state | 	st := r.state | ||||||
| 	helm := r.helm | 	helm := r.helm | ||||||
| 
 | 
 | ||||||
| 	toRender, _, err := a.getSelectedReleases(r) | 	toRender, _, err := a.getSelectedReleases(r, false) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, []error{err} | 		return false, []error{err} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -17,8 +17,9 @@ import ( | ||||||
| 
 | 
 | ||||||
| func TestApply_3(t *testing.T) { | func TestApply_3(t *testing.T) { | ||||||
| 	type fields struct { | 	type fields struct { | ||||||
| 		skipNeeds    bool | 		skipNeeds              bool | ||||||
| 		includeNeeds bool | 		includeNeeds           bool | ||||||
|  | 		includeTransitiveNeeds bool | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	type testcase struct { | 	type testcase struct { | ||||||
|  | @ -109,11 +110,12 @@ func TestApply_3(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 			syncErr := app.Apply(applyConfig{ | 			syncErr := app.Apply(applyConfig{ | ||||||
| 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | ||||||
| 				concurrency:       tc.concurrency, | 				concurrency:            tc.concurrency, | ||||||
| 				logger:            logger, | 				logger:                 logger, | ||||||
| 				skipDiffOnInstall: tc.skipDiffOnInstall, | 				skipDiffOnInstall:      tc.skipDiffOnInstall, | ||||||
| 				skipNeeds:         tc.fields.skipNeeds, | 				skipNeeds:              tc.fields.skipNeeds, | ||||||
| 				includeNeeds:      tc.fields.includeNeeds, | 				includeNeeds:           tc.fields.includeNeeds, | ||||||
|  | 				includeTransitiveNeeds: tc.fields.includeTransitiveNeeds, | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			var gotErr string | 			var gotErr string | ||||||
|  |  | ||||||
|  | @ -17,8 +17,9 @@ import ( | ||||||
| 
 | 
 | ||||||
| func TestApply_2(t *testing.T) { | func TestApply_2(t *testing.T) { | ||||||
| 	type fields struct { | 	type fields struct { | ||||||
| 		skipNeeds    bool | 		skipNeeds              bool | ||||||
| 		includeNeeds bool | 		includeNeeds           bool | ||||||
|  | 		includeTransitiveNeeds bool | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	type testcase struct { | 	type testcase struct { | ||||||
|  | @ -109,11 +110,12 @@ func TestApply_2(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 			syncErr := app.Apply(applyConfig{ | 			syncErr := app.Apply(applyConfig{ | ||||||
| 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | ||||||
| 				concurrency:       tc.concurrency, | 				concurrency:            tc.concurrency, | ||||||
| 				logger:            logger, | 				logger:                 logger, | ||||||
| 				skipDiffOnInstall: tc.skipDiffOnInstall, | 				skipDiffOnInstall:      tc.skipDiffOnInstall, | ||||||
| 				skipNeeds:         tc.fields.skipNeeds, | 				skipNeeds:              tc.fields.skipNeeds, | ||||||
| 				includeNeeds:      tc.fields.includeNeeds, | 				includeNeeds:           tc.fields.includeNeeds, | ||||||
|  | 				includeTransitiveNeeds: tc.fields.includeTransitiveNeeds, | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			var gotErr string | 			var gotErr string | ||||||
|  | @ -960,6 +962,128 @@ NAME               CHART           VERSION | ||||||
| external-secrets   incubator/raw           | external-secrets   incubator/raw           | ||||||
| my-release         incubator/raw           | my-release         incubator/raw           | ||||||
| 
 | 
 | ||||||
|  | `, | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("include-transitive-needs=true", func(t *testing.T) { | ||||||
|  | 		check(t, testcase{ | ||||||
|  | 			fields: fields{ | ||||||
|  | 				skipNeeds:              false, | ||||||
|  | 				includeNeeds:           true, | ||||||
|  | 				includeTransitiveNeeds: true, | ||||||
|  | 			}, | ||||||
|  | 			error: ``, | ||||||
|  | 			files: map[string]string{ | ||||||
|  | 				"/path/to/helmfile.yaml": ` | ||||||
|  | {{ $mark := "a" }} | ||||||
|  | 
 | ||||||
|  | releases: | ||||||
|  | - name: serviceA | ||||||
|  |   chart: my/chart | ||||||
|  |   needs: | ||||||
|  |   - serviceB | ||||||
|  | 
 | ||||||
|  | - name: serviceB | ||||||
|  |   chart: my/chart | ||||||
|  |   needs: | ||||||
|  |   - serviceC | ||||||
|  | 
 | ||||||
|  | - name: serviceC | ||||||
|  |   chart: my/chart | ||||||
|  | 
 | ||||||
|  | - name: serviceD | ||||||
|  |   chart: my/chart | ||||||
|  | `, | ||||||
|  | 			}, | ||||||
|  | 			selectors: []string{"name=serviceA"}, | ||||||
|  | 			upgraded:  []exectest.Release{}, | ||||||
|  | 			diffs: map[exectest.DiffKey]error{ | ||||||
|  | 				exectest.DiffKey{Name: "serviceA", Chart: "my/chart", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, | ||||||
|  | 				exectest.DiffKey{Name: "serviceB", Chart: "my/chart", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, | ||||||
|  | 				exectest.DiffKey{Name: "serviceC", Chart: "my/chart", Flags: "--kube-contextdefault--detailed-exitcode"}: helmexec.ExitError{Code: 2}, | ||||||
|  | 			}, | ||||||
|  | 			// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
 | ||||||
|  | 			concurrency: 1, | ||||||
|  | 			log: `processing file "helmfile.yaml" in directory "." | ||||||
|  | first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil> | ||||||
|  | first-pass uses: &{default map[] map[]} | ||||||
|  | first-pass rendering output of "helmfile.yaml.part.0": | ||||||
|  |  0:  | ||||||
|  |  1:  | ||||||
|  |  2:  | ||||||
|  |  3: releases: | ||||||
|  |  4: - name: serviceA | ||||||
|  |  5:   chart: my/chart | ||||||
|  |  6:   needs: | ||||||
|  |  7:   - serviceB | ||||||
|  |  8:  | ||||||
|  |  9: - name: serviceB | ||||||
|  | 10:   chart: my/chart | ||||||
|  | 11:   needs: | ||||||
|  | 12:   - serviceC | ||||||
|  | 13:  | ||||||
|  | 14: - name: serviceC | ||||||
|  | 15:   chart: my/chart | ||||||
|  | 16:  | ||||||
|  | 17: - name: serviceD | ||||||
|  | 18:   chart: my/chart | ||||||
|  | 19:  | ||||||
|  | 
 | ||||||
|  | first-pass produced: &{default map[] map[]} | ||||||
|  | first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} | ||||||
|  | vals: | ||||||
|  | map[] | ||||||
|  | defaultVals:[] | ||||||
|  | second-pass rendering result of "helmfile.yaml.part.0": | ||||||
|  |  0:  | ||||||
|  |  1:  | ||||||
|  |  2:  | ||||||
|  |  3: releases: | ||||||
|  |  4: - name: serviceA | ||||||
|  |  5:   chart: my/chart | ||||||
|  |  6:   needs: | ||||||
|  |  7:   - serviceB | ||||||
|  |  8:  | ||||||
|  |  9: - name: serviceB | ||||||
|  | 10:   chart: my/chart | ||||||
|  | 11:   needs: | ||||||
|  | 12:   - serviceC | ||||||
|  | 13:  | ||||||
|  | 14: - name: serviceC | ||||||
|  | 15:   chart: my/chart | ||||||
|  | 16:  | ||||||
|  | 17: - name: serviceD | ||||||
|  | 18:   chart: my/chart | ||||||
|  | 19:  | ||||||
|  | 
 | ||||||
|  | merged environment: &{default map[] map[]} | ||||||
|  | 3 release(s) matching name=serviceA found in helmfile.yaml | ||||||
|  | 
 | ||||||
|  | Affected releases are: | ||||||
|  |   serviceA (my/chart) UPDATED | ||||||
|  |   serviceB (my/chart) UPDATED | ||||||
|  |   serviceC (my/chart) UPDATED | ||||||
|  | 
 | ||||||
|  | processing 3 groups of releases in this order: | ||||||
|  | GROUP RELEASES | ||||||
|  | 1     default//serviceC
 | ||||||
|  | 2     default//serviceB
 | ||||||
|  | 3     default//serviceA
 | ||||||
|  | 
 | ||||||
|  | processing releases in group 1/3: default//serviceC
 | ||||||
|  | getting deployed release version failed:unexpected list key: {^serviceC$ --kube-contextdefault--deleting--deployed--failed--pending} | ||||||
|  | processing releases in group 2/3: default//serviceB
 | ||||||
|  | getting deployed release version failed:unexpected list key: {^serviceB$ --kube-contextdefault--deleting--deployed--failed--pending} | ||||||
|  | processing releases in group 3/3: default//serviceA
 | ||||||
|  | getting deployed release version failed:unexpected list key: {^serviceA$ --kube-contextdefault--deleting--deployed--failed--pending} | ||||||
|  | 
 | ||||||
|  | UPDATED RELEASES: | ||||||
|  | NAME       CHART      VERSION | ||||||
|  | serviceC   my/chart           | ||||||
|  | serviceB   my/chart           | ||||||
|  | serviceA   my/chart           | ||||||
|  | 
 | ||||||
| `, | `, | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -17,8 +17,9 @@ import ( | ||||||
| 
 | 
 | ||||||
| func TestSync(t *testing.T) { | func TestSync(t *testing.T) { | ||||||
| 	type fields struct { | 	type fields struct { | ||||||
| 		skipNeeds    bool | 		skipNeeds              bool | ||||||
| 		includeNeeds bool | 		includeNeeds           bool | ||||||
|  | 		includeTransitiveNeeds bool | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	type testcase struct { | 	type testcase struct { | ||||||
|  | @ -107,11 +108,12 @@ func TestSync(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 			syncErr := app.Sync(applyConfig{ | 			syncErr := app.Sync(applyConfig{ | ||||||
| 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | ||||||
| 				concurrency:       tc.concurrency, | 				concurrency:            tc.concurrency, | ||||||
| 				logger:            logger, | 				logger:                 logger, | ||||||
| 				skipDiffOnInstall: tc.skipDiffOnInstall, | 				skipDiffOnInstall:      tc.skipDiffOnInstall, | ||||||
| 				skipNeeds:         tc.fields.skipNeeds, | 				skipNeeds:              tc.fields.skipNeeds, | ||||||
| 				includeNeeds:      tc.fields.includeNeeds, | 				includeNeeds:           tc.fields.includeNeeds, | ||||||
|  | 				includeTransitiveNeeds: tc.fields.includeTransitiveNeeds, | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			var gotErr string | 			var gotErr string | ||||||
|  | @ -412,6 +414,122 @@ kubernetes-external-secrets   incubator/raw | ||||||
| external-secrets              incubator/raw           | external-secrets              incubator/raw           | ||||||
| my-release                    incubator/raw           | my-release                    incubator/raw           | ||||||
| 
 | 
 | ||||||
|  | `, | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("include-transitive-needs=true", func(t *testing.T) { | ||||||
|  | 		check(t, testcase{ | ||||||
|  | 			fields: fields{ | ||||||
|  | 				skipNeeds:              false, | ||||||
|  | 				includeTransitiveNeeds: true, | ||||||
|  | 			}, | ||||||
|  | 			error: ``, | ||||||
|  | 			files: map[string]string{ | ||||||
|  | 				"/path/to/helmfile.yaml": ` | ||||||
|  | {{ $mark := "a" }} | ||||||
|  | 
 | ||||||
|  | releases: | ||||||
|  | - name: serviceA | ||||||
|  |   chart: my/chart | ||||||
|  |   needs: | ||||||
|  |   - serviceB | ||||||
|  | 
 | ||||||
|  | - name: serviceB | ||||||
|  |   chart: my/chart | ||||||
|  |   needs: | ||||||
|  |   - serviceC | ||||||
|  | 
 | ||||||
|  | - name: serviceC | ||||||
|  |   chart: my/chart | ||||||
|  | 
 | ||||||
|  | - name: serviceD | ||||||
|  |   chart: my/chart | ||||||
|  | `, | ||||||
|  | 			}, | ||||||
|  | 			selectors: []string{"name=serviceA"}, | ||||||
|  | 			upgraded:  []exectest.Release{}, | ||||||
|  | 			// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
 | ||||||
|  | 			concurrency: 1, | ||||||
|  | 			log: `processing file "helmfile.yaml" in directory "." | ||||||
|  | first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil> | ||||||
|  | first-pass uses: &{default map[] map[]} | ||||||
|  | first-pass rendering output of "helmfile.yaml.part.0": | ||||||
|  |  0:  | ||||||
|  |  1:  | ||||||
|  |  2:  | ||||||
|  |  3: releases: | ||||||
|  |  4: - name: serviceA | ||||||
|  |  5:   chart: my/chart | ||||||
|  |  6:   needs: | ||||||
|  |  7:   - serviceB | ||||||
|  |  8:  | ||||||
|  |  9: - name: serviceB | ||||||
|  | 10:   chart: my/chart | ||||||
|  | 11:   needs: | ||||||
|  | 12:   - serviceC | ||||||
|  | 13:  | ||||||
|  | 14: - name: serviceC | ||||||
|  | 15:   chart: my/chart | ||||||
|  | 16:  | ||||||
|  | 17: - name: serviceD | ||||||
|  | 18:   chart: my/chart | ||||||
|  | 19:  | ||||||
|  | 
 | ||||||
|  | first-pass produced: &{default map[] map[]} | ||||||
|  | first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]} | ||||||
|  | vals: | ||||||
|  | map[] | ||||||
|  | defaultVals:[] | ||||||
|  | second-pass rendering result of "helmfile.yaml.part.0": | ||||||
|  |  0:  | ||||||
|  |  1:  | ||||||
|  |  2:  | ||||||
|  |  3: releases: | ||||||
|  |  4: - name: serviceA | ||||||
|  |  5:   chart: my/chart | ||||||
|  |  6:   needs: | ||||||
|  |  7:   - serviceB | ||||||
|  |  8:  | ||||||
|  |  9: - name: serviceB | ||||||
|  | 10:   chart: my/chart | ||||||
|  | 11:   needs: | ||||||
|  | 12:   - serviceC | ||||||
|  | 13:  | ||||||
|  | 14: - name: serviceC | ||||||
|  | 15:   chart: my/chart | ||||||
|  | 16:  | ||||||
|  | 17: - name: serviceD | ||||||
|  | 18:   chart: my/chart | ||||||
|  | 19:  | ||||||
|  | 
 | ||||||
|  | merged environment: &{default map[] map[]} | ||||||
|  | 3 release(s) matching name=serviceA found in helmfile.yaml | ||||||
|  | 
 | ||||||
|  | Affected releases are: | ||||||
|  |   serviceA (my/chart) UPDATED | ||||||
|  |   serviceB (my/chart) UPDATED | ||||||
|  |   serviceC (my/chart) UPDATED | ||||||
|  | 
 | ||||||
|  | processing 3 groups of releases in this order: | ||||||
|  | GROUP RELEASES | ||||||
|  | 1     default//serviceC
 | ||||||
|  | 2     default//serviceB
 | ||||||
|  | 3     default//serviceA
 | ||||||
|  | 
 | ||||||
|  | processing releases in group 1/3: default//serviceC
 | ||||||
|  | getting deployed release version failed:unexpected list key: {^serviceC$ --kube-contextdefault--deleting--deployed--failed--pending} | ||||||
|  | processing releases in group 2/3: default//serviceB
 | ||||||
|  | getting deployed release version failed:unexpected list key: {^serviceB$ --kube-contextdefault--deleting--deployed--failed--pending} | ||||||
|  | processing releases in group 3/3: default//serviceA
 | ||||||
|  | getting deployed release version failed:unexpected list key: {^serviceA$ --kube-contextdefault--deleting--deployed--failed--pending} | ||||||
|  | 
 | ||||||
|  | UPDATED RELEASES: | ||||||
|  | NAME       CHART      VERSION | ||||||
|  | serviceC   my/chart           | ||||||
|  | serviceB   my/chart           | ||||||
|  | serviceA   my/chart           | ||||||
|  | 
 | ||||||
| `, | `, | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
|  | @ -419,8 +537,9 @@ my-release                    incubator/raw | ||||||
| 	t.Run("skip-needs=false include-needs=true with installed but disabled release", func(t *testing.T) { | 	t.Run("skip-needs=false include-needs=true with installed but disabled release", func(t *testing.T) { | ||||||
| 		check(t, testcase{ | 		check(t, testcase{ | ||||||
| 			fields: fields{ | 			fields: fields{ | ||||||
| 				skipNeeds:    false, | 				skipNeeds:              false, | ||||||
| 				includeNeeds: true, | 				includeNeeds:           true, | ||||||
|  | 				includeTransitiveNeeds: false, | ||||||
| 			}, | 			}, | ||||||
| 			error: ``, | 			error: ``, | ||||||
| 			files: map[string]string{ | 			files: map[string]string{ | ||||||
|  | @ -561,8 +680,9 @@ kubernetes-external-secrets | ||||||
| 	t.Run("skip-needs=false include-needs=true with not installed and disabled release", func(t *testing.T) { | 	t.Run("skip-needs=false include-needs=true with not installed and disabled release", func(t *testing.T) { | ||||||
| 		check(t, testcase{ | 		check(t, testcase{ | ||||||
| 			fields: fields{ | 			fields: fields{ | ||||||
| 				skipNeeds:    false, | 				skipNeeds:              false, | ||||||
| 				includeNeeds: true, | 				includeTransitiveNeeds: false, | ||||||
|  | 				includeNeeds:           true, | ||||||
| 			}, | 			}, | ||||||
| 			error: ``, | 			error: ``, | ||||||
| 			files: map[string]string{ | 			files: map[string]string{ | ||||||
|  |  | ||||||
|  | @ -106,6 +106,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		noop, | 		noop, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -157,6 +158,7 @@ BAZ: 4 | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		Noop, | 		Noop, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -198,6 +200,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		Noop, | 		Noop, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 	if err == nil { | 	if err == nil { | ||||||
|  | @ -242,6 +245,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		Noop, | 		Noop, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -293,6 +297,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 			err := app.ForEachState( | 			err := app.ForEachState( | ||||||
| 				Noop, | 				Noop, | ||||||
|  | 				false, | ||||||
| 				SetFilter(true), | 				SetFilter(true), | ||||||
| 			) | 			) | ||||||
| 			if testcase.expectErr && err == nil { | 			if testcase.expectErr && err == nil { | ||||||
|  | @ -359,6 +364,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 		err := app.ForEachState( | 		err := app.ForEachState( | ||||||
| 			Noop, | 			Noop, | ||||||
|  | 			false, | ||||||
| 			SetFilter(true), | 			SetFilter(true), | ||||||
| 		) | 		) | ||||||
| 		if testcase.expectErr && err == nil { | 		if testcase.expectErr && err == nil { | ||||||
|  | @ -413,6 +419,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 		err := app.ForEachState( | 		err := app.ForEachState( | ||||||
| 			Noop, | 			Noop, | ||||||
|  | 			false, | ||||||
| 			SetFilter(true), | 			SetFilter(true), | ||||||
| 		) | 		) | ||||||
| 		if testcase.expectErr && err == nil { | 		if testcase.expectErr && err == nil { | ||||||
|  | @ -531,6 +538,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 			err := app.ForEachState( | 			err := app.ForEachState( | ||||||
| 				collectReleases, | 				collectReleases, | ||||||
|  | 				false, | ||||||
| 				SetFilter(true), | 				SetFilter(true), | ||||||
| 			) | 			) | ||||||
| 			if testcase.expectErr { | 			if testcase.expectErr { | ||||||
|  | @ -774,6 +782,7 @@ func runFilterSubHelmFilesTests(testcases []struct { | ||||||
| 
 | 
 | ||||||
| 		err := app.ForEachState( | 		err := app.ForEachState( | ||||||
| 			collectReleases, | 			collectReleases, | ||||||
|  | 			false, | ||||||
| 			SetFilter(true), | 			SetFilter(true), | ||||||
| 		) | 		) | ||||||
| 		if testcase.expectErr { | 		if testcase.expectErr { | ||||||
|  | @ -869,6 +878,7 @@ tillerNs: INLINE_TILLER_NS_2 | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | @ -970,6 +980,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 		err := app.ForEachState( | 		err := app.ForEachState( | ||||||
| 			collectReleases, | 			collectReleases, | ||||||
|  | 			false, | ||||||
| 			SetReverse(testcase.reverse), | 			SetReverse(testcase.reverse), | ||||||
| 			SetFilter(true), | 			SetFilter(true), | ||||||
| 		) | 		) | ||||||
|  | @ -1035,6 +1046,7 @@ bar: "bar1" | ||||||
| 
 | 
 | ||||||
| 		err := app.ForEachState( | 		err := app.ForEachState( | ||||||
| 			collectReleases, | 			collectReleases, | ||||||
|  | 			false, | ||||||
| 			SetFilter(true), | 			SetFilter(true), | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -1157,6 +1169,7 @@ x: | ||||||
| 
 | 
 | ||||||
| 			err := app.ForEachState( | 			err := app.ForEachState( | ||||||
| 				collectReleases, | 				collectReleases, | ||||||
|  | 				false, | ||||||
| 				SetFilter(true), | 				SetFilter(true), | ||||||
| 			) | 			) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -1209,6 +1222,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | @ -1266,6 +1280,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 			err := app.ForEachState( | 			err := app.ForEachState( | ||||||
| 				collectReleases, | 				collectReleases, | ||||||
|  | 				false, | ||||||
| 				SetFilter(true), | 				SetFilter(true), | ||||||
| 			) | 			) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
|  | @ -1316,6 +1331,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | @ -1364,6 +1380,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | @ -1407,6 +1424,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | @ -1450,6 +1468,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | @ -1497,6 +1516,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | @ -2246,8 +2266,9 @@ type configImpl struct { | ||||||
| 	skipCRDs    bool | 	skipCRDs    bool | ||||||
| 	skipDeps    bool | 	skipDeps    bool | ||||||
| 
 | 
 | ||||||
| 	skipNeeds    bool | 	skipNeeds              bool | ||||||
| 	includeNeeds bool | 	includeNeeds           bool | ||||||
|  | 	includeTransitiveNeeds bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a configImpl) Selectors() []string { | func (a configImpl) Selectors() []string { | ||||||
|  | @ -2290,6 +2311,10 @@ func (c configImpl) IncludeNeeds() bool { | ||||||
| 	return c.includeNeeds | 	return c.includeNeeds | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (c configImpl) IncludeTransitiveNeeds() bool { | ||||||
|  | 	return c.includeTransitiveNeeds | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (c configImpl) OutputDir() string { | func (c configImpl) OutputDir() string { | ||||||
| 	return "output/subdir" | 	return "output/subdir" | ||||||
| } | } | ||||||
|  | @ -2315,30 +2340,31 @@ func (c configImpl) Output() string { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type applyConfig struct { | type applyConfig struct { | ||||||
| 	args              string | 	args                   string | ||||||
| 	values            []string | 	values                 []string | ||||||
| 	retainValuesFiles bool | 	retainValuesFiles      bool | ||||||
| 	set               []string | 	set                    []string | ||||||
| 	validate          bool | 	validate               bool | ||||||
| 	skipCleanup       bool | 	skipCleanup            bool | ||||||
| 	skipCRDs          bool | 	skipCRDs               bool | ||||||
| 	skipDeps          bool | 	skipDeps               bool | ||||||
| 	skipNeeds         bool | 	skipNeeds              bool | ||||||
| 	includeNeeds      bool | 	includeNeeds           bool | ||||||
| 	includeTests      bool | 	includeTransitiveNeeds bool | ||||||
| 	suppressSecrets   bool | 	includeTests           bool | ||||||
| 	showSecrets       bool | 	suppressSecrets        bool | ||||||
| 	suppressDiff      bool | 	showSecrets            bool | ||||||
| 	noColor           bool | 	suppressDiff           bool | ||||||
| 	context           int | 	noColor                bool | ||||||
| 	diffOutput        string | 	context                int | ||||||
| 	concurrency       int | 	diffOutput             string | ||||||
| 	detailedExitcode  bool | 	concurrency            int | ||||||
| 	interactive       bool | 	detailedExitcode       bool | ||||||
| 	skipDiffOnInstall bool | 	interactive            bool | ||||||
| 	logger            *zap.SugaredLogger | 	skipDiffOnInstall      bool | ||||||
| 	wait              bool | 	logger                 *zap.SugaredLogger | ||||||
| 	waitForJobs       bool | 	wait                   bool | ||||||
|  | 	waitForJobs            bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a applyConfig) Args() string { | func (a applyConfig) Args() string { | ||||||
|  | @ -2385,6 +2411,10 @@ func (c applyConfig) IncludeNeeds() bool { | ||||||
| 	return c.includeNeeds | 	return c.includeNeeds | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (c applyConfig) IncludeTransitiveNeeds() bool { | ||||||
|  | 	return c.includeTransitiveNeeds | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (a applyConfig) IncludeTests() bool { | func (a applyConfig) IncludeTests() bool { | ||||||
| 	return a.includeTests | 	return a.includeTests | ||||||
| } | } | ||||||
|  | @ -2438,13 +2468,18 @@ func (a applyConfig) SkipDiffOnInstall() bool { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type depsConfig struct { | type depsConfig struct { | ||||||
| 	skipRepos bool | 	skipRepos              bool | ||||||
|  | 	includeTransitiveNeeds bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (d depsConfig) SkipRepos() bool { | func (d depsConfig) SkipRepos() bool { | ||||||
| 	return d.skipRepos | 	return d.skipRepos | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (d depsConfig) IncludeTransitiveNeeds() bool { | ||||||
|  | 	return d.includeTransitiveNeeds | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (d depsConfig) Args() string { | func (d depsConfig) Args() string { | ||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
|  | @ -4482,7 +4517,8 @@ See https://github.com/roboll/helmfile/issues/878 for more information. | ||||||
| 				}, tc.files) | 				}, tc.files) | ||||||
| 
 | 
 | ||||||
| 				depsErr := app.Deps(depsConfig{ | 				depsErr := app.Deps(depsConfig{ | ||||||
| 					skipRepos: false, | 					skipRepos:              false, | ||||||
|  | 					includeTransitiveNeeds: false, | ||||||
| 				}) | 				}) | ||||||
| 
 | 
 | ||||||
| 				if tc.error == "" && depsErr != nil { | 				if tc.error == "" && depsErr != nil { | ||||||
|  | @ -4777,6 +4813,7 @@ releases: | ||||||
| 
 | 
 | ||||||
| 	err := app.ForEachState( | 	err := app.ForEachState( | ||||||
| 		collectReleases, | 		collectReleases, | ||||||
|  | 		false, | ||||||
| 		SetFilter(true), | 		SetFilter(true), | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -23,15 +23,18 @@ type DeprecatedChartsConfigProvider interface { | ||||||
| 
 | 
 | ||||||
| 	concurrencyConfig | 	concurrencyConfig | ||||||
| 	loggingConfig | 	loggingConfig | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type DepsConfigProvider interface { | type DepsConfigProvider interface { | ||||||
| 	Args() string | 	Args() string | ||||||
| 	SkipRepos() bool | 	SkipRepos() bool | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ReposConfigProvider interface { | type ReposConfigProvider interface { | ||||||
| 	Args() string | 	Args() string | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ApplyConfigProvider interface { | type ApplyConfigProvider interface { | ||||||
|  | @ -63,6 +66,7 @@ type ApplyConfigProvider interface { | ||||||
| 
 | 
 | ||||||
| 	SkipNeeds() bool | 	SkipNeeds() bool | ||||||
| 	IncludeNeeds() bool | 	IncludeNeeds() bool | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| 
 | 
 | ||||||
| 	concurrencyConfig | 	concurrencyConfig | ||||||
| 	interactive | 	interactive | ||||||
|  | @ -81,6 +85,7 @@ type SyncConfigProvider interface { | ||||||
| 
 | 
 | ||||||
| 	SkipNeeds() bool | 	SkipNeeds() bool | ||||||
| 	IncludeNeeds() bool | 	IncludeNeeds() bool | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| 
 | 
 | ||||||
| 	concurrencyConfig | 	concurrencyConfig | ||||||
| 	loggingConfig | 	loggingConfig | ||||||
|  | @ -174,6 +179,7 @@ type TemplateConfigProvider interface { | ||||||
| 	OutputDir() string | 	OutputDir() string | ||||||
| 	IncludeCRDs() bool | 	IncludeCRDs() bool | ||||||
| 	IncludeNeeds() bool | 	IncludeNeeds() bool | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| 
 | 
 | ||||||
| 	concurrencyConfig | 	concurrencyConfig | ||||||
| } | } | ||||||
|  | @ -183,6 +189,7 @@ type WriteValuesConfigProvider interface { | ||||||
| 	Set() []string | 	Set() []string | ||||||
| 	OutputFileTemplate() string | 	OutputFileTemplate() string | ||||||
| 	SkipDeps() bool | 	SkipDeps() bool | ||||||
|  | 	IncludeTransitiveNeeds() bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type StatusesConfigProvider interface { | type StatusesConfigProvider interface { | ||||||
|  |  | ||||||
|  | @ -103,8 +103,9 @@ func TestDestroy_2(t *testing.T) { | ||||||
| 
 | 
 | ||||||
| 			destroyErr := app.Destroy(destroyConfig{ | 			destroyErr := app.Destroy(destroyConfig{ | ||||||
| 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | 				// if we check log output, concurrency must be 1. otherwise the test becomes non-deterministic.
 | ||||||
| 				concurrency: tc.concurrency, | 				concurrency:            tc.concurrency, | ||||||
| 				logger:      logger, | 				logger:                 logger, | ||||||
|  | 				includeTransitiveNeeds: false, | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			if tc.error == "" && destroyErr != nil { | 			if tc.error == "" && destroyErr != nil { | ||||||
|  |  | ||||||
|  | @ -23,11 +23,12 @@ const ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type destroyConfig struct { | type destroyConfig struct { | ||||||
| 	args        string | 	args                   string | ||||||
| 	concurrency int | 	concurrency            int | ||||||
| 	interactive bool | 	interactive            bool | ||||||
| 	skipDeps    bool | 	skipDeps               bool | ||||||
| 	logger      *zap.SugaredLogger | 	logger                 *zap.SugaredLogger | ||||||
|  | 	includeTransitiveNeeds bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (d destroyConfig) Args() string { | func (d destroyConfig) Args() string { | ||||||
|  | @ -50,6 +51,10 @@ func (d destroyConfig) SkipDeps() bool { | ||||||
| 	return d.skipDeps | 	return d.skipDeps | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (d destroyConfig) IncludeTransitiveNeeds() bool { | ||||||
|  | 	return d.includeTransitiveNeeds | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func TestDestroy(t *testing.T) { | func TestDestroy(t *testing.T) { | ||||||
| 	type testcase struct { | 	type testcase struct { | ||||||
| 		helm3       bool | 		helm3       bool | ||||||
|  |  | ||||||
|  | @ -98,7 +98,7 @@ func (r *Run) withPreparedCharts(helmfileCommand string, opts state.ChartPrepare | ||||||
| func (r *Run) Deps(c DepsConfigProvider) []error { | func (r *Run) Deps(c DepsConfigProvider) []error { | ||||||
| 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | 	r.helm.SetExtraArgs(argparser.GetArgs(c.Args(), r.state)...) | ||||||
| 
 | 
 | ||||||
| 	return r.state.UpdateDeps(r.helm) | 	return r.state.UpdateDeps(r.helm, c.IncludeTransitiveNeeds()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (r *Run) Repos(c ReposConfigProvider) error { | func (r *Run) Repos(c ReposConfigProvider) error { | ||||||
|  |  | ||||||
|  | @ -1,8 +1,9 @@ | ||||||
| package state | package state | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"github.com/google/go-cmp/cmp" |  | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/google/go-cmp/cmp" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func TestSelectReleasesWithOverrides(t *testing.T) { | func TestSelectReleasesWithOverrides(t *testing.T) { | ||||||
|  | @ -79,7 +80,76 @@ func TestSelectReleasesWithOverrides(t *testing.T) { | ||||||
| 	for _, tc := range testcases { | 	for _, tc := range testcases { | ||||||
| 		state.Selectors = tc.selector | 		state.Selectors = tc.selector | ||||||
| 
 | 
 | ||||||
| 		rs, err := state.GetSelectedReleasesWithOverrides() | 		rs, err := state.GetSelectedReleasesWithOverrides(false) | ||||||
|  | 		if err != nil { | ||||||
|  | 			t.Fatalf("%s %s: %v", tc.selector, tc.subject, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		var got []string | ||||||
|  | 
 | ||||||
|  | 		for _, r := range rs { | ||||||
|  | 			got = append(got, r.Name) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if d := cmp.Diff(tc.want, got); d != "" { | ||||||
|  | 			t.Errorf("%s %s: %s", tc.selector, tc.subject, d) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestSelectReleasesWithOverridesWithIncludedTransitives(t *testing.T) { | ||||||
|  | 	type testcase struct { | ||||||
|  | 		subject                string | ||||||
|  | 		selector               []string | ||||||
|  | 		want                   []string | ||||||
|  | 		includeTransitiveNeeds bool | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	testcases := []testcase{ | ||||||
|  | 		{ | ||||||
|  | 			subject:                "include transitives", | ||||||
|  | 			selector:               []string{"name=serviceA"}, | ||||||
|  | 			want:                   []string{"serviceA"}, | ||||||
|  | 			includeTransitiveNeeds: false, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			subject:                "include transitives", | ||||||
|  | 			selector:               []string{"name=serviceA"}, | ||||||
|  | 			want:                   []string{"serviceA", "serviceB", "serviceC"}, | ||||||
|  | 			includeTransitiveNeeds: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	example := []byte(`releases: | ||||||
|  | - name: serviceA | ||||||
|  |   namespace: default | ||||||
|  |   chart: stable/testchart | ||||||
|  |   needs: | ||||||
|  |     - serviceB | ||||||
|  | - name: serviceB | ||||||
|  |   namespace: default | ||||||
|  |   chart: stable/testchart | ||||||
|  |   needs: | ||||||
|  |     - serviceC | ||||||
|  | - name: serviceC | ||||||
|  |   namespace: default | ||||||
|  |   chart: stable/testchart | ||||||
|  | - name: serviceD | ||||||
|  |   namespace: default | ||||||
|  |   chart: stable/testchart | ||||||
|  | `) | ||||||
|  | 
 | ||||||
|  | 	state := stateTestEnv{ | ||||||
|  | 		Files: map[string]string{ | ||||||
|  | 			"/helmfile.yaml": string(example), | ||||||
|  | 		}, | ||||||
|  | 		WorkDir: "/", | ||||||
|  | 	}.MustLoadState(t, "/helmfile.yaml", "default") | ||||||
|  | 
 | ||||||
|  | 	for _, tc := range testcases { | ||||||
|  | 		state.Selectors = tc.selector | ||||||
|  | 
 | ||||||
|  | 		rs, err := state.GetSelectedReleasesWithOverrides(tc.includeTransitiveNeeds) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			t.Fatalf("%s %s: %v", tc.selector, tc.subject, err) | 			t.Fatalf("%s %s: %v", tc.selector, tc.subject, err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -398,36 +398,6 @@ type RepoUpdater interface { | ||||||
| 	RegistryLogin(name string, username string, password string) error | 	RegistryLogin(name string, username string, password string) error | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // getRepositoriesToSync returns the names of repositories to be updated
 |  | ||||||
| func (st *HelmState) getRepositoriesToSync() (map[string]bool, error) { |  | ||||||
| 	releases, err := st.GetSelectedReleasesWithOverrides() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	repositoriesToUpdate := map[string]bool{} |  | ||||||
| 
 |  | ||||||
| 	if len(releases) == 0 { |  | ||||||
| 		for _, repo := range st.Repositories { |  | ||||||
| 			repositoriesToUpdate[repo.Name] = true |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		return repositoriesToUpdate, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, release := range releases { |  | ||||||
| 		if release.Installed == nil || *release.Installed { |  | ||||||
| 			chart := strings.Split(release.Chart, "/") |  | ||||||
| 			if len(chart) == 1 { |  | ||||||
| 				continue |  | ||||||
| 			} |  | ||||||
| 			repositoriesToUpdate[chart[0]] = true |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return repositoriesToUpdate, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]string, error) { | func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]string, error) { | ||||||
| 	var updated []string | 	var updated []string | ||||||
| 
 | 
 | ||||||
|  | @ -968,11 +938,12 @@ type ChartPrepareOptions struct { | ||||||
| 	SkipCleanup   bool | 	SkipCleanup   bool | ||||||
| 	// Validate is a helm-3-only option. When it is set to true, it configures chartify to pass --validate to helm-template run by it.
 | 	// Validate is a helm-3-only option. When it is set to true, it configures chartify to pass --validate to helm-template run by it.
 | ||||||
| 	// It's required when one of your chart relies on Capabilities.APIVersions in a template
 | 	// It's required when one of your chart relies on Capabilities.APIVersions in a template
 | ||||||
| 	Validate    bool | 	Validate               bool | ||||||
| 	IncludeCRDs *bool | 	IncludeCRDs            *bool | ||||||
| 	Wait        bool | 	Wait                   bool | ||||||
| 	WaitForJobs bool | 	WaitForJobs            bool | ||||||
| 	OutputDir   string | 	OutputDir              string | ||||||
|  | 	IncludeTransitiveNeeds bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type chartPrepareResult struct { | type chartPrepareResult struct { | ||||||
|  | @ -1026,7 +997,7 @@ func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurre | ||||||
| 
 | 
 | ||||||
| 		// This and releasesNeedCharts ensures that we run operations like helm-dep-build and prepare-hook calls only on
 | 		// This and releasesNeedCharts ensures that we run operations like helm-dep-build and prepare-hook calls only on
 | ||||||
| 		// releases that are (1) selected by the selectors and (2) to be installed.
 | 		// releases that are (1) selected by the selectors and (2) to be installed.
 | ||||||
| 		selected, err = st.GetSelectedReleasesWithOverrides() | 		selected, err = st.GetSelectedReleasesWithOverrides(opts.IncludeTransitiveNeeds) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, []error{err} | 			return nil, []error{err} | ||||||
| 		} | 		} | ||||||
|  | @ -2047,16 +2018,16 @@ func (st *HelmState) GetReleasesWithOverrides() []ReleaseSpec { | ||||||
| 	return rs | 	return rs | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (st *HelmState) SelectReleasesWithOverrides() ([]Release, error) { | func (st *HelmState) SelectReleasesWithOverrides(includeTransitiveNeeds bool) ([]Release, error) { | ||||||
| 	values := st.Values() | 	values := st.Values() | ||||||
| 	rs, err := markExcludedReleases(st.GetReleasesWithOverrides(), st.Selectors, st.CommonLabels, values) | 	rs, err := markExcludedReleases(st.GetReleasesWithOverrides(), st.Selectors, st.CommonLabels, values, includeTransitiveNeeds) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return rs, nil | 	return rs, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabels map[string]string, values map[string]interface{}) ([]Release, error) { | func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabels map[string]string, values map[string]interface{}, includeTransitiveNeeds bool) ([]Release, error) { | ||||||
| 	var filteredReleases []Release | 	var filteredReleases []Release | ||||||
| 	filters := []ReleaseFilter{} | 	filters := []ReleaseFilter{} | ||||||
| 	for _, label := range selectors { | 	for _, label := range selectors { | ||||||
|  | @ -2113,12 +2084,52 @@ func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabe | ||||||
| 		} | 		} | ||||||
| 		filteredReleases = append(filteredReleases, res) | 		filteredReleases = append(filteredReleases, res) | ||||||
| 	} | 	} | ||||||
| 
 | 	if includeTransitiveNeeds { | ||||||
|  | 		unmarkNeedsAndTransitives(filteredReleases, releases) | ||||||
|  | 	} | ||||||
| 	return filteredReleases, nil | 	return filteredReleases, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) { | func unmarkNeedsAndTransitives(filteredReleases []Release, allReleases []ReleaseSpec) { | ||||||
| 	filteredReleases, err := st.SelectReleasesWithOverrides() | 	needsWithTranstives := collectAllNeedsWithTransitives(filteredReleases, allReleases) | ||||||
|  | 	unmarkReleases(needsWithTranstives, filteredReleases) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func collectAllNeedsWithTransitives(filteredReleases []Release, allReleases []ReleaseSpec) map[string]struct{} { | ||||||
|  | 	needsWithTranstives := map[string]struct{}{} | ||||||
|  | 	for _, r := range filteredReleases { | ||||||
|  | 		if !r.Filtered { | ||||||
|  | 			collectNeedsWithTransitives(r.ReleaseSpec, allReleases, needsWithTranstives) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return needsWithTranstives | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func unmarkReleases(toUnmark map[string]struct{}, releases []Release) { | ||||||
|  | 	for i, r := range releases { | ||||||
|  | 		if _, ok := toUnmark[ReleaseToID(&r.ReleaseSpec)]; ok { | ||||||
|  | 			releases[i].Filtered = false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func collectNeedsWithTransitives(release ReleaseSpec, allReleases []ReleaseSpec, needsWithTranstives map[string]struct{}) { | ||||||
|  | 	for _, id := range release.Needs { | ||||||
|  | 		if _, exists := needsWithTranstives[id]; !exists { | ||||||
|  | 			needsWithTranstives[id] = struct{}{} | ||||||
|  | 			releaseParts := strings.Split(id, "/") | ||||||
|  | 			releaseName := releaseParts[len(releaseParts)-1] | ||||||
|  | 			for _, r := range allReleases { | ||||||
|  | 				if r.Name == releaseName { | ||||||
|  | 					collectNeedsWithTransitives(r, allReleases, needsWithTranstives) | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (st *HelmState) GetSelectedReleasesWithOverrides(includeTransitiveNeeds bool) ([]ReleaseSpec, error) { | ||||||
|  | 	filteredReleases, err := st.SelectReleasesWithOverrides(includeTransitiveNeeds) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -2133,8 +2144,8 @@ func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
 | // FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
 | ||||||
| func (st *HelmState) FilterReleases() error { | func (st *HelmState) FilterReleases(includeTransitiveNeeds bool) error { | ||||||
| 	releases, err := st.GetSelectedReleasesWithOverrides() | 	releases, err := st.GetSelectedReleasesWithOverrides(includeTransitiveNeeds) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -2209,7 +2220,7 @@ func (st *HelmState) ResolveDeps() (*HelmState, error) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // UpdateDeps wrapper for updating dependencies on the releases
 | // UpdateDeps wrapper for updating dependencies on the releases
 | ||||||
| func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error { | func (st *HelmState) UpdateDeps(helm helmexec.Interface, includeTransitiveNeeds bool) []error { | ||||||
| 	var selected []ReleaseSpec | 	var selected []ReleaseSpec | ||||||
| 
 | 
 | ||||||
| 	if len(st.Selectors) > 0 { | 	if len(st.Selectors) > 0 { | ||||||
|  | @ -2217,7 +2228,7 @@ func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error { | ||||||
| 
 | 
 | ||||||
| 		// This and releasesNeedCharts ensures that we run operations like helm-dep-build and prepare-hook calls only on
 | 		// This and releasesNeedCharts ensures that we run operations like helm-dep-build and prepare-hook calls only on
 | ||||||
| 		// releases that are (1) selected by the selectors and (2) to be installed.
 | 		// releases that are (1) selected by the selectors and (2) to be installed.
 | ||||||
| 		selected, err = st.GetSelectedReleasesWithOverrides() | 		selected, err = st.GetSelectedReleasesWithOverrides(includeTransitiveNeeds) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return []error{err} | 			return []error{err} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -100,14 +100,15 @@ func (st *HelmState) iterateOnReleases(helm helmexec.Interface, concurrency int, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type PlanOptions struct { | type PlanOptions struct { | ||||||
| 	Reverse          bool | 	Reverse                bool | ||||||
| 	IncludeNeeds     bool | 	IncludeNeeds           bool | ||||||
| 	SkipNeeds        bool | 	IncludeTransitiveNeeds bool | ||||||
| 	SelectedReleases []ReleaseSpec | 	SkipNeeds              bool | ||||||
|  | 	SelectedReleases       []ReleaseSpec | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (st *HelmState) PlanReleases(opts PlanOptions) ([][]Release, error) { | func (st *HelmState) PlanReleases(opts PlanOptions) ([][]Release, error) { | ||||||
| 	marked, err := st.SelectReleasesWithOverrides() | 	marked, err := st.SelectReleasesWithOverrides(opts.IncludeTransitiveNeeds) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1848,7 +1848,7 @@ generated: 2019-05-16T15:42:45.50486+09:00 | ||||||
| 	}) | 	}) | ||||||
| 	fs.Cwd = basePath | 	fs.Cwd = basePath | ||||||
| 	state = injectFs(state, fs) | 	state = injectFs(state, fs) | ||||||
| 	errs := state.UpdateDeps(helm) | 	errs := state.UpdateDeps(helm, false) | ||||||
| 
 | 
 | ||||||
| 	want := []string{"/example", "./example", generatedDir} | 	want := []string{"/example", "./example", generatedDir} | ||||||
| 	if !reflect.DeepEqual(helm.Charts, want) { | 	if !reflect.DeepEqual(helm.Charts, want) { | ||||||
|  | @ -2141,7 +2141,7 @@ func TestHelmState_NoReleaseMatched(t *testing.T) { | ||||||
| 				RenderedValues: map[string]interface{}{}, | 				RenderedValues: map[string]interface{}{}, | ||||||
| 			} | 			} | ||||||
| 			state.Selectors = []string{tt.labels} | 			state.Selectors = []string{tt.labels} | ||||||
| 			errs := state.FilterReleases() | 			errs := state.FilterReleases(false) | ||||||
| 			if (errs != nil) != tt.wantErr { | 			if (errs != nil) != tt.wantErr { | ||||||
| 				t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr) | 				t.Errorf("ReleaseStatuses() for %s error = %v, wantErr %v", tt.name, errs, tt.wantErr) | ||||||
| 				return | 				return | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue