Update DAG with dependencies (#1477)

* Add show-dag command

Signed-off-by: vlpav030 <vpav.030@gmail.com>
This commit is contained in:
Vladan Pavlovic
2024-04-27 07:37:28 +08:00
committed by GitHub
parent ee8cee5422
commit dc20eb10c5
8 changed files with 389 additions and 0 deletions
+52
View File
@@ -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)
+2
View File
@@ -266,6 +266,8 @@ type StateConfigProvider interface {
EmbedValues() bool
}
type DAGConfigProvider any
type concurrencyConfig interface {
Concurrency() int
}
+147
View File
@@ -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"
+24
View File
@@ -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,
}
}