| // 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 |
| } |