mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
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:
@@ -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>
|
||||
|
||||
54
docs/examples/live-diff/app-modified.yaml
Normal file
54
docs/examples/live-diff/app-modified.yaml
Normal 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
|
||||
33
docs/examples/live-diff/app.yaml
Normal file
33
docs/examples/live-diff/app.yaml
Normal 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"
|
||||
62
docs/examples/live-diff/definitions/myingress.yaml
Normal file
62
docs/examples/live-diff/definitions/myingress.yaml
Normal 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
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
28
docs/examples/live-diff/definitions/myscaler.yaml
Normal file
28
docs/examples/live-diff/definitions/myscaler.yaml
Normal 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
|
||||
}
|
||||
49
docs/examples/live-diff/definitions/myworker.yaml
Normal file
49
docs/examples/live-diff/definitions/myworker.yaml
Normal 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
1
go.mod
@@ -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
2
go.sum
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
404
references/appfile/dryrun/diff.go
Normal file
404
references/appfile/dryrun/diff.go
Normal 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
|
||||
}
|
||||
154
references/appfile/dryrun/diff_test.go
Normal file
154
references/appfile/dryrun/diff_test.go
Normal 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"),
|
||||
))
|
||||
})
|
||||
|
||||
})
|
||||
69
references/appfile/dryrun/dryrun.go
Normal file
69
references/appfile/dryrun/dryrun.go
Normal 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
|
||||
}
|
||||
61
references/appfile/dryrun/dryrun_test.go
Normal file
61
references/appfile/dryrun/dryrun_test.go
Normal 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())
|
||||
})
|
||||
})
|
||||
145
references/appfile/dryrun/report.go
Normal file
145
references/appfile/dryrun/report.go
Normal 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)
|
||||
}
|
||||
}
|
||||
120
references/appfile/dryrun/suit_test.go
Normal file
120
references/appfile/dryrun/suit_test.go
Normal 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)
|
||||
}
|
||||
49
references/appfile/dryrun/testdata/cd-myworker.yaml
vendored
Normal file
49
references/appfile/dryrun/testdata/cd-myworker.yaml
vendored
Normal 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]
|
||||
}
|
||||
|
||||
269
references/appfile/dryrun/testdata/diff-apprevision.yaml
vendored
Normal file
269
references/appfile/dryrun/testdata/diff-apprevision.yaml
vendored
Normal 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: {}
|
||||
|
||||
55
references/appfile/dryrun/testdata/diff-input-app-multichanges.yaml
vendored
Normal file
55
references/appfile/dryrun/testdata/diff-input-app-multichanges.yaml
vendored
Normal 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
|
||||
35
references/appfile/dryrun/testdata/diff-input-app-nochange.yaml
vendored
Normal file
35
references/appfile/dryrun/testdata/diff-input-app-nochange.yaml
vendored
Normal 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"
|
||||
|
||||
56
references/appfile/dryrun/testdata/diff-input-app-onlyadd.yaml
vendored
Normal file
56
references/appfile/dryrun/testdata/diff-input-app-onlyadd.yaml
vendored
Normal 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
|
||||
|
||||
34
references/appfile/dryrun/testdata/diff-input-app-onlymodif.yaml
vendored
Normal file
34
references/appfile/dryrun/testdata/diff-input-app-onlymodif.yaml
vendored
Normal 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"
|
||||
34
references/appfile/dryrun/testdata/diff-input-app-onlyremove.yaml
vendored
Normal file
34
references/appfile/dryrun/testdata/diff-input-app-onlyremove.yaml
vendored
Normal 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"
|
||||
22
references/appfile/dryrun/testdata/dryrun-app.yaml
vendored
Normal file
22
references/appfile/dryrun/testdata/dryrun-app.yaml
vendored
Normal 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
|
||||
|
||||
51
references/appfile/dryrun/testdata/dryrun-exp-ac.yaml
vendored
Normal file
51
references/appfile/dryrun/testdata/dryrun-exp-ac.yaml
vendored
Normal 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
|
||||
34
references/appfile/dryrun/testdata/dryrun-exp-comp.yaml
vendored
Normal file
34
references/appfile/dryrun/testdata/dryrun-exp-comp.yaml
vendored
Normal 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
|
||||
62
references/appfile/dryrun/testdata/td-myingress.yaml
vendored
Normal file
62
references/appfile/dryrun/testdata/td-myingress.yaml
vendored
Normal 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
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
29
references/appfile/dryrun/testdata/td-myscaler.yaml
vendored
Normal file
29
references/appfile/dryrun/testdata/td-myscaler.yaml
vendored
Normal 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
|
||||
}
|
||||
|
||||
@@ -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
139
references/cli/livediff.go
Normal 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
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user