feat(env): add --output json|yaml to 'helm env'

'helm env' only emits KEY="VALUE" lines, which is inconvenient for
tooling that needs the helm paths on the current system (#11909).

Add an --output/-o flag accepting env (the default, unchanged), json,
and yaml. Single-variable mode keeps printing the bare value by
default and emits a one-entry object for the structured formats.

Closes #11909

Signed-off-by: Ruslan Shaydullin <shaydullin.r.d@outlook.com>
pull/32282/head
Ruslan Shaydullin 6 days ago
parent af25d22902
commit 17c1d0390e

@ -19,10 +19,15 @@ package cmd
import (
"fmt"
"io"
"log"
"maps"
"slices"
"sort"
"strings"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require"
)
@ -31,6 +36,7 @@ Env prints out all the environment information in use by Helm.
`
func newEnvCmd(out io.Writer) *cobra.Command {
outfmt := envFormatKeyValue
cmd := &cobra.Command{
Use: "env",
Short: "helm client environment information",
@ -44,22 +50,18 @@ func newEnvCmd(out io.Writer) *cobra.Command {
return noMoreArgsComp()
},
Run: func(_ *cobra.Command, args []string) {
RunE: func(_ *cobra.Command, args []string) error {
envVars := settings.EnvVars()
if len(args) == 0 {
// Sort the variables by alphabetical order.
// This allows for a constant output across calls to 'helm env'.
keys := getSortedEnvVarKeys()
for _, k := range keys {
fmt.Fprintf(out, "%s=\"%s\"\n", k, envVars[k])
}
} else {
fmt.Fprintf(out, "%s\n", envVars[args[0]])
if len(args) == 1 {
return outfmt.writeSingle(out, args[0], envVars[args[0]])
}
return outfmt.write(out, envVars)
},
}
bindEnvOutputFlag(cmd, &outfmt)
return cmd
}
@ -74,3 +76,92 @@ func getSortedEnvVarKeys() []string {
return keys
}
// envFormat is the set of output formats supported by 'helm env'. The
// KEY="VALUE" shell format predates the structured formats and stays the
// default; it is not part of output.Format because no other command emits it.
type envFormat string
const (
envFormatKeyValue envFormat = "env"
envFormatJSON envFormat = "json"
envFormatYAML envFormat = "yaml"
)
func envFormats() []string {
return []string{string(envFormatKeyValue), string(envFormatJSON), string(envFormatYAML)}
}
func envFormatsWithDesc() map[string]string {
return map[string]string{
string(envFormatKeyValue): `Output result in KEY="VALUE" format`,
string(envFormatJSON): "Output result in JSON format",
string(envFormatYAML): "Output result in YAML format",
}
}
// String, Set and Type make envFormat usable as a pflag.Value.
func (o *envFormat) String() string { return string(*o) }
func (o *envFormat) Type() string { return "format" }
func (o *envFormat) Set(s string) error {
if !slices.Contains(envFormats(), s) {
return output.ErrInvalidFormatType
}
*o = envFormat(s)
return nil
}
// write prints every variable in the given format.
func (o envFormat) write(out io.Writer, envVars map[string]string) error {
switch o {
case envFormatKeyValue:
// Sort the variables by alphabetical order.
// This allows for a constant output across calls to 'helm env'.
for _, k := range slices.Sorted(maps.Keys(envVars)) {
fmt.Fprintf(out, "%s=\"%s\"\n", k, envVars[k])
}
return nil
case envFormatJSON:
return output.EncodeJSON(out, envVars)
case envFormatYAML:
return output.EncodeYAML(out, envVars)
}
return output.ErrInvalidFormatType
}
// writeSingle prints one variable. The key=value format keeps the historic
// behavior of printing the bare value only.
func (o envFormat) writeSingle(out io.Writer, key, value string) error {
switch o {
case envFormatKeyValue:
fmt.Fprintf(out, "%s\n", value)
return nil
case envFormatJSON:
return output.EncodeJSON(out, map[string]string{key: value})
case envFormatYAML:
return output.EncodeYAML(out, map[string]string{key: value})
}
return output.ErrInvalidFormatType
}
func bindEnvOutputFlag(cmd *cobra.Command, varRef *envFormat) {
cmd.Flags().VarP(varRef, outputFlag, "o",
"prints the output in the specified format. Allowed values: "+strings.Join(envFormats(), ", "))
err := cmd.RegisterFlagCompletionFunc(outputFlag, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
var formatNames []string
for format, desc := range envFormatsWithDesc() {
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
// Sort the results to get a deterministic order for the tests
sort.Strings(formatNames)
return formatNames, cobra.ShellCompDirectiveNoFileComp
})
if err != nil {
log.Fatal(err)
}
}

@ -17,6 +17,10 @@ limitations under the License.
package cmd
import (
"bytes"
"encoding/json"
"regexp"
"strings"
"testing"
)
@ -25,10 +29,139 @@ func TestEnv(t *testing.T) {
name: "completion for env",
cmd: "__complete env ''",
golden: "output/env-comp.txt",
}, {
name: "completion for env output flag",
cmd: "__complete env --output ''",
golden: "output/env-output-comp.txt",
}, {
name: "env with invalid output format",
cmd: "env --output table",
golden: "output/env-output-invalid.txt",
wantError: true,
}}
runTestCmd(t, tests)
}
func TestEnvOutputDefault(t *testing.T) {
// Pin the default format: without --output, every line must keep the
// historic KEY="VALUE" shape. The values are machine-specific, so
// assert the shape rather than compare against a golden file.
_, out, err := executeActionCommand("env")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(out, `HELM_BIN="`) {
t.Errorf("expected HELM_BIN in default output, got %q", out)
}
lineRe := regexp.MustCompile(`^[A-Z0-9_]+=".*"$`)
for line := range strings.SplitSeq(strings.TrimSuffix(out, "\n"), "\n") {
if !lineRe.MatchString(line) {
t.Errorf("line %q does not match KEY=\"VALUE\" format", line)
}
}
// Single-variable mode without --output keeps printing the bare value.
_, out, err = executeActionCommand("env HELM_BURST_LIMIT")
if err != nil {
t.Fatal(err)
}
if out != "100\n" {
t.Errorf("expected %q, got %q", "100\n", out)
}
}
func TestEnvOutputJSON(t *testing.T) {
// The full listing contains machine-specific paths, so validate the
// shape instead of comparing against a golden file.
_, out, err := executeActionCommand("env --output json")
if err != nil {
t.Fatal(err)
}
var envVars map[string]string
if err := json.Unmarshal([]byte(out), &envVars); err != nil {
t.Fatalf("expected valid JSON output, got %q: %s", out, err)
}
if _, ok := envVars["HELM_BIN"]; !ok {
t.Errorf("expected HELM_BIN in JSON output, got %v", envVars)
}
}
func TestEnvFormatWrite(t *testing.T) {
envVars := map[string]string{
"HELM_BIN": "/usr/local/bin/helm",
"HELM_DEBUG": "false",
"HELM_NAMESPACE": "default",
}
tests := []struct {
name string
format envFormat
want string
}{{
name: "key=value format",
format: envFormatKeyValue,
want: `HELM_BIN="/usr/local/bin/helm"
HELM_DEBUG="false"
HELM_NAMESPACE="default"
`,
}, {
name: "json format",
format: envFormatJSON,
want: `{"HELM_BIN":"/usr/local/bin/helm","HELM_DEBUG":"false","HELM_NAMESPACE":"default"}` + "\n",
}, {
name: "yaml format",
format: envFormatYAML,
want: `HELM_BIN: /usr/local/bin/helm
HELM_DEBUG: "false"
HELM_NAMESPACE: default
`,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
if err := tt.format.write(&buf, envVars); err != nil {
t.Fatal(err)
}
if got := buf.String(); got != tt.want {
t.Errorf("expected %q, got %q", tt.want, got)
}
})
}
}
func TestEnvFormatWriteSingle(t *testing.T) {
tests := []struct {
name string
format envFormat
want string
}{{
name: "key=value format prints the bare value",
format: envFormatKeyValue,
want: "default\n",
}, {
name: "json format",
format: envFormatJSON,
want: `{"HELM_NAMESPACE":"default"}` + "\n",
}, {
name: "yaml format",
format: envFormatYAML,
want: "HELM_NAMESPACE: default\n",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var buf bytes.Buffer
if err := tt.format.writeSingle(&buf, "HELM_NAMESPACE", "default"); err != nil {
t.Fatal(err)
}
if got := buf.String(); got != tt.want {
t.Errorf("expected %q, got %q", tt.want, got)
}
})
}
}
func TestEnvFileCompletion(t *testing.T) {
checkFileCompletion(t, "env", false)
checkFileCompletion(t, "env HELM_BIN", false)

@ -0,0 +1,5 @@
env Output result in KEY="VALUE" format
json Output result in JSON format
yaml Output result in YAML format
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1 @@
Error: invalid argument "table" for "-o, --output" flag: invalid format type
Loading…
Cancel
Save