Feat: add revision command (#3506)

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
Somefive
2022-03-28 14:47:47 +08:00
committed by GitHub
parent 3f621e57b2
commit 795231ceb5
37 changed files with 959 additions and 83 deletions

View File

@@ -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{})

View File

@@ -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

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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(-)"),
))
})
})

View File

@@ -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")
}

View File

@@ -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) {

View 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

View File

@@ -0,0 +1,7 @@
apiVersion: core.oam.dev/v1alpha1
kind: Policy
metadata:
name: topology-local
type: topology
properties:
clusters: [ "local" ]

View 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"]

View 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
}

View File

@@ -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
View 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))
})
}
}

View File

@@ -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),

View File

@@ -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

View File

@@ -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
View 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
}

View File

@@ -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
}

View File

@@ -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

View 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

View File

@@ -0,0 +1,7 @@
apiVersion: core.oam.dev/v1alpha1
kind: Policy
metadata:
name: topology-worker
type: topology
properties:
clusters: [ "cluster-worker" ]

View 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" ]