blob: fd210437dd71d1dd3cb6ce3c30b61e15c36d8f4e [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
Serge Bazanskibe538db2020-11-12 00:22:42 +010053}
54
55func (c DiffCmd) Run(ctx context.Context, apiObjects []*unstructured.Unstructured, out io.Writer) error {
56 sort.Sort(utils.AlphabeticalOrder(apiObjects))
57
58 dmp := diffmatchpatch.New()
59 diffFound := false
60 for _, obj := range apiObjects {
61 desc := fmt.Sprintf("%s %s", utils.ResourceNameFor(c.Mapper, obj), utils.FqName(obj))
62 log.Debug("Fetching ", desc)
63
64 client, err := utils.ClientForResource(c.Client, c.Mapper, obj, c.DefaultNamespace)
65 if err != nil {
66 return err
67 }
68
69 if obj.GetName() == "" {
70 return fmt.Errorf("Error fetching one of the %s: it does not have a name set", utils.ResourceNameFor(c.Mapper, obj))
71 }
72
73 liveObj, err := client.Get(ctx, obj.GetName(), metav1.GetOptions{})
74 if err != nil && errors.IsNotFound(err) {
75 log.Debugf("%s doesn't exist on the server", desc)
76 liveObj = nil
77 } else if err != nil {
78 return fmt.Errorf("Error fetching %s: %v", desc, err)
79 }
80
81 fmt.Fprintln(out, "---")
82 fmt.Fprintf(out, "- live %s\n+ config %s\n", desc, desc)
83 if liveObj == nil {
84 fmt.Fprintf(out, "%s doesn't exist on server\n", desc)
85 diffFound = true
86 continue
87 }
88
89 liveObjObject := liveObj.Object
Serge Bazanski59c81492021-09-11 12:39:51 +000090 liveObjObject = removeMapFields(obj.Object, liveObjObject)
91 liveObjObject = removeClusterRoleAggregatedRules(liveObjObject)
Serge Bazanskibe538db2020-11-12 00:22:42 +010092
93 liveObjText, _ := json.MarshalIndent(liveObjObject, "", " ")
94 objText, _ := json.MarshalIndent(obj.Object, "", " ")
95
96 liveObjTextLines, objTextLines, lines := dmp.DiffLinesToChars(string(liveObjText), string(objText))
97
98 diff := dmp.DiffMain(
99 string(liveObjTextLines),
100 string(objTextLines),
101 false)
102
103 diff = dmp.DiffCharsToLines(diff, lines)
104 if (len(diff) == 1) && (diff[0].Type == diffmatchpatch.DiffEqual) {
105 fmt.Fprintf(out, "%s unchanged\n", desc)
106 } else {
107 diffFound = true
108 text := c.formatDiff(diff, isatty.IsTerminal(os.Stdout.Fd()), c.OmitSecrets && obj.GetKind() == "Secret")
109 fmt.Fprintf(out, "%s\n", text)
110 }
111 }
112
113 if diffFound {
114 return ErrDiffFound
115 }
116 return nil
117}
118
119// Formats the supplied Diff as a unified-diff-like text with infinite context and optionally colorizes it.
120func (c DiffCmd) formatDiff(diffs []diffmatchpatch.Diff, color bool, omitchanges bool) string {
121 var buff bytes.Buffer
122
123 for _, diff := range diffs {
124 text := diff.Text
125
126 if omitchanges {
127 text = DiffKeyValue.ReplaceAllString(text, "$1: <omitted>")
128 }
129 switch diff.Type {
130 case diffmatchpatch.DiffInsert:
131 if color {
132 _, _ = buff.WriteString("\x1b[32m")
133 }
134 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1+ $2"))
135 if color {
136 _, _ = buff.WriteString("\x1b[0m")
137 }
138 case diffmatchpatch.DiffDelete:
139 if color {
140 _, _ = buff.WriteString("\x1b[31m")
141 }
142 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1- $2"))
143 if color {
144 _, _ = buff.WriteString("\x1b[0m")
145 }
146 case diffmatchpatch.DiffEqual:
147 if !omitchanges {
148 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1 $2"))
149 }
150 }
151 }
152
153 return buff.String()
154}
155
156// See also feature request for golang reflect pkg at
157func isEmptyValue(i interface{}) bool {
158 switch v := i.(type) {
159 case []interface{}:
160 return len(v) == 0
161 case []string:
162 return len(v) == 0
163 case map[string]interface{}:
164 return len(v) == 0
165 case bool:
166 return !v
167 case float64:
168 return v == 0
169 case int64:
170 return v == 0
171 case string:
172 return v == ""
173 case nil:
174 return true
175 default:
176 panic(fmt.Sprintf("Found unexpected type %T in json unmarshal (value=%v)", i, i))
177 }
178}
179
180func removeFields(config, live interface{}) interface{} {
181 switch c := config.(type) {
182 case map[string]interface{}:
183 if live, ok := live.(map[string]interface{}); ok {
184 return removeMapFields(c, live)
185 }
186 case []interface{}:
187 if live, ok := live.([]interface{}); ok {
188 return removeListFields(c, live)
189 }
190 }
191 return live
192}
193
194func removeMapFields(config, live map[string]interface{}) map[string]interface{} {
195 result := map[string]interface{}{}
196 for k, v1 := range config {
197 v2, ok := live[k]
198 if !ok {
199 // Copy empty value from config, as API won't return them,
200 // see https://github.com/bitnami/kubecfg/issues/179
201 if isEmptyValue(v1) {
202 result[k] = v1
203 }
204 continue
205 }
206 result[k] = removeFields(v1, v2)
207 }
208 return result
209}
210
Serge Bazanski72d75742021-09-11 12:20:07 +0000211// removeClusterRoleAggregatedRules clears the rules field from live
212// ClusterRole objects which have an aggregationRule. This allows us to diff a
213// config object (which doesn't have these rules materialized) against a live
214// obejct (which does have these rules materialized) without spurious diffs.
215//
216// See the Aggregated ClusterRole section of the Kubernetes RBAC docuementation
217// for more information:
218//
219// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles
220func removeClusterRoleAggregatedRules(live map[string]interface{}) map[string]interface{} {
221 if version, ok := live["apiVersion"].(string); !ok || version != "rbac.authorization.k8s.io/v1" {
222 return live
223 }
224
225 if kind, ok := live["kind"].(string); !ok || kind != "ClusterRole" {
226 return live
227 }
228
229 if _, ok := live["aggregationRule"].(map[string]interface{}); !ok {
230 return live
231 }
232
233 // Make copy of map.
234 res := make(map[string]interface{})
235 for k, v := range live {
236 res[k] = v
237 }
238 // Clear rules field.
239 res["rules"] = []interface{}{}
240 return res
241}
242
Serge Bazanskibe538db2020-11-12 00:22:42 +0100243func removeListFields(config, live []interface{}) []interface{} {
244 // If live is longer than config, then the extra elements at the end of the
245 // list will be returned as is so they appear in the diff.
246 result := make([]interface{}, 0, len(live))
247 for i, v2 := range live {
248 if len(config) > i {
249 result = append(result, removeFields(config[i], v2))
250 } else {
251 result = append(result, v2)
252 }
253 }
254 return result
255}
256
257func istty(w io.Writer) bool {
258 if f, ok := w.(*os.File); ok {
259 return isatty.IsTerminal(f.Fd())
260 }
261 return false
262}