Compare commits

...

10 Commits

Author SHA1 Message Date
github-actions[bot]
3250b0003a [Backport release-1.8] Feat: add check prometheus metrics workflowStepDefinition (#5776)
* add metrics

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

add check metrics

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 18bb9ed998)

* add check-metrics definition example

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>

rename example

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 0da4e8fad9)

* small fix

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 55b5e96d8d)

* fix lint

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit 2c81264577)

* add default metrics

Signed-off-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
(cherry picked from commit b440f75fd5)

---------

Co-authored-by: 楚岳 <wangyike.wyk@alibaba-inc.com>
2023-03-30 11:13:24 +08:00
Somefive
024a34585a vela adopt support multi-cluster adoption (#5635) (#5755)
Signed-off-by: Basuotian <basuoluomiu@gmail.com>
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
Co-authored-by: Basuotian <93654253+basuotian@users.noreply.github.com>
2023-03-29 11:51:20 +08:00
github-actions[bot]
460cdbeea6 Chore: stable the version of workflow to 0.5.0 for 1.8 release (#5765)
Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
(cherry picked from commit d6b3aed716)

Co-authored-by: FogDong <dongtianxin.tx@alibaba-inc.com>
2023-03-29 11:50:53 +08:00
github-actions[bot]
21418d2f06 Fix: vela top cannot switch the theme (#5757)
Signed-off-by: howieyuen <howieyuen@outlook.com>
(cherry picked from commit cb1c33bed1)

Co-authored-by: howieyuen <howieyuen@outlook.com>
2023-03-28 15:39:21 +08:00
github-actions[bot]
65c1bea03a [Backport release-1.8] Fix: gateway message is wrong (#5752)
Co-authored-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
2023-03-28 09:30:06 +08:00
github-actions[bot]
17a76cc0e2 Feat: fix flacky test (#5742)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit ca4636030e)

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2023-03-27 14:53:19 +08:00
github-actions[bot]
ae6697b316 Feat: add qps for load test client (#5740)
Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 54bb1af66d)

Co-authored-by: Da Yin <yd219913@alibaba-inc.com>
2023-03-27 10:03:45 +08:00
github-actions[bot]
03f582ad88 [Backport release-1.8] Feat: enhance vela adopt and refactor (#5737)
* Feat: enhance vela adopt and refactor

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit 9e496ebca6)

* Fix: flaky mc test

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
(cherry picked from commit fdff5eb365)

---------

Co-authored-by: Somefive <yd219913@alibaba-inc.com>
2023-03-24 16:24:23 +08:00
github-actions[bot]
75f8209a4c [Backport release-1.8] Feat: add sub-module to Golang SDK (#5731)
* wait to deal with go.mod

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 91f9e49d21)

* seperate def and module modifier

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 8f4ef2f62a)

* fix module import

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit fa02a0f8cd)

* refine code

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 3a56d8c829)

* remove the pointer reference in loop

generalize the language-specific argument parsing

amend tests

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 38b593d6f9)

* remove focused test

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 8aa74df69f)

* fix test

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 6d40d257e1)

* update command usage

Signed-off-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
(cherry picked from commit 78bb040039)

---------

Co-authored-by: Qiaozp <qiaozhongpei.qzp@alibaba-inc.com>
2023-03-24 11:34:16 +08:00
github-actions[bot]
3975fbcda6 [Backport release-1.8] Fix: fix vela-minimal helm chart unrecognised options (#5730)
* fix(vela-minimal): fix unrecognised options

Signed-off-by: florent.madiot.e <florent.madiot.e@thalesdigital.io>
(cherry picked from commit c9394a18a6)

* Fix(vela-minimal): make reviewable

Signed-off-by: florent.madiot.e <florent.madiot.e@thalesdigital.io>
(cherry picked from commit f4f7f96cc8)

---------

Co-authored-by: florent.madiot.e <florent.madiot.e@thalesdigital.io>
2023-03-24 11:33:47 +08:00
29 changed files with 981 additions and 251 deletions

View File

@@ -42,10 +42,10 @@ jobs:
run: make vela-cli
- name: Build SDK
run: bin/vela def gen-api -f vela-templates/definitions/internal/ -o ./kubevela-go-sdk --package=github.com/kubevela-contrib/kubevela-go-sdk
run: bin/vela def gen-api -f vela-templates/definitions/internal/ -o ./kubevela-go-sdk --package=github.com/kubevela-contrib/kubevela-go-sdk --init
- name: Validate SDK
run: |
cd kubevela-go-sdk
go mod tidy
golangci-lint run --timeout 5m ./...
golangci-lint run --timeout 5m -e "exported:" -e "dot-imports" ./...

View File

@@ -0,0 +1,59 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/check-metrics.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
custom.definition.oam.dev/category: Application Delivery
definition.oam.dev/description: Verify application's metrics
labels:
custom.definition.oam.dev/catalog: Delivery
name: check-metrics
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
check: op.#PromCheck & {
query: parameter.query
metricEndpoint: parameter.metricEndpoint
condition: parameter.condition
stepID: context.stepSessionID
duration: parameter.duration
failDuration: parameter.failDuration
}
fail: op.#Steps & {
if check.failed != _|_ {
if check.failed == true {
breakWorkflow: op.#Fail & {
message: check.message
}
}
}
}
wait: op.#ConditionalWait & {
continue: check.result
if check.message != _|_ {
message: check.message
}
}
parameter: {
// +usage=Query is a raw prometheus query to perform
query: string
// +usage=The HTTP address and port of the prometheus server
metricEndpoint?: "http://prometheus-server.o11y-system.svc:9090" | string
// +usage=Condition is an expression which determines if a measurement is considered successful. eg: >=0.95
condition: string
// +usage=Duration defines the duration of time required for this step to be considered successful.
duration?: *"5m" | string
// +usage=FailDuration is the duration of time that, if the check fails, will result in the step being marked as failed.
failDuration?: *"2m" | string
}

View File

@@ -113,19 +113,20 @@ spec:
}
if context.outputs.ingress.status.loadBalancer.ingress != _|_ {
let igs = context.outputs.ingress.status.loadBalancer.ingress
let host = context.outputs.ingress.spec.rules[0].host
if igs[0].ip != _|_ {
if igs[0].host != _|_ {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + igs[0].ip
}
if igs[0].host == _|_ {
if host == _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster with IP: " + igs[0].ip
}
}
if igs[0].ip == _|_ {
if igs[0].host != _|_ {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host
}
if igs[0].host != _|_ {
if host != _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster"
}
}

View File

@@ -65,7 +65,6 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-minimal --
| `controllerArgs.reSyncPeriod` | The period for resync the applications | `5m` |
| `OAMSpecVer` | OAMSpecVer is the oam spec version controller want to setup | `minimal` |
| `disableCaps` | Disable capability | `envbinding,rollout` |
| `applyOnceOnly` | Valid applyOnceOnly values: true/false/on/off/force | `off` |
| `dependCheckWait` | dependCheckWait is the time to wait for ApplicationConfiguration's dependent-resource ready | `30s` |
@@ -97,20 +96,29 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-minimal --
| `healthCheck.port` | KubeVela health check port | `9440` |
### KubeVela controller optimization parameters
| Name | Description | Value |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `featureGates.applyOnce` | if enabled, the apply-once feature will be applied to all applications, no state-keep and no resource data storage in ResourceTracker | `false` |
### MultiCluster parameters
| Name | Description | Value |
| ----------------------------------------------------- | -------------------------------- | -------------------------------- |
| `multicluster.enabled` | Whether to enable multi-cluster | `true` |
| `multicluster.clusterGateway.replicaCount` | ClusterGateway replica count | `1` |
| `multicluster.clusterGateway.port` | ClusterGateway port | `9443` |
| `multicluster.clusterGateway.image.repository` | ClusterGateway image repository | `oamdev/cluster-gateway` |
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.7.0` |
| `multicluster.clusterGateway.image.pullPolicy` | ClusterGateway image pull policy | `IfNotPresent` |
| `multicluster.clusterGateway.resources.limits.cpu` | ClusterGateway cpu limit | `100m` |
| `multicluster.clusterGateway.resources.limits.memory` | ClusterGateway memory limit | `200Mi` |
| `multicluster.clusterGateway.secureTLS.enabled` | Whether to enable secure TLS | `true` |
| `multicluster.clusterGateway.secureTLS.certPath` | Path to the certificate file | `/etc/k8s-cluster-gateway-certs` |
| Name | Description | Value |
| ------------------------------------------------------- | -------------------------------- | -------------------------------- |
| `multicluster.enabled` | Whether to enable multi-cluster | `true` |
| `multicluster.clusterGateway.replicaCount` | ClusterGateway replica count | `1` |
| `multicluster.clusterGateway.port` | ClusterGateway port | `9443` |
| `multicluster.clusterGateway.image.repository` | ClusterGateway image repository | `oamdev/cluster-gateway` |
| `multicluster.clusterGateway.image.tag` | ClusterGateway image tag | `v1.8.0-alpha.3` |
| `multicluster.clusterGateway.image.pullPolicy` | ClusterGateway image pull policy | `IfNotPresent` |
| `multicluster.clusterGateway.resources.requests.cpu` | ClusterGateway cpu request | `50m` |
| `multicluster.clusterGateway.resources.requests.memory` | ClusterGateway memory request | `20Mi` |
| `multicluster.clusterGateway.resources.limits.cpu` | ClusterGateway cpu limit | `500m` |
| `multicluster.clusterGateway.resources.limits.memory` | ClusterGateway memory limit | `200Mi` |
| `multicluster.clusterGateway.secureTLS.enabled` | Whether to enable secure TLS | `true` |
| `multicluster.clusterGateway.secureTLS.certPath` | Path to the certificate file | `/etc/k8s-cluster-gateway-certs` |
### Test parameters

View File

@@ -0,0 +1,59 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/check-metrics.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
custom.definition.oam.dev/category: Application Delivery
definition.oam.dev/description: Verify application's metrics
labels:
custom.definition.oam.dev/catalog: Delivery
name: check-metrics
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
check: op.#PromCheck & {
query: parameter.query
metricEndpoint: parameter.metricEndpoint
condition: parameter.condition
stepID: context.stepSessionID
duration: parameter.duration
failDuration: parameter.failDuration
}
fail: op.#Steps & {
if check.failed != _|_ {
if check.failed == true {
breakWorkflow: op.#Fail & {
message: check.message
}
}
}
}
wait: op.#ConditionalWait & {
continue: check.result
if check.message != _|_ {
message: check.message
}
}
parameter: {
// +usage=Query is a raw prometheus query to perform
query: string
// +usage=The HTTP address and port of the prometheus server
metricEndpoint?: "http://prometheus-server.o11y-system.svc:9090" | string
// +usage=Condition is an expression which determines if a measurement is considered successful. eg: >=0.95
condition: string
// +usage=Duration defines the duration of time required for this step to be considered successful.
duration?: *"5m" | string
// +usage=FailDuration is the duration of time that, if the check fails, will result in the step being marked as failed.
failDuration?: *"2m" | string
}

View File

@@ -113,19 +113,20 @@ spec:
}
if context.outputs.ingress.status.loadBalancer.ingress != _|_ {
let igs = context.outputs.ingress.status.loadBalancer.ingress
let host = context.outputs.ingress.spec.rules[0].host
if igs[0].ip != _|_ {
if igs[0].host != _|_ {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + igs[0].ip
}
if igs[0].host == _|_ {
if host == _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster with IP: " + igs[0].ip
}
}
if igs[0].ip == _|_ {
if igs[0].host != _|_ {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host
}
if igs[0].host != _|_ {
if host != _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster"
}
}

View File

@@ -166,10 +166,8 @@ spec:
- "--use-webhook=true"
- "--webhook-port={{ .Values.webhookService.port }}"
- "--webhook-cert-dir={{ .Values.admissionWebhooks.certificate.mountPath }}"
- "--autogen-workload-definition={{ .Values.admissionWebhooks.autoGenWorkloadDefinition }}"
{{ end }}
- "--health-addr=:{{ .Values.healthCheck.port }}"
- "--apply-once-only={{ .Values.applyOnceOnly }}"
{{ if ne .Values.disableCaps "" }}
- "--disable-caps={{ .Values.disableCaps }}"
{{ end }}
@@ -188,6 +186,7 @@ spec:
- "--max-workflow-step-error-retry-times={{ .Values.workflow.step.errorRetryTimes }}"
- "--feature-gates=EnableSuspendOnFailure={{- .Values.workflow.enableSuspendOnFailure | toString -}}"
- "--feature-gates=AuthenticateApplication={{- .Values.authentication.enabled | toString -}}"
- "--feature-gates=ApplyOnce={{- .Values.featureGates.applyOnce | toString -}}"
{{ if .Values.authentication.enabled }}
{{ if .Values.authentication.withUser }}
- "--authentication-with-user"

View File

@@ -26,9 +26,6 @@ OAMSpecVer: "minimal"
## @param disableCaps Disable capability
disableCaps: "envbinding,rollout"
## @param applyOnceOnly Valid applyOnceOnly values: true/false/on/off/force
applyOnceOnly: "off"
## @param dependCheckWait dependCheckWait is the time to wait for ApplicationConfiguration's dependent-resource ready
dependCheckWait: 30s
@@ -86,6 +83,11 @@ webhookService:
healthCheck:
port: 9440
## @section KubeVela controller optimization parameters
##@param featureGates.applyOnce if enabled, the apply-once feature will be applied to all applications, no state-keep and no resource data storage in ResourceTracker
featureGates:
applyOnce: false
## @section MultiCluster parameters
@@ -95,6 +97,8 @@ healthCheck:
## @param multicluster.clusterGateway.image.repository ClusterGateway image repository
## @param multicluster.clusterGateway.image.tag ClusterGateway image tag
## @param multicluster.clusterGateway.image.pullPolicy ClusterGateway image pull policy
## @param multicluster.clusterGateway.resources.requests.cpu ClusterGateway cpu request
## @param multicluster.clusterGateway.resources.requests.memory ClusterGateway memory request
## @param multicluster.clusterGateway.resources.limits.cpu ClusterGateway cpu limit
## @param multicluster.clusterGateway.resources.limits.memory ClusterGateway memory limit
## @param multicluster.clusterGateway.secureTLS.enabled Whether to enable secure TLS
@@ -106,11 +110,14 @@ multicluster:
port: 9443
image:
repository: oamdev/cluster-gateway
tag: v1.7.0
tag: v1.8.0-alpha.3
pullPolicy: IfNotPresent
resources:
requests:
cpu: 50m
memory: 20Mi
limits:
cpu: 100m
cpu: 500m
memory: 200Mi
secureTLS:
enabled: true

View File

@@ -96,10 +96,10 @@ This command requires `docker` in PATH as we'll run openapi-generator in docker.
```shell
# Initialize the SDK, generate API from all definitions,
vela def gen-api --init -f /path/to/def/dir -o /path/to/sdk --lang go
vela def gen-api --init -f /path/to/def/dir -o /path/to/sdk --language go
# Incrementally generate API from definitions
vela def gen-api -f /path/to/def/dir -o /path/to/sdk --lang go
vela def gen-api -f /path/to/def/dir -o /path/to/sdk --language go
```
## Future work

2
go.mod
View File

@@ -52,7 +52,7 @@ require (
github.com/imdario/mergo v0.3.13
github.com/kubevela/pkg v0.0.0-20230316114047-e2b41b377bac
github.com/kubevela/prism v1.7.0-alpha.1
github.com/kubevela/workflow v0.4.1-0.20230313085319-59e7c1c967fe
github.com/kubevela/workflow v0.5.0
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

6
go.sum
View File

@@ -892,6 +892,7 @@ github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9q
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
@@ -953,8 +954,8 @@ github.com/kubevela/pkg v0.0.0-20230316114047-e2b41b377bac h1:TLQchMx+BRTnHyebDp
github.com/kubevela/pkg v0.0.0-20230316114047-e2b41b377bac/go.mod h1:GilLxt+9L4sU2tLeZAGHga8wiYmjjfPX/Q6JkyuuXSM=
github.com/kubevela/prism v1.7.0-alpha.1 h1:oeZFn1Oy6gxSSFzMTfsWjLOCKaaooMVm1JGNK4j4Mlo=
github.com/kubevela/prism v1.7.0-alpha.1/go.mod h1:AJSDfdA+RkRSnWx3xEcogbmOTpX+l7RSIwqVHxwUtaI=
github.com/kubevela/workflow v0.4.1-0.20230313085319-59e7c1c967fe h1:Ke96MD4rRhXkua9AbuXvcprptkwE8mD/At4AZsL14ls=
github.com/kubevela/workflow v0.4.1-0.20230313085319-59e7c1c967fe/go.mod h1:bTBlr/0s3oaxYDN0qzp33lgYA9ESkpFdPdJjSAym3V4=
github.com/kubevela/workflow v0.5.0 h1:1C3v8q7xuNQRww/pD/uu2ywTX0xOG0jb+rQsM1sjhwU=
github.com/kubevela/workflow v0.5.0/go.mod h1:l1zZpJEJmI/ieI3vM3TTGOfZSPFDl5Ax7MWfbR6It98=
github.com/kylelemons/godebug v0.0.0-20160406211939-eadb3ce320cb/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
@@ -1110,6 +1111,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=

View File

@@ -41,6 +41,10 @@ Set `CLUSTER=3` will inject the `CLUSTER` variable to the app template. You can
> To make multicluster load testing environment, you can set up multiple k3d instances and register them in the control plane.
#### QPS
By default, there is no rate limit for the client. If you want to set the QPS for each worker, you can use `QPS=2`.
### Cleanup
Run `SHARD=3 WORKER=4 BEGIN=0 SIZE=1000 bash cleanup.sh` to delete `app-0` to `app-999` from namespace `load-test-0` to `load-test-2` in 4 threads.
Run `SHARD=3 WORKER=4 BEGIN=0 SIZE=1000 bash cleanup.sh` to delete `app-0` to `app-999` from namespace `load-test-0` to `load-test-2` in 4 threads.

View File

@@ -5,6 +5,7 @@ SIZE=${SIZE:-1000}
WORKER=${WORKER:-6}
VERSION=${VERSION:-1}
CLUSTER=${CLUSTER:-4}
QPS=${QPS:-1}
SHARD=${SHARD:-3}
@@ -12,6 +13,8 @@ TEMPLATE=${TEMPLATE:-"light"}
END=$(expr $BEGIN + $SIZE - 1)
waitTime=$(expr 1000 / $QPS)e-3
run() {
for i in $(seq $1 $3 $2); do
sid=$(expr $i % $SHARD)
@@ -24,6 +27,7 @@ run() {
sed 's/CLUSTER/'$c'/g' | \
kubectl apply -f -
echo "worker $4: apply app $i to $sid"
sleep $waitTime
done
echo "worker $4: done"
}
@@ -32,4 +36,4 @@ for i in $(seq 1 $WORKER); do
run $(expr $BEGIN + $i - 1) $END $WORKER $i &
done
wait
wait

View File

@@ -33,7 +33,7 @@ config() {
git config --global user.name "kubevela-bot"
}
cloneAndClearRepo() {
cloneAndClearCoreAPI() {
echo "git clone"
if [[ -n "$SSH_DEPLOY_KEY" ]]; then
@@ -42,8 +42,13 @@ cloneAndClearRepo() {
git clone --single-branch --depth 1 https://github.com/$VELA_GO_SDK.git kubevela-go-sdk
fi
echo "Clear kubevela-go-sdk pkg/*"
rm -r kubevela-go-sdk/pkg/*
echo "Clear kubevela-go-sdk pkg/apis/common, pkg/apis/component, pkg/apis/policy, pkg/apis/trait, pkg/apis/workflow-step, pkg/apis/utils, pkg/apis/types.go "
rm -rf kubevela-go-sdk/pkg/apis/common
rm -rf kubevela-go-sdk/pkg/apis/component
rm -rf kubevela-go-sdk/pkg/apis/policy
rm -rf kubevela-go-sdk/pkg/apis/trait
rm -rf kubevela-go-sdk/pkg/apis/workflow-step
rm -rf kubevela-go-sdk/pkg/apis/utils
}
updateRepo() {
@@ -79,7 +84,7 @@ syncRepo() {
main() {
config
cloneAndClearRepo
cloneAndClearCoreAPI
updateRepo
syncRepo
}

View File

@@ -4,6 +4,8 @@ go 1.19
require (
github.com/oam-dev/kubevela-core-api v1.5.8
// for main module
github.com/pkg/errors v0.9.1
k8s.io/apimachinery v0.23.6
k8s.io/client-go v0.23.6
@@ -11,6 +13,9 @@ require (
sigs.k8s.io/yaml v1.3.0
)
// for sub-module
// require github.com/kubevela/vela-go-sdk v0.0.0-20230309022604-cd431bb25a9a
require (
cuelang.org/go v0.5.0-alpha.1 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect

View File

@@ -285,14 +285,14 @@ func (a *ApplicationBuilder) Validate() error {
return nil
}
func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) {
func FromK8sObject(app v1beta1.Application) (TypedApplication, error) {
a := &ApplicationBuilder{}
a.Name(app.Name)
a.Namespace(app.Namespace)
a.resourceVersion = app.ResourceVersion
for _, comp := range app.Spec.Components {
c, err := FromComponent(&comp)
c, err := FromComponent(comp)
if err != nil {
return nil, errors.Wrap(err, "convert component from k8s object")
}
@@ -300,7 +300,7 @@ func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) {
}
if app.Spec.Workflow != nil {
for _, step := range app.Spec.Workflow.Steps {
s, err := FromWorkflowStep(&step)
s, err := FromWorkflowStep(step)
if err != nil {
return nil, errors.Wrap(err, "convert workflow step from k8s object")
}
@@ -308,7 +308,7 @@ func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) {
}
}
for _, policy := range app.Spec.Policies {
p, err := FromPolicy(&policy)
p, err := FromPolicy(policy)
if err != nil {
return nil, errors.Wrap(err, "convert policy from k8s object")
}
@@ -317,34 +317,34 @@ func FromK8sObject(app *v1beta1.Application) (TypedApplication, error) {
return a, nil
}
func FromComponent(component *common.ApplicationComponent) (Component, error) {
func FromComponent(component common.ApplicationComponent) (Component, error) {
build, ok := ComponentsBuilders[component.Type]
if !ok {
return nil, errors.Errorf("no component type %s registered", component.Type)
}
return build(*component)
return build(component)
}
func FromWorkflowStep(step *v1beta1.WorkflowStep) (WorkflowStep, error) {
func FromWorkflowStep(step v1beta1.WorkflowStep) (WorkflowStep, error) {
build, ok := WorkflowStepsBuilders[step.Type]
if !ok {
return nil, errors.Errorf("no workflow step type %s registered", step.Type)
}
return build(*step)
return build(step)
}
func FromPolicy(policy *v1beta1.AppPolicy) (Policy, error) {
func FromPolicy(policy v1beta1.AppPolicy) (Policy, error) {
build, ok := PoliciesBuilders[policy.Type]
if !ok {
return nil, errors.Errorf("no policy type %s registered", policy.Type)
}
return build(*policy)
return build(policy)
}
func FromTrait(trait *common.ApplicationTrait) (Trait, error) {
func FromTrait(trait common.ApplicationTrait) (Trait, error) {
build, ok := TraitBuilders[trait.Type]
if !ok {
return nil, errors.Errorf("no trait type %s registered", trait.Type)
}
return build(*trait)
return build(trait)
}

View File

@@ -97,7 +97,7 @@ func (c clientImpl) Get(ctx context.Context, key client.ObjectKey) (apis.TypedAp
if err != nil {
return nil, err
}
return sdkcommon.FromK8sObject(&_app)
return sdkcommon.FromK8sObject(_app)
}
func (c clientImpl) List(ctx context.Context, opts client.ListOption) ([]apis.TypedApplication, error) {
@@ -108,7 +108,7 @@ func (c clientImpl) List(ctx context.Context, opts client.ListOption) ([]apis.Ty
}
var apps []apis.TypedApplication
for _, app := range _appList.Items {
_app, err := sdkcommon.FromK8sObject(&app)
_app, err := sdkcommon.FromK8sObject(app)
if err != nil {
return nil, err
}

View File

@@ -25,6 +25,8 @@ import (
"os/exec"
"path"
"path/filepath"
"reflect"
"runtime"
"runtime/debug"
"strings"
@@ -47,17 +49,31 @@ import (
type byteHandler func([]byte) []byte
var (
defaultAPIDir = map[string]string{
"go": "pkg/apis",
}
// LangArgsRegistry is used to store the argument info
LangArgsRegistry = map[string]map[langArgKey]LangArg{}
)
// GenMeta stores the metadata for generator.
type GenMeta struct {
config *rest.Config
name string
kind string
Output string
Lang string
Package string
Template string
File []string
InitSDK bool
Verbose bool
Output string
APIDirectory string
IsSubModule bool
Lang string
Package string
Template string
File []string
InitSDK bool
Verbose bool
LangArgs LanguageArgs
cuePaths []string
templatePath string
@@ -67,11 +83,70 @@ type GenMeta struct {
// Generator is used to generate SDK code from CUE template for one language.
type Generator struct {
meta *GenMeta
name string
kind string
def definition.Definition
openapiSchema []byte
modifiers []Modifier
// defModifiers are the modifiers for each definition.
defModifiers []Modifier
// moduleModifiers are the modifiers for the whole module. It will be executed after generating all definitions.
moduleModifiers []Modifier
}
// LanguageArgs is used to store the arguments for the language.
type LanguageArgs interface {
Get(key langArgKey) string
Set(key langArgKey, value string)
}
// langArgKey is language argument key.
type langArgKey string
// LangArg is language-specific argument.
type LangArg struct {
Name langArgKey
Desc string
Default string
}
// registerLangArg should be called in init() function of each language.
func registerLangArg(lang string, arg ...LangArg) {
if _, ok := LangArgsRegistry[lang]; !ok {
LangArgsRegistry[lang] = map[langArgKey]LangArg{}
}
for _, a := range arg {
LangArgsRegistry[lang][a.Name] = a
}
}
// NewLanguageArgs parses the language arguments and returns a LanguageArgs.
func NewLanguageArgs(lang string, langArgs []string) (LanguageArgs, error) {
availableArgs := LangArgsRegistry[lang]
res := languageArgs{}
for _, arg := range langArgs {
parts := strings.Split(arg, "=")
if len(parts) != 2 {
return nil, errors.Errorf("argument %s is not in the format of key=value", arg)
}
if _, ok := availableArgs[langArgKey(parts[0])]; !ok {
return nil, errors.Errorf("argument %s is not supported for language %s", parts[0], lang)
}
res.Set(langArgKey(parts[0]), parts[1])
}
for k, v := range availableArgs {
if res.Get(k) == "" {
res.Set(k, v.Default)
}
}
return res, nil
}
type languageArgs map[string]string
func (l languageArgs) Get(key langArgKey) string {
return l[string(key)]
}
func (l languageArgs) Set(key langArgKey, value string) {
l[string(key)] = value
}
// Modifier is used to modify the generated code.
@@ -82,7 +157,7 @@ type Modifier interface {
// Init initializes the generator.
// It will validate the param, analyze the CUE files, read them to memory, mkdir for output.
func (meta *GenMeta) Init(c common.Args) (err error) {
func (meta *GenMeta) Init(c common.Args, langArgs []string) (err error) {
meta.config, err = c.GetConfig()
if err != nil {
klog.Info("No kubeconfig found, skipping")
@@ -95,9 +170,18 @@ func (meta *GenMeta) Init(c common.Args) (err error) {
return fmt.Errorf("language %s is not supported", meta.Lang)
}
// Init arguments
if meta.APIDirectory == "" {
meta.APIDirectory = defaultAPIDir[meta.Lang]
}
meta.LangArgs, err = NewLanguageArgs(meta.Lang, langArgs)
if err != nil {
return err
}
packageFuncs := map[string]byteHandler{
"go": func(b []byte) []byte {
return bytes.ReplaceAll(b, []byte("github.com/kubevela/vela-go-sdk"), []byte(meta.Package))
return bytes.ReplaceAll(b, []byte(PackagePlaceHolder), []byte(meta.Package))
},
}
@@ -137,7 +221,7 @@ func (meta *GenMeta) CreateScaffold() error {
if !meta.InitSDK {
return nil
}
fmt.Println("Flag --init is set, creating scaffold...")
klog.Info("Flag --init is set, creating scaffold...")
langDirPrefix := fmt.Sprintf("%s/%s", ScaffoldDir, meta.Lang)
err := fs.WalkDir(Scaffold, ScaffoldDir, func(_path string, d fs.DirEntry, err error) error {
if err != nil {
@@ -155,7 +239,7 @@ func (meta *GenMeta) CreateScaffold() error {
}
fileContent = meta.packageFunc(fileContent)
fileName := path.Join(meta.Output, strings.TrimPrefix(_path, langDirPrefix))
// go.mod_ is a special file name, it will be renamed to go.mod. Go will exclude directory go.mod located from the build process.
// go.mod_ is a special file name, it will be renamed to go.mod. Go will ignore directory containing go.mod during the build process.
fileName = strings.ReplaceAll(fileName, "go.mod_", "go.mod")
fileDir := path.Dir(fileName)
if err = os.MkdirAll(fileDir, 0750); err != nil {
@@ -236,18 +320,24 @@ func (meta *GenMeta) PrepareGeneratorAndTemplate() error {
// 1. Generate OpenAPI schema from cue files
// 2. Generate code from OpenAPI schema
func (meta *GenMeta) Run() error {
g := NewModifiableGenerator(meta)
if len(meta.cuePaths) == 0 {
return nil
}
APIGenerated := false
for _, cuePath := range meta.cuePaths {
klog.Infof("Generating SDK for %s", cuePath)
g := NewModifiableGenerator(meta)
klog.Infof("Generating API for %s", cuePath)
// nolint:gosec
cueBytes, err := os.ReadFile(cuePath)
if err != nil {
return errors.Wrapf(err, "failed to read %s", cuePath)
}
template, err := g.GetDefinitionValue(cueBytes)
template, defName, defKind, err := g.GetDefinitionValue(cueBytes)
if err != nil {
return err
}
g.meta.SetDefinition(defName, defKind)
err = g.GenOpenAPISchema(template)
if err != nil {
if strings.Contains(err.Error(), "unsupported node string (*ast.Ident)") {
@@ -257,32 +347,51 @@ func (meta *GenMeta) Run() error {
}
return errors.Wrapf(err, "generate OpenAPI schema")
}
err = g.GenerateCode()
if err != nil {
return err
}
APIGenerated = true
}
if !APIGenerated {
return nil
}
for _, m := range g.moduleModifiers {
err := m.Modify()
if err != nil {
return err
}
}
return nil
}
// GetDefinitionValue returns a value.Value from cue bytes
func (g *Generator) GetDefinitionValue(cueBytes []byte) (*value.Value, error) {
// SetDefinition sets definition name and kind
func (meta *GenMeta) SetDefinition(defName, defKind string) {
meta.name = defName
meta.kind = defKind
}
// GetDefinitionValue returns a value.Value definition name, definition kind from cue bytes
func (g *Generator) GetDefinitionValue(cueBytes []byte) (*value.Value, string, string, error) {
g.def = definition.Definition{Unstructured: unstructured.Unstructured{}}
if err := g.def.FromCUEString(string(cueBytes), g.meta.config); err != nil {
return nil, errors.Wrapf(err, "failed to parse CUE")
return nil, "", "", errors.Wrapf(err, "failed to parse CUE")
}
g.name = g.def.GetName()
g.kind = g.def.GetKind()
templateString, _, err := unstructured.NestedString(g.def.Object, definition.DefinitionTemplateKeys...)
if err != nil {
return nil, err
return nil, "", "", err
}
if templateString == "" {
return nil, "", "", errors.New("definition doesn't include cue schematic")
}
template, err := value.NewValue(templateString+velacue.BaseTemplate, nil, "")
if err != nil {
return nil, err
return nil, "", "", err
}
return template, nil
return template, g.def.GetName(), g.def.GetKind(), nil
}
// GenOpenAPISchema generates OpenAPI json schema from cue.Instance
@@ -327,8 +436,8 @@ func (g *Generator) GenOpenAPISchema(val *value.Value) error {
openapiSchema, err := doc.MarshalJSON()
g.openapiSchema = openapiSchema
if g.meta.Verbose {
fmt.Println("OpenAPI schema:")
fmt.Println(string(g.openapiSchema))
klog.Info("OpenAPI schema:")
klog.Info(string(g.openapiSchema))
}
return err
}
@@ -337,13 +446,13 @@ func (g *Generator) completeOpenAPISchema(doc *openapi3.T) {
for key, schema := range doc.Components.Schemas {
switch key {
case "parameter":
spec := g.name + "-spec"
spec := g.meta.name + "-spec"
schema.Value.Title = spec
completeFreeFormSchema(schema)
completeSchema(key, schema)
doc.Components.Schemas[spec] = schema
delete(doc.Components.Schemas, key)
case g.name + "-spec":
case g.meta.name + "-spec":
continue
default:
completeSchema(key, schema)
@@ -353,7 +462,7 @@ func (g *Generator) completeOpenAPISchema(doc *openapi3.T) {
// GenerateCode will call openapi-generator to generate code and modify it
func (g *Generator) GenerateCode() (err error) {
tmpFile, err := os.CreateTemp("", g.name+"-*.json")
tmpFile, err := os.CreateTemp("", g.meta.name+"-*.json")
_, err = tmpFile.Write(g.openapiSchema)
if err != nil {
return errors.Wrap(err, "write openapi schema to temporary file")
@@ -364,11 +473,11 @@ func (g *Generator) GenerateCode() (err error) {
_ = os.Remove(tmpFile.Name())
}
}()
apiDir, err := filepath.Abs(path.Join(g.meta.Output, "pkg", "apis"))
apiDir, err := filepath.Abs(path.Join(g.meta.Output, g.meta.APIDirectory))
if err != nil {
return errors.Wrapf(err, "get absolute path of %s", apiDir)
}
err = os.MkdirAll(path.Join(apiDir, definition.DefinitionKindToType[g.kind]), 0750)
err = os.MkdirAll(path.Join(apiDir, definition.DefinitionKindToType[g.meta.kind]), 0750)
if err != nil {
return errors.Wrapf(err, "create directory %s", apiDir)
}
@@ -384,28 +493,28 @@ func (g *Generator) GenerateCode() (err error) {
"generate",
"-i", "/local/input/"+filepath.Base(tmpFile.Name()),
"-g", g.meta.Lang,
"-o", fmt.Sprintf("/local/output/%s/%s", definition.DefinitionKindToType[g.kind], g.name),
"-o", fmt.Sprintf("/local/output/%s/%s", definition.DefinitionKindToType[g.meta.kind], g.meta.name),
"-t", "/local/template",
"--skip-validate-spec",
"--enable-post-process-file",
"--generate-alias-as-model",
"--inline-schema-name-defaults", "arrayItemSuffix=,mapItemSuffix=",
"--additional-properties", fmt.Sprintf("isGoSubmodule=true,packageName=%s", strings.ReplaceAll(g.name, "-", "_")),
"--additional-properties", fmt.Sprintf("packageName=%s", strings.ReplaceAll(g.meta.name, "-", "_")),
"--global-property", "modelDocs=false,models,supportingFiles=utils.go",
)
if g.meta.Verbose {
fmt.Println(cmd.String())
klog.Info(cmd.String())
}
output, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrap(err, string(output))
}
if g.meta.Verbose {
fmt.Println(string(output))
klog.Info(string(output))
}
// Adjust the generated files and code
for _, m := range g.modifiers {
for _, m := range g.defModifiers {
err := m.Modify()
if err != nil {
return errors.Wrapf(err, "modify fail by %s", m.Name())
@@ -531,20 +640,21 @@ func completeSchemas(schemas openapi3.Schemas) {
// NewModifiableGenerator returns a new Generator with modifiers
func NewModifiableGenerator(meta *GenMeta) *Generator {
g := &Generator{
meta: meta,
modifiers: []Modifier{},
meta: meta,
defModifiers: []Modifier{},
moduleModifiers: []Modifier{},
}
mo := newModifierOnLanguage(meta.Lang, g)
g.modifiers = append(g.modifiers, mo)
appendModifiersByLanguage(g, meta)
return g
}
func newModifierOnLanguage(lang string, generator *Generator) Modifier {
switch lang {
func appendModifiersByLanguage(g *Generator, meta *GenMeta) {
switch meta.Lang {
case "go":
return &GoModifier{g: generator}
g.defModifiers = append(g.defModifiers, &GoDefModifier{GenMeta: meta})
g.moduleModifiers = append(g.moduleModifiers, &GoModuleModifier{GenMeta: meta})
default:
panic("unsupported language: " + lang)
panic(fmt.Sprintf("unsupported language: %s", meta.Lang))
}
}
@@ -607,3 +717,7 @@ func defaultValueMatchOneOfItem(item *openapi3.Schema, defaultValue interface{})
}
return false
}
func fnName(fn interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name()
}

View File

@@ -30,19 +30,28 @@ import (
var _ = Describe("Test Generating SDK", func() {
var err error
outputDir := filepath.Join("testdata", "output")
lang := "go"
meta := GenMeta{
Output: outputDir,
Lang: "go",
Package: "github.com/kubevela/test-gen-sdk",
File: []string{filepath.Join("testdata", "cron-task.cue")},
InitSDK: true,
Output: outputDir,
Lang: lang,
Package: "github.com/kubevela-contrib/kubevela-go-sdk",
APIDirectory: defaultAPIDir[lang],
Verbose: true,
}
var langArgs []string
BeforeEach(func() {
meta.InitSDK = false
meta.File = []string{filepath.Join("testdata", "cron-task.cue")}
meta.cuePaths = []string{}
})
checkDirNotEmpty := func(dir string) {
_, err = os.Stat(dir)
Expect(err).Should(BeNil())
}
genWithMeta := func(meta GenMeta) {
err = meta.Init(common.Args{})
genWithMeta := func() {
err = meta.Init(common.Args{}, langArgs)
Expect(err).Should(BeNil())
err = meta.CreateScaffold()
Expect(err).Should(BeNil())
@@ -52,43 +61,40 @@ var _ = Describe("Test Generating SDK", func() {
Expect(err).Should(BeNil())
}
It("Test generating SDK and init the scaffold", func() {
genWithMeta(meta)
meta.InitSDK = true
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis"))
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "component", "cron-task"))
})
It("Test generating SDK, append apis", func() {
meta.InitSDK = false
meta.File = append(meta.File, "testdata/shared-resource.cue")
genWithMeta(meta)
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "policy", "shared-resource"))
})
It("Test free form parameter {...}", func() {
meta.InitSDK = false
meta.File = []string{"testdata/json-merge-patch.cue"}
meta.Verbose = true
genWithMeta(meta)
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "trait", "json-merge-patch"))
})
It("Test workflow step", func() {
meta.InitSDK = false
meta.File = []string{"testdata/deploy.cue"}
meta.Verbose = true
genWithMeta(meta)
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "deploy"))
})
It("Test step-group", func() {
meta.InitSDK = false
meta.File = []string{"testdata/step-group.cue"}
meta.Verbose = true
genWithMeta(meta)
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "step-group"))
By("check if AddSubStep is generated")
content, err := os.ReadFile(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "step-group", "step_group.go"))
@@ -97,20 +103,27 @@ var _ = Describe("Test Generating SDK", func() {
})
It("Test oneOf", func() {
meta.InitSDK = false
meta.File = []string{"testdata/one_of.cue"}
meta.Verbose = true
genWithMeta(meta)
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "workflow-step", "one_of"))
By("check if ")
})
It("Test known issue: apply-terraform-provider", func() {
meta.InitSDK = false
meta.Verbose = true
meta.File = []string{"testdata/apply-terraform-provider.cue"}
genWithMeta(meta)
genWithMeta()
})
It("Test generate sub-module", func() {
meta.APIDirectory = "pkg/apis/addons/test_addon"
langArgs = []string{
string(mainModuleVersionKey) + "=" + mainModuleVersion.Default,
}
meta.IsSubModule = true
genWithMeta()
checkDirNotEmpty(filepath.Join(outputDir, "pkg", "apis", "addons", "test_addon", "component", "cron-task"))
})
AfterSuite(func() {
@@ -207,3 +220,117 @@ var _ = Describe("FixSchemaWithOneAnyAllOf", func() {
Expect(schema.Value.OneOf[0].Value.Enum).To(Equal([]interface{}{"go", "java", "python", "node", "ruby"}))
})
})
var _ = Describe("TestNewLanguageArgs", func() {
type args struct {
lang string
langArgs []string
}
tests := []struct {
name string
args args
want map[string]string
wantErr bool
}{
{
name: "should create a languageArgs struct with the correct values",
args: args{
lang: "go",
langArgs: []string{"flag1=value1", "flag2=value2"},
},
want: map[string]string{"flag1": "value1", "flag2": "value2"},
wantErr: false,
},
{
name: "should not set a value for an unknown flag",
args: args{
lang: "go",
langArgs: []string{"unknownFlag=value"},
},
want: map[string]string{},
wantErr: true,
},
{
name: "should warn if an argument is not in the key=value format",
args: args{
lang: "go",
langArgs: []string{"invalidArgument"},
},
want: map[string]string{},
wantErr: true,
},
}
for _, tt := range tests {
It(tt.name, func() {
got, err := NewLanguageArgs(tt.args.lang, tt.args.langArgs)
if tt.wantErr {
Expect(err).To(HaveOccurred())
return
}
for k, v := range tt.want {
Expect(got.Get(langArgKey(k))).To(Equal(v))
}
})
}
})
var _ = Describe("getValueType", func() {
type valueTypeTest struct {
input interface{}
expected CUEType
}
tests := []valueTypeTest{
{nil, ""},
{"hello", "string"},
{42, "integer"},
{float32(3.14), "number"},
{3.14159265358979323846, "number"},
{true, "boolean"},
{map[string]interface{}{"key": "value"}, "object"},
{[]interface{}{1, 2, 3}, "array"},
}
for _, tt := range tests {
tt := tt // capture range variable
It("should return the correct CUEType for the input", func() {
Expect(getValueType(tt.input)).To(Equal(tt.expected))
})
}
})
var _ = Describe("type fit", func() {
var schema *openapi3.Schema
BeforeEach(func() {
schema = &openapi3.Schema{Type: "string"}
})
var testCases = []struct {
name string
cueType CUEType
expectedFit bool
schemaType string
}{
{"string can be oas string", CUEType("string"), true, "string"},
{"string not oas integer", CUEType("string"), false, "integer"},
{"integer can be oas integer", CUEType("integer"), true, "integer"},
{"integer can be oas number", CUEType("integer"), true, "number"},
{"number can be oas number", CUEType("number"), true, "number"},
{"number not oas integer", CUEType("number"), false, "integer"},
{"boolean can be oas boolean", CUEType("boolean"), true, "boolean"},
{"array can be oas array", CUEType("array"), true, "array"},
{"invalid type and any schema", CUEType(""), false, "anyschema"},
}
It("should return whether the CUEType fits the schema type or not", func() {
for _, tc := range testCases {
schema.Type = tc.schemaType
result := tc.cueType.fit(schema)
Expect(result).To(Equal(tc.expectedFit), tc.name)
}
})
})

View File

@@ -23,11 +23,10 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strings"
// we need dot import here to make the complex go code generating simpler
// nolint:revive
j "github.com/dave/jennifer/jen"
"github.com/ettle/strcase"
"github.com/pkg/errors"
@@ -36,6 +35,32 @@ import (
pkgdef "github.com/oam-dev/kubevela/pkg/definition"
)
var (
mainModuleVersionKey langArgKey = "MainModuleVersion"
goProxyKey langArgKey = "GoProxy"
mainModuleVersion = LangArg{
Name: mainModuleVersionKey,
Desc: "The version of main module, it will be used in go get command. For example, tag, commit id, branch name",
// default hash of main module. This is a commit hash of kubevela-contrib/kubvela-go-sdk. It will be used in go get command.
Default: "cd431bb25a9a",
}
goProxy = LangArg{
Name: goProxyKey,
Desc: "The proxy for go get/go mod tidy command",
Default: "https://goproxy.cn,direct",
}
)
func init() {
registerLangArg("go", mainModuleVersion, goProxy)
}
const (
// PackagePlaceHolder is the package name placeholder
PackagePlaceHolder = "github.com/kubevela/vela-go-sdk"
)
var (
// DefinitionKindToPascal is the map of definition kind to pascal case
DefinitionKindToPascal = map[string]string{
@@ -60,14 +85,22 @@ var (
}
)
// GoModifier is the Modifier for golang
type GoModifier struct {
g *Generator
// GoDefModifier is the Modifier for golang, modify code for each definition
type GoDefModifier struct {
*GenMeta
*goArgs
defName string
defKind string
verbose bool
defStructPointer *j.Statement
}
// GoModuleModifier is the Modifier for golang, modify code for each module which contains multiple definitions
type GoModuleModifier struct {
*GenMeta
*goArgs
}
type goArgs struct {
apiDir string
defDir string
utilsDir string
// def name of different cases
@@ -77,16 +110,57 @@ type GoModifier struct {
typeVarName string
defStructName string
defFuncReceiver string
defStructPointer *j.Statement
}
func (a *goArgs) init(m *GenMeta) error {
var err error
a.apiDir, err = filepath.Abs(path.Join(m.Output, m.APIDirectory))
if err != nil {
return err
}
a.defDir = path.Join(a.apiDir, pkgdef.DefinitionKindToType[m.kind], m.name)
a.utilsDir = path.Join(m.Output, "pkg", "apis", "utils")
a.nameInSnakeCase = strcase.ToSnake(m.name)
a.nameInPascalCase = strcase.ToPascal(m.name)
a.typeVarName = a.nameInPascalCase + "Type"
a.specNameInPascalCase = a.nameInPascalCase + "Spec"
a.defStructName = strcase.ToGoPascal(m.name + "-" + pkgdef.DefinitionKindToType[m.kind])
a.defFuncReceiver = m.name[:1]
return nil
}
// Modify implements Modifier
func (m *GoModuleModifier) Modify() error {
for _, fn := range []func() error{
m.init,
m.format,
m.addSubGoMod,
m.tidyMainMod,
} {
if err := fn(); err != nil {
return errors.Wrap(err, fnName(fn))
}
}
return nil
}
func (m *GoModuleModifier) init() error {
m.goArgs = &goArgs{}
return m.goArgs.init(m.GenMeta)
}
// Name the name of modifier
func (m *GoModifier) Name() string {
return "GoModifier"
func (m *GoModuleModifier) Name() string {
return "goModuleModifier"
}
// Name the name of modifier
func (m *GoDefModifier) Name() string {
return "GoDefModifier"
}
// Modify the modification of generated code
func (m *GoModifier) Modify() error {
func (m *GoDefModifier) Modify() error {
for _, fn := range []func() error{
m.init,
m.clean,
@@ -95,35 +169,28 @@ func (m *GoModifier) Modify() error {
m.addDefAPI,
m.addValidateTraits,
m.exportMethods,
m.format,
} {
if err := fn(); err != nil {
return err
return errors.Wrap(err, fnName(fn))
}
}
return nil
}
func (m *GoModifier) init() error {
m.defName = m.g.name
m.defKind = m.g.kind
m.verbose = m.g.meta.Verbose
func (m *GoDefModifier) init() error {
m.goArgs = &goArgs{}
err := m.goArgs.init(m.GenMeta)
if err != nil {
return err
}
pkgAPIDir := path.Join(m.g.meta.Output, "pkg", "apis")
m.defDir = path.Join(pkgAPIDir, pkgdef.DefinitionKindToType[m.defKind], m.defName)
m.utilsDir = path.Join(pkgAPIDir, "utils")
m.nameInSnakeCase = strcase.ToSnake(m.defName)
m.nameInPascalCase = strcase.ToPascal(m.defName)
m.typeVarName = m.nameInPascalCase + "Type"
m.specNameInPascalCase = m.nameInPascalCase + "Spec"
m.defStructName = strcase.ToGoPascal(m.defName + "-" + pkgdef.DefinitionKindToType[m.defKind])
m.defStructPointer = j.Op("*").Id(m.defStructName)
m.defFuncReceiver = m.defName[:1]
err := os.MkdirAll(m.utilsDir, 0750)
err = os.MkdirAll(m.utilsDir, 0750)
return err
}
func (m *GoModifier) clean() error {
func (m *GoDefModifier) clean() error {
err := os.RemoveAll(path.Join(m.defDir, ".openapi-generator"))
if err != nil {
return err
@@ -148,17 +215,101 @@ func (m *GoModifier) clean() error {
}
// addSubGoMod will add a go.mod and go.sum in the api directory if user mark that the api is a submodule
func (m *GoModuleModifier) addSubGoMod() error {
if !m.IsSubModule {
return nil
}
files := map[string]string{
"go.mod_": "go.mod",
"go.sum": "go.sum",
}
for src, dst := range files {
srcContent, err := Scaffold.ReadFile(path.Join(ScaffoldDir, "go", src))
if err != nil {
return errors.Wrap(err, "read "+src)
}
subModuleName := strings.TrimSuffix(fmt.Sprintf("%s/%s", m.Package, m.APIDirectory), "/")
srcContent = bytes.ReplaceAll(srcContent, []byte("module "+PackagePlaceHolder), []byte("module "+subModuleName))
srcContent = bytes.ReplaceAll(srcContent, []byte("// require "+PackagePlaceHolder), []byte("require "+m.Package))
err = os.WriteFile(path.Join(m.apiDir, dst), srcContent, 0600)
if err != nil {
return errors.Wrap(err, "write "+dst)
}
}
cmds := make([]*exec.Cmd, 0)
if m.LangArgs.Get(mainModuleVersionKey) != mainModuleVersion.Default {
// nolint:gosec
cmds = append(cmds, exec.Command("docker", "run",
"--rm",
"-v", m.apiDir+":/api",
"-w", "/api",
"golang:1.19-alpine",
"go", "get", fmt.Sprintf("%s@%s", m.Package, m.LangArgs.Get(mainModuleVersionKey)),
))
}
// nolint:gosec
cmds = append(cmds, exec.Command("docker", "run",
"--rm",
"-v", m.apiDir+":/api",
"-w", "/api",
"--env", "GOPROXY="+m.LangArgs.Get(goProxyKey),
"golang:1.19-alpine",
"go", "mod", "tidy",
))
for _, cmd := range cmds {
if m.Verbose {
fmt.Println(cmd.String())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
err := cmd.Run()
if err != nil {
return errors.Wrapf(err, "fail to run command %s", cmd.String())
}
}
return nil
}
// tidyMainMod will run go mod tidy in the main module
func (m *GoModuleModifier) tidyMainMod() error {
if !m.InitSDK {
return nil
}
outDir, err := filepath.Abs(m.GenMeta.Output)
if err != nil {
return err
}
// nolint:gosec
cmd := exec.Command("docker", "run",
"--rm",
"-v", outDir+":/api",
"-w", "/api",
"golang:1.19-alpine",
"go", "mod", "tidy",
)
if m.Verbose {
fmt.Println(cmd.String())
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
return cmd.Run()
}
// read all files in definition directory,
// 1. replace the Nullable* Struct
// 2. replace the package name
func (m *GoModifier) modifyDefs() error {
func (m *GoDefModifier) modifyDefs() error {
changeNullableType := func(b []byte) []byte {
return regexp.MustCompile("Nullable(String|(Float|Int)(32|64)|Bool)").ReplaceAll(b, []byte("utils.Nullable$1"))
}
files, err := os.ReadDir(m.defDir)
defHandleFunc := []byteHandler{
m.g.meta.packageFunc,
m.packageFunc,
changeNullableType,
}
if err != nil {
@@ -180,7 +331,7 @@ func (m *GoModifier) modifyDefs() error {
return nil
}
func (m *GoModifier) moveUtils() error {
func (m *GoDefModifier) moveUtils() error {
// Adjust the generated files and code
err := os.Rename(path.Join(m.defDir, "utils.go"), path.Join(m.utilsDir, "utils.go"))
if err != nil {
@@ -193,7 +344,7 @@ func (m *GoModifier) moveUtils() error {
if err != nil {
return err
}
utilsBytes = bytes.Replace(utilsBytes, []byte(fmt.Sprintf("package %s", strcase.ToSnake(m.defName))), []byte("package utils"), 1)
utilsBytes = bytes.Replace(utilsBytes, []byte(fmt.Sprintf("package %s", strcase.ToSnake(m.name))), []byte("package utils"), 1)
utilsBytes = bytes.ReplaceAll(utilsBytes, []byte("isNil"), []byte("IsNil"))
err = os.WriteFile(utilsFile, utilsBytes, 0600)
if err != nil {
@@ -203,7 +354,7 @@ func (m *GoModifier) moveUtils() error {
}
// addDefAPI will add component/trait/workflowstep/policy Object to the api
func (m *GoModifier) addDefAPI() error {
func (m *GoDefModifier) addDefAPI() error {
file, err := os.OpenFile(path.Join(m.defDir, m.nameInSnakeCase+".go"), os.O_APPEND|os.O_WRONLY, 0600)
if err != nil {
return err
@@ -236,11 +387,11 @@ func (m *GoModifier) addDefAPI() error {
return nil
}
func (m *GoModifier) genCommonFunc() []*j.Statement {
kind := m.defKind
func (m *GoDefModifier) genCommonFunc() []*j.Statement {
kind := m.kind
typeName := j.Id(m.nameInPascalCase + "Type")
typeConst := j.Const().Add(typeName).Op("=").Lit(m.defName)
j.Op("=").Lit(m.defName)
typeConst := j.Const().Add(typeName).Op("=").Lit(m.name)
j.Op("=").Lit(m.name)
defStruct := j.Type().Id(m.defStructName).Struct(
j.Id("Base").Id("apis").Dot(DefinitionKindToBaseType[kind]),
j.Id("Properties").Id(m.specNameInPascalCase),
@@ -328,8 +479,8 @@ func (m *GoModifier) genCommonFunc() []*j.Statement {
return []*j.Statement{typeConst, initFunc, defStruct, defStructConstructor, buildFunc}
}
func (m *GoModifier) genFromFunc() []*j.Statement {
kind := m.g.kind
func (m *GoDefModifier) genFromFunc() []*j.Statement {
kind := m.kind
kindBaseProperties := map[string][]string{
v1beta1.ComponentDefinitionKind: {"Name", "DependsOn", "Inputs", "Outputs"},
v1beta1.WorkflowStepDefinitionKind: {"Name", "DependsOn", "Inputs", "Outputs", "If", "Timeout", "Meta"},
@@ -340,7 +491,7 @@ func (m *GoModifier) genFromFunc() []*j.Statement {
// fromFuncRsv means build from a part of K8s Object (e.g. v1beta1.Application.spec.component[*] to internal presentation (e.g. Component)
// fromFuncRsv will have a function receiver
getSubSteps := func(sub bool) func(g *j.Group) {
if m.defKind != v1beta1.WorkflowStepDefinitionKind || sub {
if m.kind != v1beta1.WorkflowStepDefinitionKind || sub {
return func(g *j.Group) {}
}
return func(g *j.Group) {
@@ -358,7 +509,7 @@ func (m *GoModifier) genFromFunc() []*j.Statement {
}
}
assignSubSteps := func(sub bool) func(g *j.Group) {
if m.defKind != v1beta1.WorkflowStepDefinitionKind || sub {
if m.kind != v1beta1.WorkflowStepDefinitionKind || sub {
return func(g *j.Group) {}
}
return func(g *j.Group) {
@@ -379,7 +530,7 @@ func (m *GoModifier) genFromFunc() []*j.Statement {
BlockFunc(func(g *j.Group) {
if kind == v1beta1.ComponentDefinitionKind {
g.Add(j.For(j.List(j.Id("_"), j.Id("trait")).Op(":=").Range().Id("from").Dot("Traits")).Block(
j.List(j.Id("_t"), j.Err()).Op(":=").Qual("sdkcommon", "FromTrait").Call(j.Op("&").Id("trait")),
j.List(j.Id("_t"), j.Err()).Op(":=").Qual("sdkcommon", "FromTrait").Call(j.Id("trait")),
j.If(j.Err().Op("!=").Nil()).Block(
j.Return(j.Nil(), j.Err()),
),
@@ -425,15 +576,15 @@ func (m *GoModifier) genFromFunc() []*j.Statement {
)
res := []*j.Statement{fromFuncRsv(false), fromFunc}
if m.defKind == v1beta1.WorkflowStepDefinitionKind {
if m.kind == v1beta1.WorkflowStepDefinitionKind {
res = append(res, fromFuncRsv(true), fromSubFunc)
}
return res
}
// genDedicatedFunc generate functions for definition kinds
func (m *GoModifier) genDedicatedFunc() []*j.Statement {
switch m.defKind {
func (m *GoDefModifier) genDedicatedFunc() []*j.Statement {
switch m.kind {
case v1beta1.ComponentDefinitionKind:
setTraitFunc := j.Func().
Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).
@@ -484,14 +635,14 @@ func (m *GoModifier) genDedicatedFunc() []*j.Statement {
return nil
}
func (m *GoModifier) genNameTypeFunc() []*j.Statement {
nameFunc := j.Func().Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).Id(DefinitionKindToPascal[m.defKind] + "Name").Params().String().Block(
func (m *GoDefModifier) genNameTypeFunc() []*j.Statement {
nameFunc := j.Func().Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).Id(DefinitionKindToPascal[m.kind] + "Name").Params().String().Block(
j.Return(j.Id(m.defFuncReceiver).Dot("Base").Dot("Name")),
)
typeFunc := j.Func().Params(j.Id(m.defFuncReceiver).Add(m.defStructPointer)).Id("DefType").Params().String().Block(
j.Return(j.Id(m.typeVarName)),
)
switch m.defKind {
switch m.kind {
case v1beta1.ComponentDefinitionKind, v1beta1.WorkflowStepDefinitionKind, v1beta1.PolicyDefinitionKind:
return []*j.Statement{nameFunc, typeFunc}
case v1beta1.TraitDefinitionKind:
@@ -500,11 +651,11 @@ func (m *GoModifier) genNameTypeFunc() []*j.Statement {
return nil
}
func (m *GoModifier) genUnmarshalFunc() []*j.Statement {
func (m *GoDefModifier) genUnmarshalFunc() []*j.Statement {
return []*j.Statement{j.Null()}
}
func (m *GoModifier) genBaseSetterFunc() []*j.Statement {
func (m *GoDefModifier) genBaseSetterFunc() []*j.Statement {
baseFuncArgs := map[string][]struct {
funcName string
argName string
@@ -533,7 +684,7 @@ func (m *GoModifier) genBaseSetterFunc() []*j.Statement {
},
}
baseFuncs := make([]*j.Statement, 0)
for _, fn := range baseFuncArgs[m.defKind] {
for _, fn := range baseFuncArgs[m.kind] {
if fn.dst == nil {
fn.dst = j.Dot(fn.funcName)
}
@@ -556,8 +707,8 @@ func (m *GoModifier) genBaseSetterFunc() []*j.Statement {
return baseFuncs
}
func (m *GoModifier) genAddSubStepFunc() *j.Statement {
if m.defName != "step-group" || m.defKind != v1beta1.WorkflowStepDefinitionKind {
func (m *GoDefModifier) genAddSubStepFunc() *j.Statement {
if m.name != "step-group" || m.kind != v1beta1.WorkflowStepDefinitionKind {
return j.Null()
}
subList := j.Id(m.defFuncReceiver).Dot("Base").Dot("SubSteps")
@@ -573,7 +724,7 @@ func (m *GoModifier) genAddSubStepFunc() *j.Statement {
}
// exportMethods will export methods from definition spec struct to definition struct
func (m *GoModifier) exportMethods() error {
func (m *GoDefModifier) exportMethods() error {
fileLoc := path.Join(m.defDir, m.nameInSnakeCase+".go")
// nolint:gosec
file, err := os.ReadFile(fileLoc)
@@ -604,8 +755,8 @@ func (m *GoModifier) exportMethods() error {
return os.WriteFile(fileLoc, []byte(fileStr), 0600)
}
func (m *GoModifier) addValidateTraits() error {
if m.defKind != v1beta1.ComponentDefinitionKind {
func (m *GoDefModifier) addValidateTraits() error {
if m.kind != v1beta1.ComponentDefinitionKind {
return nil
}
fileLoc := path.Join(m.defDir, m.nameInSnakeCase+".go")
@@ -632,11 +783,12 @@ func (m *GoModifier) addValidateTraits() error {
return os.WriteFile(fileLoc, []byte(fileStr), 0600)
}
func (m *GoModifier) format() error {
func (m *GoModuleModifier) format() error {
// check if gofmt is installed
// todo (chivalryq): support go mod tidy for sub-module
formatters := []string{"gofmt", "goimports"}
formatterPaths := []string{}
var formatterPaths []string
allFormattersInstalled := true
for _, formatter := range formatters {
p, err := exec.LookPath(formatter)
@@ -648,11 +800,11 @@ func (m *GoModifier) format() error {
}
if allFormattersInstalled {
for _, fmter := range formatterPaths {
if m.verbose {
if m.Verbose {
fmt.Printf("Use %s to format code\n", fmter)
}
// nolint:gosec
cmd := exec.Command(fmter, "-w", m.defDir)
cmd := exec.Command(fmter, "-w", m.apiDir)
output, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrap(err, string(output))
@@ -661,31 +813,31 @@ func (m *GoModifier) format() error {
return nil
}
// fallback to use go lib
if m.verbose {
if m.Verbose {
fmt.Println("At least one of linters is not installed, use go/format lib to format code")
}
files, err := os.ReadDir(m.defDir)
if err != nil {
return errors.Wrap(err, "read dir")
}
for _, f := range files {
if !strings.HasSuffix(f.Name(), ".go") {
continue
}
filePath := path.Join(m.defDir, f.Name())
// nolint:gosec
content, err := os.ReadFile(filePath)
// format all .go files
return filepath.Walk(m.apiDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return errors.Wrapf(err, "read file %s", filePath)
return err
}
if !strings.HasSuffix(path, ".go") {
return nil
}
// nolint:gosec
content, err := os.ReadFile(path)
if err != nil {
return errors.Wrapf(err, "read file %s", path)
}
formatted, err := format.Source(content)
if err != nil {
return errors.Wrapf(err, "format file %s", filePath)
return errors.Wrapf(err, "format file %s", path)
}
err = os.WriteFile(filePath, formatted, 0600)
err = os.WriteFile(path, formatted, 0600)
if err != nil {
return errors.Wrapf(err, "write file %s", filePath)
return errors.Wrapf(err, "write file %s", path)
}
}
return nil
return nil
})
}

View File

@@ -6,6 +6,7 @@ import "list"
metadata: {
name: string
namespace?: string
annotations?: [string]: string
...
}
...
@@ -30,6 +31,12 @@ import "list"
properties?: {...}
}
#WorkflowStep: {
type: string
name: string
properties?: {...}
}
#Application: {
apiVersion: "core.oam.dev/v1beta1"
kind: "Application"
@@ -42,7 +49,9 @@ import "list"
spec: {
components: [...#Component]
policies?: [...#Policy]
workflow?: {...}
workflow?: {
steps: [...#WorkflowStep]
}
}
}
@@ -51,7 +60,7 @@ import "list"
type: *"native" | "helm" | string
appName: string
appNamespace: string
resources: [...#Resource]
resources: [...#Resource]
...
}
@@ -77,8 +86,10 @@ import "list"
_category: *"unknown" | string
for key, kinds in resourceCategoryMap if list.Contains(kinds, r.kind) {
_category: key
}
},
_cluster: r.metadata.annotations["app.oam.dev/cluster"]
}]
_clusters: [ for r in _resources {r._cluster} ]
resourceMap: {
for key, val in resourceCategoryMap {
"\(key)": [ for r in _resources if r._category == key {r}]
@@ -95,56 +106,74 @@ import "list"
comps: [
if len(resourceMap.crd) > 0 {
type: "k8s-objects"
name: "\(appName).crds"
name: "crds"
properties: objects: [ for r in resourceMap.crd {
apiVersion: r.apiVersion
kind: r.kind
metadata: name: r.metadata.name
metadata: annotations: "app.oam.dev/cluster": "\(r._cluster)"
}]
},
for r in resourceMap.ns {
if len(resourceMap.ns) > 0 {
type: "k8s-objects"
name: "\(appName).ns.\(r.metadata.name)"
properties: objects: [{
name: "ns"
properties: objects: [ for r in resourceMap.ns {
apiVersion: r.apiVersion
kind: r.kind
metadata: name: r.metadata.name
metadata: annotations: "app.oam.dev/cluster": "\(r._cluster)"
}]
},
for r in resourceMap.workload + resourceMap.service {
type: "k8s-objects"
name: "\(appName).\(r.kind).\(r.metadata.name)"
name: "\(r.kind).\(r.metadata.name)"
properties: objects: [{
apiVersion: r.apiVersion
kind: r.kind
metadata: name: r.metadata.name
metadata: namespace: r.metadata.namespace
if r.metadata.namespace != _|_ {
metadata: namespace: r.metadata.namespace
}
spec: r.spec
}]
},
for key in ["config", "sa", "operator", "storage"] if len(resourceMap[key]) > 0 {
type: "k8s-objects"
name: "\(appName).\(key)"
properties: objects: [ for r in resourceMap.config {
name: "\(key)"
properties: objects: [ for r in resourceMap[key] {
apiVersion: r.apiVersion
kind: r.kind
metadata: name: r.metadata.name
if r.metadata.namespace != _|_ {
metadata: namespace: r.metadata.namespace
}
metadata: annotations: "app.oam.dev/cluster": "\(r._cluster)"
}]
},
for kind, rs in unknownByKinds {
type: "k8s-objects"
name: "\(appName).\(kind)"
name: "\(kind)"
properties: objects: [ for r in rs {
apiVersion: r.apiVersion
kind: r.kind
metadata: name: r.metadata.name
metadata: annotations: "app.oam.dev/cluster": "\(r._cluster)"
}]
},
]
clusterCompMap: {
for cluster in _clusters {
"\(cluster)": [ for comp in comps if comp.properties.objects[0].metadata.annotations["app.oam.dev/cluster"] == cluster {comp.name} ]
}
}
compClusterMap: {
for comp in comps {
"\(comp.name)": comp.properties.objects[0].metadata.annotations["app.oam.dev/cluster"]
}
}
$returns: #Application & {
metadata: {
name: $args.appName
@@ -152,7 +181,7 @@ import "list"
labels: "app.oam.dev/adopt": $args.type
}
spec: components: comps
spec: policies: [
spec: policies: [
{
type: $args.mode
name: $args.mode
@@ -160,6 +189,11 @@ import "list"
selector: componentNames: [ for comp in spec.components {comp.name}]
}]
},
for cluster, comp in clusterCompMap if (len(clusterCompMap) < 2) && (cluster != "local") {
type: "topology"
name: "topology-" + cluster
properties: clusters: [cluster]
},
if $args.mode == "take-over" {
type: "garbage-collect"
name: "garbage-collect"
@@ -175,5 +209,11 @@ import "list"
selector: resourceTypes: ["CustomResourceDefinition"]
}]
}]
spec: workflow: steps: [for comp, c in compClusterMap if len(clusterCompMap) > 1 {
type: "apply-component"
name: "apply-component-" + comp
properties: component: comp
properties: cluster: c
}]
}
}
}

View File

@@ -26,7 +26,7 @@ import (
"time"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/kubevela/pkg/cue/cuex"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/release"
@@ -50,6 +50,8 @@ import (
"github.com/kubevela/pkg/util/resourcetopology"
velaslices "github.com/kubevela/pkg/util/slices"
"github.com/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@@ -63,14 +65,15 @@ import (
)
const (
adoptTypeNative = "native"
adoptTypeHelm = "helm"
adoptModeReadOnly = v1alpha1.ReadOnlyPolicyType
adoptModeTakeOver = v1alpha1.TakeOverPolicyType
helmDriverEnvKey = "HELM_DRIVER"
defaultHelmDriver = "secret"
adoptCUETempVal = "adopt"
adoptCUETempFunc = "#Adopt"
adoptTypeNative = "native"
adoptTypeHelm = "helm"
adoptModeReadOnly = v1alpha1.ReadOnlyPolicyType
adoptModeTakeOver = v1alpha1.TakeOverPolicyType
helmDriverEnvKey = "HELM_DRIVER"
defaultHelmDriver = "secret"
adoptCUETempVal = "adopt"
adoptCUETempFunc = "#Adopt"
defaultLocalCluster = "local"
)
//go:embed adopt-templates/default.cue
@@ -87,7 +90,8 @@ var (
type resourceRef struct {
schema.GroupVersionKind
apitypes.NamespacedName
Arg string
Cluster string
Arg string
}
// AdoptOptions options for vela adopt command
@@ -151,7 +155,7 @@ func (opt *AdoptOptions) parseResourceRef(f velacmd.Factory, cmd *cobra.Command,
return nil, fmt.Errorf("no mappings found for resource %s: %w", arg, err)
}
mapping := mappings[0]
or := &resourceRef{GroupVersionKind: gvk, Arg: arg}
or := &resourceRef{GroupVersionKind: gvk, Cluster: defaultLocalCluster, Arg: arg}
switch len(parts) {
case 2:
or.Name = parts[1]
@@ -164,14 +168,18 @@ func (opt *AdoptOptions) parseResourceRef(f velacmd.Factory, cmd *cobra.Command,
case 3:
or.Namespace = parts[1]
or.Name = parts[2]
case 4:
or.Cluster = parts[1]
or.Namespace = parts[2]
or.Name = parts[3]
default:
return nil, fmt.Errorf("resource should be like <type>/<name> or <type>/<namespace>/<name>")
return nil, fmt.Errorf("resource should be like <type>/<name> or <type>/<namespace>/<name> or <type>/<cluster>/<namespace>/<name>")
}
return or, nil
}
// Init .
func (opt *AdoptOptions) Init(f velacmd.Factory, cmd *cobra.Command, args []string) error {
func (opt *AdoptOptions) Init(f velacmd.Factory, cmd *cobra.Command, args []string) (err error) {
if opt.All {
if len(args) > 0 {
for _, arg := range args {
@@ -181,7 +189,7 @@ func (opt *AdoptOptions) Init(f velacmd.Factory, cmd *cobra.Command, args []stri
}
opt.AllGVKs = append(opt.AllGVKs, gvk)
apiVersion, kind := gvk.ToAPIVersionAndKind()
fmt.Fprintf(opt.Out, "Adopt all %s/%s resources\n", apiVersion, kind)
_, _ = fmt.Fprintf(opt.Out, "Adopt all %s/%s resources\n", apiVersion, kind)
}
}
if len(opt.AllGVKs) == 0 {
@@ -212,7 +220,10 @@ func (opt *AdoptOptions) Init(f velacmd.Factory, cmd *cobra.Command, args []stri
opt.ResourceTopologyRule = defaultResourceTopologyRule
}
opt.AppNamespace = velacmd.GetNamespace(f, cmd)
opt.AdoptTemplateCUEValue = cuecontext.New().CompileString(fmt.Sprintf("%s\n\n%s: %s", opt.AdoptTemplate, adoptCUETempVal, adoptCUETempFunc))
opt.AdoptTemplateCUEValue, err = cuex.CompileString(cmd.Context(), fmt.Sprintf("%s\n\n%s: %s", opt.AdoptTemplate, adoptCUETempVal, adoptCUETempFunc))
if err != nil {
return fmt.Errorf("failed to compile template: %w", err)
}
switch opt.Type {
case adoptTypeNative:
if opt.Recycle {
@@ -351,7 +362,7 @@ func (opt *AdoptOptions) MultipleRun(f velacmd.Factory, cmd *cobra.Command) erro
}
// Complete autofill fields in opts
func (opt *AdoptOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) error {
func (opt *AdoptOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []string) (err error) {
opt.AppNamespace = velacmd.GetNamespace(f, cmd)
switch opt.Type {
case adoptTypeNative:
@@ -389,21 +400,25 @@ func (opt *AdoptOptions) Complete(f velacmd.Factory, cmd *cobra.Command, args []
default:
}
if opt.AppName != "" {
var ctx = context.Background()
app := &v1beta1.Application{}
err := f.Client().Get(ctx, apitypes.NamespacedName{Namespace: opt.AppNamespace, Name: opt.AppName}, app)
err := f.Client().Get(cmd.Context(), apitypes.NamespacedName{Namespace: opt.AppNamespace, Name: opt.AppName}, app)
if err == nil && app != nil {
if !opt.Yes {
if !opt.Yes && opt.Apply {
userInput := NewUserInput()
confirm := userInput.AskBool("Application '%s' already exists, apply will override the existing app with the adopted one, please confirm [Y/n]: "+opt.AppName, &UserInputOptions{AssumeYes: false})
confirm := userInput.AskBool(
fmt.Sprintf("Application '%s' already exists, apply will override the existing app with the adopted one, please confirm [Y/n]: ", opt.AppName),
&UserInputOptions{AssumeYes: false})
if !confirm {
return nil
}
}
}
}
opt.AdoptTemplateCUEValue = cuecontext.New().CompileString(fmt.Sprintf("%s\n\n%s: %s", opt.AdoptTemplate, adoptCUETempVal, adoptCUETempFunc))
return nil
opt.AdoptTemplateCUEValue, err = cuex.CompileString(cmd.Context(), fmt.Sprintf("%s\n\n%s: %s", opt.AdoptTemplate, adoptCUETempVal, adoptCUETempFunc))
if err != nil {
return fmt.Errorf("failed to compile cue template: %w", err)
}
return err
}
// Validate if opts is valid
@@ -428,9 +443,13 @@ func (opt *AdoptOptions) loadNative(f velacmd.Factory, cmd *cobra.Command) error
for _, ref := range opt.NativeResourceRefs {
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(ref.GroupVersionKind)
if err := f.Client().Get(cmd.Context(), apitypes.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, obj); err != nil {
return fmt.Errorf("failed to get resource for %s: %w", ref.Arg, err)
if err := f.Client().Get(multicluster.WithCluster(cmd.Context(), ref.Cluster), apitypes.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}, obj); err != nil {
return fmt.Errorf("fail to get resource for %s: %w", ref.Arg, err)
}
annos := map[string]string{
oam.LabelAppCluster: ref.Cluster,
}
obj.SetAnnotations(annos)
opt.Resources = append(opt.Resources, obj)
}
return nil
@@ -456,6 +475,10 @@ func (opt *AdoptOptions) loadHelm() error {
klog.Warningf("unable to decode object %s: %s", val, err)
continue
}
annos := map[string]string{
oam.LabelAppCluster: defaultLocalCluster,
}
obj.SetAnnotations(annos)
objs = append(objs, obj)
}
opt.Resources = objs

View File

@@ -1065,19 +1065,22 @@ func NewDefinitionValidateCommand(c common.Args) *cobra.Command {
// NewDefinitionGenAPICommand create the `vela def gen-api` command to help user generate Go code from the definition
func NewDefinitionGenAPICommand(c common.Args) *cobra.Command {
meta := gen_sdk.GenMeta{}
var languageArgs []string
cmd := &cobra.Command{
Use: "gen-api DEFINITION.cue",
Short: "Generate SDK from X-Definition.",
Long: "Generate SDK from X-definition file.\n" +
"* This command leverage openapi-generator project. Therefore demands \"docker\" exist in PATH" +
"* This command leverage openapi-generator project. Therefore demands \"docker\" exist in PATH\n" +
"* Currently, this function is still working in progress and not all formats of parameter in X-definition are supported yet.",
Example: "# Generate SDK for golang with scaffold initialized\n" +
"> vela def gen-api --init --lang go -f /path/to/def -o /path/to/sdk\n" +
"> vela def gen-api --init --language go -f /path/to/def -o /path/to/sdk\n" +
"# Generate incremental definition files to existing sdk directory\n" +
"> vela def gen-api --lang go -f /path/to/def -o /path/to/sdk",
"> vela def gen-api --language go -f /path/to/def -o /path/to/sdk\n" +
"# Generate definitions to a sub-module\n" +
"> vela def gen-api --language go -f /path/to/def -o /path/to/sdk --submodule --api-dir path/relative/to/output --language-args arg1=val1,arg2=val2\n",
RunE: func(cmd *cobra.Command, args []string) error {
err := meta.Init(c)
err := meta.Init(c, languageArgs)
if err != nil {
return err
}
@@ -1097,13 +1100,26 @@ func NewDefinitionGenAPICommand(c common.Args) *cobra.Command {
return nil
},
}
cmd.Flags().StringVarP(&meta.Output, "output", "o", "./apis", "Output directory path")
cmd.Flags().StringVarP(&meta.Package, "package", "p", "github.com/kubevela/vela-go-sdk", "Package name of generated code")
cmd.Flags().StringVarP(&meta.Lang, "lang", "g", "go", "Language to generate code. Valid languages: go")
cmd.Flags().StringVar(&meta.APIDirectory, "api-dir", "", "API directory path to put definition API files, relative to output directory. Default value: go: pkg/apis")
cmd.Flags().BoolVar(&meta.IsSubModule, "submodule", false, "Whether the generated code is a submodule of the project. If set, the directory specified by `api-dir` will be treated as a submodule of the project")
cmd.Flags().StringVarP(&meta.Package, "package", "p", gen_sdk.PackagePlaceHolder, "Package name of generated code")
cmd.Flags().StringVarP(&meta.Lang, "language", "g", "go", "Language to generate code. Valid languages: go")
cmd.Flags().StringVarP(&meta.Template, "template", "t", "", "Template file path, if not specified, the default template will be used")
cmd.Flags().StringSliceVarP(&meta.File, "file", "f", nil, "File name of definitions, can be specified multiple times, or use comma to separate multiple files. If directory specified, all files found recursively in the directory will be used")
cmd.Flags().BoolVar(&meta.InitSDK, "init", false, "Init the whole SDK project, if not set, only the API file will be generated")
cmd.Flags().BoolVarP(&meta.Verbose, "verbose", "v", false, "Print verbose logs")
var langArgsDescStr string
for lang, args := range gen_sdk.LangArgsRegistry {
langArgsDescStr += lang + ": \n"
for key, arg := range args {
langArgsDescStr += fmt.Sprintf("\t%s: %s(default: %s)\n", key, arg.Name, arg.Default)
}
}
cmd.Flags().StringSliceVar(&languageArgs, "language-args", []string{},
fmt.Sprintf("language-specific arguments to pass to the go generator, available options: \n"+langArgsDescStr),
)
return cmd
}

View File

@@ -291,7 +291,7 @@ func makeThemeConfigFileIfNotExist() bool {
if _, err := os.Open(filepath.Clean(themeConfigFilePath)); err != nil {
if os.IsNotExist(err) {
// make file if not exist
_ = os.MkdirAll(filepath.Clean(velaThemeHome), 0600)
_ = os.MkdirAll(filepath.Clean(velaThemeHome), 0700)
_ = os.WriteFile(filepath.Clean(themeConfigFilePath), []byte("name : "+DefaultTheme), 0600)
}
return false

View File

@@ -17,6 +17,8 @@ limitations under the License.
package config
import (
"os"
"strings"
"testing"
"github.com/gdamore/tcell/v2"
@@ -35,3 +37,11 @@ func TestColor(t *testing.T) {
c2 := Color("red")
assert.Equal(t, c2.Color(), tcell.GetColor("red").TrueColor())
}
func TestPersistentThemeConfig(t *testing.T) {
defer PersistentThemeConfig(DefaultTheme)
PersistentThemeConfig("foo")
bytes, err := os.ReadFile(themeConfigFilePath)
assert.Nil(t, err)
assert.True(t, strings.Contains(string(bytes), "foo"))
}

View File

@@ -0,0 +1,35 @@
```yaml
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: canary-demo
annotations:
app.oam.dev/publishVersion: v2
spec:
components:
- name: canary-demo
type: webservice
properties:
image: wangyikewyk/canarydemo:v2
ports:
- port: 8090
traits:
- type: scaler
properties:
replicas: 5
- type: gateway
properties:
domain: canary-demo.com
http:
"/version": 8090
workflow:
steps:
- name: 200-status-percent-2-phase
type: check-metrics
timeout: 3m
properties:
query: sum(irate(nginx_ingress_controller_requests{host="canary-demo.com",status="200"}[5m]))/sum(irate(nginx_ingress_controller_requests{host="canary-demo.com"}[2m]))
promAddress: "http://prometheus-server.o11y-system.svc:9090"
condition: ">=0.95"
duration: 2m
```

View File

@@ -711,7 +711,9 @@ var _ = Describe("Test multicluster scenario", func() {
app := &v1beta1.Application{}
Expect(yaml.Unmarshal(bs, app)).Should(Succeed())
app.SetNamespace(testNamespace)
Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
Eventually(func(g Gomega) { // informer may have latency for the added definition
g.Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
}).WithTimeout(10 * time.Second).WithPolling(2 * time.Second).Should(Succeed())
key := client.ObjectKeyFromObject(app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, key, app)).Should(Succeed())
@@ -967,7 +969,9 @@ var _ = Describe("Test multicluster scenario", func() {
}},
}}},
}
Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
Eventually(func(g Gomega) { // in case the trait definition has not been watched by vela-core
g.Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
}).WithTimeout(10 * time.Second).WithPolling(2 * time.Second).Should(Succeed())
appKey := client.ObjectKeyFromObject(app)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(hubCtx, appKey, app)).Should(Succeed())

View File

@@ -14,19 +14,20 @@ gateway: {
}
if context.outputs.ingress.status.loadBalancer.ingress != _|_ {
let igs = context.outputs.ingress.status.loadBalancer.ingress
let host = context.outputs.ingress.spec.rules[0].host
if igs[0].ip != _|_ {
if igs[0].host != _|_ {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host + ", IP: " + igs[0].ip
}
if igs[0].host == _|_ {
if host == _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster with IP: " + igs[0].ip
}
}
if igs[0].ip == _|_ {
if igs[0].host != _|_ {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host
}
if igs[0].host != _|_ {
if host != _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster"
}
}

View File

@@ -0,0 +1,54 @@
import (
"vela/op"
)
"check-metrics": {
type: "workflow-step"
labels: {
"catalog": "Delivery"
}
annotations: {
"category": "Application Delivery"
}
description: "Verify application's metrics"
}
template: {
check: op.#PromCheck & {
query: parameter.query
metricEndpoint: parameter.metricEndpoint
condition: parameter.condition
stepID: context.stepSessionID
duration: parameter.duration
failDuration: parameter.failDuration
}
fail: op.#Steps & {
if check.failed != _|_ {
if check.failed == true {
breakWorkflow: op.#Fail & {
message: check.message
}
}
}
}
wait: op.#ConditionalWait & {
continue: check.result
if check.message != _|_ {
message: check.message
}
}
parameter: {
// +usage=Query is a raw prometheus query to perform
query: string
// +usage=The HTTP address and port of the prometheus server
metricEndpoint?: "http://prometheus-server.o11y-system.svc:9090" | string
// +usage=Condition is an expression which determines if a measurement is considered successful. eg: >=0.95
condition: string
// +usage=Duration defines the duration of time required for this step to be considered successful.
duration?: *"5m" | string
// +usage=FailDuration is the duration of time that, if the check fails, will result in the step being marked as failed.
failDuration?: *"2m" | string
}
}