github.com/splunk/dan1-qbec@v0.7.3/internal/commands/diff.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 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 */ 16 17 package commands 18 19 import ( 20 "fmt" 21 "io" 22 "sort" 23 "sync" 24 25 "github.com/ghodss/yaml" 26 "github.com/spf13/cobra" 27 "github.com/splunk/qbec/internal/diff" 28 "github.com/splunk/qbec/internal/model" 29 "github.com/splunk/qbec/internal/objsort" 30 "github.com/splunk/qbec/internal/remote" 31 "github.com/splunk/qbec/internal/sio" 32 "github.com/splunk/qbec/internal/types" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 ) 35 36 type diffIgnores struct { 37 allAnnotations bool 38 allLabels bool 39 annotationNames []string 40 labelNames []string 41 } 42 43 func (di diffIgnores) preprocess(obj *unstructured.Unstructured) { 44 if di.allLabels || len(di.labelNames) > 0 { 45 labels := obj.GetLabels() 46 if labels == nil { 47 labels = map[string]string{} 48 } 49 if di.allLabels { 50 labels = map[string]string{} 51 } else { 52 for _, l := range di.labelNames { 53 delete(labels, l) 54 } 55 } 56 obj.SetLabels(labels) 57 } 58 if di.allAnnotations || len(di.annotationNames) > 0 { 59 annotations := obj.GetAnnotations() 60 if annotations == nil { 61 annotations = map[string]string{} 62 } 63 if di.allAnnotations { 64 annotations = map[string]string{} 65 } else { 66 for _, l := range di.annotationNames { 67 delete(annotations, l) 68 } 69 } 70 obj.SetAnnotations(annotations) 71 } 72 } 73 74 type diffStats struct { 75 l sync.Mutex 76 Additions []string `json:"additions,omitempty"` 77 Changes []string `json:"changes,omitempty"` 78 Deletions []string `json:"deletions,omitempty"` 79 SameCount int `json:"same,omitempty"` 80 Errors []string `json:"errors,omitempty"` 81 } 82 83 func (d *diffStats) added(s string) { 84 d.l.Lock() 85 defer d.l.Unlock() 86 d.Additions = append(d.Additions, s) 87 } 88 89 func (d *diffStats) changed(s string) { 90 d.l.Lock() 91 defer d.l.Unlock() 92 d.Changes = append(d.Changes, s) 93 } 94 95 func (d *diffStats) deleted(s string) { 96 d.l.Lock() 97 defer d.l.Unlock() 98 d.Deletions = append(d.Deletions, s) 99 } 100 101 func (d *diffStats) same(s string) { 102 d.l.Lock() 103 defer d.l.Unlock() 104 d.SameCount++ 105 } 106 107 func (d *diffStats) errors(s string) { 108 d.l.Lock() 109 defer d.l.Unlock() 110 d.Errors = append(d.Errors, s) 111 } 112 113 func (d *diffStats) done() { 114 sort.Strings(d.Additions) 115 sort.Strings(d.Changes) 116 sort.Strings(d.Errors) 117 } 118 119 type differ struct { 120 w io.Writer 121 client Client 122 opts diff.Options 123 stats diffStats 124 ignores diffIgnores 125 showSecrets bool 126 verbose int 127 } 128 129 func (d *differ) names(ob model.K8sMeta) (name, leftName, rightName string) { 130 name = d.client.DisplayName(ob) 131 leftName = "live " + name 132 rightName = "config " + name 133 return 134 } 135 136 type namedUn struct { 137 name string 138 obj *unstructured.Unstructured 139 } 140 141 // writeDiff writes the diff between the left and right objects. Either of these 142 // objects may be nil in which case the supplied object text is diffed against 143 // a blank string. Care must be taken to ensure that only a single write is made to the writer for every invocation. 144 // Otherwise output will be interleaved across diffs. 145 func (d *differ) writeDiff(name string, left, right namedUn) (finalErr error) { 146 asYaml := func(obj interface{}) (string, error) { 147 b, err := yaml.Marshal(obj) 148 if err != nil { 149 return "", err 150 } 151 return string(b), nil 152 } 153 addLeader := func(s, leader string) string { 154 l := fmt.Sprintf("#\n# %s\n#\n", leader) 155 return l + s 156 } 157 defer func() { 158 if finalErr != nil { 159 d.stats.errors(name) 160 sio.Errorf("error diffing %s, %v\n", name, finalErr) 161 } 162 }() 163 164 fileOpts := d.opts 165 fileOpts.LeftName = left.name 166 fileOpts.RightName = right.name 167 switch { 168 case left.obj == nil && &right.obj == nil: 169 return fmt.Errorf("internal error: both left and right objects were nil for diff") 170 case left.obj != nil && right.obj != nil: 171 b, err := diff.Objects(left.obj, right.obj, fileOpts) 172 if err != nil { 173 return err 174 } 175 if len(b) == 0 { 176 if d.verbose > 0 { 177 fmt.Fprintf(d.w, "%s unchanged\n", name) 178 } 179 d.stats.same(name) 180 } else { 181 fmt.Fprintln(d.w, string(b)) 182 d.stats.changed(name) 183 } 184 case left.obj == nil: 185 rightContent, err := asYaml(right.obj) 186 if err != nil { 187 return err 188 } 189 leaderComment := "object doesn't exist on the server" 190 if right.obj.GetName() == "" { 191 leaderComment += " (generated name)" 192 } 193 rightContent = addLeader(rightContent, leaderComment) 194 b, err := diff.Strings("", rightContent, fileOpts) 195 if err != nil { 196 return err 197 } 198 fmt.Fprintln(d.w, string(b)) 199 d.stats.added(name) 200 default: 201 leftContent, err := asYaml(left.obj) 202 if err != nil { 203 return err 204 } 205 leftContent = addLeader(leftContent, "object doesn't exist locally") 206 b, err := diff.Strings(leftContent, "", fileOpts) 207 if err != nil { 208 return err 209 } 210 fmt.Fprintln(d.w, string(b)) 211 d.stats.deleted(name) 212 } 213 return nil 214 } 215 216 // diff diffs the supplied object with its remote version and writes output to its writer. 217 // The local version is found by downcasting the supplied metadata to a local object. 218 // This cast should succeed for all but the deletion use case. 219 func (d *differ) diff(ob model.K8sMeta) error { 220 name, leftName, rightName := d.names(ob) 221 222 var remoteObject *unstructured.Unstructured 223 var err error 224 225 if ob.GetName() != "" { 226 remoteObject, err = d.client.Get(ob) 227 if err != nil && err != remote.ErrNotFound { 228 d.stats.errors(name) 229 sio.Errorf("error fetching %s, %v\n", name, err) 230 return err 231 } 232 } 233 234 fixup := func(u *unstructured.Unstructured) *unstructured.Unstructured { 235 if u == nil { 236 return u 237 } 238 if !d.showSecrets { 239 u, _ = types.HideSensitiveInfo(u) 240 } 241 d.ignores.preprocess(u) 242 return u 243 } 244 245 var left, right *unstructured.Unstructured 246 if remoteObject != nil { 247 var source string 248 left, source = remote.GetPristineVersionForDiff(remoteObject) 249 leftName += " (source: " + source + ")" 250 } 251 left = fixup(left) 252 253 if r, ok := ob.(model.K8sObject); ok { 254 right = fixup(r.ToUnstructured()) 255 } 256 return d.writeDiff(name, namedUn{name: leftName, obj: left}, namedUn{name: rightName, obj: right}) 257 } 258 259 // diffLocal adapts the diff method to run as a parallel worker. 260 func (d *differ) diffLocal(ob model.K8sLocalObject) error { 261 return d.diff(ob) 262 } 263 264 type diffCommandConfig struct { 265 *Config 266 showDeletions bool 267 showSecrets bool 268 parallel int 269 contextLines int 270 di diffIgnores 271 filterFunc func() (filterParams, error) 272 } 273 274 func doDiff(args []string, config diffCommandConfig) error { 275 if len(args) != 1 { 276 return newUsageError("exactly one environment required") 277 } 278 279 env := args[0] 280 if env == model.Baseline { 281 return newUsageError("cannot diff baseline environment, use a real environment") 282 } 283 fp, err := config.filterFunc() 284 if err != nil { 285 return err 286 } 287 288 client, err := config.Client(env) 289 if err != nil { 290 return err 291 } 292 293 objects, err := filteredObjects(config.Config, env, client.ObjectKey, fp) 294 if err != nil { 295 return err 296 } 297 298 var lister lister = &stubLister{} 299 var all []model.K8sLocalObject 300 var retainObjects []model.K8sLocalObject 301 if config.showDeletions { 302 all, err = allObjects(config.Config, env) 303 if err != nil { 304 return err 305 } 306 for _, o := range all { 307 if o.GetName() != "" { 308 retainObjects = append(retainObjects, o) 309 } 310 } 311 var scope remote.ListQueryScope 312 lister, scope, err = newRemoteLister(client, all, config.app.DefaultNamespace(env)) 313 if err != nil { 314 return err 315 } 316 lister.start(remote.ListQueryConfig{ 317 Application: config.App().Name(), 318 Tag: config.App().Tag(), 319 Environment: env, 320 KindFilter: fp.kindFilter, 321 ListQueryScope: scope, 322 }) 323 } 324 325 objects = objsort.Sort(objects, sortConfig(client.IsNamespaced)) 326 327 // since the 0 value of context is turned to 3 by the diff library, 328 // special case to turn 0 into a negative number so that zero means zero. 329 if config.contextLines == 0 { 330 config.contextLines = -1 331 } 332 opts := diff.Options{Context: config.contextLines, Colorize: config.Colorize()} 333 334 w := &lockWriter{Writer: config.Stdout()} 335 d := &differ{ 336 w: w, 337 client: client, 338 opts: opts, 339 ignores: config.di, 340 showSecrets: config.showSecrets, 341 verbose: config.Verbosity(), 342 } 343 dErr := runInParallel(objects, d.diffLocal, config.parallel) 344 345 var listErr error 346 if dErr == nil { 347 extra, err := lister.deletions(retainObjects, fp.Includes) 348 if err != nil { 349 listErr = err 350 } else { 351 for _, ob := range extra { 352 if err := d.diff(ob); err != nil { 353 return err 354 } 355 } 356 } 357 } 358 359 d.stats.done() 360 printStats(d.w, &d.stats) 361 numDiffs := len(d.stats.Additions) + len(d.stats.Changes) + len(d.stats.Deletions) 362 363 switch { 364 case dErr != nil: 365 return dErr 366 case listErr != nil: 367 return listErr 368 case numDiffs > 0: 369 return fmt.Errorf("%d object(s) different", numDiffs) 370 default: 371 return nil 372 } 373 } 374 375 func newDiffCommand(cp ConfigProvider) *cobra.Command { 376 cmd := &cobra.Command{ 377 Use: "diff <environment>", 378 Short: "diff one or more components against objects in a Kubernetes cluster", 379 Example: diffExamples(), 380 } 381 382 config := diffCommandConfig{ 383 filterFunc: addFilterParams(cmd, true), 384 } 385 cmd.Flags().BoolVar(&config.showDeletions, "show-deletes", true, "include deletions in diff") 386 cmd.Flags().IntVar(&config.contextLines, "context", 3, "context lines for diff") 387 cmd.Flags().IntVar(&config.parallel, "parallel", 5, "number of parallel routines to run") 388 cmd.Flags().BoolVarP(&config.showSecrets, "show-secrets", "S", false, "do not obfuscate secret values in the diff") 389 cmd.Flags().BoolVar(&config.di.allAnnotations, "ignore-all-annotations", false, "remove all annotations from objects before diff") 390 cmd.Flags().StringArrayVar(&config.di.annotationNames, "ignore-annotation", nil, "remove specific annotation from objects before diff") 391 cmd.Flags().BoolVar(&config.di.allLabels, "ignore-all-labels", false, "remove all labels from objects before diff") 392 cmd.Flags().StringArrayVar(&config.di.labelNames, "ignore-label", nil, "remove specific label from objects before diff") 393 394 cmd.RunE = func(c *cobra.Command, args []string) error { 395 config.Config = cp() 396 return wrapError(doDiff(args, config)) 397 } 398 return cmd 399 }