mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
Feat: add revision command (#3506)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
@@ -38,6 +38,18 @@ var (
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
||||
// Policy meta
|
||||
var (
|
||||
PolicyKind = "Policy"
|
||||
PolicyGroupVersionKind = SchemeGroupVersion.WithKind(PolicyKind)
|
||||
)
|
||||
|
||||
// Workflow meta
|
||||
var (
|
||||
WorkflowKind = "Workflow"
|
||||
WorkflowGroupVersionKind = SchemeGroupVersion.WithKind(PolicyKind)
|
||||
)
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&Policy{}, &PolicyList{})
|
||||
SchemeBuilder.Register(&Workflow{}, &WorkflowList{})
|
||||
|
||||
@@ -728,9 +728,7 @@ spec:
|
||||
---
|
||||
`
|
||||
|
||||
var livediffResult = `---
|
||||
# Application (test-vela-app) has been modified(*)
|
||||
---
|
||||
var livediffResult = `Application (test-vela-app) has been modified(*)
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
@@ -759,15 +757,11 @@ var livediffResult = `---
|
||||
type: test-webservice
|
||||
status: {}
|
||||
|
||||
---
|
||||
## Component (express-server) has been removed(-)
|
||||
---
|
||||
* Component (express-server) has been removed(-)
|
||||
- apiVersion: apps/v1
|
||||
- kind: Deployment
|
||||
- metadata:
|
||||
- annotations: {}
|
||||
- labels:
|
||||
- app.oam.dev/appRevision: ""
|
||||
- app.oam.dev/component: express-server
|
||||
- app.oam.dev/name: test-vela-app
|
||||
- app.oam.dev/namespace: default
|
||||
@@ -790,15 +784,11 @@ var livediffResult = `---
|
||||
- ports:
|
||||
- - containerPort: 80
|
||||
|
||||
---
|
||||
### Component (express-server) / Trait (test-ingress/service) has been removed(-)
|
||||
---
|
||||
* Component (express-server) / Trait (test-ingress/service) has been removed(-)
|
||||
- apiVersion: v1
|
||||
- kind: Service
|
||||
- metadata:
|
||||
- annotations: {}
|
||||
- labels:
|
||||
- app.oam.dev/appRevision: ""
|
||||
- app.oam.dev/component: express-server
|
||||
- app.oam.dev/name: test-vela-app
|
||||
- app.oam.dev/namespace: default
|
||||
@@ -814,15 +804,11 @@ var livediffResult = `---
|
||||
- selector:
|
||||
- app.oam.dev/component: express-server
|
||||
|
||||
---
|
||||
### Component (express-server) / Trait (test-ingress/ingress) has been removed(-)
|
||||
---
|
||||
* Component (express-server) / Trait (test-ingress/ingress) has been removed(-)
|
||||
- apiVersion: networking.k8s.io/v1beta1
|
||||
- kind: Ingress
|
||||
- metadata:
|
||||
- annotations: {}
|
||||
- labels:
|
||||
- app.oam.dev/appRevision: ""
|
||||
- app.oam.dev/component: express-server
|
||||
- app.oam.dev/name: test-vela-app
|
||||
- app.oam.dev/namespace: default
|
||||
@@ -841,15 +827,11 @@ var livediffResult = `---
|
||||
- servicePort: 80
|
||||
- path: /
|
||||
|
||||
---
|
||||
## Component (new-express-server) has been added(+)
|
||||
---
|
||||
* Component (new-express-server) has been added(+)
|
||||
+ apiVersion: apps/v1
|
||||
+ kind: Deployment
|
||||
+ metadata:
|
||||
+ annotations: {}
|
||||
+ labels:
|
||||
+ app.oam.dev/appRevision: ""
|
||||
+ app.oam.dev/component: new-express-server
|
||||
+ app.oam.dev/name: test-vela-app
|
||||
+ app.oam.dev/namespace: default
|
||||
@@ -877,15 +859,11 @@ var livediffResult = `---
|
||||
+ requests:
|
||||
+ cpu: "0.5"
|
||||
|
||||
---
|
||||
### Component (new-express-server) / Trait (test-ingress/service) has been added(+)
|
||||
---
|
||||
* Component (new-express-server) / Trait (test-ingress/service) has been added(+)
|
||||
+ apiVersion: v1
|
||||
+ kind: Service
|
||||
+ metadata:
|
||||
+ annotations: {}
|
||||
+ labels:
|
||||
+ app.oam.dev/appRevision: ""
|
||||
+ app.oam.dev/component: new-express-server
|
||||
+ app.oam.dev/name: test-vela-app
|
||||
+ app.oam.dev/namespace: default
|
||||
@@ -901,15 +879,11 @@ var livediffResult = `---
|
||||
+ selector:
|
||||
+ app.oam.dev/component: new-express-server
|
||||
|
||||
---
|
||||
### Component (new-express-server) / Trait (test-ingress/ingress) has been added(+)
|
||||
---
|
||||
* Component (new-express-server) / Trait (test-ingress/ingress) has been added(+)
|
||||
+ apiVersion: networking.k8s.io/v1beta1
|
||||
+ kind: Ingress
|
||||
+ metadata:
|
||||
+ annotations: {}
|
||||
+ labels:
|
||||
+ app.oam.dev/appRevision: ""
|
||||
+ app.oam.dev/component: new-express-server
|
||||
+ app.oam.dev/name: test-vela-app
|
||||
+ app.oam.dev/namespace: default
|
||||
|
||||
@@ -49,12 +49,12 @@ import (
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode"
|
||||
"github.com/oam-dev/kubevela/pkg/apiserver/sync"
|
||||
"github.com/oam-dev/kubevela/pkg/appfile/dryrun"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
utils2 "github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/apply"
|
||||
common2 "github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
"github.com/oam-dev/kubevela/references/appfile/dryrun"
|
||||
)
|
||||
|
||||
// PolicyType build-in policy type
|
||||
|
||||
@@ -18,6 +18,7 @@ package dryrun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"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"
|
||||
@@ -50,6 +53,9 @@ const (
|
||||
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
|
||||
@@ -84,6 +90,10 @@ type manifest struct {
|
||||
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 {
|
||||
@@ -91,6 +101,125 @@ type LiveDiffOption struct {
|
||||
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.Name, Kind: WorkflowKind, Data: string(bs)})
|
||||
} else {
|
||||
m.Subs = append(m.Subs, &manifest{Name: af.Name, Kind: WorkflowKind, Data: "Error: " + errors.Wrapf(err, "failed to marshal external workflow %s", af.ExternalWorkflow.Name).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.
|
||||
func (l *LiveDiffOption) Diff(ctx context.Context, app *v1beta1.Application, appRevision *v1beta1.ApplicationRevision) (*DiffEntry, error) {
|
||||
@@ -373,29 +502,39 @@ func extractNameFromRevisionName(r string) string {
|
||||
return strings.Join(s[0:len(s)-1], "-")
|
||||
}
|
||||
|
||||
// removeRevisionRelatedLabelAndAnnotation will set label oam.LabelAppRevision to empty
|
||||
// because dry-run cannot set value to this label
|
||||
func removeRevisionRelatedLabelAndAnnotation(o client.Object) {
|
||||
func clearedLabels(labels map[string]string) map[string]string {
|
||||
newLabels := map[string]string{}
|
||||
labels := o.GetLabels()
|
||||
for k, v := range labels {
|
||||
if k == oam.LabelAppRevision {
|
||||
newLabels[k] = ""
|
||||
continue
|
||||
}
|
||||
newLabels[k] = v
|
||||
}
|
||||
o.SetLabels(newLabels)
|
||||
if len(newLabels) == 0 {
|
||||
return nil
|
||||
}
|
||||
return newLabels
|
||||
}
|
||||
|
||||
func clearedAnnotations(annotations map[string]string) map[string]string {
|
||||
newAnnotations := map[string]string{}
|
||||
annotations := o.GetAnnotations()
|
||||
for k, v := range annotations {
|
||||
if k == oam.AnnotationKubeVelaVersion || k == oam.AnnotationAppRevision || k == "kubectl.kubernetes.io/last-applied-configuration" {
|
||||
continue
|
||||
}
|
||||
newAnnotations[k] = v
|
||||
}
|
||||
o.SetAnnotations(newAnnotations)
|
||||
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
|
||||
@@ -408,3 +547,37 @@ func hasChanges(diffs []difflib.DiffRecord) bool {
|
||||
}
|
||||
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 *v1alpha1.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)
|
||||
}
|
||||
@@ -19,12 +19,18 @@ package dryrun
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/appfile"
|
||||
)
|
||||
|
||||
var _ = Describe("Test Live-Diff", func() {
|
||||
@@ -152,4 +158,60 @@ var _ = Describe("Test Live-Diff", func() {
|
||||
))
|
||||
})
|
||||
|
||||
It("Test renderless diff", func() {
|
||||
liveDiffOpt := LiveDiffOption{
|
||||
DryRun: NewDryRunOption(k8sClient, cfg, dm, pd, nil),
|
||||
Parser: appfile.NewApplicationParser(k8sClient, dm, pd),
|
||||
}
|
||||
applyFile := func(filename string, ns string) {
|
||||
bs, err := ioutil.ReadFile("./testdata/" + filename)
|
||||
Expect(err).Should(Succeed())
|
||||
un := &unstructured.Unstructured{}
|
||||
Expect(yaml.Unmarshal(bs, un)).Should(Succeed())
|
||||
un.SetNamespace(ns)
|
||||
Expect(k8sClient.Create(context.Background(), un)).Should(Succeed())
|
||||
}
|
||||
ctx := context.Background()
|
||||
Expect(k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(Succeed())
|
||||
applyFile("diff-input-app-with-externals.yaml", "default")
|
||||
applyFile("diff-apprevision.yaml", "default")
|
||||
app := &v1beta1.Application{}
|
||||
apprev := &v1beta1.ApplicationRevision{}
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "livediff-demo"}, app)).Should(Succeed())
|
||||
Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: "livediff-demo-v1"}, apprev)).Should(Succeed())
|
||||
reverse := false
|
||||
runDiff := func() string {
|
||||
a, b := LiveDiffObject{Application: app}, LiveDiffObject{ApplicationRevision: apprev}
|
||||
if reverse {
|
||||
a, b = b, a
|
||||
}
|
||||
de, err := liveDiffOpt.RenderlessDiff(ctx, a, b)
|
||||
Expect(err).Should(Succeed())
|
||||
buff := &bytes.Buffer{}
|
||||
reportOpt := NewReportDiffOption(-1, buff)
|
||||
reportOpt.PrintDiffReport(de)
|
||||
return buff.String()
|
||||
}
|
||||
Expect(runDiff()).Should(ContainSubstring("\"myworker\" not found"))
|
||||
applyFile("td-myingress.yaml", "vela-system")
|
||||
applyFile("td-myscaler.yaml", "vela-system")
|
||||
applyFile("cd-myworker.yaml", "vela-system")
|
||||
applyFile("wd-deploy.yaml", "vela-system")
|
||||
Expect(runDiff()).Should(ContainSubstring("\"deploy-livediff-demo\" not found"))
|
||||
applyFile("external-workflow.yaml", "default")
|
||||
Expect(runDiff()).Should(ContainSubstring("topology-local not found"))
|
||||
applyFile("external-policy.yaml", "default")
|
||||
Expect(runDiff()).Should(SatisfyAll(
|
||||
ContainSubstring("Application (livediff-demo) has been modified(*)"),
|
||||
ContainSubstring("External Policy (topology-local) has been added(+)"),
|
||||
ContainSubstring("External Workflow (livediff-demo) has been added(+)"),
|
||||
))
|
||||
reverse = true
|
||||
Expect(runDiff()).Should(SatisfyAll(
|
||||
ContainSubstring("Application (livediff-demo) has been modified(*)"),
|
||||
ContainSubstring("External Policy (topology-local) has been removed(-)"),
|
||||
ContainSubstring("External Workflow (livediff-demo) has been removed(-)"),
|
||||
))
|
||||
})
|
||||
|
||||
})
|
||||
@@ -125,7 +125,7 @@ func (d *Option) ExecuteDryRun(ctx context.Context, app *v1beta1.Application) ([
|
||||
if app.Namespace != "" {
|
||||
ctx = oamutil.SetNamespaceInCtx(ctx, app.Namespace)
|
||||
}
|
||||
appFile, err := parser.GenerateAppFile(ctx, app)
|
||||
appFile, err := parser.GenerateAppFileFromApp(ctx, app)
|
||||
if err != nil {
|
||||
return nil, errors.WithMessage(err, "cannot generate appFile from application")
|
||||
}
|
||||
@@ -29,6 +29,7 @@ var (
|
||||
red = color.New(color.FgRed)
|
||||
green = color.New(color.FgGreen)
|
||||
yellow = color.New(color.FgYellow)
|
||||
white = color.New(color.FgWhite)
|
||||
)
|
||||
|
||||
// NewReportDiffOption creats a new ReportDiffOption that can formats and prints
|
||||
@@ -54,25 +55,45 @@ type ReportDiffOption struct {
|
||||
}
|
||||
|
||||
// PrintDiffReport formats and prints diff data into target io.Writer
|
||||
// 'app' should be a diifEntry whose top-level is an application
|
||||
func (r *ReportDiffOption) PrintDiffReport(app *DiffEntry) {
|
||||
_, _ = yellow.Fprintf(r.To, "---\n# Application (%s) %s\n---\n", app.Name, r.DiffMsgs[app.DiffType])
|
||||
printDiffs(app.Diffs, r.Context, r.To)
|
||||
func (r *ReportDiffOption) PrintDiffReport(diff *DiffEntry) {
|
||||
r.printDiffReport(diff, "")
|
||||
}
|
||||
|
||||
for _, acc := range app.Subs {
|
||||
compName := acc.Name
|
||||
for _, accSub := range acc.Subs {
|
||||
switch accSub.Kind {
|
||||
case RawCompKind:
|
||||
_, _ = yellow.Fprintf(r.To, "---\n## Component (%s) %s\n---\n", compName, r.DiffMsgs[accSub.DiffType])
|
||||
case TraitKind:
|
||||
_, _ = yellow.Fprintf(r.To, "---\n### Component (%s) / Trait (%s) %s\n---\n", compName, accSub.Name, r.DiffMsgs[accSub.DiffType])
|
||||
default:
|
||||
continue
|
||||
}
|
||||
printDiffs(accSub.Diffs, r.Context, r.To)
|
||||
func (r *ReportDiffOption) printDiffReport(diff *DiffEntry, prefix string) {
|
||||
var header string
|
||||
switch diff.Kind {
|
||||
case AppKind:
|
||||
header = "Application"
|
||||
case AppConfigCompKind:
|
||||
case RawCompKind:
|
||||
header = "Component"
|
||||
case TraitKind:
|
||||
header = "Trait"
|
||||
case PolicyKind:
|
||||
header = "External Policy"
|
||||
case WorkflowKind:
|
||||
header = "External Workflow"
|
||||
case ReferredObject:
|
||||
header = "Referred Object"
|
||||
default:
|
||||
return
|
||||
}
|
||||
if diff.Kind != AppConfigCompKind {
|
||||
editMsg := r.DiffMsgs[diff.DiffType]
|
||||
if diff.DiffType != NoDiff {
|
||||
_, _ = yellow.Fprintf(r.To, "* %s%s (%s) %s\n", prefix, header, diff.Name, editMsg)
|
||||
printDiffs(diff.Diffs, r.Context, r.To)
|
||||
} else {
|
||||
_, _ = white.Fprintf(r.To, "* %s%s (%s) %s\n", prefix, header, diff.Name, editMsg)
|
||||
}
|
||||
}
|
||||
for _, sub := range diff.Subs {
|
||||
var subPrefix string
|
||||
if sub.Kind == TraitKind && diff.Kind == AppConfigCompKind {
|
||||
subPrefix = fmt.Sprintf("Component (%s) / ", diff.Name)
|
||||
}
|
||||
r.printDiffReport(sub, subPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
func printDiffs(diffs []difflib.DiffRecord, context int, to io.Writer) {
|
||||
36
pkg/appfile/dryrun/testdata/diff-input-app-with-externals.yaml
vendored
Normal file
36
pkg/appfile/dryrun/testdata/diff-input-app-with-externals.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: livediff-demo
|
||||
namespace: default
|
||||
spec:
|
||||
components:
|
||||
- name: myweb-1
|
||||
type: myworker
|
||||
properties:
|
||||
image: "busybox"
|
||||
cmd:
|
||||
- sleep
|
||||
- "1000"
|
||||
lives: "3"
|
||||
enemies: "alien"
|
||||
traits:
|
||||
- type: myingress
|
||||
properties:
|
||||
domain: "www.example.com"
|
||||
http:
|
||||
"/": 80
|
||||
- type: myscaler
|
||||
properties:
|
||||
replicas: 2
|
||||
- name: myweb-3
|
||||
type: myworker
|
||||
properties:
|
||||
image: "busybox"
|
||||
cmd:
|
||||
- sleep
|
||||
- "1000"
|
||||
lives: "3"
|
||||
enemies: "alien"
|
||||
workflow:
|
||||
ref: deploy-livediff-demo
|
||||
7
pkg/appfile/dryrun/testdata/external-policy.yaml
vendored
Normal file
7
pkg/appfile/dryrun/testdata/external-policy.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: core.oam.dev/v1alpha1
|
||||
kind: Policy
|
||||
metadata:
|
||||
name: topology-local
|
||||
type: topology
|
||||
properties:
|
||||
clusters: [ "local" ]
|
||||
9
pkg/appfile/dryrun/testdata/external-workflow.yaml
vendored
Normal file
9
pkg/appfile/dryrun/testdata/external-workflow.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: core.oam.dev/v1alpha1
|
||||
kind: Workflow
|
||||
metadata:
|
||||
name: deploy-livediff-demo
|
||||
steps:
|
||||
- type: deploy
|
||||
name: deploy-local
|
||||
properties:
|
||||
policies: ["topology-local"]
|
||||
56
pkg/appfile/dryrun/testdata/wd-deploy.yaml
vendored
Normal file
56
pkg/appfile/dryrun/testdata/wd-deploy.yaml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: WorkflowStepDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Deploy components with policies.
|
||||
name: deploy
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import (
|
||||
"vela/op"
|
||||
)
|
||||
|
||||
deploy: op.#Steps & {
|
||||
load: op.#Load @step(1)
|
||||
_components: [ for k, v in load.value {v}]
|
||||
loadPoliciesInOrder: op.#LoadPoliciesInOrder & {
|
||||
if parameter.policies != _|_ {
|
||||
input: parameter.policies
|
||||
}
|
||||
} @step(2)
|
||||
_policies: loadPoliciesInOrder.output
|
||||
handleDeployPolicies: op.#HandleDeployPolicies & {
|
||||
inputs: {
|
||||
components: _components
|
||||
policies: _policies
|
||||
}
|
||||
} @step(3)
|
||||
_decisions: handleDeployPolicies.outputs.decisions
|
||||
_patchedComponents: handleDeployPolicies.outputs.components
|
||||
deploy: op.#ApplyComponents & {
|
||||
parallelism: parameter.parallelism
|
||||
components: {
|
||||
for decision in _decisions {
|
||||
for key, comp in _patchedComponents {
|
||||
"\(decision.cluster)-\(decision.namespace)-\(key)": {
|
||||
value: comp
|
||||
if decision.cluster != _|_ {
|
||||
cluster: decision.cluster
|
||||
}
|
||||
if decision.namespace != _|_ {
|
||||
namespace: decision.namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} @step(4)
|
||||
}
|
||||
parameter: {
|
||||
auto: *true | bool
|
||||
policies?: [...string]
|
||||
parallelism: *5 | int
|
||||
}
|
||||
|
||||
@@ -182,3 +182,19 @@ func Parse(addr string) (string, *Content, error) {
|
||||
|
||||
return TypeUnknown, nil, nil
|
||||
}
|
||||
|
||||
// ByteCountIEC convert number of bytes into readable string
|
||||
// borrowed from https://yourbasic.org/golang/formatting-byte-size-to-human-readable-format/
|
||||
func ByteCountIEC(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB",
|
||||
float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
53
pkg/utils/parse_test.go
Normal file
53
pkg/utils/parse_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
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 utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestByteCountIEC(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Input int64
|
||||
Output string
|
||||
}{
|
||||
"1 B": {
|
||||
Input: int64(1),
|
||||
Output: "1 B",
|
||||
},
|
||||
"1.1 KiB": {
|
||||
Input: int64(1124),
|
||||
Output: "1.1 KiB",
|
||||
},
|
||||
"1.2 MiB": {
|
||||
Input: int64(1258291),
|
||||
Output: "1.2 MiB",
|
||||
},
|
||||
"3.3 GiB": {
|
||||
Input: int64(3543348020),
|
||||
Output: "3.3 GiB",
|
||||
},
|
||||
}
|
||||
r := require.New(t)
|
||||
for name, tt := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r.Equal(tt.Output, ByteCountIEC(tt.Input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ func NewCommand() *cobra.Command {
|
||||
NewLogsCommand(commandArgs, "4", ioStream),
|
||||
NewLiveDiffCommand(commandArgs, "3", ioStream),
|
||||
NewDryRunCommand(commandArgs, ioStream),
|
||||
RevisionCommandGroup(commandArgs),
|
||||
|
||||
// Workflows
|
||||
NewWorkflowCommand(commandArgs, ioStream),
|
||||
|
||||
@@ -31,12 +31,12 @@ import (
|
||||
|
||||
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/appfile/dryrun"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/appfile/dryrun"
|
||||
)
|
||||
|
||||
// DryRunCmdOptions contains dry-run cmd options
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -27,18 +28,21 @@ import (
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/appfile/dryrun"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/appfile/dryrun"
|
||||
)
|
||||
|
||||
// LiveDiffCmdOptions contains the live-diff cmd options
|
||||
type LiveDiffCmdOptions struct {
|
||||
DryRunCmdOptions
|
||||
Revision string
|
||||
Context int
|
||||
AppName string
|
||||
Namespace string
|
||||
Revision string
|
||||
SecondaryRevision string
|
||||
Context int
|
||||
}
|
||||
|
||||
// NewLiveDiffCommand creates `live-diff` command
|
||||
@@ -50,38 +54,48 @@ func NewLiveDiffCommand(c common.Args, order string, ioStreams cmdutil.IOStreams
|
||||
cmd := &cobra.Command{
|
||||
Use: "live-diff",
|
||||
DisableFlagsInUseLine: true,
|
||||
Short: "Dry-run application locally, and diff with a deployed application version",
|
||||
Long: "Dry-run application locally, and diff with a deployed application version.",
|
||||
Example: "vela live-diff -f app-v2.yaml -r app-v1 --context 10",
|
||||
Short: "Compare application and revisions",
|
||||
Long: "Compare application and revisions",
|
||||
Example: "# compare the current application and the running revision\n" +
|
||||
"> vela live-diff my-app\n" +
|
||||
"# compare the current application and the specified revision\n" +
|
||||
"> vela live-diff my-app --revision my-app-v1\n" +
|
||||
"# compare two application revisions\n" +
|
||||
"> vela live-diff --revision my-app-v1,my-app-v2\n" +
|
||||
"# compare the application file and the specified revision\n" +
|
||||
"> vela live-diff -f my-app.yaml -r my-app-v1 --context 10",
|
||||
Annotations: map[string]string{
|
||||
types.TagCommandOrder: order,
|
||||
types.TagCommandType: types.TypeApp,
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
o.Namespace, err = GetFlagNamespaceOrEnv(cmd, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buff, err := LiveDiffApplication(o, c, namespace)
|
||||
if err = o.loadAndValidate(args); err != nil {
|
||||
return err
|
||||
}
|
||||
buff, err := LiveDiffApplication(o, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
o.Info(buff.String())
|
||||
cmd.Println(buff.String())
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&o.ApplicationFile, "file", "f", "./app.yaml", "application file name")
|
||||
cmd.Flags().StringVarP(&o.ApplicationFile, "file", "f", "", "application file name")
|
||||
cmd.Flags().StringVarP(&o.DefinitionFile, "definition", "d", "", "specify a file or directory containing capability definitions, they will only be used in dry-run rather than applied to K8s cluster")
|
||||
cmd.Flags().StringVarP(&o.Revision, "Revision", "r", "", "specify an application Revision name, by default, it will compare with the latest Revision")
|
||||
cmd.Flags().StringVarP(&o.Revision, "revision", "r", "", "specify one or two application revision name(s), by default, it will compare with the latest revision")
|
||||
cmd.Flags().IntVarP(&o.Context, "context", "c", -1, "output number lines of context around changes, by default show all unchanged lines")
|
||||
addNamespaceAndEnvArg(cmd)
|
||||
cmd.SetOut(ioStreams.Out)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// LiveDiffApplication can return user what would change if upgrade an application.
|
||||
func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args, namespace string) (bytes.Buffer, error) {
|
||||
func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args) (bytes.Buffer, error) {
|
||||
var buff = bytes.Buffer{}
|
||||
|
||||
newClient, err := c.GetClient()
|
||||
@@ -107,13 +121,17 @@ func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args, namespace
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(newClient, config, dm, pd, objs)
|
||||
if cmdOption.ApplicationFile == "" {
|
||||
return cmdOption.renderlessDiff(newClient, liveDiffOption)
|
||||
}
|
||||
|
||||
app, err := readApplicationFromFile(cmdOption.ApplicationFile)
|
||||
if err != nil {
|
||||
return buff, errors.WithMessagef(err, "read application file: %s", cmdOption.ApplicationFile)
|
||||
}
|
||||
if app.Namespace == "" {
|
||||
app.SetNamespace(namespace)
|
||||
app.SetNamespace(cmdOption.Namespace)
|
||||
}
|
||||
|
||||
appRevision := &v1beta1.ApplicationRevision{}
|
||||
@@ -143,7 +161,6 @@ func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args, namespace
|
||||
}
|
||||
}
|
||||
|
||||
liveDiffOption := dryrun.NewLiveDiffOption(newClient, config, dm, pd, objs)
|
||||
diffResult, err := liveDiffOption.Diff(context.Background(), app, appRevision)
|
||||
if err != nil {
|
||||
return buff, errors.WithMessage(err, "cannot calculate diff")
|
||||
@@ -154,3 +171,69 @@ func LiveDiffApplication(cmdOption *LiveDiffCmdOptions, c common.Args, namespace
|
||||
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (o *LiveDiffCmdOptions) loadAndValidate(args []string) error {
|
||||
if len(args) > 0 {
|
||||
o.AppName = args[0]
|
||||
}
|
||||
revisions := strings.Split(o.Revision, ",")
|
||||
if len(revisions) > 2 {
|
||||
return errors.Errorf("cannot use more than 2 revisions")
|
||||
}
|
||||
o.Revision = revisions[0]
|
||||
if len(revisions) == 2 {
|
||||
o.SecondaryRevision = revisions[1]
|
||||
}
|
||||
if (o.AppName == "" && len(revisions) == 1) && o.ApplicationFile == "" {
|
||||
return errors.Errorf("either application name or application file must be set")
|
||||
}
|
||||
if (o.AppName != "" || len(revisions) > 1) && o.ApplicationFile != "" {
|
||||
return errors.Errorf("cannot set application name and application file at the same time")
|
||||
}
|
||||
if o.AppName != "" && o.SecondaryRevision != "" {
|
||||
return errors.Errorf("cannot use application name and two revisions at the same time")
|
||||
}
|
||||
if o.SecondaryRevision != "" && o.ApplicationFile != "" {
|
||||
return errors.Errorf("cannot use application file and two revisions at the same time")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *LiveDiffCmdOptions) renderlessDiff(cli client.Client, option *dryrun.LiveDiffOption) (bytes.Buffer, error) {
|
||||
var base, comparor dryrun.LiveDiffObject
|
||||
ctx := context.Background()
|
||||
var buf bytes.Buffer
|
||||
if o.AppName != "" {
|
||||
app := &v1beta1.Application{}
|
||||
if err := cli.Get(ctx, client.ObjectKey{Name: o.AppName, Namespace: o.Namespace}, app); err != nil {
|
||||
return buf, errors.Wrapf(err, "cannot get application %s/%s", o.Namespace, o.AppName)
|
||||
}
|
||||
base = dryrun.LiveDiffObject{Application: app}
|
||||
if o.Revision == "" {
|
||||
if app.Status.LatestRevision == nil {
|
||||
return buf, errors.Errorf("no latest application revision available for application %s/%s", o.Namespace, o.AppName)
|
||||
}
|
||||
o.Revision = app.Status.LatestRevision.Name
|
||||
}
|
||||
}
|
||||
rev, secondaryRev := &v1beta1.ApplicationRevision{}, &v1beta1.ApplicationRevision{}
|
||||
if err := cli.Get(ctx, client.ObjectKey{Name: o.Revision, Namespace: o.Namespace}, rev); err != nil {
|
||||
return buf, errors.Wrapf(err, "cannot get application revision %s/%s", o.Namespace, o.Revision)
|
||||
}
|
||||
if o.SecondaryRevision == "" {
|
||||
comparor = dryrun.LiveDiffObject{ApplicationRevision: rev}
|
||||
} else {
|
||||
if err := cli.Get(ctx, client.ObjectKey{Name: o.SecondaryRevision, Namespace: o.Namespace}, secondaryRev); err != nil {
|
||||
return buf, errors.Wrapf(err, "cannot get application revision %s/%s", o.Namespace, o.SecondaryRevision)
|
||||
}
|
||||
base = dryrun.LiveDiffObject{ApplicationRevision: rev}
|
||||
comparor = dryrun.LiveDiffObject{ApplicationRevision: secondaryRev}
|
||||
}
|
||||
diffResult, err := option.RenderlessDiff(ctx, base, comparor)
|
||||
if err != nil {
|
||||
return buf, errors.WithMessage(err, "cannot calculate diff")
|
||||
}
|
||||
reportDiffOpt := dryrun.NewReportDiffOption(o.Context, &buf)
|
||||
reportDiffOpt.PrintDiffReport(diffResult)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
109
references/cli/revision.go
Normal file
109
references/cli/revision.go
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
apitypes "k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
)
|
||||
|
||||
// RevisionCommandGroup the commands for managing application revisions
|
||||
func RevisionCommandGroup(c common.Args) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "revision",
|
||||
Short: "Manage Application Revisions",
|
||||
Long: "Manage KubeVela Application Revisions",
|
||||
Annotations: map[string]string{
|
||||
types.TagCommandType: types.TypeApp,
|
||||
},
|
||||
}
|
||||
cmd.AddCommand(
|
||||
NewRevisionListCommand(c),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// NewRevisionListCommand list the revisions for application
|
||||
func NewRevisionListCommand(c common.Args) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Short: "list application revisions",
|
||||
Long: "list Kubevela application revisions",
|
||||
Args: cobra.ExactValidArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli, err := c.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := args[0]
|
||||
app := &v1beta1.Application{}
|
||||
ctx := context.Background()
|
||||
if err = cli.Get(ctx, apitypes.NamespacedName{Namespace: namespace, Name: name}, app); err != nil {
|
||||
return errors.Wrapf(err, "failed to get application %s/%s", namespace, name)
|
||||
}
|
||||
revs, err := application.GetSortedAppRevisions(ctx, cli, name, namespace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
table := newUITable().AddRow("NAME", "PUBLISH_VERSION", "SUCCEEDED", "HASH", "BEGIN_TIME", "STATUS", "SIZE")
|
||||
for _, rev := range revs {
|
||||
var begin, status, hash, size string
|
||||
status = "NotStart"
|
||||
if rev.Status.Workflow != nil {
|
||||
begin = rev.Status.Workflow.StartTime.Format("2006-01-02 15:04:05")
|
||||
// aggregate workflow result
|
||||
switch {
|
||||
case rev.Status.Succeeded:
|
||||
status = "Succeeded"
|
||||
case rev.Status.Workflow.Terminated || rev.Status.Workflow.Suspend || rev.Status.Workflow.Finished:
|
||||
status = "Failed"
|
||||
default:
|
||||
status = "Executing"
|
||||
}
|
||||
}
|
||||
if labels := rev.GetLabels(); labels != nil {
|
||||
hash = rev.GetLabels()[oam.LabelAppRevisionHash]
|
||||
}
|
||||
if bs, err := yaml.Marshal(rev.Spec); err == nil {
|
||||
size = utils.ByteCountIEC(int64(len(bs)))
|
||||
}
|
||||
table.AddRow(rev.Name, oam.GetPublishVersion(rev.DeepCopy()), rev.Status.Succeeded, hash, begin, status, size)
|
||||
}
|
||||
if len(table.Rows) == 0 {
|
||||
cmd.Printf("No revisions found for application %s/%s.\n", namespace, name)
|
||||
} else {
|
||||
cmd.Println(table.String())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
addNamespaceAndEnvArg(cmd)
|
||||
return cmd
|
||||
}
|
||||
@@ -20,13 +20,22 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
k8stypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
oamcommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/apis/types"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
"github.com/oam-dev/kubevela/pkg/resourcetracker"
|
||||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||||
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
|
||||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||||
"github.com/oam-dev/kubevela/references/appfile"
|
||||
)
|
||||
@@ -238,7 +247,7 @@ func NewWorkflowRollbackCommand(c common.Args, ioStream cmdutil.IOStreams) *cobr
|
||||
return err
|
||||
}
|
||||
|
||||
err = rollbackWorkflow(client, app)
|
||||
err = rollbackWorkflow(cmd, client, app)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -250,13 +259,18 @@ func NewWorkflowRollbackCommand(c common.Args, ioStream cmdutil.IOStreams) *cobr
|
||||
}
|
||||
|
||||
func suspendWorkflow(kubecli client.Client, app *v1beta1.Application) error {
|
||||
// set the workflow suspend to true
|
||||
app.Status.Workflow.Suspend = true
|
||||
|
||||
if err := kubecli.Status().Patch(context.TODO(), app, client.Merge); err != nil {
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
ctx := context.Background()
|
||||
if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err := kubecli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
// set the workflow suspend to true
|
||||
app.Status.Workflow.Suspend = true
|
||||
return kubecli.Status().Patch(ctx, app, client.Merge)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Successfully suspend workflow: %s\n", app.Name)
|
||||
return nil
|
||||
}
|
||||
@@ -297,7 +311,10 @@ func restartWorkflow(kubecli client.Client, app *v1beta1.Application) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func rollbackWorkflow(kubecli client.Client, app *v1beta1.Application) error {
|
||||
func rollbackWorkflow(cmd *cobra.Command, kubecli client.Client, app *v1beta1.Application) error {
|
||||
if oam.GetPublishVersion(app) != "" {
|
||||
return rollbackApplicationWithPublishVersion(cmd, kubecli, app)
|
||||
}
|
||||
if app.Status.LatestRevision == nil || app.Status.LatestRevision.Name == "" {
|
||||
return fmt.Errorf("the latest revision is not set: %s", app.Name)
|
||||
}
|
||||
@@ -315,3 +332,136 @@ func rollbackWorkflow(kubecli client.Client, app *v1beta1.Application) error {
|
||||
fmt.Printf("Successfully rollback workflow to the latest revision: %s\n", app.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func rollbackApplicationWithPublishVersion(cmd *cobra.Command, cli client.Client, app *v1beta1.Application) error {
|
||||
ctx := context.Background()
|
||||
appRevs, err := application.GetSortedAppRevisions(ctx, cli, app.Name, app.Namespace)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to list revisions for application %s/%s", app.Namespace, app.Name)
|
||||
}
|
||||
|
||||
// find succeeded revision to rollback
|
||||
var rev *v1beta1.ApplicationRevision
|
||||
var outdatedRev []*v1beta1.ApplicationRevision
|
||||
for i := range appRevs {
|
||||
candidate := appRevs[len(appRevs)-i-1]
|
||||
_rev := candidate.DeepCopy()
|
||||
if !candidate.Status.Succeeded || oam.GetPublishVersion(_rev) == "" {
|
||||
outdatedRev = append(outdatedRev, _rev)
|
||||
continue
|
||||
}
|
||||
rev = _rev
|
||||
break
|
||||
}
|
||||
if rev == nil {
|
||||
return errors.Errorf("failed to find previous succeeded revision for application %s/%s", app.Namespace, app.Name)
|
||||
}
|
||||
publishVersion := oam.GetPublishVersion(rev)
|
||||
revisionNumber, err := utils.ExtractRevision(rev.Name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to extract revision number from revision %s", rev.Name)
|
||||
}
|
||||
_, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, cli, app)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to list resource trackers for application %s/%s", app.Namespace, app.Name)
|
||||
}
|
||||
var matchRT *v1beta1.ResourceTracker
|
||||
for _, rt := range append(historyRTs, currentRT) {
|
||||
if rt == nil {
|
||||
continue
|
||||
}
|
||||
labels := rt.GetLabels()
|
||||
if labels != nil && labels[oam.LabelAppRevision] == rev.Name {
|
||||
matchRT = rt.DeepCopy()
|
||||
}
|
||||
}
|
||||
if matchRT == nil {
|
||||
return errors.Errorf("cannot find resource tracker for previous revision %s, unable to rollback", rev.Name)
|
||||
}
|
||||
if matchRT.DeletionTimestamp != nil {
|
||||
return errors.Errorf("previous revision %s is being recycled, unable to rollback", rev.Name)
|
||||
}
|
||||
cmd.Printf("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n", rev.Name, publishVersion)
|
||||
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
var controllerRequirement string
|
||||
// rollback application spec and freeze
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationPublishVersion, publishVersion)
|
||||
controllerRequirement = app.GetAnnotations()[oam.AnnotationControllerRequirement]
|
||||
v1.SetMetaDataAnnotation(&app.ObjectMeta, oam.AnnotationControllerRequirement, "Not Available")
|
||||
app.Spec = rev.Spec.Application.Spec
|
||||
return cli.Update(ctx, app)
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
|
||||
}
|
||||
cmd.Printf("Application spec rollback successfully.\n")
|
||||
|
||||
// rollback application status
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
app.Status.Workflow = rev.Status.Workflow
|
||||
app.Status.Services = []oamcommon.ApplicationComponentStatus{}
|
||||
app.Status.AppliedResources = []oamcommon.ClusterObjectReference{}
|
||||
for _, rsc := range matchRT.Spec.ManagedResources {
|
||||
app.Status.AppliedResources = append(app.Status.AppliedResources, rsc.ClusterObjectReference)
|
||||
}
|
||||
app.Status.LatestRevision = &oamcommon.Revision{
|
||||
Name: rev.Name,
|
||||
Revision: int64(revisionNumber),
|
||||
RevisionHash: rev.GetLabels()[oam.LabelAppRevisionHash],
|
||||
}
|
||||
return cli.Status().Update(ctx, app)
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "failed to rollback application status to revision %s (PublishVersion: %s)", rev.Name, publishVersion)
|
||||
}
|
||||
cmd.Printf("Application status rollback successfully.\n")
|
||||
|
||||
// update resource tracker generation
|
||||
matchRTKey := client.ObjectKeyFromObject(matchRT)
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, matchRTKey, matchRT); err != nil {
|
||||
return err
|
||||
}
|
||||
matchRT.Spec.ApplicationGeneration = app.Generation
|
||||
return cli.Update(ctx, matchRT)
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "failed to update application generation in resource tracker")
|
||||
}
|
||||
|
||||
// unfreeze application
|
||||
if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||
if err = cli.Get(ctx, appKey, app); err != nil {
|
||||
return err
|
||||
}
|
||||
annotations := app.GetAnnotations()
|
||||
if controllerRequirement != "" {
|
||||
annotations[oam.AnnotationControllerRequirement] = controllerRequirement
|
||||
} else {
|
||||
delete(annotations, oam.AnnotationControllerRequirement)
|
||||
}
|
||||
app.SetAnnotations(annotations)
|
||||
return cli.Update(ctx, app)
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "failed to resume application to restart")
|
||||
}
|
||||
cmd.Printf("Application rollback completed.\n")
|
||||
|
||||
// clean up outdated revisions
|
||||
var errs velaerrors.ErrorList
|
||||
for _, _rev := range outdatedRev {
|
||||
if err = cli.Delete(ctx, _rev); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if errs.HasError() {
|
||||
return errors.Wrapf(errs, "failed to clean up outdated revisions")
|
||||
}
|
||||
cmd.Printf("Application outdated revision cleaned up.\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
oamcomm "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/pkg/controller/core.oam.dev/v1alpha2/application"
|
||||
"github.com/oam-dev/kubevela/pkg/oam"
|
||||
)
|
||||
|
||||
@@ -180,6 +182,87 @@ var _ = Describe("Test multicluster standalone scenario", func() {
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Test rollback application with publish version", func() {
|
||||
By("Apply application successfully")
|
||||
applyFile("topology-policy.yaml")
|
||||
applyFile("workflow-deploy-worker.yaml")
|
||||
applyFile("app-with-publish-version-native.yaml")
|
||||
app := &v1beta1.Application{}
|
||||
appKey := types.NamespacedName{Namespace: namespace, Name: "busybox"}
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
|
||||
g.Expect(app.Status.Phase).Should(Equal(oamcomm.ApplicationRunning))
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
|
||||
By("Update Application to first failed version")
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
|
||||
app.Annotations[oam.AnnotationPublishVersion] = "alpha2"
|
||||
app.Spec.Components[0].Properties = &runtime.RawExtension{Raw: []byte(`{"image":"busybox:bad"}`)}
|
||||
g.Expect(k8sClient.Update(hubCtx, app)).Should(Succeed())
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
|
||||
g.Expect(app.Status.Phase).Should(Equal(oamcomm.ApplicationRunningWorkflow))
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
|
||||
By("Update Application to second failed version")
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
|
||||
app.Annotations[oam.AnnotationPublishVersion] = "alpha3"
|
||||
app.Spec.Components[0].Name = "busybox-bad"
|
||||
g.Expect(k8sClient.Update(hubCtx, app)).Should(Succeed())
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
Eventually(func(g Gomega) {
|
||||
deploy := &v1.Deployment{}
|
||||
g.Expect(k8sClient.Get(workerCtx, types.NamespacedName{Namespace: namespace, Name: "busybox"}, deploy)).Should(Succeed())
|
||||
g.Expect(k8sClient.Delete(workerCtx, deploy)).Should(Succeed())
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
|
||||
By("Change external policy")
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Namespace: namespace, Name: "busybox-v3"}, &v1beta1.ApplicationRevision{})).Should(Succeed())
|
||||
policy := &v1alpha1.Policy{}
|
||||
g.Expect(k8sClient.Get(hubCtx, types.NamespacedName{Namespace: namespace, Name: "topology-worker"}, policy)).Should(Succeed())
|
||||
policy.Properties = &runtime.RawExtension{Raw: []byte(`{"clusters":["changed"]}`)}
|
||||
g.Expect(k8sClient.Update(hubCtx, policy)).Should(Succeed())
|
||||
}, 30*time.Second).Should(Succeed())
|
||||
|
||||
By("Live-diff application")
|
||||
outputs, err := execCommand("live-diff", "-r", "busybox-v3,busybox-v1", "-n", namespace)
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(outputs).Should(SatisfyAll(
|
||||
ContainSubstring("Application (busybox) has been modified(*)"),
|
||||
ContainSubstring("External Policy (topology-worker) has no change"),
|
||||
ContainSubstring("External Workflow (busybox) has no change"),
|
||||
))
|
||||
outputs, err = execCommand("live-diff", "busybox", "-n", namespace)
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(outputs).Should(SatisfyAll(
|
||||
ContainSubstring("Application (busybox) has no change"),
|
||||
ContainSubstring("External Policy (topology-worker) has been modified(*)"),
|
||||
ContainSubstring("External Workflow (busybox) has no change"),
|
||||
))
|
||||
|
||||
By("Rollback application")
|
||||
_, err = execCommand("workflow", "suspend", "busybox", "-n", namespace)
|
||||
Expect(err).Should(Succeed())
|
||||
_, err = execCommand("workflow", "rollback", "busybox", "-n", namespace)
|
||||
Expect(err).Should(Succeed())
|
||||
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())
|
||||
g.Expect(app.Status.Phase).Should(Equal(oamcomm.ApplicationRunning))
|
||||
deploy := &v1.Deployment{}
|
||||
g.Expect(k8sClient.Get(workerCtx, types.NamespacedName{Namespace: namespace, Name: "busybox"}, deploy)).Should(Succeed())
|
||||
g.Expect(deploy.Spec.Template.Spec.Containers[0].Image).Should(Equal("busybox"))
|
||||
revs, err := application.GetSortedAppRevisions(hubCtx, k8sClient, app.Name, namespace)
|
||||
g.Expect(err).Should(Succeed())
|
||||
g.Expect(len(revs)).Should(Equal(1))
|
||||
})
|
||||
})
|
||||
|
||||
It("Test large application parallel apply and delete", func() {
|
||||
newApp := &v1beta1.Application{ObjectMeta: metav1.ObjectMeta{Namespace: namespace, Name: "large-app"}}
|
||||
size := 30
|
||||
|
||||
15
test/e2e-multicluster-test/testdata/app/standalone/app-with-publish-version-native.yaml
vendored
Normal file
15
test/e2e-multicluster-test/testdata/app/standalone/app-with-publish-version-native.yaml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: busybox
|
||||
annotations:
|
||||
app.oam.dev/publishVersion: alpha1
|
||||
spec:
|
||||
components:
|
||||
- name: busybox
|
||||
type: webservice
|
||||
properties:
|
||||
image: busybox
|
||||
cmd: [ "sleep", "86400" ]
|
||||
workflow:
|
||||
ref: deploy-worker
|
||||
7
test/e2e-multicluster-test/testdata/app/standalone/topology-policy.yaml
vendored
Normal file
7
test/e2e-multicluster-test/testdata/app/standalone/topology-policy.yaml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: core.oam.dev/v1alpha1
|
||||
kind: Policy
|
||||
metadata:
|
||||
name: topology-worker
|
||||
type: topology
|
||||
properties:
|
||||
clusters: [ "cluster-worker" ]
|
||||
9
test/e2e-multicluster-test/testdata/app/standalone/workflow-deploy-worker.yaml
vendored
Normal file
9
test/e2e-multicluster-test/testdata/app/standalone/workflow-deploy-worker.yaml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
apiVersion: core.oam.dev/v1alpha1
|
||||
kind: Workflow
|
||||
metadata:
|
||||
name: deploy-worker
|
||||
steps:
|
||||
- type: deploy
|
||||
name: deploy-worker
|
||||
properties:
|
||||
policies: [ "topology-worker" ]
|
||||
Reference in New Issue
Block a user