-
Notifications
You must be signed in to change notification settings - Fork 0
/
diff.go
597 lines (532 loc) · 16.6 KB
/
diff.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
// Copyright 2018 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package bundlechanges
import (
"reflect"
"sort"
"strings"
"github.com/juju/collections/set"
"github.com/juju/errors"
"github.com/juju/juju/core/logger"
"github.com/juju/juju/internal/charm"
)
// DiffSide represents one side of a bundle-model diff.
type DiffSide string
const (
// None represents neither the bundle or model side (used when neither is missing).
None DiffSide = ""
// BundleSide represents the bundle side of a diff.
BundleSide DiffSide = "bundle"
// ModelSide represents the model side of a diff.
ModelSide DiffSide = "model"
allEndpoints = ""
)
// DiffConfig provides the values and configuration needed to diff the
// bundle and model.
type DiffConfig struct {
Bundle *charm.BundleData
Model *Model
IncludeAnnotations bool
Logger logger.Logger
}
// Validate returns whether this is a valid configuration for diffing.
func (config DiffConfig) Validate() error {
if config.Bundle == nil {
return errors.NotValidf("nil bundle")
}
if config.Model == nil {
return errors.NotValidf("nil model")
}
if config.Logger == nil {
return errors.NotValidf("nil logger")
}
return config.Bundle.Verify(nil, nil, nil)
}
// BuildDiff returns a BundleDiff with the differences between the
// passed in bundle and model.
func BuildDiff(config DiffConfig) (*BundleDiff, error) {
if err := config.Validate(); err != nil {
return nil, errors.Trace(err)
}
differ := &differ{config: config}
return differ.build()
}
type differ struct {
config DiffConfig
}
func (d *differ) build() (*BundleDiff, error) {
return &BundleDiff{
Applications: d.diffApplications(),
Machines: d.diffMachines(),
Relations: d.diffRelations(),
}, nil
}
func (d *differ) diffApplications() map[string]*ApplicationDiff {
// Collect applications from both sides.
allApps := set.NewStrings()
for app := range d.config.Bundle.Applications {
allApps.Add(app)
}
for app := range d.config.Model.Applications {
allApps.Add(app)
}
results := make(map[string]*ApplicationDiff)
for _, name := range allApps.SortedValues() {
diff := d.diffApplication(name)
if diff != nil {
results[name] = diff
}
}
if len(results) == 0 {
return nil
}
return results
}
func (d *differ) diffApplication(name string) *ApplicationDiff {
bundle, found := d.config.Bundle.Applications[name]
if !found {
return &ApplicationDiff{Missing: BundleSide}
}
model, found := d.config.Model.Applications[name]
if !found {
return &ApplicationDiff{Missing: ModelSide}
}
// To avoid potential security issues, exported bundles with explicit
// per-endpoint expose settings do not include the "exposed:true" flag.
// To this end, we must calculate an effective exposed value to use for
// the comparison.
effectiveBundleExpose := bundle.Expose || len(bundle.ExposedEndpoints) != 0
effectiveModelExpose := model.Exposed || len(model.ExposedEndpoints) != 0
// If any of the sides is exposed but lacks any expose endpoint details
// assume an implicit expose "" to 0.0.0.0/0 for comparison purposes.
// This allows us to compute correct diffs with bundles that only
// specify the "exposed:true" flag. This matches the expose behavior on
// pre 2.9 controllers.
if effectiveBundleExpose && len(bundle.ExposedEndpoints) == 0 {
bundle.ExposedEndpoints = map[string]charm.ExposedEndpointSpec{
allEndpoints: {
ExposeToCIDRs: []string{"0.0.0.0/0", "::/0"},
},
}
}
if effectiveModelExpose && len(model.ExposedEndpoints) == 0 {
model.ExposedEndpoints = map[string]ExposedEndpoint{
allEndpoints: {
ExposeToCIDRs: []string{"0.0.0.0/0", "::/0"},
},
}
}
// Use the bundle base as the fallback base if the application doesn't
// supply a base. This is the same machinery that Juju itself uses to
// apply base for applications.
bundleBase := bundle.Base
if bundleBase == "" {
bundleBase = d.config.Bundle.DefaultBase
}
result := &ApplicationDiff{
Charm: d.diffStrings(bundle.Charm, model.Charm),
Expose: d.diffBools(effectiveBundleExpose, effectiveModelExpose),
ExposedEndpoints: d.diffExposedEndpoints(bundle.ExposedEndpoints, model.ExposedEndpoints),
Base: d.diffStrings(bundleBase, model.Base.String()),
Channel: d.diffStrings(bundle.Channel, model.Channel),
Constraints: d.diffStrings(bundle.Constraints, model.Constraints),
Options: d.diffOptions(bundle.Options, model.Options),
}
if bundle.Revision != nil {
result.Revision = d.diffInts(*bundle.Revision, model.Revision)
} else {
result.Revision = d.diffInts(-1, model.Revision)
}
if d.config.IncludeAnnotations {
result.Annotations = d.diffAnnotations(bundle.Annotations, model.Annotations)
}
if len(model.SubordinateTo) == 0 {
// We don't check num_units for subordinate apps.
if d.config.Bundle.Type == Kubernetes {
result.Scale = d.diffInts(bundle.NumUnits, model.Scale)
} else {
result.NumUnits = d.diffInts(bundle.NumUnits, len(model.Units))
}
}
if d.config.Bundle.Type == Kubernetes && len(bundle.To) > 0 {
result.Placement = d.diffStrings(bundle.To[0], model.Placement)
}
if result.Empty() {
return nil
}
return result
}
func (d *differ) diffMachines() map[string]*MachineDiff {
unseen := set.NewStrings()
for machineID := range d.config.Model.Machines {
unseen.Add(machineID)
}
// Go through the machines from the bundle, but keep track of
// which model machines we've seen.
results := make(map[string]*MachineDiff)
for bundleID, bundleMachine := range d.config.Bundle.Machines {
modelID := d.toModelMachineID(bundleID)
unseen.Remove(modelID)
if bundleMachine == nil {
// This is equivalent to an empty machine spec.
bundleMachine = &charm.MachineSpec{}
}
modelMachine, found := d.config.Model.Machines[modelID]
if !found {
results[modelID] = &MachineDiff{Missing: ModelSide}
continue
}
// Use the bundle base as the fallback base if the machine doesn't
// supply a base. This is the same machinery that Juju itself uses to
// apply base for machines.
bundleBase := bundleMachine.Base
if bundleBase == "" {
bundleBase = d.config.Bundle.DefaultBase
}
diff := &MachineDiff{
Base: d.diffStrings(
bundleBase, modelMachine.Base.String(),
),
}
if d.config.IncludeAnnotations {
diff.Annotations = d.diffAnnotations(
bundleMachine.Annotations, modelMachine.Annotations,
)
}
if !diff.Empty() {
results[modelID] = diff
}
}
// Add missing bundle machines for any model machines that weren't
// seen.
for _, modelName := range unseen.Values() {
results[modelName] = &MachineDiff{Missing: BundleSide}
}
if len(results) == 0 {
return nil
}
return results
}
func (d *differ) toModelMachineID(bundleMachineID string) string {
result, found := d.config.Model.MachineMap[bundleMachineID]
if !found {
// We always assume use-existing-machines.
return bundleMachineID
}
return result
}
func (d *differ) diffRelations() *RelationsDiff {
bundleSet := make(map[Relation]bool)
for _, relation := range d.config.Bundle.Relations {
bundleSet[relationFromEndpoints(relation)] = true
}
modelSet := make(map[Relation]bool)
var modelAdditions []Relation
for _, original := range d.config.Model.Relations {
relation := canonicalRelation(original)
modelSet[relation] = true
_, found := bundleSet[relation]
if !found {
modelAdditions = append(modelAdditions, relation)
}
}
var bundleAdditions []Relation
for relation := range bundleSet {
_, found := modelSet[relation]
if !found {
bundleAdditions = append(bundleAdditions, relation)
}
}
if len(bundleAdditions) == 0 && len(modelAdditions) == 0 {
return nil
}
sort.Slice(bundleAdditions, relationLess(bundleAdditions))
sort.Slice(modelAdditions, relationLess(modelAdditions))
return &RelationsDiff{
BundleAdditions: toRelationSlices(bundleAdditions),
ModelAdditions: toRelationSlices(modelAdditions),
}
}
func (d *differ) diffAnnotations(bundle, model map[string]string) map[string]StringDiff {
all := set.NewStrings()
for name := range bundle {
all.Add(name)
}
for name := range model {
all.Add(name)
}
result := make(map[string]StringDiff)
for _, name := range all.Values() {
bundleValue := bundle[name]
modelValue := model[name]
if bundleValue != modelValue {
result[name] = StringDiff{
Bundle: bundleValue,
Model: modelValue,
}
}
}
if len(result) == 0 {
return nil
}
return result
}
func (d *differ) diffOptions(bundle, model map[string]interface{}) map[string]OptionDiff {
all := set.NewStrings()
for name := range bundle {
all.Add(name)
}
for name := range model {
all.Add(name)
}
result := make(map[string]OptionDiff)
for _, name := range all.Values() {
bundleValue := bundle[name]
modelValue := model[name]
if !reflect.DeepEqual(bundleValue, modelValue) {
result[name] = OptionDiff{
Bundle: bundleValue,
Model: modelValue,
}
}
}
if len(result) == 0 {
return nil
}
return result
}
func (d *differ) diffExposedEndpoints(bundle map[string]charm.ExposedEndpointSpec, model map[string]ExposedEndpoint) map[string]ExposedEndpointDiff {
allEndpoints := set.NewStrings()
for name := range bundle {
allEndpoints.Add(name)
}
for name := range model {
allEndpoints.Add(name)
}
result := make(map[string]ExposedEndpointDiff)
for _, name := range allEndpoints.Values() {
bundleValue, foundInBundle := bundle[name]
modelValue, foundInModel := model[name]
if !reflect.DeepEqual(bundleValue.ExposeToSpaces, modelValue.ExposeToSpaces) ||
!reflect.DeepEqual(bundleValue.ExposeToCIDRs, modelValue.ExposeToCIDRs) ||
foundInBundle != foundInModel {
expDiff := ExposedEndpointDiff{}
if foundInBundle {
expDiff.Bundle = &ExposedEndpointDiffEntry{
ExposeToSpaces: bundleValue.ExposeToSpaces,
ExposeToCIDRs: bundleValue.ExposeToCIDRs,
}
}
if foundInModel {
expDiff.Model = &ExposedEndpointDiffEntry{
ExposeToSpaces: modelValue.ExposeToSpaces,
ExposeToCIDRs: modelValue.ExposeToCIDRs,
}
}
result[name] = expDiff
}
}
if len(result) == 0 {
return nil
}
return result
}
func (d *differ) diffStrings(bundle, model string) *StringDiff {
if bundle == model {
return nil
}
return &StringDiff{Bundle: bundle, Model: model}
}
func (d *differ) diffInts(bundle, model int) *IntDiff {
if bundle == model {
return nil
}
return &IntDiff{Bundle: bundle, Model: model}
}
func (d *differ) diffBools(bundle, model bool) *BoolDiff {
if bundle == model {
return nil
}
return &BoolDiff{Bundle: bundle, Model: model}
}
// BundleDiff stores differences between a bundle and a model.
type BundleDiff struct {
Applications map[string]*ApplicationDiff `yaml:"applications,omitempty"`
Machines map[string]*MachineDiff `yaml:"machines,omitempty"`
Relations *RelationsDiff `yaml:"relations,omitempty"`
}
// Empty returns whether the compared bundle and model match (at least
// in terms of the details we check).
func (d *BundleDiff) Empty() bool {
return len(d.Applications) == 0 &&
len(d.Machines) == 0 &&
d.Relations == nil
}
// ApplicationDiff stores differences between an application in a bundle and a model.
type ApplicationDiff struct {
Missing DiffSide `yaml:"missing,omitempty"`
Charm *StringDiff `yaml:"charm,omitempty"`
Base *StringDiff `yaml:"base,omitempty"`
Channel *StringDiff `yaml:"channel,omitempty"`
Revision *IntDiff `yaml:"revision,omitempty"`
Placement *StringDiff `yaml:"placement,omitempty"`
NumUnits *IntDiff `yaml:"num_units,omitempty"`
Scale *IntDiff `yaml:"scale,omitempty"`
Expose *BoolDiff `yaml:"expose,omitempty"`
ExposedEndpoints map[string]ExposedEndpointDiff `yaml:"exposed_endpoints,omitempty"`
Options map[string]OptionDiff `yaml:"options,omitempty"`
Annotations map[string]StringDiff `yaml:"annotations,omitempty"`
Constraints *StringDiff `yaml:"constraints,omitempty"`
// TODO (bundlediff): resources, storage, devices, endpoint
// bindings
}
// Empty returns whether the compared bundle and model applications
// match.
func (d *ApplicationDiff) Empty() bool {
return d.Missing == None &&
d.Charm == nil &&
d.Base == nil &&
d.Channel == nil &&
d.Revision == nil &&
d.Placement == nil &&
d.NumUnits == nil &&
d.Scale == nil &&
d.Expose == nil &&
len(d.ExposedEndpoints) == 0 &&
len(d.Options) == 0 &&
len(d.Annotations) == 0 &&
d.Constraints == nil
}
// StringDiff stores different bundle and model values for some
// string.
type StringDiff struct {
Bundle string `yaml:"bundle"`
Model string `yaml:"model"`
}
// IntDiff stores different bundle and model values for some int.
type IntDiff struct {
Bundle int `yaml:"bundle"`
Model int `yaml:"model"`
}
// BoolDiff stores different bundle and model values for some bool.
type BoolDiff struct {
Bundle bool `yaml:"bundle"`
Model bool `yaml:"model"`
}
// OptionDiff stores different bundle and model values for some
// configuration value.
type OptionDiff struct {
Bundle interface{} `yaml:"bundle"`
Model interface{} `yaml:"model"`
}
// MachineDiff stores differences between a machine in a bundle and a model.
type MachineDiff struct {
Missing DiffSide `yaml:"missing,omitempty"`
Annotations map[string]StringDiff `yaml:"annotations,omitempty"`
Base *StringDiff `yaml:"base,omitempty"`
}
// Empty returns whether the compared bundle and model machines match.
func (d *MachineDiff) Empty() bool {
return d.Missing == None &&
len(d.Annotations) == 0 &&
d.Base == nil
}
// RelationsDiff stores differences between relations in a bundle and
// model.
type RelationsDiff struct {
BundleAdditions [][]string `yaml:"bundle-additions,omitempty"`
ModelAdditions [][]string `yaml:"model-additions,omitempty"`
}
// relationFromEndpoints returns a (canonicalised) Relation from a
// [app1:ep1 app2:ep2] bundle relation.
func relationFromEndpoints(relation []string) Relation {
relation = relation[:]
sort.Strings(relation)
parts1 := strings.SplitN(relation[0], ":", 2)
parts2 := strings.SplitN(relation[1], ":", 2)
// According to our docs, bundles may optionally omit the endpoint from
// relations which will cause an index out of bounds panic when trying
// to construct the Relation instance below.
if len(parts1) == 1 {
parts1 = append(parts1, "")
}
if len(parts2) == 1 {
parts2 = append(parts2, "")
}
return Relation{
App1: parts1[0],
Endpoint1: parts1[1],
App2: parts2[0],
Endpoint2: parts2[1],
}
}
// canonicalRelation ensures that the endpoints of the relation are in
// lexicographic order so we can put them into a map and find them
// even a relation was given to us in the other order.
func canonicalRelation(relation Relation) Relation {
if relation.App1 < relation.App2 {
return relation
}
if relation.App1 == relation.App2 && relation.Endpoint1 <= relation.Endpoint2 {
return relation
}
// The endpoints need to be swapped.
return Relation{
App1: relation.App2,
Endpoint1: relation.Endpoint2,
App2: relation.App1,
Endpoint2: relation.Endpoint1,
}
}
// relationLess returns a func that compares Relations
// lexicographically.
func relationLess(relations []Relation) func(i, j int) bool {
return func(i, j int) bool {
a := relations[i]
b := relations[j]
if a.App1 < b.App1 {
return true
}
if a.App1 > b.App1 {
return false
}
if a.Endpoint1 < b.Endpoint1 {
return true
}
if a.Endpoint1 > b.Endpoint1 {
return false
}
if a.App2 < b.App2 {
return true
}
if a.App2 > b.App2 {
return false
}
return a.Endpoint2 < b.Endpoint2
}
}
// toRelationSlices converts []Relation to [][]string{{"app:ep",
// "app:ep"}}.
func toRelationSlices(relations []Relation) [][]string {
result := make([][]string, len(relations))
for i, relation := range relations {
result[i] = []string{
relation.App1 + ":" + relation.Endpoint1,
relation.App2 + ":" + relation.Endpoint2,
}
}
return result
}
// ExposedEndpointDiff stores different bundle and model values for the expose
// settings for a particular endpoint. Nil values indicate that the value
// was not present in the bundle or model.
type ExposedEndpointDiff struct {
Bundle *ExposedEndpointDiffEntry `yaml:"bundle"`
Model *ExposedEndpointDiffEntry `yaml:"model"`
}
// ExposedEndpointDiffEntry stores the exposed endpoint parameters for
// an ExposedEndpointDiff entry.
type ExposedEndpointDiffEntry struct {
ExposeToSpaces []string `yaml:"expose_to_spaces,omitempty"`
ExposeToCIDRs []string `yaml:"expose_to_cidrs,omitempty"`
}