add color output functionality and tests for release statuses

Signed-off-by: Mohammadreza Asadollahifard <mazafard@gmail.com>
pull/31034/head
Mohammadreza Asadollahifard 2 months ago
parent d21a8a04cb
commit c547d1f2ae
No known key found for this signature in database
GPG Key ID: 89C260092433E3CC

@ -89,6 +89,8 @@ type EnvSettings struct {
BurstLimit int
// QPS is queries per second which may be used to avoid throttling.
QPS float32
// NoColor disables colorized output
NoColor bool
}
func New() *EnvSettings {
@ -109,6 +111,7 @@ func New() *EnvSettings {
RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")),
BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit),
QPS: envFloat32Or("HELM_QPS", defaultQPS),
NoColor: envBoolOr("NO_COLOR", false),
}
env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG"))
@ -160,6 +163,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the directory containing cached repository indexes")
fs.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "client-side default throttling limit")
fs.Float32Var(&s.QPS, "qps", s.QPS, "queries per second used when communicating with the Kubernetes API, not including bursting")
fs.BoolVar(&s.NoColor, "no-color", s.NoColor, "disable colorized output")
}
func envOr(name, def string) string {

@ -0,0 +1,70 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package output
import (
"os"
"github.com/fatih/color"
"golang.org/x/term"
release "helm.sh/helm/v4/pkg/release/v1"
)
// ColorizeStatus returns a colorized version of the status string based on the status value
func ColorizeStatus(status release.Status, noColor bool) string {
// Disable color if requested or if not in a terminal
if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) {
return status.String()
}
switch status {
case release.StatusDeployed:
return color.GreenString(status.String())
case release.StatusFailed:
return color.RedString(status.String())
case release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback, release.StatusUninstalling:
return color.YellowString(status.String())
case release.StatusUnknown:
return color.RedString(status.String())
default:
// For uninstalled, superseded, and any other status
return status.String()
}
}
// ColorizeHeader returns a colorized version of a header string
func ColorizeHeader(header string, noColor bool) string {
// Disable color if requested or if not in a terminal
if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) {
return header
}
// Use bold for headers
return color.New(color.Bold).Sprint(header)
}
// ColorizeNamespace returns a colorized version of a namespace string
func ColorizeNamespace(namespace string, noColor bool) string {
// Disable color if requested or if not in a terminal
if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) {
return namespace
}
// Use cyan for namespaces
return color.CyanString(namespace)
}

@ -0,0 +1,219 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package output
import (
"os"
"strings"
"testing"
release "helm.sh/helm/v4/pkg/release/v1"
)
func TestColorizeStatus(t *testing.T) {
// Save original NO_COLOR env var
originalNoColor := os.Getenv("NO_COLOR")
defer func() {
if err := os.Setenv("NO_COLOR", originalNoColor); err != nil {
t.Errorf("Failed to restore NO_COLOR env var: %v", err)
}
}()
tests := []struct {
name string
status release.Status
noColor bool
envNoColor string
wantColor bool // whether we expect color codes in output
}{
{
name: "deployed status with color",
status: release.StatusDeployed,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "deployed status without color flag",
status: release.StatusDeployed,
noColor: true,
envNoColor: "",
wantColor: false,
},
{
name: "deployed status with NO_COLOR env",
status: release.StatusDeployed,
noColor: false,
envNoColor: "1",
wantColor: false,
},
{
name: "failed status with color",
status: release.StatusFailed,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "pending install status with color",
status: release.StatusPendingInstall,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "unknown status with color",
status: release.StatusUnknown,
noColor: false,
envNoColor: "",
wantColor: true,
},
{
name: "superseded status with color",
status: release.StatusSuperseded,
noColor: false,
envNoColor: "",
wantColor: false, // superseded doesn't get colored
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil {
t.Fatalf("Failed to set NO_COLOR env var: %v", err)
}
result := ColorizeStatus(tt.status, tt.noColor)
// Check if result contains ANSI escape codes
hasColor := strings.Contains(result, "\033[")
// In test environment, term.IsTerminal will be false, so we won't get color
// unless we're testing the logic without terminal detection
if hasColor && !tt.wantColor {
t.Errorf("ColorizeStatus() returned color when none expected: %q", result)
}
// Always check the status text is present
if !strings.Contains(result, tt.status.String()) {
t.Errorf("ColorizeStatus() = %q, want to contain %q", result, tt.status.String())
}
})
}
}
func TestColorizeHeader(t *testing.T) {
// Save original NO_COLOR env var
originalNoColor := os.Getenv("NO_COLOR")
defer func() {
if err := os.Setenv("NO_COLOR", originalNoColor); err != nil {
t.Errorf("Failed to restore NO_COLOR env var: %v", err)
}
}()
tests := []struct {
name string
header string
noColor bool
envNoColor string
}{
{
name: "header with color",
header: "NAME",
noColor: false,
envNoColor: "",
},
{
name: "header without color flag",
header: "NAME",
noColor: true,
envNoColor: "",
},
{
name: "header with NO_COLOR env",
header: "NAME",
noColor: false,
envNoColor: "1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil {
t.Fatalf("Failed to set NO_COLOR env var: %v", err)
}
result := ColorizeHeader(tt.header, tt.noColor)
// Always check the header text is present
if !strings.Contains(result, tt.header) {
t.Errorf("ColorizeHeader() = %q, want to contain %q", result, tt.header)
}
})
}
}
func TestColorizeNamespace(t *testing.T) {
// Save original NO_COLOR env var
originalNoColor := os.Getenv("NO_COLOR")
defer func() {
if err := os.Setenv("NO_COLOR", originalNoColor); err != nil {
t.Errorf("Failed to restore NO_COLOR env var: %v", err)
}
}()
tests := []struct {
name string
namespace string
noColor bool
envNoColor string
}{
{
name: "namespace with color",
namespace: "default",
noColor: false,
envNoColor: "",
},
{
name: "namespace without color flag",
namespace: "default",
noColor: true,
envNoColor: "",
},
{
name: "namespace with NO_COLOR env",
namespace: "default",
noColor: false,
envNoColor: "1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil {
t.Fatalf("Failed to set NO_COLOR env var: %v", err)
}
result := ColorizeNamespace(tt.namespace, tt.noColor)
// Always check the namespace text is present
if !strings.Contains(result, tt.namespace) {
t.Errorf("ColorizeNamespace() = %q, want to contain %q", result, tt.namespace)
}
})
}
}

@ -63,6 +63,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
debug: true,
showMetadata: true,
hideNotes: false,
noColor: settings.NoColor,
})
},
}

@ -168,6 +168,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
debug: settings.Debug,
showMetadata: false,
hideNotes: client.HideNotes,
noColor: settings.NoColor,
})
},
}
@ -237,13 +238,13 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Version = ">0.0.0-0"
}
name, chart, err := client.NameAndChart(args)
name, chartRef, err := client.NameAndChart(args)
if err != nil {
return nil, err
}
client.ReleaseName = name
cp, err := client.LocateChart(chart, settings)
cp, err := client.LocateChart(chartRef, settings)
if err != nil {
return nil, err
}

@ -106,7 +106,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
}
return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders))
return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders, settings.NoColor))
},
}
@ -146,9 +146,10 @@ type releaseElement struct {
type releaseListWriter struct {
releases []releaseElement
noHeaders bool
noColor bool
}
func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool) *releaseListWriter {
func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool, noColor bool) *releaseListWriter {
// Initialize the array so no results returns an empty array instead of null
elements := make([]releaseElement, 0, len(releases))
for _, r := range releases {
@ -173,26 +174,58 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string, noHead
elements = append(elements, element)
}
return &releaseListWriter{elements, noHeaders}
return &releaseListWriter{elements, noHeaders, noColor}
}
func (r *releaseListWriter) WriteTable(out io.Writer) error {
func (w *releaseListWriter) WriteTable(out io.Writer) error {
table := uitable.New()
if !r.noHeaders {
table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION")
if !w.noHeaders {
table.AddRow(
output.ColorizeHeader("NAME", w.noColor),
output.ColorizeHeader("NAMESPACE", w.noColor),
output.ColorizeHeader("REVISION", w.noColor),
output.ColorizeHeader("UPDATED", w.noColor),
output.ColorizeHeader("STATUS", w.noColor),
output.ColorizeHeader("CHART", w.noColor),
output.ColorizeHeader("APP VERSION", w.noColor),
)
}
for _, r := range r.releases {
table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion)
for _, r := range w.releases {
// Parse the status string back to a release.Status to use color
var status release.Status
switch r.Status {
case "deployed":
status = release.StatusDeployed
case "failed":
status = release.StatusFailed
case "pending-install":
status = release.StatusPendingInstall
case "pending-upgrade":
status = release.StatusPendingUpgrade
case "pending-rollback":
status = release.StatusPendingRollback
case "uninstalling":
status = release.StatusUninstalling
case "uninstalled":
status = release.StatusUninstalled
case "superseded":
status = release.StatusSuperseded
case "unknown":
status = release.StatusUnknown
default:
status = release.Status(r.Status)
}
table.AddRow(r.Name, output.ColorizeNamespace(r.Namespace, w.noColor), r.Revision, r.Updated, output.ColorizeStatus(status, w.noColor), r.Chart, r.AppVersion)
}
return output.EncodeTable(out, table)
}
func (r *releaseListWriter) WriteJSON(out io.Writer) error {
return output.EncodeJSON(out, r.releases)
func (w *releaseListWriter) WriteJSON(out io.Writer) error {
return output.EncodeJSON(out, w.releases)
}
func (r *releaseListWriter) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, r.releases)
func (w *releaseListWriter) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, w.releases)
}
// Returns all releases from 'releases', except those with names matching 'ignoredReleases'

@ -78,6 +78,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
debug: settings.Debug,
showMetadata: false,
hideNotes: client.HideNotes,
noColor: settings.NoColor,
}); err != nil {
return err
}

@ -84,6 +84,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
debug: false,
showMetadata: false,
hideNotes: false,
noColor: settings.NoColor,
})
},
}
@ -112,6 +113,7 @@ type statusPrinter struct {
debug bool
showMetadata bool
hideNotes bool
noColor bool
}
func (s statusPrinter) WriteJSON(out io.Writer) error {
@ -130,8 +132,8 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
if !s.release.Info.LastDeployed.IsZero() {
_, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC))
}
_, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", s.release.Namespace)
_, _ = fmt.Fprintf(out, "STATUS: %s\n", s.release.Info.Status.String())
_, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", output.ColorizeNamespace(s.release.Namespace, s.noColor))
_, _ = fmt.Fprintf(out, "STATUS: %s\n", output.ColorizeStatus(s.release.Info.Status, s.noColor))
_, _ = fmt.Fprintf(out, "REVISION: %d\n", s.release.Version)
if s.showMetadata {
_, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name)
@ -218,7 +220,7 @@ func (s statusPrinter) WriteTable(out io.Writer) error {
// Hide notes from output - option in install and upgrades
if !s.hideNotes && len(s.release.Info.Notes) > 0 {
fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes))
_, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes))
}
return nil
}

@ -166,6 +166,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
debug: settings.Debug,
showMetadata: false,
hideNotes: instClient.HideNotes,
noColor: settings.NoColor,
})
} else if err != nil {
return err
@ -257,6 +258,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
debug: settings.Debug,
showMetadata: false,
hideNotes: client.HideNotes,
noColor: settings.NoColor,
})
},
}

Loading…
Cancel
Save