Chore: fix definition parse logic and allow if/for comprehension & use op.#Suspend for deploy (#5743)

* Chore: fix definition parse logic and allow if/for comprehension & use op.#Suspend for deploy

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: flaky mc test

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

* Fix: flaky mc test

Signed-off-by: Somefive <yd219913@alibaba-inc.com>

---------

Signed-off-by: Somefive <yd219913@alibaba-inc.com>
This commit is contained in:
Somefive
2023-03-28 15:35:15 +08:00
committed by GitHub
parent 8619f1c413
commit 542b32bcf4
94 changed files with 237 additions and 95 deletions

View File

@@ -113,6 +113,7 @@ spec:
}]
}
}
#labelSelector: {
matchLabels?: [string]: string
matchExpressions?: [...{
@@ -121,21 +122,25 @@ spec:
values?: [...string]
}]
}
#podAffinityTerm: {
labelSelector?: #labelSelector
namespaces?: [...string]
topologyKey: string
namespaceSelector?: #labelSelector
}
#nodeSelecor: {
key: string
operator: *"In" | "NotIn" | "Exists" | "DoesNotExist" | "Gt" | "Lt"
values?: [...string]
}
#nodeSelectorTerm: {
matchExpressions?: [...#nodeSelecor]
matchFields?: [...#nodeSelecor]
}
parameter: {
// +usage=Specify the pod affinity scheduling rules
podAffinity?: {

View File

@@ -17,12 +17,14 @@ spec:
// +usage=Specify the path of the resource that allow configuration drift
path: [...string]
}
#ApplyOncePolicyRule: {
// +usage=Specify how to select the targets of the rule
selector?: #ResourcePolicyRuleSelector
// +usage=Specify the strategy for configuring the resource level configuration drift behaviour
strategy: #ApplyOnceStrategy
}
#ResourcePolicyRuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -37,6 +39,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Whether to enable apply-once for the whole application
enable: *false | bool

View File

@@ -24,6 +24,7 @@ spec:
apply: op.#ApplyRemaining & {
parameter
}
parameter: {
// +usage=Declare the name of the component
exceptions?: [...string]

View File

@@ -20,6 +20,7 @@ spec:
labelselector?: {...}
namespace: *context.namespace | string
}
cleanJobs: op.#Delete & {
value: {
apiVersion: "batch/v1"
@@ -39,6 +40,7 @@ spec:
}
}
}
cleanPods: op.#Delete & {
value: {
apiVersion: "v1"

View File

@@ -35,6 +35,7 @@ spec:
}
}
} @step(1)
outputs: {
eps_port_name_filtered: *[] | [...]
if parameter.portName == _|_ {
@@ -69,9 +70,11 @@ spec:
endpoints: eps_port_filtered
}
}
wait: op.#ConditionalWait & {
continue: len(outputs.endpoints) > 0
} @step(2)
value: {
if len(outputs.endpoints) > 0 {
endpoint: outputs.endpoints[0].endpoint
@@ -79,6 +82,7 @@ spec:
url: "\(parameter.protocal)://\(endpoint.host):\(_portStr)"
}
}
parameter: {
// +usage=Specify the name of the application
name?: string

View File

@@ -107,9 +107,11 @@ spec:
}]
}
}
parameter: *#PatchParams | close({
// +usage=Specify the commands for multiple containers
containers: [...#PatchParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -72,9 +72,11 @@ spec:
}]
}
}
parameter: #PatchParams | close({
// +usage=Specify the container image for multiple containers
containers: [...#PatchParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -29,6 +29,7 @@ spec:
targetCPUUtilizationPercentage: parameter.cpuUtil
}
}
parameter: {
// +usage=Specify the minimal number of replicas to which the autoscaler can scale down
min: *1 | int

View File

@@ -147,6 +147,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -276,6 +277,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -66,6 +66,7 @@ spec:
}
},
]
volumesList: [
if parameter.volumeMounts != _|_ && parameter.volumeMounts.pvc != _|_ for v in parameter.volumeMounts.pvc {
{
@@ -114,6 +115,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -126,6 +128,7 @@ spec:
val
},
]
output: {
apiVersion: "apps/v1"
kind: "DaemonSet"
@@ -275,6 +278,7 @@ spec:
}
}
}
exposePorts: [
if parameter.ports != _|_ for v in parameter.ports if v.expose == true {
port: v.port
@@ -287,6 +291,7 @@ spec:
}
},
]
outputs: {
if len(exposePorts) != 0 {
webserviceExpose: {
@@ -301,6 +306,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -469,6 +475,7 @@ spec:
hostnames: [...string]
}]
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -26,6 +26,7 @@ spec:
// context.namespace indicates the name of the app
name: context.name
}
parameter: {
// +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used
policy: *"" | string

View File

@@ -18,6 +18,9 @@ spec:
"vela/op"
)
if parameter.auto == false {
suspend: op.#Suspend & {message: "Waiting approval to the deploy step \"\(context.stepName)\""}
}
deploy: op.#Deploy & {
policies: parameter.policies
parallelism: parameter.parallelism

View File

@@ -27,6 +27,7 @@ spec:
// context.namespace indicates the namespace of the app
namespace: context.namespace
}
parameter: {
// +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used
policy: *"" | string

View File

@@ -41,6 +41,7 @@ spec:
}
}
}
parameter: {
// +usage=Declare the runtime clusters to apply, if empty, all runtime clusters will be used
clusters?: [...string]

View File

@@ -98,9 +98,11 @@ spec:
}]
}
}
parameter: *#PatchParams | close({
// +usage=Specify the environment variables for multiple containers
containers: [...#PatchParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -26,6 +26,7 @@ spec:
disable: *false | bool
}]
}
parameter: envs: [...{
name: string
placement?: {

View File

@@ -38,12 +38,14 @@ spec:
stringData: parameter.data
}
} @step(1)
getPlacements: op.#GetPlacementsFromTopologyPolicies & {
policies: *[] | [...string]
if parameter.topology != _|_ {
policies: [parameter.topology]
}
} @step(2)
} @step(2)
apply: op.#Steps & {
for p in getPlacements.placements {
(p.cluster): op.#Apply & {
@@ -52,6 +54,7 @@ spec:
}
}
} @step(3)
parameter: {
// +usage=Specify the name of the export destination
name?: string

View File

@@ -49,12 +49,14 @@ spec:
ports: [{port: parameter.targetPort}]
}]
}] @step(1)
getPlacements: op.#GetPlacementsFromTopologyPolicies & {
policies: *[] | [...string]
if parameter.topology != _|_ {
policies: [parameter.topology]
}
} @step(2)
} @step(2)
apply: op.#Steps & {
for p in getPlacements.placements {
for o in objects {
@@ -65,6 +67,7 @@ spec:
}
}
} @step(3)
parameter: {
// +usage=Specify the name of the export destination
name?: string

View File

@@ -17,6 +17,7 @@ spec:
// +usage=Specify the strategy for target resource to recycle
strategy: *"onAppUpdate" | "onAppDelete" | "never"
}
#ResourcePolicyRuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -31,6 +32,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=If is set, outdated versioned resourcetracker will not be recycled automatically, outdated resources will be kept until resourcetracker be deleted manually
keepLegacyResource: *false | bool

View File

@@ -30,6 +30,7 @@ spec:
]
}
}
legacyAPI: context.clusterVersion.minor < 19
outputs: ingress: {
if legacyAPI {
@@ -87,6 +88,7 @@ spec:
}]
}
}
parameter: {
// +usage=Specify the domain you want to expose
domain?: string
@@ -126,7 +128,7 @@ spec:
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host
}
if host != _|_ {
if host == _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster"
}
}

View File

@@ -34,11 +34,13 @@ spec:
dbName: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_NAME"])}
username: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_USER"])}
password: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_PASSWORD"])}
env: [
{name: "url", value: "jdbc://" + dbHost.str + ":" + dbPort.str + "/" + dbName.str + "?characterEncoding=utf8&useSSL=false"},
{name: "username", value: username.str},
{name: "password", value: password.str},
]
parameter: {
// +usage=Specify the name of the secret generated by database component
name: string

View File

@@ -29,6 +29,7 @@ spec:
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1"
kind: "Ingress"
@@ -50,6 +51,7 @@ spec:
]
}]
}
parameter: {
// +usage=Specify the domain you want to expose
domain: string

View File

@@ -29,6 +29,7 @@ spec:
]
}
}
outputs: ingress: {
apiVersion: "networking.k8s.io/v1beta1"
kind: "Ingress"
@@ -46,6 +47,7 @@ spec:
]
}]
}
parameter: {
// +usage=Specify the domain you want to expose
domain: string

View File

@@ -12,6 +12,7 @@ spec:
cue:
template: |
output: parameter.objects[0]
outputs: {
for i, v in parameter.objects {
if i > 0 {

View File

@@ -38,6 +38,7 @@ spec:
type: "ClusterIP"
}
}
patch: metadata: annotations: {
"dev.nocalhost/application-name": context.appName
"dev.nocalhost/application-namespace": context.namespace

View File

@@ -159,6 +159,7 @@ spec:
}
}
}
block: {
type: string
block_id?: string
@@ -192,18 +193,21 @@ spec:
initial_time?: string
}]
}
textType: {
type: string
text: string
emoji?: bool
verbatim?: bool
}
option: {
text: textType
value: string
description?: textType
url?: string
}
// send webhook notification
ding: op.#Steps & {
if parameter.dingding != _|_ {
@@ -233,6 +237,7 @@ spec:
}
}
}
lark: op.#Steps & {
if parameter.lark != _|_ {
if parameter.lark.url.value != _|_ {
@@ -261,6 +266,7 @@ spec:
}
}
}
slack: op.#Steps & {
if parameter.slack != _|_ {
if parameter.slack.url.value != _|_ {
@@ -289,6 +295,7 @@ spec:
}
}
}
email: op.#Steps & {
if parameter.email != _|_ {
if parameter.email.from.password.value != _|_ {

View File

@@ -28,6 +28,7 @@ spec:
disable: *false | bool
}]
}
parameter: {
// +usage=Specify the overridden component configuration.
components: [...#PatchParams]

View File

@@ -17,6 +17,7 @@ spec:
)
parameter: message: string
msg: op.#Message & {
message: parameter.message
}

View File

@@ -15,6 +15,7 @@ spec:
// +usage=Specify how to select the targets of the rule
selector: #RuleSelector
}
#RuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -29,6 +30,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Specify the list of rules to control read only strategy at resource level.
// The selected resource will be read-only to the current application. If the target resource does

View File

@@ -28,12 +28,14 @@ spec:
labelSelector?: [string]: string
...
}
output: {
if len(parameter.objects) > 0 {
parameter.objects[0]
}
...
}
outputs: {
for i, v in parameter.objects {
if i > 0 {

View File

@@ -51,6 +51,7 @@ spec:
}
}]
}
parameter: {
// +usage=Specify the amount of cpu for requests and limits
cpu?: *1 | number | string

View File

@@ -41,6 +41,7 @@ spec:
}
// +patchStrategy=retainKeys
patch: spec: template: spec: serviceAccountName: parameter.name
_clusterPrivileges: [ if parameter.privileges != _|_ for p in parameter.privileges if p.scope == "cluster" {p}]
_namespacePrivileges: [ if parameter.privileges != _|_ for p in parameter.privileges if p.scope == "namespace" {p}]
outputs: {

View File

@@ -39,6 +39,7 @@ spec:
]
}]
}
parameter: {
// +usage=The mapping of environment variables to secret
envMappings: [string]: #KeySecret

View File

@@ -27,6 +27,7 @@ spec:
// context.namespace indicates the name of the app
name: context.name
}
parameter: {
// +usage=Declare the location to bind
placements: [...{

View File

@@ -15,6 +15,7 @@ spec:
// +usage=Specify how to select the targets of the rule
selector: [...#ResourcePolicyRuleSelector]
}
#ResourcePolicyRuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -29,6 +30,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Specify the list of rules to control shared-resource strategy at resource level.
// The selected resource will be sharable across applications. (That means multiple applications

View File

@@ -103,6 +103,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -116,6 +116,7 @@ spec:
}
}
}
patch: spec: template: spec: {
if parameter.probes == _|_ {
// +patchKey=name
@@ -160,9 +161,11 @@ spec:
}]
}
}
parameter: *#StartupProbeParams | close({
// +usage=Specify the startup probe for multiple containers
probes: [...#StartupProbeParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -61,6 +61,7 @@ spec:
}
},
]
volumeMountsList: [
if parameter.pvc != _|_ for v in parameter.pvc {
if v.volumeMode == "Filesystem" {
@@ -107,6 +108,7 @@ spec:
}
},
]
envList: [
if parameter.configMap != _|_ for v in parameter.configMap if v.mountToEnv != _|_ {
{
@@ -145,6 +147,7 @@ spec:
}
},
]
volumeDevicesList: *[
for v in parameter.pvc if v.volumeMode == "Block" {
{
@@ -156,6 +159,7 @@ spec:
}
},
] | []
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -168,6 +172,7 @@ spec:
val
},
]
patch: spec: template: spec: {
// +patchKey=name
volumes: deDupVolumesArray
@@ -182,6 +187,7 @@ spec:
}, ...]
}
outputs: {
for v in parameter.pvc {
if v.mountOnly == false {
@@ -252,6 +258,7 @@ spec:
}
}
parameter: {
// +usage=Declare pvc type storage
pvc?: [...{

View File

@@ -24,6 +24,7 @@ spec:
message: parameter.message
}
}
parameter: {
// +usage=Specify the wait duration time to resume workflow such as "30s", "1min" or "2m15s"
duration?: string

View File

@@ -15,6 +15,7 @@ spec:
// +usage=Specify how to select the targets of the rule
selector: [...#RuleSelector]
}
#RuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -29,6 +30,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Specify the list of rules to control take over strategy at resource level.
// The selected resource will be able to be taken over by the current application when the resource belongs to no

View File

@@ -113,6 +113,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -209,6 +210,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -33,6 +33,7 @@ spec:
}
},
]
volumesList: [
if parameter.storage != _|_ && parameter.storage.secret != _|_ for v in parameter.storage.secret {
{
@@ -53,6 +54,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -65,6 +67,7 @@ spec:
val
},
]
job: op.#Apply & {
value: {
apiVersion: "batch/v1"
@@ -99,9 +102,11 @@ spec:
}
}
}
log: op.#Log & {
source: resources: [{labelSelector: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"}]
}
fail: op.#Steps & {
if job.value.status.failed != _|_ {
if job.value.status.failed > 2 {
@@ -111,9 +116,11 @@ spec:
}
}
}
wait: op.#ConditionalWait & {
continue: job.value.status.succeeded != _|_ && job.value.status.succeeded > 0
}
parameter: {
// +usage=Specify the name of the addon.
addonName: string

View File

@@ -50,6 +50,7 @@ spec:
},
]
}
parameter: {
// +usage=Declare volumes and volumeMounts
volumes?: [...{

View File

@@ -68,6 +68,7 @@ spec:
} @step(7)
}
}
parameter: {
// +usage=Specify the webhook url
url: close({

View File

@@ -67,6 +67,7 @@ spec:
}
},
]
volumesList: [
if parameter.volumeMounts != _|_ && parameter.volumeMounts.pvc != _|_ for v in parameter.volumeMounts.pvc {
{
@@ -115,6 +116,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -127,6 +129,7 @@ spec:
val
},
]
output: {
apiVersion: "apps/v1"
kind: "Deployment"
@@ -284,6 +287,7 @@ spec:
}
}
}
exposePorts: [
if parameter.ports != _|_ for v in parameter.ports if v.expose == true {
port: v.port
@@ -306,6 +310,7 @@ spec:
}
},
]
outputs: {
if len(exposePorts) != 0 {
webserviceExpose: {
@@ -320,6 +325,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -496,6 +502,7 @@ spec:
hostnames: [...string]
}]
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -64,6 +64,7 @@ spec:
}
},
]
volumesList: [
if parameter.volumeMounts != _|_ && parameter.volumeMounts.pvc != _|_ for v in parameter.volumeMounts.pvc {
{
@@ -112,6 +113,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -124,6 +126,7 @@ spec:
val
},
]
output: {
apiVersion: "apps/v1"
kind: "Deployment"
@@ -234,6 +237,7 @@ spec:
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
@@ -363,6 +367,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -113,6 +113,7 @@ spec:
}]
}
}
#labelSelector: {
matchLabels?: [string]: string
matchExpressions?: [...{
@@ -121,21 +122,25 @@ spec:
values?: [...string]
}]
}
#podAffinityTerm: {
labelSelector?: #labelSelector
namespaces?: [...string]
topologyKey: string
namespaceSelector?: #labelSelector
}
#nodeSelecor: {
key: string
operator: *"In" | "NotIn" | "Exists" | "DoesNotExist" | "Gt" | "Lt"
values?: [...string]
}
#nodeSelectorTerm: {
matchExpressions?: [...#nodeSelecor]
matchFields?: [...#nodeSelecor]
}
parameter: {
// +usage=Specify the pod affinity scheduling rules
podAffinity?: {

View File

@@ -17,12 +17,14 @@ spec:
// +usage=Specify the path of the resource that allow configuration drift
path: [...string]
}
#ApplyOncePolicyRule: {
// +usage=Specify how to select the targets of the rule
selector?: #ResourcePolicyRuleSelector
// +usage=Specify the strategy for configuring the resource level configuration drift behaviour
strategy: #ApplyOnceStrategy
}
#ResourcePolicyRuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -37,6 +39,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Whether to enable apply-once for the whole application
enable: *false | bool

View File

@@ -20,6 +20,7 @@ spec:
labelselector?: {...}
namespace: *context.namespace | string
}
cleanJobs: op.#Delete & {
value: {
apiVersion: "batch/v1"
@@ -39,6 +40,7 @@ spec:
}
}
}
cleanPods: op.#Delete & {
value: {
apiVersion: "v1"

View File

@@ -35,6 +35,7 @@ spec:
}
}
} @step(1)
outputs: {
eps_port_name_filtered: *[] | [...]
if parameter.portName == _|_ {
@@ -69,9 +70,11 @@ spec:
endpoints: eps_port_filtered
}
}
wait: op.#ConditionalWait & {
continue: len(outputs.endpoints) > 0
} @step(2)
value: {
if len(outputs.endpoints) > 0 {
endpoint: outputs.endpoints[0].endpoint
@@ -79,6 +82,7 @@ spec:
url: "\(parameter.protocal)://\(endpoint.host):\(_portStr)"
}
}
parameter: {
// +usage=Specify the name of the application
name?: string

View File

@@ -107,9 +107,11 @@ spec:
}]
}
}
parameter: *#PatchParams | close({
// +usage=Specify the commands for multiple containers
containers: [...#PatchParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -72,9 +72,11 @@ spec:
}]
}
}
parameter: #PatchParams | close({
// +usage=Specify the container image for multiple containers
containers: [...#PatchParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -29,6 +29,7 @@ spec:
targetCPUUtilizationPercentage: parameter.cpuUtil
}
}
parameter: {
// +usage=Specify the minimal number of replicas to which the autoscaler can scale down
min: *1 | int

View File

@@ -147,6 +147,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -276,6 +277,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -66,6 +66,7 @@ spec:
}
},
]
volumesList: [
if parameter.volumeMounts != _|_ && parameter.volumeMounts.pvc != _|_ for v in parameter.volumeMounts.pvc {
{
@@ -114,6 +115,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -126,6 +128,7 @@ spec:
val
},
]
output: {
apiVersion: "apps/v1"
kind: "DaemonSet"
@@ -275,6 +278,7 @@ spec:
}
}
}
exposePorts: [
if parameter.ports != _|_ for v in parameter.ports if v.expose == true {
port: v.port
@@ -287,6 +291,7 @@ spec:
}
},
]
outputs: {
if len(exposePorts) != 0 {
webserviceExpose: {
@@ -301,6 +306,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -469,6 +475,7 @@ spec:
hostnames: [...string]
}]
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -26,6 +26,7 @@ spec:
// context.namespace indicates the name of the app
name: context.name
}
parameter: {
// +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used
policy: *"" | string

View File

@@ -18,6 +18,9 @@ spec:
"vela/op"
)
if parameter.auto == false {
suspend: op.#Suspend & {message: "Waiting approval to the deploy step \"\(context.stepName)\""}
}
deploy: op.#Deploy & {
policies: parameter.policies
parallelism: parameter.parallelism

View File

@@ -98,9 +98,11 @@ spec:
}]
}
}
parameter: *#PatchParams | close({
// +usage=Specify the environment variables for multiple containers
containers: [...#PatchParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -38,12 +38,14 @@ spec:
stringData: parameter.data
}
} @step(1)
getPlacements: op.#GetPlacementsFromTopologyPolicies & {
policies: *[] | [...string]
if parameter.topology != _|_ {
policies: [parameter.topology]
}
} @step(2)
} @step(2)
apply: op.#Steps & {
for p in getPlacements.placements {
(p.cluster): op.#Apply & {
@@ -52,6 +54,7 @@ spec:
}
}
} @step(3)
parameter: {
// +usage=Specify the name of the export destination
name?: string

View File

@@ -49,12 +49,14 @@ spec:
ports: [{port: parameter.targetPort}]
}]
}] @step(1)
getPlacements: op.#GetPlacementsFromTopologyPolicies & {
policies: *[] | [...string]
if parameter.topology != _|_ {
policies: [parameter.topology]
}
} @step(2)
} @step(2)
apply: op.#Steps & {
for p in getPlacements.placements {
for o in objects {
@@ -65,6 +67,7 @@ spec:
}
}
} @step(3)
parameter: {
// +usage=Specify the name of the export destination
name?: string

View File

@@ -17,6 +17,7 @@ spec:
// +usage=Specify the strategy for target resource to recycle
strategy: *"onAppUpdate" | "onAppDelete" | "never"
}
#ResourcePolicyRuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -31,6 +32,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=If is set, outdated versioned resourcetracker will not be recycled automatically, outdated resources will be kept until resourcetracker be deleted manually
keepLegacyResource: *false | bool

View File

@@ -30,6 +30,7 @@ spec:
]
}
}
legacyAPI: context.clusterVersion.minor < 19
outputs: ingress: {
if legacyAPI {
@@ -87,6 +88,7 @@ spec:
}]
}
}
parameter: {
// +usage=Specify the domain you want to expose
domain?: string
@@ -126,7 +128,7 @@ spec:
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host
}
if host != _|_ {
if host == _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster"
}
}

View File

@@ -34,11 +34,13 @@ spec:
dbName: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_NAME"])}
username: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_USER"])}
password: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_PASSWORD"])}
env: [
{name: "url", value: "jdbc://" + dbHost.str + ":" + dbPort.str + "/" + dbName.str + "?characterEncoding=utf8&useSSL=false"},
{name: "username", value: username.str},
{name: "password", value: password.str},
]
parameter: {
// +usage=Specify the name of the secret generated by database component
name: string

View File

@@ -12,6 +12,7 @@ spec:
cue:
template: |
output: parameter.objects[0]
outputs: {
for i, v in parameter.objects {
if i > 0 {

View File

@@ -38,6 +38,7 @@ spec:
type: "ClusterIP"
}
}
patch: metadata: annotations: {
"dev.nocalhost/application-name": context.appName
"dev.nocalhost/application-namespace": context.namespace

View File

@@ -159,6 +159,7 @@ spec:
}
}
}
block: {
type: string
block_id?: string
@@ -192,18 +193,21 @@ spec:
initial_time?: string
}]
}
textType: {
type: string
text: string
emoji?: bool
verbatim?: bool
}
option: {
text: textType
value: string
description?: textType
url?: string
}
// send webhook notification
ding: op.#Steps & {
if parameter.dingding != _|_ {
@@ -233,6 +237,7 @@ spec:
}
}
}
lark: op.#Steps & {
if parameter.lark != _|_ {
if parameter.lark.url.value != _|_ {
@@ -261,6 +266,7 @@ spec:
}
}
}
slack: op.#Steps & {
if parameter.slack != _|_ {
if parameter.slack.url.value != _|_ {
@@ -289,6 +295,7 @@ spec:
}
}
}
email: op.#Steps & {
if parameter.email != _|_ {
if parameter.email.from.password.value != _|_ {

View File

@@ -28,6 +28,7 @@ spec:
disable: *false | bool
}]
}
parameter: {
// +usage=Specify the overridden component configuration.
components: [...#PatchParams]

View File

@@ -17,6 +17,7 @@ spec:
)
parameter: message: string
msg: op.#Message & {
message: parameter.message
}

View File

@@ -15,6 +15,7 @@ spec:
// +usage=Specify how to select the targets of the rule
selector: #RuleSelector
}
#RuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -29,6 +30,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Specify the list of rules to control read only strategy at resource level.
// The selected resource will be read-only to the current application. If the target resource does

View File

@@ -28,12 +28,14 @@ spec:
labelSelector?: [string]: string
...
}
output: {
if len(parameter.objects) > 0 {
parameter.objects[0]
}
...
}
outputs: {
for i, v in parameter.objects {
if i > 0 {

View File

@@ -51,6 +51,7 @@ spec:
}
}]
}
parameter: {
// +usage=Specify the amount of cpu for requests and limits
cpu?: *1 | number | string

View File

@@ -41,6 +41,7 @@ spec:
}
// +patchStrategy=retainKeys
patch: spec: template: spec: serviceAccountName: parameter.name
_clusterPrivileges: [ if parameter.privileges != _|_ for p in parameter.privileges if p.scope == "cluster" {p}]
_namespacePrivileges: [ if parameter.privileges != _|_ for p in parameter.privileges if p.scope == "namespace" {p}]
outputs: {

View File

@@ -39,6 +39,7 @@ spec:
]
}]
}
parameter: {
// +usage=The mapping of environment variables to secret
envMappings: [string]: #KeySecret

View File

@@ -27,6 +27,7 @@ spec:
// context.namespace indicates the name of the app
name: context.name
}
parameter: {
// +usage=Declare the location to bind
placements: [...{

View File

@@ -15,6 +15,7 @@ spec:
// +usage=Specify how to select the targets of the rule
selector: [...#ResourcePolicyRuleSelector]
}
#ResourcePolicyRuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -29,6 +30,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Specify the list of rules to control shared-resource strategy at resource level.
// The selected resource will be sharable across applications. (That means multiple applications

View File

@@ -103,6 +103,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -116,6 +116,7 @@ spec:
}
}
}
patch: spec: template: spec: {
if parameter.probes == _|_ {
// +patchKey=name
@@ -160,9 +161,11 @@ spec:
}]
}
}
parameter: *#StartupProbeParams | close({
// +usage=Specify the startup probe for multiple containers
probes: [...#StartupProbeParams]
})
errs: [ for c in patch.spec.template.spec.containers if c.err != _|_ {c.err}]

View File

@@ -61,6 +61,7 @@ spec:
}
},
]
volumeMountsList: [
if parameter.pvc != _|_ for v in parameter.pvc {
if v.volumeMode == "Filesystem" {
@@ -107,6 +108,7 @@ spec:
}
},
]
envList: [
if parameter.configMap != _|_ for v in parameter.configMap if v.mountToEnv != _|_ {
{
@@ -145,6 +147,7 @@ spec:
}
},
]
volumeDevicesList: *[
for v in parameter.pvc if v.volumeMode == "Block" {
{
@@ -156,6 +159,7 @@ spec:
}
},
] | []
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -168,6 +172,7 @@ spec:
val
},
]
patch: spec: template: spec: {
// +patchKey=name
volumes: deDupVolumesArray
@@ -182,6 +187,7 @@ spec:
}, ...]
}
outputs: {
for v in parameter.pvc {
if v.mountOnly == false {
@@ -252,6 +258,7 @@ spec:
}
}
parameter: {
// +usage=Declare pvc type storage
pvc?: [...{

View File

@@ -24,6 +24,7 @@ spec:
message: parameter.message
}
}
parameter: {
// +usage=Specify the wait duration time to resume workflow such as "30s", "1min" or "2m15s"
duration?: string

View File

@@ -15,6 +15,7 @@ spec:
// +usage=Specify how to select the targets of the rule
selector: [...#RuleSelector]
}
#RuleSelector: {
// +usage=Select resources by component names
componentNames?: [...string]
@@ -29,6 +30,7 @@ spec:
// +usage=Select resources by their names
resourceNames?: [...string]
}
parameter: {
// +usage=Specify the list of rules to control take over strategy at resource level.
// The selected resource will be able to be taken over by the current application when the resource belongs to no

View File

@@ -113,6 +113,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -209,6 +210,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -33,6 +33,7 @@ spec:
}
},
]
volumesList: [
if parameter.storage != _|_ && parameter.storage.secret != _|_ for v in parameter.storage.secret {
{
@@ -53,6 +54,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -65,6 +67,7 @@ spec:
val
},
]
job: op.#Apply & {
value: {
apiVersion: "batch/v1"
@@ -99,9 +102,11 @@ spec:
}
}
}
log: op.#Log & {
source: resources: [{labelSelector: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"}]
}
fail: op.#Steps & {
if job.value.status.failed != _|_ {
if job.value.status.failed > 2 {
@@ -111,9 +116,11 @@ spec:
}
}
}
wait: op.#ConditionalWait & {
continue: job.value.status.succeeded != _|_ && job.value.status.succeeded > 0
}
parameter: {
// +usage=Specify the name of the addon.
addonName: string

View File

@@ -68,6 +68,7 @@ spec:
} @step(7)
}
}
parameter: {
// +usage=Specify the webhook url
url: close({

View File

@@ -67,6 +67,7 @@ spec:
}
},
]
volumesList: [
if parameter.volumeMounts != _|_ && parameter.volumeMounts.pvc != _|_ for v in parameter.volumeMounts.pvc {
{
@@ -115,6 +116,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -127,6 +129,7 @@ spec:
val
},
]
output: {
apiVersion: "apps/v1"
kind: "Deployment"
@@ -284,6 +287,7 @@ spec:
}
}
}
exposePorts: [
if parameter.ports != _|_ for v in parameter.ports if v.expose == true {
port: v.port
@@ -306,6 +310,7 @@ spec:
}
},
]
outputs: {
if len(exposePorts) != 0 {
webserviceExpose: {
@@ -320,6 +325,7 @@ spec:
}
}
}
parameter: {
// +usage=Specify the labels in the workload
labels?: [string]: string
@@ -496,6 +502,7 @@ spec:
hostnames: [...string]
}]
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -64,6 +64,7 @@ spec:
}
},
]
volumesList: [
if parameter.volumeMounts != _|_ && parameter.volumeMounts.pvc != _|_ for v in parameter.volumeMounts.pvc {
{
@@ -112,6 +113,7 @@ spec:
}
},
]
deDupVolumesArray: [
for val in [
for i, vi in volumesList {
@@ -124,6 +126,7 @@ spec:
val
},
]
output: {
apiVersion: "apps/v1"
kind: "Deployment"
@@ -234,6 +237,7 @@ spec:
}
}
}
parameter: {
// +usage=Which image would you like to use for your service
// +short=i
@@ -363,6 +367,7 @@ spec:
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
readinessProbe?: #HealthProbe
}
#HealthProbe: {
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.

View File

@@ -482,7 +482,6 @@ func (p *Parser) loadWorkflowToAppfile(ctx context.Context, af *Appfile) error {
&step.DeployWorkflowStepGenerator{},
&step.Deploy2EnvWorkflowStepGenerator{},
&step.ApplyComponentWorkflowStepGenerator{},
&step.DeployPreApproveWorkflowStepGenerator{},
).Generate(af.app, af.WorkflowSteps)
return err
}

View File

@@ -336,15 +336,11 @@ func (def *Definition) FromCUE(val *cue.Value, templateString string) error {
}
func encodeDeclsToString(decls []ast.Decl) (string, error) {
s := ""
for _, decl := range decls {
bs, err := format.Node(decl, format.Simplify())
if err != nil {
return "", errors.Wrapf(err, "failed to encode decl to string: %v", decl)
}
s += string(bs) + "\n"
bs, err := format.Node(&ast.File{Decls: decls}, format.Simplify())
if err != nil {
return "", fmt.Errorf("failed to encode cue: %w", err)
}
return s, nil
return strings.TrimSpace(string(bs)) + "\n", nil
}
// FromYAML converts yaml into Definition

View File

@@ -30,7 +30,6 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils"
)
// WorkflowStepGenerator generator generates workflow steps
@@ -184,33 +183,6 @@ func (g *DeployWorkflowStepGenerator) Generate(app *v1beta1.Application, existin
return steps, nil
}
// DeployPreApproveWorkflowStepGenerator generate suspend workflow steps before all deploy steps
type DeployPreApproveWorkflowStepGenerator struct{}
// Generate generate workflow steps
func (g *DeployPreApproveWorkflowStepGenerator) Generate(app *v1beta1.Application, existingSteps []workflowv1alpha1.WorkflowStep) (steps []workflowv1alpha1.WorkflowStep, err error) {
lastSuspend := false
for _, step := range existingSteps {
if step.Type == "deploy" && !lastSuspend {
props := DeployWorkflowStepSpec{}
if step.Properties != nil {
_ = utils.StrictUnmarshal(step.Properties.Raw, &props)
}
if props.Auto != nil && !*props.Auto {
steps = append(steps, workflowv1alpha1.WorkflowStep{
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "manual-approve-" + step.Name,
Type: wftypes.WorkflowStepTypeSuspend,
},
})
}
}
lastSuspend = step.Type == wftypes.WorkflowStepTypeSuspend
steps = append(steps, step)
}
return steps, nil
}
// IsBuiltinWorkflowStepType checks if workflow step type is builtin type
func IsBuiltinWorkflowStepType(wfType string) bool {
for _, _type := range []string{

View File

@@ -208,46 +208,6 @@ func TestWorkflowStepGenerator(t *testing.T) {
},
}},
},
"pre-approve-workflow": {
input: []workflowv1alpha1.WorkflowStep{{
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "deploy-example-topology-policy-1",
Type: "deploy",
Properties: &runtime.RawExtension{Raw: []byte(`{"policies":["example-topology-policy-1"]}`)},
},
}, {
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "deploy-example-topology-policy-2",
Type: "deploy",
Properties: &runtime.RawExtension{Raw: []byte(`{"auto":false,"policies":["example-topology-policy-2"]}`)},
},
}},
app: &v1beta1.Application{
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Name: "example-comp-1",
}},
},
},
output: []workflowv1alpha1.WorkflowStep{{
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "deploy-example-topology-policy-1",
Type: "deploy",
Properties: &runtime.RawExtension{Raw: []byte(`{"policies":["example-topology-policy-1"]}`)},
},
}, {
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "manual-approve-deploy-example-topology-policy-2",
Type: "suspend",
},
}, {
WorkflowStepBase: workflowv1alpha1.WorkflowStepBase{
Name: "deploy-example-topology-policy-2",
Type: "deploy",
Properties: &runtime.RawExtension{Raw: []byte(`{"auto":false,"policies":["example-topology-policy-2"]}`)},
},
}},
},
"ref-workflow": {
input: nil,
app: &v1beta1.Application{
@@ -317,7 +277,6 @@ func TestWorkflowStepGenerator(t *testing.T) {
&DeployWorkflowStepGenerator{},
&Deploy2EnvWorkflowStepGenerator{},
&ApplyComponentWorkflowStepGenerator{},
&DeployPreApproveWorkflowStepGenerator{},
)
for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {

View File

@@ -42,6 +42,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/workflow/operation"
)
var _ = Describe("Test multicluster standalone scenario", func() {
@@ -134,7 +135,9 @@ var _ = Describe("Test multicluster standalone scenario", func() {
Expect(k8sClient.Create(hubCtx, policy)).Should(Succeed())
waitObject(hubCtx, *policy)
app := readFile("app-with-publish-version.yaml")
Expect(k8sClient.Create(hubCtx, app)).Should(Succeed())
Eventually(func(g Gomega) {
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) {
@@ -149,13 +152,7 @@ var _ = Describe("Test multicluster standalone scenario", func() {
Eventually(func(g Gomega) {
_app := &v1beta1.Application{}
g.Expect(k8sClient.Get(hubCtx, appKey, _app)).Should(Succeed())
_app.Status.Workflow.Suspend = false
for i, step := range _app.Status.Workflow.Steps {
if step.Type == "suspend" {
_app.Status.Workflow.Steps[i].Phase = workflowv1alpha1.WorkflowStepPhaseSucceeded
}
}
g.Expect(k8sClient.Status().Update(hubCtx, _app)).Should(Succeed())
g.Expect(operation.ResumeWorkflow(hubCtx, k8sClient, _app, "")).Should(Succeed())
}, 15*time.Second).Should(Succeed())
// test application can run without external policies and workflow since they are recorded in the application revision
@@ -302,7 +299,7 @@ var _ = Describe("Test multicluster standalone scenario", func() {
revs, err := application.GetSortedAppRevisions(hubCtx, k8sClient, app.Name, namespace)
g.Expect(err).Should(Succeed())
g.Expect(len(revs)).Should(Equal(1))
}).WithTimeout(30 * time.Second).WithPolling(2 * time.Second).Should(Succeed())
}).WithTimeout(time.Minute).WithPolling(2 * time.Second).Should(Succeed())
})
It("Test large application parallel apply and delete", func() {

View File

@@ -160,7 +160,7 @@ var _ = Describe("HealthScope", func() {
if len(compSts1.Traits) != 1 {
return fmt.Errorf("expect 1 trait statuses, but got %d", len(compSts1.Traits))
}
if !strings.Contains(compSts1.Traits[0].Message, "visit the cluster or load balancer in front of the cluster") {
if !strings.Contains(compSts1.Traits[0].Message, "Visiting URL") {
return fmt.Errorf("trait message isn't right, now is %s", compSts1.Traits[0].Message)
}

View File

@@ -27,7 +27,7 @@ gateway: {
if host != _|_ {
message: "Visiting URL: " + context.outputs.ingress.spec.rules[0].host
}
if host != _|_ {
if host == _|_ {
message: "Host not specified, visit the cluster or load balancer in front of the cluster"
}
}

View File

@@ -13,6 +13,9 @@ import (
description: "A powerful and unified deploy step for components multi-cluster delivery with policies."
}
template: {
if parameter.auto == false {
suspend: op.#Suspend & {message: "Waiting approval to the deploy step \"\(context.stepName)\""}
}
deploy: op.#Deploy & {
policies: parameter.policies
parallelism: parameter.parallelism

View File

@@ -36,6 +36,7 @@ spec:
validation: "client"
}
}
parameter: {
//+usage=The repository URL, can be a HTTP/S or SSH address.
repoUrl: string