Skip to content
Snippets Groups Projects
Commit 6a6704fc authored by Jessica Yuen's avatar Jessica Yuen
Browse files

Introduce command: `ks param diff <env1> <env2> [--component]`

Pretty prints differences between the component parameters of two
environments.

A component flag is accepted to diff against a single component. By
default, the diff is performed against all components.
parent eabd30c5
No related branches found
No related tags found
No related merge requests found
...@@ -24,7 +24,8 @@ import ( ...@@ -24,7 +24,8 @@ import (
) )
const ( const (
flagParamEnv = "env" flagParamEnv = "env"
flagParamComponent = "component"
) )
func init() { func init() {
...@@ -32,9 +33,11 @@ func init() { ...@@ -32,9 +33,11 @@ func init() {
paramCmd.AddCommand(paramSetCmd) paramCmd.AddCommand(paramSetCmd)
paramCmd.AddCommand(paramListCmd) paramCmd.AddCommand(paramListCmd)
paramCmd.AddCommand(paramDiffCmd)
paramSetCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to set parameters for") paramSetCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to set parameters for")
paramListCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to list parameters for") paramListCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to list parameters for")
paramDiffCmd.PersistentFlags().String(flagParamComponent, "", "Specify the component to diff against")
} }
var paramCmd = &cobra.Command{ var paramCmd = &cobra.Command{
...@@ -97,7 +100,7 @@ of environment parameters, we suggest modifying the ...@@ -97,7 +100,7 @@ of environment parameters, we suggest modifying the
} }
var paramListCmd = &cobra.Command{ var paramListCmd = &cobra.Command{
Use: "list [component-name]", Use: "list <component-name>",
Short: "List all parameters for a component(s)", Short: "List all parameters for a component(s)",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags() flags := cmd.Flags()
...@@ -138,3 +141,36 @@ Furthermore, parameters can be listed on a per-environment basis. ...@@ -138,3 +141,36 @@ Furthermore, parameters can be listed on a per-environment basis.
# List all parameters for the component "guestbook" in the environment "dev" # List all parameters for the component "guestbook" in the environment "dev"
ks param list guestbook --env=dev`, ks param list guestbook --env=dev`,
} }
var paramDiffCmd = &cobra.Command{
Use: "diff <env1> <env2>",
Short: "Display differences between the component parameters of two environments",
RunE: func(cmd *cobra.Command, args []string) error {
flags := cmd.Flags()
if len(args) != 2 {
return fmt.Errorf("'param diff' takes exactly two arguments, that is the name of the environments to diff against")
}
env1 := args[0]
env2 := args[1]
component, err := flags.GetString(flagParamComponent)
if err != nil {
return err
}
c := kubecfg.NewParamDiffCmd(env1, env2, component)
return c.Run(cmd.OutOrStdout())
},
Long: `"Pretty prints differences between the component parameters of two environments.
A component flag is accepted to diff against a single component. By default, the
diff is performed against all components.
`,
Example: ` # Diff between the component parameters on environments 'dev' and 'prod'
ks param diff dev prod
# Diff between the component 'guestbook' on environments 'dev' and 'prod'
ks param diff dev prod --component=guestbook`,
}
...@@ -206,7 +206,7 @@ func (m *manager) DeleteEnvironment(name string) error { ...@@ -206,7 +206,7 @@ func (m *manager) DeleteEnvironment(name string) error {
func (m *manager) GetEnvironments() ([]*Environment, error) { func (m *manager) GetEnvironments() ([]*Environment, error) {
envs := []*Environment{} envs := []*Environment{}
log.Info("Retrieving all environments") log.Debug("Retrieving all environments")
err := afero.Walk(m.appFS, string(m.environmentsPath), func(path string, f os.FileInfo, err error) error { err := afero.Walk(m.appFS, string(m.environmentsPath), func(path string, f os.FileInfo, err error) error {
isDir, err := afero.IsDir(m.appFS, path) isDir, err := afero.IsDir(m.appFS, path)
if err != nil { if err != nil {
......
...@@ -18,12 +18,14 @@ package kubecfg ...@@ -18,12 +18,14 @@ package kubecfg
import ( import (
"fmt" "fmt"
"io" "io"
"reflect"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
param "github.com/ksonnet/ksonnet/metadata/params" param "github.com/ksonnet/ksonnet/metadata/params"
"github.com/fatih/color"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
...@@ -240,3 +242,175 @@ func outputParams(params map[string]param.Params, out io.Writer) error { ...@@ -240,3 +242,175 @@ func outputParams(params map[string]param.Params, out io.Writer) error {
_, err := fmt.Fprint(out, strings.Join(lines, "")) _, err := fmt.Fprint(out, strings.Join(lines, ""))
return err 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
color.New(color.FgBlack).Fprintln(out, paramComponentHeader+componentSpacing+
paramNameHeader+nameSpacing+c.env1+envSpacing+c.env2)
color.New(color.FgBlack).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.FgGreen).Fprintln(out, line)
} else if len(k.value2) == 0 {
color.New(color.FgRed).Fprintln(out, line)
} else if k.value1 != k.value2 {
color.New(color.FgYellow).Fprintln(out, line)
} else {
color.New(color.FgBlack).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
}
// 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 (
"testing"
param "github.com/ksonnet/ksonnet/metadata/params"
"github.com/stretchr/testify/require"
)
func TestDiffParams(t *testing.T) {
tests := []struct {
params1 map[string]param.Params
params2 map[string]param.Params
expected []*paramDiffRecord
}{
{
map[string]param.Params{
"bar": param.Params{"replicas": "4"},
"foo": param.Params{"replicas": "1", "name": `"foo"`},
},
map[string]param.Params{
"bar": param.Params{"replicas": "3"},
"foo": param.Params{"name": `"foo-dev"`, "replicas": "1"},
"baz": param.Params{"name": `"baz"`, "replicas": "4"},
},
[]*paramDiffRecord{
&paramDiffRecord{
component: "bar",
param: "replicas",
value1: "4",
value2: "3",
},
&paramDiffRecord{
component: "baz",
param: "name",
value1: "",
value2: `"baz"`,
},
&paramDiffRecord{
component: "baz",
param: "replicas",
value1: "",
value2: "4",
},
&paramDiffRecord{
component: "foo",
param: "name",
value1: `"foo"`,
value2: `"foo-dev"`,
},
&paramDiffRecord{
component: "foo",
param: "replicas",
value1: "1",
value2: "1",
},
},
},
}
for _, s := range tests {
records := diffParams(s.params1, s.params2)
require.Equal(t, len(records), len(s.expected), "Record lengths not equivalent")
for i, record := range records {
require.EqualValues(t, *s.expected[i], *record)
}
}
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment