mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-18 04:45:42 +02:00
Update DAG with dependencies (#1477)
* Add show-dag command Signed-off-by: vlpav030 <vpav.030@gmail.com>
This commit is contained in:
@@ -530,6 +530,23 @@ func (a *App) Test(c TestConfigProvider) error {
|
||||
}, false, SetFilter(true))
|
||||
}
|
||||
|
||||
func (a *App) PrintDAGState(c DAGConfigProvider) error {
|
||||
var err error
|
||||
return a.ForEachState(func(run *Run) (ok bool, errs []error) {
|
||||
err = run.withPreparedCharts("show-dag", state.ChartPrepareOptions{
|
||||
SkipRepos: true,
|
||||
SkipDeps: true,
|
||||
Concurrency: 2,
|
||||
}, func() {
|
||||
err = a.dag(run)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
})
|
||||
return ok, errs
|
||||
}, false, SetFilter(true))
|
||||
}
|
||||
|
||||
func (a *App) PrintState(c StateConfigProvider) error {
|
||||
return a.ForEachState(func(run *Run) (_ bool, errs []error) {
|
||||
err := run.withPreparedCharts("build", state.ChartPrepareOptions{
|
||||
@@ -583,6 +600,19 @@ func (a *App) PrintState(c StateConfigProvider) error {
|
||||
}, false, SetFilter(true))
|
||||
}
|
||||
|
||||
func (a *App) dag(r *Run) error {
|
||||
st := r.state
|
||||
|
||||
batches, err := st.PlanReleases(state.PlanOptions{SelectedReleases: st.Releases, Reverse: false, SkipNeeds: false, IncludeNeeds: true, IncludeTransitiveNeeds: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Print(printDAG(batches))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) ListReleases(c ListConfigProvider) error {
|
||||
var releases []*HelmRelease
|
||||
|
||||
@@ -990,6 +1020,28 @@ func printBatches(batches [][]state.Release) string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func printDAG(batches [][]state.Release) string {
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
w := new(tabwriter.Writer)
|
||||
|
||||
w.Init(buf, 0, 1, 1, ' ', 0)
|
||||
|
||||
fmt.Fprintln(w, "GROUP\tRELEASE\tDEPENDENCIES")
|
||||
|
||||
for i, batch := range batches {
|
||||
for _, r := range batch {
|
||||
id := state.ReleaseToID(&r.ReleaseSpec)
|
||||
needs := r.ReleaseSpec.Needs
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\n", i+1, id, strings.Join(needs, ", "))
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.Flush()
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// nolint: unparam
|
||||
func withDAG(templated *state.HelmState, helm helmexec.Interface, logger *zap.SugaredLogger, opts state.PlanOptions, converge func(*state.HelmState, helmexec.Interface) (bool, []error)) (bool, []error) {
|
||||
batches, err := templated.PlanReleases(opts)
|
||||
|
||||
@@ -266,6 +266,8 @@ type StateConfigProvider interface {
|
||||
EmbedValues() bool
|
||||
}
|
||||
|
||||
type DAGConfigProvider any
|
||||
|
||||
type concurrencyConfig interface {
|
||||
Concurrency() int
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"go.uber.org/zap"
|
||||
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/testhelper"
|
||||
"github.com/helmfile/helmfile/pkg/testutil"
|
||||
)
|
||||
|
||||
func testDAG(t *testing.T, cfg configImpl) {
|
||||
type testcase struct {
|
||||
environment string
|
||||
ns string
|
||||
error string
|
||||
selectors []string
|
||||
expected string
|
||||
}
|
||||
|
||||
check := func(t *testing.T, tc testcase, cfg configImpl) {
|
||||
t.Helper()
|
||||
|
||||
bs := runWithLogCapture(t, "debug", func(t *testing.T, logger *zap.SugaredLogger) {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating vals runtime: %v", err)
|
||||
}
|
||||
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
development: {}
|
||||
shared: {}
|
||||
---
|
||||
releases:
|
||||
- name: logging
|
||||
chart: incubator/raw
|
||||
namespace: kube-system
|
||||
- name: kubernetes-external-secrets
|
||||
chart: incubator/raw
|
||||
namespace: kube-system
|
||||
needs:
|
||||
- kube-system/logging
|
||||
- name: external-secrets
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
labels:
|
||||
app: test
|
||||
needs:
|
||||
- kube-system/kubernetes-external-secrets
|
||||
- name: my-release
|
||||
chart: incubator/raw
|
||||
namespace: default
|
||||
labels:
|
||||
app: test
|
||||
needs:
|
||||
- default/external-secrets
|
||||
# Disabled releases are treated as missing
|
||||
- name: disabled
|
||||
chart: incubator/raw
|
||||
namespace: kube-system
|
||||
installed: false
|
||||
- name: test2
|
||||
chart: incubator/raw
|
||||
needs:
|
||||
- kube-system/disabled
|
||||
- name: test3
|
||||
chart: incubator/raw
|
||||
needs:
|
||||
- test2
|
||||
- name: test4
|
||||
chart: incubator/raw
|
||||
needs:
|
||||
- test2
|
||||
- test3
|
||||
`,
|
||||
}
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: ffs.DefaultFileSystem(),
|
||||
OverrideKubeContext: "default",
|
||||
Env: tc.environment,
|
||||
Logger: logger,
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
expectNoCallsToHelm(app)
|
||||
|
||||
if tc.ns != "" {
|
||||
app.Namespace = tc.ns
|
||||
}
|
||||
|
||||
if tc.selectors != nil {
|
||||
app.Selectors = tc.selectors
|
||||
}
|
||||
|
||||
var dagErr error
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
dagErr = app.PrintDAGState(cfg)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
var gotErr string
|
||||
if dagErr != nil {
|
||||
gotErr = dagErr.Error()
|
||||
}
|
||||
|
||||
if d := cmp.Diff(tc.error, gotErr); d != "" {
|
||||
t.Fatalf("unexpected error: want (-), got (+): %s", d)
|
||||
}
|
||||
|
||||
assert.Equal(t, tc.expected, out)
|
||||
})
|
||||
|
||||
testhelper.RequireLog(t, "dag_test", bs)
|
||||
}
|
||||
|
||||
t.Run("DAG lists dependencies in order", func(t *testing.T) {
|
||||
check(t, testcase{
|
||||
environment: "default",
|
||||
expected: `GROUP RELEASE DEPENDENCIES
|
||||
1 default/kube-system/logging
|
||||
1 default/kube-system/disabled
|
||||
2 default/kube-system/kubernetes-external-secrets default/kube-system/logging
|
||||
2 default//test2 default/kube-system/disabled
|
||||
3 default/default/external-secrets default/kube-system/kubernetes-external-secrets
|
||||
3 default//test3 default//test2
|
||||
4 default/default/my-release default/default/external-secrets
|
||||
4 default//test4 default//test2, default//test3
|
||||
`,
|
||||
}, cfg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestDAG(t *testing.T) {
|
||||
t.Run("DAG", func(t *testing.T) {
|
||||
testDAG(t, configImpl{})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
processing file "helmfile.yaml" in directory "."
|
||||
changing working directory to "/path/to"
|
||||
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: environments:
|
||||
2: development: {}
|
||||
3: shared: {}
|
||||
|
||||
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: environments:
|
||||
2: development: {}
|
||||
3: shared: {}
|
||||
|
||||
merged environment: &{default map[] map[]}
|
||||
first-pass rendering starting for "helmfile.yaml.part.1": inherited=&{default map[] map[]}, overrode=<nil>
|
||||
first-pass uses: &{default map[] map[]}
|
||||
first-pass rendering output of "helmfile.yaml.part.1":
|
||||
0: releases:
|
||||
1: - name: logging
|
||||
2: chart: incubator/raw
|
||||
3: namespace: kube-system
|
||||
4: - name: kubernetes-external-secrets
|
||||
5: chart: incubator/raw
|
||||
6: namespace: kube-system
|
||||
7: needs:
|
||||
8: - kube-system/logging
|
||||
9: - name: external-secrets
|
||||
10: chart: incubator/raw
|
||||
11: namespace: default
|
||||
12: labels:
|
||||
13: app: test
|
||||
14: needs:
|
||||
15: - kube-system/kubernetes-external-secrets
|
||||
16: - name: my-release
|
||||
17: chart: incubator/raw
|
||||
18: namespace: default
|
||||
19: labels:
|
||||
20: app: test
|
||||
21: needs:
|
||||
22: - default/external-secrets
|
||||
23: # Disabled releases are treated as missing
|
||||
24: - name: disabled
|
||||
25: chart: incubator/raw
|
||||
26: namespace: kube-system
|
||||
27: installed: false
|
||||
28: - name: test2
|
||||
29: chart: incubator/raw
|
||||
30: needs:
|
||||
31: - kube-system/disabled
|
||||
32: - name: test3
|
||||
33: chart: incubator/raw
|
||||
34: needs:
|
||||
35: - test2
|
||||
36: - name: test4
|
||||
37: chart: incubator/raw
|
||||
38: needs:
|
||||
39: - test2
|
||||
40: - test3
|
||||
41:
|
||||
|
||||
first-pass produced: &{default map[] map[]}
|
||||
first-pass rendering result of "helmfile.yaml.part.1": {default map[] map[]}
|
||||
vals:
|
||||
map[]
|
||||
defaultVals:[]
|
||||
second-pass rendering result of "helmfile.yaml.part.1":
|
||||
0: releases:
|
||||
1: - name: logging
|
||||
2: chart: incubator/raw
|
||||
3: namespace: kube-system
|
||||
4: - name: kubernetes-external-secrets
|
||||
5: chart: incubator/raw
|
||||
6: namespace: kube-system
|
||||
7: needs:
|
||||
8: - kube-system/logging
|
||||
9: - name: external-secrets
|
||||
10: chart: incubator/raw
|
||||
11: namespace: default
|
||||
12: labels:
|
||||
13: app: test
|
||||
14: needs:
|
||||
15: - kube-system/kubernetes-external-secrets
|
||||
16: - name: my-release
|
||||
17: chart: incubator/raw
|
||||
18: namespace: default
|
||||
19: labels:
|
||||
20: app: test
|
||||
21: needs:
|
||||
22: - default/external-secrets
|
||||
23: # Disabled releases are treated as missing
|
||||
24: - name: disabled
|
||||
25: chart: incubator/raw
|
||||
26: namespace: kube-system
|
||||
27: installed: false
|
||||
28: - name: test2
|
||||
29: chart: incubator/raw
|
||||
30: needs:
|
||||
31: - kube-system/disabled
|
||||
32: - name: test3
|
||||
33: chart: incubator/raw
|
||||
34: needs:
|
||||
35: - test2
|
||||
36: - name: test4
|
||||
37: chart: incubator/raw
|
||||
38: needs:
|
||||
39: - test2
|
||||
40: - test3
|
||||
41:
|
||||
|
||||
merged environment: &{default map[] map[]}
|
||||
WARNING: release test2 needs disabled, but disabled is not installed due to installed: false. Either mark disabled as installed or remove disabled from test2's needs
|
||||
changing working directory back to "/path/to"
|
||||
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
// ShowDAGOptions is the options for the build command
|
||||
type ShowDAGOptions struct {
|
||||
}
|
||||
|
||||
// NewShowDAGOptions creates a new ShowDAGOptions
|
||||
func NewShowDAGOptions() *ShowDAGOptions {
|
||||
return &ShowDAGOptions{}
|
||||
}
|
||||
|
||||
// ShowDAGImpl is impl for applyOptions
|
||||
type ShowDAGImpl struct {
|
||||
*GlobalImpl
|
||||
*ShowDAGOptions
|
||||
}
|
||||
|
||||
// NewShowDAGImpl creates a new ShowDAGImpl
|
||||
func NewShowDAGImpl(g *GlobalImpl, b *ShowDAGOptions) *ShowDAGImpl {
|
||||
return &ShowDAGImpl{
|
||||
GlobalImpl: g,
|
||||
ShowDAGOptions: b,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user