feat: add print-env command
Signed-off-by: Dominik Schmidt <dev@dominik-schmidt.de>
This commit is contained in:
parent
2158502ce0
commit
586408691e
|
|
@ -0,0 +1,37 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/app"
|
||||
"github.com/helmfile/helmfile/pkg/config"
|
||||
)
|
||||
|
||||
// NewPrintEnvCmd returns print-env subcmd
|
||||
func NewPrintEnvCmd(globalCfg *config.GlobalImpl) *cobra.Command {
|
||||
printEnvOptions := config.NewPrintEnvOptions()
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "print-env",
|
||||
Short: "Print parsed environment configuration including values and secrets",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
printEnvImpl := config.NewPrintEnvImpl(globalCfg, printEnvOptions)
|
||||
err := config.NewCLIConfigImpl(printEnvImpl.GlobalImpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := printEnvImpl.ValidateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a := app.New(printEnvImpl)
|
||||
return toCLIError(printEnvImpl.GlobalImpl, a.PrintEnv(printEnvImpl))
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&printEnvOptions.Output, "output", "yaml", "output format: yaml or json")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
@ -104,6 +104,7 @@ func NewRootCmd(globalConfig *config.GlobalOptions) (*cobra.Command, error) {
|
|||
NewDiffCmd(globalImpl),
|
||||
NewStatusCmd(globalImpl),
|
||||
NewShowDAGCmd(globalImpl),
|
||||
NewPrintEnvCmd(globalImpl),
|
||||
extension.NewVersionCobraCmd(
|
||||
versionOpts...,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -294,6 +294,10 @@ type InitConfigProvider interface {
|
|||
Force() bool
|
||||
}
|
||||
|
||||
type PrintEnvConfigProvider interface {
|
||||
Output() string
|
||||
}
|
||||
|
||||
// reset/reuse values helm cli flags handling for apply/sync/diff
|
||||
type valuesControlMode interface {
|
||||
ReuseValues() bool
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/helmfile/helmfile/pkg/yaml"
|
||||
)
|
||||
|
||||
// PrintEnv prints the parsed environment configuration
|
||||
func (a *App) PrintEnv(c PrintEnvConfigProvider) error {
|
||||
docCount := 0
|
||||
|
||||
err := a.ForEachState(func(run *Run) (_ bool, errs []error) {
|
||||
st := run.state
|
||||
|
||||
// Get merged values (includes secrets if present)
|
||||
values, err := st.Env.GetMergedValues()
|
||||
if err != nil {
|
||||
return false, []error{fmt.Errorf("failed to get merged values: %w", err)}
|
||||
}
|
||||
|
||||
// Get full absolute path to identify which helmfile this environment comes from
|
||||
filePath := st.FilePath
|
||||
if fullPath, err := st.FullFilePath(); err != nil {
|
||||
a.Logger.Warnf("failed to get full file path for %s: %v", st.FilePath, err)
|
||||
} else {
|
||||
filePath = fullPath
|
||||
}
|
||||
|
||||
// Prepare output structure - include file path to identify source
|
||||
output := map[string]any{
|
||||
"filePath": filePath,
|
||||
"name": st.Env.Name,
|
||||
"kubeContext": st.Env.KubeContext,
|
||||
"values": values,
|
||||
}
|
||||
|
||||
// Marshal based on output format
|
||||
var outputBytes []byte
|
||||
switch c.Output() {
|
||||
case "json":
|
||||
// For JSON, print array of documents
|
||||
if docCount > 0 {
|
||||
fmt.Println(",")
|
||||
} else {
|
||||
fmt.Println("[")
|
||||
}
|
||||
outputBytes, err = json.MarshalIndent(output, " ", " ")
|
||||
if err != nil {
|
||||
return false, []error{fmt.Errorf("failed to marshal to JSON: %w", err)}
|
||||
}
|
||||
fmt.Print(" ")
|
||||
fmt.Print(string(outputBytes))
|
||||
case "yaml", "":
|
||||
// For YAML, use multi-document format with --- separator
|
||||
if docCount > 0 {
|
||||
fmt.Println("---")
|
||||
}
|
||||
outputBytes, err = yaml.Marshal(output)
|
||||
if err != nil {
|
||||
return false, []error{fmt.Errorf("failed to marshal to YAML: %w", err)}
|
||||
}
|
||||
fmt.Print(string(outputBytes))
|
||||
default:
|
||||
return false, []error{fmt.Errorf("unsupported output format: %s (supported: yaml, json)", c.Output())}
|
||||
}
|
||||
|
||||
docCount++
|
||||
return false, nil
|
||||
}, false)
|
||||
|
||||
// Close JSON array
|
||||
if c.Output() == "json" && docCount > 0 {
|
||||
fmt.Println()
|
||||
fmt.Println("]")
|
||||
}
|
||||
|
||||
// Suppress "no releases found" error - print-env doesn't need releases
|
||||
if _, ok := err.(*NoMatchingHelmfileError); ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,440 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/helmfile/vals"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
ffs "github.com/helmfile/helmfile/pkg/filesystem"
|
||||
"github.com/helmfile/helmfile/pkg/helmexec"
|
||||
"github.com/helmfile/helmfile/pkg/testhelper"
|
||||
"github.com/helmfile/helmfile/pkg/testutil"
|
||||
)
|
||||
|
||||
func TestPrintEnv_SingleHelmfile_YAML(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
production:
|
||||
kubeContext: prod-cluster
|
||||
values:
|
||||
- region: us-east-1
|
||||
environment: production
|
||||
debug: false
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "production")
|
||||
cfg := configImpl{output: "yaml"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse YAML output
|
||||
var result map[string]any
|
||||
err = yaml.Unmarshal([]byte(out), &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify structure
|
||||
assert.Equal(t, "production", result["name"])
|
||||
assert.Equal(t, "prod-cluster", result["kubeContext"])
|
||||
assert.Contains(t, result["filePath"], "helmfile.yaml")
|
||||
|
||||
// Verify values
|
||||
values, ok := result["values"].(map[string]any)
|
||||
require.True(t, ok, "values should be a map")
|
||||
assert.Equal(t, "us-east-1", values["region"])
|
||||
assert.Equal(t, "production", values["environment"])
|
||||
assert.Equal(t, false, values["debug"])
|
||||
}
|
||||
|
||||
func TestPrintEnv_SingleHelmfile_JSON(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
staging:
|
||||
kubeContext: staging-cluster
|
||||
values:
|
||||
- database:
|
||||
host: db.staging.local
|
||||
port: 5432
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "staging")
|
||||
cfg := configImpl{output: "json"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse JSON output (should be an array)
|
||||
var results []map[string]any
|
||||
err = json.Unmarshal([]byte(out), &results)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1, "should have one environment")
|
||||
|
||||
result := results[0]
|
||||
assert.Equal(t, "staging", result["name"])
|
||||
assert.Equal(t, "staging-cluster", result["kubeContext"])
|
||||
assert.Contains(t, result["filePath"], "helmfile.yaml")
|
||||
|
||||
// Verify nested values
|
||||
values, ok := result["values"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
database, ok := values["database"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "db.staging.local", database["host"])
|
||||
assert.Equal(t, float64(5432), database["port"]) // JSON numbers are float64
|
||||
}
|
||||
|
||||
func TestPrintEnv_MultipleHelmfiles_YAML(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
dev:
|
||||
kubeContext: main-context
|
||||
values:
|
||||
- source: main
|
||||
sharedValue: from-main
|
||||
---
|
||||
helmfiles:
|
||||
- path: sub/helmfile.yaml
|
||||
releases: []
|
||||
`,
|
||||
"/path/to/sub/helmfile.yaml": `
|
||||
environments:
|
||||
dev:
|
||||
kubeContext: sub-context
|
||||
values:
|
||||
- source: sub
|
||||
subValue: from-sub
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "dev")
|
||||
cfg := configImpl{output: "yaml"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Split by --- to get individual documents
|
||||
docs := strings.Split(out, "---\n")
|
||||
// Filter out empty documents
|
||||
var nonEmptyDocs []string
|
||||
for _, doc := range docs {
|
||||
trimmed := strings.TrimSpace(doc)
|
||||
if trimmed != "" {
|
||||
nonEmptyDocs = append(nonEmptyDocs, trimmed)
|
||||
}
|
||||
}
|
||||
|
||||
assert.GreaterOrEqual(t, len(nonEmptyDocs), 2, "should have at least 2 environment documents")
|
||||
|
||||
// Verify each document is valid YAML
|
||||
for i, doc := range nonEmptyDocs {
|
||||
var result map[string]any
|
||||
err := yaml.Unmarshal([]byte(doc), &result)
|
||||
require.NoError(t, err, "document %d should be valid YAML", i)
|
||||
assert.Equal(t, "dev", result["name"], "document %d should have correct name", i)
|
||||
assert.Contains(t, result, "kubeContext", "document %d should have kubeContext", i)
|
||||
assert.Contains(t, result, "values", "document %d should have values", i)
|
||||
assert.Contains(t, result, "filePath", "document %d should have filePath", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintEnv_MultipleHelmfiles_JSON(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
test:
|
||||
kubeContext: main-kube
|
||||
values:
|
||||
- mainKey: mainValue
|
||||
---
|
||||
helmfiles:
|
||||
- path: child1/helmfile.yaml
|
||||
- path: child2/helmfile.yaml
|
||||
releases: []
|
||||
`,
|
||||
"/path/to/child1/helmfile.yaml": `
|
||||
environments:
|
||||
test:
|
||||
kubeContext: child1-kube
|
||||
values:
|
||||
- child1Key: child1Value
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
"/path/to/child2/helmfile.yaml": `
|
||||
environments:
|
||||
test:
|
||||
kubeContext: child2-kube
|
||||
values:
|
||||
- child2Key: child2Value
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "test")
|
||||
cfg := configImpl{output: "json"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Parse JSON array
|
||||
var results []map[string]any
|
||||
err = json.Unmarshal([]byte(out), &results)
|
||||
require.NoError(t, err)
|
||||
assert.GreaterOrEqual(t, len(results), 3, "should have at least 3 environments")
|
||||
|
||||
// Verify all have correct structure
|
||||
for i, result := range results {
|
||||
assert.Equal(t, "test", result["name"], "result %d should have name 'test'", i)
|
||||
assert.Contains(t, result, "kubeContext", "result %d should have kubeContext", i)
|
||||
assert.Contains(t, result, "values", "result %d should have values", i)
|
||||
assert.Contains(t, result, "filePath", "result %d should have filePath", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintEnv_WithDefaults(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
default:
|
||||
values:
|
||||
- color: blue
|
||||
prod:
|
||||
values:
|
||||
- color: red
|
||||
size: large
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "prod")
|
||||
cfg := configImpl{output: "json"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var results []map[string]any
|
||||
err = json.Unmarshal([]byte(out), &results)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
|
||||
values, ok := results[0]["values"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
|
||||
// Should have values from prod environment
|
||||
assert.Equal(t, "red", values["color"])
|
||||
assert.Equal(t, "large", values["size"])
|
||||
}
|
||||
|
||||
func TestPrintEnv_InvalidOutputFormat(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- test: value
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "dev")
|
||||
cfg := configImpl{output: "xml"} // Invalid format
|
||||
|
||||
_, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported output format")
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestPrintEnv_EmptyValues(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
minimal:
|
||||
kubeContext: minimal-cluster
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "minimal")
|
||||
cfg := configImpl{output: "json"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var results []map[string]any
|
||||
err = json.Unmarshal([]byte(out), &results)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
|
||||
result := results[0]
|
||||
assert.Equal(t, "minimal", result["name"])
|
||||
assert.Equal(t, "minimal-cluster", result["kubeContext"])
|
||||
|
||||
// Values should exist but be empty
|
||||
values, ok := result["values"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Empty(t, values)
|
||||
}
|
||||
|
||||
func TestPrintEnv_NoKubeContext(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
local:
|
||||
values:
|
||||
- app: myapp
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "local")
|
||||
cfg := configImpl{output: "yaml"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]any
|
||||
err = yaml.Unmarshal([]byte(out), &result)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "local", result["name"])
|
||||
// kubeContext should be present but empty
|
||||
kubeContext, exists := result["kubeContext"]
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "", kubeContext)
|
||||
}
|
||||
|
||||
func TestPrintEnv_DefaultOutput(t *testing.T) {
|
||||
// When output is empty string, should default to YAML
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
dev:
|
||||
values:
|
||||
- key: value
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "dev")
|
||||
cfg := configImpl{output: ""} // Empty output should default to yaml
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Should be valid YAML
|
||||
var result map[string]any
|
||||
err = yaml.Unmarshal([]byte(out), &result)
|
||||
require.NoError(t, err, "empty output format should default to YAML")
|
||||
}
|
||||
|
||||
func TestPrintEnv_UndefinedEnvironment(t *testing.T) {
|
||||
// Test behavior with undefined environment
|
||||
files := map[string]string{
|
||||
"/path/to/helmfile.yaml": `
|
||||
environments:
|
||||
production:
|
||||
values:
|
||||
- env: prod
|
||||
---
|
||||
releases: []
|
||||
`,
|
||||
}
|
||||
|
||||
app := createTestApp(t, files, "staging") // staging is not defined
|
||||
cfg := configImpl{output: "json"}
|
||||
|
||||
out, err := testutil.CaptureStdout(func() {
|
||||
err := app.PrintEnv(cfg)
|
||||
// The behavior depends on helmfile's environment handling
|
||||
// It may succeed with empty values or fail
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), "environment")
|
||||
}
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// If no error, output should be valid JSON (potentially empty array)
|
||||
if out != "" {
|
||||
var results []map[string]any
|
||||
err = json.Unmarshal([]byte(out), &results)
|
||||
// Should either be valid JSON or empty
|
||||
if err != nil {
|
||||
assert.Equal(t, "", out, "if not valid JSON, output should be empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create test app with common setup
|
||||
func createTestApp(t *testing.T, files map[string]string, environment string) *App {
|
||||
t.Helper()
|
||||
|
||||
valsRuntime, err := vals.New(vals.Options{CacheSize: 32})
|
||||
require.NoError(t, err)
|
||||
|
||||
var buffer bytes.Buffer
|
||||
syncWriter := testhelper.NewSyncWriter(&buffer)
|
||||
logger := helmexec.NewLogger(syncWriter, "warn")
|
||||
|
||||
app := appWithFs(&App{
|
||||
OverrideHelmBinary: DefaultHelmBinary,
|
||||
fs: ffs.DefaultFileSystem(),
|
||||
OverrideKubeContext: "default",
|
||||
Env: environment,
|
||||
Logger: logger,
|
||||
valsRuntime: valsRuntime,
|
||||
}, files)
|
||||
|
||||
expectNoCallsToHelm(app)
|
||||
|
||||
return app
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package config
|
||||
|
||||
// PrintEnvOptions is the options for the print-env command
|
||||
type PrintEnvOptions struct {
|
||||
// Output is the output format (yaml or json)
|
||||
Output string
|
||||
}
|
||||
|
||||
// NewPrintEnvOptions creates a new PrintEnvOptions
|
||||
func NewPrintEnvOptions() *PrintEnvOptions {
|
||||
return &PrintEnvOptions{
|
||||
Output: "yaml", // default to yaml
|
||||
}
|
||||
}
|
||||
|
||||
// PrintEnvImpl is impl for PrintEnvOptions
|
||||
type PrintEnvImpl struct {
|
||||
*GlobalImpl
|
||||
*PrintEnvOptions
|
||||
}
|
||||
|
||||
// NewPrintEnvImpl creates a new PrintEnvImpl
|
||||
func NewPrintEnvImpl(g *GlobalImpl, p *PrintEnvOptions) *PrintEnvImpl {
|
||||
return &PrintEnvImpl{
|
||||
GlobalImpl: g,
|
||||
PrintEnvOptions: p,
|
||||
}
|
||||
}
|
||||
|
||||
// Output returns the output format
|
||||
func (c *PrintEnvImpl) Output() string {
|
||||
return c.PrintEnvOptions.Output
|
||||
}
|
||||
Loading…
Reference in New Issue