Feat: add new providers and fix definitions (#6599)

* feat: add new providers and fix definitions

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix definitions and tests

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix lint and helm

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix definitions

Signed-off-by: FogDong <fog@bentoml.com>

* fix: add multicluster

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix e2e

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix dynamic client for cli

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix api gen

Signed-off-by: FogDong <fog@bentoml.com>

* fix: fix lint

Signed-off-by: FogDong <fog@bentoml.com>

---------

Signed-off-by: FogDong <fog@bentoml.com>
This commit is contained in:
Tianxin Dong
2024-10-01 12:29:44 +05:30
committed by GitHub
parent b1d62aa6ca
commit 0f780dec75
133 changed files with 26713 additions and 1161 deletions
+5
View File
@@ -38,6 +38,11 @@ jobs:
make goimports
make golangci
- name: Setup KinD
run: |
go install sigs.k8s.io/kind@v0.19.0
kind create cluster
- name: Build CLI
run: make vela-cli
+1 -1
View File
@@ -42,7 +42,7 @@ fmt: goimports installcue
$(CUE) fmt ./vela-templates/definitions/deprecated/*
$(CUE) fmt ./vela-templates/definitions/registry/*
$(CUE) fmt ./pkg/workflow/template/static/*
$(CUE) fmt ./pkg/workflow/providers/legacy/...
$(CUE) fmt ./pkg/workflow/providers/...
## sdk_fmt: Run go fmt against code
sdk_fmt:
+8 -6
View File
@@ -48,12 +48,14 @@ helm install --create-namespace -n vela-system kubevela kubevela/vela-core --wai
### KubeVela workflow parameters
| Name | Description | Value |
| -------------------------------------- | ------------------------------------------------------ | ------- |
| `workflow.enableSuspendOnFailure` | Enable suspend on workflow failure | `false` |
| `workflow.backoff.maxTime.waitState` | The max backoff time of workflow in a wait condition | `60` |
| `workflow.backoff.maxTime.failedState` | The max backoff time of workflow in a failed condition | `300` |
| `workflow.step.errorRetryTimes` | The max retry times of a failed workflow step | `10` |
| Name | Description | Value |
| ------------------------------------------------------- | ------------------------------------------------------- | ------- |
| `workflow.enableSuspendOnFailure` | Enable suspend on workflow failure | `false` |
| `workflow.enableExternalPackageForDefaultCompiler` | Enable external package for default cuex compiler | `true` |
| `workflow.enableExternalPackageWatchForDefaultCompiler` | Enable external package watch for default cuex compiler | `false` |
| `workflow.backoff.maxTime.waitState` | The max backoff time of workflow in a wait condition | `60` |
| `workflow.backoff.maxTime.failedState` | The max backoff time of workflow in a failed condition | `300` |
| `workflow.step.errorRetryTimes` | The max retry times of a failed workflow step | `10` |
### KubeVela controller parameters
@@ -0,0 +1,81 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.11.3
creationTimestamp: null
name: packages.cue.oam.dev
spec:
group: cue.oam.dev
names:
kind: Package
listKind: PackageList
plural: packages
shortNames:
- pkg
- cpkg
- cuepkg
- cuepackage
singular: package
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .spec.path
name: PATH
type: string
- jsonPath: .spec.provider.protocol
name: PROTO
type: string
- jsonPath: .spec.provider.endpoint
name: ENDPOINT
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: Package is an extension for cuex engine
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: PackageSpec the spec for Package
properties:
path:
type: string
provider:
description: Provider the external Provider in Package for cuex to
run functions
properties:
endpoint:
type: string
protocol:
description: ProviderProtocol the protocol type for external Provider
type: string
required:
- endpoint
- protocol
type: object
templates:
additionalProperties:
type: string
type: object
required:
- path
- templates
type: object
required:
- spec
type: object
served: true
storage: true
subresources: {}
@@ -16,36 +16,39 @@ spec:
import (
"strconv"
"strings"
"vela/op"
"vela/kube"
"vela/builtin"
)
output: op.#Apply & {
cluster: parameter.cluster
value: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: context.stepName
namespace: context.namespace
}
spec: {
selector: matchLabels: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"
replicas: parameter.replicas
template: {
metadata: labels: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"
spec: containers: [{
name: context.stepName
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
output: kube.#Apply & {
$params: {
cluster: parameter.cluster
value: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: {
name: context.stepName
namespace: context.namespace
}
spec: {
selector: matchLabels: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"
replicas: parameter.replicas
template: {
metadata: labels: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"
spec: containers: [{
name: context.stepName
image: parameter.image
if parameter["cmd"] != _|_ {
command: parameter.cmd
}
}]
}
}
}
}
}
wait: op.#ConditionalWait & {
continue: output.value.status.readyReplicas == parameter.replicas
wait: builtin.#ConditionalWait & {
$params: continue: output.$returns.value.status.readyReplicas == parameter.replicas
}
parameter: {
image: string
@@ -13,13 +13,13 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
)
apply: op.#Apply & {
value: parameter.value
cluster: parameter.cluster
apply: kube.#Apply & {
$params: parameter
}
parameter: {
// +usage=Specify Kubernetes native resource object to be applied
value: {...}
@@ -14,11 +14,12 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
"vela/builtin"
)
apply: op.#Apply & {
value: {
apply: kube.#Apply & {
$params: value: {
apiVersion: "terraform.core.oam.dev/v1beta2"
kind: "Configuration"
metadata: {
@@ -53,8 +54,10 @@ spec:
}
}
}
check: op.#ConditionalWait & {
continue: apply.value.status != _|_ && apply.value.status.apply != _|_ && apply.value.status.apply.state == "Available"
check: builtin.#ConditionalWait & {
if apply.$returns.value.status != _|_ if apply.$returns.value.status.apply != _|_ {
$params: continue: apply.$returns.value.status.apply.state == "Available"
}
}
parameter: {
// +usage=specify the source of the terraform configuration
@@ -14,61 +14,65 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/config"
"vela/kube"
"vela/builtin"
"strings"
)
config: op.#CreateConfig & {
name: "\(context.name)-\(context.stepName)"
namespace: context.namespace
template: "terraform-\(parameter.type)"
config: {
name: parameter.name
if parameter.type == "alibaba" {
ALICLOUD_ACCESS_KEY: parameter.accessKey
ALICLOUD_SECRET_KEY: parameter.secretKey
ALICLOUD_REGION: parameter.region
}
if parameter.type == "aws" {
AWS_ACCESS_KEY_ID: parameter.accessKey
AWS_SECRET_ACCESS_KEY: parameter.secretKey
AWS_DEFAULT_REGION: parameter.region
AWS_SESSION_TOKEN: parameter.token
}
if parameter.type == "azure" {
ARM_CLIENT_ID: parameter.clientID
ARM_CLIENT_SECRET: parameter.clientSecret
ARM_SUBSCRIPTION_ID: parameter.subscriptionID
ARM_TENANT_ID: parameter.tenantID
}
if parameter.type == "baidu" {
BAIDUCLOUD_ACCESS_KEY: parameter.accessKey
BAIDUCLOUD_SECRET_KEY: parameter.secretKey
BAIDUCLOUD_REGION: parameter.region
}
if parameter.type == "ec" {
EC_API_KEY: parameter.apiKey
}
if parameter.type == "gcp" {
GOOGLE_CREDENTIALS: parameter.credentials
GOOGLE_REGION: parameter.region
GOOGLE_PROJECT: parameter.project
}
if parameter.type == "tencent" {
TENCENTCLOUD_SECRET_ID: parameter.secretID
TENCENTCLOUD_SECRET_KEY: parameter.secretKey
TENCENTCLOUD_REGION: parameter.region
}
if parameter.type == "ucloud" {
UCLOUD_PRIVATE_KEY: parameter.privateKey
UCLOUD_PUBLIC_KEY: parameter.publicKey
UCLOUD_PROJECT_ID: parameter.projectID
UCLOUD_REGION: parameter.region
cfg: config.#CreateConfig & {
$params: {
name: "\(context.name)-\(context.stepName)"
namespace: context.namespace
template: "terraform-\(parameter.type)"
config: {
name: parameter.name
if parameter.type == "alibaba" {
ALICLOUD_ACCESS_KEY: parameter.accessKey
ALICLOUD_SECRET_KEY: parameter.secretKey
ALICLOUD_REGION: parameter.region
}
if parameter.type == "aws" {
AWS_ACCESS_KEY_ID: parameter.accessKey
AWS_SECRET_ACCESS_KEY: parameter.secretKey
AWS_DEFAULT_REGION: parameter.region
AWS_SESSION_TOKEN: parameter.token
}
if parameter.type == "azure" {
ARM_CLIENT_ID: parameter.clientID
ARM_CLIENT_SECRET: parameter.clientSecret
ARM_SUBSCRIPTION_ID: parameter.subscriptionID
ARM_TENANT_ID: parameter.tenantID
}
if parameter.type == "baidu" {
BAIDUCLOUD_ACCESS_KEY: parameter.accessKey
BAIDUCLOUD_SECRET_KEY: parameter.secretKey
BAIDUCLOUD_REGION: parameter.region
}
if parameter.type == "ec" {
EC_API_KEY: parameter.apiKey
}
if parameter.type == "gcp" {
GOOGLE_CREDENTIALS: parameter.credentials
GOOGLE_REGION: parameter.region
GOOGLE_PROJECT: parameter.project
}
if parameter.type == "tencent" {
TENCENTCLOUD_SECRET_ID: parameter.secretID
TENCENTCLOUD_SECRET_KEY: parameter.secretKey
TENCENTCLOUD_REGION: parameter.region
}
if parameter.type == "ucloud" {
UCLOUD_PRIVATE_KEY: parameter.privateKey
UCLOUD_PUBLIC_KEY: parameter.publicKey
UCLOUD_PROJECT_ID: parameter.projectID
UCLOUD_REGION: parameter.region
}
}
}
}
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
apiVersion: "terraform.core.oam.dev/v1beta1"
kind: "Provider"
metadata: {
@@ -77,12 +81,9 @@ spec:
}
}
}
check: op.#ConditionalWait & {
if read.value.status != _|_ {
continue: read.value.status.state == "ready"
}
if read.value.status == _|_ {
continue: false
check: builtin.#ConditionalWait & {
if read.$returns.value.status != _|_ {
$params: continue: read.$returns.value.status.state == "ready"
}
}
providerBasic: {
@@ -14,7 +14,9 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/builtin"
"vela/kube"
"vela/util"
"encoding/json"
"strings"
)
@@ -28,8 +30,8 @@ spec:
value: parameter.context
}
}
kaniko: op.#Apply & {
value: {
kaniko: kube.#Apply & {
$params: value: {
apiVersion: "v1"
kind: "Pod"
metadata: {
@@ -95,14 +97,14 @@ spec:
}
}
}
log: op.#Log & {
source: resources: [{
log: util.#Log & {
$params: source: resources: [{
name: "\(context.name)-\(context.stepSessionID)-kaniko"
namespace: context.namespace
}]
}
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
apiVersion: "v1"
kind: "Pod"
metadata: {
@@ -111,8 +113,10 @@ spec:
}
}
}
wait: op.#ConditionalWait & {
continue: read.value.status != _|_ && read.value.status.phase == "Succeeded"
wait: builtin.#ConditionalWait & {
if read.$returns.value.status != _|_ {
$params: continue: read.$returns.value.status.phase == "Succeeded"
}
}
#secret: {
name: string
@@ -15,32 +15,35 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/metrics"
"vela/builtin"
)
check: op.#PromCheck & {
query: parameter.query
metricEndpoint: parameter.metricEndpoint
condition: parameter.condition
stepID: context.stepSessionID
duration: parameter.duration
failDuration: parameter.failDuration
check: metrics.#PromCheck & {
$params: {
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
fail: {
if check.$returns.failed != _|_ {
if check.$returns.failed == true {
breakWorkflow: builtin.#Fail & {
$params: message: check.$returns.message
}
}
}
}
wait: op.#ConditionalWait & {
continue: check.result
if check.message != _|_ {
message: check.message
wait: builtin.#ConditionalWait & {
$params: continue: check.$returns.result
if check.$returns.message != _|_ {
$params: message: check.$returns.message
}
}
@@ -13,7 +13,7 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
)
parameter: {
@@ -21,42 +21,46 @@ spec:
namespace: *context.namespace | string
}
cleanJobs: op.#Delete & {
value: {
apiVersion: "batch/v1"
kind: "Job"
metadata: {
name: context.name
cleanJobs: kube.#Delete & {
$params: {
value: {
apiVersion: "batch/v1"
kind: "Job"
metadata: {
name: context.name
namespace: parameter.namespace
}
}
filter: {
namespace: parameter.namespace
}
}
filter: {
namespace: parameter.namespace
if parameter.labelselector != _|_ {
matchingLabels: parameter.labelselector
}
if parameter.labelselector == _|_ {
matchingLabels: "workflow.oam.dev/name": context.name
if parameter.labelselector != _|_ {
matchingLabels: parameter.labelselector
}
if parameter.labelselector == _|_ {
matchingLabels: "workflow.oam.dev/name": context.name
}
}
}
}
cleanPods: op.#Delete & {
value: {
apiVersion: "v1"
kind: "pod"
metadata: {
name: context.name
cleanPods: kube.#Delete & {
$params: {
value: {
apiVersion: "v1"
kind: "pod"
metadata: {
name: context.name
namespace: parameter.namespace
}
}
filter: {
namespace: parameter.namespace
}
}
filter: {
namespace: parameter.namespace
if parameter.labelselector != _|_ {
matchingLabels: parameter.labelselector
}
if parameter.labelselector == _|_ {
matchingLabels: "workflow.oam.dev/name": context.name
if parameter.labelselector != _|_ {
matchingLabels: parameter.labelselector
}
if parameter.labelselector == _|_ {
matchingLabels: "workflow.oam.dev/name": context.name
}
}
}
}
@@ -13,21 +13,15 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/ql"
"vela/builtin"
"vela/query"
"strconv"
)
collect: ql.#CollectServiceEndpoints & {
app: {
name: *context.name | string
namespace: *context.namespace | string
if parameter.name != _|_ {
name: parameter.name
}
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
collect: query.#CollectServiceEndpoints & {
$params: app: {
name: parameter.name
namespace: parameter.namespace
filter: {
if parameter.components != _|_ {
components: parameter.components
@@ -39,10 +33,10 @@ spec:
outputs: {
eps_port_name_filtered: *[] | [...]
if parameter.portName == _|_ {
eps_port_name_filtered: collect.list
eps_port_name_filtered: collect.$returns.list
}
if parameter.portName != _|_ {
eps_port_name_filtered: [for ep in collect.list if parameter.portName == ep.endpoint.portName {ep}]
eps_port_name_filtered: [for ep in collect.$returns.list if parameter.portName == ep.endpoint.portName {ep}]
}
eps_port_filtered: *[] | [...]
@@ -71,8 +65,8 @@ spec:
}
}
wait: op.#ConditionalWait & {
continue: len(outputs.endpoints) > 0
wait: builtin.#ConditionalWait & {
$params: continue: len(outputs.endpoints) > 0
}
value: {
@@ -85,9 +79,9 @@ spec:
parameter: {
// +usage=Specify the name of the application
name?: string
name: *context.name | string
// +usage=Specify the namespace of the application
namespace?: string
namespace: *context.namespace | string
// +usage=Filter the component of the endpoints
components?: [...string]
// +usage=Filter the port of the endpoints
@@ -13,28 +13,18 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/config"
)
deploy: op.#CreateConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
if parameter.template != _|_ {
template: parameter.template
}
config: parameter.config
deploy: config.#CreateConfig & {
$params: parameter
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
namespace: *context.namespace | string
//+usage=Specify the template of the config.
template?: string
@@ -13,23 +13,17 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/config"
)
deploy: op.#DeleteConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
deploy: config.#DeleteConfig & {
$params: parameter
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
namespace: *context.namespace | string
}
@@ -13,12 +13,13 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
"vela/builtin"
"encoding/yaml"
)
dependsOn: op.#Read & {
value: {
dependsOn: kube.#Read & {
$params: value: {
apiVersion: "core.oam.dev/v1beta1"
kind: "Application"
metadata: {
@@ -27,10 +28,10 @@ spec:
}
}
}
load: op.#Steps & {
if dependsOn.err != _|_ {
configMap: op.#Read & {
value: {
load: {
if dependsOn.$returns.err != _|_ {
configMap: kube.#Read & {
$params: value: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
@@ -39,18 +40,18 @@ spec:
}
}
}
template: configMap.value.data["application"]
apply: op.#Apply & {
value: yaml.Unmarshal(template)
template: configMap.$returns.value.data["application"]
apply: kube.#Apply & {
$params: value: yaml.Unmarshal(template)
}
wait: op.#ConditionalWait & {
continue: apply.value.status.status == "running"
wait: builtin.#ConditionalWait & {
$params: continue: apply.$returns.value.status.status == "running"
}
}
if dependsOn.err == _|_ {
wait: op.#ConditionalWait & {
continue: dependsOn.value.status.status == "running"
if dependsOn.$returns.err == _|_ {
wait: builtin.#ConditionalWait & {
$params: continue: dependsOn.$returns.value.status.status == "running"
}
}
}
@@ -15,16 +15,19 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/multicluster"
"vela/builtin"
)
if parameter.auto == false {
suspend: op.#Suspend & {message: "Waiting approval to the deploy step \"\(context.stepName)\""}
suspend: builtin.#Suspend & {$params: message: "Waiting approval to the deploy step \"\(context.stepName)\""}
}
deploy: op.#Deploy & {
policies: parameter.policies
parallelism: parameter.parallelism
ignoreTerraformComponent: parameter.ignoreTerraformComponent
deploy: multicluster.#Deploy & {
$params: {
policies: parameter.policies
parallelism: parameter.parallelism
ignoreTerraformComponent: parameter.ignoreTerraformComponent
}
}
parameter: {
//+usage=If set to false, the workflow will suspend automatically before this step, default to be true.
@@ -16,6 +16,7 @@ spec:
template: |
import (
"vela/op"
"vela/kube"
)
object: {
@@ -46,11 +47,13 @@ spec:
}
}
apply: op.#Steps & {
apply: {
for p in getPlacements.placements {
(p.cluster): op.#Apply & {
value: object
cluster: p.cluster
(p.cluster): kube.#Apply & {
$params: {
value: object
cluster: p.cluster
}
}
}
}
@@ -16,6 +16,7 @@ spec:
template: |
import (
"vela/op"
"vela/kube"
)
meta: {
@@ -57,12 +58,14 @@ spec:
}
}
apply: op.#Steps & {
apply: {
for p in getPlacements.placements {
for o in objects {
"\(p.cluster)-\(o.kind)": op.#Apply & {
value: o
cluster: p.cluster
"\(p.cluster)-\(o.kind)": kube.#Apply & {
$params: {
value: o
cluster: p.cluster
}
}
}
}
@@ -13,25 +13,27 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
)
apply: op.#Apply & {
value: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: parameter.configName
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
apply: kube.#Apply & {
$params: {
value: {
apiVersion: "v1"
kind: "ConfigMap"
metadata: {
name: parameter.configName
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
}
data: parameter.data
}
data: parameter.data
cluster: parameter.cluster
}
cluster: parameter.cluster
}
parameter: {
// +usage=Specify the name of the config map
@@ -13,12 +13,12 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
"encoding/base64"
"encoding/json"
)
secret: op.#Steps & {
secret: {
data: *parameter.data | {}
if parameter.kind == "docker-registry" && parameter.dockerRegistry != _|_ {
registryData: auths: "\(parameter.dockerRegistry.server)": {
@@ -28,28 +28,30 @@ spec:
}
data: ".dockerconfigjson": json.Marshal(registryData)
}
apply: op.#Apply & {
value: {
apiVersion: "v1"
kind: "Secret"
if parameter.type == _|_ && parameter.kind == "docker-registry" {
type: "kubernetes.io/dockerconfigjson"
}
if parameter.type != _|_ {
type: parameter.type
}
metadata: {
name: parameter.secretName
if parameter.namespace != _|_ {
namespace: parameter.namespace
apply: kube.#Apply & {
$params: {
value: {
apiVersion: "v1"
kind: "Secret"
if parameter.type == _|_ && parameter.kind == "docker-registry" {
type: "kubernetes.io/dockerconfigjson"
}
if parameter.namespace == _|_ {
namespace: context.namespace
if parameter.type != _|_ {
type: parameter.type
}
metadata: {
name: parameter.secretName
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
}
stringData: data
}
stringData: data
cluster: parameter.cluster
}
cluster: parameter.cluster
}
}
parameter: {
@@ -13,12 +13,13 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
"vela/util"
"encoding/base64"
)
output: op.#Read & {
value: {
output: kube.#Read & {
$params: value: {
apiVersion: "v1"
kind: "Secret"
metadata: {
@@ -29,16 +30,16 @@ spec:
}
}
}
dbHost: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_HOST"])}
dbPort: op.#ConvertString & {bt: base64.Decode(null, output.value.data["DB_PORT"])}
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"])}
dbHost: util.#ConvertString & {$params: bt: base64.Decode(null, output.$returns.value.data["DB_HOST"])}
dbPort: util.#ConvertString & {$params: bt: base64.Decode(null, output.$returns.value.data["DB_PORT"])}
dbName: util.#ConvertString & {$params: bt: base64.Decode(null, output.$returns.value.data["DB_NAME"])}
username: util.#ConvertString & {$params: bt: base64.Decode(null, output.$returns.value.data["DB_USER"])}
password: util.#ConvertString & {$params: bt: base64.Decode(null, output.$returns.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},
{name: "url", value: "jdbc://" + dbHost.$returns.str + ":" + dbPort.$returns.str + "/" + dbName.$returns.str + "?characterEncoding=utf8&useSSL=false"},
{name: "username", value: username.$returns.str},
{name: "password", value: password.$returns.str},
]
parameter: {
@@ -13,22 +13,16 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/config"
)
output: op.#ListConfig & {
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
template: parameter.template
output: config.#ListConfig & {
$params: parameter
}
parameter: {
//+usage=Specify the template of the config.
template: string
//+usage=Specify the namespace of the config.
namespace?: string
namespace: *context.namespace | string
}
@@ -13,8 +13,12 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/http"
"vela/email"
"vela/kube"
"vela/util"
"encoding/base64"
"encoding/json"
)
parameter: {
@@ -209,17 +213,23 @@ spec:
}
// send webhook notification
ding: op.#Steps & {
ding: {
if parameter.dingding != _|_ {
if parameter.dingding.url.value != _|_ {
ding1: op.#DingTalk & {
message: parameter.dingding.message
dingUrl: parameter.dingding.url.value
ding1: http.#Do & {
$params: {
method: "POST"
url: parameter.dingding.url.value
request: {
body: json.Marshal(parameter.dingding.message)
header: "Content-Type": "application/json"
}
}
}
}
if parameter.dingding.url.secretRef != _|_ && parameter.dingding.url.value == _|_ {
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
apiVersion: "v1"
kind: "Secret"
metadata: {
@@ -229,26 +239,38 @@ spec:
}
}
stringValue: op.#ConvertString & {bt: base64.Decode(null, read.value.data[parameter.dingding.url.secretRef.key])}
ding2: op.#DingTalk & {
message: parameter.dingding.message
dingUrl: stringValue.str
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.dingding.url.secretRef.key])}
ding2: http.#Do & {
$params: {
method: "POST"
url: stringValue.$returns.str
request: {
body: json.Marshal(parameter.dingding.message)
header: "Content-Type": "application/json"
}
}
}
}
}
}
lark: op.#Steps & {
lark: {
if parameter.lark != _|_ {
if parameter.lark.url.value != _|_ {
lark1: op.#Lark & {
message: parameter.lark.message
larkUrl: parameter.lark.url.value
lark1: http.#Do & {
$params: {
method: "POST"
url: parameter.lark.message
request: {
body: json.Marshal(parameter.lark.message)
header: "Content-Type": "application/json"
}
}
}
}
if parameter.lark.url.secretRef != _|_ && parameter.lark.url.value == _|_ {
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
apiVersion: "v1"
kind: "Secret"
metadata: {
@@ -258,26 +280,39 @@ spec:
}
}
stringValue: op.#ConvertString & {bt: base64.Decode(null, read.value.data[parameter.lark.url.secretRef.key])}
lark2: op.#Lark & {
message: parameter.lark.message
larkUrl: stringValue.str
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.lark.url.secretRef.key])}
lark2: http.#Do & {
$params: {
method: "POST"
url: stringValue.$returns.str
request: {
body: json.Marshal(parameter.lark.message)
header: "Content-Type": "application/json"
}
}
}
}
}
}
slack: op.#Steps & {
slack: {
if parameter.slack != _|_ {
if parameter.slack.url.value != _|_ {
slack1: op.#Slack & {
message: parameter.slack.message
slackUrl: parameter.slack.url.value
slack1: http.#Do & {
$params: {
method: "POST"
url: parameter.slack.url.value
request: {
body: json.Marshal(parameter.slack.message)
header: "Content-Type": "application/json"
}
}
}
}
if parameter.slack.url.secretRef != _|_ && parameter.slack.url.value == _|_ {
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
kind: "Secret"
apiVersion: "v1"
metadata: {
@@ -287,36 +322,44 @@ spec:
}
}
stringValue: op.#ConvertString & {bt: base64.Decode(null, read.value.data[parameter.slack.url.secretRef.key])}
slack2: op.#Slack & {
message: parameter.slack.message
slackUrl: stringValue.str
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.slack.url.secretRef.key])}
slack2: http.#Do & {
$params: {
method: "POST"
url: stringValue.$returns.str
request: {
body: json.Marshal(parameter.slack.message)
header: "Content-Type": "application/json"
}
}
}
}
}
}
email: op.#Steps & {
email0: {
if parameter.email != _|_ {
if parameter.email.from.password.value != _|_ {
email1: op.#SendEmail & {
from: {
address: parameter.email.from.address
if parameter.email.from.alias != _|_ {
alias: parameter.email.from.alias
email1: email.#SendEmail & {
$params: {
from: {
address: parameter.email.from.address
if parameter.email.from.alias != _|_ {
alias: parameter.email.from.alias
}
password: parameter.email.from.password.value
host: parameter.email.from.host
port: parameter.email.from.port
}
password: parameter.email.from.password.value
host: parameter.email.from.host
port: parameter.email.from.port
to: parameter.email.to
content: parameter.email.content
}
to: parameter.email.to
content: parameter.email.content
}
}
if parameter.email.from.password.secretRef != _|_ && parameter.email.from.password.value == _|_ {
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
kind: "Secret"
apiVersion: "v1"
metadata: {
@@ -326,19 +369,21 @@ spec:
}
}
stringValue: op.#ConvertString & {bt: base64.Decode(null, read.value.data[parameter.email.from.password.secretRef.key])}
email2: op.#SendEmail & {
from: {
address: parameter.email.from.address
if parameter.email.from.alias != _|_ {
alias: parameter.email.from.alias
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.email.from.password.secretRef.key])}
email2: email.#SendEmail & {
$params: {
from: {
address: parameter.email.from.address
if parameter.email.from.alias != _|_ {
alias: parameter.email.from.alias
}
password: stringValue.str
host: parameter.email.from.host
port: parameter.email.from.port
}
password: stringValue.str
host: parameter.email.from.host
port: parameter.email.from.port
to: parameter.email.to
content: parameter.email.content
}
to: parameter.email.to
content: parameter.email.content
}
}
}
@@ -13,12 +13,12 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/builtin"
)
parameter: message: string
msg: op.#Message & {
message: parameter.message
msg: builtin.#Message & {
$params: parameter
}
@@ -13,23 +13,17 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/config"
)
output: op.#ReadConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
output: config.#ReadConfig & {
$params: parameter
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
namespace: *context.namespace | string
}
@@ -13,50 +13,31 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
)
output: {
if parameter.apiVersion == _|_ && parameter.kind == _|_ {
op.#Read & {
value: {
apiVersion: "core.oam.dev/v1beta1"
kind: "Application"
metadata: {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
}
output: kube.#Read & {
$params: {
cluster: parameter.cluster
value: {
apiVersion: parameter.apiVersion
kind: parameter.kind
metadata: {
name: parameter.name
namespace: parameter.namespace
}
cluster: parameter.cluster
}
}
if parameter.apiVersion != _|_ || parameter.kind != _|_ {
op.#Read & {
value: {
apiVersion: parameter.apiVersion
kind: parameter.kind
metadata: {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
}
}
cluster: parameter.cluster
}
}
}
parameter: {
// +usage=Specify the apiVersion of the object, defaults to 'core.oam.dev/v1beta1'
apiVersion?: string
apiVersion: *"core.oam.dev/v1beta1" | string
// +usage=Specify the kind of the object, defaults to Application
kind?: string
kind: *"Application" | string
// +usage=Specify the name of the object
name: string
// +usage=The namespace of the resource you want to read
namespace?: *"default" | string
namespace: *"default" | string
// +usage=The cluster you want to apply the resource to, default is the current control plane cluster
cluster: *"" | string
}
@@ -14,30 +14,33 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/http"
"vela/builtin"
"encoding/json"
)
http: op.#HTTPDo & {
method: parameter.method
url: parameter.url
request: {
if parameter.body != _|_ {
body: json.Marshal(parameter.body)
}
if parameter.header != _|_ {
header: parameter.header
req: http.#HTTPDo & {
$params: {
method: parameter.method
url: parameter.url
request: {
if parameter.body != _|_ {
body: json.Marshal(parameter.body)
}
if parameter.header != _|_ {
header: parameter.header
}
}
}
}
fail: op.#Steps & {
if http.response.statusCode > 400 {
requestFail: op.#Fail & {
message: "request of \(parameter.url) is fail: \(http.response.statusCode)"
fail: {
if http.$returns.response.statusCode > 400 {
requestFail: builtin.#Fail & {
$params: message: "request of \(parameter.url) is fail: \(http.response.statusCode)"
}
}
}
response: json.Unmarshal(http.response.body)
response: json.Unmarshal(http.$returns.response.body)
parameter: {
url: string
method: *"GET" | "POST" | "PUT" | "DELETE"
@@ -13,16 +13,11 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/builtin"
)
suspend: op.#Suspend & {
if parameter.duration != _|_ {
duration: parameter.duration
}
if parameter.message != _|_ {
message: parameter.message
}
suspend: builtin.#Suspend & {
$params: parameter
}
parameter: {
@@ -13,7 +13,9 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/kube"
"vela/builtin"
"vela/util"
)
mountsArray: [
@@ -68,8 +70,8 @@ spec:
},
]
job: op.#Apply & {
value: {
job: kube.#Apply & {
$params: value: {
apiVersion: "batch/v1"
kind: "Job"
metadata: {
@@ -103,22 +105,24 @@ spec:
}
}
log: op.#Log & {
source: resources: [{labelSelector: "workflow.oam.dev/step-name": "\(context.name)-\(context.stepName)"}]
log: util.#Log & {
$params: 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 {
breakWorkflow: op.#Fail & {
message: "failed to execute vela command"
fail: {
if job.$returns.value.status != _|_ if job.$returns.value.status.failed != _|_ {
if job.$returns.value.status.failed > 2 {
breakWorkflow: builtin.#Fail & {
$params: message: "failed to execute vela command"
}
}
}
}
wait: op.#ConditionalWait & {
continue: job.value.status.succeeded != _|_ && job.value.status.succeeded > 0
wait: builtin.#ConditionalWait & {
if job.$returns.value.status != _|_ if job.$returns.value.status.succeeded != _|_ {
$params: continue: job.$returns.value.status.succeeded > 0
}
}
parameter: {
@@ -13,15 +13,17 @@ spec:
cue:
template: |
import (
"vela/op"
"vela/http"
"vela/kube"
"vela/util"
"encoding/json"
"encoding/base64"
)
data: op.#Steps & {
data: {
if parameter.data == _|_ {
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
apiVersion: "core.oam.dev/v1beta1"
kind: "Application"
metadata: {
@@ -30,25 +32,27 @@ spec:
}
}
}
value: json.Marshal(read.value)
value: json.Marshal(read.$returns.value)
}
if parameter.data != _|_ {
value: json.Marshal(parameter.data)
}
}
webhook: op.#Steps & {
webhook: {
if parameter.url.value != _|_ {
http: op.#HTTPPost & {
url: parameter.url.value
request: {
body: data.value
header: "Content-Type": "application/json"
req: http.#HTTPPost & {
$params: {
url: parameter.url.value
request: {
body: data.value
header: "Content-Type": "application/json"
}
}
}
}
if parameter.url.secretRef != _|_ && parameter.url.value == _|_ {
read: op.#Read & {
value: {
read: kube.#Read & {
$params: value: {
apiVersion: "v1"
kind: "Secret"
metadata: {
@@ -58,12 +62,14 @@ spec:
}
}
stringValue: op.#ConvertString & {bt: base64.Decode(null, read.value.data[parameter.url.secretRef.key])}
http: op.#HTTPPost & {
url: stringValue.str
request: {
body: data.value
header: "Content-Type": "application/json"
stringValue: util.#ConvertString & {$params: bt: base64.Decode(null, read.$returns.value.data[parameter.url.secretRef.key])}
req: http.#HTTPPost & {
$params: {
url: stringValue.$returns.str
request: {
body: data.value
header: "Content-Type": "application/json"
}
}
}
}
@@ -296,6 +296,8 @@ spec:
- "--max-workflow-wait-backoff-time={{ .Values.workflow.backoff.maxTime.waitState }}"
- "--max-workflow-failed-backoff-time={{ .Values.workflow.backoff.maxTime.failedState }}"
- "--max-workflow-step-error-retry-times={{ .Values.workflow.step.errorRetryTimes }}"
- "--enable-external-package-for-default-compiler={{ .Values.workflow.enableExternalPackageForDefaultCompiler }}"
- "--enable-external-package-watch-for-default-compiler={{ .Values.workflow.enableExternalPackageWatchForDefaultCompiler }}"
- "--feature-gates=EnableSuspendOnFailure={{- .Values.workflow.enableSuspendOnFailure | toString -}}"
- "--feature-gates=AuthenticateApplication={{- .Values.authentication.enabled | toString -}}"
- "--feature-gates=GzipResourceTracker={{- .Values.featureGates.gzipResourceTracker | toString -}}"
+4
View File
@@ -24,11 +24,15 @@ controllerArgs:
## @section KubeVela workflow parameters
## @param workflow.enableSuspendOnFailure Enable suspend on workflow failure
## @param workflow.enableExternalPackageForDefaultCompiler Enable external package for default cuex compiler
## @param workflow.enableExternalPackageWatchForDefaultCompiler Enable external package watch for default cuex compiler
## @param workflow.backoff.maxTime.waitState The max backoff time of workflow in a wait condition
## @param workflow.backoff.maxTime.failedState The max backoff time of workflow in a failed condition
## @param workflow.step.errorRetryTimes The max retry times of a failed workflow step
workflow:
enableSuspendOnFailure: false
enableExternalPackageForDefaultCompiler: true
enableExternalPackageWatchForDefaultCompiler: false
backoff:
maxTime:
waitState: 60
+3
View File
@@ -35,6 +35,7 @@ import (
oamcontroller "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/resourcekeeper"
"github.com/oam-dev/kubevela/pkg/workflow/providers"
)
// CoreOptions contains everything necessary to create and run vela-core
@@ -131,6 +132,8 @@ func (s *CoreOptions) Flags() cliflag.NamedFlagSets {
gfs.BoolVar(&s.EnableClusterGateway, "enable-cluster-gateway", s.EnableClusterGateway, "Enable cluster-gateway to use multicluster, disabled by default.")
gfs.BoolVar(&s.EnableClusterMetrics, "enable-cluster-metrics", s.EnableClusterMetrics, "Enable cluster-metrics-management to collect metrics from clusters with cluster-gateway, disabled by default. When this param is enabled, enable-cluster-gateway should be enabled")
gfs.DurationVar(&s.ClusterMetricsInterval, "cluster-metrics-interval", s.ClusterMetricsInterval, "The interval that ClusterMetricsMgr will collect metrics from clusters, default value is 15 seconds.")
gfs.BoolVar(&providers.EnableExternalPackageForDefaultCompiler, "enable-external-package-for-default-compiler", providers.EnableExternalPackageForDefaultCompiler, "Enable external package for default compiler")
gfs.BoolVar(&providers.EnableExternalPackageWatchForDefaultCompiler, "enable-external-package-watch-for-default-compiler", providers.EnableExternalPackageWatchForDefaultCompiler, "Enable external package watch for default compiler")
s.ControllerArgs.AddFlags(fss.FlagSet("controllerArgs"), s.ControllerArgs)
@@ -26,7 +26,7 @@ spec:
type: webhook
outputs:
- name: mysecret
valueFrom: webhook.http.response.body
valueFrom: webhook.req.$returns.response.body
properties:
url:
value: <url>
+2 -2
View File
@@ -16,9 +16,9 @@ spec:
type: read-object
outputs:
- name: cpu
valueFrom: output.value.data["cpu"]
valueFrom: output.$returns.value.data["cpu"]
- name: memory
valueFrom: output.value.data["memory"]
valueFrom: output.$returns.value.data["memory"]
properties:
apiVersion: v1
kind: ConfigMap
+1 -1
View File
@@ -37,7 +37,7 @@ require (
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174
github.com/imdario/mergo v0.3.16
github.com/kubevela/pkg v1.9.2
github.com/kubevela/workflow v0.6.1-0.20240727094441-7d94489306a8
github.com/kubevela/workflow v0.6.1-0.20240924152948-55f1433fd7f8
github.com/kyokomi/emoji v2.2.4+incompatible
github.com/magiconair/properties v1.8.7
github.com/mattn/go-runewidth v0.0.15
+2 -2
View File
@@ -632,8 +632,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubevela/pkg v1.9.2 h1:K6pGoJikf6l8vlfehewmb36hyToX0KpUaQP4NVET/S8=
github.com/kubevela/pkg v1.9.2/go.mod h1:u/MGuFXVSECxvIWdTKS4AQs1H+USfAMQgi30BUrOb04=
github.com/kubevela/workflow v0.6.1-0.20240727094441-7d94489306a8 h1:67JnKJMDcf/nbibuiR+lEFiIEUKwlPm6ufQxbyjQLmQ=
github.com/kubevela/workflow v0.6.1-0.20240727094441-7d94489306a8/go.mod h1:/tWZOtO+bp/EUQCJZjL8t3O5YDx5QXnKVllQkDTKKuw=
github.com/kubevela/workflow v0.6.1-0.20240924152948-55f1433fd7f8 h1:dvqNMluYo4P9ngpUuoAym9WSr4E8TFZLQ8qc5D/XwU4=
github.com/kubevela/workflow v0.6.1-0.20240924152948-55f1433fd7f8/go.mod h1:/tWZOtO+bp/EUQCJZjL8t3O5YDx5QXnKVllQkDTKKuw=
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
@@ -4545,34 +4545,37 @@ spec:
cue:
template: |
import (
"vela/op"
"list"
"list"
"vela/kube"
"vela/oam"
"vela/util"
)
components: op.#LoadInOrder & {}
targetComponent: components.value[0]
resources: op.#RenderComponent & {
value: targetComponent
components: oam.#LoadComponetsInOrder & {}
targetComponent: components.$returns.value[0]
resources: oam.#RenderComponent & {
$params: value: targetComponent
}
workload: resources.output
arr: list.Range(0, parameter.parallelism, 1)
patchWorkloads: op.#Steps & {
for idx in arr {
"\(idx)": op.#PatchK8sObject & {
value: workload
patch: {
// +patchStrategy=retainKeys
metadata: name: "\(targetComponent.name)-\(idx)"
}
}
}
workload: resources.$returns.output
arr: list.Range(0, parameter.parallelism, 1)
patchWorkloads: {
for idx in arr {
"\(idx)": util.#PatchK8sObject & {
$params: {
value: workload
patch: {
// +patchStrategy=retainKeys
metadata: name: "\(targetComponent.name)-\(idx)"
}
}
}
}
}
workloads: [ for patchResult in patchWorkloads {patchResult.result}]
apply: op.#ApplyInParallel & {
value: workloads
workloads: [for patchResult in patchWorkloads {patchResult.$returns.result}]
apply: kube.#ApplyInParallel & {
$params: value: workloads
}
parameter: parallelism: int
`
)
@@ -56,7 +56,7 @@ import (
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/workflow/providers"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
"github.com/oam-dev/kubevela/pkg/workflow/template"
)
@@ -109,7 +109,7 @@ func (h *AppHandler) GenerateApplicationSteps(ctx monitorContext.Context,
instance := generateWorkflowInstance(af, app)
executor.InitializeWorkflowInstance(instance)
runners, err := generator.GenerateRunners(ctx, instance, wfTypes.StepGeneratorOptions{
Compiler: providers.Compiler.Get(),
Compiler: providers.DefaultCompiler.Get(),
ProcessCtx: pCtx,
TemplateLoader: template.NewWorkflowStepTemplateRevisionLoader(appRev, h.Client.RESTMapper()),
StepConvertor: map[string]func(step workflowv1alpha1.WorkflowStep) (workflowv1alpha1.WorkflowStep, error){
@@ -27,6 +27,7 @@ import (
"time"
"github.com/crossplane/crossplane-runtime/pkg/event"
cuexv1alpha1 "github.com/kubevela/pkg/apis/cue/v1alpha1"
"github.com/kubevela/pkg/util/singleton"
terraformv1beta2 "github.com/oam-dev/terraform-controller/api/v1beta2"
. "github.com/onsi/ginkgo/v2"
@@ -37,6 +38,7 @@ import (
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
@@ -100,15 +102,20 @@ var _ = BeforeSuite(func() {
err = scheme.AddToScheme(testScheme)
Expect(err).NotTo(HaveOccurred())
terraformv1beta2.AddToScheme(testScheme)
crdv1.AddToScheme(testScheme)
err = terraformv1beta2.AddToScheme(testScheme)
Expect(err).NotTo(HaveOccurred())
err = crdv1.AddToScheme(testScheme)
Expect(err).NotTo(HaveOccurred())
err = cuexv1alpha1.AddToScheme(testScheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
singleton.KubeClient.Set(k8sClient)
fakeDynamicClient := fake.NewSimpleDynamicClient(testScheme)
singleton.DynamicClient.Set(fakeDynamicClient)
appParser = appfile.NewApplicationParser(k8sClient)
reconciler = &Reconciler{
@@ -808,30 +808,31 @@ spec:
schematic:
cue:
template: |
import ("vela/op")
parameter: {
namespace: string
}
import (
"vela/kube"
"vela/builtin"
)
parameter: namespace: string
// apply workload to kubernetes cluster
apply: op.#Apply & {
value: {
apiVersion: "example.com/v1"
kind: "Foo"
metadata: {
name: "test-foo"
namespace: parameter.namespace
}
}
apply: kube.#Apply & {
$params: value: {
apiVersion: "example.com/v1"
kind: "Foo"
metadata: {
name: "test-foo"
namespace: parameter.namespace
}
}
}
// wait until workload.status equal "Running"
w: *false | bool
if apply.value.spec != _|_ if apply.value.spec.key != "" {
w: true
if apply.$returns.value.spec != _|_ if apply.$returns.value.spec.key != "" {
w: true
}
wait: op.#ConditionalWait & {
continue: w
wait: builtin.#ConditionalWait & {
$params: continue: w
}
`
)
+2 -2
View File
@@ -430,7 +430,7 @@ func (def *Definition) FromCUEString(cueString string, _ *rest.Config) error {
return errors.Wrapf(err, "failed to encode template decls to string")
}
inst, err := providers.Compiler.Get().CompileStringWithOptions(context.Background(), metadataString, cuex.DisableResolveProviderFunctions{})
inst, err := providers.DefaultCompiler.Get().CompileStringWithOptions(context.Background(), metadataString, cuex.DisableResolveProviderFunctions{})
if err != nil {
return err
}
@@ -438,7 +438,7 @@ func (def *Definition) FromCUEString(cueString string, _ *rest.Config) error {
if err != nil {
return err
}
if _, err := providers.Compiler.Get().CompileStringWithOptions(context.Background(), templateString+"\n"+velacue.BaseTemplate, cuex.DisableResolveProviderFunctions{}); err != nil {
if _, err := providers.DefaultCompiler.Get().CompileStringWithOptions(context.Background(), templateString+"\n"+velacue.BaseTemplate, cuex.DisableResolveProviderFunctions{}); err != nil {
return err
}
return def.FromCUE(&inst, templateString)
+16 -1
View File
@@ -41,6 +41,8 @@ import (
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"github.com/kubevela/pkg/util/singleton"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/utils/common"
@@ -163,6 +165,19 @@ func (meta *GenMeta) Init(c common.Args, langArgs []string) (err error) {
if err != nil {
klog.Info("No kubeconfig found, skipping")
}
clt, err := c.GetClient()
if err != nil {
return fmt.Errorf("failed to get client: %w", err)
}
singleton.KubeClient.Set(clt)
dc, err := c.GetDynamicClient()
if err != nil {
return fmt.Errorf("failed to get dynamic client: %w", err)
}
singleton.DynamicClient.Set(dc)
if _, ok := SupportedLangs[meta.Lang]; !ok {
return fmt.Errorf("language %s is not supported", meta.Lang)
}
@@ -385,7 +400,7 @@ func (g *Generator) GetDefinitionValue(ctx context.Context, cueBytes []byte) (cu
return cue.Value{}, "", "", errors.New("definition doesn't include cue schematic")
}
template, err := providers.Compiler.Get().CompileStringWithOptions(ctx, templateString+velacue.BaseTemplate, cuex.DisableResolveProviderFunctions{})
template, err := providers.DefaultCompiler.Get().CompileStringWithOptions(ctx, templateString+velacue.BaseTemplate, cuex.DisableResolveProviderFunctions{})
if err != nil {
return cue.Value{}, "", "", err
}
+3 -3
View File
@@ -251,7 +251,7 @@ func (m *GoModuleModifier) addSubGoMod() error {
"--rm",
"-v", m.apiDir+":/api",
"-w", "/api",
"golang:1.19-alpine",
"golang:1.22-alpine3.18",
"go", "get", fmt.Sprintf("%s@%s", m.Package, m.LangArgs.Get(mainModuleVersionKey)),
))
}
@@ -261,7 +261,7 @@ func (m *GoModuleModifier) addSubGoMod() error {
"-v", m.apiDir+":/api",
"-w", "/api",
"--env", "GOPROXY="+m.LangArgs.Get(goProxyKey),
"golang:1.19-alpine",
"golang:1.22-alpine3.18",
"go", "mod", "tidy",
))
for _, cmd := range cmds {
@@ -293,7 +293,7 @@ func (m *GoModuleModifier) tidyMainMod() error {
"--rm",
"-v", outDir+":/api",
"-w", "/api",
"golang:1.19-alpine",
"golang:1.22-alpine3.18",
"go", "mod", "tidy",
)
if m.Verbose {
+1 -1
View File
@@ -51,7 +51,7 @@ const ErrGenerateOpenAPIV2JSONSchemaForCapability = "cannot generate OpenAPI v3
// ParsePropertiesToSchema parse the properties in cue script to the openapi schema
func ParsePropertiesToSchema(ctx context.Context, s string, templateFieldPath ...string) (*openapi3.Schema, error) {
t := s + "\n" + BaseTemplate
val, err := providers.Compiler.Get().CompileStringWithOptions(ctx, t, cuex.DisableResolveProviderFunctions{})
val, err := providers.DefaultCompiler.Get().CompileStringWithOptions(ctx, t, cuex.DisableResolveProviderFunctions{})
if err != nil {
return nil, err
}
+27 -5
View File
@@ -21,6 +21,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/clientcmd/api"
@@ -32,11 +33,12 @@ import (
// Args is args for controller-runtime client
type Args struct {
config *rest.Config
rawConfig *api.Config
Schema *runtime.Scheme
client client.Client
dc *discovery.DiscoveryClient
config *rest.Config
rawConfig *api.Config
Schema *runtime.Scheme
client client.Client
dc *discovery.DiscoveryClient
dynamicClient dynamic.Interface
}
// SetConfig insert kubeconfig into Args
@@ -151,3 +153,23 @@ func (a *Args) GetDiscoveryClient() (*discovery.DiscoveryClient, error) {
a.dc = dc
return dc, nil
}
// GetDynamicClient return a dynamic client from cli args
func (a *Args) GetDynamicClient() (dynamic.Interface, error) {
if a.dynamicClient != nil {
return a.dynamicClient, nil
}
cfg, err := a.GetConfig()
if err != nil {
return nil, err
}
dynClient, err := dynamic.NewForConfig(cfg)
if err != nil {
return nil, err
}
a.dynamicClient = dynClient
return dynClient, nil
}
+2
View File
@@ -37,6 +37,7 @@ import (
"cuelang.org/go/encoding/openapi"
"github.com/AlecAivazis/survey/v2"
"github.com/hashicorp/hcl/v2/hclparse"
cuexv1alpha1 "github.com/kubevela/pkg/apis/cue/v1alpha1"
"github.com/oam-dev/terraform-config-inspect/tfconfig"
kruise "github.com/openkruise/kruise-api/apps/v1alpha1"
kruisev1alpha1 "github.com/openkruise/rollouts/api/v1alpha1"
@@ -94,6 +95,7 @@ func init() {
_ = kruisev1alpha1.AddToScheme(Scheme)
_ = gatewayv1beta1.AddToScheme(Scheme)
_ = workflowv1alpha1.AddToScheme(Scheme)
_ = cuexv1alpha1.AddToScheme(Scheme)
// +kubebuilder:scaffold:scheme
}
+1 -1
View File
@@ -104,7 +104,7 @@ func ParseVelaQLFromPath(ctx context.Context, velaQLViewPath string) (*QueryView
if err != nil {
return nil, errors.Errorf("read view file from %s: %v", velaQLViewPath, err)
}
val, err := providers.Compiler.Get().CompileString(ctx, string(body))
val, err := providers.DefaultCompiler.Get().CompileString(ctx, string(body))
if err != nil {
return nil, errors.Errorf("error when parsing view: %v", err)
}
+8 -1
View File
@@ -22,11 +22,13 @@ import (
"testing"
"time"
cuexv1alpha1 "github.com/kubevela/pkg/apis/cue/v1alpha1"
"github.com/kubevela/pkg/util/singleton"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -64,11 +66,16 @@ var _ = BeforeSuite(func() {
By("new kube client")
cfg.Timeout = time.Minute * 2
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
testScheme := common.Scheme
err = cuexv1alpha1.AddToScheme(testScheme)
Expect(err).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
By("new kube client success")
singleton.KubeClient.Set(k8sClient)
fakeDynamicClient := fake.NewSimpleDynamicClient(testScheme)
singleton.DynamicClient.Set(fakeDynamicClient)
viewHandler = NewViewHandler(k8sClient, cfg)
ctx := context.Background()
+3 -3
View File
@@ -44,7 +44,7 @@ import (
"github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/workflow/providers"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
"github.com/oam-dev/kubevela/pkg/workflow/template"
)
@@ -91,7 +91,7 @@ func (handler *ViewHandler) QueryView(ctx context.Context, qv QueryView) (cue.Va
if err != nil {
return cue.Value{}, fmt.Errorf("failed to load query templates: %w", err)
}
v, err := providers.Compiler.Get().CompileStringWithOptions(ctx, temp, cuex.WithExtraData("parameter", qv.Parameter))
v, err := providers.DefaultCompiler.Get().CompileStringWithOptions(ctx, temp, cuex.WithExtraData("parameter", qv.Parameter))
if err != nil {
return cue.Value{}, fmt.Errorf("failed to compile query: %w", err)
}
@@ -121,7 +121,7 @@ func (handler *ViewHandler) delete(ctx context.Context, _ client.Client, _ strin
//
// For now, we only check 1. cue is valid 2. `status` or `view` field exists
func ValidateView(ctx context.Context, viewStr string) error {
val, err := providers.Compiler.Get().CompileStringWithOptions(ctx, viewStr, cuex.DisableResolveProviderFunctions{})
val, err := providers.DefaultCompiler.Get().CompileStringWithOptions(ctx, viewStr, cuex.DisableResolveProviderFunctions{})
if err != nil {
return errors.Errorf("error when parsing view: %v", err)
}
+57 -7
View File
@@ -17,30 +17,80 @@ limitations under the License.
package providers
import (
"context"
"github.com/kubevela/pkg/cue/cuex"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
"github.com/kubevela/pkg/util/runtime"
"github.com/kubevela/pkg/util/singleton"
"k8s.io/klog/v2"
"github.com/kubevela/workflow/pkg/providers/builtin"
"github.com/kubevela/workflow/pkg/providers/email"
"github.com/kubevela/workflow/pkg/providers/http"
"github.com/kubevela/workflow/pkg/providers/kube"
"github.com/kubevela/workflow/pkg/providers/metrics"
"github.com/kubevela/workflow/pkg/providers/time"
"github.com/kubevela/workflow/pkg/providers/util"
"github.com/oam-dev/kubevela/pkg/workflow/providers/config"
"github.com/oam-dev/kubevela/pkg/workflow/providers/legacy"
"github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/query"
legacyquery "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/query"
"github.com/oam-dev/kubevela/pkg/workflow/providers/multicluster"
"github.com/oam-dev/kubevela/pkg/workflow/providers/oam"
"github.com/oam-dev/kubevela/pkg/workflow/providers/query"
"github.com/oam-dev/kubevela/pkg/workflow/providers/terraform"
)
const (
// LegacyProviderName is the name of legacy provider
LegacyProviderName = "op"
// ConfigProviderName is the name of config provider
ConfigProviderName = "config"
// QLProviderName is the name of ql provider
QLProviderName = "ql"
)
// Compiler is the workflow default compiler
var Compiler = singleton.NewSingletonE[*cuex.Compiler](func() (*cuex.Compiler, error) {
var (
// EnableExternalPackageForDefaultCompiler .
EnableExternalPackageForDefaultCompiler = true
// EnableExternalPackageWatchForDefaultCompiler .
EnableExternalPackageWatchForDefaultCompiler = false
)
// compiler is the workflow default compiler
var compiler = singleton.NewSingletonE[*cuex.Compiler](func() (*cuex.Compiler, error) {
return cuex.NewCompilerWithInternalPackages(
// legacy packages
runtime.Must(cuexruntime.NewInternalPackage(LegacyProviderName, legacy.GetLegacyTemplate(), legacy.GetLegacyProviders())),
// runtime.Must(cuexruntime.NewInternalPackage(ConfigProviderName, config.GetTemplate(), config.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage(QLProviderName, query.GetTemplate(), query.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage(QLProviderName, legacyquery.GetTemplate(), legacyquery.GetProviders())),
// workflow internal packages
runtime.Must(cuexruntime.NewInternalPackage("email", email.GetTemplate(), email.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("http", http.GetTemplate(), http.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("kube", kube.GetTemplate(), kube.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("metrics", metrics.GetTemplate(), metrics.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("time", time.GetTemplate(), time.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("util", util.GetTemplate(), util.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("builtin", builtin.GetTemplate(), builtin.GetProviders())),
// kubevela internal packages
runtime.Must(cuexruntime.NewInternalPackage("multicluster", multicluster.GetTemplate(), multicluster.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("config", config.GetTemplate(), config.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("oam", oam.GetTemplate(), oam.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("query", query.GetTemplate(), query.GetProviders())),
runtime.Must(cuexruntime.NewInternalPackage("terraform", terraform.GetTemplate(), terraform.GetProviders())),
), nil
})
// DefaultCompiler compiler for cuex to compile
var DefaultCompiler = singleton.NewSingleton[*cuex.Compiler](func() *cuex.Compiler {
c := compiler.Get()
if EnableExternalPackageForDefaultCompiler {
if err := c.LoadExternalPackages(context.Background()); err != nil {
klog.Errorf("failed to load external packages for cuex default compiler: ", err.Error())
}
}
if EnableExternalPackageWatchForDefaultCompiler {
go c.ListenExternalPackages(nil)
}
return c
})
+54
View File
@@ -0,0 +1,54 @@
// config.cue
#CreateConfig: {
#do: "create"
#provider: "config"
$params: {
name: string
namespace: string
template?: string
config: {
...
}
}
}
#DeleteConfig: {
#do: "delete"
#provider: "config"
$params: {
name: string
namespace: string
}
}
#ReadConfig: {
#do: "read"
#provider: "config"
$params: {
name: string
namespace: string
}
$returns: {
config: {...}
}
}
#ListConfig: {
#do: "list"
#provider: "config"
$params: {
// Must query with the template
template: string
namespace: string
}
$returns: {
configs: [...{...}]
}
}
+170
View File
@@ -0,0 +1,170 @@
/*
Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"context"
_ "embed"
"errors"
"strings"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
"github.com/oam-dev/kubevela/pkg/config"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
const (
// ProviderName is provider name
ProviderName = "config"
)
// ErrRequestInvalid means the request is invalid
var ErrRequestInvalid = errors.New("the request is in valid")
// CreateConfigProperties the request body for creating a config
type CreateConfigProperties struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Template string `json:"template,omitempty"`
Config map[string]interface{} `json:"config"`
}
// CreateParams is the create params
type CreateParams = oamprovidertypes.Params[CreateConfigProperties]
// CreateConfig creates a config
func CreateConfig(ctx context.Context, params *CreateParams) (*any, error) {
ccp := params.Params
name := ccp.Template
namespace := "vela-system"
if strings.Contains(ccp.Template, "/") {
namespacedName := strings.SplitN(ccp.Template, "/", 2)
namespace = namespacedName[0]
name = namespacedName[1]
}
factory := params.ConfigFactory
configItem, err := factory.ParseConfig(ctx, config.NamespacedName{
Name: name,
Namespace: namespace,
}, config.Metadata{
NamespacedName: config.NamespacedName{
Name: ccp.Name,
Namespace: ccp.Namespace,
},
Properties: ccp.Config,
})
if err != nil {
return nil, err
}
return nil, factory.CreateOrUpdateConfig(ctx, configItem, ccp.Namespace)
}
// ReadReturnVars is the read return vars
type ReadReturnVars struct {
Config map[string]any `json:"config"`
}
// ReadReturns is the read returns
type ReadReturns = oamprovidertypes.Returns[ReadReturnVars]
// ReadConfig reads the config
func ReadConfig(ctx context.Context, params *oamprovidertypes.Params[config.NamespacedName]) (*ReadReturns, error) {
nn := params.Params
factory := params.ConfigFactory
content, err := factory.ReadConfig(ctx, nn.Namespace, nn.Name)
if err != nil {
return nil, err
}
return &ReadReturns{
Returns: ReadReturnVars{
Config: content,
},
}, nil
}
// ListVars is the list vars
type ListVars struct {
Namespace string `json:"namespace"`
Template string `json:"template"`
}
// ListReturnVars is the list return vars
type ListReturnVars struct {
Configs []map[string]any `json:"configs"`
}
// ListReturns is the list returns
type ListReturns = oamprovidertypes.Returns[ListReturnVars]
// ListConfig lists the config
func ListConfig(ctx context.Context, params *oamprovidertypes.Params[ListVars]) (*ListReturns, error) {
template := params.Params.Template
namespace := params.Params.Namespace
if template == "" || namespace == "" {
return nil, ErrRequestInvalid
}
if strings.Contains(template, "/") {
namespacedName := strings.SplitN(template, "/", 2)
template = namespacedName[1]
}
factory := params.ConfigFactory
configs, err := factory.ListConfigs(ctx, namespace, template, "", false)
if err != nil {
return nil, err
}
var contents = []map[string]interface{}{}
for _, c := range configs {
contents = append(contents, map[string]interface{}{
"name": c.Name,
"alias": c.Alias,
"description": c.Description,
"config": c.Properties,
})
}
return &ListReturns{
Returns: ListReturnVars{
Configs: contents,
},
}, nil
}
// DeleteConfig deletes a config
func DeleteConfig(ctx context.Context, params *oamprovidertypes.Params[config.NamespacedName]) (*any, error) {
nn := params.Params
factory := params.ConfigFactory
return nil, factory.DeleteConfig(ctx, nn.Namespace, nn.Name)
}
//go:embed config.cue
var template string
// GetTemplate returns the cue template.
func GetTemplate() string {
return template
}
// GetProviders returns the cue providers.
func GetProviders() map[string]cuexruntime.ProviderFn {
return map[string]cuexruntime.ProviderFn{
"create": oamprovidertypes.GenericProviderFn[CreateConfigProperties, any](CreateConfig),
"read": oamprovidertypes.GenericProviderFn[config.NamespacedName, ReadReturns](ReadConfig),
"list": oamprovidertypes.GenericProviderFn[ListVars, ListReturns](ListConfig),
"delete": oamprovidertypes.GenericProviderFn[config.NamespacedName, any](DeleteConfig),
}
}
@@ -0,0 +1,247 @@
/*
Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package config
import (
"context"
"strings"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/apimachinery/pkg/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/config"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var scheme = runtime.NewScheme()
var factory config.Factory
func TestProvider(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Test Config Suite")
}
var _ = BeforeSuite(func() {
By("Bootstrapping test environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.Bool(false),
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
Expect(clientgoscheme.AddToScheme(scheme)).Should(BeNil())
// +kubebuilder:scaffold:scheme
By("Create the k8s client")
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
factory = config.NewConfigFactory(k8sClient)
})
var _ = AfterSuite(func() {
By("Tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
var _ = Describe("Test the config provider", func() {
It("test creating a config", func() {
ctx := context.Background()
params := &CreateParams{
Params: CreateConfigProperties{
Name: "hub-kubevela",
Namespace: "default",
Template: "default/test-image-registry",
Config: map[string]interface{}{
"registry": "hub.kubevela.net",
},
},
RuntimeParams: oamprovidertypes.RuntimeParams{
ConfigFactory: factory,
},
}
_, err := CreateConfig(ctx, params)
Expect(strings.Contains(err.Error(), "the template does not exist")).Should(BeTrue())
template, err := factory.ParseTemplate(ctx, "test-image-registry", []byte(templateContent))
Expect(err).ToNot(HaveOccurred())
Expect(factory.CreateOrUpdateConfigTemplate(ctx, "default", template)).ToNot(HaveOccurred())
_, err = CreateConfig(ctx, params)
Expect(err).ToNot(HaveOccurred())
})
It("test creating a config without the template", func() {
params := &CreateParams{
Params: CreateConfigProperties{
Name: "www-kubevela",
Namespace: "default",
Config: map[string]interface{}{
"url": "kubevela.net",
},
},
RuntimeParams: oamprovidertypes.RuntimeParams{
ConfigFactory: factory,
},
}
_, err := CreateConfig(context.Background(), params)
Expect(err).ToNot(HaveOccurred())
})
It("test listing the config", func() {
ctx := context.Background()
res, err := ListConfig(ctx, &oamprovidertypes.Params[ListVars]{
Params: ListVars{
Namespace: "default",
Template: "test-image-registry",
},
RuntimeParams: oamprovidertypes.RuntimeParams{
ConfigFactory: factory,
},
})
Expect(err).ToNot(HaveOccurred())
contents := res.Returns.Configs
Expect(len(contents)).To(Equal(1))
Expect(contents[0]["config"].(map[string]interface{})["registry"]).To(Equal("hub.kubevela.net"))
})
It("test reading the config", func() {
ctx := context.Background()
res, err := ReadConfig(ctx, &oamprovidertypes.Params[config.NamespacedName]{
Params: config.NamespacedName{
Namespace: "default",
Name: "hub-kubevela",
},
RuntimeParams: oamprovidertypes.RuntimeParams{
ConfigFactory: factory,
},
})
Expect(err).ToNot(HaveOccurred())
Expect(res.Returns.Config["registry"]).To(Equal("hub.kubevela.net"))
})
It("test deleting the config", func() {
ctx := context.Background()
_, err := DeleteConfig(ctx, &oamprovidertypes.Params[config.NamespacedName]{
Params: config.NamespacedName{
Namespace: "default",
Name: "hub-kubevela",
},
RuntimeParams: oamprovidertypes.RuntimeParams{
ConfigFactory: factory,
},
})
Expect(err).ToNot(HaveOccurred())
configs, err := factory.ListConfigs(context.Background(), "default", "", "", false)
Expect(err).ToNot(HaveOccurred())
Expect(len(configs)).To(Equal(1))
Expect(configs[0].Properties["url"]).To(Equal("kubevela.net"))
})
})
var templateContent = `
import (
"encoding/base64"
"encoding/json"
"strconv"
)
metadata: {
name: "image-registry"
alias: "Image Registry"
scope: "project"
description: "Config information to authenticate image registry"
sensitive: false
}
template: {
output: {
apiVersion: "v1"
kind: "Secret"
metadata: {
name: context.name
namespace: context.namespace
labels: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "image-registry"
}
}
if parameter.auth != _|_ {
type: "kubernetes.io/dockerconfigjson"
}
if parameter.auth == _|_ {
type: "Opaque"
}
stringData: {
if parameter.auth != _|_ && parameter.auth.username != _|_ {
".dockerconfigjson": json.Marshal({
"auths": (parameter.registry): {
"username": parameter.auth.username
"password": parameter.auth.password
if parameter.auth.email != _|_ {
"email": parameter.auth.email
}
"auth": base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password))
}
})
}
if parameter.insecure != _|_ {
"insecure-skip-verify": strconv.FormatBool(parameter.insecure)
}
if parameter.useHTTP != _|_ {
"protocol-use-http": strconv.FormatBool(parameter.useHTTP)
}
}
}
parameter: {
// +usage=Image registry FQDN, such as: index.docker.io
registry: *"index.docker.io" | string
// +usage=Authenticate the image registry
auth?: {
// +usage=Private Image registry username
username: string
// +usage=Private Image registry password
password: string
// +usage=Private Image registry email
email?: string
}
// +usage=For the registry server that uses the self-signed certificate
insecure?: bool
// +usage=For the registry server that uses the HTTP protocol
useHTTP?: bool
}
}
`
@@ -1,8 +1,8 @@
// config.cue
#Create: {
#CreateConfig: {
#do: "create"
#provider: "config"
#provider: "op"
name: string
namespace: string
@@ -12,17 +12,17 @@
}
}
#Delete: {
#DeleteConfig: {
#do: "delete"
#provider: "config"
#provider: "op"
name: string
namespace: string
}
#Read: {
#ReadConfig: {
#do: "read"
#provider: "config"
#provider: "op"
name: string
namespace: string
@@ -30,9 +30,9 @@
config: {...}
}
#List: {
#ListConfig: {
#do: "list"
#provider: "config"
#provider: "op"
// Must query with the template
template: string
@@ -25,7 +25,7 @@ import (
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
"github.com/oam-dev/kubevela/pkg/config"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
const (
@@ -148,9 +148,9 @@ func GetTemplate() string {
// GetProviders returns the cue providers.
func GetProviders() map[string]cuexruntime.ProviderFn {
return map[string]cuexruntime.ProviderFn{
"create-config": oamprovidertypes.OAMGenericProviderFn[CreateConfigProperties, any](CreateConfig),
"read-config": oamprovidertypes.OAMGenericProviderFn[config.NamespacedName, ReadResult](ReadConfig),
"list-config": oamprovidertypes.OAMGenericProviderFn[ListVars, ListResult](ListConfig),
"delete-config": oamprovidertypes.OAMGenericProviderFn[config.NamespacedName, any](DeleteConfig),
"create": oamprovidertypes.OAMGenericProviderFn[CreateConfigProperties, any](CreateConfig),
"read": oamprovidertypes.OAMGenericProviderFn[config.NamespacedName, ReadResult](ReadConfig),
"list": oamprovidertypes.OAMGenericProviderFn[ListVars, ListResult](ListConfig),
"delete": oamprovidertypes.OAMGenericProviderFn[config.NamespacedName, any](DeleteConfig),
}
}
@@ -32,7 +32,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/config"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
var cfg *rest.Config
+3
View File
@@ -23,6 +23,7 @@ import (
wflegacy "github.com/kubevela/workflow/pkg/providers/legacy"
"github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/config"
"github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/multicluster"
"github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/oam"
"github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/terraform"
@@ -42,6 +43,7 @@ func GetLegacyProviders() map[string]cuexruntime.ProviderFn {
registerProviders(providers, multicluster.GetProviders())
registerProviders(providers, oam.GetProviders())
registerProviders(providers, terraform.GetProviders())
registerProviders(providers, config.GetProviders())
registerProviders(providers, wflegacy.GetLegacyProviders())
return providers
@@ -53,6 +55,7 @@ func GetLegacyTemplate() string {
multicluster.GetTemplate(),
oam.GetTemplate(),
terraform.GetTemplate(),
config.GetTemplate(),
wflegacy.GetLegacyTemplate(),
},
"\n")
@@ -45,7 +45,7 @@ import (
"github.com/oam-dev/kubevela/pkg/resourcekeeper"
"github.com/oam-dev/kubevela/pkg/utils"
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
// DeployParameter is the parameter of deploy workflow step
@@ -30,7 +30,7 @@ import (
"github.com/oam-dev/kubevela/pkg/multicluster"
pkgpolicy "github.com/oam-dev/kubevela/pkg/policy"
"github.com/oam-dev/kubevela/pkg/policy/envbinding"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
// Inputs is the inputs for multi cluster
@@ -37,7 +37,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/utils/common"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func TestMakePlacementDecisions(t *testing.T) {
+1 -1
View File
@@ -31,7 +31,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
const (
@@ -35,7 +35,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func setupClient(ctx context.Context, t *testing.T) client.Client {
@@ -37,7 +37,7 @@ import (
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
var _ = Describe("Test query endpoints", func() {
@@ -43,7 +43,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/multicluster"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
const (
@@ -39,7 +39,7 @@ import (
helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
type AppResourcesList struct {
@@ -45,7 +45,7 @@ import (
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func TestPodStatus(t *testing.T) {
@@ -28,7 +28,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
// Outputs is the output parameters for Terraform components.
@@ -31,7 +31,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/legacy/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func fakeWorkloadRenderer(_ context.Context, comp apicommon.ApplicationComponent) (*appfile.Component, error) {
@@ -0,0 +1,433 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package multicluster
import (
"context"
"fmt"
"strings"
"sync"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
pkgmaps "github.com/kubevela/pkg/util/maps"
"github.com/kubevela/pkg/util/slices"
"github.com/kubevela/workflow/pkg/cue/model/value"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
workflowerrors "github.com/kubevela/workflow/pkg/errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/oam"
pkgpolicy "github.com/oam-dev/kubevela/pkg/policy"
"github.com/oam-dev/kubevela/pkg/policy/envbinding"
"github.com/oam-dev/kubevela/pkg/resourcekeeper"
"github.com/oam-dev/kubevela/pkg/utils"
velaerrors "github.com/oam-dev/kubevela/pkg/utils/errors"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
// DeployParameter is the parameter of deploy workflow step
type DeployParameter struct {
// Declare the policies that used for this deployment. If not specified, the components will be deployed to the hub cluster.
Policies []string `json:"policies,omitempty"`
// Maximum number of concurrent delivered components.
Parallelism int64 `json:"parallelism"`
// If set false, this step will apply the components with the terraform workload.
IgnoreTerraformComponent bool `json:"ignoreTerraformComponent"`
// The policies that embeds in the `deploy` step directly
InlinePolicies []v1beta1.AppPolicy `json:"inlinePolicies,omitempty"`
}
// DeployWorkflowStepExecutor executor to run deploy workflow step
type DeployWorkflowStepExecutor interface {
Deploy(ctx context.Context) (healthy bool, reason string, err error)
}
// NewDeployWorkflowStepExecutor .
func NewDeployWorkflowStepExecutor(cli client.Client, af *appfile.Appfile, apply oamprovidertypes.ComponentApply, healthCheck oamprovidertypes.ComponentHealthCheck, renderer oamprovidertypes.WorkloadRender, parameter DeployParameter) DeployWorkflowStepExecutor {
return &deployWorkflowStepExecutor{
cli: cli,
af: af,
apply: apply,
healthCheck: healthCheck,
renderer: renderer,
parameter: parameter,
}
}
type deployWorkflowStepExecutor struct {
cli client.Client
af *appfile.Appfile
apply oamprovidertypes.ComponentApply
healthCheck oamprovidertypes.ComponentHealthCheck
renderer oamprovidertypes.WorkloadRender
parameter DeployParameter
}
// Deploy execute deploy workflow step
func (executor *deployWorkflowStepExecutor) Deploy(ctx context.Context) (bool, string, error) {
policies, err := selectPolicies(executor.af.Policies, executor.parameter.Policies)
if err != nil {
return false, "", err
}
policies = append(policies, fillInlinePolicyNames(executor.parameter.InlinePolicies)...)
components, err := loadComponents(ctx, executor.renderer, executor.cli, executor.af, executor.af.Components, executor.parameter.IgnoreTerraformComponent)
if err != nil {
return false, "", err
}
// Dealing with topology, override and replication policies in order.
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(ctx, executor.cli, executor.af.Namespace, policies, resourcekeeper.AllowCrossNamespaceResource)
if err != nil {
return false, "", err
}
components, err = overrideConfiguration(policies, components)
if err != nil {
return false, "", err
}
components, err = pkgpolicy.ReplicateComponents(policies, components)
if err != nil {
return false, "", err
}
return applyComponents(ctx, executor.apply, executor.healthCheck, components, placements, int(executor.parameter.Parallelism))
}
func selectPolicies(policies []v1beta1.AppPolicy, policyNames []string) ([]v1beta1.AppPolicy, error) {
policyMap := make(map[string]v1beta1.AppPolicy)
for _, policy := range policies {
policyMap[policy.Name] = policy
}
var selectedPolicies []v1beta1.AppPolicy
for _, policyName := range policyNames {
if policy, found := policyMap[policyName]; found {
selectedPolicies = append(selectedPolicies, policy)
} else {
return nil, errors.Errorf("policy %s not found", policyName)
}
}
return selectedPolicies, nil
}
func fillInlinePolicyNames(policies []v1beta1.AppPolicy) []v1beta1.AppPolicy {
for i := range policies {
if policies[i].Name == "" {
policies[i].Name = fmt.Sprintf("inline-%s-policy-%d", policies[i].Type, i)
}
}
return policies
}
func loadComponents(ctx context.Context, render oamprovidertypes.WorkloadRender, cli client.Client, af *appfile.Appfile, components []common.ApplicationComponent, ignoreTerraformComponent bool) ([]common.ApplicationComponent, error) {
var loadedComponents []common.ApplicationComponent
for _, comp := range components {
loadedComp, err := af.LoadDynamicComponent(ctx, cli, comp.DeepCopy())
if err != nil {
return nil, err
}
if ignoreTerraformComponent {
wl, err := render(ctx, comp)
if err != nil {
return nil, errors.Wrapf(err, "failed to render component into workload")
}
if wl.CapabilityCategory == types.TerraformCategory {
continue
}
}
loadedComponents = append(loadedComponents, *loadedComp)
}
return loadedComponents, nil
}
func overrideConfiguration(policies []v1beta1.AppPolicy, components []common.ApplicationComponent) ([]common.ApplicationComponent, error) {
var err error
for _, policy := range policies {
if policy.Type == v1alpha1.OverridePolicyType {
if policy.Properties == nil {
return nil, fmt.Errorf("override policy %s must not have empty properties", policy.Name)
}
overrideSpec := &v1alpha1.OverridePolicySpec{}
if err := utils.StrictUnmarshal(policy.Properties.Raw, overrideSpec); err != nil {
return nil, errors.Wrapf(err, "failed to parse override policy %s", policy.Name)
}
components, err = envbinding.PatchComponents(components, overrideSpec.Components, overrideSpec.Selector)
if err != nil {
return nil, errors.Wrapf(err, "failed to apply override policy %s", policy.Name)
}
}
}
return components, nil
}
type valueBuilder func(s string) cue.Value
type applyTask struct {
component common.ApplicationComponent
placement v1alpha1.PlacementDecision
healthy *bool
}
func (t *applyTask) key() string {
return fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, t.component.ReplicaKey, t.component.Name)
}
func (t *applyTask) varKey(v string) string {
return fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, t.component.ReplicaKey, v)
}
func (t *applyTask) varKeyWithoutReplica(v string) string {
return fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, "", v)
}
func (t *applyTask) getVar(from string, cache *pkgmaps.SyncMap[string, cue.Value]) cue.Value {
key := t.varKey(from)
keyWithNoReplica := t.varKeyWithoutReplica(from)
var val cue.Value
var ok bool
if val, ok = cache.Get(key); !ok {
if val, ok = cache.Get(keyWithNoReplica); !ok {
return cue.Value{}
}
}
return val
}
func (t *applyTask) fillInputs(inputs *pkgmaps.SyncMap[string, cue.Value], build valueBuilder) error {
if len(t.component.Inputs) == 0 {
return nil
}
var err error
x := component2Value(t.component, build)
for _, input := range t.component.Inputs {
var inputVal cue.Value
if inputVal = t.getVar(input.From, inputs); inputVal == (cue.Value{}) {
return fmt.Errorf("input %s is not ready", input)
}
x, err = value.SetValueByScript(x, inputVal, fieldPathToComponent(input.ParameterKey))
if err != nil {
return errors.Wrap(err, "fill value to component")
}
}
newComp, err := value2Component(x)
if err != nil {
return err
}
t.component = *newComp
return nil
}
func (t *applyTask) generateOutput(output *unstructured.Unstructured, outputs []*unstructured.Unstructured, cache *pkgmaps.SyncMap[string, cue.Value], build valueBuilder) error {
if len(t.component.Outputs) == 0 {
return nil
}
var cueString string
if output != nil {
outputJSON, err := output.MarshalJSON()
if err != nil {
return errors.Wrap(err, "marshal output")
}
cueString += fmt.Sprintf("output:%s\n", string(outputJSON))
}
componentVal := build(cueString)
for _, os := range outputs {
name := os.GetLabels()[oam.TraitResource]
if name != "" {
componentVal = componentVal.FillPath(cue.ParsePath(fmt.Sprintf("outputs.%s", name)), os.Object)
}
}
for _, o := range t.component.Outputs {
pathToSetVar := t.varKey(o.Name)
actualOutput := componentVal.LookupPath(cue.ParsePath(o.ValueFrom))
if !actualOutput.Exists() {
return workflowerrors.LookUpNotFoundErr(o.ValueFrom)
}
cache.Set(pathToSetVar, actualOutput)
}
return nil
}
func (t *applyTask) allDependsReady(healthyMap map[string]bool) bool {
for _, d := range t.component.DependsOn {
dKey := fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, t.component.ReplicaKey, d)
dKeyWithoutReplica := fmt.Sprintf("%s/%s/%s/%s", t.placement.Cluster, t.placement.Namespace, "", d)
if !healthyMap[dKey] && !healthyMap[dKeyWithoutReplica] {
return false
}
}
return true
}
func (t *applyTask) allInputReady(cache *pkgmaps.SyncMap[string, cue.Value]) bool {
for _, in := range t.component.Inputs {
if val := t.getVar(in.From, cache); val == (cue.Value{}) {
return false
}
}
return true
}
type applyTaskResult struct {
healthy bool
err error
task *applyTask
}
// applyComponents will apply components to placements.
func applyComponents(ctx context.Context, apply oamprovidertypes.ComponentApply, healthCheck oamprovidertypes.ComponentHealthCheck, components []common.ApplicationComponent, placements []v1alpha1.PlacementDecision, parallelism int) (bool, string, error) {
var tasks []*applyTask
var cache = pkgmaps.NewSyncMap[string, cue.Value]()
rootValue := cuecontext.New().CompileString("{}")
if rootValue.Err() != nil {
return false, "", rootValue.Err()
}
var cueMutex sync.Mutex
var makeValue = func(s string) cue.Value {
cueMutex.Lock()
defer cueMutex.Unlock()
return rootValue.Context().CompileString(s)
}
taskHealthyMap := map[string]bool{}
for _, comp := range components {
for _, pl := range placements {
tasks = append(tasks, &applyTask{component: comp, placement: pl})
}
}
unhealthyResults := make([]*applyTaskResult, 0)
maxHealthCheckTimes := len(tasks)
HealthCheck:
for i := 0; i < maxHealthCheckTimes; i++ {
checkTasks := make([]*applyTask, 0)
for _, task := range tasks {
if task.healthy == nil && task.allDependsReady(taskHealthyMap) && task.allInputReady(cache) {
task.healthy = new(bool)
err := task.fillInputs(cache, makeValue)
if err != nil {
taskHealthyMap[task.key()] = false
unhealthyResults = append(unhealthyResults, &applyTaskResult{healthy: false, err: err, task: task})
continue
}
checkTasks = append(checkTasks, task)
}
}
if len(checkTasks) == 0 {
break HealthCheck
}
checkResults := slices.ParMap[*applyTask, *applyTaskResult](checkTasks, func(task *applyTask) *applyTaskResult {
healthy, output, outputs, err := healthCheck(ctx, task.component, nil, task.placement.Cluster, task.placement.Namespace)
task.healthy = pointer.Bool(healthy)
if healthy {
err = task.generateOutput(output, outputs, cache, makeValue)
}
return &applyTaskResult{healthy: healthy, err: err, task: task}
}, slices.Parallelism(parallelism))
for _, res := range checkResults {
taskHealthyMap[res.task.key()] = res.healthy
if !res.healthy || res.err != nil {
unhealthyResults = append(unhealthyResults, res)
}
}
}
var pendingTasks []*applyTask
var todoTasks []*applyTask
for _, task := range tasks {
if healthy, ok := taskHealthyMap[task.key()]; healthy && ok {
continue
}
if task.allDependsReady(taskHealthyMap) && task.allInputReady(cache) {
todoTasks = append(todoTasks, task)
} else {
pendingTasks = append(pendingTasks, task)
}
}
var results []*applyTaskResult
if len(todoTasks) > 0 {
results = slices.ParMap[*applyTask, *applyTaskResult](todoTasks, func(task *applyTask) *applyTaskResult {
err := task.fillInputs(cache, makeValue)
if err != nil {
return &applyTaskResult{healthy: false, err: err, task: task}
}
_, _, healthy, err := apply(ctx, task.component, nil, task.placement.Cluster, task.placement.Namespace)
if err != nil {
return &applyTaskResult{healthy: healthy, err: err, task: task}
}
return &applyTaskResult{healthy: healthy, err: err, task: task}
}, slices.Parallelism(parallelism))
}
var errs []error
var allHealthy = true
var reasons []string
for _, res := range unhealthyResults {
if res.err != nil {
errs = append(errs, fmt.Errorf("error health check from %s: %w", res.task.key(), res.err))
}
}
for _, res := range results {
if res.err != nil {
errs = append(errs, fmt.Errorf("error encountered in cluster %s: %w", res.task.placement.Cluster, res.err))
}
if !res.healthy {
allHealthy = false
reasons = append(reasons, fmt.Sprintf("%s is not healthy", res.task.key()))
}
}
for _, t := range pendingTasks {
reasons = append(reasons, fmt.Sprintf("%s is waiting dependents", t.key()))
}
return allHealthy && len(pendingTasks) == 0, strings.Join(reasons, ","), velaerrors.AggregateErrors(errs)
}
func fieldPathToComponent(input string) string {
return fmt.Sprintf("properties.%s", strings.TrimSpace(input))
}
func component2Value(comp common.ApplicationComponent, build valueBuilder) cue.Value {
x := build("")
x = x.FillPath(cue.ParsePath(""), comp)
// Component.ReplicaKey have no json tag, so we need to set it manually
x = x.FillPath(cue.ParsePath("replicaKey"), comp.ReplicaKey)
return x
}
func value2Component(v cue.Value) (*common.ApplicationComponent, error) {
var comp common.ApplicationComponent
err := value.UnmarshalTo(v, &comp)
if err != nil {
return nil, err
}
if rk, err := v.LookupPath(cue.ParsePath("replicaKey")).String(); err == nil {
comp.ReplicaKey = rk
}
return &comp, nil
}
@@ -0,0 +1,410 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package multicluster
import (
"context"
"fmt"
"math/rand"
"sync"
"testing"
"time"
"cuelang.org/go/cue"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
)
func TestOverrideConfiguration(t *testing.T) {
testCases := map[string]struct {
Policies []v1beta1.AppPolicy
Components []apicommon.ApplicationComponent
Outputs []apicommon.ApplicationComponent
Error string
}{
"invalid-policies": {
Policies: []v1beta1.AppPolicy{{
Name: "override-policy",
Type: "override",
Properties: &runtime.RawExtension{Raw: []byte(`bad value`)},
}},
Error: "failed to parse override policy",
},
"empty-policy": {
Policies: []v1beta1.AppPolicy{{
Name: "override-policy",
Type: "override",
Properties: nil,
}},
Error: "empty properties",
},
"normal": {
Policies: []v1beta1.AppPolicy{{
Name: "override-policy",
Type: "override",
Properties: &runtime.RawExtension{Raw: []byte(`{"components":[{"name":"comp","properties":{"x":5}}]}`)},
}},
Components: []apicommon.ApplicationComponent{{
Name: "comp",
Traits: []apicommon.ApplicationTrait{},
Properties: &runtime.RawExtension{Raw: []byte(`{"x":1}`)},
}},
Outputs: []apicommon.ApplicationComponent{{
Name: "comp",
Traits: []apicommon.ApplicationTrait{},
Properties: &runtime.RawExtension{Raw: []byte(`{"x":5}`)},
}},
},
}
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
r := require.New(t)
comps, err := overrideConfiguration(tt.Policies, tt.Components)
if tt.Error != "" {
r.NotNil(err)
r.Contains(err.Error(), tt.Error)
} else {
r.NoError(err)
r.Equal(tt.Outputs, comps)
}
})
}
}
func TestApplyComponentsDepends(t *testing.T) {
r := require.New(t)
const n, m = 50, 5
var components []apicommon.ApplicationComponent
var placements []v1alpha1.PlacementDecision
for i := 0; i < n*3; i++ {
comp := apicommon.ApplicationComponent{Name: fmt.Sprintf("comp-%d", i)}
if i%3 != 0 {
comp.DependsOn = append(comp.DependsOn, fmt.Sprintf("comp-%d", i-1))
}
if i%3 == 2 {
comp.DependsOn = append(comp.DependsOn, fmt.Sprintf("comp-%d", i-1))
}
components = append(components, comp)
}
for i := 0; i < m; i++ {
placements = append(placements, v1alpha1.PlacementDecision{Cluster: fmt.Sprintf("cluster-%d", i)})
}
applyMap := &sync.Map{}
apply := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
time.Sleep(time.Duration(rand.Intn(200)+25) * time.Millisecond)
applyMap.Store(fmt.Sprintf("%s/%s", clusterName, comp.Name), true)
return nil, nil, true, nil
}
healthCheck := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (bool, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
_, found := applyMap.Load(fmt.Sprintf("%s/%s", clusterName, comp.Name))
return found, nil, nil, nil
}
parallelism := 10
countMap := func() int {
cnt := 0
applyMap.Range(func(key, value interface{}) bool {
cnt++
return true
})
return cnt
}
ctx := context.Background()
healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.False(healthy)
r.Equal(n*m, countMap())
healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.False(healthy)
r.Equal(2*n*m, countMap())
healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.True(healthy)
r.Equal(3*n*m, countMap())
}
func TestApplyComponentsIO(t *testing.T) {
r := require.New(t)
var (
parallelism = 10
applyMap = new(sync.Map)
ctx = context.Background()
)
apply := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
time.Sleep(time.Duration(rand.Intn(200)+25) * time.Millisecond)
applyMap.Store(fmt.Sprintf("%s/%s", clusterName, comp.Name), true)
return nil, nil, true, nil
}
healthCheck := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (bool, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
_, found := applyMap.Load(fmt.Sprintf("%s/%s", clusterName, comp.Name))
return found, &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"path": fmt.Sprintf("%s/%s", clusterName, comp.Name),
},
}}, []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
oam.TraitResource: "obj",
},
},
"spec": map[string]interface{}{
"path": fmt.Sprintf("%s/%s", clusterName, comp.Name),
},
},
},
}, nil
}
resetStore := func() {
applyMap = &sync.Map{}
}
countMap := func() int {
cnt := 0
applyMap.Range(func(key, value interface{}) bool {
cnt++
return true
})
return cnt
}
t.Run("apply components with io successfully", func(t *testing.T) {
resetStore()
const n, m = 10, 5
var components []apicommon.ApplicationComponent
var placements []v1alpha1.PlacementDecision
for i := 0; i < n; i++ {
comp := apicommon.ApplicationComponent{
Name: fmt.Sprintf("comp-%d", i),
Properties: &runtime.RawExtension{Raw: []byte(fmt.Sprintf(`{"placeholder":%d}`, i))},
}
if i != 0 {
comp.Inputs = workflowv1alpha1.StepInputs{
{
ParameterKey: "input_slot_1",
From: fmt.Sprintf("var-output-%d", i-1),
},
{
ParameterKey: "input_slot_2",
From: fmt.Sprintf("var-outputs-%d", i-1),
},
}
}
if i != n-1 {
comp.Outputs = workflowv1alpha1.StepOutputs{
{
ValueFrom: "output.spec.path",
Name: fmt.Sprintf("var-output-%d", i),
},
{
ValueFrom: "outputs.obj.spec.path",
Name: fmt.Sprintf("var-outputs-%d", i),
},
}
}
components = append(components, comp)
}
for i := 0; i < m; i++ {
placements = append(placements, v1alpha1.PlacementDecision{Cluster: fmt.Sprintf("cluster-%d", i)})
}
for i := 0; i < n; i++ {
healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.Equal((i+1)*m, countMap())
if i == n-1 {
r.True(healthy)
} else {
r.False(healthy)
}
}
})
t.Run("apply components with io failed", func(t *testing.T) {
resetStore()
components := []apicommon.ApplicationComponent{
{
Name: "comp-0",
Outputs: workflowv1alpha1.StepOutputs{
{
ValueFrom: "output.spec.error_path",
Name: "var1",
},
},
},
{
Name: "comp-1",
Inputs: workflowv1alpha1.StepInputs{
{
ParameterKey: "input_slot_1",
From: "var1",
},
},
},
}
placements := []v1alpha1.PlacementDecision{
{Cluster: "cluster-0"},
}
healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.False(healthy)
healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.ErrorContains(err, "failed to lookup value")
r.False(healthy)
})
t.Run("apply components with io and replication", func(t *testing.T) {
// comp-0 ---> comp1-beijing --> comp2-beijing
// |-> comp1-shanghai --> comp2-shanghai
resetStore()
storeKey := func(clusterName string, comp apicommon.ApplicationComponent) string {
return fmt.Sprintf("%s/%s/%s", clusterName, comp.Name, comp.ReplicaKey)
}
type applyResult struct {
output *unstructured.Unstructured
outputs []*unstructured.Unstructured
}
apply := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
time.Sleep(time.Duration(rand.Intn(200)+25) * time.Millisecond)
key := storeKey(clusterName, comp)
result := applyResult{
output: &unstructured.Unstructured{Object: map[string]interface{}{
"spec": map[string]interface{}{
"path": key,
"anotherPath": key,
},
}}, outputs: []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"labels": map[string]interface{}{
oam.TraitResource: "obj",
},
},
"spec": map[string]interface{}{
"path": key,
},
},
},
},
}
applyMap.Store(storeKey(clusterName, comp), result)
return nil, nil, true, nil
}
healthCheck := func(_ context.Context, comp apicommon.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (bool, *unstructured.Unstructured, []*unstructured.Unstructured, error) {
key := storeKey(clusterName, comp)
r, found := applyMap.Load(key)
result, _ := r.(applyResult)
return found, result.output, result.outputs, nil
}
inputSlot := "input_slot"
components := []apicommon.ApplicationComponent{
{
Name: "comp-0",
Outputs: workflowv1alpha1.StepOutputs{
{
ValueFrom: "output.spec.path",
Name: "var1",
},
},
},
{
Name: "comp-1",
Inputs: workflowv1alpha1.StepInputs{
{
ParameterKey: inputSlot,
From: "var1",
},
},
Outputs: workflowv1alpha1.StepOutputs{
{
ValueFrom: "output.spec.anotherPath",
Name: "var2",
},
},
ReplicaKey: "beijing",
},
{
Name: "comp-1",
Inputs: workflowv1alpha1.StepInputs{
{
ParameterKey: inputSlot,
From: "var1",
},
},
Outputs: workflowv1alpha1.StepOutputs{
{
ValueFrom: "output.spec.anotherPath",
Name: "var2",
},
},
ReplicaKey: "shanghai",
},
{
Name: "comp-2",
Inputs: workflowv1alpha1.StepInputs{
{
ParameterKey: inputSlot,
From: "var2",
},
},
ReplicaKey: "beijing",
},
{
Name: "comp-2",
Inputs: workflowv1alpha1.StepInputs{
{
ParameterKey: inputSlot,
From: "var2",
},
},
ReplicaKey: "shanghai",
},
}
placements := []v1alpha1.PlacementDecision{
{Cluster: "cluster-0"},
}
healthy, _, err := applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.False(healthy)
healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.False(healthy)
healthy, _, err = applyComponents(ctx, apply, healthCheck, components, placements, parallelism)
r.NoError(err)
r.True(healthy)
})
}
@@ -0,0 +1,40 @@
// multicluster.cue
#ListClusters: {
#provider: "multicluster"
#do: "list-clusters"
$returns?: {
outputs: {
clusters: [...string]
}
}
}
#GetPlacementsFromTmulticlusterologyPolicies: {
#provider: "multicluster"
#do: "get-placements-from-tmulticlusterology-policies"
$params: {
policies: [...string]
}
$returns?: {
placements: [...{
cluster: string
namespace: string
}]
}
}
#Deploy: {
#provider: "multicluster"
#do: "deploy"
$params: {
policies: [...string]
parallelism: int
ignoreTerraformComponent: bool
inlinePolicies: *[] | [...{...}]
}
$returns?: {...}
}
@@ -0,0 +1,156 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package multicluster
import (
"context"
_ "embed"
"github.com/pkg/errors"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/pkg/multicluster"
pkgpolicy "github.com/oam-dev/kubevela/pkg/policy"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
// Inputs is the inputs for multi cluster
type Inputs[T any] struct {
Inputs T `json:"inputs"`
}
// Outputs is the outputs for multi cluster
type Outputs[T any] struct {
Outputs T `json:"outputs"`
}
// PlacementDecisionVars is the vars for make placement decisions
type PlacementDecisionVars struct {
PolicyName string `json:"policyName"`
EnvName string `json:"envName"`
Placement *v1alpha1.EnvPlacement `json:"placement,omitempty"`
}
// PlacementDecisionResult is the result for make placement decisions
type PlacementDecisionResult struct {
Decisions []v1alpha1.PlacementDecision `json:"decisions"`
}
// PlacementDecisionParams is the parameter for make placement decisions
type PlacementDecisionParams = oamprovidertypes.OAMParams[Inputs[PlacementDecisionVars]]
// PlacementDecisionReturns is the return value for make placement decisions
type PlacementDecisionReturns = Outputs[PlacementDecisionResult]
// ApplicationVars is the vars for patching application
type ApplicationVars struct {
EnvName string `json:"envName"`
Patch *v1alpha1.EnvPatch `json:"patch,omitempty"`
Selector *v1alpha1.EnvSelector `json:"selector,omitempty"`
}
// ApplicationParams is the parameter for patch application
type ApplicationParams = oamprovidertypes.OAMParams[Inputs[ApplicationVars]]
// ClusterParams is the parameter for list clusters
type ClusterParams struct {
Clusters []string `json:"clusters"`
}
// ClusterReturns is the return value for list clusters
type ClusterReturns = oamprovidertypes.Returns[Outputs[ClusterParams]]
// ListClusters lists clusters
func ListClusters(ctx context.Context, params *oamprovidertypes.Params[any]) (*ClusterReturns, error) {
secrets, err := multicluster.ListExistingClusterSecrets(ctx, params.KubeClient)
if err != nil {
return nil, err
}
var clusters []string
for _, secret := range secrets {
clusters = append(clusters, secret.Name)
}
return &ClusterReturns{Returns: Outputs[ClusterParams]{Outputs: ClusterParams{Clusters: clusters}}}, nil
}
// DeployParams is the parameter for deploy
type DeployParams = oamprovidertypes.Params[DeployParameter]
// Deploy deploys the application
func Deploy(ctx context.Context, params *DeployParams) (*any, error) {
if params.Params.Parallelism <= 0 {
return nil, errors.Errorf("parallelism cannot be smaller than 1")
}
executor := NewDeployWorkflowStepExecutor(params.KubeClient, params.Appfile, params.ComponentApply, params.ComponentHealthCheck, params.WorkloadRender, params.Params)
healthy, reason, err := executor.Deploy(ctx)
if err != nil {
return nil, err
}
if !healthy {
params.Action.Wait(reason)
}
return nil, nil
}
// PoliciesVars is the vars for getting placements from topology policies
type PoliciesVars struct {
Policies []string `json:"policies"`
}
// PoliciesResult is the result for getting placements from topology policies
type PoliciesResult struct {
Placements []v1alpha1.PlacementDecision `json:"placements"`
}
// PoliciesParams is the params for getting placements from topology policies
type PoliciesParams = oamprovidertypes.Params[PoliciesVars]
// PoliciesReturns is the return value for getting placements from topology policies
type PoliciesReturns = oamprovidertypes.Returns[PoliciesResult]
// GetPlacementsFromTopologyPolicies gets placements from topology policies
func GetPlacementsFromTopologyPolicies(ctx context.Context, params *PoliciesParams) (*PoliciesReturns, error) {
policyNames := params.Params.Policies
policies, err := selectPolicies(params.Appfile.Policies, policyNames)
if err != nil {
return nil, err
}
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(ctx, params.KubeClient, params.Appfile.Namespace, policies, true)
if err != nil {
return nil, err
}
return &PoliciesReturns{Returns: PoliciesResult{Placements: placements}}, nil
}
//go:embed multicluster.cue
var template string
// GetTemplate returns the cue template.
func GetTemplate() string {
return template
}
// GetProviders returns the cue providers.
func GetProviders() map[string]cuexruntime.ProviderFn {
return map[string]cuexruntime.ProviderFn{
"list-clusters": oamprovidertypes.GenericProviderFn[any, ClusterReturns](ListClusters),
"get-placements-from-topology-policies": oamprovidertypes.GenericProviderFn[PoliciesVars, PoliciesReturns](GetPlacementsFromTopologyPolicies),
"deploy": oamprovidertypes.GenericProviderFn[DeployParameter, any](Deploy),
}
}
@@ -0,0 +1,56 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package multicluster
import (
"context"
"testing"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
clusterv1alpha1 "github.com/oam-dev/cluster-gateway/pkg/apis/cluster/v1alpha1"
clustercommon "github.com/oam-dev/cluster-gateway/pkg/common"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/utils/common"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func TestListClusters(t *testing.T) {
multicluster.ClusterGatewaySecretNamespace = types.DefaultKubeVelaNS
r := require.New(t)
ctx := context.Background()
cli := fake.NewClientBuilder().WithScheme(common.Scheme).Build()
clusterNames := []string{"cluster-a", "cluster-b"}
for _, secretName := range clusterNames {
secret := &corev1.Secret{}
secret.Name = secretName
secret.Namespace = multicluster.ClusterGatewaySecretNamespace
secret.Labels = map[string]string{clustercommon.LabelKeyClusterCredentialType: string(clusterv1alpha1.CredentialTypeX509Certificate)}
r.NoError(cli.Create(context.Background(), secret))
}
res, err := ListClusters(ctx, &oamprovidertypes.Params[any]{
RuntimeParams: oamprovidertypes.RuntimeParams{
KubeClient: cli,
},
})
r.NoError(err)
r.Equal(clusterNames, res.Returns.Outputs.Clusters)
}
+231
View File
@@ -0,0 +1,231 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oam
import (
"context"
_ "embed"
"cuelang.org/go/cue"
"k8s.io/apimachinery/pkg/types"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
"github.com/kubevela/workflow/pkg/cue/model/value"
workflowerrors "github.com/kubevela/workflow/pkg/errors"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
const (
// ProviderName is provider name for install.
ProviderName = "oam"
)
// RenderComponent render component
func RenderComponent(ctx context.Context, params *oamprovidertypes.Params[cue.Value]) (cue.Value, error) {
v := params.Params
parameter := v.LookupPath(cue.ParsePath("$params"))
if !parameter.Exists() {
return cue.Value{}, workflowerrors.LookUpNotFoundErr("$params")
}
comp, patcher, clusterName, overrideNamespace, err := lookUpCompInfo(parameter)
if err != nil {
return cue.Value{}, err
}
workload, traits, err := params.ComponentRender(ctx, *comp, patcher, clusterName, overrideNamespace)
if err != nil {
return cue.Value{}, err
}
if workload != nil {
v = v.FillPath(value.FieldPath("$returns", "output"), workload.Object)
}
for _, trait := range traits {
name := trait.GetLabels()[oam.TraitResource]
if name != "" {
v = v.FillPath(value.FieldPath("$returns", "outputs", name), workload.Object)
}
}
return v, nil
}
// ApplyComponent apply component.
func ApplyComponent(ctx context.Context, params *oamprovidertypes.Params[cue.Value]) (cue.Value, error) {
v := params.Params
parameter := v.LookupPath(cue.ParsePath("$params"))
if !parameter.Exists() {
return cue.Value{}, workflowerrors.LookUpNotFoundErr("$params")
}
comp, patcher, clusterName, overrideNamespace, err := lookUpCompInfo(parameter)
if err != nil {
return cue.Value{}, err
}
workload, traits, healthy, err := params.ComponentApply(ctx, *comp, patcher, clusterName, overrideNamespace)
if err != nil {
return cue.Value{}, err
}
if workload != nil {
v = v.FillPath(value.FieldPath("$returns", "output"), workload.Object)
}
for _, trait := range traits {
name := trait.GetLabels()[oam.TraitResource]
if name != "" {
v = v.FillPath(value.FieldPath("$returns", "outputs", name), trait)
}
}
waitHealthy, err := v.LookupPath(cue.ParsePath("waitHealthy")).Bool()
if err != nil {
waitHealthy = true
}
if waitHealthy && !healthy {
params.Action.Wait("wait healthy")
}
return v, nil
}
func lookUpCompInfo(v cue.Value) (*common.ApplicationComponent, *cue.Value, string, string, error) {
compSettings := v.LookupPath(cue.ParsePath("value"))
if !compSettings.Exists() {
return nil, nil, "", "", workflowerrors.LookUpNotFoundErr("value")
}
comp := &common.ApplicationComponent{}
if err := value.UnmarshalTo(compSettings, comp); err != nil {
return nil, nil, "", "", err
}
var patcherValue *cue.Value
patcher := v.LookupPath(cue.ParsePath("patch"))
if patcher.Exists() {
patcherValue = &patcher
}
clusterName, err := v.LookupPath(cue.ParsePath("cluster")).String()
if err != nil {
clusterName = ""
}
overrideNamespace, err := v.LookupPath(cue.ParsePath("namespace")).String()
if err != nil {
overrideNamespace = ""
}
return comp, patcherValue, clusterName, overrideNamespace, nil
}
// LoadVars is the load provider vars.
type LoadVars struct {
App string `json:"app,omitempty"`
}
// LoadReturnVars is the load provider return vars.
type LoadReturnVars struct {
Value any `json:"value"`
}
// LoadParams is the load provider params.
type LoadParams = oamprovidertypes.Params[LoadVars]
// LoadReturns is the load provider returns.
type LoadReturns = oamprovidertypes.Returns[LoadReturnVars]
// LoadComponent load component describe info in application.
func LoadComponent(ctx context.Context, params *LoadParams) (*LoadReturns, error) {
app := &v1beta1.Application{}
cli := params.KubeClient
// if specify `app`, use specified application otherwise use default application from provider
appSettings := params.Params.App
if appSettings == "" {
app = params.App
} else {
if err := cli.Get(ctx, types.NamespacedName{Name: appSettings, Namespace: params.App.Namespace}, app); err != nil {
return nil, err
}
}
comps := make(map[string]*common.ApplicationComponent, 0)
for _, _comp := range app.Spec.Components {
comp, err := params.Appfile.LoadDynamicComponent(ctx, cli, _comp.DeepCopy())
if err != nil {
return nil, err
}
comp.Inputs = nil
comp.Outputs = nil
comps[_comp.Name] = comp
}
return &LoadReturns{Returns: LoadReturnVars{Value: comps}}, nil
}
// LoadComponentInOrder load component describe info in application output will be a list with order defined in application.
func LoadComponentInOrder(ctx context.Context, params *LoadParams) (*LoadReturns, error) {
app := &v1beta1.Application{}
cli := params.KubeClient
// if specify `app`, use specified application otherwise use default application from provider
appSettings := params.Params.App
if appSettings == "" {
app = params.App
} else {
if err := cli.Get(ctx, types.NamespacedName{Name: appSettings, Namespace: params.App.Namespace}, app); err != nil {
return nil, err
}
}
comps := make([]common.ApplicationComponent, len(app.Spec.Components))
for idx, _comp := range app.Spec.Components {
comp, err := params.Appfile.LoadDynamicComponent(ctx, cli, _comp.DeepCopy())
if err != nil {
return nil, err
}
comp.Inputs = nil
comp.Outputs = nil
comps[idx] = *comp
}
return &LoadReturns{Returns: LoadReturnVars{Value: comps}}, nil
}
// LoadPolicies load policy describe info in application.
func LoadPolicies(_ context.Context, params *LoadParams) (*LoadReturns, error) {
app := params.App
policies := make(map[string]v1beta1.AppPolicy, 0)
for _, po := range app.Spec.Policies {
policies[po.Name] = po
}
return &LoadReturns{Returns: LoadReturnVars{Value: policies}}, nil
}
//go:embed oam.cue
var template string
// GetTemplate returns the cue template.
func GetTemplate() string {
return template
}
// GetProviders returns the cue providers.
func GetProviders() map[string]cuexruntime.ProviderFn {
return map[string]cuexruntime.ProviderFn{
"component-render": oamprovidertypes.NativeProviderFn(RenderComponent),
"component-apply": oamprovidertypes.NativeProviderFn(ApplyComponent),
"load": oamprovidertypes.GenericProviderFn[LoadVars, LoadReturns](LoadComponent),
"load-comps-in-order": oamprovidertypes.GenericProviderFn[LoadVars, LoadReturns](LoadComponentInOrder),
"load-policies": oamprovidertypes.GenericProviderFn[LoadVars, LoadReturns](LoadPolicies),
}
}
+207
View File
@@ -0,0 +1,207 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package oam
import (
"context"
"encoding/json"
"testing"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/kubevela/workflow/pkg/mock"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func setupClient(ctx context.Context, t *testing.T) client.Client {
r := require.New(t)
scheme := runtime.NewScheme()
r.NoError(v1beta1.AddToScheme(scheme))
r.NoError(appsv1.AddToScheme(scheme))
cli := fake.NewClientBuilder().WithScheme(scheme).Build()
return cli
}
func TestParser(t *testing.T) {
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
cuectx := cuecontext.New()
cli := setupClient(ctx, t)
v := cuectx.CompileString("")
_, err := ApplyComponent(ctx, &oamprovidertypes.Params[cue.Value]{
Params: v,
RuntimeParams: oamprovidertypes.RuntimeParams{
KubeClient: cli,
},
})
r.Equal(err.Error(), "failed to lookup value: var(path=$params) not exist")
v = cuectx.CompileString(`$params: {
value: {
name: "test",
type: "test",
}
}`)
res, err := ApplyComponent(ctx, &oamprovidertypes.Params[cue.Value]{
Params: v,
RuntimeParams: oamprovidertypes.RuntimeParams{
Action: act,
ComponentApply: oamprovidertypes.ComponentApply(func(ctx context.Context, comp common.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": comp.Name,
},
},
}, []*unstructured.Unstructured{
{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "service",
"labels": map[string]interface{}{
"trait.oam.dev/resource": "service",
},
},
},
},
}, false, nil
}),
},
})
r.NoError(err)
output, err := res.LookupPath(cue.ParsePath("$returns.output.metadata.name")).String()
r.NoError(err)
r.Equal(output, "test")
outputs, err := res.LookupPath(cue.ParsePath("$returns.outputs.service.metadata.name")).String()
r.NoError(err)
r.Equal(outputs, "service")
r.Equal(act.Phase, "Wait")
}
func TestLoadComponent(t *testing.T) {
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
res, err := LoadComponent(ctx, &oamprovidertypes.Params[LoadVars]{
Params: LoadVars{},
RuntimeParams: oamprovidertypes.RuntimeParams{
Action: act,
App: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "c1",
Type: "test",
Properties: &runtime.RawExtension{Raw: []byte(`{"image": "busybox"}`)},
},
},
},
},
},
})
r.NoError(err)
b, err := json.Marshal(res.Returns.Value)
r.NoError(err)
r.Equal(string(b), `{"c1":{"name":"c1","type":"test","properties":{"image":"busybox"}}}`)
app2 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test2",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "c2",
Type: "test",
Properties: &runtime.RawExtension{Raw: []byte(`{"image": "nginx"}`)},
},
},
},
}
cli := setupClient(ctx, t)
err = cli.Create(ctx, app2)
r.NoError(err)
res, err = LoadComponent(ctx, &oamprovidertypes.Params[LoadVars]{
Params: LoadVars{
App: "test2",
},
RuntimeParams: oamprovidertypes.RuntimeParams{
Action: act,
App: app2,
KubeClient: cli,
},
})
r.NoError(err)
b, err = json.Marshal(res.Returns.Value)
r.NoError(err)
r.Equal(string(b), `{"c2":{"name":"c2","type":"test","properties":{"image":"nginx"}}}`)
}
func TestLoadComponentInOrder(t *testing.T) {
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
res, err := LoadComponentInOrder(ctx, &oamprovidertypes.Params[LoadVars]{
Params: LoadVars{},
RuntimeParams: oamprovidertypes.RuntimeParams{
Action: act,
App: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "c1",
Type: "test",
Properties: &runtime.RawExtension{Raw: []byte(`{"image": "busybox"}`)},
},
{
Name: "c2",
Type: "test",
Properties: &runtime.RawExtension{Raw: []byte(`{"image": "busybox"}`)},
},
},
},
},
},
})
r.NoError(err)
b, err := json.Marshal(res.Returns.Value)
r.NoError(err)
r.Equal(string(b), `[{"name":"c1","type":"test","properties":{"image":"busybox"}},{"name":"c2","type":"test","properties":{"image":"busybox"}}]`)
}
+111
View File
@@ -0,0 +1,111 @@
// oam.cue
#ApplyComponent: {
#provider: "oam"
#do: "component-apply"
$params: {
// +usage=The cluster to use
cluster: *"" | string
// +usage=The env to use
env: *"" | string
// +usage=The namespace to apply
namespace: *"" | string
// +usage=Whether to wait healthy of the applied component
waitHealthy: *true | bool
// +usage=The value of the component resource
value: {...}
// +usage=The patcher that will be applied to the resource, you can define the strategy of list merge through comments. Reference doc here: https://kubevela.io/docs/platform-engineers/traits/patch-trait#patch-in-workflow-step
patch?: {...}
}
$returns: {
output?: {...}
outputs?: {...}
}
...
}
#RenderComponent: {
#provider: "oam"
#do: "component-render"
$params: {
cluster: *"" | string
env: *"" | string
namespace: *"" | string
value: {...}
patch?: {...}
}
$returns: {
output?: {...}
outputs?: {...}
}
...
}
#LoadComponets: {
#provider: "oam"
#do: "load"
$params: {
// +usage=If specify `app`, use specified application to load its component resources otherwise use current application
app?: string
}
$returns: {
// +usage=The value of the components will be filled in this field after the action is executed, you can use value[componentName] to refer a specified component
value?: {...}
}
...
}
#LoadPolicies: {
#provider: "oam"
#do: "load-policies"
$params: {
// +usage=If specify `app`, use specified application to load its component resources otherwise use current application
app?: string
}
$returns: {
// +usage=The value of the components will be filled in this field after the action is executed, you can use value[componentName] to refer a specified component
value?: {...}
}
...
}
#LoadComponetsInOrder: {
#provider: "oam"
#do: "load-comps-in-order"
$params: {
// +usage=If specify `app`, use specified application to load its component resources otherwise use current application
app?: string
}
$returns: {
// +usage=The value of the components will be filled in this field after the action is executed, you can use value[componentName] to refer a specified component
value?: [{...}]
}
...
}
// This operator will dispatch all the components in parallel when applying an application.
// Currently it works for Addon Observability to speed up the installation. It can also works for other applications, which
// needs to skip health check for components.
#ApplyApplicationInParallel: {
load: #LoadComponetsInOrder
components: {
for name, c in load.$returns.value {
"\(name)": #ApplyComponent & {
$params: {
value: c
waitHealthy: false
}
}
}
}
}
+339
View File
@@ -0,0 +1,339 @@
/*
Copyright 2021. The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
import (
"context"
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/resourcetracker"
"github.com/oam-dev/kubevela/pkg/utils/types"
)
// AppCollector collect resource created by application
type AppCollector struct {
k8sClient client.Client
opt Option
}
// NewAppCollector create a app collector
func NewAppCollector(cli client.Client, opt Option) *AppCollector {
return &AppCollector{
k8sClient: cli,
opt: opt,
}
}
const velaVersionNumberToUpgradeVelaQL = "v1.2.0-rc.1"
// CollectResourceFromApp collect resources created by application
func (c *AppCollector) CollectResourceFromApp(ctx context.Context) ([]Resource, error) {
app := new(v1beta1.Application)
appKey := client.ObjectKey{Name: c.opt.Name, Namespace: c.opt.Namespace}
if err := c.k8sClient.Get(ctx, appKey, app); err != nil {
return nil, err
}
var currentVersionNumber string
if annotations := app.GetAnnotations(); annotations != nil && annotations[oam.AnnotationKubeVelaVersion] != "" {
currentVersionNumber = annotations[oam.AnnotationKubeVelaVersion]
}
velaVersionToUpgradeVelaQL, _ := version.NewVersion(velaVersionNumberToUpgradeVelaQL)
currentVersion, err := version.NewVersion(currentVersionNumber)
if err != nil {
resources, err := c.FindResourceFromResourceTrackerSpec(ctx, app)
if err != nil {
return c.FindResourceFromAppliedResourcesField(ctx, app)
}
return resources, nil
}
if velaVersionToUpgradeVelaQL.GreaterThan(currentVersion) {
return c.FindResourceFromAppliedResourcesField(ctx, app)
}
return c.FindResourceFromResourceTrackerSpec(ctx, app)
}
// ListApplicationResources list application applied resources from tracker
func (c *AppCollector) ListApplicationResources(ctx context.Context, app *v1beta1.Application) ([]types.AppliedResource, error) {
rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, c.k8sClient, app)
if err != nil {
return nil, errors.WithMessage(err, "list application resource trackers")
}
var managedResources []types.AppliedResource
existResources := make(map[common.ClusterObjectReference]bool, len(app.Spec.Components))
if c.opt.Filter.QueryNewest {
historyRTs = nil
}
for _, rt := range append(historyRTs, rootRT, currentRT) {
if rt != nil {
for _, managedResource := range rt.Spec.ManagedResources {
if isResourceInTargetCluster(c.opt.Filter, managedResource.ClusterObjectReference) &&
isResourceInTargetComponent(c.opt.Filter, managedResource.Component) &&
(c.opt.WithTree || isResourceMatchKindAndVersion(c.opt.Filter, managedResource.Kind, managedResource.APIVersion)) {
if c.opt.WithTree {
// If we want to query the tree, we only need to query once for the same resource.
if _, exist := existResources[managedResource.ClusterObjectReference]; exist {
continue
}
existResources[managedResource.ClusterObjectReference] = true
}
managedResources = append(managedResources, types.AppliedResource{
Cluster: func() string {
if managedResource.Cluster != "" {
return managedResource.Cluster
}
return "local"
}(),
Kind: managedResource.Kind,
Component: managedResource.Component,
Trait: managedResource.Trait,
Name: managedResource.Name,
Namespace: managedResource.Namespace,
APIVersion: managedResource.APIVersion,
ResourceVersion: managedResource.ResourceVersion,
UID: managedResource.UID,
PublishVersion: oam.GetPublishVersion(rt),
DeployVersion: func() string {
obj, _ := managedResource.ToUnstructuredWithData()
if obj != nil {
return oam.GetDeployVersion(obj)
}
return ""
}(),
Revision: rt.GetLabels()[oam.LabelAppRevision],
Latest: currentRT != nil && rt.Name == currentRT.Name,
})
}
}
}
}
if !c.opt.WithTree {
return managedResources, nil
}
// merge user defined customize rule before every request.
err = mergeCustomRules(ctx, c.k8sClient)
if err != nil {
return managedResources, err
}
filter := func(node types.ResourceTreeNode) bool {
return isResourceMatchKindAndVersion(c.opt.Filter, node.Kind, node.APIVersion)
}
var matchedResources []types.AppliedResource
// error from leaf nodes won't block the results
for i := range managedResources {
resource := managedResources[i]
root := types.ResourceTreeNode{
Cluster: resource.Cluster,
APIVersion: resource.APIVersion,
Kind: resource.Kind,
Namespace: resource.Namespace,
Name: resource.Name,
UID: resource.UID,
}
root.LeafNodes, err = iterateListSubResources(ctx, resource.Cluster, c.k8sClient, root, 1, filter)
if err != nil {
// if the resource has been deleted, continue access next appliedResource don't break the whole request
if kerrors.IsNotFound(err) {
continue
}
klog.Errorf("query leaf node resource apiVersion=%s kind=%s namespace=%s name=%s failure %s, skip this resource", root.APIVersion, root.Kind, root.Namespace, root.Name, err.Error())
continue
}
if !filter(root) && len(root.LeafNodes) == 0 {
continue
}
rootObject, err := fetchObjectWithResourceTreeNode(ctx, resource.Cluster, c.k8sClient, root)
if err != nil {
// if the resource has been deleted, continue access next appliedResource don't break the whole request
if kerrors.IsNotFound(err) {
continue
}
klog.Errorf("fetch object for resource apiVersion=%s kind=%s namespace=%s name=%s failure %s, skip this resource", root.APIVersion, root.Kind, root.Namespace, root.Name, err.Error())
continue
}
rootStatus, err := CheckResourceStatus(*rootObject)
if err != nil {
klog.Errorf("check status for resource apiVersion=%s kind=%s namespace=%s name=%s failure %s, skip this resource", root.APIVersion, root.Kind, root.Namespace, root.Name, err.Error())
continue
}
root.HealthStatus = *rootStatus
addInfo, err := additionalInfo(*rootObject)
if err != nil {
klog.Errorf("check additionalInfo for resource apiVersion=%s kind=%s namespace=%s name=%s failure %s, skip this resource", root.APIVersion, root.Kind, root.Namespace, root.Name, err.Error())
continue
}
root.AdditionalInfo = addInfo
root.CreationTimestamp = rootObject.GetCreationTimestamp().Time
if !rootObject.GetDeletionTimestamp().IsZero() {
root.DeletionTimestamp = rootObject.GetDeletionTimestamp().Time
}
root.Object = rootObject
resource.ResourceTree = &root
matchedResources = append(matchedResources, resource)
}
return matchedResources, nil
}
// FindResourceFromResourceTrackerSpec find resources from ResourceTracker spec
func (c *AppCollector) FindResourceFromResourceTrackerSpec(ctx context.Context, app *v1beta1.Application) ([]Resource, error) {
rootRT, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, c.k8sClient, app)
if err != nil {
klog.Errorf("query the resourcetrackers failure %s", err.Error())
return nil, err
}
var resources = []Resource{}
existResources := make(map[common.ClusterObjectReference]bool, len(app.Spec.Components))
for _, rt := range append([]*v1beta1.ResourceTracker{rootRT, currentRT}, historyRTs...) {
if rt != nil {
for _, managedResource := range rt.Spec.ManagedResources {
if isResourceInTargetCluster(c.opt.Filter, managedResource.ClusterObjectReference) &&
isResourceInTargetComponent(c.opt.Filter, managedResource.Component) &&
isResourceMatchKindAndVersion(c.opt.Filter, managedResource.Kind, managedResource.APIVersion) {
if _, exist := existResources[managedResource.ClusterObjectReference]; exist {
continue
}
existResources[managedResource.ClusterObjectReference] = true
obj, err := managedResource.ToUnstructuredWithData()
if err != nil || c.opt.WithStatus {
// For the application with apply once policy, there is no data in RT.
// IF the WithStatus is true, get the object from cluster
_, obj, err = getObjectCreatedByComponent(ctx, c.k8sClient, managedResource.ObjectReference, managedResource.Cluster)
if err != nil {
klog.Errorf("get obj from the cluster failure %s", err.Error())
continue
}
}
clusterName := managedResource.Cluster
if clusterName == "" {
clusterName = multicluster.ClusterLocalName
}
resources = append(resources, Resource{
Cluster: clusterName,
Revision: oam.GetPublishVersion(rt),
Component: managedResource.Component,
Object: obj,
})
}
}
}
}
return resources, nil
}
// FindResourceFromAppliedResourcesField find resources from AppliedResources field
func (c *AppCollector) FindResourceFromAppliedResourcesField(ctx context.Context, app *v1beta1.Application) ([]Resource, error) {
resources := make([]Resource, 0, len(app.Spec.Components))
for _, res := range app.Status.AppliedResources {
if !isResourceInTargetCluster(c.opt.Filter, res) {
continue
}
if !isResourceMatchKindAndVersion(c.opt.Filter, res.APIVersion, res.Kind) {
continue
}
compName, obj, err := getObjectCreatedByComponent(ctx, c.k8sClient, res.ObjectReference, res.Cluster)
if err != nil {
return nil, err
}
if len(compName) != 0 && isResourceInTargetComponent(c.opt.Filter, compName) {
resources = append(resources, Resource{
Component: compName,
Revision: obj.GetLabels()[oam.LabelAppRevision],
Cluster: res.Cluster,
Object: obj,
})
}
}
if len(resources) == 0 {
return nil, errors.Errorf("fail to find resources created by application: %v", c.opt.Name)
}
return resources, nil
}
// getObjectCreatedByComponent get k8s obj created by components
func getObjectCreatedByComponent(ctx context.Context, cli client.Client, objRef corev1.ObjectReference, cluster string) (string, *unstructured.Unstructured, error) {
ctx = multicluster.ContextWithClusterName(ctx, cluster)
obj := new(unstructured.Unstructured)
obj.SetGroupVersionKind(objRef.GroupVersionKind())
obj.SetNamespace(objRef.Namespace)
obj.SetName(objRef.Name)
if err := cli.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
if kerrors.IsNotFound(err) {
return "", nil, nil
}
return "", nil, err
}
componentName := obj.GetLabels()[oam.LabelAppComponent]
return componentName, obj, nil
}
func getEventFieldSelector(obj *unstructured.Unstructured) fields.Selector {
field := fields.Set{}
field["involvedObject.name"] = obj.GetName()
field["involvedObject.namespace"] = obj.GetNamespace()
field["involvedObject.kind"] = obj.GetObjectKind().GroupVersionKind().Kind
field["involvedObject.uid"] = string(obj.GetUID())
return field.AsSelector()
}
func isResourceInTargetCluster(opt FilterOption, resource common.ClusterObjectReference) bool {
if opt.Cluster == "" && opt.ClusterNamespace == "" {
return true
}
if (opt.Cluster == resource.Cluster || (opt.Cluster == "local" && resource.Cluster == "")) &&
(opt.ClusterNamespace == resource.ObjectReference.Namespace || opt.ClusterNamespace == "") {
return true
}
return false
}
func isResourceInTargetComponent(opt FilterOption, componentName string) bool {
if len(opt.Components) == 0 {
return true
}
for _, component := range opt.Components {
if component == componentName {
return true
}
}
return false
}
func isResourceMatchKindAndVersion(opt FilterOption, kind, version string) bool {
if opt.APIVersion != "" && opt.APIVersion != version {
return false
}
if opt.Kind != "" && opt.Kind != kind {
return false
}
return true
}
+443
View File
@@ -0,0 +1,443 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/kubevela/pkg/util/slices"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/networking/v1"
networkv1beta1 "k8s.io/api/networking/v1beta1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
apis "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/multicluster"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
)
// CollectServiceEndpoints generator service endpoints is available for common component type,
// such as webservice or helm
// it can not support the cloud service component currently
func CollectServiceEndpoints(ctx context.Context, params *ListParams) (*ListReturns[querytypes.ServiceEndpoint], error) {
opt := params.Params.App
cli := params.KubeClient
app := new(v1beta1.Application)
err := findResource(ctx, cli, app, opt.Name, opt.Namespace, "")
if err != nil {
return nil, fmt.Errorf("query app failure %w", err)
}
serviceEndpoints := make([]querytypes.ServiceEndpoint, 0)
var clusterGatewayNodeIP = make(map[string]string)
collector := NewAppCollector(cli, opt)
resources, err := collector.ListApplicationResources(ctx, app)
if err != nil {
return nil, err
}
for i, resource := range resources {
cluster := resources[i].Cluster
cachedSelectorNodeIP := func() string {
if ip, exist := clusterGatewayNodeIP[cluster]; exist {
return ip
}
ip := selectorNodeIP(ctx, cluster, cli)
if ip != "" {
clusterGatewayNodeIP[cluster] = ip
}
return ip
}
if resource.ResourceTree != nil {
serviceEndpoints = append(serviceEndpoints, getEndpointFromNode(ctx, cli, resource.ResourceTree, resource.Component, cachedSelectorNodeIP)...)
} else {
serviceEndpoints = append(serviceEndpoints, getServiceEndpoints(ctx, cli, resource.GroupVersionKind(), resource.Name, resource.Namespace, resource.Cluster, resource.Component, cachedSelectorNodeIP)...)
}
}
return &ListReturns[querytypes.ServiceEndpoint]{Returns: ListReturnVars[querytypes.ServiceEndpoint]{List: serviceEndpoints}}, nil
}
func getEndpointFromNode(ctx context.Context, cli client.Client, node *querytypes.ResourceTreeNode, component string, cachedSelectorNodeIP func() string) []querytypes.ServiceEndpoint {
if node == nil {
return nil
}
var serviceEndpoints []querytypes.ServiceEndpoint
serviceEndpoints = append(serviceEndpoints, getServiceEndpoints(ctx, cli, node.GroupVersionKind(), node.Name, node.Namespace, node.Cluster, component, cachedSelectorNodeIP)...)
for _, child := range node.LeafNodes {
serviceEndpoints = append(serviceEndpoints, getEndpointFromNode(ctx, cli, child, component, cachedSelectorNodeIP)...)
}
return serviceEndpoints
}
func getServiceEndpoints(ctx context.Context, cli client.Client, gvk schema.GroupVersionKind, name, namespace, cluster, component string, cachedSelectorNodeIP func() string) []querytypes.ServiceEndpoint {
var serviceEndpoints []querytypes.ServiceEndpoint
switch gvk.Kind {
case "Ingress":
if gvk.Group == networkv1beta1.GroupName && (gvk.Version == "v1beta1" || gvk.Version == "v1") {
var ingress v1.Ingress
ingress.SetGroupVersionKind(gvk)
if err := findResource(ctx, cli, &ingress, name, namespace, cluster); err != nil {
klog.Error(err, fmt.Sprintf("find v1 Ingress %s/%s from cluster %s failure", name, namespace, cluster))
return nil
}
serviceEndpoints = append(serviceEndpoints, generatorFromIngress(ingress, cluster, component)...)
} else {
klog.Warning("not support ingress version", "version", gvk)
}
case "Service":
var service corev1.Service
service.SetGroupVersionKind(gvk)
if err := findResource(ctx, cli, &service, name, namespace, cluster); err != nil {
klog.Error(err, fmt.Sprintf("find v1 Service %s/%s from cluster %s failure", name, namespace, cluster))
return nil
}
serviceEndpoints = append(serviceEndpoints, generatorFromService(service, cachedSelectorNodeIP, cluster, component, "")...)
case "SeldonDeployment":
obj := new(unstructured.Unstructured)
obj.SetGroupVersionKind(gvk)
if err := findResource(ctx, cli, obj, name, namespace, cluster); err != nil {
klog.Error(err, fmt.Sprintf("find v1 Seldon Deployment %s/%s from cluster %s failure", name, namespace, cluster))
return nil
}
anno := obj.GetAnnotations()
serviceName := "ambassador"
serviceNS := apis.DefaultKubeVelaNS
if anno != nil {
if anno[annoAmbassadorServiceName] != "" {
serviceName = anno[annoAmbassadorServiceName]
}
if anno[annoAmbassadorServiceNamespace] != "" {
serviceNS = anno[annoAmbassadorServiceNamespace]
}
}
var service corev1.Service
if err := findResource(ctx, cli, &service, serviceName, serviceNS, cluster); err != nil {
klog.Error(err, fmt.Sprintf("find v1 Service %s/%s from cluster %s failure", serviceName, serviceNS, cluster))
return nil
}
serviceEndpoints = append(serviceEndpoints, generatorFromService(service, cachedSelectorNodeIP, cluster, component, fmt.Sprintf("/seldon/%s/%s", namespace, name))...)
case "HTTPRoute":
var route gatewayv1beta1.HTTPRoute
route.SetGroupVersionKind(gvk)
if err := findResource(ctx, cli, &route, name, namespace, cluster); err != nil {
klog.Error(err, fmt.Sprintf("find HTTPRoute %s/%s from cluster %s failure", name, namespace, cluster))
return nil
}
serviceEndpoints = append(serviceEndpoints, generatorFromHTTPRoute(ctx, cli, route, cluster, component)...)
}
return serviceEndpoints
}
func findResource(ctx context.Context, cli client.Client, obj client.Object, name, namespace, cluster string) error {
obj.SetNamespace(namespace)
obj.SetName(name)
gctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
if err := cli.Get(multicluster.ContextWithClusterName(gctx, cluster),
client.ObjectKeyFromObject(obj), obj); err != nil {
if kerrors.IsNotFound(err) {
return nil
}
return err
}
return nil
}
func generatorFromService(service corev1.Service, selectorNodeIP func() string, cluster, component, path string) []querytypes.ServiceEndpoint {
var serviceEndpoints []querytypes.ServiceEndpoint
var objRef = corev1.ObjectReference{
Kind: "Service",
Namespace: service.ObjectMeta.Namespace,
Name: service.ObjectMeta.Name,
UID: service.UID,
APIVersion: service.APIVersion,
ResourceVersion: service.ResourceVersion,
}
formatEndpoint := func(host, appProtocol string, portName string, portProtocol corev1.Protocol, portNum int32, inner bool) querytypes.ServiceEndpoint {
return querytypes.ServiceEndpoint{
Endpoint: querytypes.Endpoint{
Protocol: portProtocol,
AppProtocol: &appProtocol,
Host: host,
Port: int(portNum),
PortName: portName,
Path: path,
Inner: inner,
},
Ref: objRef,
Cluster: cluster,
Component: component,
}
}
switch service.Spec.Type {
case corev1.ServiceTypeLoadBalancer:
for _, port := range service.Spec.Ports {
appp := judgeAppProtocol(port.Port)
for _, ingress := range service.Status.LoadBalancer.Ingress {
if ingress.Hostname != "" {
serviceEndpoints = append(serviceEndpoints, formatEndpoint(ingress.Hostname, appp, port.Name, port.Protocol, port.Port, false))
}
if ingress.IP != "" {
serviceEndpoints = append(serviceEndpoints, formatEndpoint(ingress.IP, appp, port.Name, port.Protocol, port.Port, false))
}
}
}
case corev1.ServiceTypeNodePort:
for _, port := range service.Spec.Ports {
appp := judgeAppProtocol(port.Port)
serviceEndpoints = append(serviceEndpoints, formatEndpoint(selectorNodeIP(), appp, port.Name, port.Protocol, port.NodePort, false))
}
case corev1.ServiceTypeClusterIP, corev1.ServiceTypeExternalName:
for _, port := range service.Spec.Ports {
appp := judgeAppProtocol(port.Port)
serviceEndpoints = append(serviceEndpoints, formatEndpoint(fmt.Sprintf("%s.%s", service.Name, service.Namespace), appp, port.Name, port.Protocol, port.Port, true))
}
}
return serviceEndpoints
}
func generatorFromIngress(ingress v1.Ingress, cluster, component string) (serviceEndpoints []querytypes.ServiceEndpoint) {
getAppProtocol := func(host string) string {
if len(ingress.Spec.TLS) > 0 {
for _, tls := range ingress.Spec.TLS {
if len(tls.Hosts) > 0 && slices.Contains(tls.Hosts, host) {
return querytypes.HTTPS
}
if len(tls.Hosts) == 0 {
return querytypes.HTTPS
}
}
}
return querytypes.HTTP
}
// It depends on the Ingress Controller
getEndpointPort := func(appProtocol string) int {
if appProtocol == querytypes.HTTPS {
if port, err := strconv.Atoi(ingress.Annotations[apis.AnnoIngressControllerHTTPSPort]); port > 0 && err == nil {
return port
}
return 443
}
if port, err := strconv.Atoi(ingress.Annotations[apis.AnnoIngressControllerHTTPPort]); port > 0 && err == nil {
return port
}
return 80
}
// The host in rule maybe empty, means access the application by the Gateway Host(IP)
getHost := func(host string) string {
if host != "" {
return host
}
return ingress.Annotations[apis.AnnoIngressControllerHost]
}
for _, rule := range ingress.Spec.Rules {
var appProtocol = getAppProtocol(rule.Host)
var appPort = getEndpointPort(appProtocol)
if rule.HTTP != nil {
for _, path := range rule.HTTP.Paths {
serviceEndpoints = append(serviceEndpoints, querytypes.ServiceEndpoint{
Endpoint: querytypes.Endpoint{
Protocol: corev1.ProtocolTCP,
AppProtocol: &appProtocol,
Host: getHost(rule.Host),
Path: path.Path,
Port: appPort,
},
Ref: corev1.ObjectReference{
Kind: "Ingress",
Namespace: ingress.ObjectMeta.Namespace,
Name: ingress.ObjectMeta.Name,
UID: ingress.UID,
APIVersion: ingress.APIVersion,
ResourceVersion: ingress.ResourceVersion,
},
Cluster: cluster,
Component: component,
})
}
}
}
return serviceEndpoints
}
func getGatewayPortAndProtocol(ctx context.Context, cli client.Client, defaultNamespace, cluster string, parents []gatewayv1beta1.ParentReference) (string, int) {
for _, parent := range parents {
if parent.Kind != nil && *parent.Kind == "Gateway" {
var gateway gatewayv1beta1.Gateway
namespace := defaultNamespace
if parent.Namespace != nil {
namespace = string(*parent.Namespace)
}
if err := findResource(ctx, cli, &gateway, string(parent.Name), namespace, cluster); err != nil {
klog.Errorf("query the Gateway %s/%s/%s failure %s", cluster, namespace, string(parent.Name), err.Error())
}
var listener *gatewayv1beta1.Listener
if parent.SectionName != nil {
for i, lis := range gateway.Spec.Listeners {
if lis.Name == *parent.SectionName {
listener = &gateway.Spec.Listeners[i]
break
}
}
} else if len(gateway.Spec.Listeners) > 0 {
listener = &gateway.Spec.Listeners[0]
}
if listener != nil {
var protocol = querytypes.HTTP
if listener.Protocol == gatewayv1beta1.HTTPSProtocolType {
protocol = querytypes.HTTPS
}
var port = int(listener.Port)
// The gateway listener port may not be the externally exposed port.
// For example, the traefik addon has a default port mapping configuration of 8443->443 8000->80
// So users could set the `ports-mapping` annotation.
if mapping := gateway.Annotations["ports-mapping"]; mapping != "" {
for _, portItem := range strings.Split(mapping, ",") {
if portMap := strings.Split(portItem, ":"); len(portMap) == 2 {
if portMap[0] == fmt.Sprintf("%d", listener.Port) {
newPort, err := strconv.Atoi(portMap[1])
if err == nil {
port = newPort
}
}
}
}
}
return protocol, port
}
}
}
return querytypes.HTTP, 80
}
func generatorFromHTTPRoute(ctx context.Context, cli client.Client, route gatewayv1beta1.HTTPRoute, cluster, component string) []querytypes.ServiceEndpoint {
existPath := make(map[string]bool)
var serviceEndpoints []querytypes.ServiceEndpoint
for _, rule := range route.Spec.Rules {
for _, host := range route.Spec.Hostnames {
appProtocol, appPort := getGatewayPortAndProtocol(ctx, cli, route.Namespace, cluster, route.Spec.ParentRefs)
for _, match := range rule.Matches {
path := ""
if match.Path != nil && (match.Path.Type == nil || string(*match.Path.Type) == string(gatewayv1beta1.PathMatchPathPrefix)) {
path = *match.Path.Value
}
if !existPath[path] {
existPath[path] = true
serviceEndpoints = append(serviceEndpoints, querytypes.ServiceEndpoint{
Endpoint: querytypes.Endpoint{
Protocol: corev1.ProtocolTCP,
AppProtocol: &appProtocol,
Host: string(host),
Path: path,
Port: appPort,
},
Ref: corev1.ObjectReference{
Kind: route.Kind,
Namespace: route.ObjectMeta.Namespace,
Name: route.ObjectMeta.Name,
UID: route.UID,
APIVersion: route.APIVersion,
ResourceVersion: route.ResourceVersion,
},
Cluster: cluster,
Component: component,
})
}
}
}
}
return serviceEndpoints
}
func selectorNodeIP(ctx context.Context, clusterName string, client client.Client) string {
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()
var nodes corev1.NodeList
if err := client.List(multicluster.ContextWithClusterName(ctx, clusterName), &nodes); err != nil {
return ""
}
if len(nodes.Items) == 0 {
return ""
}
return selectGatewayIP(nodes.Items)
}
// judgeAppProtocol RFC-6335 and http://www.iana.org/assignments/service-names).
func judgeAppProtocol(port int32) string {
switch port {
case 80, 8080:
return querytypes.HTTP
case 443:
return querytypes.HTTPS
case 3306:
return querytypes.Mysql
case 6379:
return querytypes.Redis
default:
return ""
}
}
// selectGatewayIP will choose one gateway IP from all nodes, it will pick up external IP first. If there isn't any, it will pick the first node's internal IP.
func selectGatewayIP(nodes []corev1.Node) string {
var gatewayNode *corev1.Node
var workerNodes []corev1.Node
for i, node := range nodes {
if _, exist := node.Labels[apis.LabelNodeRoleGateway]; exist {
gatewayNode = &nodes[i]
break
} else if _, exist := node.Labels[apis.LabelNodeRoleWorker]; exist {
workerNodes = append(workerNodes, nodes[i])
}
}
var candidates = nodes
if gatewayNode != nil {
candidates = []corev1.Node{*gatewayNode}
} else if len(workerNodes) > 0 {
candidates = workerNodes
}
if len(candidates) == 0 {
return ""
}
var addressMaps = make([]map[corev1.NodeAddressType]string, 0)
for _, node := range candidates {
var addressMap = make(map[corev1.NodeAddressType]string)
for _, address := range node.Status.Addresses {
addressMap[address.Type] = address.Address
}
// first get external ip
if ip, exist := addressMap[corev1.NodeExternalIP]; exist {
return ip
}
addressMaps = append(addressMaps, addressMap)
}
return addressMaps[0][corev1.NodeInternalIP]
}
@@ -0,0 +1,410 @@
/*
Copyright 2022 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
import (
"context"
"fmt"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/networking/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/intstr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
var _ = Describe("Test query endpoints", func() {
BeforeEach(func() {
})
Context("Test Generate Endpoints", func() {
It("Test endpoints with additional rules", func() {
err := k8sClient.Create(context.TODO(), &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "vela-system",
},
})
Expect(err).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
sts := common.AppStatus{
AppliedResources: []common.ClusterObjectReference{
{
Cluster: "",
ObjectReference: corev1.ObjectReference{
APIVersion: "machinelearning.seldon.io/v1",
Kind: "SeldonDeployment",
Namespace: "default",
Name: "sdep2",
},
},
},
}
testApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "endpoints-app-2",
Namespace: "default",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: "endpoints-test-2",
Type: "webservice",
},
},
},
Status: sts,
}
Expect(k8sClient.Create(context.TODO(), testApp)).Should(BeNil())
var gtapp v1beta1.Application
Expect(k8sClient.Get(context.TODO(), client.ObjectKey{Name: "endpoints-app-2", Namespace: "default"}, &gtapp)).Should(BeNil())
gtapp.Status = sts
Expect(k8sClient.Status().Update(ctx, &gtapp)).Should(BeNil())
var mr []v1beta1.ManagedResource
for _, ar := range sts.AppliedResources {
smr := v1beta1.ManagedResource{
ClusterObjectReference: ar,
}
smr.Component = "endpoints-test-2"
mr = append(mr, smr)
}
rt := &v1beta1.ResourceTracker{
ObjectMeta: metav1.ObjectMeta{
Name: "endpoints-app-2",
Namespace: "default",
Labels: map[string]string{
oam.LabelAppName: testApp.Name,
oam.LabelAppNamespace: testApp.Namespace,
},
},
Spec: v1beta1.ResourceTrackerSpec{
Type: v1beta1.ResourceTrackerTypeRoot,
ManagedResources: mr,
},
}
err = k8sClient.Create(context.TODO(), rt)
Expect(err).Should(BeNil())
By("Prepare configmap for relationship")
err = k8sClient.Create(context.TODO(), &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "rule-for-seldon-test",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
oam.LabelResourceRules: "true",
oam.LabelResourceRuleFormat: oam.ResourceTopologyFormatJSON,
},
},
Data: map[string]string{
"rules": `[
{
"parentResourceType": {
"group": "machinelearning.seldon.io",
"kind": "SeldonDeployment"
},
"childrenResourceType": [
{
"apiVersion": "v1",
"kind": "Service"
}
]
}
]`,
},
})
Expect(err).Should(BeNil())
testServiceList := []map[string]interface{}{
{
"name": "clusterip-2",
"ports": []corev1.ServicePort{
{Port: 80, TargetPort: intstr.FromInt(80), Name: "80port"},
{Port: 81, TargetPort: intstr.FromInt(81), Name: "81port"},
},
"type": corev1.ServiceTypeClusterIP,
},
{
"name": "load-balancer",
"ports": []corev1.ServicePort{
{Port: 8080, TargetPort: intstr.FromInt(8080), Name: "8080port", NodePort: 30020},
},
"type": corev1.ServiceTypeLoadBalancer,
"status": corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
IP: "2.2.2.2",
},
},
},
},
},
{
"name": "seldon-ambassador-2",
"ports": []corev1.ServicePort{
{Port: 80, TargetPort: intstr.FromInt(80), Name: "80port"},
},
"type": corev1.ServiceTypeLoadBalancer,
"status": corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{
{
IP: "1.1.1.1",
},
},
},
},
},
}
abgvk := schema.GroupVersionKind{
Group: "machinelearning.seldon.io",
Version: "v1",
Kind: "SeldonDeployment",
}
obj := &unstructured.Unstructured{}
obj.SetName("sdep2")
obj.SetNamespace("default")
obj.SetAnnotations(map[string]string{
annoAmbassadorServiceName: "seldon-ambassador-2",
annoAmbassadorServiceNamespace: "default",
})
obj.SetGroupVersionKind(abgvk)
err = k8sClient.Create(context.TODO(), obj)
Expect(err).Should(BeNil())
abobj := &unstructured.Unstructured{}
abobj.SetGroupVersionKind(abgvk)
Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "sdep2", Namespace: "default"}, abobj)).Should(BeNil())
for _, s := range testServiceList {
ns := "default"
if s["namespace"] != nil {
ns = s["namespace"].(string)
}
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: s["name"].(string),
Namespace: ns,
OwnerReferences: []metav1.OwnerReference{
{APIVersion: "machinelearning.seldon.io/v1", Kind: "SeldonDeployment", Name: "sdep2", UID: abobj.GetUID()},
},
},
Spec: corev1.ServiceSpec{
Ports: s["ports"].([]corev1.ServicePort),
Type: s["type"].(corev1.ServiceType),
},
}
if s["labels"] != nil {
service.Labels = s["labels"].(map[string]string)
}
err := k8sClient.Create(context.TODO(), service)
Expect(err).Should(BeNil())
if s["status"] != nil {
service.Status = s["status"].(corev1.ServiceStatus)
err := k8sClient.Status().Update(context.TODO(), service)
Expect(err).Should(BeNil())
}
}
params := &ListParams{
Params: ListVars{
App: Option{
Name: "endpoints-app-2",
Namespace: "default",
Filter: FilterOption{
Cluster: "",
ClusterNamespace: "default",
},
WithTree: true,
},
},
RuntimeParams: oamprovidertypes.RuntimeParams{
KubeClient: k8sClient,
},
}
res, err := CollectServiceEndpoints(context.Background(), params)
Expect(err).Should(BeNil())
urls := []string{
"http://1.1.1.1/seldon/default/sdep2",
"http://clusterip-2.default",
"clusterip-2.default:81",
"http://2.2.2.2:8080",
"http://1.1.1.1",
}
for i, e := range (*res).Returns.List {
Expect(urls[i]).Should(Equal(e.String()))
}
})
It("Test select gateway IP", func() {
masterNode := corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "node-1",
Labels: map[string]string{
"node-role.kubernetes.io/master": "true",
},
},
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{
Type: corev1.NodeInternalIP,
Address: "node1-internal-ip",
},
},
},
}
workerNode1 := corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "node-2",
Labels: map[string]string{
"node-role.kubernetes.io/worker": "true",
},
},
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{
Type: corev1.NodeInternalIP,
Address: "node2-internal-ip",
},
{
Type: corev1.NodeExternalIP,
Address: "node2-external-ip",
},
},
},
}
workerNode2 := corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "node-3",
Labels: map[string]string{
"node-role.kubernetes.io/worker": "true",
},
},
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{
Type: corev1.NodeInternalIP,
Address: "node3-internal-ip",
},
},
},
}
gatewayNode := corev1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "node-4",
Labels: map[string]string{
"node-role.kubernetes.io/gateway": "true",
},
},
Status: corev1.NodeStatus{
Addresses: []corev1.NodeAddress{
{
Type: corev1.NodeInternalIP,
Address: "node4-internal-ip",
},
{
Type: corev1.NodeExternalIP,
Address: "node4-external-ip",
},
},
},
}
testCase := []struct {
note string
nodes []corev1.Node
wantIP string
}{
{
note: "only master node",
nodes: []corev1.Node{masterNode},
wantIP: "node1-internal-ip",
},
{
note: "with worker node, select external ip first",
nodes: []corev1.Node{masterNode, workerNode1},
wantIP: "node2-external-ip",
},
{
note: "with worker node, select worker's internal ip",
nodes: []corev1.Node{masterNode, workerNode2},
wantIP: "node3-internal-ip",
},
{
note: "with gateway node, gateway node first",
nodes: []corev1.Node{masterNode, workerNode1, workerNode1, gatewayNode},
wantIP: "node4-external-ip",
},
}
for _, tc := range testCase {
By(tc.note)
ip := selectGatewayIP(tc.nodes)
Expect(ip).Should(Equal(tc.wantIP))
}
})
})
})
var _ = Describe("Test get ingress endpoint", func() {
It("Test get ingress endpoint with different apiVersion", func() {
ingress1 := v1.Ingress{}
Expect(yaml.Unmarshal([]byte(ingressYaml1), &ingress1)).Should(BeNil())
err := k8sClient.Create(ctx, &ingress1)
Expect(err).Should(BeNil())
gvk := schema.GroupVersionKind{Group: "networking.k8s.io", Version: "v1", Kind: "Ingress"}
Eventually(func() error {
eps := getServiceEndpoints(ctx, k8sClient, gvk, ingress1.Name, ingress1.Namespace, "", "", nil)
if len(eps) != 1 {
return fmt.Errorf("result length missmatch")
}
return nil
}, 2*time.Second, 500*time.Millisecond).Should(BeNil())
})
})
var ingressYaml1 = `
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-1
namespace: default
spec:
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80
`
+335
View File
@@ -0,0 +1,335 @@
/*
Copyright 2021. The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
import (
"bufio"
"bytes"
"context"
_ "embed"
"encoding/base64"
"fmt"
"io"
"strings"
"time"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
pkgmulticluster "github.com/kubevela/pkg/multicluster"
"github.com/kubevela/workflow/pkg/providers/legacy/kube"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/multicluster"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
const (
// ProviderName is provider name for install.
ProviderName = "query"
// HelmReleaseKind is the kind of HelmRelease
HelmReleaseKind = "HelmRelease"
annoAmbassadorServiceName = "ambassador.service/name"
annoAmbassadorServiceNamespace = "ambassador.service/namespace"
)
// Resource refer to an object with cluster info
type Resource struct {
Cluster string `json:"cluster"`
Component string `json:"component"`
Revision string `json:"revision"`
Object *unstructured.Unstructured `json:"object"`
}
// Option is the query option
type Option struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Filter FilterOption `json:"filter,omitempty"`
// WithStatus means query the object from the cluster and get the latest status
// This field only suitable for ListResourcesInApp
WithStatus bool `json:"withStatus,omitempty"`
// WithTree means recursively query the resource tree.
WithTree bool `json:"withTree,omitempty"`
}
// FilterOption filter resource created by component
type FilterOption struct {
Cluster string `json:"cluster,omitempty"`
ClusterNamespace string `json:"clusterNamespace,omitempty"`
Components []string `json:"components,omitempty"`
APIVersion string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
QueryNewest bool `json:"queryNewest,omitempty"`
}
// ListVars is the vars for list
type ListVars struct {
App Option `json:"app"`
}
// ListParams is the params for list
type ListParams = oamprovidertypes.Params[ListVars]
// ListReturnVars is the vars for list return
type ListReturnVars[T any] struct {
List []T `json:"list"`
Error string `json:"err,omitempty"`
}
// ListReturns is the returns for list
type ListReturns[T any] oamprovidertypes.Returns[ListReturnVars[T]]
// ListResourcesInApp lists CRs created by Application, this provider queries the object data.
func ListResourcesInApp(ctx context.Context, params *ListParams) (*ListReturns[Resource], error) {
collector := NewAppCollector(params.KubeClient, params.Params.App)
appResList, err := collector.CollectResourceFromApp(ctx)
if err != nil {
// nolint:nilerr
return &ListReturns[Resource]{Returns: ListReturnVars[Resource]{Error: err.Error()}}, nil
}
if appResList == nil {
appResList = make([]Resource, 0)
}
return &ListReturns[Resource]{Returns: ListReturnVars[Resource]{List: appResList}}, nil
}
// ListAppliedResources list applied resource from tracker, this provider only queries the metadata.
func ListAppliedResources(ctx context.Context, params *ListParams) (*ListReturns[querytypes.AppliedResource], error) {
opt := params.Params.App
cli := params.KubeClient
collector := NewAppCollector(cli, opt)
app := new(v1beta1.Application)
appKey := client.ObjectKey{Name: opt.Name, Namespace: opt.Namespace}
if err := cli.Get(ctx, appKey, app); err != nil {
// nolint:nilerr
return &ListReturns[querytypes.AppliedResource]{Returns: ListReturnVars[querytypes.AppliedResource]{Error: err.Error()}}, nil
}
appResList, err := collector.ListApplicationResources(ctx, app)
if err != nil {
// nolint:nilerr
return &ListReturns[querytypes.AppliedResource]{Returns: ListReturnVars[querytypes.AppliedResource]{Error: err.Error()}}, nil
}
if appResList == nil {
appResList = make([]querytypes.AppliedResource, 0)
}
return &ListReturns[querytypes.AppliedResource]{Returns: ListReturnVars[querytypes.AppliedResource]{List: appResList}}, nil
}
// CollectResources collects resources from the cluster
func CollectResources(ctx context.Context, params *ListParams) (*ListReturns[querytypes.ResourceItem], error) {
opt := params.Params.App
cli := params.KubeClient
collector := NewAppCollector(cli, opt)
app := new(v1beta1.Application)
appKey := client.ObjectKey{Name: opt.Name, Namespace: opt.Namespace}
if err := cli.Get(ctx, appKey, app); err != nil {
// nolint:nilerr
return &ListReturns[querytypes.ResourceItem]{Returns: ListReturnVars[querytypes.ResourceItem]{Error: err.Error()}}, nil
}
appResList, err := collector.ListApplicationResources(ctx, app)
if err != nil {
// nolint:nilerr
return &ListReturns[querytypes.ResourceItem]{Returns: ListReturnVars[querytypes.ResourceItem]{Error: err.Error()}}, nil
}
var resources = make([]querytypes.ResourceItem, 0)
for _, res := range appResList {
if res.ResourceTree != nil {
resources = append(resources, buildResourceArray(res, res.ResourceTree, res.ResourceTree, opt.Filter.Kind, opt.Filter.APIVersion)...)
} else if res.Kind == opt.Filter.Kind && res.APIVersion == opt.Filter.APIVersion {
object := &unstructured.Unstructured{}
object.SetAPIVersion(opt.Filter.APIVersion)
object.SetKind(opt.Filter.Kind)
if err := cli.Get(ctx, apimachinerytypes.NamespacedName{Namespace: res.Namespace, Name: res.Name}, object); err == nil {
resources = append(resources, buildResourceItem(res, querytypes.Workload{
APIVersion: app.APIVersion,
Kind: app.Kind,
Name: app.Name,
Namespace: app.Namespace,
}, object))
} else {
klog.Errorf("failed to get the service:%s", err.Error())
}
}
}
return &ListReturns[querytypes.ResourceItem]{Returns: ListReturnVars[querytypes.ResourceItem]{List: resources}}, nil
}
// SearchVars is the vars for search
type SearchVars struct {
Value *unstructured.Unstructured `json:"value"`
Cluster string `json:"cluster"`
}
// SearchParams is the params for search
type SearchParams = oamprovidertypes.Params[SearchVars]
// SearchEvents searches events
func SearchEvents(ctx context.Context, params *SearchParams) (*ListReturns[corev1.Event], error) {
obj := params.Params.Value
if obj == nil {
return nil, fmt.Errorf("please provide a object value to search events")
}
cluster := params.Params.Cluster
cli := params.KubeClient
listCtx := multicluster.ContextWithClusterName(ctx, cluster)
fieldSelector := getEventFieldSelector(obj)
eventList := corev1.EventList{}
listOpts := []client.ListOption{
client.InNamespace(obj.GetNamespace()),
client.MatchingFieldsSelector{
Selector: fieldSelector,
},
}
if err := cli.List(listCtx, &eventList, listOpts...); err != nil {
// nolint:nilerr
return &ListReturns[corev1.Event]{Returns: ListReturnVars[corev1.Event]{Error: err.Error()}}, nil
}
return &ListReturns[corev1.Event]{Returns: ListReturnVars[corev1.Event]{List: eventList.Items}}, nil
}
// LogVars is the vars for log
type LogVars struct {
Cluster string `json:"cluster"`
Namespace string `json:"namespace"`
Pod string `json:"pod"`
Options *corev1.PodLogOptions `json:"options,omitempty"`
}
// LogParams is the params for log
type LogParams = oamprovidertypes.Params[LogVars]
// LogReturnVars is the log return vars
type LogReturnVars struct {
Outputs map[string]interface{} `json:"outputs"`
}
// LogReturns is the log returns
type LogReturns = oamprovidertypes.Returns[LogReturnVars]
// CollectLogsInPod collects logs in pod
func CollectLogsInPod(ctx context.Context, params *LogParams) (*LogReturns, error) {
cluster := params.Params.Cluster
namespace := params.Params.Namespace
pod := params.Params.Pod
if pod == "" {
return nil, fmt.Errorf("please provide a pod name to collect logs")
}
opts := params.Params.Options
if opts == nil || opts.Container == "" {
return nil, fmt.Errorf("please provide the container name to collect logs")
}
cliCtx := multicluster.ContextWithClusterName(ctx, cluster)
cfg := params.KubeConfig
cfg.Wrap(pkgmulticluster.NewTransportWrapper())
clientSet, err := kubernetes.NewForConfig(cfg)
if err != nil {
return nil, errors.Wrapf(err, "failed to create kubernetes client")
}
var defaultOutputs = make(map[string]interface{})
var errMsg string
podInst, err := clientSet.CoreV1().Pods(namespace).Get(cliCtx, pod, v1.GetOptions{})
if err != nil {
errMsg += fmt.Sprintf("failed to get pod: %s; ", err.Error())
}
req := clientSet.CoreV1().Pods(namespace).GetLogs(pod, opts)
readCloser, err := req.Stream(cliCtx)
if err != nil {
errMsg += fmt.Sprintf("failed to get stream logs %s; ", err.Error())
}
if readCloser != nil && podInst != nil {
r := bufio.NewReader(readCloser)
buffer := bytes.NewBuffer(nil)
var readErr error
defer func() {
_ = readCloser.Close()
}()
for {
s, err := r.ReadString('\n')
buffer.WriteString(s)
if err != nil {
if !errors.Is(err, io.EOF) {
readErr = err
}
break
}
}
toDate := v1.Now()
var fromDate v1.Time
// nolint
if opts.SinceTime != nil {
fromDate = *opts.SinceTime
} else if opts.SinceSeconds != nil {
fromDate = v1.NewTime(toDate.Add(time.Duration(-(*opts.SinceSeconds) * int64(time.Second))))
} else {
fromDate = podInst.CreationTimestamp
}
// the cue string can not support the special characters
logs := base64.StdEncoding.EncodeToString(buffer.Bytes())
defaultOutputs = map[string]interface{}{
"logs": logs,
"info": map[string]interface{}{
"fromDate": fromDate,
"toDate": toDate,
},
}
if readErr != nil {
errMsg += readErr.Error()
}
}
if errMsg != "" {
klog.Warningf(errMsg)
defaultOutputs["err"] = errMsg
}
return &LogReturns{Returns: LogReturnVars{Outputs: defaultOutputs}}, nil
}
//go:embed ql.cue
var qlTemplate string
// GetTemplate returns the cue template.
func GetTemplate() string {
return strings.Join([]string{qlTemplate, kube.GetTemplate()}, "\n")
}
// GetProviders returns the cue providers.
func GetProviders() map[string]cuexruntime.ProviderFn {
qlProvider := map[string]cuexruntime.ProviderFn{
"listResourcesInApp": oamprovidertypes.GenericProviderFn[ListVars, ListReturns[Resource]](ListResourcesInApp),
"listAppliedResources": oamprovidertypes.GenericProviderFn[ListVars, ListReturns[querytypes.AppliedResource]](ListAppliedResources),
"collectResources": oamprovidertypes.GenericProviderFn[ListVars, ListReturns[querytypes.ResourceItem]](CollectResources),
"searchEvents": oamprovidertypes.GenericProviderFn[SearchVars, ListReturns[corev1.Event]](SearchEvents),
"collectLogsInPod": oamprovidertypes.GenericProviderFn[LogVars, LogReturns](CollectLogsInPod),
"collectServiceEndpoints": oamprovidertypes.GenericProviderFn[ListVars, ListReturns[querytypes.ServiceEndpoint]](CollectServiceEndpoints),
}
kubeProviders := kube.GetProviders()
for k, v := range kubeProviders {
qlProvider[k] = v
}
return qlProvider
}
File diff suppressed because it is too large Load Diff
+233
View File
@@ -0,0 +1,233 @@
#ListResourcesInApp: {
#do: "listResourcesInApp"
#provider: "query"
$params: {
app: {
name: string
namespace: string
filter?: {
cluster?: string
clusterNamespace?: string
components?: [...string]
kind?: string
apiVersion?: string
}
withStatus?: bool
}
}
$returns: {
list?: [...{
cluster: string
component: string
revision: string
object: {...}
}]
}
...
}
#ListAppliedResources: {
#do: "listAppliedResources"
#provider: "query"
$params: {
app: {
name: string
namespace: string
filter?: {
cluster?: string
clusterNamespace?: string
components?: [...string]
kind?: string
apiVersion?: string
}
}
}
$returns: {
list?: [...{
name: string
namespace?: string
cluster?: string
component?: string
trait?: string
kind?: string
uid?: string
apiVersion?: string
resourceVersion?: string
publishVersion?: string
deployVersion?: string
revision?: string
latest?: bool
resourceTree?: {
...
}
}]
}
...
}
#CollectPods: {
#do: "collectResources"
#provider: "query"
$params: {
app: {
name: string
namespace: string
filter?: {
cluster?: string
clusterNamespace?: string
components?: [...string]
kind: "Pod"
apiVersion: "v1"
}
withTree: true
}
}
$returns: {
list: [...{...}]
}
...
}
#CollectServices: {
#do: "collectResources"
#provider: "query"
$params: {
app: {
name: string
namespace: string
filter?: {
cluster?: string
clusterNamespace?: string
components?: [...string]
kind: "Service"
apiVersion: "v1"
}
withTree: true
}
}
$returns: {
list: [...{...}]
}
...
}
#SearchEvents: {
#do: "searchEvents"
#provider: "query"
$params: {
value: {...}
cluster: string
}
$returns: {
list: [...{...}]
}
...
}
#CollectLogsInPod: {
#do: "collectLogsInPod"
#provider: "query"
$params: {
cluster: string
namespace: string
pod: string
options: {
container: string
previous: *false | bool
sinceSeconds: *null | int
sinceTime: *null | string
timestamps: *false | bool
tailLines: *null | int
limitBytes: *null | int
}
}
$returns: {
outputs?: {
logs?: string
err?: string
info?: {
fromDate: string
toDate: string
}
...
}
}
...
}
#CollectServiceEndpoints: {
#do: "collectServiceEndpoints"
#provider: "query"
$params: {
app: {
name: string
namespace: string
filter?: {
cluster?: string
clusterNamespace?: string
components?: [...string]
}
withTree: true
}
}
$returns: {
list?: [...{
endpoint: {
protocol: string
appProtocol?: string
host?: string
port: int
portName?: string
path?: string
inner?: bool
}
ref: {...}
cluster?: string
component?: string
...
}]
}
...
}
#GetApplicationTree: {
#do: "listAppliedResources"
#provider: "query"
app: {
name: string
namespace: string
filter?: {
cluster?: string
clusterNamespace?: string
components?: [...string]
queryNewest?: bool
}
withTree: true
}
list?: [...{
name: string
namespace?: string
cluster?: string
component?: string
trait?: string
kind?: string
uid?: string
apiVersion?: string
resourceVersion?: string
publishVersion?: string
deployVersion?: string
revision?: string
latest?: bool
...
}]
...
}
@@ -0,0 +1,88 @@
/*
Copyright 2021. The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
import (
"context"
"testing"
"time"
cuexv1alpha1 "github.com/kubevela/pkg/apis/cue/v1alpha1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
batchv1 "k8s.io/api/batch/v1"
"k8s.io/client-go/rest"
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
var cfg *rest.Config
var k8sClient client.Client
var testEnv *envtest.Environment
var ctx context.Context
var _ = BeforeSuite(func() {
By("bootstrapping test environment")
testEnv = &envtest.Environment{
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.Bool(false),
CRDDirectoryPaths: []string{
"./testdata/gateway/crds",
"../../../../charts/vela-core/crds",
"./testdata/machinelearning.seldon.io_seldondeployments.yaml",
"./testdata/helm-release-crd.yaml",
},
}
By("start kube test env")
var err error
cfg, err = testEnv.Start()
Expect(err).ShouldNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
By("new kube client")
cfg.Timeout = time.Minute * 2
scheme := common.Scheme
err = batchv1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
err = cuexv1alpha1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).Should(BeNil())
Expect(k8sClient).ToNot(BeNil())
ctx = context.Background()
Expect(err).To(BeNil())
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
func TestQueryProvider(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "VelaQL Suite")
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
annotations:
oam.dev/kubevela-version: v1.5.0-alpha.2
ports-mapping: "8000:80,8443:443"
labels:
addons.oam.dev/name: velaux
addons.oam.dev/registry: KubeVela
addons.oam.dev/version: v1.5.0-alpha.3
app.oam.dev/app-revision-hash: 33e813ddfe9a34be
app.oam.dev/appRevision: addon-velaux-v36
app.oam.dev/cluster: ""
app.oam.dev/component: velaux
app.oam.dev/name: addon-velaux
app.oam.dev/namespace: vela-system
app.oam.dev/resourceType: TRAIT
app.oam.dev/revision: velaux-v16
oam.dev/render-hash: e7be271bfad2cb55
trait.oam.dev/resource: gateway
trait.oam.dev/type: https-route
name: velaux-gateway-tls
namespace: vela-system
spec:
gatewayClassName: traefik
listeners:
- allowedRoutes:
namespaces:
from: Same
name: kubevela
port: 8443
protocol: HTTPS
tls:
certificateRefs:
- group: ""
kind: Secret
name: kubevela
namespace: vela-system
mode: Terminate
@@ -0,0 +1,16 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: Gateway
metadata:
name: traefik-gateway
namespace: vela-system
annotations:
ports-mapping: "8000:80,8443:443"
spec:
gatewayClassName: traefik
listeners:
- allowedRoutes:
namespaces:
from: All
name: web
port: 8000
protocol: HTTP
@@ -0,0 +1,40 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: http-test-route
namespace: default
spec:
hostnames:
- gateway.domain
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: traefik-gateway
namespace: vela-system
sectionName: web
rules:
- backendRefs:
- group: ""
kind: Service
name: game2048
port: 80
weight: 1
matches:
- path:
type: PathPrefix
value: /
- group: ""
kind: Service
name: game2048
port: 80
weight: 1
- backendRefs:
- group: ""
kind: Service
name: game2048-2
port: 80
weight: 1
matches:
- path:
type: PathPrefix
value: /api
@@ -0,0 +1,41 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
annotations:
oam.dev/kubevela-version: v1.5.0-alpha.2
labels:
addons.oam.dev/name: velaux
addons.oam.dev/registry: KubeVela
addons.oam.dev/version: v1.5.0-alpha.3
app.oam.dev/app-revision-hash: 33e813ddfe9a34be
app.oam.dev/appRevision: addon-velaux-v36
app.oam.dev/cluster: ""
app.oam.dev/component: velaux
app.oam.dev/name: addon-velaux
app.oam.dev/namespace: vela-system
app.oam.dev/resourceType: TRAIT
app.oam.dev/revision: velaux-v16
oam.dev/render-hash: 2e8aa179bec2b4ec
trait.oam.dev/resource: httpsRoute
trait.oam.dev/type: https-route
name: velaux-ssl
namespace: default
spec:
hostnames:
- demo.kubevela.net
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: velaux-gateway-tls
namespace: vela-system
rules:
- backendRefs:
- group: ""
kind: Service
name: velaux
port: 80
weight: 1
matches:
- path:
type: PathPrefix
value: /
@@ -0,0 +1,736 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: helmreleases.helm.toolkit.fluxcd.io
spec:
conversion:
strategy: None
group: helm.toolkit.fluxcd.io
names:
kind: HelmRelease
listKind: HelmReleaseList
plural: helmreleases
shortNames:
- hr
singular: helmrelease
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
type: string
- jsonPath: .status.conditions[?(@.type=="Ready")].message
name: Status
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
name: v2beta1
schema:
openAPIV3Schema:
description: HelmRelease is the Schema for the helmreleases API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: HelmReleaseSpec defines the desired state of a Helm release.
properties:
chart:
description: Chart defines the template of the v1beta1.HelmChart that
should be created for this HelmRelease.
properties:
spec:
description: Spec holds the template for the v1beta1.HelmChartSpec
for this HelmRelease.
properties:
chart:
description: The name or path the Helm chart is available
at in the SourceRef.
type: string
interval:
description: Interval at which to check the v1beta1.Source
for updates. Defaults to 'HelmReleaseSpec.Interval'.
type: string
sourceRef:
description: The name and namespace of the v1beta1.Source
the chart is available at.
properties:
apiVersion:
description: APIVersion of the referent.
type: string
kind:
description: Kind of the referent.
enum:
- HelmRepository
- GitRepository
- Bucket
type: string
name:
description: Name of the referent.
maxLength: 253
minLength: 1
type: string
namespace:
description: Namespace of the referent.
maxLength: 63
minLength: 1
type: string
required:
- name
type: object
valuesFile:
description: Alternative values file to use as the default
chart values, expected to be a relative path in the SourceRef.
Deprecated in favor of ValuesFiles, for backwards compatibility
the file defined here is merged before the ValuesFiles items.
Ignored when omitted.
type: string
valuesFiles:
description: Alternative list of values files to use as the
chart values (values.yaml is not included by default), expected
to be a relative path in the SourceRef. Values files are
merged in the order of this list with the last file overriding
the first. Ignored when omitted.
items:
type: string
type: array
version:
default: '*'
description: Version semver expression, ignored for charts
from v1beta1.GitRepository and v1beta1.Bucket sources. Defaults
to latest when omitted.
type: string
required:
- chart
- sourceRef
type: object
required:
- spec
type: object
dependsOn:
description: DependsOn may contain a dependency.CrossNamespaceDependencyReference
slice with references to HelmRelease resources that must be ready
before this HelmRelease can be reconciled.
items:
description: CrossNamespaceDependencyReference holds the reference
to a dependency.
properties:
name:
description: Name holds the name reference of a dependency.
type: string
namespace:
description: Namespace holds the namespace reference of a dependency.
type: string
required:
- name
type: object
type: array
install:
description: Install holds the configuration for Helm install actions
for this HelmRelease.
properties:
crds:
description: "CRDs upgrade CRDs from the Helm Chart's crds directory
according to the CRD upgrade policy provided here. Valid values
are `Skip`, `Create` or `CreateReplace`. Default is `Create`
and if omitted CRDs are installed but not updated. \n Skip:
do neither install nor replace (update) any CRDs. \n Create:
new CRDs are created, existing CRDs are neither updated nor
deleted. \n CreateReplace: new CRDs are created, existing CRDs
are updated (replaced) but not deleted. \n By default, CRDs
are applied (installed) during Helm install action. With this
option users can opt-in to CRD replace existing CRDs on Helm
install actions, which is not (yet) natively supported by Helm.
https://helm.sh/docs/chart_best_practices/custom_resource_definitions."
enum:
- Skip
- Create
- CreateReplace
type: string
createNamespace:
description: CreateNamespace tells the Helm install action to
create the HelmReleaseSpec.TargetNamespace if it does not exist
yet. On uninstall, the namespace will not be garbage collected.
type: boolean
disableHooks:
description: DisableHooks prevents hooks from running during the
Helm install action.
type: boolean
disableOpenAPIValidation:
description: DisableOpenAPIValidation prevents the Helm install
action from validating rendered templates against the Kubernetes
OpenAPI Schema.
type: boolean
disableWait:
description: DisableWait disables the waiting for resources to
be ready after a Helm install has been performed.
type: boolean
disableWaitForJobs:
description: DisableWaitForJobs disables waiting for jobs to complete
after a Helm install has been performed.
type: boolean
remediation:
description: Remediation holds the remediation configuration for
when the Helm install action for the HelmRelease fails. The
default is to not perform any action.
properties:
ignoreTestFailures:
description: IgnoreTestFailures tells the controller to skip
remediation when the Helm tests are run after an install
action but fail. Defaults to 'Test.IgnoreFailures'.
type: boolean
remediateLastFailure:
description: RemediateLastFailure tells the controller to
remediate the last failure, when no retries remain. Defaults
to 'false'.
type: boolean
retries:
description: Retries is the number of retries that should
be attempted on failures before bailing. Remediation, using
an uninstall, is performed between each attempt. Defaults
to '0', a negative integer equals to unlimited retries.
type: integer
type: object
replace:
description: Replace tells the Helm install action to re-use the
'ReleaseName', but only if that name is a deleted release which
remains in the history.
type: boolean
skipCRDs:
description: "SkipCRDs tells the Helm install action to not install
any CRDs. By default, CRDs are installed if not already present.
\n Deprecated use CRD policy (`crds`) attribute with value `Skip`
instead."
type: boolean
timeout:
description: Timeout is the time to wait for any individual Kubernetes
operation (like Jobs for hooks) during the performance of a
Helm install action. Defaults to 'HelmReleaseSpec.Timeout'.
type: string
type: object
interval:
description: Interval at which to reconcile the Helm release.
type: string
kubeConfig:
description: KubeConfig for reconciling the HelmRelease on a remote
cluster. When specified, KubeConfig takes precedence over ServiceAccountName.
properties:
secretRef:
description: SecretRef holds the name to a secret that contains
a 'value' key with the kubeconfig file as the value. It must
be in the same namespace as the HelmRelease. It is recommended
that the kubeconfig is self-contained, and the secret is regularly
updated if credentials such as a cloud-access-token expire.
Cloud specific `cmd-path` auth helpers will not function without
adding binaries and credentials to the Pod that is responsible
for reconciling the HelmRelease.
properties:
name:
description: Name of the referent
type: string
required:
- name
type: object
type: object
maxHistory:
description: MaxHistory is the number of revisions saved by Helm for
this HelmRelease. Use '0' for an unlimited number of revisions;
defaults to '10'.
type: integer
postRenderers:
description: PostRenderers holds an array of Helm PostRenderers, which
will be applied in order of their definition.
items:
description: PostRenderer contains a Helm PostRenderer specification.
properties:
kustomize:
description: Kustomization to apply as PostRenderer.
properties:
images:
description: Images is a list of (image name, new name,
new tag or digest) for changing image names, tags or digests.
This can also be achieved with a patch, but this operator
is simpler to specify.
items:
description: Image contains an image name, a new name,
a new tag or digest, which will replace the original
name and tag.
properties:
digest:
description: Digest is the value used to replace the
original image tag. If digest is present NewTag
value is ignored.
type: string
name:
description: Name is a tag-less image name.
type: string
newName:
description: NewName is the value used to replace
the original name.
type: string
newTag:
description: NewTag is the value used to replace the
original tag.
type: string
required:
- name
type: object
type: array
patchesJson6902:
description: JSON 6902 patches, defined as inline YAML objects.
items:
description: JSON6902Patch contains a JSON6902 patch and
the target the patch should be applied to.
properties:
patch:
description: Patch contains the JSON6902 patch document
with an array of operation objects.
items:
description: JSON6902 is a JSON6902 operation object.
https://tools.ietf.org/html/rfc6902#section-4
properties:
from:
type: string
op:
enum:
- test
- remove
- add
- replace
- move
- copy
type: string
path:
type: string
value:
x-kubernetes-preserve-unknown-fields: true
required:
- op
- path
type: object
type: array
target:
description: Target points to the resources that the
patch document should be applied to.
properties:
annotationSelector:
description: AnnotationSelector is a string that
follows the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
It matches with the resource annotations.
type: string
group:
description: Group is the API group to select
resources from. Together with Version and Kind
it is capable of unambiguously identifying and/or
selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type: string
kind:
description: Kind of the API Group to select resources
from. Together with Group and Version it is
capable of unambiguously identifying and/or
selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type: string
labelSelector:
description: LabelSelector is a string that follows
the label selection expression https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api
It matches with the resource labels.
type: string
name:
description: Name to match resources with.
type: string
namespace:
description: Namespace to select resources from.
type: string
version:
description: Version of the API Group to select
resources from. Together with Group and Kind
it is capable of unambiguously identifying and/or
selecting resources. https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type: string
type: object
required:
- patch
- target
type: object
type: array
patchesStrategicMerge:
description: Strategic merge patches, defined as inline
YAML objects.
items:
x-kubernetes-preserve-unknown-fields: true
type: array
type: object
type: object
type: array
releaseName:
description: ReleaseName used for the Helm release. Defaults to a
composition of '[TargetNamespace-]Name'.
maxLength: 53
minLength: 1
type: string
rollback:
description: Rollback holds the configuration for Helm rollback actions
for this HelmRelease.
properties:
cleanupOnFail:
description: CleanupOnFail allows deletion of new resources created
during the Helm rollback action when it fails.
type: boolean
disableHooks:
description: DisableHooks prevents hooks from running during the
Helm rollback action.
type: boolean
disableWait:
description: DisableWait disables the waiting for resources to
be ready after a Helm rollback has been performed.
type: boolean
disableWaitForJobs:
description: DisableWaitForJobs disables waiting for jobs to complete
after a Helm rollback has been performed.
type: boolean
force:
description: Force forces resource updates through a replacement
strategy.
type: boolean
recreate:
description: Recreate performs pod restarts for the resource if
applicable.
type: boolean
timeout:
description: Timeout is the time to wait for any individual Kubernetes
operation (like Jobs for hooks) during the performance of a
Helm rollback action. Defaults to 'HelmReleaseSpec.Timeout'.
type: string
type: object
serviceAccountName:
description: The name of the Kubernetes service account to impersonate
when reconciling this HelmRelease.
type: string
storageNamespace:
description: StorageNamespace used for the Helm storage. Defaults
to the namespace of the HelmRelease.
maxLength: 63
minLength: 1
type: string
suspend:
description: Suspend tells the controller to suspend reconciliation
for this HelmRelease, it does not apply to already started reconciliations.
Defaults to false.
type: boolean
targetNamespace:
description: TargetNamespace to target when performing operations
for the HelmRelease. Defaults to the namespace of the HelmRelease.
maxLength: 63
minLength: 1
type: string
test:
description: Test holds the configuration for Helm test actions for
this HelmRelease.
properties:
enable:
description: Enable enables Helm test actions for this HelmRelease
after an Helm install or upgrade action has been performed.
type: boolean
ignoreFailures:
description: IgnoreFailures tells the controller to skip remediation
when the Helm tests are run but fail. Can be overwritten for
tests run after install or upgrade actions in 'Install.IgnoreTestFailures'
and 'Upgrade.IgnoreTestFailures'.
type: boolean
timeout:
description: Timeout is the time to wait for any individual Kubernetes
operation during the performance of a Helm test action. Defaults
to 'HelmReleaseSpec.Timeout'.
type: string
type: object
timeout:
description: Timeout is the time to wait for any individual Kubernetes
operation (like Jobs for hooks) during the performance of a Helm
action. Defaults to '5m0s'.
type: string
uninstall:
description: Uninstall holds the configuration for Helm uninstall
actions for this HelmRelease.
properties:
disableHooks:
description: DisableHooks prevents hooks from running during the
Helm rollback action.
type: boolean
keepHistory:
description: KeepHistory tells Helm to remove all associated resources
and mark the release as deleted, but retain the release history.
type: boolean
timeout:
description: Timeout is the time to wait for any individual Kubernetes
operation (like Jobs for hooks) during the performance of a
Helm uninstall action. Defaults to 'HelmReleaseSpec.Timeout'.
type: string
type: object
upgrade:
description: Upgrade holds the configuration for Helm upgrade actions
for this HelmRelease.
properties:
cleanupOnFail:
description: CleanupOnFail allows deletion of new resources created
during the Helm upgrade action when it fails.
type: boolean
crds:
description: "CRDs upgrade CRDs from the Helm Chart's crds directory
according to the CRD upgrade policy provided here. Valid values
are `Skip`, `Create` or `CreateReplace`. Default is `Skip` and
if omitted CRDs are neither installed nor upgraded. \n Skip:
do neither install nor replace (update) any CRDs. \n Create:
new CRDs are created, existing CRDs are neither updated nor
deleted. \n CreateReplace: new CRDs are created, existing CRDs
are updated (replaced) but not deleted. \n By default, CRDs
are not applied during Helm upgrade action. With this option
users can opt-in to CRD upgrade, which is not (yet) natively
supported by Helm. https://helm.sh/docs/chart_best_practices/custom_resource_definitions."
enum:
- Skip
- Create
- CreateReplace
type: string
disableHooks:
description: DisableHooks prevents hooks from running during the
Helm upgrade action.
type: boolean
disableOpenAPIValidation:
description: DisableOpenAPIValidation prevents the Helm upgrade
action from validating rendered templates against the Kubernetes
OpenAPI Schema.
type: boolean
disableWait:
description: DisableWait disables the waiting for resources to
be ready after a Helm upgrade has been performed.
type: boolean
disableWaitForJobs:
description: DisableWaitForJobs disables waiting for jobs to complete
after a Helm upgrade has been performed.
type: boolean
force:
description: Force forces resource updates through a replacement
strategy.
type: boolean
preserveValues:
description: PreserveValues will make Helm reuse the last release's
values and merge in overrides from 'Values'. Setting this flag
makes the HelmRelease non-declarative.
type: boolean
remediation:
description: Remediation holds the remediation configuration for
when the Helm upgrade action for the HelmRelease fails. The
default is to not perform any action.
properties:
ignoreTestFailures:
description: IgnoreTestFailures tells the controller to skip
remediation when the Helm tests are run after an upgrade
action but fail. Defaults to 'Test.IgnoreFailures'.
type: boolean
remediateLastFailure:
description: RemediateLastFailure tells the controller to
remediate the last failure, when no retries remain. Defaults
to 'false' unless 'Retries' is greater than 0.
type: boolean
retries:
description: Retries is the number of retries that should
be attempted on failures before bailing. Remediation, using
'Strategy', is performed between each attempt. Defaults
to '0', a negative integer equals to unlimited retries.
type: integer
strategy:
description: Strategy to use for failure remediation. Defaults
to 'rollback'.
enum:
- rollback
- uninstall
type: string
type: object
timeout:
description: Timeout is the time to wait for any individual Kubernetes
operation (like Jobs for hooks) during the performance of a
Helm upgrade action. Defaults to 'HelmReleaseSpec.Timeout'.
type: string
type: object
values:
description: Values holds the values for this Helm release.
x-kubernetes-preserve-unknown-fields: true
valuesFrom:
description: ValuesFrom holds references to resources containing Helm
values for this HelmRelease, and information about how they should
be merged.
items:
description: ValuesReference contains a reference to a resource
containing Helm values, and optionally the key they can be found
at.
properties:
kind:
description: Kind of the values referent, valid values are ('Secret',
'ConfigMap').
enum:
- Secret
- ConfigMap
type: string
name:
description: Name of the values referent. Should reside in the
same namespace as the referring resource.
maxLength: 253
minLength: 1
type: string
optional:
description: Optional marks this ValuesReference as optional.
When set, a not found error for the values reference is ignored,
but any ValuesKey, TargetPath or transient error will still
result in a reconciliation failure.
type: boolean
targetPath:
description: TargetPath is the YAML dot notation path the value
should be merged at. When set, the ValuesKey is expected to
be a single flat value. Defaults to 'None', which results
in the values getting merged at the root.
type: string
valuesKey:
description: ValuesKey is the data key where the values.yaml
or a specific value can be found at. Defaults to 'values.yaml'.
type: string
required:
- kind
- name
type: object
type: array
required:
- chart
- interval
type: object
status:
description: HelmReleaseStatus defines the observed state of a HelmRelease.
properties:
conditions:
description: Conditions holds the conditions for the HelmRelease.
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are:
\"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type
\ // +patchStrategy=merge // +listType=map // +listMapKey=type
\ Conditions []metav1.Condition `json:\"conditions,omitempty\"
patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`
\n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
failures:
description: Failures is the reconciliation failure count against
the latest desired state. It is reset after a successful reconciliation.
format: int64
type: integer
helmChart:
description: HelmChart is the namespaced name of the HelmChart resource
created by the controller for the HelmRelease.
type: string
installFailures:
description: InstallFailures is the install failure count against
the latest desired state. It is reset after a successful reconciliation.
format: int64
type: integer
lastAppliedRevision:
description: LastAppliedRevision is the revision of the last successfully
applied source.
type: string
lastAttemptedRevision:
description: LastAttemptedRevision is the revision of the last reconciliation
attempt.
type: string
lastAttemptedValuesChecksum:
description: LastAttemptedValuesChecksum is the SHA1 checksum of the
values of the last reconciliation attempt.
type: string
lastHandledReconcileAt:
description: LastHandledReconcileAt holds the value of the most recent
reconcile request value, so a change can be detected.
type: string
lastReleaseRevision:
description: LastReleaseRevision is the revision of the last successful
Helm release.
type: integer
observedGeneration:
description: ObservedGeneration is the last observed generation.
format: int64
type: integer
upgradeFailures:
description: UpgradeFailures is the upgrade failure count against
the latest desired state. It is reset after a successful reconciliation.
format: int64
type: integer
type: object
type: object
served: true
storage: true
subresources:
status: {}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2022. The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/oam-dev/kubevela/pkg/oam"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
)
func buildResourceArray(res querytypes.AppliedResource, parent, node *querytypes.ResourceTreeNode, kind string, apiVersion string) (pods []querytypes.ResourceItem) {
if node.LeafNodes != nil {
for _, subNode := range node.LeafNodes {
pods = append(pods, buildResourceArray(res, node, subNode, kind, apiVersion)...)
}
} else if node.Kind == kind && node.APIVersion == apiVersion {
pods = append(pods, buildResourceItem(res, querytypes.Workload{
APIVersion: parent.APIVersion,
Kind: parent.Kind,
Name: parent.Name,
Namespace: parent.Namespace,
}, node.Object))
}
return
}
func buildResourceItem(res querytypes.AppliedResource, workload querytypes.Workload, object *unstructured.Unstructured) querytypes.ResourceItem {
return querytypes.ResourceItem{
Cluster: res.Cluster,
Workload: workload,
Component: res.Component,
Object: object,
PublishVersion: func() string {
if object.GetAnnotations()[oam.AnnotationPublishVersion] != "" {
return object.GetAnnotations()[oam.AnnotationPublishVersion]
}
return res.PublishVersion
}(),
DeployVersion: func() string {
if object.GetAnnotations()[oam.AnnotationDeployVersion] != "" {
return object.GetAnnotations()[oam.AnnotationDeployVersion]
}
return res.DeployVersion
}(),
}
}
@@ -0,0 +1,17 @@
/*
Copyright 2022. The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package query
@@ -0,0 +1,28 @@
// terraform.cue
#LoadTerraformComponents: {
#provider: "terraform"
#do: "load-terraform-components"
$returns: {
outputs: {
components: [...#Component]
}
}
}
#GetConnectionStatus: {
#provider: "terraform"
#do: "get-connection-status"
$params: {
inputs: {
componentName: string
}
}
$returns: {
outputs: {
healthy?: bool
}
}
}
@@ -0,0 +1,131 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package terraform
import (
"context"
_ "embed"
"fmt"
"github.com/pkg/errors"
cuexruntime "github.com/kubevela/pkg/cue/cuex/runtime"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/types"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
// Outputs is the output parameters for Terraform components.
type Outputs[T any] struct {
Outputs T `json:"outputs"`
}
// Inputs is the input parameters for Terraform components.
type Inputs[T any] struct {
Inputs T `json:"inputs"`
}
// ComponentVars is the input parameters for LoadTerraformComponents.
type ComponentVars struct {
Components []common.ApplicationComponent `json:"components"`
}
// ComponentReturns is the return value for LoadTerraformComponents.
type ComponentReturns = oamprovidertypes.Returns[Outputs[ComponentVars]]
// LoadTerraformComponents loads Terraform components.
func LoadTerraformComponents(ctx context.Context, params *oamprovidertypes.Params[any]) (*ComponentReturns, error) {
res := &ComponentReturns{
Returns: Outputs[ComponentVars]{
Outputs: ComponentVars{
Components: make([]common.ApplicationComponent, 0),
},
},
}
for _, comp := range params.App.Spec.Components {
wl, err := params.WorkloadRender(ctx, comp)
if err != nil {
return nil, errors.Wrapf(err, "failed to render component into workload")
}
if wl.CapabilityCategory != types.TerraformCategory {
continue
}
res.Returns.Outputs.Components = append(res.Returns.Outputs.Components, comp)
}
return res, nil
}
// ComponentNameVars is the input parameters for GetConnectionStatus.
type ComponentNameVars struct {
ComponentName string `json:"componentName"`
}
// ConnectionParams is the input parameters for GetConnectionStatus.
type ConnectionParams = oamprovidertypes.Params[Inputs[ComponentNameVars]]
// ConnectionResult is the result for connection status.
type ConnectionResult struct {
Healthy bool `json:"healthy"`
}
// ConnectionReturns is the return value for connection status.
type ConnectionReturns = oamprovidertypes.Returns[Outputs[ConnectionResult]]
// GetConnectionStatus returns the connection status of a component.
func GetConnectionStatus(_ context.Context, params *ConnectionParams) (*ConnectionReturns, error) {
app := params.RuntimeParams.App
componentName := params.Params.Inputs.ComponentName
if componentName == "" {
return nil, fmt.Errorf("componentName is required")
}
for _, svc := range app.Status.Services {
if svc.Name == componentName {
return &ConnectionReturns{
Returns: Outputs[ConnectionResult]{
Outputs: ConnectionResult{
Healthy: svc.Healthy,
},
},
}, nil
}
}
return &ConnectionReturns{
Returns: Outputs[ConnectionResult]{
Outputs: ConnectionResult{
Healthy: false,
},
},
}, nil
}
//go:embed terraform.cue
var template string
// GetTemplate returns the cue template.
func GetTemplate() string {
return template
}
// GetProviders returns the cue providers.
func GetProviders() map[string]cuexruntime.ProviderFn {
return map[string]cuexruntime.ProviderFn{
"load-terraform-components": oamprovidertypes.GenericProviderFn[any, ComponentReturns](LoadTerraformComponents),
"get-connection-status": oamprovidertypes.GenericProviderFn[Inputs[ComponentNameVars], ConnectionReturns](GetConnectionStatus),
}
}
@@ -0,0 +1,175 @@
/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package terraform
import (
"context"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/appfile"
oamprovidertypes "github.com/oam-dev/kubevela/pkg/workflow/providers/types"
)
func fakeWorkloadRenderer(_ context.Context, comp apicommon.ApplicationComponent) (*appfile.Component, error) {
if strings.HasPrefix(comp.Name, "error") {
return nil, errors.New(comp.Name)
}
if strings.HasPrefix(comp.Name, "terraform") {
return &appfile.Component{CapabilityCategory: types.TerraformCategory}, nil
}
return &appfile.Component{CapabilityCategory: types.CUECategory}, nil
}
func TestLoadTerraformComponents(t *testing.T) {
r := require.New(t)
ctx := context.Background()
scheme := runtime.NewScheme()
r.NoError(v1beta1.AddToScheme(scheme))
cli := fake.NewClientBuilder().WithScheme(scheme).Build()
terraformCD := &v1beta1.ComponentDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "terraform"},
Spec: v1beta1.ComponentDefinitionSpec{
Schematic: &apicommon.Schematic{
Terraform: &apicommon.Terraform{},
},
},
}
require.NoError(t, cli.Create(ctx, terraformCD))
cueCD := &v1beta1.ComponentDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "cue"},
Spec: v1beta1.ComponentDefinitionSpec{
Schematic: &apicommon.Schematic{
CUE: &apicommon.CUE{},
},
},
}
require.NoError(t, cli.Create(ctx, cueCD))
testCases := []struct {
Inputs []apicommon.ApplicationComponent
HasError bool
Outputs []apicommon.ApplicationComponent
}{
{
Inputs: []apicommon.ApplicationComponent{{Name: "error"}},
HasError: true,
},
{
Inputs: []apicommon.ApplicationComponent{
{Name: "terraform-1", Type: "terraform"},
{Name: "cue", Type: "cue"},
{Name: "terraform-2", Type: "terraform"},
},
Outputs: []apicommon.ApplicationComponent{
{Name: "terraform-1", Type: "terraform"},
{Name: "terraform-2", Type: "terraform"},
},
},
{
Inputs: []apicommon.ApplicationComponent{{Name: "cue", Type: "cue"}},
Outputs: []apicommon.ApplicationComponent{},
},
}
for _, testCase := range testCases {
app := &v1beta1.Application{}
app.Spec.Components = testCase.Inputs
res, err := LoadTerraformComponents(ctx, &oamprovidertypes.Params[any]{
RuntimeParams: oamprovidertypes.RuntimeParams{
WorkloadRender: fakeWorkloadRenderer,
App: app,
},
})
if testCase.HasError {
r.Error(err)
continue
}
r.NoError(err)
r.Equal(testCase.Outputs, res.Returns.Outputs.Components)
}
}
func TestGetConnectionStatus(t *testing.T) {
ctx := context.Background()
r := require.New(t)
testCases := []struct {
ComponentName string
Services []apicommon.ApplicationComponentStatus
Healthy bool
Error string
}{{
ComponentName: "",
Error: "componentName is required",
}, {
ComponentName: "comp",
Services: []apicommon.ApplicationComponentStatus{{
Name: "not-comp",
Healthy: true,
}},
Healthy: false,
}, {
ComponentName: "comp",
Services: []apicommon.ApplicationComponentStatus{{
Name: "not-comp",
Healthy: true,
}, {
Name: "comp",
Healthy: true,
}},
Healthy: true,
}, {
ComponentName: "comp",
Services: []apicommon.ApplicationComponentStatus{{
Name: "not-comp",
Healthy: true,
}, {
Name: "comp",
Healthy: false,
}},
Healthy: false,
}}
for _, testCase := range testCases {
app := &v1beta1.Application{}
app.Status.Services = testCase.Services
res, err := GetConnectionStatus(ctx, &ConnectionParams{
Params: Inputs[ComponentNameVars]{
Inputs: ComponentNameVars{
ComponentName: testCase.ComponentName,
},
},
RuntimeParams: oamprovidertypes.RuntimeParams{
App: app,
},
})
if testCase.Error != "" {
r.Error(err)
r.Contains(err.Error(), testCase.Error)
continue
}
r.NoError(err)
r.Equal(testCase.Healthy, res.Returns.Outputs.Healthy)
}
}
@@ -72,6 +72,7 @@ type RuntimeParams struct {
KubeHandlers *providertypes.KubeHandlers
KubeClient client.Client
KubeConfig *rest.Config
FieldLabel string
}
// OAMParams is the legacy oam input parameters of a provider.
@@ -80,6 +81,44 @@ type OAMParams[T any] struct {
RuntimeParams
}
// Params is the input parameters of a provider.
type Params[T any] struct {
Params T `json:"$params"`
RuntimeParams
}
// Returns is the returns of a provider.
type Returns[T any] struct {
Returns T `json:"$returns"`
}
// GenericProviderFn is the provider function
type GenericProviderFn[T any, U any] func(context.Context, *Params[T]) (*U, error)
// Call marshal value into json and decode into underlying function input
// parameters, then fill back the returned output value
func (fn GenericProviderFn[T, U]) Call(ctx context.Context, value cue.Value) (cue.Value, error) {
type p struct {
Params T `json:"$params"`
}
params := new(p)
bs, err := value.MarshalJSON()
if err != nil {
return value, err
}
if err = json.Unmarshal(bs, params); err != nil {
return value, err
}
runtimeParams := RuntimeParamsFrom(ctx)
label, _ := value.Label()
runtimeParams.FieldLabel = label
ret, err := fn(ctx, &Params[T]{Params: params.Params, RuntimeParams: runtimeParams})
if err != nil {
return value, err
}
return value.FillPath(cue.ParsePath(""), ret), nil
}
// OAMGenericProviderFn is the legacy oam provider function
type OAMGenericProviderFn[T any, U any] func(context.Context, *OAMParams[T]) (*U, error)
@@ -95,6 +134,8 @@ func (fn OAMGenericProviderFn[T, U]) Call(ctx context.Context, value cue.Value)
return value, err
}
runtimeParams := RuntimeParamsFrom(ctx)
label, _ := value.Label()
runtimeParams.FieldLabel = label
ret, err := fn(ctx, &OAMParams[T]{Params: *params, RuntimeParams: runtimeParams})
if err != nil {
return value, err
@@ -102,6 +143,16 @@ func (fn OAMGenericProviderFn[T, U]) Call(ctx context.Context, value cue.Value)
return value.FillPath(cue.ParsePath(""), ret), nil
}
// NativeProviderFn is the legacy native provider function
type NativeProviderFn func(context.Context, *Params[cue.Value]) (cue.Value, error)
// Call marshal value into json and decode into underlying function input
// parameters, then fill back the returned output value
func (fn NativeProviderFn) Call(ctx context.Context, value cue.Value) (cue.Value, error) {
runtimeParams := RuntimeParamsFrom(ctx)
return fn(ctx, &Params[cue.Value]{Params: value, RuntimeParams: runtimeParams})
}
// OAMNativeProviderFn is the legacy oam native provider function
type OAMNativeProviderFn func(context.Context, *OAMParams[cue.Value]) (cue.Value, error)

Some files were not shown because too many files have changed in this diff Show More