implement cli vela system live-diff (#1419)

move dry-run and live-diff into an independent pkg

WIP add sample and doc

WIP unit test for live-diff

add unit test

Signed-off-by: roy wang <seiwy2010@gmail.com>
This commit is contained in:
Yue Wang
2021-04-13 13:29:25 +09:00
committed by GitHub
parent 2e43a6dc78
commit 63b348f4d8
33 changed files with 2830 additions and 142 deletions

View File

@@ -664,3 +664,485 @@ spec:
---
```
`-d` or `--definitions` is a useful flag permitting user to provide capability
definitions used in the application from local files.
`dry-run` cmd will prioritize the provided capabilities than the living
ones in the cluster.
If the capability is not found in local files and cluster, it will raise an error.
## Live-Diff the `Application`
`vela system live-diff` allows users to have a preview of what would change if
upgrade an application.
It basically generates a diff between the specific revision of an application
and the result of `vela system dry-run`.
The result shows the changes (added/modified/removed/no_change) of the application as well as its sub-resources, such as components and traits.
`live-diff` will not make any changes to the living cluster, so it's very
helpful if you want to update an application but worry about the unknown results
that may be produced.
Let's prepare an application and deploy it.
> ComponentDefinitions and TraitDefinitions used in this sample are stored in
`./doc/examples/live-diff/definitions`.
```yaml
# app.yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: livediff-demo
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-2
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
```
```shell
kubectl apply ./doc/examples/live-diff/definitions
kubectl apply ./doc/examples/live-diff/app.yaml
```
Then, assume we want to update the application with below configuration.
To preview changes brought by updating while not really apply updated
configuration into the cluster, we can use `live-diff` here.
```yaml
# app-updated.yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: livediff-demo
spec:
components:
- name: myweb-1
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "2000" # change a component property
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90 # change a trait
# - type: myscaler # remove a trait
# properties:
# replicas: 2
- name: myweb-2
type: myworker
properties: # no change on component property
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress # add a trait
properties:
domain: "www.example.com"
http:
"/": 90
- name: myweb-3 # add a component
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90
```
```shell
vela system live-diff -f ./doc/examples/live-diff/app-modified.yaml -r livediff-demo-v1
```
`-r` or `--revision` is a flag that specifies the name of a living
`ApplicationRevision` with which you want to compare the updated application.
`-c` or `--context` is a flag that specifies the number of lines shown around a
change.
The unchanged lines which are out of the context of a change will be omitted.
It's useful if the diff result contains a lot of unchanged content while
you just want to focus on the changed ones.
<details><summary> Click to view diff result </summary>
```shell
---
# Application (application-sample) has been modified(*)
---
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
creationTimestamp: null
- name: application-sample
+ name: livediff-demo
namespace: default
spec:
components:
- name: myweb-1
+ properties:
+ cmd:
+ - sleep
+ - "2000"
+ enemies: alien
+ image: busybox
+ lives: "3"
+ traits:
+ - properties:
+ domain: www.example.com
+ http:
+ /: 90
+ type: myingress
+ type: myworker
+ - name: myweb-2
properties:
cmd:
- sleep
- "1000"
enemies: alien
image: busybox
lives: "3"
traits:
- properties:
domain: www.example.com
http:
- /: 80
+ /: 90
type: myingress
- - properties:
- replicas: 2
- type: myscaler
type: myworker
- - name: myweb-2
+ - name: myweb-3
properties:
cmd:
- sleep
- "1000"
enemies: alien
image: busybox
lives: "3"
+ traits:
+ - properties:
+ domain: www.example.com
+ http:
+ /: 90
+ type: myingress
type: myworker
status:
batchRollingState: ""
currentBatch: 0
rollingState: ""
upgradedReadyReplicas: 0
upgradedReplicas: 0
---
## Component (myweb-1) has been modified(*)
---
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
creationTimestamp: null
labels:
- app.oam.dev/name: application-sample
+ app.oam.dev/name: livediff-demo
name: myweb-1
spec:
workload:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb-1
- app.oam.dev/name: application-sample
+ app.oam.dev/name: livediff-demo
workload.oam.dev/type: myworker
spec:
selector:
matchLabels:
app.oam.dev/component: myweb-1
template:
metadata:
labels:
app.oam.dev/component: myweb-1
spec:
containers:
- command:
- sleep
- - "1000"
+ - "2000"
image: busybox
name: myweb-1
status:
observedGeneration: 0
---
### Component (myweb-1) / Trait (myingress/ingress) has been modified(*)
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb-1
- app.oam.dev/name: application-sample
+ app.oam.dev/name: livediff-demo
trait.oam.dev/resource: ingress
trait.oam.dev/type: myingress
name: myweb-1
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: myweb-1
- servicePort: 80
+ servicePort: 90
path: /
---
### Component (myweb-1) / Trait (myingress/service) has been modified(*)
---
apiVersion: v1
kind: Service
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb-1
- app.oam.dev/name: application-sample
+ app.oam.dev/name: livediff-demo
trait.oam.dev/resource: service
trait.oam.dev/type: myingress
name: myweb-1
spec:
ports:
- - port: 80
- targetPort: 80
+ - port: 90
+ targetPort: 90
selector:
app.oam.dev/component: myweb-1
---
### Component (myweb-1) / Trait (myscaler/scaler) has been removed(-)
---
- apiVersion: core.oam.dev/v1alpha2
- kind: ManualScalerTrait
- metadata:
- labels:
- app.oam.dev/appRevision: ""
- app.oam.dev/component: myweb-1
- app.oam.dev/name: application-sample
- trait.oam.dev/resource: scaler
- trait.oam.dev/type: myscaler
- spec:
- replicaCount: 2
---
## Component (myweb-2) has been modified(*)
---
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
creationTimestamp: null
labels:
- app.oam.dev/name: application-sample
+ app.oam.dev/name: livediff-demo
name: myweb-2
spec:
workload:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb-2
- app.oam.dev/name: application-sample
+ app.oam.dev/name: livediff-demo
workload.oam.dev/type: myworker
spec:
selector:
matchLabels:
app.oam.dev/component: myweb-2
template:
metadata:
labels:
app.oam.dev/component: myweb-2
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb-2
status:
observedGeneration: 0
---
### Component (myweb-2) / Trait (myingress/ingress) has been added(+)
---
+ apiVersion: networking.k8s.io/v1beta1
+ kind: Ingress
+ metadata:
+ labels:
+ app.oam.dev/appRevision: ""
+ app.oam.dev/component: myweb-2
+ app.oam.dev/name: livediff-demo
+ trait.oam.dev/resource: ingress
+ trait.oam.dev/type: myingress
+ name: myweb-2
+ spec:
+ rules:
+ - host: www.example.com
+ http:
+ paths:
+ - backend:
+ serviceName: myweb-2
+ servicePort: 90
+ path: /
---
### Component (myweb-2) / Trait (myingress/service) has been added(+)
---
+ apiVersion: v1
+ kind: Service
+ metadata:
+ labels:
+ app.oam.dev/appRevision: ""
+ app.oam.dev/component: myweb-2
+ app.oam.dev/name: livediff-demo
+ trait.oam.dev/resource: service
+ trait.oam.dev/type: myingress
+ name: myweb-2
+ spec:
+ ports:
+ - port: 90
+ targetPort: 90
+ selector:
+ app.oam.dev/component: myweb-2
---
## Component (myweb-3) has been added(+)
---
+ apiVersion: core.oam.dev/v1alpha2
+ kind: Component
+ metadata:
+ creationTimestamp: null
+ labels:
+ app.oam.dev/name: livediff-demo
+ name: myweb-3
+ spec:
+ workload:
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+ labels:
+ app.oam.dev/appRevision: ""
+ app.oam.dev/component: myweb-3
+ app.oam.dev/name: livediff-demo
+ workload.oam.dev/type: myworker
+ spec:
+ selector:
+ matchLabels:
+ app.oam.dev/component: myweb-3
+ template:
+ metadata:
+ labels:
+ app.oam.dev/component: myweb-3
+ spec:
+ containers:
+ - command:
+ - sleep
+ - "1000"
+ image: busybox
+ name: myweb-3
+ status:
+ observedGeneration: 0
---
### Component (myweb-3) / Trait (myingress/ingress) has been added(+)
---
+ apiVersion: networking.k8s.io/v1beta1
+ kind: Ingress
+ metadata:
+ labels:
+ app.oam.dev/appRevision: ""
+ app.oam.dev/component: myweb-3
+ app.oam.dev/name: livediff-demo
+ trait.oam.dev/resource: ingress
+ trait.oam.dev/type: myingress
+ name: myweb-3
+ spec:
+ rules:
+ - host: www.example.com
+ http:
+ paths:
+ - backend:
+ serviceName: myweb-3
+ servicePort: 90
+ path: /
---
### Component (myweb-3) / Trait (myingress/service) has been added(+)
---
+ apiVersion: v1
+ kind: Service
+ metadata:
+ labels:
+ app.oam.dev/appRevision: ""
+ app.oam.dev/component: myweb-3
+ app.oam.dev/name: livediff-demo
+ trait.oam.dev/resource: service
+ trait.oam.dev/type: myingress
+ name: myweb-3
+ spec:
+ ports:
+ - port: 90
+ targetPort: 90
+ selector:
+ app.oam.dev/component: myweb-3
```
</details>

View File

@@ -0,0 +1,54 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: livediff-demo
spec:
components:
- name: myweb-1
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "2000" # change a component property
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90 # change a trait
# - type: myscaler # remove a trait
# properties:
# replicas: 2
- name: myweb-2
type: myworker
properties: # no change on component property
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress # add a trait
properties:
domain: "www.example.com"
http:
"/": 90
- name: myweb-3 # add a component
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90

View File

@@ -0,0 +1,33 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: livediff-demo
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-2
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"

View File

@@ -0,0 +1,62 @@
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata:
name: myingress
spec:
appliesToWorkloads:
- "*"
schematic:
cue:
template: |
import (
kubev1 "kube/v1"
network "kube/networking.k8s.io/v1beta1"
)
parameter: {
domain: string
http: [string]: int
}
outputs: {
service: kubev1.#Service
ingress: network.#Ingress
}
// trait template can have multiple outputs in one trait
outputs: service: {
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}

View File

@@ -0,0 +1,28 @@
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "Configures replicas for your service."
name: myscaler
spec:
appliesToWorkloads:
- webservice
- worker
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
schematic:
cue:
template: |
outputs: scaler: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
//+usage=Replicas of the workload
replicas: *1 | int
}

View File

@@ -0,0 +1,49 @@
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
name: myworker
spec:
workload:
definition:
apiVersion: apps/v1
kind: Deployment
schematic:
cue:
template: |
import (
apps "kube/apps/v1"
)
output: apps.#Deployment
output: {
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
}

1
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/AlecAivazis/survey/v2 v2.1.1
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869
github.com/briandowns/spinner v1.11.1
github.com/coreos/prometheus-operator v0.41.1

2
go.sum
View File

@@ -181,6 +181,8 @@ github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a h1:pv34s756C4pEXnjgPfGYgdhg/ZdajGhyOvzx8k+23nw=
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b h1:uUXgbcPDK3KpW29o4iy7GtuappbWT0l5NaMo9H9pJDw=
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06GQ59hwDQAvmK1qxOQGB3WuVTRoY0okPTAv0=

View File

@@ -25,14 +25,17 @@ import (
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile/config"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/dsl/process"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -42,19 +45,39 @@ const (
AppfileBuiltinConfig = "config"
)
// TemplateLoaderFn load template of a capability definition
type TemplateLoaderFn func(context.Context, discoverymapper.DiscoveryMapper, client.Reader, string, types.CapType) (*Template, error)
// LoadTemplate load template of a capability definition
func (fn TemplateLoaderFn) LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, c client.Reader, capName string, capType types.CapType) (*Template, error) {
return fn(ctx, dm, c, capName, capType)
}
// Parser is an application parser
type Parser struct {
client client.Client
dm discoverymapper.DiscoveryMapper
pd *definition.PackageDiscover
client client.Client
dm discoverymapper.DiscoveryMapper
pd *definition.PackageDiscover
tmplLoader TemplateLoaderFn
}
// NewApplicationParser create appfile parser
func NewApplicationParser(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *definition.PackageDiscover) *Parser {
return &Parser{
client: cli,
dm: dm,
pd: pd,
client: cli,
dm: dm,
pd: pd,
tmplLoader: LoadTemplate,
}
}
// NewDryRunApplicationParser create an appfile parser for DryRun
func NewDryRunApplicationParser(cli client.Client, dm discoverymapper.DiscoveryMapper, pd *definition.PackageDiscover, defs []oam.Object) *Parser {
return &Parser{
client: cli,
dm: dm,
pd: pd,
tmplLoader: DryRunTemplateLoader(defs),
}
}
@@ -81,7 +104,7 @@ func (p *Parser) GenerateAppFile(ctx context.Context, app *v1beta1.Application)
// parseWorkload resolve an ApplicationComponent and generate a Workload
// containing ALL information required by an Appfile.
func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComponent, appName, ns string) (*Workload, error) {
templ, err := LoadTemplate(ctx, p.dm, p.client, comp.Type, types.TypeComponentDefinition)
templ, err := p.tmplLoader.LoadTemplate(ctx, p.dm, p.client, comp.Type, types.TypeComponentDefinition)
if err != nil && !kerrors.IsNotFound(err) {
return nil, errors.WithMessagef(err, "fetch type of %s", comp.Name)
}
@@ -132,7 +155,7 @@ func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComp
workload.Traits = append(workload.Traits, trait)
}
for scopeType, instanceName := range comp.Scopes {
gvk, err := GetScopeGVK(ctx, p.client, p.dm, scopeType)
gvk, err := getScopeGVK(ctx, p.client, p.dm, scopeType)
if err != nil {
return nil, err
}
@@ -145,7 +168,7 @@ func (p *Parser) parseWorkload(ctx context.Context, comp v1beta1.ApplicationComp
}
func (p *Parser) parseTrait(ctx context.Context, name string, properties map[string]interface{}) (*Trait, error) {
templ, err := LoadTemplate(ctx, p.dm, p.client, name, types.TypeTrait)
templ, err := p.tmplLoader.LoadTemplate(ctx, p.dm, p.client, name, types.TypeTrait)
if kerrors.IsNotFound(err) {
return nil, errors.Errorf("trait definition of %s not found", name)
}
@@ -250,3 +273,15 @@ func getComponentSetting(settingParamName string, params map[string]interface{})
}
return nil, fmt.Errorf("failed to get the value of component setting %s", settingParamName)
}
func getScopeGVK(ctx context.Context, cli client.Reader, dm discoverymapper.DiscoveryMapper,
name string) (schema.GroupVersionKind, error) {
var gvk schema.GroupVersionKind
sd := new(v1alpha2.ScopeDefinition)
err := util.GetDefinition(ctx, cli, sd, name)
if err != nil {
return gvk, err
}
return util.GetGVKFromDefinition(dm, sd.Spec.Reference)
}

View File

@@ -23,14 +23,14 @@ import (
"github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -44,7 +44,9 @@ const (
InsertSecretToTag = "+insertSecretTo="
)
// Template includes its string, health and its category
// Template is a helper struct for processing capability including
// ComponentDefinition, TraitDefinition, ScopeDefinition.
// It mainly collects schematic and status data of a capability definition.
type Template struct {
TemplateStr string
Health string
@@ -59,48 +61,28 @@ type Template struct {
TraitDefinition *v1beta1.TraitDefinition
}
// GetScopeGVK Get ScopeDefinition
func GetScopeGVK(ctx context.Context, cli client.Reader, dm discoverymapper.DiscoveryMapper,
name string) (schema.GroupVersionKind, error) {
var gvk schema.GroupVersionKind
sd := new(v1alpha2.ScopeDefinition)
err := oamutil.GetDefinition(ctx, cli, sd, name)
if err != nil {
return gvk, err
}
return oamutil.GetGVKFromDefinition(dm, sd.Spec.Reference)
}
// LoadTemplate Get template according to key
func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli client.Reader, key string, kd types.CapType) (*Template, error) {
// LoadTemplate gets the capability definition from cluster and resolve it.
// It returns a helper struct, Template, which will be used for further
// processing.
func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli client.Reader, capName string, capType types.CapType) (*Template, error) {
// Application Controller only load template from ComponentDefinition and TraitDefinition
switch kd {
switch capType {
case types.TypeComponentDefinition:
var schematic *common.Schematic
var status *common.Status
var extension *runtime.RawExtension
cd := new(v1beta1.ComponentDefinition)
err := oamutil.GetDefinition(ctx, cli, cd, key)
err := oamutil.GetDefinition(ctx, cli, cd, capName)
if err != nil {
if kerrors.IsNotFound(err) {
wd := new(v1beta1.WorkloadDefinition)
if err := oamutil.GetDefinition(ctx, cli, wd, key); err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate from workloadDefinition [%s] ", key)
if err := oamutil.GetDefinition(ctx, cli, wd, capName); err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate from workloadDefinition [%s] ", capName)
}
schematic, status, extension = wd.Spec.Schematic, wd.Spec.Status, wd.Spec.Extension
tmpl, err := NewTemplate(schematic, status, extension)
tmpl, err := newTemplateOfWorkloadDefinition(wd)
if err != nil {
return nil, errors.WithMessagef(err, "Create template [%s] from workload definition", key)
return nil, err
}
if cd.Annotations["type"] == string(types.TerraformCategory) {
tmpl.CapabilityCategory = types.TerraformCategory
}
tmpl.WorkloadDefinition = wd
gvk, err := oamutil.GetGVKFromDefinition(dm, wd.Spec.Reference)
if err != nil {
return nil, errors.WithMessagef(err, "Get GVK from workload definition [%s]", key)
return nil, errors.WithMessagef(err, "Get GVK from workload definition [%s]", capName)
}
tmpl.Reference = common.WorkloadGVK{
APIVersion: gvk.GroupVersion().String(),
@@ -108,105 +90,168 @@ func LoadTemplate(ctx context.Context, dm discoverymapper.DiscoveryMapper, cli c
}
return tmpl, nil
}
return nil, errors.WithMessagef(err, "LoadTemplate from ComponentDefinition [%s] ", key)
return nil, errors.WithMessagef(err, "LoadTemplate from ComponentDefinition [%s] ", capName)
}
schematic, status, extension = cd.Spec.Schematic, cd.Spec.Status, cd.Spec.Extension
tmpl, err := NewTemplate(schematic, status, extension)
tmpl, err := newTemplateOfCompDefinition(cd)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
return nil, err
}
if cd.Annotations["type"] == string(types.TerraformCategory) {
tmpl.CapabilityCategory = types.TerraformCategory
}
tmpl.ComponentDefinition = cd
tmpl.Reference = cd.Spec.Workload.Definition
return tmpl, nil
case types.TypeTrait:
td := new(v1beta1.TraitDefinition)
err := oamutil.GetDefinition(ctx, cli, td, key)
err := oamutil.GetDefinition(ctx, cli, td, capName)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", capName)
}
var capabilityCategory types.CapabilityCategory
if td.Annotations["type"] == string(types.TerraformCategory) {
capabilityCategory = types.TerraformCategory
}
tmpl, err := NewTemplate(td.Spec.Schematic, td.Spec.Status, td.Spec.Extension)
tmpl, err := newTemplateOfTraitDefinition(td)
if err != nil {
return nil, errors.WithMessagef(err, "LoadTemplate [%s] ", key)
return nil, err
}
if tmpl == nil {
return nil, errors.New("no template found in definition")
}
tmpl.CapabilityCategory = capabilityCategory
tmpl.TraitDefinition = td
return tmpl, nil
case types.TypeScope:
// TODO: add scope template support
default:
return nil, fmt.Errorf("kind(%s) of %s not supported", kd, key)
return nil, fmt.Errorf("kind(%s) of %s not supported", capType, capName)
}
return nil, fmt.Errorf("kind(%s) of %s not supported", kd, key)
return nil, fmt.Errorf("kind(%s) of %s not supported", capType, capName)
}
// NewTemplate will create template for inner AbstractEngine using.
func NewTemplate(schematic *common.Schematic, status *common.Status, raw *runtime.RawExtension) (*Template, error) {
tmp := &Template{}
// DryRunTemplateLoader return a function that do the same work as
// LoadTemplate, but load template from provided ones before loading from
// cluster through LoadTemplate
func DryRunTemplateLoader(defs []oam.Object) TemplateLoaderFn {
return TemplateLoaderFn(func(ctx context.Context, dm discoverymapper.DiscoveryMapper, r client.Reader, capName string, capType types.CapType) (*Template, error) {
// retrieve provided cap definitions
for _, def := range defs {
if unstructDef, ok := def.(*unstructured.Unstructured); ok {
if unstructDef.GetKind() == v1beta1.ComponentDefinitionKind &&
capType == types.TypeComponentDefinition && unstructDef.GetName() == capName {
compDef := &v1beta1.ComponentDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructDef.Object, compDef); err != nil {
return nil, errors.Wrap(err, "invalid component definition")
}
tmpl, err := newTemplateOfCompDefinition(compDef)
if err != nil {
return nil, errors.WithMessagef(err, "cannot load template of component definition %q", capName)
}
return tmpl, nil
}
if unstructDef.GetKind() == v1beta1.TraitDefinitionKind &&
capType == types.TypeTrait && unstructDef.GetName() == capName {
traitDef := &v1beta1.TraitDefinition{}
if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructDef.Object, traitDef); err != nil {
return nil, errors.Wrap(err, "invalid trait definition")
}
tmpl, err := newTemplateOfTraitDefinition(traitDef)
if err != nil {
return nil, errors.WithMessagef(err, "cannot load template of trait definition %q", capName)
}
return tmpl, nil
}
// TODO(roywang) add support for ScopeDefinition
}
}
// not found in provided cap definitions
// then try to retrieve from cluster
tmpl, err := LoadTemplate(ctx, dm, r, capName, capType)
if err != nil {
return nil, errors.WithMessagef(err, "cannot load template %q from cluster and provided ones", capName)
}
return tmpl, nil
})
}
if status != nil {
tmp.CustomStatus = status.CustomStatus
tmp.Health = status.HealthPolicy
func newTemplateOfCompDefinition(compDef *v1beta1.ComponentDefinition) (*Template, error) {
tmpl := &Template{
Reference: compDef.Spec.Workload.Definition,
ComponentDefinition: compDef,
}
if schematic != nil {
if schematic.CUE != nil {
tmp.TemplateStr = schematic.CUE.Template
// CUE module has highest priority
// no need to check other schematic types
return tmp, nil
if err := loadSchematicToTemplate(tmpl, compDef.Spec.Status, compDef.Spec.Schematic, compDef.Spec.Extension); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
if compDef.Annotations["type"] == string(types.TerraformCategory) {
tmpl.CapabilityCategory = types.TerraformCategory
}
return tmpl, nil
}
func newTemplateOfTraitDefinition(traitDef *v1beta1.TraitDefinition) (*Template, error) {
tmpl := &Template{
TraitDefinition: traitDef,
}
if err := loadSchematicToTemplate(tmpl, traitDef.Spec.Status, traitDef.Spec.Schematic, traitDef.Spec.Extension); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
return tmpl, nil
}
func newTemplateOfWorkloadDefinition(wlDef *v1beta1.WorkloadDefinition) (*Template, error) {
tmpl := &Template{
WorkloadDefinition: wlDef,
}
if err := loadSchematicToTemplate(tmpl, wlDef.Spec.Status, wlDef.Spec.Schematic, wlDef.Spec.Extension); err != nil {
return nil, errors.WithMessage(err, "cannot load template")
}
return tmpl, nil
}
// loadSchematicToTemplate loads common data that all kind definitions have.
func loadSchematicToTemplate(tmpl *Template, sts *common.Status, schem *common.Schematic, ext *runtime.RawExtension) error {
if sts != nil {
tmpl.CustomStatus = sts.CustomStatus
tmpl.Health = sts.HealthPolicy
}
if schem != nil {
if schem.CUE != nil {
tmpl.CapabilityCategory = types.CUECategory
tmpl.TemplateStr = schem.CUE.Template
}
if schematic.HELM != nil {
tmp.Helm = schematic.HELM
tmp.CapabilityCategory = types.HelmCategory
return tmp, nil
if schem.HELM != nil {
tmpl.CapabilityCategory = types.HelmCategory
tmpl.Helm = schem.HELM
return nil
}
if schematic.KUBE != nil {
tmp.Kube = schematic.KUBE
tmp.CapabilityCategory = types.KubeCategory
if schem.KUBE != nil {
tmpl.CapabilityCategory = types.KubeCategory
tmpl.Kube = schem.KUBE
return nil
}
}
extension := map[string]interface{}{}
if tmp.TemplateStr == "" && raw != nil {
if err := json.Unmarshal(raw.Raw, &extension); err != nil {
return nil, err
if tmpl.TemplateStr == "" && ext != nil {
tmpl.CapabilityCategory = types.CUECategory
extension := map[string]interface{}{}
if err := json.Unmarshal(ext.Raw, &extension); err != nil {
return errors.Wrap(err, "cannot parse capability extension")
}
if extTemplate, ok := extension["template"]; ok {
if tmpStr, ok := extTemplate.(string); ok {
tmp.TemplateStr = tmpStr
tmpl.TemplateStr = tmpStr
return nil
}
}
}
return tmp, nil
return nil
}
// ConvertTemplateJSON2Object convert spec.extension to object
func ConvertTemplateJSON2Object(capabilityName string, in *runtime.RawExtension, schematic *common.Schematic) (types.Capability, error) {
var t types.Capability
t.Name = capabilityName
capTemplate, err := NewTemplate(schematic, nil, in)
if err != nil {
return t, errors.Wrapf(err, "parse cue template")
}
if in != nil && in.Raw != nil {
err := json.Unmarshal(in.Raw, &t)
if err != nil {
return t, errors.Wrapf(err, "parse extension fail")
}
}
capTemplate := &Template{}
if err := loadSchematicToTemplate(capTemplate, nil, schematic, in); err != nil {
return t, errors.WithMessage(err, "cannot resolve schematic")
}
if capTemplate.TemplateStr != "" {
t.CueTemplate = capTemplate.TemplateStr
}
return t, err
return t, nil
}

View File

@@ -22,6 +22,7 @@ import (
"cuelang.org/go/cue"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/runtime"
ktypes "k8s.io/apimachinery/pkg/types"
@@ -30,6 +31,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/mock"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
@@ -361,41 +363,45 @@ spec:
}
}
func TestNewTemplate(t *testing.T) {
func TestLoadSchematicToTemplate(t *testing.T) {
testCases := map[string]struct {
tmp *common.Schematic
schem *common.Schematic
status *common.Status
ext *runtime.RawExtension
exp *Template
want *Template
}{
"only tmp": {
tmp: &common.Schematic{CUE: &common.CUE{Template: "t1"}},
exp: &Template{
TemplateStr: "t1",
schem: &common.Schematic{CUE: &common.CUE{Template: "t1"}},
want: &Template{
TemplateStr: "t1",
CapabilityCategory: types.CUECategory,
},
},
"no tmp,but has extension": {
ext: &runtime.RawExtension{Raw: []byte(`{"template":"t1"}`)},
exp: &Template{
TemplateStr: "t1",
want: &Template{
TemplateStr: "t1",
CapabilityCategory: types.CUECategory,
},
},
"no tmp,but has extension without temp": {
ext: &runtime.RawExtension{Raw: []byte(`{"template":{"t1":"t2"}}`)},
exp: &Template{
TemplateStr: "",
want: &Template{
TemplateStr: "",
CapabilityCategory: types.CUECategory,
},
},
"tmp with status": {
tmp: &common.Schematic{CUE: &common.CUE{Template: "t1"}},
schem: &common.Schematic{CUE: &common.CUE{Template: "t1"}},
status: &common.Status{
CustomStatus: "s1",
HealthPolicy: "h1",
},
exp: &Template{
TemplateStr: "t1",
CustomStatus: "s1",
Health: "h1",
want: &Template{
TemplateStr: "t1",
CustomStatus: "s1",
Health: "h1",
CapabilityCategory: types.CUECategory,
},
},
"no tmp only status": {
@@ -403,15 +409,97 @@ func TestNewTemplate(t *testing.T) {
CustomStatus: "s1",
HealthPolicy: "h1",
},
exp: &Template{
want: &Template{
CustomStatus: "s1",
Health: "h1",
},
},
}
for reason, casei := range testCases {
gtmp, err := NewTemplate(casei.tmp, casei.status, casei.ext)
gtmp := &Template{}
err := loadSchematicToTemplate(gtmp, casei.status, casei.schem, casei.ext)
assert.NoError(t, err, reason)
assert.Equal(t, gtmp, casei.exp, reason)
assert.Equal(t, casei.want, gtmp, reason)
}
}
func TestDryRunTemplateLoader(t *testing.T) {
compDefStr := `
apiVersion: core.oam.dev/v1alpha2
kind: ComponentDefinition
metadata:
name: myworker
spec:
status:
customStatus: testCustomStatus
healthPolicy: testHealthPolicy
workload:
definition:
apiVersion: apps/v1
kind: Deployment
schematic:
cue:
template: testCUE `
traitDefStr := `
apiVersion: core.oam.dev/v1alpha2
kind: TraitDefinition
metadata:
name: myingress
spec:
status:
customStatus: testCustomStatus
healthPolicy: testHealthPolicy
appliesToWorkloads:
- webservice
- worker
schematic:
cue:
template: testCUE `
compDef, _ := oamutil.UnMarshalStringToComponentDefinition(compDefStr)
traitDef, _ := oamutil.UnMarshalStringToTraitDefinition(traitDefStr)
unstrctCompDef, _ := oamutil.Object2Unstructured(compDef)
unstrctTraitDef, _ := oamutil.Object2Unstructured(traitDef)
expectedCompTmpl := &Template{
TemplateStr: "testCUE",
Health: "testHealthPolicy",
CustomStatus: "testCustomStatus",
CapabilityCategory: types.CUECategory,
Reference: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
Helm: nil,
Kube: nil,
ComponentDefinition: compDef,
}
expectedTraitTmpl := &Template{
TemplateStr: "testCUE",
Health: "testHealthPolicy",
CustomStatus: "testCustomStatus",
CapabilityCategory: types.CUECategory,
Helm: nil,
Kube: nil,
TraitDefinition: traitDef,
}
dryRunLoadTemplate := DryRunTemplateLoader([]oam.Object{unstrctCompDef, unstrctTraitDef})
compTmpl, err := dryRunLoadTemplate(nil, nil, nil, "myworker", types.TypeComponentDefinition)
if err != nil {
t.Error("failed load template of component defintion", err)
}
if diff := cmp.Diff(expectedCompTmpl, compTmpl); diff != "" {
t.Fatal("failed load template of component defintion", diff)
}
traitTmpl, err := dryRunLoadTemplate(nil, nil, nil, "myingress", types.TypeTrait)
if err != nil {
t.Error("failed load template of component defintion", err)
}
if diff := cmp.Diff(expectedTraitTmpl, traitTmpl); diff != "" {
t.Fatal("failed load template of trait definition ", diff)
}
}

View File

@@ -134,12 +134,13 @@ func ValidateDefinitionReference(_ context.Context, td v1beta1.TraitDefinition)
if len(td.Spec.Reference.Name) > 0 {
return nil
}
tmp, err := appfile.NewTemplate(td.Spec.Schematic, td.Spec.Status, td.Spec.Extension)
cap, err := appfile.ConvertTemplateJSON2Object(td.Name, td.Spec.Extension, td.Spec.Schematic)
if err != nil {
return errors.Wrap(err, errValidateDefRef)
return errors.WithMessage(err, errValidateDefRef)
}
if len(tmp.TemplateStr) == 0 {
if cap.CueTemplate == "" {
return errors.New(failInfoDefRefOmitted)
}
return nil
}

View File

@@ -0,0 +1,404 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/aryann/difflib"
"github.com/ghodss/yaml"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
// NewLiveDiffOption creates a live-diff option
func NewLiveDiffOption(c client.Client, dm discoverymapper.DiscoveryMapper, pd *definition.PackageDiscover, as []oam.Object) *LiveDiffOption {
return &LiveDiffOption{NewDryRunOption(c, dm, pd, as)}
}
// ManifestKind enums the kind of OAM objects
type ManifestKind string
// enum kinds of manifest objects
const (
AppKind ManifestKind = "Application"
AppConfigCompKind ManifestKind = "AppConfigComponent"
RawCompKind ManifestKind = "Component"
TraitKind ManifestKind = "Trait"
)
// DiffEntry records diff info of OAM object
type DiffEntry struct {
Name string `json:"name"`
Kind ManifestKind `json:"kind"`
DiffType DiffType `json:"diffType,omitempty"`
Diffs []difflib.DiffRecord `json:"diffs,omitempty"`
Subs []*DiffEntry `json:"subs,omitempty"`
}
// DiffType enums the type of diff
type DiffType string
// enum types of diff
const (
AddDiff DiffType = "ADD"
ModifyDiff DiffType = "MODIFY"
RemoveDiff DiffType = "REMOVE"
NoDiff DiffType = ""
)
// manifest is a helper struct used to calculate diff on applications and
// sub-resources.
type manifest struct {
Name string
Kind ManifestKind
// Data is unmarshalled object in YAML
Data string
// application's subs means appConfigComponents
// appConfigComponent's subs means rawComponent and traits
Subs []*manifest
}
// LiveDiffOption contains options for comparing an application with a
// living AppRevision in the cluster
type LiveDiffOption struct {
DryRun
}
// 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) {
ac, comps, err := l.ExecuteDryRun(ctx, app)
if err != nil {
return nil, errors.WithMessagef(err, "cannot dry-run for app %q", app.Name)
}
// new refers to the app as input to dry-run
newManifest, err := generateManifest(app, ac, comps)
if err != nil {
return nil, errors.WithMessagef(err, "cannot generate diff manifest for app %q", app.Name)
}
// old refers to the living app revision
oldManifest, err := generateManifestFromAppRevision(appRevision)
if err != nil {
return nil, errors.WithMessagef(err, "cannot generate diff manifest for AppRevision %q", appRevision.Name)
}
diffResult := l.calculateDiff(oldManifest, newManifest)
return diffResult, nil
}
// calculateDiff calculate diff between two application and their sub-resources
func (l *LiveDiffOption) calculateDiff(oldApp, newApp *manifest) *DiffEntry {
emptyManifest := &manifest{}
r := &DiffEntry{
Name: oldApp.Name,
Kind: oldApp.Kind,
}
appDiffs := diffManifest(oldApp, newApp)
if hasChanges(appDiffs) {
r.DiffType = ModifyDiff
r.Diffs = appDiffs
}
// check modified and removed components
for _, oldAcc := range oldApp.Subs {
accDiffEntry := &DiffEntry{
Name: oldAcc.Name,
Kind: oldAcc.Kind,
Subs: make([]*DiffEntry, 0),
}
var newAcc *manifest
// check whether component is removed
for _, acc := range newApp.Subs {
if oldAcc.Name == acc.Name {
newAcc = acc
break
}
}
if newAcc != nil {
// component is not removed
// check modified and removed ACC subs (rawComponent and traits)
for _, oldAccSub := range oldAcc.Subs {
accSubDiffEntry := &DiffEntry{
Name: oldAccSub.Name,
Kind: oldAccSub.Kind,
}
var newAccSub *manifest
for _, accSub := range newAcc.Subs {
if accSub.Kind == oldAccSub.Kind &&
accSub.Name == oldAccSub.Name {
newAccSub = accSub
break
}
}
var diffs []difflib.DiffRecord
if newAccSub != nil {
// accSub is not removed, then check modification
diffs = diffManifest(oldAccSub, newAccSub)
if hasChanges(diffs) {
accSubDiffEntry.DiffType = ModifyDiff
} else {
accSubDiffEntry.DiffType = NoDiff
}
} else {
// accSub is removed
diffs = diffManifest(oldAccSub, emptyManifest)
accSubDiffEntry.DiffType = RemoveDiff
}
accSubDiffEntry.Diffs = diffs
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
// check added ACC subs (traits)
for _, newAccSub := range newAcc.Subs {
isAdded := true
for _, oldAccSub := range oldAcc.Subs {
if oldAccSub.Kind == newAccSub.Kind &&
oldAccSub.Name == newAccSub.Name {
isAdded = false
break
}
}
if isAdded {
accSubDiffEntry := &DiffEntry{
Name: newAccSub.Name,
Kind: newAccSub.Kind,
DiffType: AddDiff,
}
diffs := diffManifest(emptyManifest, newAccSub)
accSubDiffEntry.Diffs = diffs
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
}
} else {
// component is removed as well as its subs
accDiffEntry.DiffType = RemoveDiff
for _, oldAccSub := range oldAcc.Subs {
diffs := diffManifest(oldAccSub, emptyManifest)
accSubDiffEntry := &DiffEntry{
Name: oldAccSub.Name,
Kind: oldAccSub.Kind,
DiffType: RemoveDiff,
Diffs: diffs,
}
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
}
r.Subs = append(r.Subs, accDiffEntry)
}
// check added component
for _, newAcc := range newApp.Subs {
isAdded := true
for _, oldAcc := range oldApp.Subs {
if oldAcc.Kind == newAcc.Kind &&
oldAcc.Name == newAcc.Name {
isAdded = false
break
}
}
if isAdded {
accDiffEntry := &DiffEntry{
Name: newAcc.Name,
Kind: newAcc.Kind,
DiffType: AddDiff,
Subs: make([]*DiffEntry, 0),
}
// added component's subs are all added
for _, newAccSub := range newAcc.Subs {
diffs := diffManifest(emptyManifest, newAccSub)
accSubDiffEntry := &DiffEntry{
Name: newAccSub.Name,
Kind: newAccSub.Kind,
DiffType: AddDiff,
Diffs: diffs,
}
accDiffEntry.Subs = append(accDiffEntry.Subs, accSubDiffEntry)
}
r.Subs = append(r.Subs, accDiffEntry)
}
}
return r
}
// generateManifest generates a manifest whose top-level is an application
func generateManifest(app *v1beta1.Application, ac *v1alpha2.ApplicationConfiguration, comps []*v1alpha2.Component) (*manifest, error) {
r := &manifest{
Name: app.Name,
Kind: AppKind,
}
b, err := yaml.Marshal(app)
if err != nil {
return nil, errors.Wrapf(err, "cannot marshal application %q", app.Name)
}
r.Data = string(b)
appSubs := make([]*manifest, 0, len(app.Spec.Components))
// a helper map recording all rawComponents with compName as key
rawCompManifests := map[string]*manifest{}
for _, comp := range comps {
cM := &manifest{
Name: comp.Name,
Kind: RawCompKind,
}
// dry-run doesn't set namespace and ownerRef to a component
// we should remove them before comparing
comp.SetNamespace("")
comp.SetOwnerReferences(nil)
if err := emptifyAppRevisionLabel(&comp.Spec.Workload); err != nil {
return nil, errors.WithMessagef(err, "cannot emptify appRevision label in component %q", comp.Name)
}
b, err := yaml.Marshal(comp)
if err != nil {
return nil, errors.Wrapf(err, "cannot marshal component %q", comp.Name)
}
cM.Data = string(b)
rawCompManifests[comp.Name] = cM
}
// generate appConfigComponent manifests
for _, acc := range ac.Spec.Components {
if acc.ComponentName == "" && acc.RevisionName != "" {
// dry-run cannot generate revision name
// we should compare with comp name rather than revision name
acc.ComponentName = extractNameFromRevisionName(acc.RevisionName)
acc.RevisionName = ""
}
accM := &manifest{
Name: acc.ComponentName,
Kind: AppConfigCompKind,
}
// get matched raw component and add it into appConfigComponent's subs
subs := []*manifest{rawCompManifests[acc.ComponentName]}
for _, t := range acc.Traits {
if err := emptifyAppRevisionLabel(&t.Trait); err != nil {
return nil, errors.WithMessage(err, "cannot emptify appRevision label of trait")
}
tObj, err := oamutil.RawExtension2Unstructured(&t.Trait)
if err != nil {
return nil, errors.WithMessage(err, "cannot parser trait raw")
}
tType := tObj.GetLabels()[oam.TraitTypeLabel]
tResource := tObj.GetLabels()[oam.TraitResource]
// dry-run cannot generate name for a trait
// a join of trait tyupe&resource is unique in a component
// we use it to identify a trait
tUnique := fmt.Sprintf("%s/%s", tType, tResource)
b, err := yaml.JSONToYAML(t.Trait.Raw)
if err != nil {
return nil, errors.Wrapf(err, "cannot parse trait %q raw to YAML", tUnique)
}
subs = append(subs, &manifest{
Name: tUnique,
Kind: TraitKind,
Data: string(b),
})
}
accM.Subs = subs
appSubs = append(appSubs, accM)
}
r.Subs = appSubs
return r, nil
}
// generateManifestFromAppRevision generates manifest from an AppRevision
func generateManifestFromAppRevision(appRevision *v1beta1.ApplicationRevision) (*manifest, error) {
ac := &v1alpha2.ApplicationConfiguration{}
if err := json.Unmarshal(appRevision.Spec.ApplicationConfiguration.Raw, ac); err != nil {
return nil, errors.Wrap(err, "cannot unmarshal appconfig")
}
comps := []*v1alpha2.Component{}
for _, rawComp := range appRevision.Spec.Components {
c := &v1alpha2.Component{}
if err := json.Unmarshal(rawComp.Raw.Raw, c); err != nil {
return nil, errors.Wrap(err, "cannot unmarshal component")
}
comps = append(comps, c)
}
app := appRevision.Spec.Application
// app in appRevision has no name & namespace
// we should extract/get them from appRappRevision
app.Name = extractNameFromRevisionName(appRevision.Name)
app.Namespace = appRevision.Namespace
return generateManifest(&app, ac, comps)
}
// diffManifest calculates diff between data of two manifest line by line
func diffManifest(old, new *manifest) []difflib.DiffRecord {
const sep = "\n"
return difflib.Diff(strings.Split(old.Data, sep), strings.Split(new.Data, sep))
}
func extractNameFromRevisionName(r string) string {
s := strings.Split(r, "-")
return strings.Join(s[0:len(s)-1], "-")
}
// emptifyAppRevisionLabel will set label oam.LabelAppRevision to empty
// because dry-run cannot set value to this lable
func emptifyAppRevisionLabel(o *runtime.RawExtension) error {
u, err := oamutil.RawExtension2Unstructured(o)
if err != nil {
return errors.WithMessage(err, "cannot reset appRevision label of raw object")
}
newLabels := map[string]string{}
labels := u.GetLabels()
for k, v := range labels {
if k == oam.LabelAppRevision {
newLabels[k] = ""
continue
}
newLabels[k] = v
}
u.SetLabels(newLabels)
b, err := u.MarshalJSON()
if err != nil {
return errors.WithMessage(err, "cannot reset appRevision label of raw object")
}
o.Raw = b
return nil
}
// hasChanges checks whether existing change in diff records
func hasChanges(diffs []difflib.DiffRecord) bool {
for _, d := range diffs {
// diffliib.Common means no change between two sides
if d.Delta != difflib.Common {
return true
}
}
return false
}

View File

@@ -0,0 +1,154 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"bytes"
"context"
"github.com/ghodss/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Test Live-Diff", func() {
appMultiChangesYAML := readDataFromFile("./testdata/diff-input-app-multichanges.yaml")
appNoChangeYAML := readDataFromFile("./testdata/diff-input-app-nochange.yaml")
appOnlyAddYAML := readDataFromFile("./testdata/diff-input-app-onlyadd.yaml")
appOnlyModifYAML := readDataFromFile("./testdata/diff-input-app-onlymodif.yaml")
appOnlyRemoveYAML := readDataFromFile("./testdata/diff-input-app-onlyremove.yaml")
appMultiChanges := new(v1beta1.Application)
appNoChange := new(v1beta1.Application)
appOnlyAdd := new(v1beta1.Application)
appOnlyModif := new(v1beta1.Application)
appOnlyRemove := new(v1beta1.Application)
origAppRevYAML := readDataFromFile("./testdata/diff-apprevision.yaml")
originalAppRev := new(v1beta1.ApplicationRevision)
diffAndPrint := func(app *v1beta1.Application) string {
By("Execute Live-diff")
diffResult, err := diffOpt.Diff(context.Background(), app, originalAppRev)
Expect(err).Should(BeNil())
Expect(diffResult).ShouldNot(BeNil())
By("Print diff result into buffer")
buff := &bytes.Buffer{}
reportOpt := NewReportDiffOption(10, buff)
reportOpt.PrintDiffReport(diffResult)
return buff.String()
}
BeforeEach(func() {
By("Prepare AppRevision data")
Expect(yaml.Unmarshal([]byte(origAppRevYAML), originalAppRev)).Should(Succeed())
})
It("Test app containing multiple changes(add/modify/remove/no)", func() {
Expect(yaml.Unmarshal([]byte(appMultiChangesYAML), appMultiChanges)).Should(Succeed())
diffResultStr := diffAndPrint(appMultiChanges)
Expect(diffResultStr).Should(SatisfyAll(
ContainSubstring("Application (livediff-demo) has been modified(*)"),
ContainSubstring("Component (myweb-1) has been modified(*)"),
ContainSubstring("Component (myweb-1) / Trait (myingress/service) has been modified(*)"),
ContainSubstring("Component (myweb-1) / Trait (myingress/ingress) has been modified(*)"),
ContainSubstring("Component (myweb-1) / Trait (myscaler/scaler) has been removed(-)"),
ContainSubstring("Component (myweb-2) has no change"),
ContainSubstring("Component (myweb-2) / Trait (myingress/service) has been added(+)"),
ContainSubstring("Component (myweb-2) / Trait (myingress/ingress) has been added(+)"),
ContainSubstring("Component (myweb-3) has been added(+)"),
ContainSubstring("Component (myweb-3) / Trait (myingress/service) has been added(+)"),
ContainSubstring("Component (myweb-3) / Trait (myingress/ingress) has been added(+)"),
))
})
It("Test no change", func() {
Expect(yaml.Unmarshal([]byte(appNoChangeYAML), appNoChange)).Should(Succeed())
diffResultStr := diffAndPrint(appNoChange)
Expect(diffResultStr).Should(SatisfyAll(
ContainSubstring("Application (livediff-demo) has no change"),
ContainSubstring("Component (myweb-1) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myingress/service) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myingress/ingress) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myscaler/scaler) has no change"),
ContainSubstring("Component (myweb-2) has no change"),
))
Expect(diffResultStr).ShouldNot(SatisfyAny(
ContainSubstring("added"),
ContainSubstring("removed"),
ContainSubstring("modified"),
))
})
It("Test only added change", func() {
Expect(yaml.Unmarshal([]byte(appOnlyAddYAML), appOnlyAdd)).Should(Succeed())
diffResultStr := diffAndPrint(appOnlyAdd)
Expect(diffResultStr).Should(SatisfyAll(
ContainSubstring("Application (livediff-demo) has been modified"),
ContainSubstring("Component (myweb-1) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myingress/service) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myingress/ingress) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myscaler/scaler) has no change"),
ContainSubstring("Component (myweb-2) has no change"),
ContainSubstring("Component (myweb-2) / Trait (myingress/service) has been added"),
ContainSubstring("Component (myweb-2) / Trait (myingress/ingress) has been added"),
ContainSubstring("Component (myweb-3) has been added"),
ContainSubstring("Component (myweb-3) / Trait (myingress/service) has been added"),
ContainSubstring("Component (myweb-3) / Trait (myingress/ingress) has been added"),
))
Expect(diffResultStr).ShouldNot(SatisfyAny(
ContainSubstring("removed"),
))
})
It("Test only modified change", func() {
Expect(yaml.Unmarshal([]byte(appOnlyModifYAML), appOnlyModif)).Should(Succeed())
diffResultStr := diffAndPrint(appOnlyModif)
Expect(diffResultStr).Should(SatisfyAll(
ContainSubstring("Application (livediff-demo) has been modified"),
ContainSubstring("Component (myweb-1) has been modified"),
ContainSubstring("Component (myweb-1) / Trait (myingress/service) has been modified"),
ContainSubstring("Component (myweb-1) / Trait (myingress/ingress) has been modified"),
ContainSubstring("Component (myweb-1) / Trait (myscaler/scaler) has no change"),
ContainSubstring("Component (myweb-2) has no change"),
))
Expect(diffResultStr).ShouldNot(SatisfyAny(
ContainSubstring("removed"),
ContainSubstring("added"),
))
})
It("Test only removed change", func() {
Expect(yaml.Unmarshal([]byte(appOnlyRemoveYAML), appOnlyRemove)).Should(Succeed())
diffResultStr := diffAndPrint(appOnlyRemove)
Expect(diffResultStr).Should(SatisfyAll(
ContainSubstring("Application (livediff-demo) has been modified"),
ContainSubstring("Component (myweb-1) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myingress/service) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myingress/ingress) has no change"),
ContainSubstring("Component (myweb-1) / Trait (myscaler/scaler) has been removed"),
ContainSubstring("Component (myweb-2) has been removed"),
))
Expect(diffResultStr).ShouldNot(SatisfyAny(
ContainSubstring("added"),
))
})
})

View File

@@ -0,0 +1,69 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"context"
"github.com/pkg/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
// DryRun executes dry-run on an application
type DryRun interface {
ExecuteDryRun(ctx context.Context, app *v1beta1.Application) (*v1alpha2.ApplicationConfiguration, []*v1alpha2.Component, error)
}
// NewDryRunOption creates a dry-run option
func NewDryRunOption(c client.Client, dm discoverymapper.DiscoveryMapper, pd *definition.PackageDiscover, as []oam.Object) *Option {
return &Option{c, dm, pd, as}
}
// Option contains options to execute dry-run
type Option struct {
Client client.Client
DiscoveryMapper discoverymapper.DiscoveryMapper
PackageDiscover *definition.PackageDiscover
// Auxiliaries are capability definitions used to parse application.
// DryRun will use capabilities in Auxiliaries as higher priority than
// getting one from cluster.
Auxiliaries []oam.Object
}
// ExecuteDryRun simulates applying an application into cluster and returns rendered
// resoures but not persist them into cluster.
func (d *Option) ExecuteDryRun(ctx context.Context, app *v1beta1.Application) (*v1alpha2.ApplicationConfiguration, []*v1alpha2.Component, error) {
parser := appfile.NewDryRunApplicationParser(d.Client, d.DiscoveryMapper, d.PackageDiscover, d.Auxiliaries)
ctxWithNamespace := oamutil.SetNamespaceInCtx(ctx, app.Namespace)
appFile, err := parser.GenerateAppFile(ctxWithNamespace, app)
if err != nil {
return nil, nil, errors.WithMessage(err, "cannot generate appFile from application")
}
ac, comps, err := appFile.GenerateApplicationConfiguration()
if err != nil {
return nil, nil, errors.WithMessage(err, "cannot generate AppConfig and Components")
}
return ac, comps, nil
}

View File

@@ -0,0 +1,61 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"context"
"encoding/json"
"github.com/google/go-cmp/cmp"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"sigs.k8s.io/yaml"
)
var _ = Describe("Test DryRun", func() {
It("Test DryRun", func() {
appYAML := readDataFromFile("./testdata/dryrun-app.yaml")
By("Prepare test data")
app := &v1beta1.Application{}
b, err := yaml.YAMLToJSON([]byte(appYAML))
Expect(err).Should(BeNil())
err = json.Unmarshal(b, app)
Expect(err).Should(BeNil())
By("Execute DryRun")
ac, comps, err := dryrunOpt.ExecuteDryRun(context.Background(), app)
Expect(err).Should(BeNil())
expectACYAML := readDataFromFile("./testdata/dryrun-exp-ac.yaml")
By("Verify generated AppConfig")
resultACstr, err := yaml.Marshal(ac)
Expect(err).Should(BeNil())
diff := cmp.Diff(expectACYAML, string(resultACstr))
Expect(diff).Should(BeEmpty())
expectCompYAML := readDataFromFile("./testdata/dryrun-exp-comp.yaml")
By("Verify generated Comp")
Expect(comps).ShouldNot(BeEmpty())
resultCompStr, err := yaml.Marshal(comps[0])
Expect(err).Should(BeNil())
diff = cmp.Diff(expectCompYAML, string(resultCompStr))
Expect(diff).Should(BeEmpty())
})
})

View File

@@ -0,0 +1,145 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"fmt"
"io"
"math"
"github.com/aryann/difflib"
"github.com/fatih/color"
)
var (
red = color.New(color.FgRed)
green = color.New(color.FgGreen)
yellow = color.New(color.FgYellow)
)
// NewReportDiffOption creats a new ReportDiffOption that can formats and prints
// diff report into an io.Writer
func NewReportDiffOption(ctx int, to io.Writer) *ReportDiffOption {
return &ReportDiffOption{
DiffMsgs: map[DiffType]string{
AddDiff: "has been added(+)",
ModifyDiff: "has been modified(*)",
RemoveDiff: "has been removed(-)",
NoDiff: "has no change",
},
Context: ctx,
To: to,
}
}
// ReportDiffOption contains options to formats and prints diff report
type ReportDiffOption struct {
DiffMsgs map[DiffType]string
Context int
To io.Writer
}
// 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)
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 printDiffs(diffs []difflib.DiffRecord, context int, to io.Writer) {
if context > 0 {
ctx := calculateContext(diffs)
skip := false
for i, diff := range diffs {
if ctx[i] <= context {
// only print the line whose distance to a closest diff is less
// than context
printDiffRecord(to, diff)
skip = false
} else if !skip {
fmt.Fprint(to, "...\n")
// skip print if next line is still omitted
skip = true
}
}
} else {
for _, diff := range diffs {
printDiffRecord(to, diff)
}
}
}
// calculateContext calculate the min distance from each line to its closest diff
func calculateContext(diffs []difflib.DiffRecord) map[int]int {
ctx := map[int]int{}
// retrieve forward to calculate the min distance from each line to a
// changed line behind it
changeLineNum := -1
for i, diff := range diffs {
if diff.Delta != difflib.Common {
changeLineNum = i
}
distance := math.MaxInt32
if changeLineNum != -1 {
distance = i - changeLineNum
}
ctx[i] = distance
}
// retrieve backward to calculate the min distance from each line to a
// changed line before it
changeLineNum = -1
for i := len(diffs) - 1; i >= 0; i-- {
if diffs[i].Delta != difflib.Common {
changeLineNum = i
}
if changeLineNum != -1 {
distance := changeLineNum - i
if distance < ctx[i] {
ctx[i] = distance
}
}
}
return ctx
}
func printDiffRecord(to io.Writer, diff difflib.DiffRecord) {
data := diff.Payload
switch diff.Delta {
case difflib.RightOnly:
_, _ = green.Fprintf(to, "+ %s\n", data)
case difflib.LeftOnly:
_, _ = red.Fprintf(to, "- %s\n", data)
case difflib.Common:
_, _ = fmt.Fprintf(to, " %s\n", data)
}
}

View File

@@ -0,0 +1,120 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dryrun
import (
"io/ioutil"
"path/filepath"
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
coreoam "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/dsl/definition"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
oamutil "github.com/oam-dev/kubevela/pkg/oam/util"
)
var cfg *rest.Config
var scheme *runtime.Scheme
var k8sClient client.Client
var testEnv *envtest.Environment
var dm discoverymapper.DiscoveryMapper
var pd *definition.PackageDiscover
var dryrunOpt *Option
var diffOpt *LiveDiffOption
func TestDryRun(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecsWithDefaultAndCustomReporters(t,
"Cli Suite",
[]Reporter{printer.NewlineReporter{}})
}
var _ = BeforeSuite(func(done Done) {
logf.SetLogger(zap.New(zap.UseDevMode(true), zap.WriteTo(GinkgoWriter)))
By("bootstrapping test environment")
useExistCluster := false
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "charts", "vela-core", "crds")},
UseExistingCluster: &useExistCluster,
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
scheme = runtime.NewScheme()
Expect(coreoam.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(clientgoscheme.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(v1beta1.AddToScheme(scheme)).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
dm, err = discoverymapper.New(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(dm).ToNot(BeNil())
pd, err = definition.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
Expect(pd).ToNot(BeNil())
By("Prepare capability definitions")
myingressYAML := readDataFromFile("./testdata/td-myingress.yaml")
myscalerYAML := readDataFromFile("./testdata/td-myscaler.yaml")
myworkerYAML := readDataFromFile("./testdata/cd-myworker.yaml")
myworkerDef, err := oamutil.UnMarshalStringToComponentDefinition(myworkerYAML)
Expect(err).Should(BeNil())
myingressDef, err := oamutil.UnMarshalStringToTraitDefinition(myingressYAML)
Expect(err).Should(BeNil())
myscalerDef, err := oamutil.UnMarshalStringToTraitDefinition(myscalerYAML)
Expect(err).Should(BeNil())
cdMyWorker, err := oamutil.Object2Unstructured(myworkerDef)
Expect(err).Should(BeNil())
tdMyIngress, err := oamutil.Object2Unstructured(myingressDef)
Expect(err).Should(BeNil())
tdMyScaler, err := oamutil.Object2Unstructured(myscalerDef)
Expect(err).Should(BeNil())
dryrunOpt = NewDryRunOption(k8sClient, dm, pd, []oam.Object{cdMyWorker, tdMyIngress, tdMyScaler})
diffOpt = &LiveDiffOption{dryrunOpt}
close(done)
}, 60)
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
func readDataFromFile(path string) string {
b, _ := ioutil.ReadFile(path)
return string(b)
}

View File

@@ -0,0 +1,49 @@
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
name: myworker
spec:
workload:
definition:
apiVersion: apps/v1
kind: Deployment
schematic:
cue:
template: |
import (
apps "kube/apps/v1"
)
output: apps.#Deployment
output: {
spec: {
selector: matchLabels: {
"app.oam.dev/component": context.name
}
template: {
metadata: labels: {
"app.oam.dev/component": context.name
}
spec: {
containers: [{
name: context.name
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
image: string
// +usage=Commands to run in the container
cmd?: [...string]
}

View File

@@ -0,0 +1,269 @@
apiVersion: core.oam.dev/v1beta1
kind: ApplicationRevision
metadata:
annotations:
name: livediff-demo-v1
namespace: default
spec:
application:
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata: {}
spec:
components:
- name: myweb-1
properties:
cmd:
- sleep
- "1000"
enemies: alien
image: busybox
lives: "3"
traits:
- properties:
domain: www.example.com
http:
/: 80
type: myingress
- properties:
replicas: 2
type: myscaler
type: myworker
- name: myweb-2
properties:
cmd:
- sleep
- "1000"
enemies: alien
image: busybox
lives: "3"
type: myworker
status:
batchRollingState: ""
currentBatch: 0
rollingState: ""
upgradedReadyReplicas: 0
upgradedReplicas: 0
applicationConfiguration:
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"core.oam.dev/v1beta1","kind":"Application","metadata":{"annotations":{},"name":"livediff-demo","namespace":"default"},"spec":{"components":[{"name":"myweb-1","properties":{"cmd":["sleep","1000"],"enemies":"alien","image":"busybox","lives":"3"},"traits":[{"properties":{"domain":"www.example.com","http":{"/":80}},"type":"myingress"},{"properties":{"replicas":2},"type":"myscaler"}],"type":"myworker"},{"name":"myweb-2","properties":{"cmd":["sleep","1000"],"enemies":"alien","image":"busybox","lives":"3"},"type":"myworker"}]}}
labels:
app.oam.dev/name: livediff-demo
name: livediff-demo
namespace: default
ownerReferences:
- apiVersion: core.oam.dev/v1beta1
controller: true
kind: Application
name: livediff-demo
uid: afdaa0dc-b226-4045-a255-2c282f258481
spec:
components:
- revisionName: myweb-1-v1
traits:
- trait:
apiVersion: v1
kind: Service
metadata:
labels:
app.oam.dev/appRevision: livediff-demo-v1
app.oam.dev/component: myweb-1
app.oam.dev/name: livediff-demo
trait.oam.dev/resource: service
trait.oam.dev/type: myingress
name: myweb-1
spec:
ports:
- port: 80
targetPort: 80
selector:
app.oam.dev/component: myweb-1
- trait:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
labels:
app.oam.dev/appRevision: livediff-demo-v1
app.oam.dev/component: myweb-1
app.oam.dev/name: livediff-demo
trait.oam.dev/resource: ingress
trait.oam.dev/type: myingress
name: myweb-1
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: myweb-1
servicePort: 80
path: /
- trait:
apiVersion: core.oam.dev/v1alpha2
kind: ManualScalerTrait
metadata:
labels:
app.oam.dev/appRevision: livediff-demo-v1
app.oam.dev/component: myweb-1
app.oam.dev/name: livediff-demo
trait.oam.dev/resource: scaler
trait.oam.dev/type: myscaler
spec:
replicaCount: 2
- revisionName: myweb-2-v1
status:
dependency: {}
observedGeneration: 0
componentDefinitions:
myworker:
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata: {}
spec:
schematic:
cue:
template: "import (\n apps \"kube/apps/v1\"\n)\noutput: apps.#Deployment\noutput:
{\n\tspec: {\n\t\tselector: matchLabels: {\n\t\t\t\"app.oam.dev/component\":
context.name\n\t\t}\n\n\t\ttemplate: {\n\t\t\tmetadata: labels: {\n\t\t\t\t\"app.oam.dev/component\":
context.name\n\t\t\t}\n\n\t\t\tspec: {\n\t\t\t\tcontainers: [{\n\t\t\t\t\tname:
\ context.name\n\t\t\t\t\timage: parameter.image\n\n\t\t\t\t\tif parameter[\"cmd\"]
!= _|_ {\n\t\t\t\t\t\tcommand: parameter.cmd\n\t\t\t\t\t}\n\t\t\t\t}]\n\t\t\t}\n\t\t}\n\t}\n}\n\nparameter:
{\n\t// +usage=Which image would you like to use for your service\n\t//
+short=i\n\timage: string\n\t// +usage=Commands to run in the container\n\tcmd?:
[...string]\n}\n"
workload:
definition:
apiVersion: apps/v1
kind: Deployment
status: {}
components:
- raw:
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
labels:
app.oam.dev/name: livediff-demo
name: myweb-1
namespace: default
ownerReferences:
- apiVersion: core.oam.dev/v1beta1
controller: true
kind: Application
name: livediff-demo
uid: afdaa0dc-b226-4045-a255-2c282f258481
spec:
workload:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.oam.dev/appRevision: livediff-demo-v1
app.oam.dev/component: myweb-1
app.oam.dev/name: livediff-demo
workload.oam.dev/type: myworker
spec:
selector:
matchLabels:
app.oam.dev/component: myweb-1
template:
metadata:
labels:
app.oam.dev/component: myweb-1
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb-1
status:
observedGeneration: 0
- raw:
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
labels:
app.oam.dev/name: livediff-demo
name: myweb-2
namespace: default
ownerReferences:
- apiVersion: core.oam.dev/v1beta1
controller: true
kind: Application
name: livediff-demo
uid: afdaa0dc-b226-4045-a255-2c282f258481
spec:
workload:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.oam.dev/appRevision: livediff-demo-v1
app.oam.dev/component: myweb-2
app.oam.dev/name: livediff-demo
workload.oam.dev/type: myworker
spec:
selector:
matchLabels:
app.oam.dev/component: myweb-2
template:
metadata:
labels:
app.oam.dev/component: myweb-2
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb-2
status:
observedGeneration: 0
traitDefinitions:
myingress:
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata: {}
spec:
appliesToWorkloads:
- '*'
definitionRef:
name: ""
schematic:
cue:
template: "import (\n\tkubev1 \"kube/v1\"\n\tnetwork \"kube/networking.k8s.io/v1beta1\"\n)\n\nparameter:
{\n\tdomain: string\n\thttp: [string]: int\n}\n\noutputs: {\nservice:
kubev1.#Service\ningress: network.#Ingress\n}\n\n// trait template can
have multiple outputs in one trait\noutputs: service: {\n\tmetadata:\n\t\tname:
context.name\n\tspec: {\n\t\tselector:\n\t\t\t\"app.oam.dev/component\":
context.name\n\t\tports: [\n\t\t\tfor k, v in parameter.http {\n\t\t\t\tport:
\ v\n\t\t\t\ttargetPort: v\n\t\t\t},\n\t\t]\n\t}\n}\n\noutputs:
ingress: {\n\tmetadata:\n\t\tname: context.name\n\tspec: {\n\t\trules:
[{\n\t\t\thost: parameter.domain\n\t\t\thttp: {\n\t\t\t\tpaths: [\n\t\t\t\t\tfor
k, v in parameter.http {\n\t\t\t\t\t\tpath: k\n\t\t\t\t\t\tbackend:
{\n\t\t\t\t\t\t\tserviceName: context.name\n\t\t\t\t\t\t\tservicePort:
v\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t]\n\t\t\t}\n\t\t}]\n\t}\n}\n"
status: {}
myscaler:
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata: {}
spec:
appliesToWorkloads:
- webservice
- worker
definitionRef:
name: manualscalertraits.core.oam.dev
schematic:
cue:
template: "outputs: scaler: {\n\tapiVersion: \"core.oam.dev/v1alpha2\"\n\tkind:
\ \"ManualScalerTrait\"\n\tspec: {\n\t\treplicaCount: parameter.replicas\n\t}\n}\nparameter:
{\n\t//+short=r\n\t//+usage=Replicas of the workload\n\treplicas: *1
| int\n}\n"
workloadRefPath: spec.workloadRef
status: {}

View File

@@ -0,0 +1,55 @@
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
- "2000" # change a component property
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90 # change a trait
# - type: myscaler # remove a trait
# properties:
# replicas: 2
- name: myweb-2
type: myworker
properties: # no change on component property
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress # add a trait
properties:
domain: "www.example.com"
http:
"/": 90
- name: myweb-3 # add a component
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90

View File

@@ -0,0 +1,35 @@
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-2
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"

View File

@@ -0,0 +1,56 @@
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-2
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress # add a trait
properties:
domain: "www.example.com"
http:
"/": 90
- name: myweb-3 # add a component
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90

View File

@@ -0,0 +1,34 @@
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
- "2000" # change a component property
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 90 # change a trait
- type: myscaler
properties:
replicas: 2
- name: myweb-2
type: myworker
properties: # no change on component property
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"

View File

@@ -0,0 +1,34 @@
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 # remove a trait
# properties:
# replicas: 2
# - name: myweb-2 # remove a component
# type: myworker
# properties:
# image: "busybox"
# cmd:
# - sleep
# - "1000"
# lives: "3"
# enemies: "alien"

View File

@@ -0,0 +1,22 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: app-dryrun
spec:
components:
- name: myweb
type: myworker
properties:
image: "busybox"
cmd:
- sleep
- "1000"
lives: "3"
enemies: "alien"
traits:
- type: myingress
properties:
domain: "www.example.com"
http:
"/": 80

View File

@@ -0,0 +1,51 @@
apiVersion: core.oam.dev/v1alpha2
kind: ApplicationConfiguration
metadata:
creationTimestamp: null
labels:
app.oam.dev/name: app-dryrun
name: app-dryrun
spec:
components:
- componentName: myweb
traits:
- trait:
apiVersion: v1
kind: Service
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb
app.oam.dev/name: app-dryrun
trait.oam.dev/resource: service
trait.oam.dev/type: myingress
name: myweb
spec:
ports:
- port: 80
targetPort: 80
selector:
app.oam.dev/component: myweb
- trait:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb
app.oam.dev/name: app-dryrun
trait.oam.dev/resource: ingress
trait.oam.dev/type: myingress
name: myweb
spec:
rules:
- host: www.example.com
http:
paths:
- backend:
serviceName: myweb
servicePort: 80
path: /
status:
dependency: {}
observedGeneration: 0

View File

@@ -0,0 +1,34 @@
apiVersion: core.oam.dev/v1alpha2
kind: Component
metadata:
creationTimestamp: null
labels:
app.oam.dev/name: app-dryrun
name: myweb
spec:
workload:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.oam.dev/appRevision: ""
app.oam.dev/component: myweb
app.oam.dev/name: app-dryrun
workload.oam.dev/type: myworker
spec:
selector:
matchLabels:
app.oam.dev/component: myweb
template:
metadata:
labels:
app.oam.dev/component: myweb
spec:
containers:
- command:
- sleep
- "1000"
image: busybox
name: myweb
status:
observedGeneration: 0

View File

@@ -0,0 +1,62 @@
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata:
name: myingress
spec:
appliesToWorkloads:
- "*"
podDisruptive: false
schematic:
cue:
template: |
import (
kubev1 "kube/v1"
network "kube/networking.k8s.io/v1beta1"
)
parameter: {
domain: string
http: [string]: int
}
outputs: {
service: kubev1.#Service
ingress: network.#Ingress
}
// trait template can have multiple outputs in one trait
outputs: service: {
metadata:
name: context.name
spec: {
selector:
"app.oam.dev/component": context.name
ports: [
for k, v in parameter.http {
port: v
targetPort: v
},
]
}
}
outputs: ingress: {
metadata:
name: context.name
spec: {
rules: [{
host: parameter.domain
http: {
paths: [
for k, v in parameter.http {
path: k
backend: {
serviceName: context.name
servicePort: v
}
},
]
}
}]
}
}

View File

@@ -0,0 +1,29 @@
apiVersion: core.oam.dev/v1beta1
kind: TraitDefinition
metadata:
annotations:
definition.oam.dev/description: "Configures replicas for your service."
name: myscaler
spec:
appliesToWorkloads:
- webservice
- worker
definitionRef:
name: manualscalertraits.core.oam.dev
workloadRefPath: spec.workloadRef
schematic:
cue:
template: |
outputs: scaler: {
apiVersion: "core.oam.dev/v1alpha2"
kind: "ManualScalerTrait"
spec: {
replicaCount: parameter.replicas
}
}
parameter: {
//+short=r
//+usage=Replicas of the workload
replicas: *1 | int
}

View File

@@ -32,16 +32,15 @@ import (
"sigs.k8s.io/yaml"
corev1beta1 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/appfile"
"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"
appfile2 "github.com/oam-dev/kubevela/references/appfile"
"github.com/oam-dev/kubevela/references/appfile/dryrun"
)
type dryRunOptions struct {
type dryRunCmdOptions struct {
cmdutil.IOStreams
applicationFile string
definitionFile string
@@ -49,7 +48,7 @@ type dryRunOptions struct {
// NewDryRunCommand creates `dry-run` command
func NewDryRunCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
o := &dryRunOptions{IOStreams: ioStreams}
o := &dryRunCmdOptions{IOStreams: ioStreams}
cmd := &cobra.Command{
Use: "dry-run",
DisableFlagsInUseLine: true,
@@ -69,19 +68,12 @@ func NewDryRunCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command
if err != nil {
return err
}
objs := []oam.Object{}
if o.definitionFile != "" {
objs, err := ReadObjectsFromFile(o.definitionFile)
objs, err = ReadObjectsFromFile(o.definitionFile)
if err != nil {
return err
}
for _, obj := range objs {
if obj.GetNamespace() == "" {
obj.SetNamespace(velaEnv.Namespace)
}
}
if err = appfile2.CreateOrUpdateObjects(context.TODO(), newClient, objs); err != nil {
return err
}
}
pd, err := c.GetPackageDiscover()
if err != nil {
@@ -98,16 +90,9 @@ func NewDryRunCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command
return errors.WithMessagef(err, "read application file: %s", o.applicationFile)
}
parser := appfile.NewApplicationParser(newClient, dm, pd)
dryRunOpt := dryrun.NewDryRunOption(newClient, dm, pd, objs)
ctx := oamutil.SetNamespaceInCtx(context.Background(), velaEnv.Namespace)
appFile, err := parser.GenerateAppFile(ctx, app)
if err != nil {
return errors.WithMessage(err, "generate appFile")
}
ac, comps, err := appFile.GenerateApplicationConfiguration()
ac, comps, err := dryRunOpt.ExecuteDryRun(ctx, app)
if err != nil {
return errors.WithMessage(err, "generate OAM objects")
}
@@ -140,7 +125,7 @@ func NewDryRunCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command
}
cmd.Flags().StringVarP(&o.applicationFile, "file", "f", "./app.yaml", "application file name")
cmd.Flags().StringVarP(&o.definitionFile, "definition", "d", "", "specify a definition file or directory, it will automatically applied to the K8s cluster")
cmd.Flags().StringVarP(&o.definitionFile, "definition", "d", "", "specify a definition file or directory, it will only be used in dry-run rather than applied to K8s cluster")
cmd.SetOut(ioStreams.Out)
return cmd
}

139
references/cli/livediff.go Normal file
View File

@@ -0,0 +1,139 @@
/*
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 (
"bytes"
"context"
"fmt"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"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"
)
type livediffCmdOptions struct {
dryRunCmdOptions
revision string
context int
}
// NewLiveDiffCommand creates `live-diff` command
func NewLiveDiffCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
o := &livediffCmdOptions{
dryRunCmdOptions: dryRunCmdOptions{
IOStreams: ioStreams,
}}
cmd := &cobra.Command{
Use: "live-diff",
DisableFlagsInUseLine: true,
Short: "Dry-run an application, and do diff on a specific app revison",
Long: "Dry-run an application, and do diff on a specific app revison. The provided capability definitions will be used during Dry-run. If any capabilities used in the app are not found in the provided ones, it will try to find from cluster.",
Example: "vela live-diff -f app-v2.yaml -r app-v1 --context 10",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return c.SetConfig()
},
RunE: func(cmd *cobra.Command, args []string) error {
newClient, err := c.GetClient()
if err != nil {
return err
}
velaEnv, err := GetEnv(cmd)
if err != nil {
return err
}
objs := []oam.Object{}
if o.definitionFile != "" {
objs, err = ReadObjectsFromFile(o.definitionFile)
if err != nil {
return err
}
}
pd, err := c.GetPackageDiscover()
if err != nil {
return err
}
dm, err := discoverymapper.New(c.Config)
if err != nil {
return err
}
app, err := readApplicationFromFile(o.applicationFile)
if err != nil {
return errors.WithMessagef(err, "read application file: %s", o.applicationFile)
}
if app.Namespace == "" {
app.SetNamespace(velaEnv.Namespace)
}
appRevision := &v1beta1.ApplicationRevision{}
if o.revision != "" {
// get the revision if user specifies
if err := newClient.Get(context.Background(),
client.ObjectKey{Name: o.revision, Namespace: app.Namespace}, appRevision); err != nil {
return errors.Wrapf(err, "cannot get application revision %q", o.revision)
}
} else {
// get the latest revision of the application
livingApp := &v1beta1.Application{}
if err := newClient.Get(context.Background(),
client.ObjectKey{Name: app.Name, Namespace: app.Namespace}, livingApp); err != nil {
return errors.Wrapf(err, "cannot get application %q", app.Name)
}
if livingApp.Status.LatestRevision != nil {
latestRevName := livingApp.Status.LatestRevision.Name
if err := newClient.Get(context.Background(),
client.ObjectKey{Name: latestRevName, Namespace: app.Namespace}, appRevision); err != nil {
return errors.Wrapf(err, "cannot get application revision %q", o.revision)
}
} else {
// .status.latestRevision is nil, that means the app has not
// been rendered yet
return fmt.Errorf("the application %q has no revision in the cluster", app.Name)
}
}
liveDiffOption := dryrun.NewLiveDiffOption(newClient, dm, pd, objs)
diffResult, err := liveDiffOption.Diff(context.Background(), app, appRevision)
if err != nil {
return errors.WithMessage(err, "cannot calculate diff")
}
var buff = bytes.Buffer{}
reportDiffOpt := dryrun.NewReportDiffOption(o.context, &buff)
reportDiffOpt.PrintDiffReport(diffResult)
o.Info(buff.String())
return nil
},
}
cmd.Flags().StringVarP(&o.applicationFile, "file", "f", "./app.yaml", "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().IntVarP(&o.context, "context", "c", -1, "output number lines of context around changes, by default show all unchanged lines")
cmd.SetOut(ioStreams.Out)
return cmd
}

View File

@@ -82,6 +82,7 @@ func SystemCommandGroup(c common.Args, ioStream cmdutil.IOStreams) *cobra.Comman
types.TagCommandType: types.TypeSystem,
},
}
cmd.AddCommand(NewLiveDiffCommand(c, ioStream))
cmd.AddCommand(NewDryRunCommand(c, ioStream))
cmd.AddCommand(NewAdminInfoCommand(ioStream))
cmd.AddCommand(NewCUEPackageCommand(c, ioStream))