cluster/tools/kartongips: init
This forks bitnami/kubecfg into kartongips. The rationale is that we
want to implement hscloud-specific functionality that wouldn't really be
upstreamable into kubecfg (like secret support, mulit-cluster support).
We forked off from github.com/q3k/kubecfg at commit b6817a94492c561ed61a44eeea2d92dcf2e6b8c0.
Change-Id: If5ba513905e0a86f971576fe7061a471c1d8b398
diff --git a/cluster/tools/kartongips/pkg/kubecfg/diff.go b/cluster/tools/kartongips/pkg/kubecfg/diff.go
new file mode 100644
index 0000000..f1136be
--- /dev/null
+++ b/cluster/tools/kartongips/pkg/kubecfg/diff.go
@@ -0,0 +1,233 @@
+// 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 (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "regexp"
+ "sort"
+
+ isatty "github.com/mattn/go-isatty"
+ "github.com/sergi/go-diff/diffmatchpatch"
+ log "github.com/sirupsen/logrus"
+ "k8s.io/apimachinery/pkg/api/errors"
+ "k8s.io/apimachinery/pkg/api/meta"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+ "k8s.io/client-go/dynamic"
+
+ "code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils"
+)
+
+var ErrDiffFound = fmt.Errorf("Differences found.")
+
+// Matches all the line starts on a diff text, which is where we put diff markers and indent
+var DiffLineStart = regexp.MustCompile("(^|\n)(.)")
+
+var DiffKeyValue = regexp.MustCompile(`"([-._a-zA-Z0-9]+)":\s"([[:alnum:]=+]+)",?`)
+
+// DiffCmd represents the diff subcommand
+type DiffCmd struct {
+ Client dynamic.Interface
+ Mapper meta.RESTMapper
+ DefaultNamespace string
+ OmitSecrets bool
+
+ DiffStrategy string
+}
+
+func (c DiffCmd) Run(ctx context.Context, apiObjects []*unstructured.Unstructured, out io.Writer) error {
+ sort.Sort(utils.AlphabeticalOrder(apiObjects))
+
+ dmp := diffmatchpatch.New()
+ diffFound := false
+ for _, obj := range apiObjects {
+ desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(c.Mapper, obj), utils.FqName(obj))
+ log.Debug("Fetching ", desc)
+
+ client, err := utils.ClientForResource(c.Client, c.Mapper, obj, c.DefaultNamespace)
+ if err != nil {
+ return err
+ }
+
+ if obj.GetName() == "" {
+ return fmt.Errorf("Error fetching one of the %s: it does not have a name set", utils.ResourceNameFor(c.Mapper, obj))
+ }
+
+ liveObj, err := client.Get(ctx, obj.GetName(), metav1.GetOptions{})
+ if err != nil && errors.IsNotFound(err) {
+ log.Debugf("%s doesn't exist on the server", desc)
+ liveObj = nil
+ } else if err != nil {
+ return fmt.Errorf("Error fetching %s: %v", desc, err)
+ }
+
+ fmt.Fprintln(out, "---")
+ fmt.Fprintf(out, "- live %s\n+ config %s\n", desc, desc)
+ if liveObj == nil {
+ fmt.Fprintf(out, "%s doesn't exist on server\n", desc)
+ diffFound = true
+ continue
+ }
+
+ liveObjObject := liveObj.Object
+ if c.DiffStrategy == "subset" {
+ liveObjObject = removeMapFields(obj.Object, liveObjObject)
+ }
+
+ liveObjText, _ := json.MarshalIndent(liveObjObject, "", " ")
+ objText, _ := json.MarshalIndent(obj.Object, "", " ")
+
+ liveObjTextLines, objTextLines, lines := dmp.DiffLinesToChars(string(liveObjText), string(objText))
+
+ diff := dmp.DiffMain(
+ string(liveObjTextLines),
+ string(objTextLines),
+ false)
+
+ diff = dmp.DiffCharsToLines(diff, lines)
+ if (len(diff) == 1) && (diff[0].Type == diffmatchpatch.DiffEqual) {
+ fmt.Fprintf(out, "%s unchanged\n", desc)
+ } else {
+ diffFound = true
+ text := c.formatDiff(diff, isatty.IsTerminal(os.Stdout.Fd()), c.OmitSecrets && obj.GetKind() == "Secret")
+ fmt.Fprintf(out, "%s\n", text)
+ }
+ }
+
+ if diffFound {
+ return ErrDiffFound
+ }
+ return nil
+}
+
+// Formats the supplied Diff as a unified-diff-like text with infinite context and optionally colorizes it.
+func (c DiffCmd) formatDiff(diffs []diffmatchpatch.Diff, color bool, omitchanges bool) string {
+ var buff bytes.Buffer
+
+ for _, diff := range diffs {
+ text := diff.Text
+
+ if omitchanges {
+ text = DiffKeyValue.ReplaceAllString(text, "$1: <omitted>")
+ }
+ switch diff.Type {
+ case diffmatchpatch.DiffInsert:
+ if color {
+ _, _ = buff.WriteString("\x1b[32m")
+ }
+ _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1+ $2"))
+ if color {
+ _, _ = buff.WriteString("\x1b[0m")
+ }
+ case diffmatchpatch.DiffDelete:
+ if color {
+ _, _ = buff.WriteString("\x1b[31m")
+ }
+ _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1- $2"))
+ if color {
+ _, _ = buff.WriteString("\x1b[0m")
+ }
+ case diffmatchpatch.DiffEqual:
+ if !omitchanges {
+ _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1 $2"))
+ }
+ }
+ }
+
+ return buff.String()
+}
+
+// See also feature request for golang reflect pkg at
+func isEmptyValue(i interface{}) bool {
+ switch v := i.(type) {
+ case []interface{}:
+ return len(v) == 0
+ case []string:
+ return len(v) == 0
+ case map[string]interface{}:
+ return len(v) == 0
+ case bool:
+ return !v
+ case float64:
+ return v == 0
+ case int64:
+ return v == 0
+ case string:
+ return v == ""
+ case nil:
+ return true
+ default:
+ panic(fmt.Sprintf("Found unexpected type %T in json unmarshal (value=%v)", i, i))
+ }
+}
+
+func removeFields(config, live interface{}) interface{} {
+ switch c := config.(type) {
+ case map[string]interface{}:
+ if live, ok := live.(map[string]interface{}); ok {
+ return removeMapFields(c, live)
+ }
+ case []interface{}:
+ if live, ok := live.([]interface{}); ok {
+ return removeListFields(c, live)
+ }
+ }
+ return live
+}
+
+func removeMapFields(config, live map[string]interface{}) map[string]interface{} {
+ result := map[string]interface{}{}
+ for k, v1 := range config {
+ v2, ok := live[k]
+ if !ok {
+ // Copy empty value from config, as API won't return them,
+ // see https://github.com/bitnami/kubecfg/issues/179
+ if isEmptyValue(v1) {
+ result[k] = v1
+ }
+ continue
+ }
+ result[k] = removeFields(v1, v2)
+ }
+ return result
+}
+
+func removeListFields(config, live []interface{}) []interface{} {
+ // If live is longer than config, then the extra elements at the end of the
+ // list will be returned as is so they appear in the diff.
+ result := make([]interface{}, 0, len(live))
+ for i, v2 := range live {
+ if len(config) > i {
+ result = append(result, removeFields(config[i], v2))
+ } else {
+ result = append(result, v2)
+ }
+ }
+ return result
+}
+
+func istty(w io.Writer) bool {
+ if f, ok := w.(*os.File); ok {
+ return isatty.IsTerminal(f.Fd())
+ }
+ return false
+}