blob: 5d32b9e72720d2e0c6743a52a944db6d471d2eaf [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)
Serge Bazanski72d75742021-09-11 12:20:07 +000094 liveObjObject = removeClusterRoleAggregatedRules(liveObjObject)
Serge Bazanskibe538db2020-11-12 00:22:42 +010095 }
96
97 liveObjText, _ := json.MarshalIndent(liveObjObject, "", " ")
98 objText, _ := json.MarshalIndent(obj.Object, "", " ")
99
100 liveObjTextLines, objTextLines, lines := dmp.DiffLinesToChars(string(liveObjText), string(objText))
101
102 diff := dmp.DiffMain(
103 string(liveObjTextLines),
104 string(objTextLines),
105 false)
106
107 diff = dmp.DiffCharsToLines(diff, lines)
108 if (len(diff) == 1) && (diff[0].Type == diffmatchpatch.DiffEqual) {
109 fmt.Fprintf(out, "%s unchanged\n", desc)
110 } else {
111 diffFound = true
112 text := c.formatDiff(diff, isatty.IsTerminal(os.Stdout.Fd()), c.OmitSecrets && obj.GetKind() == "Secret")
113 fmt.Fprintf(out, "%s\n", text)
114 }
115 }
116
117 if diffFound {
118 return ErrDiffFound
119 }
120 return nil
121}
122
123// Formats the supplied Diff as a unified-diff-like text with infinite context and optionally colorizes it.
124func (c DiffCmd) formatDiff(diffs []diffmatchpatch.Diff, color bool, omitchanges bool) string {
125 var buff bytes.Buffer
126
127 for _, diff := range diffs {
128 text := diff.Text
129
130 if omitchanges {
131 text = DiffKeyValue.ReplaceAllString(text, "$1: <omitted>")
132 }
133 switch diff.Type {
134 case diffmatchpatch.DiffInsert:
135 if color {
136 _, _ = buff.WriteString("\x1b[32m")
137 }
138 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1+ $2"))
139 if color {
140 _, _ = buff.WriteString("\x1b[0m")
141 }
142 case diffmatchpatch.DiffDelete:
143 if color {
144 _, _ = buff.WriteString("\x1b[31m")
145 }
146 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1- $2"))
147 if color {
148 _, _ = buff.WriteString("\x1b[0m")
149 }
150 case diffmatchpatch.DiffEqual:
151 if !omitchanges {
152 _, _ = buff.WriteString(DiffLineStart.ReplaceAllString(text, "$1 $2"))
153 }
154 }
155 }
156
157 return buff.String()
158}
159
160// See also feature request for golang reflect pkg at
161func isEmptyValue(i interface{}) bool {
162 switch v := i.(type) {
163 case []interface{}:
164 return len(v) == 0
165 case []string:
166 return len(v) == 0
167 case map[string]interface{}:
168 return len(v) == 0
169 case bool:
170 return !v
171 case float64:
172 return v == 0
173 case int64:
174 return v == 0
175 case string:
176 return v == ""
177 case nil:
178 return true
179 default:
180 panic(fmt.Sprintf("Found unexpected type %T in json unmarshal (value=%v)", i, i))
181 }
182}
183
184func removeFields(config, live interface{}) interface{} {
185 switch c := config.(type) {
186 case map[string]interface{}:
187 if live, ok := live.(map[string]interface{}); ok {
188 return removeMapFields(c, live)
189 }
190 case []interface{}:
191 if live, ok := live.([]interface{}); ok {
192 return removeListFields(c, live)
193 }
194 }
195 return live
196}
197
198func removeMapFields(config, live map[string]interface{}) map[string]interface{} {
199 result := map[string]interface{}{}
200 for k, v1 := range config {
201 v2, ok := live[k]
202 if !ok {
203 // Copy empty value from config, as API won't return them,
204 // see https://github.com/bitnami/kubecfg/issues/179
205 if isEmptyValue(v1) {
206 result[k] = v1
207 }
208 continue
209 }
210 result[k] = removeFields(v1, v2)
211 }
212 return result
213}
214
Serge Bazanski72d75742021-09-11 12:20:07 +0000215// removeClusterRoleAggregatedRules clears the rules field from live
216// ClusterRole objects which have an aggregationRule. This allows us to diff a
217// config object (which doesn't have these rules materialized) against a live
218// obejct (which does have these rules materialized) without spurious diffs.
219//
220// See the Aggregated ClusterRole section of the Kubernetes RBAC docuementation
221// for more information:
222//
223// https://kubernetes.io/docs/reference/access-authn-authz/rbac/#aggregated-clusterroles
224func removeClusterRoleAggregatedRules(live map[string]interface{}) map[string]interface{} {
225 if version, ok := live["apiVersion"].(string); !ok || version != "rbac.authorization.k8s.io/v1" {
226 return live
227 }
228
229 if kind, ok := live["kind"].(string); !ok || kind != "ClusterRole" {
230 return live
231 }
232
233 if _, ok := live["aggregationRule"].(map[string]interface{}); !ok {
234 return live
235 }
236
237 // Make copy of map.
238 res := make(map[string]interface{})
239 for k, v := range live {
240 res[k] = v
241 }
242 // Clear rules field.
243 res["rules"] = []interface{}{}
244 return res
245}
246
Serge Bazanskibe538db2020-11-12 00:22:42 +0100247func removeListFields(config, live []interface{}) []interface{} {
248 // If live is longer than config, then the extra elements at the end of the
249 // list will be returned as is so they appear in the diff.
250 result := make([]interface{}, 0, len(live))
251 for i, v2 := range live {
252 if len(config) > i {
253 result = append(result, removeFields(config[i], v2))
254 } else {
255 result = append(result, v2)
256 }
257 }
258 return result
259}
260
261func istty(w io.Writer) bool {
262 if f, ok := w.(*os.File); ok {
263 return isatty.IsTerminal(f.Fd())
264 }
265 return false
266}