github.com/oam-dev/kubevela@v1.9.11/pkg/appfile/dryrun/diff.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 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 dryrun 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "strings" 24 25 "github.com/aryann/difflib" 26 "github.com/pkg/errors" 27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 "k8s.io/client-go/rest" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 "sigs.k8s.io/yaml" 31 32 workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" 33 "github.com/kubevela/workflow/pkg/cue/packages" 34 35 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 36 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1" 37 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 38 "github.com/oam-dev/kubevela/apis/types" 39 "github.com/oam-dev/kubevela/pkg/appfile" 40 "github.com/oam-dev/kubevela/pkg/oam" 41 ) 42 43 // NewLiveDiffOption creates a live-diff option 44 func NewLiveDiffOption(c client.Client, cfg *rest.Config, pd *packages.PackageDiscover, as []*unstructured.Unstructured) *LiveDiffOption { 45 parser := appfile.NewApplicationParser(c, pd) 46 return &LiveDiffOption{DryRun: NewDryRunOption(c, cfg, pd, as, false), Parser: parser} 47 } 48 49 // ManifestKind enums the kind of OAM objects 50 type ManifestKind string 51 52 // enum kinds of manifest objects 53 const ( 54 AppKind ManifestKind = "Application" 55 AppConfigCompKind ManifestKind = "AppConfigComponent" 56 RawCompKind ManifestKind = "Component" 57 TraitKind ManifestKind = "Trait" 58 PolicyKind ManifestKind = "Policy" 59 WorkflowKind ManifestKind = "Workflow" 60 ReferredObject ManifestKind = "ReferredObject" 61 ) 62 63 // DiffEntry records diff info of OAM object 64 type DiffEntry struct { 65 Name string `json:"name"` 66 Kind ManifestKind `json:"kind"` 67 DiffType DiffType `json:"diffType,omitempty"` 68 Diffs []difflib.DiffRecord `json:"diffs,omitempty"` 69 Subs []*DiffEntry `json:"subs,omitempty"` 70 } 71 72 // DiffType enums the type of diff 73 type DiffType string 74 75 // enum types of diff 76 const ( 77 AddDiff DiffType = "ADD" 78 ModifyDiff DiffType = "MODIFY" 79 RemoveDiff DiffType = "REMOVE" 80 NoDiff DiffType = "" 81 ) 82 83 // manifest is a helper struct used to calculate diff on applications and 84 // sub-resources. 85 type manifest struct { 86 Name string 87 Kind ManifestKind 88 // Data is unmarshalled object in YAML 89 Data string 90 // application's subs means appConfigComponents 91 // appConfigComponent's subs means rawComponent and traits 92 Subs []*manifest 93 } 94 95 func (m *manifest) key() string { 96 return string(m.Kind) + "/" + m.Name 97 } 98 99 // LiveDiffOption contains options for comparing an application with a 100 // living AppRevision in the cluster 101 type LiveDiffOption struct { 102 DryRun 103 Parser *appfile.Parser 104 } 105 106 // LiveDiffObject wraps the objects for diff 107 type LiveDiffObject struct { 108 *v1beta1.Application 109 *v1beta1.ApplicationRevision 110 } 111 112 // RenderlessDiff will not compare the rendered component results but only compare the application spec and 113 // original external dependency objects such as external workflow/policies 114 func (l *LiveDiffOption) RenderlessDiff(ctx context.Context, base, comparor LiveDiffObject) (*DiffEntry, error) { 115 genManifest := func(obj LiveDiffObject) (*manifest, error) { 116 var af *appfile.Appfile 117 var err error 118 var app *v1beta1.Application 119 switch { 120 case obj.Application != nil: 121 app = obj.Application.DeepCopy() 122 af, err = l.Parser.GenerateAppFileFromApp(ctx, obj.Application) 123 case obj.ApplicationRevision != nil: 124 app = obj.ApplicationRevision.Spec.Application.DeepCopy() 125 af, err = l.Parser.GenerateAppFileFromRevision(obj.ApplicationRevision) 126 default: 127 err = errors.Errorf("either application or application revision should be set for LiveDiffObject") 128 } 129 var appfileError error 130 if err != nil { 131 appfileError = err 132 } 133 bs, err := marshalObject(app) 134 if err != nil { 135 return nil, errors.Wrapf(err, "failed to marshal application") 136 } 137 m := &manifest{Name: app.Name, Kind: AppKind, Data: string(bs)} 138 if appfileError != nil { 139 m.Data += "Error: " + appfileError.Error() + "\n" 140 return m, nil // nolint 141 } 142 for _, policy := range af.ExternalPolicies { 143 if bs, err = marshalObject(policy); err == nil { 144 m.Subs = append(m.Subs, &manifest{Name: policy.Name, Kind: PolicyKind, Data: string(bs)}) 145 } else { 146 m.Subs = append(m.Subs, &manifest{Name: policy.Name, Kind: PolicyKind, Data: "Error: " + errors.Wrapf(err, "failed to marshal external policy %s", policy.Name).Error()}) 147 } 148 } 149 if af.ExternalWorkflow != nil { 150 if bs, err = marshalObject(af.ExternalWorkflow); err == nil { 151 m.Subs = append(m.Subs, &manifest{Name: af.ExternalWorkflow.Name, Kind: WorkflowKind, Data: string(bs)}) 152 } else { 153 m.Subs = append(m.Subs, &manifest{Name: af.ExternalWorkflow.Name, Kind: WorkflowKind, Data: "Error: " + errors.Wrapf(err, "failed to marshal external workflow %s", af.ExternalWorkflow.Name).Error()}) 154 } 155 } 156 if af.ReferredObjects != nil { 157 for _, refObj := range af.ReferredObjects { 158 manifestName := fmt.Sprintf("%s %s %s", refObj.GetAPIVersion(), refObj.GetKind(), client.ObjectKeyFromObject(refObj).String()) 159 if bs, err = marshalObject(refObj); err == nil { 160 m.Subs = append(m.Subs, &manifest{Name: manifestName, Kind: ReferredObject, Data: string(bs)}) 161 } else { 162 m.Subs = append(m.Subs, &manifest{Name: manifestName, Kind: ReferredObject, Data: "Error: " + errors.Wrapf(err, "failed to marshal referred object").Error()}) 163 } 164 } 165 } 166 return m, nil 167 } 168 baseManifest, err := genManifest(base) 169 if err != nil { 170 return nil, err 171 } 172 comparorManifest, err := genManifest(comparor) 173 if err != nil { 174 return nil, err 175 } 176 diffResult := l.diffManifest(baseManifest, comparorManifest) 177 return diffResult, nil 178 } 179 180 func calDiffType(diffs []difflib.DiffRecord) DiffType { 181 hasAdd, hasRemove := false, false 182 for _, d := range diffs { 183 switch d.Delta { 184 case difflib.LeftOnly: 185 hasRemove = true 186 case difflib.RightOnly: 187 hasAdd = true 188 default: 189 } 190 } 191 switch { 192 case hasAdd && hasRemove: 193 return ModifyDiff 194 case hasAdd && !hasRemove: 195 return AddDiff 196 case !hasAdd && hasRemove: 197 return RemoveDiff 198 default: 199 return NoDiff 200 } 201 } 202 203 func (l *LiveDiffOption) diffManifest(base, comparor *manifest) *DiffEntry { 204 if base == nil { 205 base = &manifest{} 206 } 207 if comparor == nil { 208 comparor = &manifest{} 209 } 210 entry := &DiffEntry{Name: base.Name, Kind: base.Kind} 211 if base.Name == "" { 212 entry = &DiffEntry{Name: comparor.Name, Kind: comparor.Kind} 213 } 214 const sep = "\n" 215 entry.Diffs = difflib.Diff(strings.Split(comparor.Data, sep), strings.Split(base.Data, sep)) 216 entry.DiffType = calDiffType(entry.Diffs) 217 baseManifestMap, comparorManifestMap := make(map[string]*manifest), make(map[string]*manifest) 218 var keys []string 219 for _, _base := range base.Subs { 220 baseManifestMap[_base.key()] = _base 221 keys = append(keys, _base.key()) 222 } 223 for _, _comparor := range comparor.Subs { 224 comparorManifestMap[_comparor.key()] = _comparor 225 if _, found := baseManifestMap[_comparor.key()]; !found { 226 keys = append(keys, _comparor.key()) 227 } 228 } 229 for _, key := range keys { 230 entry.Subs = append(entry.Subs, l.diffManifest(baseManifestMap[key], comparorManifestMap[key])) 231 } 232 return entry 233 } 234 235 // Diff does three phases, dry-run on input app, preparing manifest for diff, and 236 // calculating diff on manifests. 237 // TODO(wonderflow): vela live-diff don't diff for policies now. 238 func (l *LiveDiffOption) Diff(ctx context.Context, app *v1beta1.Application, appRevision *v1beta1.ApplicationRevision) (*DiffEntry, error) { 239 comps, _, err := l.ExecuteDryRun(ctx, app) 240 if err != nil { 241 return nil, errors.WithMessagef(err, "cannot dry-run for app %q", app.Name) 242 } 243 // new refers to the app as input to dry-run 244 newManifest, err := generateManifest(app, comps) 245 if err != nil { 246 return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", app.Name) 247 } 248 249 // old refers to the living app revision 250 oldManifest, err := generateManifestFromAppRevision(l.Parser, appRevision) 251 if err != nil { 252 return nil, errors.WithMessagef(err, "cannot generate diff manifest for AppRevision %q", appRevision.Name) 253 } 254 diffResult := l.calculateDiff(oldManifest, newManifest) 255 return diffResult, nil 256 } 257 258 // DiffApps does three phases, dry-run on input app, preparing manifest for diff, and 259 // calculating diff on manifests. 260 // TODO(wonderflow): vela live-diff don't diff for policies now. 261 func (l *LiveDiffOption) DiffApps(ctx context.Context, app *v1beta1.Application, oldApp *v1beta1.Application) (*DiffEntry, error) { 262 comps, _, err := l.ExecuteDryRun(ctx, app) 263 if err != nil { 264 return nil, errors.WithMessagef(err, "cannot dry-run for app %q", app.Name) 265 } 266 // new refers to the app as input to dry-run 267 newManifest, err := generateManifest(app, comps) 268 if err != nil { 269 return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", app.Name) 270 } 271 272 oldComps, _, err := l.ExecuteDryRun(ctx, oldApp) 273 if err != nil { 274 return nil, errors.WithMessagef(err, "cannot dry-run for app %q", oldApp.Name) 275 } 276 // new refers to the app as input to dry-run 277 oldManifest, err := generateManifest(oldApp, oldComps) 278 if err != nil { 279 return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", oldApp.Name) 280 } 281 282 diffResult := l.calculateDiff(oldManifest, newManifest) 283 return diffResult, nil 284 } 285 286 // calculateDiff calculate diff between two application and their sub-resources 287 func (l *LiveDiffOption) calculateDiff(oldApp, newApp *manifest) *DiffEntry { 288 emptyManifest := &manifest{} 289 r := &DiffEntry{ 290 Name: oldApp.Name, 291 Kind: oldApp.Kind, 292 } 293 294 appDiffs := diffManifest(oldApp, newApp) 295 if hasChanges(appDiffs) { 296 r.DiffType = ModifyDiff 297 r.Diffs = appDiffs 298 } 299 300 // check modified and removed components 301 for _, oldAcc := range oldApp.Subs { 302 accDiffEntry := &DiffEntry{ 303 Name: oldAcc.Name, 304 Kind: oldAcc.Kind, 305 Subs: make([]*DiffEntry, 0), 306 } 307 308 var newAcc *manifest 309 // check whether component is removed 310 for _, acc := range newApp.Subs { 311 if oldAcc.Name == acc.Name { 312 newAcc = acc 313 break 314 } 315 } 316 if newAcc != nil { 317 // component is not removed 318 // check modified and removed ACC subs (rawComponent and traits) 319 for _, oldAccSub := range oldAcc.Subs { 320 accSubDiffEntry := &DiffEntry{ 321 Name: oldAccSub.Name, 322 Kind: oldAccSub.Kind, 323 } 324 var newAccSub *manifest 325 for _, accSub := range newAcc.Subs { 326 if accSub.Kind == oldAccSub.Kind && 327 accSub.Name == oldAccSub.Name { 328 newAccSub = accSub 329 break 330 } 331 } 332 var diffs []difflib.DiffRecord 333 if newAccSub != nil { 334 // accSub is not removed, then check modification 335 diffs = diffManifest(oldAccSub, newAccSub) 336 if hasChanges(diffs) { 337 accSubDiffEntry.DiffType = ModifyDiff 338 } else { 339 accSubDiffEntry.DiffType = NoDiff 340 } 341 } else { 342 // accSub is removed 343 diffs = diffManifest(oldAccSub, emptyManifest) 344 accSubDiffEntry.DiffType = RemoveDiff 345 } 346 accSubDiffEntry.Diffs = diffs 347 accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry) 348 } 349 350 // check added ACC subs (traits) 351 for _, newAccSub := range newAcc.Subs { 352 isAdded := true 353 for _, oldAccSub := range oldAcc.Subs { 354 if oldAccSub.Kind == newAccSub.Kind && 355 oldAccSub.Name == newAccSub.Name { 356 isAdded = false 357 break 358 } 359 } 360 if isAdded { 361 accSubDiffEntry := &DiffEntry{ 362 Name: newAccSub.Name, 363 Kind: newAccSub.Kind, 364 DiffType: AddDiff, 365 } 366 diffs := diffManifest(emptyManifest, newAccSub) 367 accSubDiffEntry.Diffs = diffs 368 accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry) 369 } 370 } 371 } else { 372 // component is removed as well as its subs 373 accDiffEntry.DiffType = RemoveDiff 374 for _, oldAccSub := range oldAcc.Subs { 375 diffs := diffManifest(oldAccSub, emptyManifest) 376 accSubDiffEntry := &DiffEntry{ 377 Name: oldAccSub.Name, 378 Kind: oldAccSub.Kind, 379 DiffType: RemoveDiff, 380 Diffs: diffs, 381 } 382 accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry) 383 } 384 } 385 r.Subs = append(r.Subs, accDiffEntry) 386 } 387 388 // check added component 389 for _, newAcc := range newApp.Subs { 390 isAdded := true 391 for _, oldAcc := range oldApp.Subs { 392 if oldAcc.Kind == newAcc.Kind && 393 oldAcc.Name == newAcc.Name { 394 isAdded = false 395 break 396 } 397 } 398 if isAdded { 399 accDiffEntry := &DiffEntry{ 400 Name: newAcc.Name, 401 Kind: newAcc.Kind, 402 DiffType: AddDiff, 403 Subs: make([]*DiffEntry, 0), 404 } 405 // added component's subs are all added 406 for _, newAccSub := range newAcc.Subs { 407 diffs := diffManifest(emptyManifest, newAccSub) 408 accSubDiffEntry := &DiffEntry{ 409 Name: newAccSub.Name, 410 Kind: newAccSub.Kind, 411 DiffType: AddDiff, 412 Diffs: diffs, 413 } 414 accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry) 415 } 416 r.Subs = append(r.Subs, accDiffEntry) 417 } 418 } 419 420 return r 421 } 422 423 // generateManifest generates a manifest whose top-level is an application 424 func generateManifest(app *v1beta1.Application, comps []*types.ComponentManifest) (*manifest, error) { 425 r := &manifest{ 426 Name: app.Name, 427 Kind: AppKind, 428 } 429 removeRevisionRelatedLabelAndAnnotation(app) 430 b, err := yaml.Marshal(app) 431 if err != nil { 432 return nil, errors.Wrapf(err, "cannot marshal application %q", app.Name) 433 } 434 r.Data = string(b) 435 appSubs := make([]*manifest, 0, len(app.Spec.Components)) 436 437 // a helper map recording all rawComponents with compName as key 438 rawCompManifests := map[string]*manifest{} 439 for _, comp := range comps { 440 cM := &manifest{ 441 Name: comp.Name, 442 Kind: RawCompKind, 443 } 444 removeRevisionRelatedLabelAndAnnotation(comp.ComponentOutput) 445 b, err := yaml.Marshal(comp.ComponentOutput) 446 if err != nil { 447 return nil, errors.Wrapf(err, "cannot marshal component %q", comp.Name) 448 } 449 cM.Data = string(b) 450 rawCompManifests[comp.Name] = cM 451 } 452 453 // generate appConfigComponent manifests 454 for _, comp := range comps { 455 compM := &manifest{ 456 Name: comp.Name, 457 Kind: AppConfigCompKind, 458 } 459 comp.RevisionHash = "" 460 comp.RevisionName = "" 461 // get matched raw component and add it into appConfigComponent's subs 462 subs := []*manifest{rawCompManifests[comp.Name]} 463 for _, t := range comp.ComponentOutputsAndTraits { 464 removeRevisionRelatedLabelAndAnnotation(t) 465 466 tType := t.GetLabels()[oam.TraitTypeLabel] 467 tResource := t.GetLabels()[oam.TraitResource] 468 // dry-run cannot generate name for a trait 469 // a join of trait type&resource is unique in a component 470 // we use it to identify a trait 471 tUnique := fmt.Sprintf("%s/%s", tType, tResource) 472 473 b, err := yaml.Marshal(t) 474 if err != nil { 475 return nil, errors.Wrapf(err, "cannot parse trait %q raw to YAML", tUnique) 476 } 477 subs = append(subs, &manifest{ 478 Name: tUnique, 479 Kind: TraitKind, 480 Data: string(b), 481 }) 482 } 483 compM.Subs = subs 484 appSubs = append(appSubs, compM) 485 } 486 r.Subs = appSubs 487 return r, nil 488 } 489 490 // generateManifestFromAppRevision generates manifest from an AppRevision 491 func generateManifestFromAppRevision(parser *appfile.Parser, appRevision *v1beta1.ApplicationRevision) (*manifest, error) { 492 af, err := parser.GenerateAppFileFromRevision(appRevision) 493 if err != nil { 494 return nil, err 495 } 496 comps, err := af.GenerateComponentManifests() 497 if err != nil { 498 return nil, err 499 } 500 app := appRevision.Spec.Application 501 // app in appRevision has no name & namespace 502 // we should extract/get them from appRappRevision 503 app.Name = extractNameFromRevisionName(appRevision.Name) 504 app.Namespace = appRevision.Namespace 505 return generateManifest(&app, comps) 506 } 507 508 // diffManifest calculates diff between data of two manifest line by line 509 func diffManifest(old, new *manifest) []difflib.DiffRecord { 510 const sep = "\n" 511 return difflib.Diff(strings.Split(old.Data, sep), strings.Split(new.Data, sep)) 512 } 513 514 func extractNameFromRevisionName(r string) string { 515 s := strings.Split(r, "-") 516 return strings.Join(s[0:len(s)-1], "-") 517 } 518 519 func clearedLabels(labels map[string]string) map[string]string { 520 newLabels := map[string]string{} 521 for k, v := range labels { 522 if k == oam.LabelAppRevision { 523 continue 524 } 525 newLabels[k] = v 526 } 527 if len(newLabels) == 0 { 528 return nil 529 } 530 return newLabels 531 } 532 533 func clearedAnnotations(annotations map[string]string) map[string]string { 534 newAnnotations := map[string]string{} 535 for k, v := range annotations { 536 if k == oam.AnnotationKubeVelaVersion || k == oam.AnnotationAppRevision || k == "kubectl.kubernetes.io/last-applied-configuration" { 537 continue 538 } 539 newAnnotations[k] = v 540 } 541 if len(newAnnotations) == 0 { 542 return nil 543 } 544 return newAnnotations 545 } 546 547 // removeRevisionRelatedLabelAndAnnotation will set label oam.LabelAppRevision to empty 548 // because dry-run cannot set value to this label 549 func removeRevisionRelatedLabelAndAnnotation(o client.Object) { 550 o.SetLabels(clearedLabels(o.GetLabels())) 551 o.SetAnnotations(clearedAnnotations(o.GetAnnotations())) 552 } 553 554 // hasChanges checks whether existing change in diff records 555 func hasChanges(diffs []difflib.DiffRecord) bool { 556 for _, d := range diffs { 557 // difflib.Common means no change between two sides 558 if d.Delta != difflib.Common { 559 return true 560 } 561 } 562 return false 563 } 564 565 func marshalObject(o client.Object) ([]byte, error) { 566 switch obj := o.(type) { 567 case *v1beta1.Application: 568 obj.SetGroupVersionKind(v1beta1.ApplicationKindVersionKind) 569 obj.Status = common.AppStatus{} 570 case *v1alpha1.Policy: 571 obj.SetGroupVersionKind(v1alpha1.PolicyGroupVersionKind) 572 case *workflowv1alpha1.Workflow: 573 obj.SetGroupVersionKind(v1alpha1.WorkflowGroupVersionKind) 574 } 575 o.SetLabels(clearedLabels(o.GetLabels())) 576 o.SetAnnotations(clearedAnnotations(o.GetAnnotations())) 577 bs, err := json.Marshal(o) 578 if err != nil { 579 return bs, err 580 } 581 m := make(map[string]interface{}) 582 if err = json.Unmarshal(bs, &m); err != nil { 583 return bs, err 584 } 585 if metadata, found := m["metadata"]; found { 586 if md, ok := metadata.(map[string]interface{}); ok { 587 _m := make(map[string]interface{}) 588 for k, v := range md { 589 if k == "name" || k == "namespace" || k == "labels" || k == "annotations" { 590 _m[k] = v 591 } 592 } 593 m["metadata"] = _m 594 } 595 } 596 return yaml.Marshal(m) 597 }