Files
kubevela/pkg/appfile/dryrun/diff.go
Tianxin Dong 4f8bf44684 Refactor: use cuex engine (#6575)
* refactor: use cuex engine

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix lint

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix unit test

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix static check and sdk tests

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix testdata

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix velaql unit test

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix docgen parser

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix cuegen

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix velaql

Signed-off-by: FogDong <fog@bentoml.com>

* fix: delete useless print

Signed-off-by: FogDong <fog@bentoml.com>

* fix: set client for ql

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix mt tests

Signed-off-by: FogDong <fog@bentoml.com>

* fix: set kubeclient in generator

Signed-off-by: FogDong <fog@bentoml.com>

* fix: use pass kube client

Signed-off-by: FogDong <fog@bentoml.com>

* fix: simplify ql

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix lint

Signed-off-by: FogDong <fog@bentoml.com>

* fix: add wf debug back

Signed-off-by: FogDong <fog@bentoml.com>

* fix: add loader

Signed-off-by: FogDong <fog@bentoml.com>

---------

Signed-off-by: FogDong <fog@bentoml.com>
2024-07-27 17:44:20 +08:00

597 lines
18 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/aryann/difflib"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/oam"
)
// NewLiveDiffOption creates a live-diff option
func NewLiveDiffOption(c client.Client, cfg *rest.Config, as []*unstructured.Unstructured) *LiveDiffOption {
parser := appfile.NewApplicationParser(c)
return &LiveDiffOption{DryRun: NewDryRunOption(c, cfg, as, false), Parser: parser}
}
// ManifestKind enums the kind of OAM objects
type ManifestKind string
// enum kinds of manifest objects
const (
AppKind ManifestKind = "Application"
AppConfigCompKind ManifestKind = "AppConfigComponent"
RawCompKind ManifestKind = "Component"
TraitKind ManifestKind = "Trait"
PolicyKind ManifestKind = "Policy"
WorkflowKind ManifestKind = "Workflow"
ReferredObject ManifestKind = "ReferredObject"
)
// DiffEntry records diff info of OAM object
type DiffEntry struct {
Name string `json:"name"`
Kind ManifestKind `json:"kind"`
DiffType DiffType `json:"diffType,omitempty"`
Diffs []difflib.DiffRecord `json:"diffs,omitempty"`
Subs []*DiffEntry `json:"subs,omitempty"`
}
// DiffType enums the type of diff
type DiffType string
// enum types of diff
const (
AddDiff DiffType = "ADD"
ModifyDiff DiffType = "MODIFY"
RemoveDiff DiffType = "REMOVE"
NoDiff DiffType = ""
)
// manifest is a helper struct used to calculate diff on applications and
// sub-resources.
type manifest struct {
Name string
Kind ManifestKind
// Data is unmarshalled object in YAML
Data string
// application's subs means appConfigComponents
// appConfigComponent's subs means rawComponent and traits
Subs []*manifest
}
func (m *manifest) key() string {
return string(m.Kind) + "/" + m.Name
}
// LiveDiffOption contains options for comparing an application with a
// living AppRevision in the cluster
type LiveDiffOption struct {
DryRun
Parser *appfile.Parser
}
// LiveDiffObject wraps the objects for diff
type LiveDiffObject struct {
*v1beta1.Application
*v1beta1.ApplicationRevision
}
// RenderlessDiff will not compare the rendered component results but only compare the application spec and
// original external dependency objects such as external workflow/policies
func (l *LiveDiffOption) RenderlessDiff(ctx context.Context, base, comparor LiveDiffObject) (*DiffEntry, error) {
genManifest := func(obj LiveDiffObject) (*manifest, error) {
var af *appfile.Appfile
var err error
var app *v1beta1.Application
switch {
case obj.Application != nil:
app = obj.Application.DeepCopy()
af, err = l.Parser.GenerateAppFileFromApp(ctx, obj.Application)
case obj.ApplicationRevision != nil:
app = obj.ApplicationRevision.Spec.Application.DeepCopy()
af, err = l.Parser.GenerateAppFileFromRevision(obj.ApplicationRevision)
default:
err = errors.Errorf("either application or application revision should be set for LiveDiffObject")
}
var appfileError error
if err != nil {
appfileError = err
}
bs, err := marshalObject(app)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal application")
}
m := &manifest{Name: app.Name, Kind: AppKind, Data: string(bs)}
if appfileError != nil {
m.Data += "Error: " + appfileError.Error() + "\n"
return m, nil // nolint
}
for _, policy := range af.ExternalPolicies {
if bs, err = marshalObject(policy); err == nil {
m.Subs = append(m.Subs, &manifest{Name: policy.Name, Kind: PolicyKind, Data: string(bs)})
} else {
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()})
}
}
if af.ExternalWorkflow != nil {
if bs, err = marshalObject(af.ExternalWorkflow); err == nil {
m.Subs = append(m.Subs, &manifest{Name: af.ExternalWorkflow.Name, Kind: WorkflowKind, Data: string(bs)})
} else {
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()})
}
}
if af.ReferredObjects != nil {
for _, refObj := range af.ReferredObjects {
manifestName := fmt.Sprintf("%s %s %s", refObj.GetAPIVersion(), refObj.GetKind(), client.ObjectKeyFromObject(refObj).String())
if bs, err = marshalObject(refObj); err == nil {
m.Subs = append(m.Subs, &manifest{Name: manifestName, Kind: ReferredObject, Data: string(bs)})
} else {
m.Subs = append(m.Subs, &manifest{Name: manifestName, Kind: ReferredObject, Data: "Error: " + errors.Wrapf(err, "failed to marshal referred object").Error()})
}
}
}
return m, nil
}
baseManifest, err := genManifest(base)
if err != nil {
return nil, err
}
comparorManifest, err := genManifest(comparor)
if err != nil {
return nil, err
}
diffResult := l.diffManifest(baseManifest, comparorManifest)
return diffResult, nil
}
func calDiffType(diffs []difflib.DiffRecord) DiffType {
hasAdd, hasRemove := false, false
for _, d := range diffs {
switch d.Delta {
case difflib.LeftOnly:
hasRemove = true
case difflib.RightOnly:
hasAdd = true
default:
}
}
switch {
case hasAdd && hasRemove:
return ModifyDiff
case hasAdd && !hasRemove:
return AddDiff
case !hasAdd && hasRemove:
return RemoveDiff
default:
return NoDiff
}
}
func (l *LiveDiffOption) diffManifest(base, comparor *manifest) *DiffEntry {
if base == nil {
base = &manifest{}
}
if comparor == nil {
comparor = &manifest{}
}
entry := &DiffEntry{Name: base.Name, Kind: base.Kind}
if base.Name == "" {
entry = &DiffEntry{Name: comparor.Name, Kind: comparor.Kind}
}
const sep = "\n"
entry.Diffs = difflib.Diff(strings.Split(comparor.Data, sep), strings.Split(base.Data, sep))
entry.DiffType = calDiffType(entry.Diffs)
baseManifestMap, comparorManifestMap := make(map[string]*manifest), make(map[string]*manifest)
var keys []string
for _, _base := range base.Subs {
baseManifestMap[_base.key()] = _base
keys = append(keys, _base.key())
}
for _, _comparor := range comparor.Subs {
comparorManifestMap[_comparor.key()] = _comparor
if _, found := baseManifestMap[_comparor.key()]; !found {
keys = append(keys, _comparor.key())
}
}
for _, key := range keys {
entry.Subs = append(entry.Subs, l.diffManifest(baseManifestMap[key], comparorManifestMap[key]))
}
return entry
}
// Diff does three phases, dry-run on input app, preparing manifest for diff, and
// calculating diff on manifests.
// TODO(wonderflow): vela live-diff don't diff for policies now.
func (l *LiveDiffOption) Diff(ctx context.Context, app *v1beta1.Application, appRevision *v1beta1.ApplicationRevision) (*DiffEntry, error) {
comps, _, err := l.ExecuteDryRun(ctx, app)
if err != nil {
return nil, errors.WithMessagef(err, "cannot dry-run for app %q", app.Name)
}
// new refers to the app as input to dry-run
newManifest, err := generateManifest(app, comps)
if err != nil {
return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", app.Name)
}
// old refers to the living app revision
oldManifest, err := generateManifestFromAppRevision(l.Parser, appRevision)
if err != nil {
return nil, errors.WithMessagef(err, "cannot generate diff manifest for AppRevision %q", appRevision.Name)
}
diffResult := l.calculateDiff(oldManifest, newManifest)
return diffResult, nil
}
// DiffApps does three phases, dry-run on input app, preparing manifest for diff, and
// calculating diff on manifests.
// TODO(wonderflow): vela live-diff don't diff for policies now.
func (l *LiveDiffOption) DiffApps(ctx context.Context, app *v1beta1.Application, oldApp *v1beta1.Application) (*DiffEntry, error) {
comps, _, err := l.ExecuteDryRun(ctx, app)
if err != nil {
return nil, errors.WithMessagef(err, "cannot dry-run for app %q", app.Name)
}
// new refers to the app as input to dry-run
newManifest, err := generateManifest(app, comps)
if err != nil {
return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", app.Name)
}
oldComps, _, err := l.ExecuteDryRun(ctx, oldApp)
if err != nil {
return nil, errors.WithMessagef(err, "cannot dry-run for app %q", oldApp.Name)
}
// new refers to the app as input to dry-run
oldManifest, err := generateManifest(oldApp, oldComps)
if err != nil {
return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", oldApp.Name)
}
diffResult := l.calculateDiff(oldManifest, newManifest)
return diffResult, nil
}
// calculateDiff calculate diff between two application and their sub-resources
func (l *LiveDiffOption) calculateDiff(oldApp, newApp *manifest) *DiffEntry {
emptyManifest := &manifest{}
r := &DiffEntry{
Name: oldApp.Name,
Kind: oldApp.Kind,
}
appDiffs := diffManifest(oldApp, newApp)
if hasChanges(appDiffs) {
r.DiffType = ModifyDiff
r.Diffs = appDiffs
}
// check modified and removed components
for _, oldAcc := range oldApp.Subs {
accDiffEntry := &DiffEntry{
Name: oldAcc.Name,
Kind: oldAcc.Kind,
Subs: make([]*DiffEntry, 0),
}
var newAcc *manifest
// check whether component is removed
for _, acc := range newApp.Subs {
if oldAcc.Name == acc.Name {
newAcc = acc
break
}
}
if newAcc != nil {
// component is not removed
// check modified and removed ACC subs (rawComponent and traits)
for _, oldAccSub := range oldAcc.Subs {
accSubDiffEntry := &DiffEntry{
Name: oldAccSub.Name,
Kind: oldAccSub.Kind,
}
var newAccSub *manifest
for _, accSub := range newAcc.Subs {
if accSub.Kind == oldAccSub.Kind &&
accSub.Name == oldAccSub.Name {
newAccSub = accSub
break
}
}
var diffs []difflib.DiffRecord
if newAccSub != nil {
// accSub is not removed, then check modification
diffs = diffManifest(oldAccSub, newAccSub)
if hasChanges(diffs) {
accSubDiffEntry.DiffType = ModifyDiff
} else {
accSubDiffEntry.DiffType = NoDiff
}
} else {
// accSub is removed
diffs = diffManifest(oldAccSub, emptyManifest)
accSubDiffEntry.DiffType = RemoveDiff
}
accSubDiffEntry.Diffs = diffs
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
// check added ACC subs (traits)
for _, newAccSub := range newAcc.Subs {
isAdded := true
for _, oldAccSub := range oldAcc.Subs {
if oldAccSub.Kind == newAccSub.Kind &&
oldAccSub.Name == newAccSub.Name {
isAdded = false
break
}
}
if isAdded {
accSubDiffEntry := &DiffEntry{
Name: newAccSub.Name,
Kind: newAccSub.Kind,
DiffType: AddDiff,
}
diffs := diffManifest(emptyManifest, newAccSub)
accSubDiffEntry.Diffs = diffs
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
}
} else {
// component is removed as well as its subs
accDiffEntry.DiffType = RemoveDiff
for _, oldAccSub := range oldAcc.Subs {
diffs := diffManifest(oldAccSub, emptyManifest)
accSubDiffEntry := &DiffEntry{
Name: oldAccSub.Name,
Kind: oldAccSub.Kind,
DiffType: RemoveDiff,
Diffs: diffs,
}
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
}
r.Subs = append(r.Subs, accDiffEntry)
}
// check added component
for _, newAcc := range newApp.Subs {
isAdded := true
for _, oldAcc := range oldApp.Subs {
if oldAcc.Kind == newAcc.Kind &&
oldAcc.Name == newAcc.Name {
isAdded = false
break
}
}
if isAdded {
accDiffEntry := &DiffEntry{
Name: newAcc.Name,
Kind: newAcc.Kind,
DiffType: AddDiff,
Subs: make([]*DiffEntry, 0),
}
// added component's subs are all added
for _, newAccSub := range newAcc.Subs {
diffs := diffManifest(emptyManifest, newAccSub)
accSubDiffEntry := &DiffEntry{
Name: newAccSub.Name,
Kind: newAccSub.Kind,
DiffType: AddDiff,
Diffs: diffs,
}
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
r.Subs = append(r.Subs, accDiffEntry)
}
}
return r
}
// generateManifest generates a manifest whose top-level is an application
func generateManifest(app *v1beta1.Application, comps []*types.ComponentManifest) (*manifest, error) {
r := &manifest{
Name: app.Name,
Kind: AppKind,
}
removeRevisionRelatedLabelAndAnnotation(app)
b, err := yaml.Marshal(app)
if err != nil {
return nil, errors.Wrapf(err, "cannot marshal application %q", app.Name)
}
r.Data = string(b)
appSubs := make([]*manifest, 0, len(app.Spec.Components))
// a helper map recording all rawComponents with compName as key
rawCompManifests := map[string]*manifest{}
for _, comp := range comps {
cM := &manifest{
Name: comp.Name,
Kind: RawCompKind,
}
removeRevisionRelatedLabelAndAnnotation(comp.ComponentOutput)
b, err := yaml.Marshal(comp.ComponentOutput)
if err != nil {
return nil, errors.Wrapf(err, "cannot marshal component %q", comp.Name)
}
cM.Data = string(b)
rawCompManifests[comp.Name] = cM
}
// generate appConfigComponent manifests
for _, comp := range comps {
compM := &manifest{
Name: comp.Name,
Kind: AppConfigCompKind,
}
comp.RevisionHash = ""
comp.RevisionName = ""
// get matched raw component and add it into appConfigComponent's subs
subs := []*manifest{rawCompManifests[comp.Name]}
for _, t := range comp.ComponentOutputsAndTraits {
removeRevisionRelatedLabelAndAnnotation(t)
tType := t.GetLabels()[oam.TraitTypeLabel]
tResource := t.GetLabels()[oam.TraitResource]
// dry-run cannot generate name for a trait
// a join of trait type&resource is unique in a component
// we use it to identify a trait
tUnique := fmt.Sprintf("%s/%s", tType, tResource)
b, err := yaml.Marshal(t)
if err != nil {
return nil, errors.Wrapf(err, "cannot parse trait %q raw to YAML", tUnique)
}
subs = append(subs, &manifest{
Name: tUnique,
Kind: TraitKind,
Data: string(b),
})
}
compM.Subs = subs
appSubs = append(appSubs, compM)
}
r.Subs = appSubs
return r, nil
}
// generateManifestFromAppRevision generates manifest from an AppRevision
func generateManifestFromAppRevision(parser *appfile.Parser, appRevision *v1beta1.ApplicationRevision) (*manifest, error) {
af, err := parser.GenerateAppFileFromRevision(appRevision)
if err != nil {
return nil, err
}
comps, err := af.GenerateComponentManifests()
if err != nil {
return nil, err
}
app := appRevision.Spec.Application
// app in appRevision has no name & namespace
// we should extract/get them from appRappRevision
app.Name = extractNameFromRevisionName(appRevision.Name)
app.Namespace = appRevision.Namespace
return generateManifest(&app, comps)
}
// diffManifest calculates diff between data of two manifest line by line
func diffManifest(old, new *manifest) []difflib.DiffRecord {
const sep = "\n"
return difflib.Diff(strings.Split(old.Data, sep), strings.Split(new.Data, sep))
}
func extractNameFromRevisionName(r string) string {
s := strings.Split(r, "-")
return strings.Join(s[0:len(s)-1], "-")
}
func clearedLabels(labels map[string]string) map[string]string {
newLabels := map[string]string{}
for k, v := range labels {
if k == oam.LabelAppRevision {
continue
}
newLabels[k] = v
}
if len(newLabels) == 0 {
return nil
}
return newLabels
}
func clearedAnnotations(annotations map[string]string) map[string]string {
newAnnotations := map[string]string{}
for k, v := range annotations {
if k == oam.AnnotationKubeVelaVersion || k == oam.AnnotationAppRevision || k == "kubectl.kubernetes.io/last-applied-configuration" {
continue
}
newAnnotations[k] = v
}
if len(newAnnotations) == 0 {
return nil
}
return newAnnotations
}
// removeRevisionRelatedLabelAndAnnotation will set label oam.LabelAppRevision to empty
// because dry-run cannot set value to this label
func removeRevisionRelatedLabelAndAnnotation(o client.Object) {
o.SetLabels(clearedLabels(o.GetLabels()))
o.SetAnnotations(clearedAnnotations(o.GetAnnotations()))
}
// hasChanges checks whether existing change in diff records
func hasChanges(diffs []difflib.DiffRecord) bool {
for _, d := range diffs {
// difflib.Common means no change between two sides
if d.Delta != difflib.Common {
return true
}
}
return false
}
func marshalObject(o client.Object) ([]byte, error) {
switch obj := o.(type) {
case *v1beta1.Application:
obj.SetGroupVersionKind(v1beta1.ApplicationKindVersionKind)
obj.Status = common.AppStatus{}
case *v1alpha1.Policy:
obj.SetGroupVersionKind(v1alpha1.PolicyGroupVersionKind)
case *workflowv1alpha1.Workflow:
obj.SetGroupVersionKind(v1alpha1.WorkflowGroupVersionKind)
}
o.SetLabels(clearedLabels(o.GetLabels()))
o.SetAnnotations(clearedAnnotations(o.GetAnnotations()))
bs, err := json.Marshal(o)
if err != nil {
return bs, err
}
m := make(map[string]interface{})
if err = json.Unmarshal(bs, &m); err != nil {
return bs, err
}
if metadata, found := m["metadata"]; found {
if md, ok := metadata.(map[string]interface{}); ok {
_m := make(map[string]interface{})
for k, v := range md {
if k == "name" || k == "namespace" || k == "labels" || k == "annotations" {
_m[k] = v
}
}
m["metadata"] = _m
}
}
return yaml.Marshal(m)
}