blob: f1136be3fca5b6ac22915c9d930d7ff5a50c9d8a [file] [log] [blame]
Serge Bazanskibe538db2020-11-12 00:22:42 +01001// Copyright 2017 The kubecfg authors
2//
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16package kubecfg
17
18import (
19 "bytes"
20 "context"
21 "encoding/json"
22 "fmt"
23 "io"
24 "os"
25 "regexp"
26 "sort"
27
28 isatty "github.com/mattn/go-isatty"
29 "github.com/sergi/go-diff/diffmatchpatch"
30 log "github.com/sirupsen/logrus"
31 "k8s.io/apimachinery/pkg/api/errors"
32 "k8s.io/apimachinery/pkg/api/meta"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
35 "k8s.io/client-go/dynamic"
36
37 "code.hackerspace.pl/hscloud/cluster/tools/kartongips/utils"
38)
39
40var ErrDiffFound = fmt.Errorf("Differences found.")
41
42// Matches all the line starts on a diff text, which is where we put diff markers and indent
43var DiffLineStart = regexp.MustCompile("(^|\n)(.)")
44
45var DiffKeyValue = regexp.MustCompile(`"([-._a-zA-Z0-9]+)":\s"([[:alnum:]=+]+)",?`)
46
47// DiffCmd represents the diff subcommand
48type DiffCmd struct {
49 Client dynamic.Interface
50 Mapper meta.RESTMapper
51 DefaultNamespace string
52 OmitSecrets bool
53
54 DiffStrategy string
55}
56
57func (c DiffCmd) Run(ctx context.Context, apiObjects []*unstructured.Unstructured, out io.Writer) error {
58 sort.Sort(utils.AlphabeticalOrder(apiObjects))
59
60 dmp := diffmatchpatch.New()
61 diffFound := false
62 for _, obj := range apiObjects {
63 desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(c.Mapper, obj), utils.FqName(obj))
64 log.Debug("Fetching ", desc)
65
66 client, err := utils.ClientForResource(c.Client, c.Mapper, obj, c.DefaultNamespace)
67 if err != nil {
68 return err
69 }
70
71 if obj.GetName() == "" {
72 return fmt.Errorf("Error fetching one of the %s: it does not have a name set", utils.ResourceNameFor(c.Mapper, obj))
73 }
74
75 liveObj, err := client.Get(ctx, obj.GetName(), metav1.GetOptions{})
76 if err != nil && errors.IsNotFound(err) {
77 log.Debugf("%s doesn't exist on the server", desc)
78 liveObj = nil
79 } else if err != nil {
80 return fmt.Errorf("Error fetching %s: %v", desc, err)
81 }
82
83 fmt.Fprintln(out, "---")
84 fmt.Fprintf(out, "- live %s\n+ config %s\n", desc, desc)
85 if liveObj == nil {
86 fmt.Fprintf(out, "%s doesn't exist on server\n", desc)
87 diffFound = true
88 continue
89 }
90
91 liveObjObject := liveObj.Object
92 if c.DiffStrategy == "subset" {
93 liveObjObject = removeMapFields(obj.Object, liveObjObject)
94 }
95
96 liveObjText, _ := json.MarshalIndent(liveObjObject, "", " ")
97 objText, _ := json.MarshalIndent(obj.Object, "", " ")
98
99 liveObjTextLines, objTextLines, lines := dmp.DiffLinesToChars(string(liveObjText), string(objText))
100
101 diff := dmp.DiffMain(
102 string(liveObjTextLines),
103 string(objTextLines),
104 false)
105
106 diff = dmp.DiffCharsToLines(diff, lines)
107 if (len(diff) == 1) && (diff[0].Type == diffmatchpatch.DiffEqual) {
108 fmt.Fprintf(out, "%s unchanged\n", desc)
109 } else {
110 diffFound = true
111 text := c.formatDiff(diff, isatty.IsTerminal(os.Stdout.Fd()), c.OmitSecrets && obj.GetKind() == "Secret")
112 fmt.Fprintf(out, "%s\n", text)
113 }
114 }
115
116 if diffFound {
117 return ErrDiffFound
118 }
119 return nil
120}
121
122// Formats the supplied Diff as a unified-diff-like text with infinite context and optionally colorizes it.
123func (c DiffCmd) formatDiff(diffs []diffmatchpatch.Diff, color bool, omitchanges bool) string {
124 var buff bytes.Buffer
125
126 for _, diff := range diffs {
127 text := diff.Text
128
129 if omitchanges {
130 text = DiffKeyValue.ReplaceAllString(text, "$1: <omitted>")
131 }
132 switch diff.Type {
133 case diffmatchpatch.DiffInsert:
134 if color {
135 _, _ = buff.WriteString("\x1b[32m")
136 }
137 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1+ $2"))
138 if color {
139 _, _ = buff.WriteString("\x1b[0m")
140 }
141 case diffmatchpatch.DiffDelete:
142 if color {
143 _, _ = buff.WriteString("\x1b[31m")
144 }
145 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1- $2"))
146 if color {
147 _, _ = buff.WriteString("\x1b[0m")
148 }
149 case diffmatchpatch.DiffEqual:
150 if !omitchanges {
151 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1 $2"))
152 }
153 }
154 }
155
156 return buff.String()
157}
158
159// See also feature request for golang reflect pkg at
160func isEmptyValue(i interface{}) bool {
161 switch v := i.(type) {
162 case []interface{}:
163 return len(v) == 0
164 case []string:
165 return len(v) == 0
166 case map[string]interface{}:
167 return len(v) == 0
168 case bool:
169 return !v
170 case float64:
171 return v == 0
172 case int64:
173 return v == 0
174 case string:
175 return v == ""
176 case nil:
177 return true
178 default:
179 panic(fmt.Sprintf("Found unexpected type %T in json unmarshal (value=%v)", i, i))
180 }
181}
182
183func removeFields(config, live interface{}) interface{} {
184 switch c := config.(type) {
185 case map[string]interface{}:
186 if live, ok := live.(map[string]interface{}); ok {
187 return removeMapFields(c, live)
188 }
189 case []interface{}:
190 if live, ok := live.([]interface{}); ok {
191 return removeListFields(c, live)
192 }
193 }
194 return live
195}
196
197func removeMapFields(config, live map[string]interface{}) map[string]interface{} {
198 result := map[string]interface{}{}
199 for k, v1 := range config {
200 v2, ok := live[k]
201 if !ok {
202 // Copy empty value from config, as API won't return them,
203 // see https://github.com/bitnami/kubecfg/issues/179
204 if isEmptyValue(v1) {
205 result[k] = v1
206 }
207 continue
208 }
209 result[k] = removeFields(v1, v2)
210 }
211 return result
212}
213
214func removeListFields(config, live []interface{}) []interface{} {
215 // If live is longer than config, then the extra elements at the end of the
216 // list will be returned as is so they appear in the diff.
217 result := make([]interface{}, 0, len(live))
218 for i, v2 := range live {
219 if len(config) > i {
220 result = append(result, removeFields(config[i], v2))
221 } else {
222 result = append(result, v2)
223 }
224 }
225 return result
226}
227
228func istty(w io.Writer) bool {
229 if f, ok := w.(*os.File); ok {
230 return isatty.IsTerminal(f.Fd())
231 }
232 return false
233}