diff --git a/cmd/param.go b/cmd/param.go index af41950c98e6a3751f7a5b09475cf16e877e7778..cd24de272abb2129bc6a3c4ae8ab9bb6f3f018cf 100644 --- a/cmd/param.go +++ b/cmd/param.go @@ -24,7 +24,8 @@ import ( ) const ( - flagParamEnv = "env" + flagParamEnv = "env" + flagParamComponent = "component" ) func init() { @@ -32,9 +33,11 @@ func init() { paramCmd.AddCommand(paramSetCmd) paramCmd.AddCommand(paramListCmd) + paramCmd.AddCommand(paramDiffCmd) paramSetCmd.PersistentFlags().String(flagParamEnv, "", "Specify environment to set 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{ @@ -97,7 +100,7 @@ of environment parameters, we suggest modifying the } var paramListCmd = &cobra.Command{ - Use: "list [component-name]", + Use: "list <component-name>", Short: "List all parameters for a component(s)", RunE: func(cmd *cobra.Command, args []string) error { flags := cmd.Flags() @@ -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" 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`, +} diff --git a/metadata/environment.go b/metadata/environment.go index abebb2e26169d1da8a358bf5e1dca514c30ee76f..29eb82af07809ec3d811eb4a6ce18d30e82df98a 100644 --- a/metadata/environment.go +++ b/metadata/environment.go @@ -206,7 +206,7 @@ func (m *manager) DeleteEnvironment(name string) error { func (m *manager) GetEnvironments() ([]*Environment, error) { 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 { isDir, err := afero.IsDir(m.appFS, path) if err != nil { diff --git a/pkg/kubecfg/param.go b/pkg/kubecfg/param.go index 3ddfb07df3181adb2acc17f1e5eb8b8d4ca5c2f0..e269607b0567395f71595ca462e78c83f0f23d3e 100644 --- a/pkg/kubecfg/param.go +++ b/pkg/kubecfg/param.go @@ -18,12 +18,14 @@ package kubecfg import ( "fmt" "io" + "reflect" "sort" "strconv" "strings" param "github.com/ksonnet/ksonnet/metadata/params" + "github.com/fatih/color" log "github.com/sirupsen/logrus" ) @@ -240,3 +242,175 @@ func outputParams(params map[string]param.Params, out io.Writer) error { _, err := fmt.Fprint(out, strings.Join(lines, "")) 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, ¶mDiffRecord{ + component: component, + param: param, + value1: value1, + value2: value2, + }) + return records +} diff --git a/pkg/kubecfg/param_test.go b/pkg/kubecfg/param_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d3425bc1e1ff2d483865f3dacddc5bd1fec688c7 --- /dev/null +++ b/pkg/kubecfg/param_test.go @@ -0,0 +1,84 @@ +// 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{ + ¶mDiffRecord{ + component: "bar", + param: "replicas", + value1: "4", + value2: "3", + }, + ¶mDiffRecord{ + component: "baz", + param: "name", + value1: "", + value2: `"baz"`, + }, + ¶mDiffRecord{ + component: "baz", + param: "replicas", + value1: "", + value2: "4", + }, + ¶mDiffRecord{ + component: "foo", + param: "name", + value1: `"foo"`, + value2: `"foo-dev"`, + }, + ¶mDiffRecord{ + 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) + } + } +}