blob: fd210437dd71d1dd3cb6ce3c30b61e15c36d8f4e [file] [log] [blame]
// 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
}
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
liveObjObject = removeMapFields(obj.Object, liveObjObject)
liveObjObject = removeClusterRoleAggregatedRules(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
}
// removeClusterRoleAggregatedRules clears the rules field from live
// ClusterRole objects which have an aggregationRule. This allows us to diff a
// config object (which doesn't have these rules materialized) against a live
// obejct (which does have these rules materialized) without spurious diffs.
//
// See the Aggregated ClusterRole section of the Kubernetes RBAC docuementation
// for more information:
//
// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles
func removeClusterRoleAggregatedRules(live map[string]interface{}) map[string]interface{} {
if version, ok := live["apiVersion"].(string); !ok || version != "rbac.authorization.k8s.io/v1" {
return live
}
if kind, ok := live["kind"].(string); !ok || kind != "ClusterRole" {
return live
}
if _, ok := live["aggregationRule"].(map[string]interface{}); !ok {
return live
}
// Make copy of map.
res := make(map[string]interface{})
for k, v := range live {
res[k] = v
}
// Clear rules field.
res["rules"] = []interface{}{}
return res
}
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
}