Skip to content
Snippets Groups Projects
  • Jessica Yuen's avatar
    Use background colors for param diff · 2fcbf68b
    Jessica Yuen authored
    Foreground colors were causing conflicts with user's terminal colors.
    i.e. Black text would show as invisible for users with a black terminal
    background.
    2fcbf68b
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
param.go 10.13 KiB
// Copyright 2017 The kubecfg 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 kubecfg

import (
	"fmt"
	"io"
	"reflect"
	"sort"
	"strconv"
	"strings"

	param "github.com/ksonnet/ksonnet/metadata/params"
	"github.com/ksonnet/ksonnet/utils"

	"github.com/fatih/color"
	log "github.com/sirupsen/logrus"
)

func sortedKeys(params map[string]param.Params) []string {
	// keys maintains an alphabetically sorted list of the components
	keys := make([]string, 0, len(params))
	for key := range params {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

func sortedParams(params param.Params) []string {
	// keys maintains an alphabetically sorted list of the params
	keys := make([]string, 0, len(params))
	for key := range params {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	return keys
}

// ----------------------------------------------------------------------------

// ParamSetCmd stores the information necessary to set component and
// environment params.
type ParamSetCmd struct {
	component string
	env       string

	param string
	value string
}

// NewParamSetCmd acts as a constructor for ParamSetCmd. It will also sanitize
// or "quote" the param value first if necessary.
func NewParamSetCmd(component, env, param, value string) *ParamSetCmd {
	return &ParamSetCmd{component: component, env: env, param: param, value: sanitizeParamValue(value)}
}
// Run executes the setting of params.
func (c *ParamSetCmd) Run() error {
	manager, err := manager()
	if err != nil {
		return err
	}

	if len(c.env) == 0 {
		if err = manager.SetComponentParams(c.component, param.Params{c.param: c.value}); err == nil {
			log.Infof("Parameter '%s' successfully set to '%s' for component '%s'", c.param, c.value, c.component)
		}
	} else {
		if err = manager.SetEnvironmentParams(c.env, c.component, param.Params{c.param: c.value}); err == nil {
			log.Infof("Parameter '%s' successfully set to '%s' for component '%s' in environment '%s'",
				c.param, c.value, c.component, c.env)
		}
	}

	return err
}

// sanitizeParamValue does a best effort to identify value types. It will put
// quotes around values which it categorizes as strings.
func sanitizeParamValue(value string) string {
	// boolean
	if value == "true" || value == "false" {
		return value
	}
	// numeric
	if _, err := strconv.ParseFloat(value, 64); err == nil {
		return value
	}
	// string
	return fmt.Sprintf(`"%s"`, value)
}

// ----------------------------------------------------------------------------

const (
	paramComponentHeader = "COMPONENT"
	paramNameHeader      = "PARAM"
	paramValueHeader     = "VALUE"
)

// ParamListCmd stores the information necessary display component or
// environment parameters
type ParamListCmd struct {
	component string
	env       string
}

// NewParamListCmd acts as a constructor for ParamListCmd.
func NewParamListCmd(component, env string) *ParamListCmd {
	return &ParamListCmd{component: component, env: env}
}

// Run executes the displaying of params.
func (c *ParamListCmd) Run(out io.Writer) error {
	manager, err := manager()
	if err != nil {
		return err
	}

	var params map[string]param.Params
	if len(c.env) != 0 {
		params, err = manager.GetEnvironmentParams(c.env)
		if err != nil {
			return err
		}
	} else {
		params, err = manager.GetAllComponentParams()
		if err != nil {
			return err
		}
	}

	if len(c.component) != 0 {
		if _, ok := params[c.component]; !ok {
			return fmt.Errorf("No such component '%s' found", c.component)
		}

		p := params[c.component]
		return outputParamsFor(c.component, p, out)
	}

	return outputParams(params, out)
}

func outputParamsFor(component string, params param.Params, out io.Writer) error {
	keys := sortedParams(params)

	rows := [][]string{
		[]string{paramNameHeader, paramValueHeader},
		[]string{strings.Repeat("=", len(paramNameHeader)), strings.Repeat("=", len(paramValueHeader))},
	}
	for _, k := range keys {
		rows = append(rows, []string{k, params[k]})
	}

	formatted, err := utils.PadRows(rows)
	if err != nil {
		return err
	}
	_, err = fmt.Fprint(out, formatted)
	return err
}

func outputParams(params map[string]param.Params, out io.Writer) error {
	keys := sortedKeys(params)

	rows := [][]string{
		[]string{paramComponentHeader, paramNameHeader, paramValueHeader},
		[]string{
			strings.Repeat("=", len(paramComponentHeader)),
			strings.Repeat("=", len(paramNameHeader)),
			strings.Repeat("=", len(paramValueHeader))},
	}
	for _, k := range keys {
		// sort params to display alphabetically
		ps := sortedParams(params[k])

		for _, p := range ps {
			rows = append(rows, []string{k, p, params[k][p]})
		}
	}

	formatted, err := utils.PadRows(rows)
	if err != nil {
		return err
	}
	_, err = fmt.Fprint(out, formatted)
	return err
}

// ----------------------------------------------------------------------------

// ParamDiffCmd stores the information necessary to diff between environment
// parameters.
type ParamDiffCmd struct {
	env1 string
	env2 string

	component string
}

// NewParamDiffCmd acts as a constructor for ParamDiffCmd.
func NewParamDiffCmd(env1, env2, component string) *ParamDiffCmd {
	return &ParamDiffCmd{env1: env1, env2: env2, component: component}
}

type paramDiffRecord struct {
	component string
	param     string
	value1    string
	value2    string
}

// Run executes the diffing of environment params.
func (c *ParamDiffCmd) Run(out io.Writer) error {
	manager, err := manager()
	if err != nil {
		return err
	}

	params1, err := manager.GetEnvironmentParams(c.env1)
	if err != nil {
		return err
	}

	params2, err := manager.GetEnvironmentParams(c.env2)
	if err != nil {
		return err
	}

	if len(c.component) != 0 {
		params1 = map[string]param.Params{c.component: params1[c.component]}
		params2 = map[string]param.Params{c.component: params2[c.component]}
	}

	if reflect.DeepEqual(params1, params2) {
		log.Info("No differences found.")
		return nil
	}

	records := diffParams(params1, params2)

	//
	// Format each component parameter information for pretty printing.
	// Each component will be outputted alphabetically like the following:
	//
	//   COMPONENT PARAM     dev       prod
	//   bar       name      "bar-dev" "bar"
	//   foo       replicas  1
	//

	maxComponentLen := len(paramComponentHeader)
	for _, k := range records {
		if l := len(k.component); l > maxComponentLen {
			maxComponentLen = l
		}
	}

	maxParamLen := len(paramNameHeader) + maxComponentLen + 1
	for _, k := range records {
		if l := len(k.param) + maxComponentLen + 1; l > maxParamLen {
			maxParamLen = l
		}
	}

	maxEnvLen := len(c.env1) + maxParamLen + 1
	for _, k := range records {
		if l := len(k.value1) + maxParamLen + 1; l > maxEnvLen {
			maxEnvLen = l
		}
	}

	componentSpacing := strings.Repeat(" ", maxComponentLen-len(paramComponentHeader)+1)
	nameSpacing := strings.Repeat(" ", maxParamLen-maxComponentLen-len(paramNameHeader))
	envSpacing := strings.Repeat(" ", maxEnvLen-maxParamLen-len(c.env1))

	// print headers
	fmt.Fprintln(out, paramComponentHeader+componentSpacing+
		paramNameHeader+nameSpacing+c.env1+envSpacing+c.env2)
	fmt.Fprintln(out, strings.Repeat("=", len(paramComponentHeader))+componentSpacing+
		strings.Repeat("=", len(paramNameHeader))+nameSpacing+
		strings.Repeat("=", len(c.env1))+envSpacing+
		strings.Repeat("=", len(c.env2)))

	// print body
	for _, k := range records {
		componentSpacing = strings.Repeat(" ", maxComponentLen-len(k.component)+1)
		nameSpacing = strings.Repeat(" ", maxParamLen-maxComponentLen-len(k.param))
		envSpacing = strings.Repeat(" ", maxEnvLen-maxParamLen-len(k.value1))
		line := fmt.Sprint(k.component + componentSpacing + k.param + nameSpacing + k.value1 + envSpacing + k.value2)
		if len(k.value1) == 0 {
			color.New(color.BgGreen).Fprint(out, line)
			fmt.Fprintln(out)
		} else if len(k.value2) == 0 {
			color.New(color.BgRed).Fprint(out, line)
			fmt.Fprintln(out)
		} else if k.value1 != k.value2 {
			color.New(color.BgYellow).Fprint(out, line)
			fmt.Fprintln(out)
		} else {
			fmt.Fprintln(out, line)
		}
	}

	return nil
}

func diffParams(params1, params2 map[string]param.Params) []*paramDiffRecord {
	var records []*paramDiffRecord

	for c := range params1 {
		if _, contains := params2[c]; !contains {
			// env2 doesn't have this component, add all params from env1 for this component
			for p := range params2[c] {
				records = addRecord(records, c, p, params1[c][p], "")
			}
		} else {
			// has same component -- need to compare params
			for p := range params1[c] {
				if _, hasParam := params2[c][p]; !hasParam {
					// env2 doesn't have this param, add a record with the param value from env1
					records = addRecord(records, c, p, params1[c][p], "")
				} else {
					// env2 has this param too, add a record with both param values
					records = addRecord(records, c, p, params1[c][p], params2[c][p])
				}
			}
			// add remaining records for params that env2 has that env1 does not for this component
			for p := range params2[c] {
				if _, hasParam := params1[c][p]; !hasParam {
					records = addRecord(records, c, p, "", params2[c][p])
				}
			}
		}
	}
	// add remaining records where env2 contains a component that env1 does not
	for c := range params2 {
		if _, contains := params1[c]; !contains {
			for p := range params2[c] {
				records = addRecord(records, c, p, "", params2[c][p])
			}
		}
	}

	sort.Slice(records, func(i, j int) bool {
		if records[i].component == records[j].component {
			return records[i].param < records[j].param
		}
		return records[i].component < records[j].component
	})

	return records
}

func addRecord(records []*paramDiffRecord, component, param, value1, value2 string) []*paramDiffRecord {
	records = append(records, &paramDiffRecord{
		component: component,
		param:     param,
		value1:    value1,
		value2:    value2,
	})
	return records
}