Unverified Commit 4ea001ce authored by bryanl's avatar bryanl
Browse files

Update diff action



Fixes #529
Signed-off-by: default avatarbryanl <bryanliles@gmail.com>
parent 1334b31e
......@@ -180,6 +180,12 @@
packages = ["."]
revision = "44e46d280b43ec1531bb25252440e34f1b800b65"
[[projects]]
name = "github.com/fatih/color"
packages = ["."]
revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260"
version = "v1.5.0"
[[projects]]
name = "github.com/fsnotify/fsnotify"
packages = ["."]
......@@ -434,6 +440,12 @@
]
revision = "32fa128f234d041f196a9f3e0fea5ac9772c08e1"
[[projects]]
name = "github.com/mattn/go-colorable"
packages = ["."]
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
......@@ -595,6 +607,15 @@
packages = ["diffmatchpatch"]
revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
[[projects]]
branch = "master"
name = "github.com/shazow/go-diff"
packages = [
".",
"difflib"
]
revision = "b6b7b6733b8c9589e2452df9345ddaf75badd0db"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
......@@ -1256,6 +1277,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "4f2dbea9b6e67d245ebe99506a294b342a0fb31aba0a9a8f20a37fc6a40a0f5a"
inputs-digest = "88071086ef49da55458e92c4c7152174773c045e206be1e5040385c304fc3239"
solver-name = "gps-cdcl"
solver-version = 1
......@@ -142,3 +142,11 @@ required = ["k8s.io/kubernetes/pkg/kubectl/cmd/util"]
[[override]]
name = "github.com/docker/docker"
revision = "04864cb3cb526ddeee70dc6e17a6c7802ef91a0b"
[[constraint]]
branch = "master"
name = "github.com/shazow/go-diff"
[[constraint]]
name = "github.com/fatih/color"
version = "1.5.0"
......@@ -20,8 +20,8 @@ import (
log "github.com/sirupsen/logrus"
"github.com/ksonnet/ksonnet/pkg/actions"
"github.com/ksonnet/ksonnet/pkg/clicmd"
"github.com/ksonnet/ksonnet/pkg/kubecfg"
)
// Version is overridden using `-X main.version` during release builds
......@@ -45,7 +45,7 @@ func main() {
log.Error(err.Error())
switch err {
case kubecfg.ErrDiffFound:
case actions.ErrDiffFound:
os.Exit(10)
default:
os.Exit(1)
......
......@@ -33,7 +33,7 @@ the manifest for that particular component.
```
ks diff <location1:env1> [location2:env2] [-c <component-name>] [flags]
ks diff <location1:env1> [location2:env2] [flags]
```
### Examples
......@@ -66,14 +66,28 @@ ks diff dev -c redis
### Options
```
-c, --component stringSlice Name of a specific component (multiple -c flags accepted, allows YAML, JSON, and Jsonnet)
--diff-strategy string Diff strategy, all or subset. (default "all")
-V, --ext-str stringSlice Values of external variables
--ext-str-file stringSlice Read external variable from a file
-h, --help help for diff
-J, --jpath stringSlice Additional jsonnet library search path
-A, --tla-str stringSlice Values of top level arguments
--tla-str-file stringSlice Read top level argument from a file
--as string Username to impersonate for the operation
--as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups.
--certificate-authority string Path to a cert file for the certificate authority
--client-certificate string Path to a client certificate file for TLS
--client-key string Path to a client key file for TLS
--cluster string The name of the kubeconfig cluster to use
--context string The name of the kubeconfig context to use
-V, --ext-str stringSlice Values of external variables
--ext-str-file stringSlice Read external variable from a file
-h, --help help for diff
--insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure
-J, --jpath stringSlice Additional jsonnet library search path
--kubeconfig string Path to a kubeconfig file. Alternative to env var $KUBECONFIG.
-n, --namespace string If present, the namespace scope for this CLI request
--password string Password for basic authentication to the API server
--request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0")
--server string The address and port of the Kubernetes API server
-A, --tla-str stringSlice Values of top level arguments
--tla-str-file stringSlice Read top level argument from a file
--token string Bearer token for authentication to the API server
--user string The name of the kubeconfig user to use
--username string Username for basic authentication to the API server
```
### Options inherited from parent commands
......
......@@ -95,6 +95,10 @@ const (
OptionSkipGc = "skip-gc"
// OptionSpecFlag is specFlag option. Used for setting k8s spec.
OptionSpecFlag = "spec-flag"
// OptionSrc1 is src1 option.
OptionSrc1 = "src-1"
// OptionSrc2 is src2 option.
OptionSrc2 = "src-2"
// OptionTlaVarFiles is jsonnet tla var files.
OptionTlaVarFiles = "tla-var-files"
// OptionTlaVars is jsonnet tla vars.
......
// Copyright 2018 The ksonnet 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 actions
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/fatih/color"
"github.com/ksonnet/ksonnet/pkg/app"
"github.com/ksonnet/ksonnet/pkg/client"
"github.com/ksonnet/ksonnet/pkg/diff"
"github.com/pkg/errors"
)
var (
// ErrDiffFound is an error returned when differences are found.
ErrDiffFound = errors.New("differences found")
diffAddColor = color.New(color.FgGreen)
diffRemoveColor = color.New(color.FgRed)
)
// RunDiff runs `diff`
func RunDiff(m map[string]interface{}) error {
d, err := NewDiff(m)
if err != nil {
return err
}
return d.Run()
}
// Diff sets targets for an environment.
type Diff struct {
app app.App
clientConfig *client.Config
src1 string
src2 string
diffFn func(app.App, *client.Config, *diff.Location, *diff.Location) (io.Reader, error)
out io.Writer
}
// NewDiff creates an instance of Diff.
func NewDiff(m map[string]interface{}) (*Diff, error) {
ol := newOptionLoader(m)
d := &Diff{
app: ol.LoadApp(),
clientConfig: ol.LoadClientConfig(),
src1: ol.LoadString(OptionSrc1),
src2: ol.LoadOptionalString(OptionSrc2),
diffFn: diff.DefaultDiff,
out: os.Stdout,
}
if ol.err != nil {
return nil, ol.err
}
return d, nil
}
// Run assigns targets to an environment.
func (d *Diff) Run() error {
location1 := diff.NewLocation(d.src1)
if d.src2 == "" {
d.src2 = fmt.Sprintf("%s:%s", "remote", location1.EnvName())
}
location2 := diff.NewLocation(d.src2)
r, err := d.diffFn(d.app, d.clientConfig, location1, location2)
if err != nil {
return err
}
var buf bytes.Buffer
scanner := bufio.NewScanner(r)
for scanner.Scan() {
t := scanner.Text()
switch {
case strings.HasPrefix(t, "+"):
diffAddColor.Fprintln(&buf, t)
case strings.HasPrefix(t, "-"):
diffRemoveColor.Fprintln(&buf, t)
default:
fmt.Fprintln(&buf, t)
}
}
if err := scanner.Err(); err != nil {
return err
}
if buf.String() != "" {
return ErrDiffFound
}
return nil
}
// Copyright 2018 The ksonnet 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 actions
import (
"bytes"
"io"
"strings"
"testing"
"github.com/ksonnet/ksonnet/pkg/app"
amocks "github.com/ksonnet/ksonnet/pkg/app/mocks"
"github.com/ksonnet/ksonnet/pkg/client"
"github.com/ksonnet/ksonnet/pkg/diff"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDiff(t *testing.T) {
cases := []struct {
name string
src1 string
src2 string
eLocation1 string
eLocation2 string
isNewError bool
isRunError bool
}{
{
name: "default",
src1: "default",
eLocation1: "local:default",
eLocation2: "remote:default",
},
{
name: "local:default remote:default",
src1: "local:default",
src2: "remote:default",
eLocation1: "local:default",
eLocation2: "remote:default",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
withApp(t, func(appMock *amocks.App) {
in := map[string]interface{}{
OptionApp: appMock,
OptionClientConfig: &client.Config{},
OptionComponentNames: []string{},
OptionSrc1: tc.src1,
OptionSrc2: tc.src2,
}
d, err := NewDiff(in)
if tc.isNewError {
require.Error(t, err)
return
}
require.NoError(t, err)
var buf bytes.Buffer
d.out = &buf
d.diffFn = func(a app.App, c *client.Config, l1 *diff.Location, l2 *diff.Location) (io.Reader, error) {
assert.Equal(t, tc.eLocation1, l1.String(), "location1")
assert.Equal(t, tc.eLocation2, l2.String(), "location2")
r := strings.NewReader("")
return r, nil
}
err = d.Run()
if tc.isRunError {
require.Error(t, err)
return
}
require.NoError(t, err)
})
})
}
}
......@@ -65,11 +65,11 @@ type actionFn func(map[string]interface{}) error
var (
actionFns = map[initName]actionFn{
actionApply: actions.RunApply,
actionComponentList: actions.RunComponentList,
actionComponentRm: actions.RunComponentRm,
actionDelete: actions.RunDelete,
// actionDiff
actionApply: actions.RunApply,
actionComponentList: actions.RunComponentList,
actionComponentRm: actions.RunComponentRm,
actionDelete: actions.RunDelete,
actionDiff: actions.RunDiff,
actionEnvAdd: actions.RunEnvAdd,
actionEnvCurrent: actions.RunEnvCurrent,
actionEnvDescribe: actions.RunEnvDescribe,
......
......@@ -17,33 +17,34 @@ package clicmd
import (
"fmt"
"os"
"strings"
"github.com/spf13/afero"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/ksonnet/ksonnet/metadata"
"github.com/ksonnet/ksonnet/pkg/actions"
"github.com/ksonnet/ksonnet/pkg/client"
"github.com/ksonnet/ksonnet/pkg/kubecfg"
"github.com/ksonnet/ksonnet/pkg/pipeline"
)
const (
flagDiffStrategy = "diff-strategy"
diffShortDesc = "Compare manifests, based on environment or location (local or remote)"
diffShortDesc = "Compare manifests, based on environment or location (local or remote)"
)
var (
diffClientConfig *client.Config
)
func init() {
addEnvCmdFlags(diffCmd)
// addEnvCmdFlags(diffCmd)
diffClientConfig = client.NewDefaultClientConfig(ka)
diffClientConfig.BindClientGoFlags(diffCmd)
bindJsonnetFlags(diffCmd, "diff")
diffCmd.PersistentFlags().String(flagDiffStrategy, "all", "Diff strategy, all or subset.")
RootCmd.AddCommand(diffCmd)
}
var diffCmd = &cobra.Command{
Use: "diff <location1:env1> [location2:env2] [-c <component-name>]",
Use: "diff <location1:env1> [location2:env2]",
Short: diffShortDesc,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
......@@ -53,39 +54,21 @@ var diffCmd = &cobra.Command{
return fmt.Errorf("'diff' takes at most two arguments, that are the name of the environments\n\n%s", cmd.UsageString())
}
flags := cmd.Flags()
cwd, err := os.Getwd()
if err != nil {
return err
}
componentNames, err := flags.GetStringSlice(flagComponent)
if err != nil {
return err
}
var env1 *string
if len(args) > 0 {
env1 = &args[0]
m := map[string]interface{}{
actions.OptionApp: ka,
actions.OptionClientConfig: diffClientConfig,
actions.OptionSrc1: args[0],
}
var env2 *string
if len(args) > 1 {
env2 = &args[1]
if len(args) == 2 {
m[actions.OptionSrc2] = args[1]
}
diffStrategy, err := flags.GetString(flagDiffStrategy)
if err != nil {
return err
if err := extractJsonnetFlags("apply"); err != nil {
return errors.Wrap(err, "handle jsonnet flags")
}
c, err := initDiffCmd(appFs, cmd, cwd, env1, env2, componentNames, diffStrategy)
if err != nil {
return err
}
return c.Run(cmd.OutOrStdout())
return runAction(actionDiff, m)
},
Long: `
The ` + "`diff`" + ` command displays standard file diffs, and can be used to compare manifests
......@@ -137,160 +120,3 @@ ks diff local:us-west/dev remote:us-west/prod
ks diff dev -c redis
`,
}
func initDiffCmd(fs afero.Fs, cmd *cobra.Command, wd string, envFq1, envFq2 *string, files []string, diffStrategy string) (kubecfg.DiffCmd, error) {
const (
remote = "remote"
local = "local"
)
if envFq2 == nil {
return initDiffSingleEnv(fs, *envFq1, diffStrategy, files, cmd, wd)
}
// expect envs to be of the format local:myenv or remote:myenv
env1 := strings.SplitN(*envFq1, ":", 2)
env2 := strings.SplitN(*envFq2, ":", 2)
// validation
if len(env1) < 2 || len(env2) < 2 || (env1[0] != local && env1[0] != remote) || (env2[0] != local && env2[0] != remote) {
return nil, fmt.Errorf("<env> must be prefaced by %s: or %s:, ex: %s:us-west/prod", local, remote, remote)
}
if len(files) > 0 {
return nil, fmt.Errorf("'-f' is not currently supported for multiple environments")
}
manager, err := metadata.Find(wd)
if err != nil {
return nil, err
}
if env1[0] == local && env2[0] == local {
return initDiffLocalCmd(fs, env1[1], env2[1], diffStrategy, cmd, manager)
}
if env1[0] == remote && env2[0] == remote {
return initDiffRemotesCmd(fs, env1[1], env2[1], diffStrategy, cmd, manager)
}
localEnv := env1[1]
remoteEnv := env2[1]
if env1[0] == remote {
localEnv = env2[1]
remoteEnv = env1[1]
}
return initDiffRemoteCmd(fs, localEnv, remoteEnv, diffStrategy, cmd, manager)
}
// initDiffSingleEnv sets up configurations for diffing using one environment
func initDiffSingleEnv(fs afero.Fs, env, diffStrategy string, files []string, cmd *cobra.Command, wd string) (kubecfg.DiffCmd, error) {
c := kubecfg.DiffRemoteCmd{}
c.DiffStrategy = diffStrategy
c.Client = &kubecfg.Client{}
var err error
if strings.HasPrefix(env, "remote:") || strings.HasPrefix(env, "local:") {
return nil, fmt.Errorf("single <env> argument with prefix 'local:' or 'remote:' not allowed")
}
te := newCmdObjExpander(cmdObjExpanderConfig{
cmd: cmd,
env: env,
components: files,
cwd: wd,
})
c.Client.APIObjects, err = te.Expand()
if err != nil {
return nil, err
}
c.Client.ClientPool, c.Client.Discovery, c.Client.Namespace, err = client.InitClient(ka, env)
if err != nil {
return nil, err
}
return &c, nil
}
// initDiffLocalCmd sets up configurations for diffing between two sets of expanded Kubernetes objects locally
func initDiffLocalCmd(fs afero.Fs, env1, env2, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) {
c := kubecfg.DiffLocalCmd{}
c.DiffStrategy = diffStrategy
var err error
c.Env1 = &kubecfg.LocalEnv{}
c.Env1.Name = env1
c.Env1.APIObjects, err = expandEnvObjs(fs, cmd, c.Env1.Name, m)
if err != nil {
return nil, err
}
c.Env2 = &kubecfg.LocalEnv{}
c.Env2.Name = env2
c.Env2.APIObjects, err = expandEnvObjs(fs, cmd, c.Env2.Name, m)
if err != nil {
return nil, err
}
return &c, nil
}
// initDiffRemotesCmd sets up configurations for diffing between objects on two remote clusters
func initDiffRemotesCmd(fs afero.Fs, env1, env2, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) {
c := kubecfg.DiffRemotesCmd{}
c.DiffStrategy = diffStrategy
c.ClientA = &kubecfg.Client{}
c.ClientB = &kubecfg.Client{}
c.ClientA.Name = env1
c.ClientB.Name = env2
var err error
c.ClientA.APIObjects, err = expandEnvObjs(fs, cmd, c.ClientA.Name, m)
if err != nil {
return nil, err
}
c.ClientB.APIObjects, err = expandEnvObjs(fs, cmd, c.ClientB.Name, m)
if err != nil {
return nil, err
}
c.ClientA.ClientPool, c.ClientA.Discovery, c.ClientA.Namespace, err = client.InitClient(ka, c.ClientA.Name)
if err != nil {
return nil, err
}
c.ClientB.ClientPool, c.ClientB.Discovery, c.ClientB.Namespace, err = client.InitClient(ka, c.ClientB.Name)
if err != nil {
return nil, err
}
return &c, nil
}
// initDiffRemoteCmd sets up configurations for diffing between local objects and objects on a remote cluster
func initDiffRemoteCmd(fs afero.Fs, localEnv, remoteEnv, diffStrategy string, cmd *cobra.Command, m metadata.Manager) (kubecfg.DiffCmd, error) {
c := kubecfg.DiffRemoteCmd{}
c.DiffStrategy = diffStrategy
c.Client = &kubecfg.Client{}
var err error
c.Client.APIObjects, err = expandEnvObjs(fs, cmd, localEnv, m)
if err != nil {
return nil, err