Feat: support to manage the configs by the CLI, UI, and workflow. (#4794)

* Feat: support to manage the integrations by the CLI and the workflow

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: remove the xml

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: add the unit test for the nacos writer

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: add the integration API

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: make the provider commands to be deprecated

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: make the unit test work

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: rename the integration to the config

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: make the unit test cases work

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: refactor the config commands

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: add the distribution status for the config

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: sort the import packages

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: refine the code style

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: refine the code style

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: get the content format before render the content

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: add some examples

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: the command test cases

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Feat: add the definitions of the workflow step

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: add some tests

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: add some tests

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: change the name

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: retry the CI

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

* Fix: refine some words

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>

Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
This commit is contained in:
barnettZQG 2022-10-17 17:15:45 +08:00 committed by GitHub
parent 5a4bdd4f6e
commit 49ed837f97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 7164 additions and 3152 deletions

View File

@ -74,9 +74,9 @@ const (
AnnoIngressControllerHTTPPort = "ingress.controller/http-port"
// AnnoIngressControllerHost define ingress controller externally host
AnnoIngressControllerHost = "ingress.controller/host"
// LabelConfigType is the label for config type
// LabelConfigType is the label marked as the template that generated the config.
LabelConfigType = "config.oam.dev/type"
// LabelConfigCatalog is the label for config catalog
// LabelConfigCatalog is the label marked as the secret generated from the config.
LabelConfigCatalog = "config.oam.dev/catalog"
// LabelConfigSubType is the sub-type for a config type
LabelConfigSubType = "config.oam.dev/sub-type"
@ -86,10 +86,18 @@ const (
LabelConfigSyncToMultiCluster = "config.oam.dev/multi-cluster"
// LabelConfigIdentifier is the label for config identifier
LabelConfigIdentifier = "config.oam.dev/identifier"
// LabelConfigScope is the label for config scope
LabelConfigScope = "config.oam.dev/scope"
// AnnotationConfigSensitive is the annotation for the sensitization
AnnotationConfigSensitive = "config.oam.dev/sensitive"
// AnnotationConfigTemplateNamespace is the annotation for the template namespace
AnnotationConfigTemplateNamespace = "config.oam.dev/template-namespace"
// AnnotationConfigDescription is the annotation for config description
AnnotationConfigDescription = "config.oam.dev/description"
// AnnotationConfigAlias is the annotation for config alias
AnnotationConfigAlias = "config.oam.dev/alias"
// AnnotationConfigDistributionSpec is the annotation key of the application that distributes the configs
AnnotationConfigDistributionSpec = "config.oam.dev/distribution-spec"
)
const (
@ -156,11 +164,13 @@ const (
// TerraformProvider is the config type for terraform provider
TerraformProvider = "terraform-provider"
// DexConnector is the config type for dex connector
DexConnector = "config-dex-connector"
DexConnector = "dex-connector"
// ImageRegistry is the config type for image registry
ImageRegistry = "config-image-registry"
ImageRegistry = "image-registry"
// HelmRepository is the config type for Helm chart repository
HelmRepository = "config-helm-repository"
HelmRepository = "helm-repository"
// CatalogConfigDistribution is the catalog type
CatalogConfigDistribution = "config-distribution"
)
const (

View File

@ -1,86 +0,0 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/config-image-registry.cue
apiVersion: core.oam.dev/v1beta1
kind: ComponentDefinition
metadata:
annotations:
alias.config.oam.dev: Image Registry
definition.oam.dev/description: Config information to authenticate image registry
labels:
catalog.config.oam.dev: velacore-config
custom.definition.oam.dev/ui-hidden: "true"
multi-cluster.config.oam.dev: "true"
type.config.oam.dev: image-registry
name: config-image-registry
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"encoding/base64"
"encoding/json"
"strconv"
)
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"
"config.oam.dev/multi-cluster": "true"
"config.oam.dev/identifier": parameter.registry
"config.oam.dev/sub-type": "auth"
}
}
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: 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
}
workload:
type: autodetects.core.oam.dev

View File

@ -0,0 +1,44 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/create-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: Create or update a config
name: create-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
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
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
//+usage=Specify the template of the config.
template?: string
//+usage=Specify the content of the config.
config: {...}
}

View File

@ -0,0 +1,31 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/delete-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: Delete a config
name: delete-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
deploy: op.#DeleteConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
}

View File

@ -0,0 +1,33 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/list-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: List the configs
name: list-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
output: op.#ListConfig & {
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
template: parameter.template
}
parameter: {
//+usage=Specify the template of the config.
template: string
//+usage=Specify the namespace of the config.
namespace?: string
}

View File

@ -0,0 +1,31 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/read-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: Read a config
name: read-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
output: op.#ReadConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
}

View File

@ -0,0 +1,44 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/create-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: Create or update a config
name: create-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
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
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
//+usage=Specify the template of the config.
template?: string
//+usage=Specify the content of the config.
config: {...}
}

View File

@ -0,0 +1,31 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/delete-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: Delete a config
name: delete-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
deploy: op.#DeleteConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
}

View File

@ -0,0 +1,33 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/list-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: List the configs
name: list-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
output: op.#ListConfig & {
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
if parameter.namespace == _|_ {
namespace: context.namespace
}
template: parameter.template
}
parameter: {
//+usage=Specify the template of the config.
template: string
//+usage=Specify the namespace of the config.
namespace?: string
}

View File

@ -0,0 +1,31 @@
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
# Definition source cue file: vela-templates/definitions/internal/read-config.cue
apiVersion: core.oam.dev/v1beta1
kind: WorkflowStepDefinition
metadata:
annotations:
definition.oam.dev/description: Read a config
name: read-config
namespace: {{ include "systemDefinitionNamespace" . }}
spec:
schematic:
cue:
template: |
import (
"vela/op"
)
output: op.#ReadConfig & {
name: parameter.name
if parameter.namespace != _|_ {
namespace: parameter.namespace
}
}
parameter: {
//+usage=Specify the name of the config.
name: string
//+usage=Specify the namespace of the config.
namespace?: string
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
# How to config a dex connector
* Step 1: Make sure there is a dex-connector template
```bash
$ vela config-template list -A
NAMESPACE NAME ALIAS SCOPE SENSITIVE CREATED-TIME
vela-system dex-connector Dex Connector system false 2022-10-12 23:48:05 +0800 CST
vela-system helm-repository Helm Repository project false 2022-10-14 12:04:58 +0800 CST
vela-system image-registry Image Registry project false 2022-10-13 15:39:37 +0800 CST
# View the document of the properties
$ vela config-template show dex-connector
```
If not exist, please enable the dex addon firstly.
* Step 2: Create a config
```bash
# Create a connector config
$ vela config create github-oauth --template dex-connector type=github github.clientID=*** github.clientSecret=*** github.redirectURI=***
```
Write a yaml file to create the config.
```bash
$ cat>github.yaml<<EOF
github:
clientID: ***
clientSecret: ***
redirectURI: ***
EOF
$ vela config create github-oauth --template dex-connector type=github -f github.yaml
```

View File

@ -1,21 +0,0 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: config-dex-connector-dev
namespace: vela-system
labels:
"app.oam.dev/source-of-truth": "from-inner-system"
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "config-dex-connector"
"config.oam.dev/sub-type": "github"
project: abc
spec:
components:
- name: dev
type: config-dex-connector
properties:
type: github
github:
clientID: "aa"
clientSecret: "bb"
redirectURI: "http://localhost:8080/callback"

View File

@ -0,0 +1,47 @@
# How to config a helm repository
* Step 1: Make sure there is a helm-repository template
```bash
$ vela config-template list -A
NAMESPACE NAME ALIAS SCOPE SENSITIVE CREATED-TIME
vela-system dex-connector Dex Connector system false 2022-10-12 23:48:05 +0800 CST
vela-system helm-repository Helm Repository project false 2022-10-14 12:04:58 +0800 CST
vela-system image-registry Image Registry project false 2022-10-13 15:39:37 +0800 CST
# View the document of the properties
$ vela config-template show helm-repository
```
If not exist, please enable the flux addon firstly.
* Step 2: Create a config and distribute to the developer namespace
```bash
# Create a developer environment(namespace)
$ vela env init developer --namespace developer
# Create a registry config for the chart repository hosted by KubeVela team
$ vela config create kubevela-core --template helm-repository --target developer url=https://charts.kubevela.net/core
```
* Step 3: Create a application to use the helm repository
```bash
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: helm-app
namespace: developer
spec:
components:
- name: helm
properties:
chart: vela-rollout
repoType: helm
retries: 3
secretRef: kubevela-core
url: https://charts.kubevela.net/core
type: helm
```

View File

@ -1,102 +0,0 @@
# How to store and use configurations
## General
- list all configuration types. Note before vela v1.5, the key is "custom.definition.oam.dev/catalog.config.oam.dev"
```shell
$ vela components --label catalog.config.oam.dev=velacore-config
NAME DEFINITION
config-dex-connector autodetects.core.oam.dev
config-helm-repository autodetects.core.oam.dev
config-image-registry autodetects.core.oam.dev
terraform-azure autodetects.core.oam.dev
terraform-baidu autodetects.core.oam.dev
```
```json
# Get http://127.0.0.1:8000/api/v1/configs
[
{
"definitions": [
"config-dex-connector"
],
"name": "Dex Connectors",
"type": "dex-connector"
},
{
"definitions": [
"config-helm-repository"
],
"name": "Helm Repository",
"type": "helm-repository"
},
{
"definitions": [
"config-image-registry"
],
"name": "Image Registry",
"type": "image-registry"
},
null,
{
"definitions": [
"terraform-baidu"
],
"name": "Terraform Cloud Provider",
"type": "terraform-provider"
}
]
```
- list all configurations
```shell
$ kubectl get secret -n vela-system -l=config.oam.dev/catalog=velacore-config
NAME TYPE DATA AGE
image-registry-dev kubernetes.io/dockerconfigjson 1 3h51m
```
## Image registry
- Create a config for an image registry
```shell
$ vela up -f app-config-image-registry-account-auth.yaml
Applying an application in vela K8s object format...
I0323 10:45:25.347102 85930 apply.go:107] "creating object" name="config-image-registry-account-auth-dev" resource="core.oam.dev/v1beta1, Kind=Application"
✅ App has been deployed 🚀🚀🚀
Port forward: vela port-forward config-image-registry-account-auth-dev
SSH: vela exec config-image-registry-account-auth-dev
Logging: vela logs config-image-registry-account-auth-dev
App status: vela status config-image-registry-account-auth-dev
Endpoint: vela status config-image-registry-account-auth-dev
--endpoint%
$ kubectl get secret -n vela-system -l=config.oam.dev/catalog=velacore-config
NAME TYPE DATA AGE
image-registry-dev kubernetes.io/dockerconfigjson 1 77s
```
- Deliver the config secret to working cluster
```shell
$ vela cluster list
CLUSTER TYPE ENDPOINT ACCEPTED LABELS
local Internal - true
bj X509Certificate https://123.57.73.107:6443 true
$ vela up -f app-deliever-secret.yaml
```
- Deploy an application who needs to pull images from the private image registry
```shell
$ export KUBECONFIG=~/.kube/config-bj
$ kubectl get secret -n vela-system -l=config.oam.dev/catalog=velacore-config
NAME TYPE DATA AGE
image-registry-dev kubernetes.io/dockerconfigjson 1 120s
$ vela up -f app-validate-imagePullSecret.yaml
```

View File

@ -1,20 +0,0 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: image-dev
namespace: vela-system
labels:
"app.oam.dev/source-of-truth": "from-inner-system"
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "config-image-registry"
project: abc
spec:
components:
- name: image-dev
type: config-image-registry
properties:
registry: "registry.cn-beijing.aliyuncs.com"
auth:
username: "xxx"
password: "PfwrjwifjFaked"
email: "a@gmail.com"

View File

@ -1,22 +0,0 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: config-project1
namespace: vela-system
labels:
config.oam.dev/catalog: "velacore-config"
config.oam.dev/type: "helm-repository"
spec:
components:
- name: deliver-secret
type: ref-objects
properties:
objects:
- name: reg-demo
resource: secret
policies:
- type: topology
name: dev
properties:
clusters: ["bj"]
namespace: default

View File

@ -1,14 +0,0 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: app-sample
namespace: ns1
spec:
components:
- name: sample
type: webservice
properties:
image: registry.cn-beijing.aliyuncs.com/vela/nginx:latest
imagePullPolicy: Always
imagePullSecrets:
- image-registry-dev

View File

@ -1,14 +0,0 @@
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: app-validate-image-pull-secret
namespace: vela-system
spec:
components:
- name: validate
type: webservice
properties:
image: registry.cn-beijing.aliyuncs.com/vela/nginx:latest
imagePullPolicy: Always
imagePullSecrets:
- image-registry-dev

View File

@ -0,0 +1,38 @@
# How to write the config to Nacos server
* Step 1: Make sure there are a nacos-server and nacos-config templates
```bash
$ vela config-template list -A
NAMESPACE NAME ALIAS SCOPE SENSITIVE CREATED-TIME
vela-system nacos-config Nacos Configuration system false 2022-10-13 15:39:44 +0800 CST
vela-system nacos-server Nacos Server system false 2022-10-13 15:39:47 +0800 CST
# View the document of the properties
$ vela config-template show nacos-server
```
If not exist, please enable the VelaUX addon firstly.
* Step 2: Create a config to added a Nacos server
```bash
# Create a nacos server config, the config name must be "nacos"
$ vela config create nacos --template nacos-server servers[0].ipAddr=127.0.0.1 servers[0].port=8849
```
* Step 3: Create a config to the Nacos server
```bash
# Use the default template, you could define custom template.
$ vela config create db-config --template nacos-config dataId=db group="DEFAULT_GROUP" contentType="properties" content.host=127.0.0.1 content.port=3306 content.username=root
```
Then, the content will be written to the Nacos server.
```properties
host = 127.0.0.1
port = 3306
username = root
```

View File

@ -0,0 +1,45 @@
# How to config a private registry
* Step 1: Make sure there is a image-registry template
```bash
$ vela config-template list -A
NAMESPACE NAME ALIAS SCOPE SENSITIVE CREATED-TIME
vela-system dex-connector Dex Connector system false 2022-10-12 23:48:05 +0800 CST
vela-system helm-repository Helm Repository project false 2022-10-14 12:04:58 +0800 CST
vela-system image-registry Image Registry project false 2022-10-13 15:39:37 +0800 CST
# View the document of the properties
$ vela config-template show image-registry
```
If not exist, please enable the VelaUX addon firstly.
* Step 2: Create a config and distribute to the developer namespace
```bash
# Create a developer environment(namespace)
$ vela env init developer --namespace developer
# Create a registry config for the docker hub, you could change the username and password
$ vela config create private-demo --template image-registry --target developer registry=index.docker.io auth.username=demo auth.password=demo
```
* Step 3: Create a application to use the private registry.
```bash
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: private-image
namespace: developer
spec:
components:
- name: private-image
properties:
cpu: "0.5"
image: private/nginx
imagePullSecrets:
- private-demo
type: webservice
```

20
go.mod
View File

@ -41,6 +41,7 @@ require (
github.com/go-openapi/spec v0.19.8
github.com/go-playground/validator/v10 v10.9.0
github.com/go-resty/resty/v2 v2.7.0
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.8
github.com/google/go-containerregistry v0.9.0
github.com/google/go-github/v32 v32.1.0
@ -68,7 +69,7 @@ require (
github.com/onsi/gomega v1.20.2
github.com/openkruise/kruise-api v1.1.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/client_golang v1.12.2
github.com/prometheus/client_model v0.2.0
github.com/rivo/tview v0.0.0-20220709181631-73bf2902b59a
github.com/robfig/cron/v3 v3.0.1
@ -83,7 +84,7 @@ require (
github.com/xanzy/go-gitlab v0.60.0
github.com/xlab/treeprint v1.1.0
go.mongodb.org/mongo-driver v1.5.1
go.uber.org/zap v1.19.1
go.uber.org/zap v1.21.0
golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122
golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
@ -145,12 +146,14 @@ require (
github.com/alibabacloud-go/tea-utils v1.4.5 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.0 // indirect
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 // indirect
github.com/aliyun/credentials-go v1.1.2 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
github.com/aws/aws-sdk-go v1.36.30 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
github.com/clbanning/mxj/v2 v2.5.5 // indirect
@ -247,7 +250,6 @@ require (
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/openshift/library-go v0.0.0-20220112153822-ac82336bd076 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021 // indirect
@ -295,21 +297,21 @@ require (
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c // indirect
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 // indirect
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
golang.org/x/tools v0.1.12 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/grpc v1.47.0 // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/gorp.v1 v1.7.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/src-d/go-billy.v4 v4.3.2 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
istio.io/api v0.0.0-20220512212136-561ffec82582 // indirect
istio.io/gogo-genproto v0.0.0-20211208193508-5ab4acc9eb1e // indirect
k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect
@ -328,10 +330,14 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
github.com/magiconair/properties v1.8.5
github.com/nacos-group/nacos-sdk-go/v2 v2.1.0
github.com/opencontainers/runc v1.1.3 // indirect
github.com/openkruise/rollouts v0.1.1-0.20220622054609-149e5a48da5e
github.com/pelletier/go-toml v1.9.4
github.com/xanzy/ssh-agent v0.3.0 // indirect
google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03 // indirect
gopkg.in/yaml.v2 v2.4.0
)
replace (

30
go.sum
View File

@ -253,6 +253,8 @@ github.com/alibabacloud-go/tea-utils/v2 v2.0.0 h1:s3XRBCDVHBQ42ck4xnLGcWgRMDf9v4
github.com/alibabacloud-go/tea-utils/v2 v2.0.0/go.mod h1:U5MTY10WwlquGPS34DOeomUGBB0gXbLueiq5Trwu0C4=
github.com/alibabacloud-go/tea-xml v1.1.2 h1:oLxa7JUXm2EDFzMg+7oRsYc+kutgCVwm+bZlhhmvW5M=
github.com/alibabacloud-go/tea-xml v1.1.2/go.mod h1:Rq08vgCcCAjHyRi/M7xlHKUykZCEtyBy9+DPF6GgEu8=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704 h1:PpfENOj/vPfhhy9N2OFRjpue0hjM5XqAp2thFmkXXIk=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.1704/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU=
github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/aliyun/credentials-go v1.1.2 h1:qU1vwGIBb3UJ8BwunHDRFtAhS6jnQLnde/yk0+Ih2GY=
github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw=
@ -342,6 +344,8 @@ github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembjv71DPz3uX/V/6MMlSyD9JBQ6kQ=
@ -910,6 +914,7 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
@ -932,6 +937,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -1258,6 +1264,7 @@ github.com/josharian/txtarfs v0.0.0-20210218200122-0702f000015a/go.mod h1:izVPOv
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -1369,6 +1376,7 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
@ -1525,6 +1533,8 @@ github.com/mwitkow/go-proto-validators v0.0.0-20180403085117-0950a7990007/go.mod
github.com/mwitkow/go-proto-validators v0.2.0/go.mod h1:ZfA1hW+UH/2ZHOWvQ3HnQaU0DtnpXu850MZiy+YUgcc=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nacos-group/nacos-sdk-go/v2 v2.1.0 h1:PxRwOzHhnK6eGGvioEGkn8s6XRXmUVuXu91i2yQcdDs=
github.com/nacos-group/nacos-sdk-go/v2 v2.1.0/go.mod h1:ys/1adWeKXXzbNWfRNbaFlX/t6HVLWdpsNDvmoWTw0g=
github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
@ -1707,8 +1717,9 @@ github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_golang v1.12.2 h1:51L9cDoUHVrXx4zWYlcLQIZ+d+VXHgqnYKkIuq4g/34=
github.com/prometheus/client_golang v1.12.2/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@ -2120,7 +2131,7 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/automaxprocs v1.2.0/go.mod h1:YfO3fm683kQpzETxlTGZhGIVmXAhaw3gxeBADbpZtnU=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA=
go.uber.org/multierr v0.0.0-20180122172545-ddea229ff1df/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@ -2136,8 +2147,8 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -2495,6 +2506,7 @@ golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -2537,8 +2549,8 @@ golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -2882,8 +2894,9 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w=
google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@ -2930,8 +2943,9 @@ gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.63.2 h1:tGK/CyBg7SMzb60vP1M03vNZ3VDu3wGQJwn7Sxi9r3c=
gopkg.in/ini.v1 v1.63.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=

View File

@ -32,7 +32,6 @@ import (
"text/template"
"time"
"cuelang.org/go/cue/cuecontext"
"github.com/Masterminds/semver/v3"
"github.com/google/go-github/v32/github"
"github.com/imdario/mergo"
@ -62,7 +61,8 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
utils2 "github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/config"
"github.com/oam-dev/kubevela/pkg/cue/script"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/oam"
@ -100,6 +100,9 @@ const (
// DefinitionsDirName is the addon definitions/ dir name
DefinitionsDirName string = "definitions"
// ConfigTemplateDirName is the addon config-templates/ dir name
ConfigTemplateDirName string = "config-templates"
// DefSchemaName is the addon definition schemas dir name
DefSchemaName string = "schemas"
@ -118,17 +121,18 @@ var ParameterFileName = strings.Join([]string{"resources", "parameter.cue"}, "/"
// ListOptions contains flags mark what files should be read in an addon directory
type ListOptions struct {
GetDetail bool
GetDefinition bool
GetResource bool
GetParameter bool
GetTemplate bool
GetDefSchema bool
GetDetail bool
GetDefinition bool
GetConfigTemplate bool
GetResource bool
GetParameter bool
GetTemplate bool
GetDefSchema bool
}
var (
// UIMetaOptions get Addon metadata for UI display
UIMetaOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true}
UIMetaOptions = ListOptions{GetDetail: true, GetDefinition: true, GetParameter: true, GetConfigTemplate: true}
// CLIMetaOptions get Addon metadata for CLI display
CLIMetaOptions = ListOptions{}
@ -206,6 +210,7 @@ type Pattern struct {
// Patterns is the file pattern that the addon should be in
var Patterns = []Pattern{
{IsDir: true, Value: ConfigTemplateDirName},
{Value: ReadmeFileName}, {Value: MetadataFileName}, {Value: TemplateFileName},
{Value: ParameterFileName}, {IsDir: true, Value: ResourcesDirName}, {IsDir: true, Value: DefinitionsDirName},
{IsDir: true, Value: DefSchemaName}, {IsDir: true, Value: ViewDirName}, {Value: AppTemplateCueFileName}, {Value: GlobalParameterFileName}, {Value: LegacyReadmeFileName}}
@ -291,6 +296,7 @@ func GetUIDataFromReader(r AsyncReader, meta *SourceMeta, opt ListOptions) (*UID
LegacyReadmeFileName: {!opt.GetDetail, readReadme},
MetadataFileName: {false, readMetadata},
DefinitionsDirName: {!opt.GetDefinition, readDefFile},
ConfigTemplateDirName: {!opt.GetConfigTemplate, readConfigTemplateFile},
ParameterFileName: {!opt.GetParameter, readParamFile},
GlobalParameterFileName: {!opt.GetParameter, readGlobalParamFile},
}
@ -318,7 +324,7 @@ func GetUIDataFromReader(r AsyncReader, meta *SourceMeta, opt ListOptions) (*UID
}
err := genAddonAPISchema(addon)
if err != nil {
return nil, fmt.Errorf("fail to generate openAPIschema for addon %s : %w", meta.Name, err)
return nil, fmt.Errorf("fail to generate openAPIschema for addon %s : %w (parameter: %s)", meta.Name, err, addon.Parameters)
}
}
addon.AvailableVersions = []string{addon.Version}
@ -338,10 +344,11 @@ func GetInstallPackageFromReader(r AsyncReader, meta *SourceMeta, uiData *UIData
// Read the installed data from UI metadata object to reduce network payload
var addon = &InstallPackage{
Meta: uiData.Meta,
Definitions: uiData.Definitions,
CUEDefinitions: uiData.CUEDefinitions,
Parameters: uiData.Parameters,
Meta: uiData.Meta,
Definitions: uiData.Definitions,
CUEDefinitions: uiData.CUEDefinitions,
Parameters: uiData.Parameters,
ConfigTemplates: uiData.ConfigTemplates,
}
for contentType, method := range addonContentsReader {
@ -454,6 +461,21 @@ func readDefFile(a *UIData, reader AsyncReader, readPath string) error {
return nil
}
// readConfigTemplateFile read single template file of the config
func readConfigTemplateFile(a *UIData, reader AsyncReader, readPath string) error {
b, err := reader.ReadFile(readPath)
if err != nil {
return err
}
filename := path.Base(readPath)
if filepath.Ext(filename) != ".cue" {
return nil
}
file := ElementFile{Data: b, Name: filepath.Base(readPath)}
a.ConfigTemplates = append(a.ConfigTemplates, file)
return nil
}
// readViewFile read single view file
func readViewFile(a *InstallPackage, reader AsyncReader, readPath string) error {
b, err := reader.ReadFile(readPath)
@ -590,23 +612,14 @@ func unmarshalToContent(content []byte) (fileContent *github.RepositoryContent,
}
func genAddonAPISchema(addonRes *UIData) error {
param, err := utils2.PrepareParameterCue(addonRes.Name, addonRes.Parameters)
cueScript, err := script.PrepareTemplateCUEScript([]byte(addonRes.Parameters))
if err != nil {
return err
}
val := cuecontext.New().CompileString(param)
schema, err := cueScript.ParsePropertiesToSchema()
if err != nil {
return err
}
data, err := common.GenOpenAPI(val)
if err != nil {
return err
}
schema, err := utils2.ConvertOpenAPISchema2SwaggerObject(data)
if err != nil {
return err
}
utils2.FixOpenAPISchema("", schema)
addonRes.APISchema = schema
return nil
}
@ -688,7 +701,6 @@ func RenderDefinitions(addon *InstallPackage, config *rest.Config) ([]*unstructu
obj.SetNamespace(types.DefaultKubeVelaNS)
defObjs = append(defObjs, obj)
}
for _, cueDef := range addon.CUEDefinitions {
def := definition.Definition{Unstructured: unstructured.Unstructured{}}
err := def.FromCUEString(cueDef.Data, config)
@ -703,6 +715,29 @@ func RenderDefinitions(addon *InstallPackage, config *rest.Config) ([]*unstructu
return defObjs, nil
}
// RenderConfigTemplates render the config template
func RenderConfigTemplates(addon *InstallPackage, cli client.Client) ([]*unstructured.Unstructured, error) {
templates := make([]*unstructured.Unstructured, 0)
factory := config.NewConfigFactory(cli)
for _, templateFile := range addon.ConfigTemplates {
t, err := factory.ParseTemplate("", []byte(templateFile.Data))
if err != nil {
return nil, err
}
t.ConfigMap.Namespace = types.DefaultKubeVelaNS
obj, err := util.Object2Unstructured(t.ConfigMap)
if err != nil {
return nil, err
}
obj.SetKind("ConfigMap")
obj.SetAPIVersion("v1")
templates = append(templates, obj)
}
return templates, nil
}
// RenderDefinitionSchema will render definitions' schema in addons.
func RenderDefinitionSchema(addon *InstallPackage) ([]*unstructured.Unstructured, error) {
schemaConfigmaps := make([]*unstructured.Unstructured, 0)
@ -1081,6 +1116,7 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error {
app.SetLabels(util.MergeMapOverrideWithDst(app.GetLabels(), map[string]string{oam.LabelAddonRegistry: h.r.Name}))
// Step1: Render the definitions
defs, err := RenderDefinitions(addon, h.config)
if err != nil {
return errors.Wrap(err, "render addon definitions fail")
@ -1096,11 +1132,19 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error {
}
}
// Step2: Render the config templates
templates, err := RenderConfigTemplates(addon, h.cli)
if err != nil {
return errors.Wrap(err, "render the config template fail")
}
// Step3: Render the definition schemas
schemas, err := RenderDefinitionSchema(addon)
if err != nil {
return errors.Wrap(err, "render addon definitions' schema fail")
}
// Step4: Render the velaQL views
views, err := RenderViews(addon)
if err != nil {
return errors.Wrap(err, "render addon views fail")
@ -1109,6 +1153,7 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error {
if err := passDefInAppAnnotation(defs, app); err != nil {
return errors.Wrapf(err, "cannot pass definition to addon app's annotation")
}
if h.dryRun {
result, err := yaml.Marshal(app)
if err != nil {
@ -1124,6 +1169,7 @@ func (h *Installer) dispatchAddonResource(addon *InstallPackage) error {
}
auxiliaryOutputs = append(auxiliaryOutputs, defs...)
auxiliaryOutputs = append(auxiliaryOutputs, templates...)
auxiliaryOutputs = append(auxiliaryOutputs, schemas...)
auxiliaryOutputs = append(auxiliaryOutputs, views...)

View File

@ -165,6 +165,8 @@ func testReaderFunc(t *testing.T, reader AsyncReader) {
assert.NoError(t, err)
assert.Equal(t, uiData.Name, testAddonName)
assert.True(t, uiData.Parameters != "")
assert.Equal(t, len(uiData.APISchema.Properties), 1)
assert.Equal(t, uiData.APISchema.Properties["example"].Value.Description, "the example field")
assert.True(t, len(uiData.Definitions) > 0)
testAddonName = "example-legacy"
@ -184,6 +186,7 @@ func testReaderFunc(t *testing.T, reader AsyncReader) {
// test get ui data
rName := "KubeVela"
uiDataList, err := ListAddonUIDataFromReader(reader, registryMeta, rName, UIMetaOptions)
fmt.Println(err.Error())
assert.True(t, strings.Contains(err.Error(), "preference mark not allowed at this position"))
assert.Equal(t, 5, len(uiDataList))
assert.Equal(t, uiDataList[0].RegistryName, rName)

View File

@ -1,3 +1,4 @@
parameter: {
example: string
}
//+usage=the example field
example: string
}

View File

@ -35,6 +35,7 @@ type UIData struct {
Definitions []ElementFile `json:"definitions"`
CUEDefinitions []ElementFile `json:"CUEDefinitions"`
ConfigTemplates []ElementFile `json:"configTemplates"`
Parameters string `json:"parameters"`
GlobalParameters string `json:"globalParameters"`
RegistryName string `json:"registryName"`
@ -49,6 +50,9 @@ type InstallPackage struct {
// Definitions and CUEDefinitions are converted as OAM X-Definitions, they will only in control plane cluster
Definitions []ElementFile `json:"definitions"`
CUEDefinitions []ElementFile `json:"CUEDefinitions"`
ConfigTemplates []ElementFile `json:"configTemplates"`
// YAMLViews and CUEViews are the instances of velaql, they will only in control plane cluster
YAMLViews []ElementFile `json:"YAMLViews"`
CUEViews []ElementFile `json:"CUEViews"`

View File

@ -16,6 +16,8 @@ limitations under the License.
package model
import "fmt"
func init() {
RegisterModel(&Project{})
}
@ -29,6 +31,11 @@ type Project struct {
Description string `json:"description,omitempty"`
}
// GetNamespace get the namespace name of this project.
func (p *Project) GetNamespace() string {
return fmt.Sprintf("project-%s", p.Name)
}
// TableName return custom table name
func (p *Project) TableName() string {
return tableNamePrefix + "project"

View File

@ -33,7 +33,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
@ -669,11 +668,6 @@ func (c *applicationServiceImpl) Deploy(ctx context.Context, app *model.Applicat
return nil, err
}
// sync configs to clusters
if err := c.syncConfigs4Application(ctx, oamApp, app.Project, workflow.EnvName); err != nil {
return nil, err
}
// step2: check and create deploy event
if !req.Force {
var lastVersion = model.ApplicationRevision{
@ -771,44 +765,6 @@ func (c *applicationServiceImpl) Deploy(ctx context.Context, app *model.Applicat
}, nil
}
// sync configs to clusters
func (c *applicationServiceImpl) syncConfigs4Application(ctx context.Context, app *v1beta1.Application, projectName, envName string) error {
var areTerraformComponents = true
for _, m := range app.Spec.Components {
d := &v1beta1.ComponentDefinition{}
if err := c.KubeClient.Get(ctx, client.ObjectKey{Namespace: velatypes.DefaultKubeVelaNS, Name: m.Type}, d); err != nil {
klog.ErrorS(err, "failed to get config type", "ComponentDefinition", m.Type)
}
// check the type of the componentDefinition is Terraform
if d.Spec.Schematic != nil && d.Spec.Schematic.Terraform == nil {
areTerraformComponents = false
}
}
// skip configs sync
if areTerraformComponents {
return nil
}
env, err := c.EnvService.GetEnv(ctx, envName)
if err != nil {
return err
}
var clusterTargets []*model.ClusterTarget
for _, t := range env.Targets {
target, err := c.TargetService.GetTarget(ctx, t)
if err != nil {
return err
}
if target.Cluster != nil {
clusterTargets = append(clusterTargets, target.Cluster)
}
}
if err := SyncConfigs(ctx, c.KubeClient, projectName, clusterTargets); err != nil {
return fmt.Errorf("sync config failure %w", err)
}
return nil
}
func (c *applicationServiceImpl) renderOAMApplication(ctx context.Context, appModel *model.Application, reqWorkflowName, envName, version string) (*v1beta1.Application, error) {
// Priority 1 uses the requested workflow as release .
// Priority 2 uses the default workflow as release .

View File

@ -38,6 +38,7 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
kubevelatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
@ -208,8 +209,8 @@ var _ = Describe("Test authentication service functions", func() {
Namespace: "vela-system",
Labels: map[string]string{
"app.oam.dev/source-of-truth": "from-inner-system",
"config.oam.dev/catalog": "velacore-config",
"config.oam.dev/type": "config-dex-connector",
"config.oam.dev/catalog": kubevelatypes.VelaCoreConfig,
"config.oam.dev/type": "dex-connector",
"config.oam.dev/sub-type": "ldap",
"project": "abc",
},

View File

@ -19,43 +19,33 @@ package service
import (
"context"
"encoding/json"
"fmt"
"strings"
"sort"
set "github.com/deckarep/golang-set"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
"github.com/pkg/errors"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/klog/v2"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"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/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
apis "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/utils/config"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
"github.com/oam-dev/kubevela/pkg/config"
"github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
const (
configIsReady = "Ready"
configIsNotReady = "Not ready"
terraformProviderAlias = "Terraform Cloud Provider"
configSyncProjectPrefix = "config-sync"
)
// ConfigService handle CRUD of configs
// ConfigService handle CRUD of config and template
type ConfigService interface {
ListConfigTypes(ctx context.Context, query string) ([]*apis.ConfigType, error)
GetConfigType(ctx context.Context, configType string) (*apis.ConfigType, error)
CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error
GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error)
GetConfig(ctx context.Context, configType, name string) (*apis.Config, error)
DeleteConfig(ctx context.Context, configType, name string) error
ListTemplates(ctx context.Context, project, scope string) ([]*apis.ConfigTemplate, error)
GetTemplate(ctx context.Context, tem config.NamespacedName) (*apis.ConfigTemplateDetail, error)
CreateConfig(ctx context.Context, project string, req apis.CreateConfigRequest) (*apis.Config, error)
UpdateConfig(ctx context.Context, project string, name string, req apis.UpdateConfigRequest) (*apis.Config, error)
ListConfigs(ctx context.Context, project, template string, withProperties bool) ([]*apis.Config, error)
GetConfig(ctx context.Context, project, name string) (*apis.Config, error)
DeleteConfig(ctx context.Context, project, name string) error
CreateConfigDistribution(ctx context.Context, project string, req apis.CreateConfigDistributionRequest) error
DeleteConfigDistribution(ctx context.Context, project, name string) error
ListConfigDistributions(ctx context.Context, project string) ([]*config.Distribution, error)
}
// NewConfigService returns a config use case
@ -64,425 +54,299 @@ func NewConfigService() ConfigService {
}
type configServiceImpl struct {
KubeClient client.Client `inject:"kubeClient"`
KubeClient client.Client `inject:"kubeClient"`
ProjectService ProjectService `inject:""`
Factory config.Factory `inject:"configFactory"`
Apply apply.Applicator `inject:"apply"`
}
// ListConfigTypes returns all config types
func (u *configServiceImpl) ListConfigTypes(ctx context.Context, query string) ([]*apis.ConfigType, error) {
defs := &v1beta1.ComponentDefinitionList{}
if err := u.KubeClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
definition.ConfigCatalog: types.VelaCoreConfig,
}); err != nil {
// ListTemplates list the config templates
func (u *configServiceImpl) ListTemplates(ctx context.Context, project, scope string) ([]*apis.ConfigTemplate, error) {
queryTemplates, err := u.Factory.ListTemplates(ctx, types.DefaultKubeVelaNS, scope)
if err != nil {
return nil, err
}
var items []v1beta1.ComponentDefinition
items = append(items, defs.Items...)
// for compatibility of the config catalog key
defsLegacy := &v1beta1.ComponentDefinitionList{}
if err := u.KubeClient.List(ctx, defsLegacy, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
// leave here as the legacy format to test the compatibility
definition.UserPrefix + definition.ConfigCatalog: types.VelaCoreConfig,
}); err != nil {
return nil, err
}
// filter repeated config,due to new labels that exist at the same time
for _, legacy := range defsLegacy.Items {
if legacy.Labels[definition.ConfigCatalog] == types.VelaCoreConfig {
continue
if scope == "project" && project != "" {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return nil, err
}
items = append(items, legacy)
}
var tfDefs []v1beta1.ComponentDefinition
var configTypes []*apis.ConfigType
for _, d := range items {
if DefinitionType(d.Labels) == types.TerraformProvider {
tfDefs = append(tfDefs, d)
continue
templates, err := u.Factory.ListTemplates(ctx, pro.GetNamespace(), scope)
if err != nil {
return nil, err
}
configTypes = append(configTypes, &apis.ConfigType{
Alias: DefinitionAlias(d.Annotations),
Name: d.Name,
Definitions: []string{d.Name},
Description: d.Annotations[types.AnnoDefinitionDescription],
queryTemplates = append(queryTemplates, templates...)
}
var templates []*apis.ConfigTemplate
for _, t := range queryTemplates {
templates = append(templates, &apis.ConfigTemplate{
Alias: t.Alias,
Name: t.Name,
Description: t.Description,
Namespace: t.Namespace,
Scope: t.Scope,
Sensitive: t.Sensitive,
CreateTime: t.CreateTime,
})
}
if len(tfDefs) > 0 {
tfType := &apis.ConfigType{
Alias: terraformProviderAlias,
Name: types.TerraformProvider,
}
definitions := make([]string, len(tfDefs))
for i, tf := range tfDefs {
definitions[i] = tf.Name
}
tfType.Definitions = definitions
return append(configTypes, tfType), nil
}
return configTypes, nil
sort.SliceStable(templates, func(i, j int) bool {
return templates[i].Alias < templates[j].Alias
})
return templates, nil
}
// GetConfigType returns a config type
func (u *configServiceImpl) GetConfigType(ctx context.Context, configType string) (*apis.ConfigType, error) {
d := &v1beta1.ComponentDefinition{}
if err := u.KubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: configType}, d); err != nil {
return nil, errors.Wrap(err, "failed to get config type")
// GetTemplate detail a template
func (u *configServiceImpl) GetTemplate(ctx context.Context, tem config.NamespacedName) (*apis.ConfigTemplateDetail, error) {
if tem.Namespace == "" {
tem.Namespace = types.DefaultKubeVelaNS
}
t := &apis.ConfigType{
Alias: DefinitionAlias(d.Annotations),
Name: configType,
Description: d.Annotations[types.AnnoDefinitionDescription],
template, err := u.Factory.LoadTemplate(ctx, tem.Name, tem.Namespace)
if err != nil {
if errors.Is(err, config.ErrTemplateNotFound) {
return nil, bcode.ErrTemplateNotFound
}
return nil, err
}
defaultUISchema := renderDefaultUISchema(template.Schema)
t := &apis.ConfigTemplateDetail{
ConfigTemplate: apis.ConfigTemplate{
Alias: template.Alias,
Name: template.Name,
Description: template.Description,
Namespace: template.Namespace,
Scope: template.Scope,
Sensitive: template.Sensitive,
CreateTime: template.CreateTime,
},
APISchema: template.Schema,
// TODO: Support to define the custom UI schema in the template cue script.
UISchema: renderCustomUISchema(ctx, u.KubeClient, template.Name, "config", defaultUISchema),
}
return t, nil
}
func (u *configServiceImpl) CreateConfig(ctx context.Context, req apis.CreateConfigRequest) error {
p := req.Properties
// If the component is Terraform type, set the provider name same as the application name and the component name
if strings.HasPrefix(req.ComponentType, types.TerraformComponentPrefix) {
var properties map[string]interface{}
if err := json.Unmarshal([]byte(p), &properties); err != nil {
return errors.Wrapf(err, "unable to process the properties of %s", req.ComponentType)
}
properties["name"] = req.Name
tmp, err := json.Marshal(properties)
if err != nil {
return errors.Wrapf(err, "unable to process the properties of %s", req.ComponentType)
}
p = string(tmp)
}
ui := config.UIParam{
Alias: req.Alias,
Description: req.Description,
Project: req.Project,
}
return config.CreateApplication(ctx, u.KubeClient, req.Name, req.ComponentType, p, ui)
}
func (u *configServiceImpl) GetConfigs(ctx context.Context, configType string) ([]*apis.Config, error) {
switch configType {
case types.TerraformProvider:
providers, err := config.ListTerraformProviders(ctx, u.KubeClient)
func (u *configServiceImpl) CreateConfig(ctx context.Context, project string, req apis.CreateConfigRequest) (*apis.Config, error) {
ns := types.DefaultKubeVelaNS
if project != "" {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return nil, err
}
configs := make([]*apis.Config, len(providers))
for i, p := range providers {
var a v1beta1.Application
if err := u.KubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: p.Name}, &a); err != nil {
if kerrors.IsNotFound(err) {
t := p.CreationTimestamp.Time
configs[i] = &apis.Config{
Name: p.Name,
CreatedTime: &t,
}
if p.Status.State == terraformtypes.ProviderIsReady {
configs[i].Status = configIsReady
} else {
configs[i].Status = configIsNotReady
}
continue
}
return nil, err
}
// If the application doesn't have any components, skip it as something wrong happened.
if !strings.HasPrefix(a.Labels[types.LabelConfigType], types.TerraformComponentPrefix) {
continue
}
configs[i] = retrieveConfigFromApplication(a, a.Labels[types.LabelConfigProject])
ns = pro.GetNamespace()
if err := utils.CreateNamespace(ctx, u.KubeClient, ns); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err
}
return configs, nil
default:
return u.getConfigsByConfigType(ctx, configType)
}
var properties = make(map[string]interface{})
if err := json.Unmarshal([]byte(req.Properties), &properties); err != nil {
return nil, err
}
if req.Template.Namespace == "" {
req.Template.Namespace = types.DefaultKubeVelaNS
}
configItem, err := u.Factory.ParseConfig(ctx, config.NamespacedName(req.Template), config.Metadata{
NamespacedName: config.NamespacedName{Name: req.Name, Namespace: ns},
Properties: properties,
Alias: req.Alias, Description: req.Description,
})
if err != nil {
if errors.Is(err, config.ErrTemplateNotFound) {
return nil, bcode.ErrTemplateNotFound
}
return nil, err
}
if err := u.Factory.CreateOrUpdateConfig(ctx, configItem, ns); err != nil {
if errors.Is(err, config.ErrConfigExist) {
return nil, bcode.ErrConfigExist
}
return nil, err
}
return convertConfig(project, *configItem), nil
}
func (u *configServiceImpl) getConfigsByConfigType(ctx context.Context, configType string) ([]*apis.Config, error) {
var apps = &v1beta1.ApplicationList{}
if err := u.KubeClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: configType,
}); err != nil {
func (u *configServiceImpl) UpdateConfig(ctx context.Context, project string, name string, req apis.UpdateConfigRequest) (*apis.Config, error) {
ns := types.DefaultKubeVelaNS
if project != "" {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return nil, err
}
ns = pro.GetNamespace()
}
it, err := u.Factory.GetConfig(ctx, ns, name, false)
if err != nil {
if errors.Is(err, config.ErrSensitiveConfig) {
return nil, bcode.ErrSensitiveConfig
}
if errors.Is(err, config.ErrConfigNotFound) {
return nil, bcode.ErrConfigNotFound
}
return nil, err
}
configs := make([]*apis.Config, len(apps.Items))
for i, a := range apps.Items {
configs[i] = retrieveConfigFromApplication(a, a.Labels[types.LabelConfigProject])
}
return configs, nil
}
func (u *configServiceImpl) GetConfig(ctx context.Context, configType, name string) (*apis.Config, error) {
var a = &v1beta1.Application{}
if err := u.KubeClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, a); err != nil {
var properties = make(map[string]interface{})
if err := json.Unmarshal([]byte(req.Properties), &properties); err != nil {
return nil, err
}
config := &apis.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: a.Labels[types.LabelConfigProject],
CreatedTime: &a.CreationTimestamp.Time,
configItem, err := u.Factory.ParseConfig(ctx,
it.Template.NamespacedName,
config.Metadata{NamespacedName: config.NamespacedName{Name: it.Name, Namespace: ns}, Alias: req.Alias, Description: req.Description, Properties: properties})
if err != nil {
return nil, err
}
return config, nil
if err := u.Factory.CreateOrUpdateConfig(ctx, configItem, ns); err != nil {
if errors.Is(err, config.ErrConfigExist) {
return nil, bcode.ErrChangeTemplate
}
return nil, err
}
return convertConfig(project, *configItem), nil
}
func (u *configServiceImpl) DeleteConfig(ctx context.Context, configType, name string) error {
var isTerraformProvider bool
if strings.HasPrefix(configType, types.TerraformComponentPrefix) {
isTerraformProvider = true
}
return config.DeleteApplication(ctx, u.KubeClient, name, isTerraformProvider)
}
// ApplicationDeployTarget is the struct of application deploy target
type ApplicationDeployTarget struct {
Namespace string `json:"namespace"`
Clusters []string `json:"clusters"`
}
// SyncConfigs will sync configs to working clusters
func SyncConfigs(ctx context.Context, k8sClient client.Client, project string, targets []*model.ClusterTarget) error {
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
// get all configs which can be synced to working clusters in the project
var secrets v1.SecretList
if err := k8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigSyncToMultiCluster: "true",
}); err != nil {
return err
}
if len(secrets.Items) == 0 {
return nil
}
var objects []map[string]string
for _, s := range secrets.Items {
if s.Labels[types.LabelConfigProject] == "" || s.Labels[types.LabelConfigProject] == project {
objects = append(objects, map[string]string{
"name": s.Name,
"resource": "secret",
})
// ListConfigs query the available configs.
// If the project is not empty, it means query all usable configs for this project.
func (u *configServiceImpl) ListConfigs(ctx context.Context, project string, template string, withProperties bool) ([]*apis.Config, error) {
var list []*apis.Config
scope := ""
var projectNamespace string
if project != "" {
scope = "project"
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return nil, err
}
projectNamespace = pro.GetNamespace()
// query the configs belong to the project scope from the system namespace
configs, err := u.Factory.ListConfigs(ctx, pro.GetNamespace(), template, "", true)
if err != nil {
return nil, err
}
for i := range configs {
list = append(list, convertConfig(project, *configs[i]))
}
}
if len(objects) == 0 {
klog.InfoS("no configs need to sync to working clusters", "project", project)
return nil
configs, err := u.Factory.ListConfigs(ctx, types.DefaultKubeVelaNS, template, scope, true)
if err != nil {
return nil, err
}
objectsBytes, err := json.Marshal(map[string][]map[string]string{"objects": objects})
for i := range configs {
if projectNamespace != "" {
if err := u.Factory.MergeDistributionStatus(ctx, configs[i], projectNamespace); err != nil && !errors.Is(err, config.ErrNotFoundDistribution) {
log.Logger.Warnf("fail to merge the status %s:%s", configs[i].Name, err.Error())
}
}
item := convertConfig(project, *configs[i])
item.Shared = true
if !withProperties {
item.Properties = nil
}
list = append(list, item)
}
return list, nil
}
// CreateConfigDistribution distribute the configs to the target namespaces.
func (u *configServiceImpl) CreateConfigDistribution(ctx context.Context, project string, req apis.CreateConfigDistributionRequest) error {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return err
}
var app = &v1beta1.Application{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
// config sync application doesn't exist, create one
clusterTargets := convertClusterTargets(targets)
if len(clusterTargets) == 0 {
errMsg := "no policy (no targets found) to sync configs"
klog.InfoS(errMsg, "project", project)
return errors.New(errMsg)
}
policies := make([]v1beta1.AppPolicy, len(clusterTargets))
for i, t := range clusterTargets {
properties, err := json.Marshal(t)
if err != nil {
return err
}
policies[i] = v1beta1.AppPolicy{
Type: "topology",
Name: t.Namespace,
Properties: &runtime.RawExtension{
Raw: properties,
},
}
}
scratch := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigProject: project,
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: name,
Type: "ref-objects",
Properties: &runtime.RawExtension{Raw: objectsBytes},
},
},
Policies: policies,
},
}
return k8sClient.Create(ctx, scratch)
if len(req.Configs) == 0 || len(req.Targets) == 0 {
return bcode.ErrNoConfigOrTarget
}
// config sync application exists, update it
app.Spec.Components = []common.ApplicationComponent{
{
Name: name,
Type: "ref-objects",
Properties: &runtime.RawExtension{Raw: objectsBytes},
},
}
currentTargets := make([]ApplicationDeployTarget, len(app.Spec.Policies))
for i, p := range app.Spec.Policies {
var t ApplicationDeployTarget
if err := json.Unmarshal(p.Properties.Raw, &t); err != nil {
return err
var targets []*config.ClusterTarget
for _, t := range req.Targets {
if t.Namespace != "" && t.ClusterName != "" {
targets = append(targets, &config.ClusterTarget{Namespace: t.Namespace, ClusterName: t.ClusterName})
}
currentTargets[i] = t
}
mergedTarget := mergeTargets(currentTargets, targets)
if len(mergedTarget) == 0 {
errMsg := "no policy (no targets found) to sync configs"
klog.InfoS(errMsg, "project", project)
return errors.New(errMsg)
var configs []*config.NamespacedName
for _, t := range req.Configs {
if t.Name != "" {
configs = append(configs, &config.NamespacedName{Namespace: t.Namespace, Name: t.Name})
}
}
mergedPolicies := make([]v1beta1.AppPolicy, len(mergedTarget))
for i, t := range mergedTarget {
properties, err := json.Marshal(t)
return u.Factory.CreateOrUpdateDistribution(ctx, pro.GetNamespace(), req.Name, &config.CreateDistributionSpec{
Configs: configs,
Targets: targets,
})
}
// ListDistributeConfigs list the all distributions
func (u *configServiceImpl) ListConfigDistributions(ctx context.Context, project string) ([]*config.Distribution, error) {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return nil, err
}
return u.Factory.ListDistributions(ctx, pro.GetNamespace())
}
// DeleteConfigDistribution delete a distribution
func (u *configServiceImpl) DeleteConfigDistribution(ctx context.Context, project, name string) error {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return err
}
if err := u.Factory.DeleteDistribution(ctx, pro.GetNamespace(), name); err != nil {
if errors.Is(err, config.ErrNotFoundDistribution) {
return bcode.ErrNotFoundDistribution
}
return err
}
return nil
}
func convertConfig(project string, config config.Config) *apis.Config {
return &apis.Config{
Template: config.Template.NamespacedName,
Sensitive: config.Template.Sensitive,
Name: config.Name,
Namespace: config.Namespace,
Project: project,
Alias: config.Alias,
Description: config.Description,
CreatedTime: &config.CreateTime,
Properties: config.Properties,
Secret: config.Secret,
Targets: config.Targets,
}
}
func (u *configServiceImpl) GetConfig(ctx context.Context, project, name string) (*apis.Config, error) {
ns := types.DefaultKubeVelaNS
if project != "" {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return nil, err
}
ns = pro.GetNamespace()
}
it, err := u.Factory.GetConfig(ctx, ns, name, true)
if err != nil {
if errors.Is(err, config.ErrSensitiveConfig) {
return nil, bcode.ErrSensitiveConfig
}
if errors.Is(err, config.ErrConfigNotFound) {
return nil, bcode.ErrConfigNotFound
}
return nil, err
}
return convertConfig(project, *it), nil
}
func (u *configServiceImpl) DeleteConfig(ctx context.Context, project, name string) error {
ns := types.DefaultKubeVelaNS
if project != "" {
pro, err := u.ProjectService.GetProject(ctx, project)
if err != nil {
return err
}
mergedPolicies[i] = v1beta1.AppPolicy{
Type: "topology",
Name: t.Namespace,
Properties: &runtime.RawExtension{
Raw: properties,
},
}
ns = pro.GetNamespace()
}
app.Spec.Policies = mergedPolicies
return k8sClient.Update(ctx, app)
}
func mergeTargets(currentTargets []ApplicationDeployTarget, targets []*model.ClusterTarget) []ApplicationDeployTarget {
var (
mergedTargets []ApplicationDeployTarget
// make sure the clusters of target with same namespace are merged
clusterTargets = convertClusterTargets(targets)
)
for _, c := range currentTargets {
var hasSameNamespace bool
for _, t := range clusterTargets {
if c.Namespace == t.Namespace {
hasSameNamespace = true
clusters := set.NewSetFromSlice(stringToInterfaceSlice(t.Clusters))
for _, cluster := range c.Clusters {
clusters.Add(cluster)
}
mergedTargets = append(mergedTargets, ApplicationDeployTarget{Namespace: c.Namespace,
Clusters: interfaceToStringSlice(clusters.ToSlice())})
}
}
if !hasSameNamespace {
mergedTargets = append(mergedTargets, c)
}
}
for _, t := range clusterTargets {
var hasSameNamespace bool
for _, c := range currentTargets {
if c.Namespace == t.Namespace {
hasSameNamespace = true
}
}
if !hasSameNamespace {
mergedTargets = append(mergedTargets, t)
}
}
return mergedTargets
}
func convertClusterTargets(targets []*model.ClusterTarget) []ApplicationDeployTarget {
type Target struct {
Namespace string `json:"namespace"`
Clusters []interface{} `json:"clusters"`
}
var (
clusterTargets []Target
namespaceSet = set.NewSet()
)
for i := 0; i < len(targets); i++ {
clusters := set.NewSet(targets[i].ClusterName)
for j := i + 1; j < len(targets); j++ {
if targets[i].Namespace == targets[j].Namespace {
clusters.Add(targets[j].ClusterName)
}
}
if namespaceSet.Contains(targets[i].Namespace) {
continue
}
clusterTargets = append(clusterTargets, Target{
Namespace: targets[i].Namespace,
Clusters: clusters.ToSlice(),
})
namespaceSet.Add(targets[i].Namespace)
}
t := make([]ApplicationDeployTarget, len(clusterTargets))
for i, ct := range clusterTargets {
t[i] = ApplicationDeployTarget{
Namespace: ct.Namespace,
Clusters: interfaceToStringSlice(ct.Clusters),
}
}
return t
}
func interfaceToStringSlice(i []interface{}) []string {
var s []string
for _, v := range i {
s = append(s, v.(string))
}
return s
}
func stringToInterfaceSlice(i []string) []interface{} {
var s []interface{}
for _, v := range i {
s = append(s, v)
}
return s
}
// destroySyncConfigsApp will delete the application which is used to sync configs
func destroySyncConfigsApp(ctx context.Context, k8sClient client.Client, project string) error {
name := fmt.Sprintf("%s-%s", configSyncProjectPrefix, project)
var app = &v1beta1.Application{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, app); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
klog.InfoS("config sync application doesn't exist, no need destroy", "application", name)
return nil
}
return k8sClient.Delete(ctx, app)
return u.Factory.DeleteConfig(ctx, ns, name)
}

View File

@ -18,755 +18,183 @@ package service
import (
"context"
"encoding/json"
"sort"
"strings"
"testing"
"time"
"errors"
. "github.com/agiledragon/gomonkey/v2"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"gotest.tools/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
apitypes "k8s.io/apimachinery/pkg/types"
"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/apiserver/domain/model"
apis "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/multicluster"
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
v1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
"github.com/oam-dev/kubevela/pkg/config"
"github.com/oam-dev/kubevela/pkg/cue/script"
)
func TestListConfigTypes(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
def1 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
definition.ConfigCatalog: types.VelaCoreConfig,
definition.DefinitionType: types.TerraformProvider,
},
},
}
def2 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def2",
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
definition.DefinitionAlias: "Def2",
},
Labels: map[string]string{
definition.ConfigCatalog: types.VelaCoreConfig,
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def1, def2).Build()
var alibabaTerraformTemplate = `
import "strings"
patches := ApplyFunc(multicluster.GetMulticlusterKubernetesClient, func() (client.Client, *rest.Config, error) {
return k8sClient, nil, nil
})
defer patches.Reset()
metadata: {
name: "terraform-provider-alibaba"
alias: "Alibaba Terraform Provider"
sensitive: true
scope: "system"
description: "Terraform Provider for Alibaba Cloud"
}
h := &configServiceImpl{
KubeClient: k8sClient,
}
type args struct {
h ConfigService
}
type want struct {
configTypes []*apis.ConfigType
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "success",
args: args{
h: h,
},
want: want{
configTypes: []*apis.ConfigType{
{
Name: "def2",
Alias: "Def2",
Definitions: []string{"def2"},
},
{
Alias: "Terraform Cloud Provider",
Name: types.TerraformProvider,
Definitions: []string{
"def1",
},
},
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.ListConfigTypes(ctx, "")
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
template: {
outputs: {
"provider": {
apiVersion: "terraform.core.oam.dev/v1beta1"
kind: "Provider"
metadata: {
name: parameter.name
namespace: context.namespace
labels: l
}
assert.DeepEqual(t, got, tc.want.configTypes)
})
}
}
func TestGetConfigType(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
def2 := &v1beta1.ComponentDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "ComponentDefinition",
APIVersion: "core.oam.dev/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "def2",
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
definition.DefinitionAlias: "Def2",
},
Labels: map[string]string{
definition.UserPrefix + definition.ConfigCatalog: types.VelaCoreConfig,
},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(def2).Build()
patches := ApplyFunc(multicluster.GetMulticlusterKubernetesClient, func() (client.Client, *rest.Config, error) {
return k8sClient, nil, nil
})
defer patches.Reset()
h := &configServiceImpl{KubeClient: k8sClient}
type args struct {
h ConfigService
name string
}
type want struct {
configType *apis.ConfigType
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "success",
args: args{
h: h,
name: "def2",
},
want: want{
configType: &apis.ConfigType{
Alias: "Def2",
Name: "def2",
},
},
},
{
name: "error",
args: args{
h: h,
name: "def99",
},
want: want{
errMsg: "failed to get config type",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.GetConfigType(ctx, tc.args.name)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configType)
})
}
}
func TestCreateConfig(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
k8sClient := fake.NewClientBuilder().WithScheme(s).Build()
h := &configServiceImpl{KubeClient: k8sClient}
type args struct {
h ConfigService
req apis.CreateConfigRequest
}
type want struct {
errMsg string
}
ctx := context.Background()
properties, err := json.Marshal(map[string]interface{}{
"name": "default",
})
assert.NilError(t, err)
testcases := []struct {
name string
args args
want want
}{
{
name: "delete config when it's not ready",
args: args{
h: h,
req: apis.CreateConfigRequest{
Name: "a",
ComponentType: "b",
Project: "c",
},
},
},
{
name: "create terraform-alibaba config",
args: args{
h: h,
req: apis.CreateConfigRequest{
Name: "n1",
ComponentType: "terraform-alibaba",
Project: "p1",
Properties: string(properties),
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := tc.args.h.CreateConfig(ctx, tc.args.req)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
})
}
}
func TestGetConfigs(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
createdTime, _ := time.Parse(time.UnixDate, "Wed Apr 7 11:06:39 PST 2022")
provider1 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider1",
Namespace: "default",
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: terraformapi.ProviderStatus{
State: terraformtypes.ProviderIsReady,
},
}
provider2 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider2",
Namespace: "default",
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: terraformapi.ProviderStatus{
State: terraformtypes.ProviderIsNotReady,
},
}
provider3 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "provider3",
Namespace: "default",
},
}
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "provider3",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigType: "terraform-alibaba",
},
CreationTimestamp: metav1.NewTime(createdTime),
},
Status: common.AppStatus{
Phase: common.ApplicationRendering,
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(provider1, provider2, provider3, app1).Build()
h := &configServiceImpl{KubeClient: k8sClient}
type args struct {
configType string
h ConfigService
}
type want struct {
configs []*apis.Config
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "success",
args: args{
configType: types.TerraformProvider,
h: h,
},
want: want{
configs: []*apis.Config{
{
Name: "provider1",
CreatedTime: &createdTime,
Status: "Ready",
},
{
Name: "provider2",
CreatedTime: &createdTime,
Status: "Not ready",
},
{
Name: "provider3",
CreatedTime: &createdTime,
Status: "Not ready",
ConfigType: "terraform-alibaba",
ApplicationStatus: common.ApplicationRendering,
},
},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.args.h.GetConfigs(ctx, tc.args.configType)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
assert.DeepEqual(t, got, tc.want.configs)
})
}
}
func TestMergeTargets(t *testing.T) {
currentTargets := []ApplicationDeployTarget{
{
Namespace: "n1",
Clusters: []string{"c1", "c2"},
}, {
Namespace: "n2",
Clusters: []string{"c3"},
},
}
targets := []*model.ClusterTarget{
{
Namespace: "n3",
ClusterName: "c4",
}, {
Namespace: "n1",
ClusterName: "c5",
},
{
Namespace: "n2",
ClusterName: "c3",
},
}
expected := []ApplicationDeployTarget{
{
Namespace: "n1",
Clusters: []string{"c1", "c2", "c5"},
}, {
Namespace: "n2",
Clusters: []string{"c3"},
}, {
Namespace: "n3",
Clusters: []string{"c4"},
},
}
got := mergeTargets(currentTargets, targets)
for i, g := range got {
clusters := g.Clusters
sort.SliceStable(clusters, func(i, j int) bool {
return clusters[i] < clusters[j]
})
got[i].Clusters = clusters
}
assert.DeepEqual(t, expected, got)
}
func TestConvert(t *testing.T) {
targets := []*model.ClusterTarget{
{
Namespace: "n3",
ClusterName: "c4",
}, {
Namespace: "n1",
ClusterName: "c5",
},
{
Namespace: "n2",
ClusterName: "c3",
},
{
Namespace: "n3",
ClusterName: "c5",
},
}
expected := []ApplicationDeployTarget{
{
Namespace: "n3",
Clusters: []string{"c4", "c5"},
},
{
Namespace: "n1",
Clusters: []string{"c5"},
}, {
Namespace: "n2",
Clusters: []string{"c3"},
},
}
got := convertClusterTargets(targets)
for i, g := range got {
clusters := g.Clusters
sort.SliceStable(clusters, func(i, j int) bool {
return clusters[i] < clusters[j]
})
got[i].Clusters = clusters
}
assert.DeepEqual(t, expected, got)
}
func TestDestroySyncConfigsApp(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-p1",
Namespace: types.DefaultKubeVelaNS,
},
}
k8sClient1 := fake.NewClientBuilder().WithScheme(s).WithObjects(app1).Build()
k8sClient2 := fake.NewClientBuilder().Build()
type args struct {
project string
k8sClient client.Client
}
type want struct {
errMsg string
}
ctx := context.Background()
testcases := map[string]struct {
args args
want want
}{
"found": {
args: args{
project: "p1",
k8sClient: k8sClient1,
},
},
"not found": {
args: args{
project: "p1",
k8sClient: k8sClient2,
},
want: want{
errMsg: "no kind is registered for the type v1beta1.Application",
},
},
}
for name, tc := range testcases {
t.Run(name, func(t *testing.T) {
err := destroySyncConfigsApp(ctx, tc.args.k8sClient, tc.args.project)
if err != nil || tc.want.errMsg != "" {
if !strings.Contains(err.Error(), tc.want.errMsg) {
assert.ErrorContains(t, err, tc.want.errMsg)
spec: {
provider: "alibaba"
region: parameter.ALICLOUD_REGION
credentials: {
source: "Secret"
secretRef: {
namespace: "vela-system"
name: context.name
key: "credentials"
}
}
}
})
}
}
output: {
apiVersion: "v1"
kind: "Secret"
metadata: {
name: context.name
namespace: context.namespace
}
type: "Opaque"
stringData: credentials: strings.Join([creds1, creds2], "\n")
}
creds1: "accessKeyID: " + parameter.ALICLOUD_ACCESS_KEY
creds2: "accessKeySecret: " + parameter.ALICLOUD_SECRET_KEY
l: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "terraform-provider"
"config.oam.dev/provider": "terraform-alibaba"
}
parameter: {
//+usage=The name of Terraform Provider for Alibaba Cloud
name: *"default" | string
//+usage=Get ALICLOUD_ACCESS_KEY per this guide https://help.aliyun.com/knowledge_detail/38738.html
ALICLOUD_ACCESS_KEY: string
//+usage=Get ALICLOUD_SECRET_KEY per this guide https://help.aliyun.com/knowledge_detail/38738.html
ALICLOUD_SECRET_KEY: string
//+usage=Get ALICLOUD_REGION by picking one RegionId from Alibaba Cloud region list https://www.alibabacloud.com/help/doc-detail/72379.htm
ALICLOUD_REGION: string
}
}
`
func TestSyncConfigs(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
secret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigProject: "p1",
types.LabelConfigSyncToMultiCluster: "true",
var _ = Describe("Test config service", func() {
var factory config.Factory
var ds datastore.DataStore
var configService *configServiceImpl
var projectService ProjectService
BeforeEach(func() {
factory = config.NewConfigFactory(k8sClient)
Expect(factory).ToNot(BeNil())
var err error
ds, err = NewDatastore(datastore.Config{Type: "kubeapi", Database: "config-test-kubevela"})
Expect(ds).ToNot(BeNil())
Expect(err).Should(BeNil())
projectService = NewTestProjectService(ds, k8sClient)
configService = &configServiceImpl{
KubeClient: k8sClient,
ProjectService: projectService,
Factory: factory,
}
})
It("Test apply terraform template", func() {
tem, err := factory.ParseTemplate("alibaba-provider", []byte(alibabaTerraformTemplate))
Expect(err).To(BeNil())
Expect(factory.CreateOrUpdateConfigTemplate(context.Background(), types.DefaultKubeVelaNS, tem)).To(BeNil())
})
It("Test detail the template", func() {
detail, err := configService.GetTemplate(context.TODO(), config.NamespacedName{Name: "alibaba-provider"})
Expect(err).To(BeNil())
Expect(len(detail.UISchema)).To(Equal(4))
})
It("Test apply a new config", func() {
_, err := configService.CreateConfig(context.TODO(), "", v1.CreateConfigRequest{
Name: "alibaba-test-error-properties",
Alias: "Alibaba Cloud",
Description: "This is a terraform provider",
Template: v1.NamespacedName{
Name: "alibaba-provider",
},
},
}
policies := []ApplicationDeployTarget{{
Namespace: "n9",
Clusters: []string{"c19"},
}}
properties, _ := json.Marshal(policies)
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-p2",
Namespace: types.DefaultKubeVelaNS,
},
Spec: v1beta1.ApplicationSpec{
Policies: []v1beta1.AppPolicy{{
Name: "c19",
Type: "topology",
Properties: &runtime.RawExtension{Raw: properties},
}},
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(secret1, app1).Build()
type args struct {
project string
targets []*model.ClusterTarget
}
type want struct {
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "create",
args: args{
project: "p1",
targets: []*model.ClusterTarget{{
ClusterName: "c1",
Namespace: "n1",
}},
},
},
{
name: "update",
args: args{
project: "p2",
targets: []*model.ClusterTarget{{
ClusterName: "c1",
Namespace: "n1",
}},
},
},
{
name: "skip config sync",
args: args{
project: "p3",
targets: []*model.ClusterTarget{{
ClusterName: "c1",
Namespace: "n1",
}},
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := SyncConfigs(ctx, k8sClient, tc.args.project, tc.args.targets)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
Properties: `{}`,
})
}
}
Expect(err).ToNot(BeNil())
var paramErr = &script.ParameterError{}
Expect(errors.As(err, &paramErr)).To(Equal(true))
Expect(paramErr.Name).To(Equal("ALICLOUD_ACCESS_KEY"))
Expect(paramErr.Message).To(Equal("This parameter is required"))
func TestDeleteConfig(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
provider1 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "p1",
Namespace: "default",
},
}
provider2 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "p2",
Namespace: "default",
},
}
provider3 := &terraformapi.Provider{
ObjectMeta: metav1.ObjectMeta{
Name: "p3",
Namespace: "default",
},
}
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-terraform-provider-p1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigType: "terraform-alibaba",
config, err := configService.CreateConfig(context.TODO(), "", v1.CreateConfigRequest{
Name: "alibaba-test",
Alias: "Alibaba Cloud",
Description: "This is a terraform provider",
Template: v1.NamespacedName{
Name: "alibaba-provider",
},
},
}
app2 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "p2",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigType: "terraform-alibaba",
},
},
}
normalApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "a9",
Namespace: types.DefaultKubeVelaNS,
},
}
k8sClient := fake.NewClientBuilder().WithScheme(s).WithObjects(provider1, provider2, provider3, app1, app2, normalApp).Build()
h := &configServiceImpl{KubeClient: k8sClient}
type args struct {
configType string
name string
h ConfigService
}
type want struct {
errMsg string
}
ctx := context.Background()
testcases := []struct {
name string
args args
want want
}{
{
name: "delete a legacy terraform provider",
args: args{
configType: "terraform-alibaba",
name: "p1",
h: h,
},
want: want{},
},
{
name: "delete a terraform provider",
args: args{
configType: "terraform-alibaba",
name: "p2",
h: h,
},
want: want{},
},
{
name: "delete a terraform provider, but its application not found",
args: args{
configType: "terraform-alibaba",
name: "p3",
h: h,
},
want: want{
errMsg: "could not be disabled because it was created by enabling a Terraform provider or was manually created",
},
},
{
name: "delete a normal config, but failed",
args: args{
configType: "config-image-registry",
name: "a10",
h: h,
},
want: want{
errMsg: "not found",
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
err := tc.args.h.DeleteConfig(ctx, tc.args.configType, tc.args.name)
if tc.want.errMsg != "" || err != nil {
assert.ErrorContains(t, err, tc.want.errMsg)
}
Properties: `{"ALICLOUD_ACCESS_KEY":"test", "ALICLOUD_SECRET_KEY": "test", "ALICLOUD_REGION": "test", "name": "test"}`,
})
}
}
Expect(err).To(BeNil())
Expect(config.Sensitive).To(Equal(true))
Expect(config.Description).To(Equal("This is a terraform provider"))
var provider terraformapi.Provider
Expect(k8sClient.Get(context.TODO(), apitypes.NamespacedName{
Namespace: types.DefaultKubeVelaNS,
Name: "test",
}, &provider)).To(BeNil())
})
It("Test list configs", func() {
list, err := configService.ListConfigs(context.TODO(), "", "alibaba-provider", false)
Expect(err).To(BeNil())
Expect(len(list)).To(Equal(1))
_, err = projectService.CreateProject(context.TODO(), v1.CreateProjectRequest{Name: "mysql-project"})
Expect(err).To(BeNil())
// can not share the config that is the system scope
list, err = configService.ListConfigs(context.TODO(), "mysql-project", "alibaba-provider", false)
Expect(err).To(BeNil())
Expect(len(list)).To(Equal(0))
list, err = configService.ListConfigs(context.TODO(), "", "not-found", false)
Expect(err).To(BeNil())
Expect(len(list)).To(Equal(0))
})
It("Test detail a config", func() {
_, err := configService.GetConfig(context.TODO(), "", "alibaba-test")
Expect(err).To(Equal(bcode.ErrSensitiveConfig))
})
It("Test delete a config", func() {
Expect(configService.DeleteConfig(context.TODO(), "", "alibaba-test")).To(BeNil())
var list terraformapi.ProviderList
Expect(k8sClient.List(context.TODO(), &list)).To(BeNil())
Expect(len(list.Items)).To(Equal(0))
})
})

View File

@ -258,15 +258,15 @@ func (d *definitionServiceImpl) DetailDefinition(ctx context.Context, name, defT
// render default ui schema
defaultUISchema := renderDefaultUISchema(schema)
// patch from custom ui schema
definition.UISchema = d.renderCustomUISchema(ctx, name, defType, defaultUISchema)
definition.UISchema = renderCustomUISchema(ctx, d.KubeClient, name, defType, defaultUISchema)
}
return definition, nil
}
func (d *definitionServiceImpl) renderCustomUISchema(ctx context.Context, name, defType string, defaultSchema []*utils.UIParameter) []*utils.UIParameter {
func renderCustomUISchema(ctx context.Context, cli client.Client, name, defType string, defaultSchema []*utils.UIParameter) []*utils.UIParameter {
var cm v1.ConfigMap
if err := d.KubeClient.Get(ctx, k8stypes.NamespacedName{
if err := cli.Get(ctx, k8stypes.NamespacedName{
Namespace: types.DefaultKubeVelaNS,
Name: fmt.Sprintf("%s-uischema-%s", defType, name),
}, &cm); err != nil {

View File

@ -20,18 +20,14 @@ import (
"context"
"strconv"
"github.com/oam-dev/kubevela/pkg/utils/config"
"github.com/oam-dev/kubevela/apis/types"
v1 "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/helm"
corev1 "k8s.io/api/core/v1"
types2 "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
@ -54,8 +50,9 @@ type HelmService interface {
}
type defaultHelmImpl struct {
helper *helm.Helper
K8sClient client.Client `inject:"kubeClient"`
helper *helm.Helper
K8sClient client.Client `inject:"kubeClient"`
ConfigService ConfigService `inject:""`
}
func (d defaultHelmImpl) ListChartNames(ctx context.Context, repoURL string, secretName string, skipCache bool) ([]string, error) {
@ -126,21 +123,20 @@ func (d defaultHelmImpl) GetChartValues(ctx context.Context, repoURL string, cha
func (d defaultHelmImpl) ListChartRepo(ctx context.Context, projectName string) (*v1.ChartRepoResponseList, error) {
var res []*v1.ChartRepoResponse
var err error
projectSecrets := corev1.SecretList{}
opts := []client.ListOption{
client.MatchingLabels{oam.LabelConfigType: "config-helm-repository"},
client.InNamespace(types.DefaultKubeVelaNS),
}
err = d.K8sClient.List(ctx, &projectSecrets, opts...)
configs, err := d.ConfigService.ListConfigs(ctx, projectName, types.HelmRepository, true)
if err != nil {
return nil, err
}
for _, item := range projectSecrets.Items {
if config.ProjectMatched(item.DeepCopy(), projectName) {
res = append(res, &v1.ChartRepoResponse{URL: string(item.Data["url"]), SecretName: item.Name})
for _, item := range configs {
if item.Properties != nil {
url, ok := item.Properties["url"].(string)
if ok {
res = append(res, &v1.ChartRepoResponse{URL: url, SecretName: item.Name})
}
}
// Compatible with historical data
if url, ok := item.Secret.Data["url"]; ok {
res = append(res, &v1.ChartRepoResponse{URL: string(url), SecretName: item.Name})
}
}

View File

@ -28,7 +28,11 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
apis "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/config"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/apply"
"github.com/oam-dev/kubevela/pkg/utils/helm"
v1 "k8s.io/api/core/v1"
@ -53,11 +57,22 @@ func TestFlattenKeyFunc(t *testing.T) {
}
// NewTestHelmService new helm service for test
func NewTestHelmService() HelmService {
func NewTestHelmService() (*defaultHelmImpl, ProjectService, error) {
ds, err := NewDatastore(datastore.Config{Type: "kubeapi", Database: "helm-test-kubevela"})
if err != nil {
return nil, nil, err
}
projectService := NewTestProjectService(ds, k8sClient)
return &defaultHelmImpl{
helper: helm.NewHelperWithCache(),
K8sClient: k8sClient,
}
ConfigService: &configServiceImpl{
KubeClient: k8sClient,
ProjectService: projectService,
Factory: config.NewConfigFactory(k8sClient),
Apply: apply.NewAPIApplicator(k8sClient),
},
}, projectService, nil
}
var _ = Describe("Test helm repo list", func() {
@ -68,6 +83,7 @@ var _ = Describe("Test helm repo list", func() {
pSec = v1.Secret{}
gSec = v1.Secret{}
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "vela-system"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(k8sClient.Create(ctx, &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "project-my-project"}})).Should(SatisfyAny(BeNil(), util.AlreadyExistMatcher{}))
Expect(yaml.Unmarshal([]byte(projectSecret), &pSec)).Should(BeNil())
Expect(yaml.Unmarshal([]byte(globalSecret), &gSec)).Should(BeNil())
Expect(k8sClient.Create(ctx, &pSec)).Should(BeNil())
@ -80,7 +96,14 @@ var _ = Describe("Test helm repo list", func() {
})
It("Test list with project ", func() {
u := NewTestHelmService()
u, p, err := NewTestHelmService()
Expect(err).Should(BeNil())
_, err = p.CreateProject(context.TODO(), apis.CreateProjectRequest{
Name: "my-project",
})
Expect(err).Should(BeNil())
list, err := u.ListChartRepo(ctx, "my-project")
Expect(err).Should(BeNil())
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(2))
@ -99,7 +122,13 @@ var _ = Describe("Test helm repo list", func() {
})
It("Test list func with not exist project", func() {
u := NewTestHelmService()
u, p, err := NewTestHelmService()
Expect(err).Should(BeNil())
_, err = p.CreateProject(context.TODO(), apis.CreateProjectRequest{
Name: "not-exist-project",
})
Expect(err).Should(BeNil())
list, err := u.ListChartRepo(ctx, "not-exist-project")
Expect(err).Should(BeNil())
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(1))
@ -108,7 +137,8 @@ var _ = Describe("Test helm repo list", func() {
})
It("Test list func without project", func() {
u := NewTestHelmService()
u, _, err := NewTestHelmService()
Expect(err).Should(BeNil())
list, err := u.ListChartRepo(ctx, "")
Expect(err).Should(BeNil())
Expect(len(list.ChartRepoResponse)).Should(BeEquivalentTo(1))
@ -171,7 +201,8 @@ var _ = Describe("test helm usecasae", func() {
defer mockServer.Close()
u := NewTestHelmService()
u, _, err := NewTestHelmService()
Expect(err).Should(BeNil())
charts, err := u.ListChartNames(ctx, mockServer.URL, "repo-secret", false)
Expect(err).Should(BeNil())
Expect(len(charts)).Should(BeEquivalentTo(1))
@ -189,8 +220,9 @@ var _ = Describe("test helm usecasae", func() {
})
It("coverage not secret notExist error", func() {
u := NewTestHelmService()
_, err := u.ListChartNames(ctx, "http://127.0.0.1:8080", "repo-secret-notExist", false)
u, _, err := NewTestHelmService()
Expect(err).Should(BeNil())
_, err = u.ListChartNames(ctx, "http://127.0.0.1:8080", "repo-secret-notExist", false)
Expect(err).ShouldNot(BeNil())
_, err = u.ListChartVersions(ctx, "http://127.0.0.1:8080", "mysql", "repo-secret-notExist", false)
@ -345,11 +377,12 @@ stringData:
url: https://charts.bitnami.com/bitnami
kind: Secret
metadata:
labels:
config.oam.dev/type: config-helm-repository
config.oam.dev/project: ""
name: global-helm-repo
namespace: vela-system
labels:
config.oam.dev/type: helm-repository
config.oam.dev/scope: project
config.oam.dev/catalog: velacore-config
type: Opaque
`
projectSecret = `
@ -357,10 +390,11 @@ apiVersion: v1
kind: Secret
metadata:
name: project-helm-repo
namespace: vela-system
namespace: project-my-project
labels:
config.oam.dev/type: config-helm-repository
config.oam.dev/project: my-project
config.oam.dev/type: helm-repository
config.oam.dev/catalog: velacore-config
config.oam.dev/scope: project
stringData:
url: https://kedacore.github.io/charts
type: Opaque
@ -372,8 +406,9 @@ metadata:
name: repo-secret
namespace: vela-system
labels:
config.oam.dev/type: config-helm-repository
config.oam.dev/type: helm-repository
config.oam.dev/project: my-project-2
config.oam.dev/catalog: velacore-config
stringData:
username: admin
password: admin

View File

@ -55,27 +55,28 @@ type ImageService interface {
}
type imageImpl struct {
K8sClient client.Client `inject:"kubeClient"`
K8sClient client.Client `inject:"kubeClient"`
ConfigService ConfigService `inject:""`
}
// ListImageRepos list the image repositories via user configuration
func (i *imageImpl) ListImageRepos(ctx context.Context, project string) ([]v1.ImageRegistry, error) {
var secrets corev1.SecretList
if err := i.K8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: types.ImageRegistry,
}); err != nil {
configs, err := i.ConfigService.ListConfigs(ctx, project, types.ImageRegistry, true)
if err != nil {
return nil, err
}
var repos []v1.ImageRegistry
for _, secret := range secrets.Items {
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
repos = append(repos, v1.ImageRegistry{
Name: secret.Name,
SecretName: secret.Name,
Domain: secret.Labels[types.LabelConfigIdentifier],
})
for _, item := range configs {
if item.Properties != nil {
registry, ok := item.Properties["registry"].(string)
if ok {
repos = append(repos, v1.ImageRegistry{
Name: item.Name,
SecretName: item.Name,
Domain: registry,
Secret: item.Secret,
})
}
}
}
return repos, nil
@ -88,56 +89,49 @@ func (i *imageImpl) GetImageInfo(ctx context.Context, project, secretName, image
}
ref, err := name.ParseReference(imageName)
if err != nil {
imageInfo.Message = "The image name is invalid"
imageInfo.Message = "The image name is invalid."
return imageInfo
}
registryDomain := ref.Context().RegistryStr()
imageInfo.Registry = registryDomain
var secrets corev1.SecretList
if err := i.K8sClient.List(ctx, &secrets, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: types.ImageRegistry,
types.LabelConfigIdentifier: registryDomain,
}); err != nil {
log.Logger.Warnf("fail to list the docker registries, %s", err.Error())
registries, err := i.ListImageRepos(ctx, project)
if err != nil {
log.Logger.Warnf("fail to list the image registries:%s", err.Error())
imageInfo.Message = "There is no registry."
return imageInfo
}
var selectSecret []*corev1.Secret
var selectSecretNames []string
var selectRegistry []v1.ImageRegistry
var selectRegistryNames []string
// get info with specified secret
if secretName != "" {
for i, secret := range secrets.Items {
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
if secretName == secret.Name {
selectSecret = append(selectSecret, &secrets.Items[i])
if secret.Type == corev1.SecretTypeDockerConfigJson {
selectSecretNames = append(selectSecretNames, secret.Name)
}
break
}
for i, registry := range registries {
if secretName == registry.SecretName {
selectRegistry = append(selectRegistry, registries[i])
selectRegistryNames = append(selectRegistryNames, registry.Name)
break
}
}
}
// get info with the secret which match the registry domain
if selectSecret == nil {
for i, secret := range secrets.Items {
if secret.Labels[types.LabelConfigProject] == "" || secret.Labels[types.LabelConfigProject] == project {
if secret.Labels[types.LabelConfigIdentifier] == registryDomain {
selectSecret = append(selectSecret, &secrets.Items[i])
if secret.Type == corev1.SecretTypeDockerConfigJson {
selectSecretNames = append(selectSecretNames, secret.Name)
}
}
if selectRegistry == nil {
for i, registry := range registries {
if registry.Domain == registryDomain {
selectRegistry = append(selectRegistry, registries[i])
selectRegistryNames = append(selectRegistryNames, registry.Name)
}
}
}
var username, password string
var insecure = false
var useHTTP = false
imageInfo.SecretNames = selectSecretNames
if len(selectSecret) > 0 {
insecure, useHTTP, username, password = getAccountFromSecret(*selectSecret[0], registryDomain)
imageInfo.SecretNames = selectRegistryNames
for _, registry := range selectRegistry {
if registry.Secret != nil {
insecure, useHTTP, username, password = getAccountFromSecret(*registry.Secret, registryDomain)
break
}
}
err = getImageInfo(imageName, insecure, useHTTP, username, password, &imageInfo)
if err != nil {

View File

@ -20,16 +20,10 @@ import (
"context"
"errors"
"fmt"
"strings"
terraformtypes "github.com/oam-dev/terraform-controller/api/types"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"k8s.io/apimachinery/pkg/api/meta"
"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/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
@ -53,7 +47,7 @@ type ProjectService interface {
DeleteProjectUser(ctx context.Context, projectName string, userName string) error
UpdateProjectUser(ctx context.Context, projectName string, userName string, req apisv1.UpdateProjectUserRequest) (*apisv1.ProjectUserBase, error)
Init(ctx context.Context) error
GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error)
ListTerraformProviders(ctx context.Context, projectName string) ([]*apisv1.TerraformProvider, error)
}
type projectServiceImpl struct {
@ -297,8 +291,7 @@ func (p *projectServiceImpl) DeleteProject(ctx context.Context, name string) err
if err := p.Store.Delete(ctx, &model.Project{Name: name}); err != nil {
return err
}
// delete config-sync application
return destroySyncConfigsApp(ctx, p.K8sClient, name)
return nil
}
// CreateProject create project
@ -507,95 +500,21 @@ func (p *projectServiceImpl) UpdateProjectUser(ctx context.Context, projectName
return ConvertProjectUserModel2Base(&projectUser, user), nil
}
func (p *projectServiceImpl) GetConfigs(ctx context.Context, projectName, configType string) ([]*apisv1.Config, error) {
var (
configs []*apisv1.Config
legacyTerraformProviders []*apisv1.Config
apps = &v1beta1.ApplicationList{}
)
if err := p.K8sClient.List(ctx, apps, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: types.VelaCoreConfig,
}); err != nil {
func (p *projectServiceImpl) ListTerraformProviders(ctx context.Context, projectName string) ([]*apisv1.TerraformProvider, error) {
l := &terraformapi.ProviderList{}
if err := p.K8sClient.List(ctx, l, client.InNamespace(types.ProviderNamespace)); err != nil {
return nil, err
}
if configType == types.TerraformProvider || configType == "" {
// legacy providers
var providers = &terraformapi.ProviderList{}
if err := p.K8sClient.List(ctx, providers, client.InNamespace(types.DefaultAppNamespace)); err != nil {
// this logic depends on the terraform addon, ignore the no matches kind error before the terraform addon is installed.
if !meta.IsNoMatchError(err) {
return nil, err
}
log.Logger.Infof("terraform Provider CRD is not installed")
}
for _, p := range providers.Items {
if p.Labels[types.LabelConfigCatalog] == types.VelaCoreConfig {
continue
}
t := p.CreationTimestamp.Time
var status = configIsNotReady
if p.Status.State == terraformtypes.ProviderIsReady {
status = configIsReady
}
legacyTerraformProviders = append(legacyTerraformProviders, &apisv1.Config{
Name: p.Name,
CreatedTime: &t,
Status: status,
})
}
var res []*apisv1.TerraformProvider
for _, provider := range l.Items {
res = append(res, &apisv1.TerraformProvider{
Name: provider.Name,
Region: provider.Spec.Region,
Provider: provider.Spec.Provider,
CreateTime: provider.CreationTimestamp.Time,
})
}
switch configType {
case types.TerraformProvider:
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if a.Status.Phase != common.ApplicationRunning || (appProject != "" && appProject != projectName) ||
!strings.Contains(a.Labels[types.LabelConfigType], types.TerraformComponentPrefix) {
continue
}
configs = append(configs, retrieveConfigFromApplication(a, appProject))
}
configs = append(configs, legacyTerraformProviders...)
case "":
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if appProject != "" && appProject != projectName {
continue
}
configs = append(configs, retrieveConfigFromApplication(a, appProject))
}
configs = append(configs, legacyTerraformProviders...)
case types.DexConnector, types.HelmRepository, types.ImageRegistry:
t := strings.ReplaceAll(configType, "config-", "")
for _, a := range apps.Items {
appProject := a.Labels[types.LabelConfigProject]
if a.Status.Phase != common.ApplicationRunning || (appProject != "" && appProject != projectName) {
continue
}
if a.Labels[types.LabelConfigType] == t {
configs = append(configs, retrieveConfigFromApplication(a, appProject))
}
}
default:
return nil, errors.New("unsupported config type")
}
for i, c := range configs {
if c.ConfigType != "" {
d := &v1beta1.ComponentDefinition{}
err := p.K8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: c.ConfigType}, d)
if err != nil {
klog.InfoS("failed to get component definition", "ComponentDefinition", configType, "err", err)
} else {
configs[i].ConfigTypeAlias = DefinitionAlias(d.Annotations)
}
}
}
return configs, nil
return res, nil
}
// ConvertProjectModel2Base convert project model to base struct
@ -628,28 +547,6 @@ func ConvertProjectUserModel2Base(user *model.ProjectUser, userModel *model.User
return base
}
func retrieveConfigFromApplication(a v1beta1.Application, project string) *apisv1.Config {
var (
applicationStatus = a.Status.Phase
status string
)
if applicationStatus == common.ApplicationRunning {
status = configIsReady
} else {
status = configIsNotReady
}
return &apisv1.Config{
ConfigType: a.Labels[types.LabelConfigType],
Name: a.Name,
Project: project,
CreatedTime: &(a.CreationTimestamp.Time),
ApplicationStatus: applicationStatus,
Status: status,
Alias: a.Annotations[types.AnnotationConfigAlias],
Description: a.Annotations[types.AnnotationConfigDescription],
}
}
// NewTestProjectService create the project service instance for testing
func NewTestProjectService(ds datastore.DataStore, c client.Client) ProjectService {
targetImpl := &targetServiceImpl{K8sClient: c, Store: ds}

View File

@ -24,11 +24,8 @@ import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
velatypes "github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
"github.com/oam-dev/kubevela/pkg/apiserver/infrastructure/datastore"
@ -158,18 +155,6 @@ var _ = Describe("Test project service functions", func() {
Name: "test-project",
Description: "this is a project description",
}
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-test-project",
Namespace: "vela-system",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Type: "aaa",
}},
},
}
Expect(k8sClient.Create(context.TODO(), app1)).Should(BeNil())
_, err := projectService.CreateProject(context.TODO(), req)
Expect(err).Should(BeNil())
@ -264,19 +249,6 @@ var _ = Describe("Test project service functions", func() {
Name: "test-project",
Description: "this is a project description",
}
app1 := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "config-sync-test-project",
Namespace: "vela-system",
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{{
Type: "aaa",
}},
},
}
Expect(k8sClient.Create(context.TODO(), app1)).Should(BeNil())
_, err := projectService.CreateProject(context.TODO(), req)
Expect(err).Should(BeNil())

View File

@ -53,6 +53,7 @@ var defaultProjectPermissionTemplate = []*model.PermissionTemplate{
Resources: []string{
"project:{projectName}",
"project:{projectName}/config:*",
"project:{projectName}/provider:*",
"project:{projectName}/role:*",
"project:{projectName}/projectUser:*",
"project:{projectName}/permission:*",
@ -90,7 +91,7 @@ var defaultProjectPermissionTemplate = []*model.PermissionTemplate{
{
Name: "configuration-read",
Alias: "Environment Management",
Resources: []string{"project:{projectName}/config:*"},
Resources: []string{"project:{projectName}/config:*", "project:{projectName}/provider:*"},
Actions: []string{"list", "detail"},
Effect: "Allow",
Scope: "project",
@ -163,9 +164,9 @@ var defaultPlatformPermission = []*model.PermissionTemplate{
Scope: "platform",
},
{
Name: "integration-management",
Alias: "Integration Management",
Resources: []string{"configType:*/*"},
Name: "config-management",
Alias: "Config Management",
Resources: []string{"config:*/*"},
Actions: []string{"*"},
Effect: "Allow",
Scope: "platform",
@ -229,7 +230,10 @@ var ResourceMaps = map[string]resourceMetadata{
pathName: "userName",
},
"applicationTemplate": {},
"config": {},
"config": {
pathName: "configName",
},
"provider": {},
},
pathName: "projectName",
},
@ -267,7 +271,9 @@ var ResourceMaps = map[string]resourceMetadata{
},
},
},
"cloudshell": {},
"cloudshell": {},
"config": {},
"configTemplate": {},
}
var existResourcePaths = convert(ResourceMaps)

View File

@ -63,7 +63,7 @@ var _ = BeforeSuite(func(done Done) {
ControlPlaneStartTimeout: time.Minute * 3,
ControlPlaneStopTimeout: time.Minute,
UseExistingCluster: pointer.BoolPtr(false),
CRDDirectoryPaths: []string{"../../../../charts/vela-core/crds"},
CRDDirectoryPaths: []string{"../../../../charts/vela-core/crds", "./testdata/crds"},
}
By("start kube test env")

View File

@ -18,6 +18,7 @@ package service
import (
"context"
"fmt"
"time"
"k8s.io/apimachinery/pkg/util/rand"
@ -138,7 +139,7 @@ func (u systemInfoServiceImpl) UpdateSystemInfo(ctx context.Context, sysInfo v1.
VelaAddress: sysInfo.VelaAddress,
Connectors: connectors,
}); err != nil {
return nil, err
return nil, fmt.Errorf("fail to generate the dex config: %w", err)
}
}
err = u.Store.Put(ctx, &modifiedInfo)

View File

@ -0,0 +1,105 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.6.0
creationTimestamp: null
name: providers.terraform.core.oam.dev
spec:
group: terraform.core.oam.dev
names:
kind: Provider
listKind: ProviderList
plural: providers
singular: provider
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.state
name: STATE
type: string
- jsonPath: .metadata.creationTimestamp
name: AGE
type: date
name: v1beta1
schema:
openAPIV3Schema:
description: Provider is the Schema for the providers 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: ProviderSpec defines the desired state of Provider.
properties:
credentials:
description: Credentials required to authenticate to this provider.
properties:
secretRef:
description: A SecretRef is a reference to a secret key that contains
the credentials that must be used to connect to the provider.
properties:
key:
description: The key to select.
type: string
name:
description: Name of the secret.
type: string
namespace:
description: Namespace of the secret.
type: string
required:
- key
- name
type: object
source:
description: Source of the provider credentials.
enum:
- None
- Secret
- InjectedIdentity
- Environment
- Filesystem
type: string
required:
- source
type: object
provider:
description: Provider is the cloud service provider, like `alibaba`
type: string
region:
description: Region is cloud provider's region
type: string
required:
- credentials
- provider
type: object
status:
description: ProviderStatus defines the observed state of Provider.
properties:
message:
type: string
state:
description: ProviderState is the type for Provider state
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

View File

@ -23,6 +23,7 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/domain/service"
apis "github.com/oam-dev/kubevela/pkg/apiserver/interfaces/api/dto/v1"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
"github.com/oam-dev/kubevela/pkg/config"
)
// ConfigAPIInterface returns config web service
@ -37,67 +38,55 @@ type configAPIInterface struct {
func (s *configAPIInterface) GetWebServiceRoute() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/config_types").
ws.Path(versionPrefix+"/configs").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML).
Doc("api for configuration management")
Doc("api for config management")
tags := []string{"config"}
ws.Route(ws.GET("/").To(s.listConfigTypes).
Doc("list all config types").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("configType", "list")).
Param(ws.QueryParameter("query", "Fuzzy search based on name and description.").DataType("string")).
Returns(200, "OK", []apis.ConfigType{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]apis.ConfigType{}))
ws.Route(ws.GET("/{configType}").To(s.getConfigType).
Doc("get a config type").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("configType", "get")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Returns(200, "OK", apis.ConfigType{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigType{}))
ws.Route(ws.POST("/{configType}").To(s.createConfig).
ws.Route(ws.POST("/").To(s.createConfig).
Doc("create or update a config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("configType/config", "create")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Filter(s.RbacService.CheckPerm("config", "create")).
Reads(apis.CreateConfigRequest{}).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(200, "OK", apis.Config{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Returns(404, "Not Found", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
Writes(apis.Config{}))
ws.Route(ws.GET("/{configType}/configs").To(s.getConfigs).
Doc("get configs from a config type").
ws.Route(ws.GET("/").To(s.getConfigs).
Doc("list all configs that belong to the system scope").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("configType/config", "list")).
Param(ws.PathParameter("configType", "identifier of the config").DataType("string")).
Filter(s.RbacService.CheckPerm("config", "list")).
Param(ws.QueryParameter("template", "the name of the template").DataType("string")).
Returns(200, "OK", apis.ListConfigResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ListConfigResponse{}))
ws.Route(ws.GET("/{configName}").To(s.getConfig).
Doc("detail a config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("config", "get")).
Param(ws.PathParameter("configName", "identifier of the config").DataType("string")).
Returns(200, "OK", []*apis.Config{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigType{}))
Writes(apis.Config{}))
ws.Route(ws.GET("/{configType}/configs/{name}").To(s.getConfig).
Doc("get a config from a config type").
ws.Route(ws.PUT("/{configName}").To(s.updateConfig).
Doc("update a config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("configType/config", "get")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Param(ws.PathParameter("name", "identifier of the config").DataType("string")).
Returns(200, "OK", []*apis.Config{}).
Filter(s.RbacService.CheckPerm("config", "update")).
Param(ws.PathParameter("configName", "identifier of the config").DataType("string")).
Returns(200, "OK", []*apis.UpdateConfigRequest{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigType{}))
Writes(apis.UpdateConfigRequest{}))
ws.Route(ws.DELETE("/{configType}/configs/{name}").To(s.deleteConfig).
ws.Route(ws.DELETE("/{configName}").To(s.deleteConfig).
Doc("delete a config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("configType/config", "delete")).
Param(ws.PathParameter("configType", "identifier of the config type").DataType("string")).
Param(ws.PathParameter("name", "identifier of the config").DataType("string")).
Filter(s.RbacService.CheckPerm("config", "delete")).
Param(ws.PathParameter("configName", "identifier of the config").DataType("string")).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Returns(404, "Not Found", bcode.Bcode{}).
@ -107,21 +96,65 @@ func (s *configAPIInterface) GetWebServiceRoute() *restful.WebService {
return ws
}
func (s *configAPIInterface) listConfigTypes(req *restful.Request, res *restful.Response) {
types, err := s.ConfigService.ListConfigTypes(req.Request.Context(), req.QueryParameter("query"))
if len(types) == 0 && err != nil {
// ConfigTemplateAPIInterface returns config web service
func ConfigTemplateAPIInterface() Interface {
return &configTemplateAPIInterface{}
}
type configTemplateAPIInterface struct {
ConfigService service.ConfigService `inject:""`
RbacService service.RBACService `inject:""`
}
func (s *configTemplateAPIInterface) GetWebServiceRoute() *restful.WebService {
ws := new(restful.WebService)
ws.Path(versionPrefix+"/config_templates").
Consumes(restful.MIME_XML, restful.MIME_JSON).
Produces(restful.MIME_JSON, restful.MIME_XML).
Doc("api for config management")
tags := []string{"config"}
ws.Route(ws.GET("/").To(s.listConfigTemplates).
Doc("List all config templates from the system namespace").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("config", "list")).
Returns(200, "OK", apis.ListConfigTemplateResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]apis.ListConfigTemplateResponse{}))
ws.Route(ws.GET("{templateName}").To(s.getConfigTemplate).
Doc("Detail a template").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(s.RbacService.CheckPerm("config", "get")).
Param(ws.PathParameter("templateName", "identifier of the config template").DataType("string")).
Param(ws.QueryParameter("namespace", "the name of the namespace").DataType("string")).
Returns(200, "OK", apis.ConfigTemplateDetail{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigTemplateDetail{}))
ws.Filter(authCheckFilter)
return ws
}
func (s *configTemplateAPIInterface) listConfigTemplates(req *restful.Request, res *restful.Response) {
templates, err := s.ConfigService.ListTemplates(req.Request.Context(), "", "")
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(types)
err = res.WriteEntity(apis.ListConfigTemplateResponse{Templates: templates})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configAPIInterface) getConfigType(req *restful.Request, res *restful.Response) {
t, err := s.ConfigService.GetConfigType(req.Request.Context(), req.PathParameter("configType"))
func (s *configTemplateAPIInterface) getConfigTemplate(req *restful.Request, res *restful.Response) {
t, err := s.ConfigService.GetTemplate(req.Request.Context(), config.NamespacedName{
Name: req.PathParameter("templateName"),
Namespace: req.QueryParameter("namespace"),
})
if err != nil {
bcode.ReturnError(req, res, err)
return
@ -145,24 +178,47 @@ func (s *configAPIInterface) createConfig(req *restful.Request, res *restful.Res
return
}
err := s.ConfigService.CreateConfig(req.Request.Context(), createReq)
config, err := s.ConfigService.CreateConfig(req.Request.Context(), "", createReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
if err := res.WriteEntity(config); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configAPIInterface) updateConfig(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var updateReq apis.UpdateConfigRequest
if err := req.ReadEntity(&updateReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&updateReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
config, err := s.ConfigService.UpdateConfig(req.Request.Context(), "", req.PathParameter("configName"), updateReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := res.WriteEntity(config); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (s *configAPIInterface) getConfigs(req *restful.Request, res *restful.Response) {
configs, err := s.ConfigService.GetConfigs(req.Request.Context(), req.PathParameter("configType"))
configs, err := s.ConfigService.ListConfigs(req.Request.Context(), "", req.QueryParameter("template"), true)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(configs)
err = res.WriteEntity(apis.ListConfigResponse{Configs: configs})
if err != nil {
bcode.ReturnError(req, res, err)
return
@ -170,7 +226,7 @@ func (s *configAPIInterface) getConfigs(req *restful.Request, res *restful.Respo
}
func (s *configAPIInterface) getConfig(req *restful.Request, res *restful.Response) {
t, err := s.ConfigService.GetConfig(req.Request.Context(), req.PathParameter("configType"), req.PathParameter("name"))
t, err := s.ConfigService.GetConfig(req.Request.Context(), "", req.PathParameter("configName"))
if err != nil {
bcode.ReturnError(req, res, err)
return
@ -183,7 +239,7 @@ func (s *configAPIInterface) getConfig(req *restful.Request, res *restful.Respon
}
func (s *configAPIInterface) deleteConfig(req *restful.Request, res *restful.Response) {
err := s.ConfigService.DeleteConfig(req.Request.Context(), req.PathParameter("configType"), req.PathParameter("name"))
err := s.ConfigService.DeleteConfig(req.Request.Context(), "", req.PathParameter("configName"))
if err != nil {
bcode.ReturnError(req, res, err)
return

View File

@ -23,6 +23,7 @@ import (
registryv1 "github.com/google/go-containerregistry/pkg/v1"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"helm.sh/helm/v3/pkg/repo"
corev1 "k8s.io/api/core/v1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@ -31,6 +32,7 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
"github.com/oam-dev/kubevela/pkg/cloudprovider"
"github.com/oam-dev/kubevela/pkg/config"
)
var (
@ -190,27 +192,64 @@ type AddonArgsResponse struct {
Args map[string]string `json:"args"`
}
// ConfigType define the format for listing configuration types
type ConfigType struct {
Definitions []string `json:"definitions"`
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
// CreateConfigRequest is the request body to creates a config
type CreateConfigRequest struct {
Name string `json:"name" validate:"checkname"`
Alias string `json:"alias"`
Description string `json:"description"`
Template NamespacedName `json:"template"`
Properties string `json:"properties,omitempty"`
}
// UpdateConfigRequest is the request body to update a config
type UpdateConfigRequest struct {
Alias string `json:"alias"`
Description string `json:"description"`
Properties string `json:"properties,omitempty"`
}
// ConfigTemplate define the format for listing configuration types
type ConfigTemplate struct {
Alias string `json:"alias"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Description string `json:"description"`
Scope string `json:"scope"`
Sensitive bool `json:"sensitive"`
CreateTime time.Time `json:"createTime"`
}
// ConfigTemplateDetail define the format for detail the config template
type ConfigTemplateDetail struct {
ConfigTemplate
APISchema *openapi3.Schema `json:"schema"`
UISchema utils.UISchema `json:"uiSchema"`
}
// Config define the metadata of a config
type Config struct {
ConfigType string `json:"configType"`
ConfigTypeAlias string `json:"configTypeAlias"`
Name string `json:"name"`
Project string `json:"project"`
Identifier string `json:"identifier"`
Alias string `json:"alias"`
Description string `json:"description"`
CreatedTime *time.Time `json:"createdTime"`
UpdatedTime *time.Time `json:"updatedTime"`
ApplicationStatus common.ApplicationPhase `json:"applicationStatus"`
Status string `json:"status"`
Template config.NamespacedName `json:"template"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Sensitive bool `json:"sensitive"`
Project string `json:"project"`
Alias string `json:"alias"`
Description string `json:"description"`
CreatedTime *time.Time `json:"createdTime"`
Properties map[string]interface{} `json:"properties,omitempty"`
Shared bool `json:"shared"`
Secret *corev1.Secret `json:"-"`
Targets []*config.ClusterTargetStatus `json:"targets"`
}
// ListConfigResponse is the response body for listing the configs
type ListConfigResponse struct {
Configs []*Config `json:"configs"`
}
// ListConfigTemplateResponse is the response body for listing the config templates
type ListConfigTemplateResponse struct {
Templates []*ConfigTemplate `json:"templates"`
}
// ImageResponse is the response for checking image
@ -438,16 +477,6 @@ type CreateApplicationRequest struct {
Component *CreateComponentRequest `json:"component"`
}
// CreateConfigRequest is the request body to creates a config
type CreateConfigRequest struct {
Name string `json:"name" validate:"checkname"`
Alias string `json:"alias"`
Description string `json:"description"`
Project string `json:"project"`
ComponentType string `json:"componentType" validate:"checkname"`
Properties string `json:"properties,omitempty"`
}
// UpdateApplicationRequest update application base config
type UpdateApplicationRequest struct {
Alias string `json:"alias" validate:"checkalias" optional:"true"`
@ -1443,9 +1472,10 @@ type ImageInfo struct {
// ImageRegistry the image repository info
type ImageRegistry struct {
Name string `json:"name"`
SecretName string `json:"secretName"`
Domain string `json:"domain"`
Name string `json:"name"`
SecretName string `json:"secretName"`
Domain string `json:"domain"`
Secret *corev1.Secret `json:"-"`
}
// ListImageRegistryResponse the response struct of listing the image registries
@ -1458,3 +1488,42 @@ type CloudShellPrepareResponse struct {
Status string `json:"status"`
Message string `json:"message"`
}
// ConfigType define the format for listing configuration types
type ConfigType struct {
Definitions []string `json:"definitions"`
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
}
// TerraformProvider define the metadata of a terraform provider
type TerraformProvider struct {
Name string `json:"name"`
Region string `json:"region"`
Provider string `json:"provider"`
CreateTime time.Time `json:"createTime"`
}
// ListTerraformProviderResponse is the response body for listing the terraform provider
type ListTerraformProviderResponse struct {
Providers []*TerraformProvider `json:"providers"`
}
// NamespacedName the name is required and the namespace is optional
type NamespacedName struct {
Name string `json:"name"`
Namespace string `json:"namespace" optional:"true"`
}
// CreateConfigDistributionRequest the request body of applying the distribution job.
type CreateConfigDistributionRequest struct {
Name string `json:"name"`
Configs []*NamespacedName `json:"configs"`
Targets []*ClusterTarget `json:"targets"`
}
// ListConfigDistributionResponse is the response body for listing the distribution
type ListConfigDistributionResponse struct {
Distributions []*config.Distribution `json:"distributions"`
}

View File

@ -71,6 +71,7 @@ func InitAPIBean() []interface{} {
// Config management
RegisterAPIInterface(ConfigAPIInterface())
RegisterAPIInterface(ConfigTemplateAPIInterface())
// Resources
RegisterAPIInterface(NewClusterAPIInterface())

View File

@ -23,5 +23,5 @@ import (
)
func TestInitAPIBean(t *testing.T) {
assert.Equal(t, len(InitAPIBean()), 22)
assert.Equal(t, len(InitAPIBean()), 23)
}

View File

@ -25,12 +25,14 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/bcode"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
"github.com/oam-dev/kubevela/pkg/config"
)
type projectAPIInterface struct {
RbacService service.RBACService `inject:""`
ProjectService service.ProjectService `inject:""`
TargetService service.TargetService `inject:""`
ConfigService service.ConfigService `inject:""`
}
// NewProjectAPIInterface new project APIInterface
@ -193,15 +195,115 @@ func (n *projectAPIInterface) GetWebServiceRoute() *restful.WebService {
Returns(200, "OK", []apis.PermissionBase{}).
Writes([]apis.PermissionBase{}))
ws.Route(ws.GET("/{projectName}/config_templates").To(n.getConfigTemplates).
Doc("get the templates which are in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "list")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Param(ws.QueryParameter("namespace", "the namespace of the template").DataType("string").Required(true)).
Returns(200, "OK", apis.ListConfigTemplateResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ListConfigTemplateResponse{}))
ws.Route(ws.GET("/{projectName}/config_templates/{templateName}").To(n.getConfigTemplate).
Doc("Detail a template").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "get")).
Param(ws.PathParameter("templateName", "identifier of the config template").DataType("string")).
Param(ws.QueryParameter("namespace", "the name of the namespace").DataType("string")).
Returns(200, "OK", apis.ConfigTemplateDetail{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ConfigTemplateDetail{}))
ws.Route(ws.GET("/{projectName}/configs").To(n.getConfigs).
Doc("get configs which are in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "list")).
Param(ws.QueryParameter("configType", "config type").DataType("string")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string")).
Returns(200, "OK", []*apis.Config{}).
Param(ws.QueryParameter("template", "the template name").DataType("string")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Returns(200, "OK", apis.ListConfigResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes([]*apis.Config{}))
Writes(apis.ListConfigResponse{}))
ws.Route(ws.POST("/{projectName}/configs").To(n.createConfig).
Doc("create a config in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "list")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Reads(apis.CreateConfigRequest{}).
Returns(200, "OK", apis.Config{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.Config{}))
ws.Route(ws.DELETE("/{projectName}/configs/{configName}").To(n.deleteConfig).
Doc("delete a config from a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "list")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Param(ws.PathParameter("configName", "identifier of the config").DataType("string").Required(true)).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Route(ws.PUT("/{projectName}/configs/{configName}").To(n.updateConfig).
Doc("update a config in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "list")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Param(ws.PathParameter("configName", "identifier of the config").DataType("string").Required(true)).
Returns(200, "OK", apis.Config{}).
Reads(apis.UpdateConfigRequest{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.Config{}))
ws.Route(ws.GET("/{projectName}/configs/{configName}").To(n.detailConfig).
Doc("detail a config in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "list")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Param(ws.PathParameter("configName", "identifier of the config").DataType("string").Required(true)).
Returns(200, "OK", apis.Config{}).
Reads(apis.UpdateConfigRequest{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.Config{}))
ws.Route(ws.POST("/{projectName}/distributions").To(n.applyDistribution).
Doc("apply the distribution job of the config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "distribute")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Reads(apis.CreateConfigDistributionRequest{}).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Route(ws.GET("/{projectName}/distributions").To(n.listDistributions).
Doc("list the distribution jobs of the config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "distribute")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Returns(200, "OK", apis.ListConfigDistributionResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ListConfigDistributionResponse{}))
ws.Route(ws.DELETE("/{projectName}/distributions/{distributionName}").To(n.deleteDistribution).
Doc("delete a distribution job of the config").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/config", "distribute")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Param(ws.PathParameter("distributionName", "identifier of the distribution").DataType("string").Required(true)).
Returns(200, "OK", apis.EmptyResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.EmptyResponse{}))
ws.Route(ws.GET("/{projectName}/providers").To(n.getProviders).
Doc("get providers which are in a project").
Metadata(restfulspec.KeyOpenAPITags, tags).
Filter(n.RbacService.CheckPerm("project/provider", "list")).
Param(ws.PathParameter("projectName", "identifier of the project").DataType("string").Required(true)).
Returns(200, "OK", apis.ListTerraformProviderResponse{}).
Returns(400, "Bad Request", bcode.Bcode{}).
Writes(apis.ListTerraformProviderResponse{}))
ws.Filter(authCheckFilter)
return ws
@ -570,20 +672,179 @@ func (n *projectAPIInterface) deleteProjectPermission(req *restful.Request, res
}
}
func (n *projectAPIInterface) getConfigs(req *restful.Request, res *restful.Response) {
configs, err := n.ProjectService.GetConfigs(req.Request.Context(), req.PathParameter("projectName"), req.QueryParameter("configType"))
func (n *projectAPIInterface) getConfigTemplates(req *restful.Request, res *restful.Response) {
templates, err := n.ConfigService.ListTemplates(req.Request.Context(), req.PathParameter("projectName"), "project")
if err != nil {
bcode.ReturnError(req, res, err)
return
}
if configs == nil {
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
return
}
err = res.WriteEntity(configs)
err = res.WriteEntity(apis.ListConfigTemplateResponse{Templates: templates})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) getConfigTemplate(req *restful.Request, res *restful.Response) {
t, err := n.ConfigService.GetTemplate(req.Request.Context(), config.NamespacedName{
Name: req.PathParameter("templateName"),
Namespace: req.QueryParameter("namespace"),
})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(t)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) getConfigs(req *restful.Request, res *restful.Response) {
configs, err := n.ConfigService.ListConfigs(req.Request.Context(), req.PathParameter("projectName"), req.QueryParameter("template"), false)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(apis.ListConfigResponse{Configs: configs})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) createConfig(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var createReq apis.CreateConfigRequest
if err := req.ReadEntity(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
config, err := n.ConfigService.CreateConfig(req.Request.Context(), req.PathParameter("projectName"), createReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(config)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) updateConfig(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var updateReq apis.UpdateConfigRequest
if err := req.ReadEntity(&updateReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&updateReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
config, err := n.ConfigService.UpdateConfig(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("configName"), updateReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(config)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) detailConfig(req *restful.Request, res *restful.Response) {
config, err := n.ConfigService.GetConfig(req.Request.Context(),
req.PathParameter("projectName"), req.PathParameter("configName"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(config)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) deleteConfig(req *restful.Request, res *restful.Response) {
err := n.ConfigService.DeleteConfig(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("configName"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(apis.EmptyResponse{})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) getProviders(req *restful.Request, res *restful.Response) {
providers, err := n.ProjectService.ListTerraformProviders(req.Request.Context(), req.PathParameter("projectName"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(apis.ListTerraformProviderResponse{Providers: providers})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) applyDistribution(req *restful.Request, res *restful.Response) {
// Verify the validity of parameters
var createReq apis.CreateConfigDistributionRequest
if err := req.ReadEntity(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
if err := validate.Struct(&createReq); err != nil {
bcode.ReturnError(req, res, err)
return
}
// Call the domain layer code
err := n.ConfigService.CreateConfigDistribution(req.Request.Context(), req.PathParameter("projectName"), createReq)
if err != nil {
bcode.ReturnError(req, res, err)
return
}
// Write back response data
if err := res.WriteEntity(apis.EmptyResponse{}); err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) listDistributions(req *restful.Request, res *restful.Response) {
distributions, err := n.ConfigService.ListConfigDistributions(req.Request.Context(), req.PathParameter("projectName"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(apis.ListConfigDistributionResponse{Distributions: distributions})
if err != nil {
bcode.ReturnError(req, res, err)
return
}
}
func (n *projectAPIInterface) deleteDistribution(req *restful.Request, res *restful.Response) {
err := n.ConfigService.DeleteConfigDistribution(req.Request.Context(), req.PathParameter("projectName"), req.PathParameter("distributionName"))
if err != nil {
bcode.ReturnError(req, res, err)
return
}
err = res.WriteEntity(apis.EmptyResponse{})
if err != nil {
bcode.ReturnError(req, res, err)
return

View File

@ -42,6 +42,7 @@ import (
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/container"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
pkgconfig "github.com/oam-dev/kubevela/pkg/config"
pkgUtils "github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
@ -116,6 +117,11 @@ func (s *restServer) buildIoCContainer() error {
return fmt.Errorf("fail to provides the apply bean to the container: %w", err)
}
factory := pkgconfig.NewConfigFactory(kubeClient)
if err := s.beanContainer.ProvideWithName("configFactory", factory); err != nil {
return fmt.Errorf("fail to provides the config factory bean to the container: %w", err)
}
// domain
if err := s.beanContainer.Provides(service.InitServiceBean(s.cfg)...); err != nil {
return fmt.Errorf("fail to provides the service bean to the container: %w", err)

View File

@ -0,0 +1,40 @@
/*
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 bcode
var (
// ErrSensitiveConfig means the config can not be read
ErrSensitiveConfig = NewBcode(400, 16001, "the config is sensitive")
// ErrNoConfigOrTarget means there is no target or config when creating the distribution.
ErrNoConfigOrTarget = NewBcode(400, 16002, "you must specify the config name and destination to distribute")
// ErrConfigExist means the config is exist
ErrConfigExist = NewBcode(400, 16003, "the config name is exist")
// ErrChangeTemplate the template of the config can not be change
ErrChangeTemplate = NewBcode(400, 16004, "the template of the config can not be change")
// ErrTemplateNotFound means the template is not exist
ErrTemplateNotFound = NewBcode(404, 16005, "the template is not exist")
// ErrConfigNotFound means the config is not exist
ErrConfigNotFound = NewBcode(404, 16006, "the config is not exist")
// ErrNotFoundDistribution means the distribution is not exist
ErrNotFoundDistribution = NewBcode(404, 16007, "the distribution is not exist")
)

View File

@ -23,6 +23,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/utils/log"
)
// GetDexConnectors returns the dex connectors for Dex connector controller
@ -36,15 +38,18 @@ func GetDexConnectors(ctx context.Context, k8sClient client.Client) ([]map[strin
for i, s := range secrets.Items {
var data map[string]interface{}
key := s.Labels[types.LabelConfigSubType]
err := json.Unmarshal(s.Data[key], &data)
if err != nil {
return nil, err
}
connectors[i] = map[string]interface{}{
"type": s.Labels[types.LabelConfigSubType],
"id": s.Name,
"name": s.Name,
"config": data,
if _, ok := s.Data[key]; ok {
err := json.Unmarshal(s.Data[key], &data)
if err != nil {
log.Logger.Warnf("the dex connector %s is invalid", s.Name)
continue
}
connectors[i] = map[string]interface{}{
"type": s.Labels[types.LabelConfigSubType],
"id": s.Name,
"name": s.Name,
"config": data,
}
}
}

View File

@ -54,7 +54,7 @@ func TestGetDexConnectors(t *testing.T) {
Labels: map[string]string{
"app.oam.dev/source-of-truth": "from-inner-system",
"config.oam.dev/catalog": "velacore-config",
"config.oam.dev/type": "config-dex-connector",
"config.oam.dev/type": "dex-connector",
"config.oam.dev/sub-type": "ldap",
"project": "abc",
},

View File

@ -137,3 +137,21 @@ func (f *deferredFactory) Client() client.Client {
}
return f.Factory.Client()
}
type testFactory struct {
cfg *rest.Config
cli client.Client
}
// NewTestFactory new a factory for the testing
func NewTestFactory(cfg *rest.Config,
cli client.Client) Factory {
return &testFactory{cli: cli, cfg: cfg}
}
func (t *testFactory) Client() client.Client {
return t.cli
}
func (t *testFactory) Config() *rest.Config {
return t.cfg
}

View File

@ -0,0 +1,72 @@
/*
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 config
import (
"testing"
"time"
gomock "github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"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 ctl *gomock.Controller
var _ = BeforeSuite(func(done Done) {
By("Bootstrapping test environment")
testEnv = &envtest.Environment{
UseExistingCluster: pointer.BoolPtr(false),
ControlPlaneStartTimeout: time.Minute,
ControlPlaneStopTimeout: time.Minute,
CRDDirectoryPaths: []string{"../../charts/vela-core/crds"},
}
var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())
// +kubebuilder:scaffold:scheme
By("Create the k8s client")
k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
close(done)
}, 60)
var _ = AfterSuite(func() {
By("Tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
func TestConfig(t *testing.T) {
ctl = gomock.NewController(t)
RegisterFailHandler(Fail)
RunSpecs(t, "Config Suite")
}

View File

@ -0,0 +1,36 @@
/*
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 context
import "context"
// DefaultContext the default context template
var DefaultContext = []byte(`
context: {
name: string
namespace: string
}
`)
// ConfigRenderContext the default context values for render the config
type ConfigRenderContext struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
// ReadConfigProvider the provide function for reading the config properties
type ReadConfigProvider func(ctx context.Context, namespace string, name string) (map[string]interface{}, error)

889
pkg/config/factory.go Normal file
View File

@ -0,0 +1,889 @@
/*
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 config
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/getkin/kin-openapi/openapi3"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
pkgtypes "k8s.io/apimachinery/pkg/types"
"k8s.io/klog"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"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/apiserver/domain/model"
"github.com/oam-dev/kubevela/pkg/apiserver/utils"
icontext "github.com/oam-dev/kubevela/pkg/config/context"
"github.com/oam-dev/kubevela/pkg/config/writer"
"github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/cue/script"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/apply"
)
// SaveInputPropertiesKey define the key name for saving the input properties in the secret.
const SaveInputPropertiesKey = "input-properties"
// SaveObjectReferenceKey define the key name for saving the outputs objects reference metadata in the secret.
const SaveObjectReferenceKey = "objects-reference"
// SaveExpandedWriterKey define the key name for saving the expanded writer config
const SaveExpandedWriterKey = "expanded-writer"
// SaveSchemaKey define the key name for saving the API schema
const SaveSchemaKey = "schema"
// SaveTemplateKey define the key name for saving the config-template
const SaveTemplateKey = "template"
// TemplateConfigMapNamePrefix the prefix of the configmap name.
const TemplateConfigMapNamePrefix = "config-template-"
// ErrSensitiveConfig means this config can not be read directly.
var ErrSensitiveConfig = errors.New("the config is sensitive")
// ErrNoConfigOrTarget means the config or the target is empty.
var ErrNoConfigOrTarget = errors.New("you must specify the config name and destination to distribute")
// ErrNotFoundDistribution means the app of the distribution is not exist.
var ErrNotFoundDistribution = errors.New("the distribution is not found")
// ErrConfigExist means the config is exist.
var ErrConfigExist = errors.New("the config is exist")
// ErrConfigNotFound means the config is not exist
var ErrConfigNotFound = errors.New("the config is not exist")
// ErrTemplateNotFound means the template is not exist
var ErrTemplateNotFound = errors.New("the template is not exist")
// NamespacedName the namespace and name model
type NamespacedName struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
// Template This is the spec of the config template, parse from the cue script.
type Template struct {
NamespacedName
Alias string `json:"alias,omitempty"`
Description string `json:"description,omitempty"`
// Scope defines the usage scope of the configuration template. Provides two options: System or Namespace
// System: The system users could use this template, and the config secret will save in the vela-system namespace.
// Namespace: The config secret will save in the target namespace, such as this namespace belonging to one project.
Scope string `json:"scope"`
// Sensitive means this config config can not be read from the API or the workflow step, only support the safe way, such as Secret.
Sensitive bool `json:"sensitive"`
CreateTime time.Time `json:"createTime"`
Template script.CUE `json:"template"`
ExpandedWriter writer.ExpandedWriterConfig `json:"expandedWriter"`
Schema *openapi3.Schema `json:"schema"`
ConfigMap *v1.ConfigMap `json:"-"`
}
// Metadata users should provide this model.
type Metadata struct {
NamespacedName
Alias string `json:"alias,omitempty"`
Description string `json:"description,omitempty"`
Properties map[string]interface{} `json:"properties"`
}
// Config this is the config model, generated from the template and properties.
type Config struct {
Metadata
CreateTime time.Time
Template Template `json:"template"`
// Secret this is default output way.
Secret *v1.Secret `json:"secret"`
// ExpandedWriterData
ExpandedWriterData *writer.ExpandedWriterData `json:"expandedWriterData"`
// OutputObjects this means users could define other objects.
// This field assign value only on config render stage.
OutputObjects map[string]*unstructured.Unstructured
// ObjectReferences correspond OutputObjects
ObjectReferences []v1.ObjectReference
Targets []*ClusterTargetStatus
}
// ClusterTargetStatus merge the status of the distribution
type ClusterTargetStatus struct {
ClusterTarget
Status string `json:"status"`
Application NamespacedName `json:"application"`
Message string `json:"message"`
}
// ClusterTarget kubernetes delivery target
type ClusterTarget struct {
ClusterName string `json:"clusterName"`
Namespace string `json:"namespace"`
}
// Distribution the config distribution model
type Distribution struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
CreatedTime time.Time `json:"createdTime"`
Configs []*NamespacedName `json:"configs"`
Targets []*ClusterTarget `json:"targets"`
Application pkgtypes.NamespacedName `json:"application"`
Status common.AppStatus `json:"status"`
}
// CreateDistributionSpec the spec of the distribution
type CreateDistributionSpec struct {
Configs []*NamespacedName
Targets []*ClusterTarget
}
// Factory handle the config
type Factory interface {
ParseTemplate(defaultName string, content []byte) (*Template, error)
ParseConfig(ctx context.Context, template NamespacedName, meta Metadata) (*Config, error)
LoadTemplate(ctx context.Context, name, ns string) (*Template, error)
CreateOrUpdateConfigTemplate(ctx context.Context, ns string, it *Template) error
DeleteTemplate(ctx context.Context, ns, name string) error
ListTemplates(ctx context.Context, ns, scope string) ([]*Template, error)
ReadConfig(ctx context.Context, namespace, name string) (map[string]interface{}, error)
GetConfig(ctx context.Context, namespace, name string, withStatus bool) (*Config, error)
ListConfigs(ctx context.Context, namespace, template, scope string, withStatus bool) ([]*Config, error)
DeleteConfig(ctx context.Context, namespace, name string) error
CreateOrUpdateConfig(ctx context.Context, i *Config, ns string) error
CreateOrUpdateDistribution(ctx context.Context, ns, name string, ads *CreateDistributionSpec) error
ListDistributions(ctx context.Context, ns string) ([]*Distribution, error)
DeleteDistribution(ctx context.Context, ns, name string) error
MergeDistributionStatus(ctx context.Context, config *Config, namespace string) error
}
// NewConfigFactory create a config factory instance
func NewConfigFactory(cli client.Client) Factory {
return &kubeConfigFactory{cli: cli, apiApply: apply.NewAPIApplicator(cli)}
}
type kubeConfigFactory struct {
cli client.Client
apiApply *apply.APIApplicator
}
// ParseTemplate parse a config template instance form the cue script
func (k *kubeConfigFactory) ParseTemplate(defaultName string, content []byte) (*Template, error) {
cueScript := script.BuildCUEScriptWithDefaultContext(icontext.DefaultContext, content)
value, err := cueScript.ParseToValue(false)
if err != nil {
return nil, fmt.Errorf("the cue script is invalid:%w", err)
}
name, err := value.GetString("metadata", "name")
if err != nil {
if defaultName == "" {
return nil, fmt.Errorf("fail to get the name from the template metadata: %w", err)
}
}
if defaultName != "" {
name = defaultName
}
schema, err := cueScript.ParsePropertiesToSchema()
if err != nil {
return nil, fmt.Errorf("the properties of the cue script is invalid:%w", err)
}
alias, err := value.GetString("metadata", "alias")
if err != nil && !IsFieldNotExist(err) {
klog.Warningf("fail to get the alias from the template metadata: %s", err.Error())
}
scope, err := value.GetString("metadata", "scope")
if err != nil && !IsFieldNotExist(err) {
klog.Warningf("fail to get the scope from the template metadata: %s", err.Error())
}
sensitive, err := value.GetBool("metadata", "sensitive")
if err != nil && !IsFieldNotExist(err) {
klog.Warningf("fail to get the sensitive from the template metadata: %s", err.Error())
}
templateValue, err := value.LookupValue("template")
if err != nil {
return nil, err
}
template := &Template{
NamespacedName: NamespacedName{
Name: name,
},
Alias: alias,
Scope: scope,
Sensitive: sensitive,
Template: cueScript,
Schema: schema,
ExpandedWriter: writer.ParseExpandedWriterConfig(templateValue),
}
var configmap v1.ConfigMap
configmap.Name = TemplateConfigMapNamePrefix + template.Name
configmap.Data = map[string]string{
SaveTemplateKey: string(template.Template),
}
if template.Schema != nil {
data, err := yaml.Marshal(template.Schema)
if err != nil {
return nil, err
}
configmap.Data[SaveSchemaKey] = string(data)
}
data, err := yaml.Marshal(template.ExpandedWriter)
if err != nil {
return nil, err
}
configmap.Data[SaveExpandedWriterKey] = string(data)
configmap.Labels = map[string]string{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigScope: template.Scope,
}
configmap.Annotations = map[string]string{
types.AnnotationConfigDescription: template.Description,
types.AnnotationConfigAlias: template.Alias,
types.AnnotationConfigSensitive: fmt.Sprintf("%t", template.Sensitive),
}
template.ConfigMap = &configmap
return template, nil
}
// IsFieldNotExist check whether the error type is the field not found
func IsFieldNotExist(err error) bool {
return strings.Contains(err.Error(), "not exist")
}
// CreateOrUpdateConfigTemplate parse and update the config template
func (k *kubeConfigFactory) CreateOrUpdateConfigTemplate(ctx context.Context, ns string, it *Template) error {
if ns != "" {
it.ConfigMap.Namespace = ns
}
return k.apiApply.Apply(ctx, it.ConfigMap, apply.DisableUpdateAnnotation(), apply.Quiet())
}
func convertConfigMap2Template(cm v1.ConfigMap) (*Template, error) {
if cm.Labels == nil || cm.Annotations == nil {
return nil, fmt.Errorf("this configmap is not a valid config-template")
}
it := &Template{
NamespacedName: NamespacedName{
Name: strings.Replace(cm.Name, TemplateConfigMapNamePrefix, "", 1),
Namespace: cm.Namespace,
},
Alias: cm.Annotations[types.AnnotationConfigAlias],
Description: cm.Annotations[types.AnnotationConfigDescription],
Sensitive: cm.Annotations[types.AnnotationConfigSensitive] == "true",
Scope: cm.Labels[types.LabelConfigScope],
CreateTime: cm.CreationTimestamp.Time,
Template: script.CUE(cm.Data[SaveTemplateKey]),
}
if cm.Data[SaveSchemaKey] != "" {
var schema openapi3.Schema
err := yaml.Unmarshal([]byte(cm.Data[SaveSchemaKey]), &schema)
if err != nil {
return nil, fmt.Errorf("fail to parse the schema: %w", err)
}
it.Schema = &schema
}
if cm.Data[SaveExpandedWriterKey] != "" {
var config writer.ExpandedWriterConfig
err := yaml.Unmarshal([]byte(cm.Data[SaveExpandedWriterKey]), &config)
if err != nil {
return nil, fmt.Errorf("fail to parse the schema: %w", err)
}
it.ExpandedWriter = config
}
return it, nil
}
// DeleteTemplate delete the config template
func (k *kubeConfigFactory) DeleteTemplate(ctx context.Context, ns, name string) error {
var configmap v1.ConfigMap
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: ns, Name: TemplateConfigMapNamePrefix + name}, &configmap); err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("the config template %s not found", name)
}
return fmt.Errorf("fail to delete the config template %s:%w", name, err)
}
return k.cli.Delete(ctx, &configmap)
}
// ListTemplates list the config templates
func (k *kubeConfigFactory) ListTemplates(ctx context.Context, ns, scope string) ([]*Template, error) {
var list = &v1.ConfigMapList{}
selector, err := labels.Parse(fmt.Sprintf("%s=%s", types.LabelConfigCatalog, types.VelaCoreConfig))
if err != nil {
return nil, err
}
if err := k.cli.List(ctx, list,
client.MatchingLabelsSelector{Selector: selector},
client.InNamespace(ns)); err != nil {
return nil, err
}
var templates []*Template
for _, item := range list.Items {
it, err := convertConfigMap2Template(item)
if err != nil {
klog.Warningf("fail to parse the configmap %s:%s", item.Name, err.Error())
}
if it != nil {
if scope == "" || it.Scope == scope {
templates = append(templates, it)
}
}
}
return templates, nil
}
// LoadTemplate load the template
func (k *kubeConfigFactory) LoadTemplate(ctx context.Context, name, ns string) (*Template, error) {
var cm v1.ConfigMap
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: ns, Name: TemplateConfigMapNamePrefix + name}, &cm); err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrTemplateNotFound
}
return nil, err
}
return convertConfigMap2Template(cm)
}
// ParseConfig merge the properties to template and build a config instance
// If the templateName is empty, means creating a secret without the template.
func (k *kubeConfigFactory) ParseConfig(ctx context.Context,
template NamespacedName, meta Metadata,
) (*Config, error) {
var secret v1.Secret
config := &Config{
Metadata: meta,
Secret: &secret,
}
if template.Name != "" {
template, err := k.LoadTemplate(ctx, template.Name, template.Namespace)
if err != nil {
return nil, err
}
contextValue := icontext.ConfigRenderContext{
Name: meta.Name,
Namespace: meta.Namespace,
}
// Render the output secret
output, err := template.Template.RunAndOutput(contextValue, meta.Properties)
if err != nil && !cue.IsFieldNotExist(err) {
return nil, err
}
if output != nil {
if err := output.UnmarshalTo(&secret); err != nil {
return nil, fmt.Errorf("the output format must be secret")
}
}
if secret.Type == "" {
secret.Type = v1.SecretType(fmt.Sprintf("%s/%s", "", template.Name))
}
if secret.Labels == nil {
secret.Labels = map[string]string{}
}
secret.Labels[types.LabelConfigCatalog] = types.VelaCoreConfig
secret.Labels[types.LabelConfigType] = template.Name
secret.Labels[types.LabelConfigType] = template.Name
secret.Labels[types.LabelConfigScope] = template.Scope
if secret.Annotations == nil {
secret.Annotations = map[string]string{}
}
secret.Annotations[types.AnnotationConfigSensitive] = fmt.Sprintf("%t", template.Sensitive)
secret.Annotations[types.AnnotationConfigTemplateNamespace] = template.Namespace
config.Template = *template
// Render the expanded writer configuration
data, err := writer.RenderForExpandedWriter(template.ExpandedWriter, config.Template.Template, contextValue, meta.Properties)
if err != nil {
return nil, fmt.Errorf("fail to render the content for the expanded writer:%w ", err)
}
config.ExpandedWriterData = data
// Render the outputs objects
outputs, err := template.Template.RunAndOutput(contextValue, meta.Properties, "template", "outputs")
if err != nil && !cue.IsFieldNotExist(err) {
return nil, err
}
if outputs != nil {
var objects = map[string]interface{}{}
if err := outputs.UnmarshalTo(&objects); err != nil {
return nil, fmt.Errorf("the outputs is invalid %w", err)
}
var objectReferences []v1.ObjectReference
config.OutputObjects = make(map[string]*unstructured.Unstructured)
for k := range objects {
if ob, ok := objects[k].(map[string]interface{}); ok {
obj := &unstructured.Unstructured{Object: ob}
config.OutputObjects[k] = obj
objectReferences = append(objectReferences, v1.ObjectReference{
Kind: obj.GetKind(),
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
APIVersion: obj.GetAPIVersion(),
})
}
}
objectReferenceJSON, err := json.Marshal(objectReferences)
if err != nil {
return nil, err
}
if secret.Data == nil {
secret.Data = map[string][]byte{}
}
secret.Data[SaveObjectReferenceKey] = objectReferenceJSON
}
} else {
secret.Labels = map[string]string{
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: "",
}
secret.Annotations = map[string]string{}
}
secret.Namespace = meta.Namespace
if secret.Name == "" {
secret.Name = meta.Name
}
secret.Annotations[types.AnnotationConfigAlias] = meta.Alias
secret.Annotations[types.AnnotationConfigDescription] = meta.Description
pp, err := json.Marshal(meta.Properties)
if err != nil {
return nil, err
}
if secret.Data == nil {
secret.Data = map[string][]byte{}
}
secret.Data[SaveInputPropertiesKey] = pp
return config, nil
}
// ReadConfig read the config secret
func (k *kubeConfigFactory) ReadConfig(ctx context.Context, namespace, name string) (map[string]interface{}, error) {
var secret v1.Secret
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil {
return nil, err
}
if secret.Annotations[types.AnnotationConfigSensitive] == "true" {
return nil, ErrSensitiveConfig
}
properties := secret.Data[SaveInputPropertiesKey]
var input = map[string]interface{}{}
if err := json.Unmarshal(properties, &input); err != nil {
return nil, err
}
return input, nil
}
func (k *kubeConfigFactory) GetConfig(ctx context.Context, namespace, name string, withStatus bool) (*Config, error) {
var secret v1.Secret
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil {
if apierrors.IsNotFound(err) {
return nil, ErrConfigNotFound
}
return nil, err
}
if secret.Annotations[types.AnnotationConfigSensitive] == "true" {
return nil, ErrSensitiveConfig
}
item, err := convertSecret2Config(&secret)
if err != nil {
return nil, err
}
if withStatus {
if err := k.MergeDistributionStatus(ctx, item, item.Namespace); err != nil && !errors.Is(err, ErrNotFoundDistribution) {
klog.Warningf("fail to merge the status %s:%s", item.Name, err.Error())
}
}
return item, nil
}
// CreateOrUpdateConfig create or update the config.
// Write the expand config to the target server.
func (k *kubeConfigFactory) CreateOrUpdateConfig(ctx context.Context, i *Config, ns string) error {
var secret v1.Secret
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: i.Namespace, Name: i.Name}, &secret); err == nil {
if secret.Labels[types.LabelConfigType] != i.Template.Name {
return ErrConfigExist
}
}
if err := k.apiApply.Apply(ctx, i.Secret, apply.Quiet()); err != nil {
return fmt.Errorf("fail to apply the secret: %w", err)
}
for key, obj := range i.OutputObjects {
obj.SetOwnerReferences([]metav1.OwnerReference{{
APIVersion: "v1",
Kind: "Secret",
Name: i.Secret.Name,
UID: i.Secret.UID,
}})
if err := k.apiApply.Apply(ctx, obj, apply.Quiet()); err != nil {
return fmt.Errorf("fail to apply the object %s: %w", key, err)
}
}
readConfig := func(ctx context.Context, namespace, name string) (map[string]interface{}, error) {
return k.ReadConfig(ctx, namespace, name)
}
if i.ExpandedWriterData != nil {
if errs := writer.Write(ctx, i.ExpandedWriterData, readConfig); len(errs) > 0 {
return errs[0]
}
}
return nil
}
func (k *kubeConfigFactory) ListConfigs(ctx context.Context, namespace, template, scope string, withStatus bool) ([]*Config, error) {
var list = &v1.SecretList{}
requirement := fmt.Sprintf("%s=%s", types.LabelConfigCatalog, types.VelaCoreConfig)
if template != "" {
requirement = fmt.Sprintf("%s,%s=%s", requirement, types.LabelConfigType, template)
}
if scope != "" {
requirement = fmt.Sprintf("%s,%s=%s", requirement, types.LabelConfigScope, scope)
}
selector, err := labels.Parse(requirement)
if err != nil {
return nil, err
}
if err := k.cli.List(ctx, list,
client.MatchingLabelsSelector{Selector: selector},
client.InNamespace(namespace)); err != nil {
return nil, err
}
var configs []*Config
for i := range list.Items {
item := list.Items[i]
it, err := convertSecret2Config(&item)
if err != nil {
klog.Warningf("fail to parse the secret %s:%s", item.Name, err.Error())
}
if it != nil {
if withStatus {
if err := k.MergeDistributionStatus(ctx, it, it.Namespace); err != nil && !errors.Is(err, ErrNotFoundDistribution) {
klog.Warningf("fail to merge the status %s:%s", item.Name, err.Error())
}
}
configs = append(configs, it)
}
}
return configs, nil
}
func (k *kubeConfigFactory) DeleteConfig(ctx context.Context, namespace, name string) error {
var secret v1.Secret
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: name}, &secret); err != nil {
if apierrors.IsNotFound(err) {
return fmt.Errorf("the config %s not found", name)
}
return fmt.Errorf("fail to delete the config %s:%w", name, err)
}
if secret.Labels[types.LabelConfigCatalog] != types.VelaCoreConfig {
return fmt.Errorf("found a secret but is not a config")
}
if objects, exist := secret.Data[SaveObjectReferenceKey]; exist {
var objectReferences []v1.ObjectReference
if err := json.Unmarshal(objects, &objectReferences); err != nil {
return err
}
for _, obj := range objectReferences {
if err := k.cli.Delete(ctx, convertObjectReference2Unstructured(obj)); err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("fail to clear the object %s:%w", obj.Name, err)
}
}
}
return k.cli.Delete(ctx, &secret)
}
func (k *kubeConfigFactory) MergeDistributionStatus(ctx context.Context, config *Config, namespace string) error {
app := &v1beta1.Application{}
if err := k.cli.Get(ctx, pkgtypes.NamespacedName{Namespace: namespace, Name: DefaultDistributionName(config.Name)}, app); err != nil {
if apierrors.IsNotFound(err) {
return ErrNotFoundDistribution
}
return err
}
var targets []*ClusterTargetStatus
for _, policy := range app.Spec.Policies {
if policy.Type == v1alpha1.TopologyPolicyType {
status := workflowv1alpha1.WorkflowStepPhasePending
message := ""
if app.Status.Workflow != nil {
for _, step := range app.Status.Workflow.Steps {
if policy.Name == strings.Replace(step.Name, "deploy-", "", 1) {
status = step.Phase
message = step.Message
}
}
}
var spec v1alpha1.TopologyPolicySpec
if err := json.Unmarshal(policy.Properties.Raw, &spec); err == nil {
for _, clu := range spec.Clusters {
targets = append(targets, &ClusterTargetStatus{
ClusterTarget: ClusterTarget{
Namespace: spec.Namespace,
ClusterName: clu,
},
Application: NamespacedName{Name: app.Name, Namespace: app.Namespace},
Status: string(status),
Message: message,
})
}
}
}
}
config.Targets = append(config.Targets, targets...)
return nil
}
func (k *kubeConfigFactory) CreateOrUpdateDistribution(ctx context.Context, ns, name string, ads *CreateDistributionSpec) error {
policies := convertTarget2TopologyPolicy(ads.Targets)
if len(policies) == 0 {
return ErrNoConfigOrTarget
}
// create the share policy
shareSpec := v1alpha1.SharedResourcePolicySpec{
Rules: []v1alpha1.SharedResourcePolicyRule{{
Selector: v1alpha1.ResourcePolicyRuleSelector{
CompNames: []string{name},
},
}},
}
properties, err := json.Marshal(shareSpec)
if err == nil {
policies = append(policies, v1beta1.AppPolicy{
Type: v1alpha1.SharedResourcePolicyType,
Name: "share-config",
Properties: &runtime.RawExtension{
Raw: properties,
},
})
}
var objects []map[string]string
for _, s := range ads.Configs {
objects = append(objects, map[string]string{
"name": s.Name,
"namespace": s.Namespace,
"resource": "secret",
})
}
if len(objects) == 0 {
return ErrNoConfigOrTarget
}
objectsBytes, err := json.Marshal(map[string][]map[string]string{"objects": objects})
if err != nil {
return err
}
reqByte, err := json.Marshal(ads)
if err != nil {
return err
}
distribution := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: ns,
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
// This label will override the secret label, then change the catalog of the distributed secrets.
types.LabelConfigCatalog: types.CatalogConfigDistribution,
},
Annotations: map[string]string{
types.AnnotationConfigDistributionSpec: string(reqByte),
oam.AnnotationPublishVersion: utils.GenerateVersion("config"),
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: name,
Type: v1alpha1.RefObjectsComponentType,
Properties: &runtime.RawExtension{Raw: objectsBytes},
},
},
Policies: policies,
},
}
return k.apiApply.Apply(ctx, distribution, apply.Quiet())
}
func (k *kubeConfigFactory) ListDistributions(ctx context.Context, ns string) ([]*Distribution, error) {
var apps v1beta1.ApplicationList
if err := k.cli.List(ctx, &apps, client.MatchingLabels{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: types.CatalogConfigDistribution,
}, client.InNamespace(ns)); err != nil {
return nil, err
}
var list []*Distribution
for _, app := range apps.Items {
dis := &Distribution{
Name: app.Name,
Namespace: app.Namespace,
CreatedTime: app.CreationTimestamp.Time,
Application: pkgtypes.NamespacedName{
Namespace: app.Namespace,
Name: app.Name,
},
Status: app.Status,
}
if spec, ok := app.Annotations[types.AnnotationConfigDistributionSpec]; ok {
var req CreateDistributionSpec
if err := json.Unmarshal([]byte(spec), &req); err == nil {
dis.Targets = req.Targets
dis.Configs = req.Configs
}
}
list = append(list, dis)
}
return list, nil
}
func (k *kubeConfigFactory) DeleteDistribution(ctx context.Context, ns, name string) error {
app := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
},
}
if err := k.cli.Delete(ctx, app); err != nil {
if apierrors.IsNotFound(err) {
return ErrNotFoundDistribution
}
return err
}
return nil
}
func convertTarget2TopologyPolicy(targets []*ClusterTarget) (policies []v1beta1.AppPolicy) {
for _, target := range targets {
policySpec := v1alpha1.TopologyPolicySpec{
Placement: v1alpha1.Placement{
Clusters: []string{target.ClusterName},
},
Namespace: target.Namespace,
}
properties, err := json.Marshal(policySpec)
if err == nil {
policies = append(policies, v1beta1.AppPolicy{
Type: v1alpha1.TopologyPolicyType,
Name: fmt.Sprintf("%s-%s", target.ClusterName, target.Namespace),
Properties: &runtime.RawExtension{
Raw: properties,
},
})
}
}
return
}
func convertSecret2Config(se *v1.Secret) (*Config, error) {
if se == nil || se.Labels == nil {
return nil, fmt.Errorf("this secret is not a valid config secret")
}
config := &Config{
Metadata: Metadata{
NamespacedName: NamespacedName{
Name: se.Name,
Namespace: se.Namespace,
},
},
CreateTime: se.CreationTimestamp.Time,
Secret: se,
Template: Template{
NamespacedName: NamespacedName{
Name: se.Labels[types.LabelConfigType],
},
},
}
if se.Annotations != nil {
config.Alias = se.Annotations[types.AnnotationConfigAlias]
config.Description = se.Annotations[types.AnnotationConfigDescription]
config.Template.Namespace = se.Annotations[types.AnnotationConfigTemplateNamespace]
config.Template.Sensitive = se.Annotations[types.AnnotationConfigSensitive] == "true"
}
if !config.Template.Sensitive && len(se.Data[SaveInputPropertiesKey]) > 0 {
var properties = map[string]interface{}{}
if err := yaml.Unmarshal(se.Data[SaveInputPropertiesKey], &properties); err != nil {
return nil, err
}
config.Properties = properties
}
if !config.Template.Sensitive {
config.Secret = se
} else {
seCope := se.DeepCopy()
seCope.Data = nil
seCope.StringData = nil
config.Secret = seCope
}
if content, ok := se.Data[SaveObjectReferenceKey]; ok {
var objectReferences []v1.ObjectReference
if err := json.Unmarshal(content, &objectReferences); err != nil {
klog.Warningf("the object references are invalid, config:%s", se.Name)
}
config.ObjectReferences = objectReferences
}
return config, nil
}
func convertObjectReference2Unstructured(ref v1.ObjectReference) *unstructured.Unstructured {
var obj unstructured.Unstructured
obj.SetAPIVersion(ref.APIVersion)
obj.SetNamespace(ref.Namespace)
obj.SetKind(ref.Kind)
obj.SetName(ref.Name)
return &obj
}
// DefaultDistributionName generate the distribution name by a config name
func DefaultDistributionName(configName string) string {
return fmt.Sprintf("distribute-%s", configName)
}

168
pkg/config/factory_test.go Normal file
View File

@ -0,0 +1,168 @@
/*
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 config
import (
"context"
"io/ioutil"
"os"
"testing"
"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/stretchr/testify/require"
nacosmock "github.com/oam-dev/kubevela/test/mock/nacos"
)
func TestParseConfigTemplate(t *testing.T) {
r := require.New(t)
content, err := ioutil.ReadFile("testdata/helm-repo.cue")
r.Equal(err, nil)
var inf = &kubeConfigFactory{}
template, err := inf.ParseTemplate("default", content)
r.Equal(err, nil)
r.NotEqual(template, nil)
r.Equal(template.Name, "default")
r.NotEqual(template.Schema, nil)
r.Equal(len(template.Schema.Properties), 4)
}
var _ = Describe("test config factory", func() {
var fac Factory
BeforeEach(func() {
fac = NewConfigFactory(k8sClient)
})
It("apply the nacos server template", func() {
data, err := os.ReadFile("./testdata/nacos-server.cue")
Expect(err).Should(BeNil())
t, err := fac.ParseTemplate("", data)
Expect(err).Should(BeNil())
Expect(fac.CreateOrUpdateConfigTemplate(context.TODO(), "default", t)).Should(BeNil())
})
It("apply a config to the nacos server", func() {
By("create a nacos server config")
nacos, err := fac.ParseConfig(context.TODO(), NamespacedName{Name: "nacos-server", Namespace: "default"}, Metadata{NamespacedName: NamespacedName{Name: "nacos", Namespace: "default"}, Properties: map[string]interface{}{
"servers": []map[string]interface{}{{
"ipAddr": "127.0.0.1",
"port": 8849,
}},
}})
Expect(err).Should(BeNil())
Expect(len(nacos.Secret.Data[SaveInputPropertiesKey]) > 0).Should(BeTrue())
Expect(fac.CreateOrUpdateConfig(context.Background(), nacos, "default")).Should(BeNil())
config, err := fac.ReadConfig(context.TODO(), "default", "nacos")
Expect(err).Should(BeNil())
servers, ok := config["servers"].([]interface{})
Expect(ok).Should(BeTrue())
Expect(len(servers)).Should(Equal(1))
By("apply a template that with the nacos writer")
data, err := os.ReadFile("./testdata/mysql-db-nacos.cue")
Expect(err).Should(BeNil())
t, err := fac.ParseTemplate("", data)
Expect(err).Should(BeNil())
Expect(t.ExpandedWriter.Nacos).ShouldNot(BeNil())
Expect(t.ExpandedWriter.Nacos.Endpoint.Name).Should(Equal("nacos"))
Expect(fac.CreateOrUpdateConfigTemplate(context.TODO(), "default", t)).Should(BeNil())
db, err := fac.ParseConfig(context.TODO(), NamespacedName{Name: "nacos", Namespace: "default"}, Metadata{NamespacedName: NamespacedName{Name: "db-config", Namespace: "default"}, Properties: map[string]interface{}{
"dataId": "dbconfig",
"appName": "db",
"content": map[string]interface{}{
"mysqlHost": "127.0.0.1:3306",
"mysqlPort": 3306,
"username": "test",
"password": "string",
},
}})
Expect(err).Should(BeNil())
Expect(db.Template.ExpandedWriter).ShouldNot(BeNil())
Expect(db.ExpandedWriterData).ShouldNot(BeNil())
Expect(len(db.ExpandedWriterData.Nacos.Content) > 0).Should(BeTrue())
Expect(db.ExpandedWriterData.Nacos.Metadata.DataID).Should(Equal("dbconfig"))
Expect(len(db.OutputObjects)).Should(Equal(1))
nacosClient := nacosmock.NewMockIConfigClient(ctl)
db.ExpandedWriterData.Nacos.Client = nacosClient
nacosClient.EXPECT().PublishConfig(gomock.Any()).Return(true, nil)
Expect(err).Should(BeNil())
Expect(fac.CreateOrUpdateConfig(context.Background(), db, "default")).Should(BeNil())
})
It("list all templates", func() {
templates, err := fac.ListTemplates(context.TODO(), "", "")
Expect(err).Should(BeNil())
Expect(len(templates)).Should(Equal(2))
})
It("list all configs", func() {
configs, err := fac.ListConfigs(context.TODO(), "", "", "", true)
Expect(err).Should(BeNil())
Expect(len(configs)).Should(Equal(2))
})
It("distribute a config", func() {
err := fac.CreateOrUpdateDistribution(context.TODO(), "default", "distribute-db-config", &CreateDistributionSpec{
Configs: []*NamespacedName{
{Name: "db-config", Namespace: "default"},
},
Targets: []*ClusterTarget{
{ClusterName: "local", Namespace: "test"},
},
})
Expect(err).Should(BeNil())
})
It("get the config", func() {
config, err := fac.GetConfig(context.TODO(), "default", "db-config", true)
Expect(err).Should(BeNil())
Expect(len(config.ObjectReferences)).ShouldNot(BeNil())
Expect(config.ObjectReferences[0].Kind).Should(Equal("ConfigMap"))
Expect(len(config.Targets)).Should(Equal(1))
})
It("list the distributions", func() {
distributions, err := fac.ListDistributions(context.TODO(), "default")
Expect(err).Should(BeNil())
Expect(len(distributions)).Should(Equal(1))
})
It("delete the distribution", func() {
err := fac.DeleteDistribution(context.TODO(), "default", "distribute-db-config")
Expect(err).Should(BeNil())
})
It("delete the config", func() {
err := fac.DeleteConfig(context.TODO(), "default", "db-config")
Expect(err).Should(BeNil())
})
It("delete the config template", func() {
err := fac.DeleteTemplate(context.TODO(), "default", "nacos")
Expect(err).Should(BeNil())
})
})

49
pkg/config/testdata/helm-repo.cue vendored Normal file
View File

@ -0,0 +1,49 @@
metadata: {
name: "helm-repository"
// alias: "Helm Repository"
scope: "system"
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": "helm-repository"
"config.oam.dev/multi-cluster": "true"
"config.oam.dev/sub-type": "helm"
}
}
// If the type is empty, it will assign value using this format.
type: "catalog.config.oam.dev/helm-repository"
stringData: {
url: parameter.url
if parameter.username != _|_ {
username: parameter.username
}
if parameter.password != _|_ {
password: parameter.password
}
}
data: {
if parameter.caFile != _|_ {
caFile: parameter.caFile
}
}
}
parameter: {
// +usage=The public url of the helm chart repository.
url: string
// +usage=The username of basic auth repo.
username?: string
// +usage=The password of basic auth repo.
password?: string
// +usage=The ca certificate of helm repository. Please encode this data with base64.
caFile?: string
}
}

49
pkg/config/testdata/mysql-db-nacos.cue vendored Normal file
View File

@ -0,0 +1,49 @@
metadata: {
name: "nacos"
alias: "Nacos Config"
}
template: {
nacos: {
// can not references the parameter
endpoint: {
name: "nacos"
namespace: "default"
}
format: "properties"
// could references the parameter
metadata: {
dataId: parameter.dataId
group: parameter.group
if parameter.appName != _|_ {
appName: parameter.appName
}
}
content: parameter.content
}
outputs: {
"test": {
kind: "ConfigMap"
apiVersion: "v1"
metadata: {
name: context.name
namespace: context.namespace
}
data: {
"string": "string"
}
}
}
parameter: {
dataId: string
group: *"DEFAULT_GROUP" | string
appName?: string
content: {
mysqlHost: string
mysqlPort: int
username?: string
password?: string
}
}
}

18
pkg/config/testdata/nacos-server.cue vendored Normal file
View File

@ -0,0 +1,18 @@
metadata: {
name: "nacos-server"
alias: "Nacos Server"
}
template: {
parameter: {
servers?: [...{
ipAddr: string
port: int
}]
client?: {
endpoint: string
accessKey: string
secretKey: string
}
}
}

203
pkg/config/writer/nacos.go Normal file
View File

@ -0,0 +1,203 @@
/*
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 writer
import (
"context"
"fmt"
"runtime/debug"
"strings"
"k8s.io/klog"
"github.com/nacos-group/nacos-sdk-go/v2/clients"
"github.com/nacos-group/nacos-sdk-go/v2/clients/config_client"
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"github.com/kubevela/workflow/pkg/cue/model/value"
"github.com/oam-dev/kubevela/apis/types"
icontext "github.com/oam-dev/kubevela/pkg/config/context"
"github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/cue/script"
)
// NacosConfig defines the nacos output
type NacosConfig struct {
Endpoint ConfigRef `json:"endpoint"`
// Format defines the format in which Data will be output.
Format string `json:"format"`
Metadata NacosConfigMetadata `json:"metadata"`
}
// NacosConfigMetadata the metadata of the nacos config
type NacosConfigMetadata struct {
DataID string `json:"dataId"`
Group string `json:"group"`
NamespaceID string `json:"namespaceId"`
AppName string `json:"appName,omitempty"`
Tenant string `json:"tenant,omitempty"`
Tag string `json:"tag,omitempty"`
}
// NacosData merge the nacos endpoint config and the rendered data
type NacosData struct {
NacosConfig
Content []byte `json:"-"`
Client config_client.IConfigClient `json:"-"`
}
// parseNacosConfig parse the nacos server config
func parseNacosConfig(templateField *value.Value, wc *ExpandedWriterConfig) {
nacos, _ := templateField.LookupValue("nacos")
if nacos != nil {
format, err := nacos.GetString("format")
if err != nil && !cue.IsFieldNotExist(err) {
klog.Warningf("fail to get the format from the nacos config: %s", err.Error())
}
endpoint, err := nacos.GetString("endpoint", "name")
if err != nil && !cue.IsFieldNotExist(err) {
klog.Warningf("fail to get the endpoint name from the nacos config: %s", err.Error())
}
wc.Nacos = &NacosConfig{
Format: format,
Endpoint: ConfigRef{
Name: endpoint,
},
}
}
}
func renderNacos(config *NacosConfig, template script.CUE, context icontext.ConfigRenderContext, properties map[string]interface{}) (*NacosData, error) {
nacos, err := template.RunAndOutput(context, properties, "template", "nacos")
if err != nil {
return nil, err
}
format, err := nacos.GetString("format")
if err != nil {
format = config.Format
}
var nacosData NacosData
if err := nacos.UnmarshalTo(&nacosData); err != nil {
return nil, err
}
content, err := nacos.LookupValue("content")
if err != nil {
return nil, err
}
out, err := encodingOutput(content, format)
if err != nil {
return nil, err
}
nacosData.Content = out
if nacosData.Endpoint.Namespace == "" {
nacosData.Endpoint.Namespace = types.DefaultKubeVelaNS
}
return &nacosData, nil
}
func (n *NacosData) write(ctx context.Context, configReader icontext.ReadConfigProvider) (err error) {
defer func() {
if rec := recover(); rec != nil {
err = fmt.Errorf("panic when writing the data to nacos:%v", rec)
debug.PrintStack()
}
}()
// the config of the nacos server saving in the default system namespace
config, err := configReader(ctx, n.Endpoint.Namespace, n.Endpoint.Name)
if err != nil {
return fmt.Errorf("fail to read the config of the nacos server:%w", err)
}
readString := func(data map[string]interface{}, key string) string {
if v, ok := data[key]; ok {
str, _ := v.(string)
return str
}
return ""
}
readUint64 := func(data map[string]interface{}, key string) uint64 {
if v, ok := data[key]; ok {
vu, _ := v.(float64)
return uint64(vu)
}
return 0
}
readBool := func(data map[string]interface{}, key string) bool {
if v, ok := data[key]; ok {
vu, _ := v.(bool)
return vu
}
return false
}
var nacosParam vo.NacosClientParam
if serverConfigs, ok := config["servers"]; ok {
var servers []constant.ServerConfig
serverEndpoints, _ := serverConfigs.([]interface{})
for _, s := range serverEndpoints {
sm, ok := s.(map[string]interface{})
if ok && sm != nil {
servers = append(servers, constant.ServerConfig{
IpAddr: readString(sm, "ipAddr"),
Port: readUint64(sm, "port"),
GrpcPort: readUint64(sm, "grpcPort"),
})
}
}
nacosParam.ServerConfigs = servers
}
// Discover the server endpoint
if clientConfigs, ok := config["client"]; ok {
client, _ := clientConfigs.(map[string]interface{})
if client != nil {
nacosParam.ClientConfig = constant.NewClientConfig(
constant.WithEndpoint(readString(client, "endpoint")),
constant.WithAppName(n.Metadata.AppName),
constant.WithNamespaceId(n.Metadata.NamespaceID),
constant.WithUsername(readString(client, "username")),
constant.WithPassword(readString(client, "password")),
constant.WithRegionId(readString(client, "regionId")),
constant.WithOpenKMS(readBool(client, "openKMS")),
constant.WithAccessKey(readString(client, "accessKey")),
constant.WithSecretKey(readString(client, "secretKey")),
)
}
}
// The mock client creates on the outer.
if n.Client == nil {
nacosClient, err := clients.NewConfigClient(nacosParam)
if err != nil {
return err
}
defer nacosClient.CloseClient()
n.Client = nacosClient
}
_, err = n.Client.PublishConfig(vo.ConfigParam{
DataId: n.Metadata.DataID,
Group: n.Metadata.Group,
Content: string(n.Content),
AppName: n.Metadata.AppName,
Tag: n.Metadata.Tag,
Type: strings.ToLower(n.Format),
})
if err != nil {
return fmt.Errorf("fail to publish the config to the nacos server:%w", err)
}
return nil
}

View File

@ -0,0 +1,146 @@
/*
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 writer
import (
"context"
"errors"
"testing"
"github.com/golang/mock/gomock"
"github.com/nacos-group/nacos-sdk-go/v2/vo"
"github.com/stretchr/testify/require"
"github.com/kubevela/workflow/pkg/cue/model/value"
configcontext "github.com/oam-dev/kubevela/pkg/config/context"
"github.com/oam-dev/kubevela/pkg/cue/script"
nacosmock "github.com/oam-dev/kubevela/test/mock/nacos"
)
func TestNacosWriter(t *testing.T) {
r := require.New(t)
v, err := value.NewValue(`
nacos: {
endpoint: {
name: "test-nacos-server"
}
format: "json"
}
`, nil, "")
r.Equal(err, nil)
ewc := &ExpandedWriterConfig{}
parseNacosConfig(v, ewc)
r.Equal(ewc.Nacos.Endpoint.Name, "test-nacos-server")
r.Equal(ewc.Nacos.Format, "json")
renderContext := configcontext.ConfigRenderContext{Name: "nacos-config", Namespace: "vela"}
data, err := renderNacos(ewc.Nacos, script.CUE(`
template: {
nacos: {
// The endpoint can not references the parameter.
endpoint: {
// Users must create a config base the nacos-server template firstly.
name: "test-nacos-server"
}
format: parameter.contentType
// could references the parameter
metadata: {
dataId: parameter.dataId
group: parameter.group
if parameter.appName != _|_ {
appName: parameter.appName
}
if parameter.namespaceId != _|_ {
namespaceId: parameter.namespaceId
}
if parameter.tenant != _|_ {
tenant: parameter.tenant
}
if parameter.tag != _|_ {
tag: parameter.tag
}
}
content: parameter.content
}
parameter: {
// +usage=Configuration ID
dataId: string
// +usage=Configuration group
group: *"DEFAULT_GROUP" | string
// +usage=The configuration content.
content: {
...
}
contentType: *"json" | "yaml" | "properties" | "toml"
// +usage=The app name of the configuration
appName?: string
// +usage=The namespaceId of the configuration
namespaceId?: string
// +usage=The tenant, corresponding to the namespace ID field of Nacos
tenant?: string
// +usage=The tag of the configuration
tag?: string
}
}
`), renderContext, map[string]interface{}{
"dataId": "hello",
"content": map[string]interface{}{
"c1": 1,
},
"contentType": "properties",
"appName": "appName",
"namespaceId": "namespaceId",
"tenant": "tenant",
"tag": "tag",
})
r.Equal(err, nil)
ctl := gomock.NewController(t)
nacosClient := nacosmock.NewMockIConfigClient(ctl)
data.Client = nacosClient
nacosClient.EXPECT().PublishConfig(gomock.Eq(vo.ConfigParam{
DataId: "hello",
Group: "DEFAULT_GROUP",
Content: "c1 = 1\n",
Tag: "tag",
AppName: "appName",
Type: "properties",
})).Return(true, nil)
err = data.write(context.TODO(), func(ctx context.Context, namespace, name string) (map[string]interface{}, error) {
if name == "test-nacos-server" {
return map[string]interface{}{
"servers": []interface{}{
map[string]interface{}{
"ipAddr": "127.0.0.1",
"port": 8849,
},
},
"client": map[string]interface{}{
"endpoint": "",
"username": "",
"password": "",
"accessKey": "accessKey",
"secretKey": "secretKey",
},
}, nil
}
return nil, errors.New("config not found")
})
r.Equal(err, nil)
}

165
pkg/config/writer/writer.go Normal file
View File

@ -0,0 +1,165 @@
/*
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 writer
import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"
"github.com/magiconair/properties"
"github.com/pelletier/go-toml"
"gopkg.in/yaml.v2"
"k8s.io/klog"
"github.com/kubevela/workflow/pkg/cue/model/value"
icontext "github.com/oam-dev/kubevela/pkg/config/context"
"github.com/oam-dev/kubevela/pkg/cue/script"
)
// ExpandedWriterConfig define the supported output ways.
type ExpandedWriterConfig struct {
Nacos *NacosConfig `json:"nacos"`
}
// ExpandedWriterData the data for the expanded writer
type ExpandedWriterData struct {
Nacos *NacosData `json:"nacos"`
}
// ConfigRef reference a config secret, it must be system scope.
type ConfigRef struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}
// ParseExpandedWriterConfig parse the expanded writer config from the template value
func ParseExpandedWriterConfig(template *value.Value) ExpandedWriterConfig {
var ewc = ExpandedWriterConfig{}
parseNacosConfig(template, &ewc)
// parse the other writer configs
return ewc
}
// RenderForExpandedWriter render the configuration for all expanded writers
func RenderForExpandedWriter(ewc ExpandedWriterConfig, template script.CUE, context icontext.ConfigRenderContext, properties map[string]interface{}) (*ExpandedWriterData, error) {
var ewd = ExpandedWriterData{}
var err error
if ewc.Nacos != nil {
ewd.Nacos, err = renderNacos(ewc.Nacos, template, context, properties)
if err != nil {
return nil, err
}
klog.Info("the config render to nacos context successfully")
}
return &ewd, nil
}
// Write write the config by the all writers
func Write(ctx context.Context, ewd *ExpandedWriterData, ri icontext.ReadConfigProvider) (list []error) {
if ewd.Nacos != nil {
if err := ewd.Nacos.write(ctx, ri); err != nil {
list = append(list, err)
} else {
klog.Info("the config write to the nacos successfully")
}
}
return
}
// encodingOutput support the json、toml、xml、properties and yaml formats.
func encodingOutput(input *value.Value, format string) ([]byte, error) {
var data = make(map[string]interface{})
if err := input.UnmarshalTo(&data); err != nil {
return nil, err
}
switch strings.ToLower(format) {
case "json":
return json.Marshal(data)
case "toml":
return toml.Marshal(data)
case "properties":
var kv = map[string]string{}
if err := convertMap2PropertiesKV("", data, kv); err != nil {
return nil, err
}
return []byte(properties.LoadMap(kv).String()), nil
default:
return yaml.Marshal(data)
}
}
func convertMap2PropertiesKV(last string, input map[string]interface{}, result map[string]string) error {
interface2str := func(key string, v interface{}, result map[string]string) (string, error) {
switch t := v.(type) {
case string:
return t, nil
case bool:
return fmt.Sprintf("%t", t), nil
case int64, int, int32:
return fmt.Sprintf("%d", t), nil
case float64, float32:
return fmt.Sprintf("%v", t), nil
case map[string]interface{}:
if err := convertMap2PropertiesKV(key, t, result); err != nil {
return "", err
}
return "", nil
default:
return fmt.Sprintf("%v", t), nil
}
}
for k, v := range input {
key := k
if last != "" {
key = fmt.Sprintf("%s.%s", last, k)
}
switch t := v.(type) {
case string, bool, int64, int, int32, float32, float64, map[string]interface{}:
v, err := interface2str(key, t, result)
if err != nil {
return err
}
if v != "" {
result[key] = v
}
case []interface{}, []string, []int64, []float64, []map[string]interface{}:
var ints []string
s := reflect.ValueOf(t)
for i := 0; i < s.Len(); i++ {
re, err := interface2str(fmt.Sprintf("%s.%d", key, i), s.Index(i).Interface(), result)
if err != nil {
return err
}
if re != "" {
ints = append(ints, re)
}
}
if len(ints) > 0 {
result[key] = strings.Join(ints, ",")
}
default:
return fmt.Errorf("the value type of %s(%T) can not be supported", key, t)
}
}
return nil
}

View File

@ -0,0 +1,88 @@
/*
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 writer
import (
"testing"
"github.com/kubevela/workflow/pkg/cue/model/value"
"github.com/stretchr/testify/require"
)
func TestConvertMap2KV(t *testing.T) {
r := require.New(t)
re := map[string]string{}
err := convertMap2PropertiesKV("", map[string]interface{}{
"s": "s",
"n": 1,
"nn": 1.5,
"b": true,
"m": map[string]interface{}{
"s": "s",
"b": false,
},
"aa": []string{"a", "a"},
"ai": []int64{1, 2},
"ar": []map[string]interface{}{{
"s2": "s2",
}},
}, re)
r.Equal(err, nil)
r.Equal(re, map[string]string{
"s": "s",
"n": "1",
"nn": "1.5",
"b": "true",
"m.s": "s",
"m.b": "false",
"aa": "a,a",
"ai": "1,2",
"ar.0.s2": "s2",
})
}
func TestEncodingOutput(t *testing.T) {
r := require.New(t)
testValue := `
context: {
key1: "hello"
key2: 2
key3: true
key4: 4.4
key5: ["hello"]
key6: [{"hello": 1}]
key7: [1, 2]
key8: [1.2, 1]
key9: {key10: [{"wang": true}]}
}
`
v, err := value.NewValue(testValue, nil, "")
r.Equal(err, nil)
_, err = encodingOutput(v, "yaml")
r.Equal(err, nil)
_, err = encodingOutput(v, "properties")
r.Equal(err, nil)
_, err = encodingOutput(v, "toml")
r.Equal(err, nil)
json, err := encodingOutput(v, "json")
r.Equal(err, nil)
r.Equal(string(json), `{"context":{"key1":"hello","key2":2,"key3":true,"key4":4.4,"key5":["hello"],"key6":[{"hello":1}],"key7":[1,2],"key8":[1.2,1],"key9":{"key10":[{"wang":true}]}}}`)
}

View File

@ -30,8 +30,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"github.com/kubevela/workflow/pkg/cue/packages"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@ -48,7 +46,6 @@ import (
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
pd *packages.PackageDiscover
Scheme *runtime.Scheme
record event.Recorder
options
@ -94,7 +91,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
def := utils.NewCapabilityComponentDef(&componentDefinition)
// Store the parameter of componentDefinition to configMap
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, r.pd, req.Namespace, req.Name, defRev.Name)
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, req.Namespace, req.Name, defRev.Name)
if err != nil {
klog.InfoS("Could not capability in ConfigMap", "err", err)
r.record.Event(&(componentDefinition), event.Warning("Could not store capability in ConfigMap", err))
@ -145,7 +142,6 @@ func Setup(mgr ctrl.Manager, args oamctrl.Args) error {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: args.DiscoveryMapper,
pd: args.PackageDiscover,
options: parseOptions(args),
}
return r.SetupWithManager(mgr)

View File

@ -33,8 +33,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/kubevela/workflow/pkg/cue/packages"
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
)
@ -87,14 +85,11 @@ var _ = BeforeSuite(func(done Done) {
Expect(err).ToNot(HaveOccurred())
_, err = dm.Refresh()
Expect(err).ToNot(HaveOccurred())
pd, err := packages.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
r = Reconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: dm,
pd: pd,
options: options{
defRevLimit: defRevisionLimit,
},

View File

@ -30,8 +30,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"github.com/kubevela/workflow/pkg/cue/packages"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@ -48,7 +46,6 @@ import (
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
pd *packages.PackageDiscover
Scheme *runtime.Scheme
record event.Recorder
defRevLimit int
@ -97,7 +94,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
def := utils.NewCapabilityPolicyDef(&policyDefinition)
def.Name = req.NamespacedName.Name
// Store the parameter of policyDefinition to configMap
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, r.pd, req.Namespace, req.Name, defRev.Name)
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, req.Namespace, req.Name, defRev.Name)
if err != nil {
klog.InfoS("Could not capability in ConfigMap", "err", err)
r.record.Event(&(policyDefinition), event.Warning("Could not store capability in ConfigMap", err))
@ -150,7 +147,6 @@ func Setup(mgr ctrl.Manager, args oamctrl.Args) error {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: args.DiscoveryMapper,
pd: args.PackageDiscover,
defRevLimit: args.DefRevisionLimit,
concurrentReconciles: args.ConcurrentReconciles,
ignoreDefNoCtrlReq: args.IgnoreDefinitionWithoutControllerRequirement,

View File

@ -34,8 +34,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/kubevela/workflow/pkg/cue/packages"
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
)
@ -84,8 +82,6 @@ var _ = BeforeSuite(func(done Done) {
})
Expect(err).ToNot(HaveOccurred())
pd, err := packages.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
dm, err := discoverymapper.New(mgr.GetConfig())
Expect(err).ToNot(HaveOccurred())
_, err = dm.Refresh()
@ -95,7 +91,6 @@ var _ = BeforeSuite(func(done Done) {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: dm,
pd: pd,
defRevLimit: defRevisionLimit,
}
Expect(r.SetupWithManager(mgr)).ToNot(HaveOccurred())

View File

@ -30,8 +30,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"github.com/kubevela/workflow/pkg/cue/packages"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@ -48,7 +46,6 @@ import (
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
pd *packages.PackageDiscover
Scheme *runtime.Scheme
record event.Recorder
options
@ -101,7 +98,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
def := utils.NewCapabilityTraitDef(&traitDefinition)
def.Name = req.NamespacedName.Name
// Store the parameter of traitDefinition to configMap
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, r.pd, req.Namespace, req.Name, defRev.Name)
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, req.Namespace, req.Name, defRev.Name)
if err != nil {
klog.InfoS("Could not store capability in ConfigMap", "err", err)
r.record.Event(&(traitDefinition), event.Warning("Could not store capability in ConfigMap", err))
@ -153,7 +150,6 @@ func Setup(mgr ctrl.Manager, args oamctrl.Args) error {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: args.DiscoveryMapper,
pd: args.PackageDiscover,
options: parseOptions(args),
}
return r.SetupWithManager(mgr)

View File

@ -33,8 +33,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/kubevela/workflow/pkg/cue/packages"
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
)
@ -83,8 +81,6 @@ var _ = BeforeSuite(func(done Done) {
})
Expect(err).ToNot(HaveOccurred())
pd, err := packages.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
dm, err := discoverymapper.New(mgr.GetConfig())
Expect(err).ToNot(HaveOccurred())
_, err = dm.Refresh()
@ -94,7 +90,6 @@ var _ = BeforeSuite(func(done Done) {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: dm,
pd: pd,
options: options{
defRevLimit: defRevisionLimit,
},

View File

@ -33,8 +33,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/kubevela/workflow/pkg/cue/packages"
oamCore "github.com/oam-dev/kubevela/apis/core.oam.dev"
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
)
@ -83,8 +81,6 @@ var _ = BeforeSuite(func(done Done) {
})
Expect(err).ToNot(HaveOccurred())
pd, err := packages.NewPackageDiscover(cfg)
Expect(err).ToNot(HaveOccurred())
dm, err := discoverymapper.New(mgr.GetConfig())
Expect(err).ToNot(HaveOccurred())
_, err = dm.Refresh()
@ -94,7 +90,6 @@ var _ = BeforeSuite(func(done Done) {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: dm,
pd: pd,
options: options{
defRevLimit: defRevisionLimit,
},

View File

@ -30,8 +30,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"github.com/kubevela/workflow/pkg/cue/packages"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/condition"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
@ -48,7 +46,6 @@ import (
type Reconciler struct {
client.Client
dm discoverymapper.DiscoveryMapper
pd *packages.PackageDiscover
Scheme *runtime.Scheme
record event.Recorder
options
@ -101,7 +98,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
def := utils.NewCapabilityStepDef(&wfStepDefinition)
def.Name = req.NamespacedName.Name
// Store the parameter of stepDefinition to configMap
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, r.pd, req.Namespace, req.Name, defRev.Name)
cmName, err := def.StoreOpenAPISchema(ctx, r.Client, req.Namespace, req.Name, defRev.Name)
if err != nil {
klog.InfoS("Could not store capability in ConfigMap", "err", err)
r.record.Event(&(wfStepDefinition), event.Warning("Could not store capability in ConfigMap", err))
@ -153,7 +150,6 @@ func Setup(mgr ctrl.Manager, args oamctrl.Args) error {
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
dm: args.DiscoveryMapper,
pd: args.PackageDiscover,
options: parseOptions(args),
}
return r.SetupWithManager(mgr)

View File

@ -22,10 +22,8 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"cuelang.org/go/cue/cuecontext"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"
"gopkg.in/src-d/go-git.v4"
@ -36,16 +34,12 @@ import (
"k8s.io/utils/pointer"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kubevela/workflow/pkg/cue/model/value"
"github.com/kubevela/workflow/pkg/cue/packages"
commontypes "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"
"github.com/oam-dev/kubevela/pkg/appfile/helm"
velacue "github.com/oam-dev/kubevela/pkg/cue"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/cue/script"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/terraform"
@ -131,12 +125,12 @@ func NewCapabilityComponentDef(componentDefinition *v1beta1.ComponentDefinition)
}
// GetOpenAPISchema gets OpenAPI v3 schema by WorkloadDefinition name
func (def *CapabilityComponentDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) {
func (def *CapabilityComponentDefinition) GetOpenAPISchema(name string) ([]byte, error) {
capability, err := appfile.ConvertTemplateJSON2Object(name, def.ComponentDefinition.Spec.Extension, def.ComponentDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert ComponentDefinition to Capability Object")
}
return getOpenAPISchema(capability, pd)
return getOpenAPISchema(capability)
}
// GetOpenAPISchemaFromTerraformComponentDefinition gets OpenAPI v3 schema by WorkloadDefinition name
@ -354,8 +348,7 @@ func GetKubeSchematicOpenAPISchema(params []commontypes.KubeParameter) ([]byte,
}
// StoreOpenAPISchema stores OpenAPI v3 schema in ConfigMap from WorkloadDefinition
func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client,
pd *packages.PackageDiscover, namespace, name, revName string) (string, error) {
func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name, revName string) (string, error) {
var jsonSchema []byte
var err error
switch def.WorkloadType {
@ -376,7 +369,7 @@ func (def *CapabilityComponentDefinition) StoreOpenAPISchema(ctx context.Context
}
jsonSchema, err = GetOpenAPISchemaFromTerraformComponentDefinition(configuration)
default:
jsonSchema, err = def.GetOpenAPISchema(pd, name)
jsonSchema, err = def.GetOpenAPISchema(name)
}
if err != nil {
return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
@ -440,23 +433,23 @@ func NewCapabilityTraitDef(traitdefinition *v1beta1.TraitDefinition) CapabilityT
}
// GetOpenAPISchema gets OpenAPI v3 schema by TraitDefinition name
func (def *CapabilityTraitDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) {
func (def *CapabilityTraitDefinition) GetOpenAPISchema(name string) ([]byte, error) {
capability, err := appfile.ConvertTemplateJSON2Object(name, def.TraitDefinition.Spec.Extension, def.TraitDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert WorkloadDefinition to Capability Object")
}
return getOpenAPISchema(capability, pd)
return getOpenAPISchema(capability)
}
// StoreOpenAPISchema stores OpenAPI v3 schema from TraitDefinition in ConfigMap
func (def *CapabilityTraitDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, pd *packages.PackageDiscover, namespace, name string, revName string) (string, error) {
func (def *CapabilityTraitDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string, revName string) (string, error) {
var jsonSchema []byte
var err error
switch def.DefCategoryType {
case util.KubeDef: // Kube template
jsonSchema, err = GetKubeSchematicOpenAPISchema(def.Kube.Parameters)
default: // CUE template
jsonSchema, err = def.GetOpenAPISchema(pd, name)
jsonSchema, err = def.GetOpenAPISchema(name)
}
if err != nil {
return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
@ -513,20 +506,20 @@ func NewCapabilityStepDef(stepdefinition *v1beta1.WorkflowStepDefinition) Capabi
}
// GetOpenAPISchema gets OpenAPI v3 schema by StepDefinition name
func (def *CapabilityStepDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) {
func (def *CapabilityStepDefinition) GetOpenAPISchema(name string) ([]byte, error) {
capability, err := appfile.ConvertTemplateJSON2Object(name, nil, def.StepDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert WorkflowStepDefinition to Capability Object")
}
return getOpenAPISchema(capability, pd)
return getOpenAPISchema(capability)
}
// StoreOpenAPISchema stores OpenAPI v3 schema from StepDefinition in ConfigMap
func (def *CapabilityStepDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, pd *packages.PackageDiscover, namespace, name string, revName string) (string, error) {
func (def *CapabilityStepDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name string, revName string) (string, error) {
var jsonSchema []byte
var err error
jsonSchema, err = def.GetOpenAPISchema(pd, name)
jsonSchema, err = def.GetOpenAPISchema(name)
if err != nil {
return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
}
@ -582,21 +575,20 @@ func NewCapabilityPolicyDef(policydefinition *v1beta1.PolicyDefinition) Capabili
}
// GetOpenAPISchema gets OpenAPI v3 schema by StepDefinition name
func (def *CapabilityPolicyDefinition) GetOpenAPISchema(pd *packages.PackageDiscover, name string) ([]byte, error) {
func (def *CapabilityPolicyDefinition) GetOpenAPISchema(name string) ([]byte, error) {
capability, err := appfile.ConvertTemplateJSON2Object(name, nil, def.PolicyDefinition.Spec.Schematic)
if err != nil {
return nil, fmt.Errorf("failed to convert WorkflowStepDefinition to Capability Object")
}
return getOpenAPISchema(capability, pd)
return getOpenAPISchema(capability)
}
// StoreOpenAPISchema stores OpenAPI v3 schema from StepDefinition in ConfigMap
func (def *CapabilityPolicyDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client,
pd *packages.PackageDiscover, namespace, name, revName string) (string, error) {
func (def *CapabilityPolicyDefinition) StoreOpenAPISchema(ctx context.Context, k8sClient client.Client, namespace, name, revName string) (string, error) {
var jsonSchema []byte
var err error
jsonSchema, err = def.GetOpenAPISchema(pd, name)
jsonSchema, err = def.GetOpenAPISchema(name)
if err != nil {
return "", fmt.Errorf("failed to generate OpenAPI v3 JSON schema for capability %s: %w", def.Name, err)
}
@ -694,115 +686,18 @@ func (def *CapabilityBaseDefinition) CreateOrUpdateConfigMap(ctx context.Context
}
// getOpenAPISchema is the main function for GetDefinition API
func getOpenAPISchema(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) {
openAPISchema, err := generateOpenAPISchemaFromCapabilityParameter(capability, pd)
func getOpenAPISchema(capability types.Capability) ([]byte, error) {
cueTemplate, err := script.PrepareTemplateCUEScript([]byte(capability.CueTemplate))
if err != nil {
return nil, err
}
schema, err := ConvertOpenAPISchema2SwaggerObject(openAPISchema)
schema, err := cueTemplate.ParsePropertiesToSchema()
if err != nil {
return nil, err
}
FixOpenAPISchema("", schema)
parameter, err := schema.MarshalJSON()
if err != nil {
return nil, err
}
return parameter, nil
}
// generateOpenAPISchemaFromCapabilityParameter returns the parameter of a definition in cue.Value format
func generateOpenAPISchemaFromCapabilityParameter(capability types.Capability, pd *packages.PackageDiscover) ([]byte, error) {
ctx := cuecontext.New()
template, err := PrepareParameterCue(capability.Name, capability.CueTemplate)
if err != nil {
if errors.As(err, &ErrNoSectionParameterInCue{}) {
// return OpenAPI with empty object parameter, making it possible to generate ConfigMap
return common.GenOpenAPI(ctx.CompileString(""))
}
return nil, err
}
template += velacue.BaseTemplate
val, err := value.NewValue(template, pd, "")
if err != nil {
return nil, err
}
return common.GenOpenAPI(val.CueValue())
}
// GenerateOpenAPISchemaFromDefinition returns the parameter of a definition
func GenerateOpenAPISchemaFromDefinition(definitionName, cueTemplate string) ([]byte, error) {
capability := types.Capability{
Name: definitionName,
CueTemplate: cueTemplate,
}
return generateOpenAPISchemaFromCapabilityParameter(capability, nil)
}
// PrepareParameterCue cuts `parameter` section form definition .cue file
func PrepareParameterCue(capabilityName, capabilityTemplate string) (string, error) {
var template string
var withParameterFlag bool
r := regexp.MustCompile(`[[:space:]]*parameter:[[:space:]]*`)
trimRe := regexp.MustCompile(`\s+`)
for _, text := range strings.Split(capabilityTemplate, "\n") {
if r.MatchString(text) {
// a variable has to be refined as a definition which starts with "#"
// text may be start with space or tab, we should clean up text
text = fmt.Sprintf("parameter: #parameter\n#%s", trimRe.ReplaceAllString(text, ""))
withParameterFlag = true
}
template += fmt.Sprintf("%s\n", text)
}
if !withParameterFlag {
return "", ErrNoSectionParameterInCue{capName: capabilityName}
}
return template, nil
}
// FixOpenAPISchema fixes tainted `description` filed, missing of title `field`.
func FixOpenAPISchema(name string, schema *openapi3.Schema) {
t := schema.Type
switch t {
case "object":
for k, v := range schema.Properties {
s := v.Value
FixOpenAPISchema(k, s)
}
case "array":
if schema.Items != nil {
FixOpenAPISchema("", schema.Items.Value)
}
}
if name != "" {
schema.Title = name
}
description := schema.Description
if strings.Contains(description, appfile.UsageTag) {
description = strings.Split(description, appfile.UsageTag)[1]
}
if strings.Contains(description, appfile.ShortTag) {
description = strings.Split(description, appfile.ShortTag)[0]
description = strings.TrimSpace(description)
}
schema.Description = description
}
// ConvertOpenAPISchema2SwaggerObject converts OpenAPI v2 JSON schema to Swagger Object
func ConvertOpenAPISchema2SwaggerObject(data []byte) (*openapi3.Schema, error) {
swagger, err := openapi3.NewLoader().LoadFromData(data)
if err != nil {
return nil, err
}
schemaRef, ok := swagger.Components.Schemas[process.ParameterFieldName]
if !ok {
return nil, errors.New(util.ErrGenerateOpenAPIV2JSONSchemaForCapability)
}
return schemaRef.Value, nil
}

View File

@ -111,7 +111,7 @@ spec:
def := &CapabilityComponentDefinition{Name: componentDefinitionName, ComponentDefinition: *componentDefinition.DeepCopy()}
By("Test GetOpenAPISchema")
schema, err := def.GetOpenAPISchema(pd, namespace)
schema, err := def.GetOpenAPISchema(namespace)
Expect(err).Should(BeNil())
Expect(schema).Should(Not(BeNil()))
})

View File

@ -19,20 +19,16 @@ package utils
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/getkin/kin-openapi/openapi3"
"github.com/google/go-cmp/cmp"
"gotest.tools/assert"
"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/appfile"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/oam/util"
)
@ -182,7 +178,7 @@ parameter: [string]: string
},
}
capability, _ := appfile.ConvertTemplateJSON2Object(tc.name, nil, schematic)
schema, err := getOpenAPISchema(capability, pd)
schema, err := getOpenAPISchema(capability)
if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\ngetOpenAPISchema(...): -want error, +got error:\n%s", tc.reason, diff)
}
@ -193,37 +189,6 @@ parameter: [string]: string
}
}
func TestFixOpenAPISchema(t *testing.T) {
cases := map[string]struct {
inputFile string
fixedFile string
}{
"StandardWorkload": {
inputFile: "webservice.json",
fixedFile: "webserviceFixed.json",
},
"ShortTagJson": {
inputFile: "shortTagSchema.json",
fixedFile: "shortTagSchemaFixed.json",
},
"EmptyArrayJson": {
inputFile: "arrayWithoutItemsSchema.json",
fixedFile: "arrayWithoutItemsSchemaFixed.json",
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
swagger, _ := openapi3.NewLoader().LoadFromFile(filepath.Join(TestDir, tc.inputFile))
schema := swagger.Components.Schemas[process.ParameterFieldName].Value
FixOpenAPISchema("", schema)
fixedSchema, _ := schema.MarshalJSON()
expectedSchema, _ := os.ReadFile(filepath.Join(TestDir, tc.fixedFile))
assert.Equal(t, string(fixedSchema), string(expectedSchema))
})
}
}
func TestNewCapabilityComponentDef(t *testing.T) {
terraform := &common.Terraform{
Configuration: "test",

View File

@ -24,5 +24,6 @@ context: {
name: string
value: string
}]
...
}
`

View File

@ -158,3 +158,8 @@ func RetrieveComments(value cue.Value) (string, string, string, bool) {
}
return short, usage, alias, ignore
}
// IsFieldNotExist check whether the error type is the field not found
func IsFieldNotExist(err error) bool {
return strings.Contains(err.Error(), "not exist")
}

96
pkg/cue/script/schema.go Normal file
View File

@ -0,0 +1,96 @@
/*
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 script
import (
"errors"
"fmt"
"strings"
"github.com/getkin/kin-openapi/openapi3"
"github.com/oam-dev/kubevela/pkg/appfile"
"github.com/oam-dev/kubevela/pkg/cue/process"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
// ParsePropertiesToSchema parse the properties in cue script to the openapi schema
// Read the template.parameter field
func (c CUE) ParsePropertiesToSchema() (*openapi3.Schema, error) {
val, err := c.ParseToValue(false)
if err != nil {
return nil, err
}
template, err := val.LookupValue("template")
if err != nil {
return nil, fmt.Errorf("%w cue script: %s", err, c)
}
data, err := common.GenOpenAPI(template.CueValue())
if err != nil {
return nil, err
}
schema, err := ConvertOpenAPISchema2SwaggerObject(data)
if err != nil {
return nil, err
}
FixOpenAPISchema("", schema)
return schema, nil
}
// FixOpenAPISchema fixes tainted `description` filed, missing of title `field`.
func FixOpenAPISchema(name string, schema *openapi3.Schema) {
t := schema.Type
switch t {
case "object":
for k, v := range schema.Properties {
s := v.Value
FixOpenAPISchema(k, s)
}
case "array":
if schema.Items != nil {
FixOpenAPISchema("", schema.Items.Value)
}
}
if name != "" {
schema.Title = name
}
description := schema.Description
if strings.Contains(description, appfile.UsageTag) {
description = strings.Split(description, appfile.UsageTag)[1]
}
if strings.Contains(description, appfile.ShortTag) {
description = strings.Split(description, appfile.ShortTag)[0]
description = strings.TrimSpace(description)
}
schema.Description = description
}
// ConvertOpenAPISchema2SwaggerObject converts OpenAPI v2 JSON schema to Swagger Object
func ConvertOpenAPISchema2SwaggerObject(data []byte) (*openapi3.Schema, error) {
swagger, err := openapi3.NewLoader().LoadFromData(data)
if err != nil {
return nil, err
}
schemaRef, ok := swagger.Components.Schemas[process.ParameterFieldName]
if !ok {
return nil, errors.New(util.ErrGenerateOpenAPIV2JSONSchemaForCapability)
}
return schemaRef.Value, nil
}

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 script
import (
"os"
"path/filepath"
"testing"
"github.com/getkin/kin-openapi/openapi3"
"gotest.tools/assert"
"github.com/oam-dev/kubevela/pkg/cue/process"
)
const TestDir = "testdata"
func TestFixOpenAPISchema(t *testing.T) {
cases := map[string]struct {
inputFile string
fixedFile string
}{
"StandardWorkload": {
inputFile: "webservice.json",
fixedFile: "webserviceFixed.json",
},
"ShortTagJson": {
inputFile: "shortTagSchema.json",
fixedFile: "shortTagSchemaFixed.json",
},
"EmptyArrayJson": {
inputFile: "arrayWithoutItemsSchema.json",
fixedFile: "arrayWithoutItemsSchemaFixed.json",
},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
swagger, _ := openapi3.NewLoader().LoadFromFile(filepath.Join(TestDir, tc.inputFile))
schema := swagger.Components.Schemas[process.ParameterFieldName].Value
FixOpenAPISchema("", schema)
fixedSchema, _ := schema.MarshalJSON()
expectedSchema, _ := os.ReadFile(filepath.Join(TestDir, tc.fixedFile))
assert.Equal(t, string(fixedSchema), string(expectedSchema))
})
}
}

228
pkg/cue/script/template.go Normal file
View File

@ -0,0 +1,228 @@
/*
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 script
import (
"encoding/json"
"fmt"
"strings"
"cuelang.org/go/cue/errors"
"github.com/kubevela/workflow/pkg/cue/model/value"
"github.com/oam-dev/kubevela/pkg/cue"
)
// CUE the cue script with the template format
// Like this:
// ------------
// metadata: {}
//
// template: {
// parameter: {}
// output: {}
// }
//
// ------------
type CUE string
// PrepareTemplateCUEScript insert the template path before the parameter field.
// The input maybe includes the `package` keywords, so we can not make it as a value directly.
// Only used to generate the API schema.
func PrepareTemplateCUEScript(content []byte) (*CUE, error) {
cueContent := string(content)
v, err := value.NewValue(cueContent, nil, "")
if err != nil {
return nil, fmt.Errorf("fail to parse the cue script:%w", err)
}
_, err = v.LookupValue("template")
if err != nil {
if cue.IsFieldNotExist(err) {
if p, err := v.LookupValue("parameter"); err == nil {
ps, err := p.String()
if err != nil {
return nil, err
}
cueContent = fmt.Sprintf("template: {\n parameter: {\n%s\n} \n}", ps)
} else if cue.IsFieldNotExist(err) {
cueContent += "\ntemplate: {\n parameter: {} \n}"
}
} else {
return nil, errors.New("the template cue is invalid")
}
}
cue := CUE(cueContent)
return &cue, nil
}
// BuildCUEScriptWithDefaultContext build a cue script instance from a byte array.
func BuildCUEScriptWithDefaultContext(defaultContext []byte, content []byte) CUE {
return CUE(content) + "\n" + CUE(defaultContext)
}
// ParseToValue parse the cue script to cue.Value
// If value.Error() is not nil and the checkAllFields is True, which will return the error.
func (c CUE) ParseToValue(checkAllFields bool) (*value.Value, error) {
// the cue script must be first, it could include the imports
template := string(c) + "\n" + cue.BaseTemplate
v, err := value.NewValue(template, nil, "")
if err != nil {
return nil, fmt.Errorf("fail to parse the template:%w", err)
}
// If the cue script reference the fields in the context, there is a error when we not provide the context struct.
// For the ParsePropertiesToSchema and the ValidateProperties function, we do not need check all fields.
if checkAllFields && v.Error() != nil {
return nil, fmt.Errorf("the template cue is invalid: %w", v.Error())
}
_, err = v.LookupValue("template")
if err != nil {
if v.Error() != nil {
return nil, fmt.Errorf("the template cue is invalid:%w", v.Error())
}
return nil, fmt.Errorf("the template cue must include the template field:%w", err)
}
_, err = v.LookupValue("template", "parameter")
if err != nil {
return nil, fmt.Errorf("the template cue must include the template.parameter field")
}
return v, nil
}
// MergeValues merge the input values to the cue script
// The context variables could be referenced in all fields.
// The parameter only could be referenced in the template area.
func (c CUE) MergeValues(context interface{}, properties map[string]interface{}) (*value.Value, error) {
parameterByte, err := json.Marshal(properties)
if err != nil {
return nil, fmt.Errorf("the parameter is invalid %w", err)
}
contextByte, err := json.Marshal(context)
if err != nil {
return nil, fmt.Errorf("the context is invalid %w", err)
}
var script = strings.Builder{}
_, err = script.WriteString(string(c) + "\n")
if err != nil {
return nil, err
}
if properties != nil {
_, err = script.WriteString(fmt.Sprintf("template: parameter: %s \n", string(parameterByte)))
if err != nil {
return nil, err
}
}
if context != nil {
_, err = script.WriteString(fmt.Sprintf("context: %s \n", string(contextByte)))
if err != nil {
return nil, err
}
}
mergeValue, err := value.NewValue(script.String(), nil, "")
if err != nil {
return nil, err
}
if err := mergeValue.CueValue().Validate(); err != nil {
return nil, fmt.Errorf("fail to validate the merged value %w", err)
}
return mergeValue, nil
}
// RunAndOutput run the cue script and return the values of the specified field.
// The output field must be under the template field.
func (c CUE) RunAndOutput(context interface{}, properties map[string]interface{}, outputField ...string) (*value.Value, error) {
// Validate the properties
if err := c.ValidateProperties(properties); err != nil {
return nil, err
}
render, err := c.MergeValues(context, properties)
if err != nil {
return nil, fmt.Errorf("fail to merge the properties to template %w", err)
}
if render.Error() != nil {
return nil, fmt.Errorf("fail to merge the properties to template %w", render.Error())
}
if len(outputField) == 0 {
outputField = []string{"template", "output"}
}
return render.LookupValue(outputField...)
}
// ValidateProperties validate the input properties by the template
func (c CUE) ValidateProperties(properties map[string]interface{}) error {
template, err := c.ParseToValue(false)
if err != nil {
return err
}
parameter, err := template.LookupValue("template", "parameter")
if err != nil {
return err
}
parameterStr, err := parameter.String()
if err != nil {
return fmt.Errorf("the parameter is invalid %w", err)
}
propertiesByte, err := json.Marshal(properties)
if err != nil {
return fmt.Errorf("the properties is invalid %w", err)
}
newCue := strings.Builder{}
newCue.WriteString(parameterStr + "\n")
newCue.WriteString(string(propertiesByte) + "\n")
value, err := value.NewValue(newCue.String(), nil, "")
if err != nil {
return ConvertFieldError(err)
}
if err := value.CueValue().Validate(); err != nil {
return ConvertFieldError(err)
}
_, err = value.CueValue().MarshalJSON()
if err != nil {
return ConvertFieldError(err)
}
return nil
}
// ParameterError the error report of the parameter field validation
type ParameterError struct {
Name string
Message string
}
// Error return the error message
func (e *ParameterError) Error() string {
return fmt.Sprintf("Field: %s Message: %s", e.Name, e.Message)
}
// ConvertFieldError convert the cue error to the field error
func ConvertFieldError(err error) error {
var cueErr errors.Error
if errors.As(err, &cueErr) {
path := cueErr.Path()
fieldName := path[len(path)-1]
format, args := cueErr.Msg()
message := fmt.Sprintf(format, args...)
if strings.Contains(message, "cannot convert incomplete value") {
message = "This parameter is required"
}
return &ParameterError{
Name: fieldName,
Message: message,
}
}
return err
}

View File

@ -0,0 +1,222 @@
/*
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 script
import (
"fmt"
"strings"
"testing"
"gotest.tools/assert"
)
var templateScript = `
metadata: {
name: "helm-repository"
alias: "Helm Repository"
scope: "system"
sensitive: false
}
template: {
output: {
url: parameter.url
}
parameter: {
// +usage=The public url of the helm chart repository.
url: string
// +usage=The username of basic auth repo.
username: string
// +usage=The password of basic auth repo.
password?: string
// +usage=The ca certificate of helm repository. Please encode this data with base64.
caFile?: string
options: "o1" | "o2"
}
}
`
var templateWithContextScript = `
import (
"strconv"
)
metadata: {
name: "helm-repository"
alias: "Helm Repository"
scope: "system"
sensitive: false
}
template: {
output: {
url: parameter.url
name: context.name
namespace: context.namespace
sensitive: strconv.FormatBool(metadata.sensitive)
}
parameter: {
// +usage=The public url of the helm chart repository.
url: string
// +usage=The username of basic auth repo.
username: string
// +usage=The password of basic auth repo.
password?: string
// +usage=The ca certificate of helm repository. Please encode this data with base64.
caFile?: string
}
}
`
var withPackage = `
package main
const: {
// +usage=The name of the addon application
name: "addon-loki"
}
parameter: {
// global parameters
// +usage=The namespace of the loki to be installed
namespace: *"o11y-system" | string
// +usage=The clusters to install
clusters?: [...string]
// loki parameters
// +usage=Specify the image of loki
image: *"grafana/loki" | string
// +usage=Specify the imagePullPolicy of the image
imagePullPolicy: *"IfNotPresent" | "Never" | "Always"
// +usage=Specify the service type for expose loki. If empty, it will be not exposed.
serviceType: *"ClusterIP" | "NodePort" | "LoadBalancer" | ""
// +usage=Specify the storage size to use. If empty, emptyDir will be used. Otherwise pvc will be used.
storage?: =~"^([1-9][0-9]{0,63})(E|P|T|G|M|K|Ei|Pi|Ti|Gi|Mi|Ki)$"
// +usage=Specify the storage class to use.
storageClassName?: string
// agent parameters
// +usage=Specify the type of log agents, if empty, no agent will be installed
agent: *"" | "vector" | "promtail"
// +usage=Specify the image of promtail
promtailImage: *"grafana/promtail" | string
// +usage=Specify the image of vector
vectorImage: *"timberio/vector:0.24.0-distroless-libc" | string
}
`
var withImport = `
import (
"vela/op"
)
apply: op.#Apply & {
value: parameter.value
cluster: parameter.cluster
}
parameter: {
// +usage=Specify the value of the object
value: {...}
// +usage=Specify the cluster of the object
cluster: *"" | string
}`
func TestMergeValues(t *testing.T) {
var cueScript = CUE(templateScript)
value, err := cueScript.MergeValues(nil, map[string]interface{}{
"url": "hub.docker.com",
"username": "name",
})
assert.Equal(t, err, nil)
output, err := value.LookupValue("template", "output")
assert.Equal(t, err, nil)
var data = map[string]interface{}{}
err = output.UnmarshalTo(&data)
assert.Equal(t, err, nil)
assert.Equal(t, data["url"], "hub.docker.com")
}
func TestRunAndOutput(t *testing.T) {
var cueScript = BuildCUEScriptWithDefaultContext([]byte("context:{namespace:string \n name:string}"), []byte(templateWithContextScript))
output, err := cueScript.RunAndOutput(map[string]interface{}{
"name": "nnn",
"namespace": "ns",
}, map[string]interface{}{
"url": "hub.docker.com",
"username": "test",
"password": "test",
"caFile": "test ca",
})
assert.Equal(t, err, nil)
var data = map[string]interface{}{}
err = output.UnmarshalTo(&data)
assert.Equal(t, err, nil)
assert.Equal(t, data["name"], "nnn")
assert.Equal(t, data["namespace"], "ns")
assert.Equal(t, data["url"], "hub.docker.com")
}
func TestValidateProperties(t *testing.T) {
var cueScript = CUE(templateScript)
// miss the required parameter
err := cueScript.ValidateProperties(map[string]interface{}{
"url": "hub.docker.com",
})
assert.Equal(t, err.(*ParameterError).Message, "This parameter is required")
// wrong the parameter value type
err = cueScript.ValidateProperties(map[string]interface{}{
"url": 1,
"username": "ddd",
})
assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "conflicting values"), true)
assert.Equal(t, strings.Contains(err.(*ParameterError).Name, "url"), true)
// wrong the parameter value
err = cueScript.ValidateProperties(map[string]interface{}{
"url": "ddd",
"username": "ddd",
})
assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "This parameter is required"), true)
assert.Equal(t, strings.Contains(err.(*ParameterError).Name, "options"), true)
// wrong the parameter value and no required value
err = cueScript.ValidateProperties(map[string]interface{}{
"url": "ddd",
"username": "ddd",
"options": "o3",
})
fmt.Println(err.(*ParameterError).Message)
assert.Equal(t, strings.Contains(err.(*ParameterError).Name, "options"), true)
assert.Equal(t, strings.Contains(err.(*ParameterError).Message, "2 errors in empty disjunction"), true)
}
func TestBuildCUEScript(t *testing.T) {
cue, err := PrepareTemplateCUEScript([]byte(withPackage))
assert.Equal(t, err, nil)
schema, err := cue.ParsePropertiesToSchema()
assert.Equal(t, err, nil)
assert.Equal(t, len(schema.Properties), 10)
cue, err = PrepareTemplateCUEScript([]byte(withImport))
assert.Equal(t, err, nil)
_, err = cue.ParsePropertiesToSchema()
assert.Equal(t, err, nil)
}

View File

@ -177,6 +177,11 @@ import (
#SendEmail: email.#Send
#CreateConfig: config.#Create
#DeleteConfig: config.#Delete
#ReadConfig: config.#Read
#ListConfig: config.#List
#Load: oam.#LoadComponets
#LoadInOrder: oam.#LoadComponetsInOrder

View File

@ -0,0 +1,40 @@
#Create: {
#do: "create"
#provider: "config"
name: string
namespace: string
template?: string
config: {
...
}
}
#Delete: {
#do: "delete"
#provider: "config"
name: string
namespace: string
}
#Read: {
#do: "delete"
#provider: "config"
name: string
namespace: string
config: {...}
}
#List: {
#do: "delete"
#provider: "config"
// Must query with the template
template: string
namespace: string
configs: [...{...}]
}

View File

@ -31,12 +31,14 @@ import (
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/features"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/oam/util"
"github.com/oam-dev/kubevela/pkg/utils/common"
)
const (
@ -58,6 +60,7 @@ type applyAction struct {
skipUpdate bool
updateAnnotation bool
dryRun bool
quiet bool
}
// ApplyOption is called before applying state to the object.
@ -104,7 +107,10 @@ type APIApplicator struct {
}
// loggingApply will record a log with desired object applied
func loggingApply(msg string, desired client.Object) {
func loggingApply(msg string, desired client.Object, quiet bool) {
if quiet {
return
}
d, ok := desired.(metav1.Object)
if !ok {
klog.InfoS(msg, "resource", desired.GetObjectKind().GroupVersionKind().String())
@ -166,13 +172,13 @@ func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...
}
if applyAct.skipUpdate {
loggingApply("skip update", desired)
loggingApply("skip update", desired, applyAct.quiet)
return nil
}
switch {
case utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByUpdate) && isUpdatableResource(desired):
loggingApply("updating object", desired)
loggingApply("updating object", desired, applyAct.quiet)
desired.SetResourceVersion(existing.GetResourceVersion())
var options []client.UpdateOption
if applyAct.dryRun {
@ -180,7 +186,7 @@ func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...
}
return errors.Wrapf(a.c.Update(ctx, desired, options...), "cannot update object")
default:
loggingApply("patching object", desired)
loggingApply("patching object", desired, applyAct.quiet)
patch, err := a.patcher.patch(existing, desired, applyAct)
if err != nil {
return errors.Wrap(err, "cannot calculate patch by computing a three way diff")
@ -227,13 +233,20 @@ func createOrGetExisting(ctx context.Context, act *applyAction, c client.Client,
return nil, err
}
}
loggingApply("creating object", desired)
loggingApply("creating object", desired, act.quiet)
if act.dryRun {
return nil, errors.Wrap(c.Create(ctx, desired, client.DryRunAll), "cannot create object")
}
return nil, errors.Wrap(c.Create(ctx, desired), "cannot create object")
}
if desired.GetObjectKind().GroupVersionKind().Kind == "" {
gvk, err := apiutil.GVKForObject(desired, common.Scheme)
if err == nil {
desired.GetObjectKind().SetGroupVersionKind(gvk)
}
}
// allow to create object with only generateName
if desired.GetName() == "" && desired.GetGenerateName() != "" {
return create()
@ -409,6 +422,14 @@ func DryRunAll() ApplyOption {
}
}
// Quiet means disable the logger
func Quiet() ApplyOption {
return func(a *applyAction, existing, _ client.Object) error {
a.quiet = true
return nil
}
}
// isUpdatableResource check whether the resource is updatable
// Resource like v1.Service cannot unset the spec field (the ip spec is filled by service controller)
func isUpdatableResource(desired client.Object) bool {

View File

@ -30,6 +30,7 @@ import (
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
@ -259,6 +260,7 @@ func GenOpenAPI(val cue.Value) (b []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("invalid cue definition to generate open api: %v", r)
debug.PrintStack()
return
}
}()
@ -293,7 +295,7 @@ func RefineParameterValue(val cue.Value) (cue.Value, error) {
case cue.BottomKind:
paramOnlyStr = fmt.Sprintf("#%s: {}", process.ParameterFieldName)
default:
return cue.Value{}, fmt.Errorf("unsupport parameter kind: %s", k.String())
return cue.Value{}, fmt.Errorf("unsupported parameter kind: %s", k.String())
}
paramOnlyVal := cuecontext.New().CompileString(paramOnlyStr)
if paramOnlyVal.Err() != nil {

View File

@ -1,159 +0,0 @@
/*
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 config
import (
"context"
"fmt"
"strings"
tcv1beta1 "github.com/oam-dev/terraform-controller/api/v1beta1"
"github.com/pkg/errors"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"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/apis/types"
"github.com/oam-dev/kubevela/pkg/apiserver/domain/model"
)
const (
errProviderExists = "terraform provider %s for %s already exists"
errDeleteProvider = "failed to delete Terraform Provider %s err: %w"
errCouldNotDeleteProvider = "the Terraform Provider %s could not be disabled because it was created by enabling a Terraform provider or was manually created"
errCheckProviderExistence = "failed to check if Terraform Provider %s exists"
)
// UIParam is the UI parameters from VelaUX for the application
type UIParam struct {
Alias string `json:"alias"`
Description string `json:"description"`
Project string `json:"project"`
}
// CreateApplication creates a new application for the config
func CreateApplication(ctx context.Context, k8sClient client.Client, name, componentType, properties string, ui UIParam) error {
if strings.HasPrefix(componentType, types.TerraformComponentPrefix) {
existed, err := IsTerraformProviderExisted(ctx, k8sClient, name)
if err != nil {
return errors.Wrapf(err, errCheckProviderExistence, name)
}
if existed {
return fmt.Errorf(errProviderExists, name, componentType)
}
}
app := v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: types.DefaultKubeVelaNS,
Annotations: map[string]string{
types.AnnotationConfigAlias: ui.Alias,
types.AnnotationConfigDescription: ui.Description,
},
Labels: map[string]string{
model.LabelSourceOfTruth: model.FromInner,
types.LabelConfigCatalog: types.VelaCoreConfig,
types.LabelConfigType: componentType,
types.LabelConfigProject: ui.Project,
},
},
Spec: v1beta1.ApplicationSpec{
Components: []common.ApplicationComponent{
{
Name: name,
Type: componentType,
Properties: &runtime.RawExtension{Raw: []byte(properties)},
},
},
},
}
return k8sClient.Create(ctx, &app)
}
// DeleteApplication deletes a config application, including a Terraform provider.
// For a Terraform Provider, it can come from
// 1). manually created a Terraform Provider object, like https://github.com/oam-dev/terraform-controller/blob/master/getting-started.md#aws
// 2). by enabling a Terraform provider addon in version older than v1.3.0
// 3). by create a Terraform provider via `vela provider add`
// 4). by VelaUX
// We will only target on deleting a provider which comes from 3) or 4) as for 1), it can be easily delete by hand, and
// for 2), it will be recreated by the addon.
func DeleteApplication(ctx context.Context, k8sClient client.Client, name string, isTerraformProvider bool) error {
if isTerraformProvider {
existed, err := IsTerraformProviderExisted(ctx, k8sClient, name)
if err != nil {
return errors.Wrapf(err, errCheckProviderExistence, name)
}
if existed {
// In version 1.3.0, we used `providerAppName` as the name of the application to create a provider, but
// in version 1.3.1, a config name is the config name, ie, the provider name. To keep backward compatibility,
// we need to check the legacy name and the current name of the application.
legacyName := fmt.Sprintf("%s-%s", types.ProviderAppPrefix, name)
err1 := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: legacyName}, &v1beta1.Application{})
if err1 == nil {
name = legacyName
}
if err1 != nil {
if !kerrors.IsNotFound(err1) {
return fmt.Errorf(errDeleteProvider, name, err1)
}
err2 := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, &v1beta1.Application{})
if err2 != nil {
if kerrors.IsNotFound(err2) {
return fmt.Errorf(errCouldNotDeleteProvider, name)
}
return fmt.Errorf(errDeleteProvider, name, err2)
}
}
}
}
a := &v1beta1.Application{}
if err := k8sClient.Get(ctx, client.ObjectKey{Namespace: types.DefaultKubeVelaNS, Name: name}, a); err != nil {
return err
}
if err := k8sClient.Delete(ctx, a); err != nil {
return err
}
return nil
}
// ListTerraformProviders returns a list of Terraform providers.
func ListTerraformProviders(ctx context.Context, k8sClient client.Client) ([]tcv1beta1.Provider, error) {
l := &tcv1beta1.ProviderList{}
if err := k8sClient.List(ctx, l, client.InNamespace(types.ProviderNamespace)); err != nil {
return nil, err
}
return l.Items, nil
}
// IsTerraformProviderExisted returns whether a Terraform provider exists.
func IsTerraformProviderExisted(ctx context.Context, k8sClient client.Client, name string) (bool, error) {
l, err := ListTerraformProviders(ctx, k8sClient)
if err != nil {
return false, err
}
for _, p := range l {
if p.Name == name {
return true, nil
}
}
return false, nil
}

View File

@ -1,31 +0,0 @@
/*
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 config
import (
v1 "k8s.io/api/core/v1"
"github.com/oam-dev/kubevela/apis/types"
)
// ProjectMatched will check whether a config secret can be used in a given project
func ProjectMatched(s *v1.Secret, project string) bool {
if s.Labels[types.LabelConfigProject] == "" || s.Labels[types.LabelConfigProject] == project {
return true
}
return false
}

View File

@ -1,97 +0,0 @@
/*
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 config
import (
"testing"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/oam-dev/kubevela/apis/types"
)
var ResponseString = "Hello HTTP Get."
func TestMatchProject(t *testing.T) {
s := runtime.NewScheme()
corev1.AddToScheme(s)
secret1 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigProject: "p1",
},
},
}
secret2 := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "s2",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{
types.LabelConfigProject: "",
},
},
}
type args struct {
secret *corev1.Secret
project string
}
type want struct {
matched bool
}
testcases := []struct {
name string
args args
want want
}{
{
name: "matched",
args: args{
project: "p99",
secret: secret1,
},
want: want{
matched: false,
},
},
{
name: "not matched",
args: args{
project: "p99",
secret: secret2,
},
want: want{
matched: true,
},
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
got := ProjectMatched(tc.args.secret, tc.args.project)
assert.Equal(t, tc.want.matched, got)
})
}
}

View File

@ -116,6 +116,10 @@ func NewCommandWithIOStreams(ioStream util.IOStreams) *cobra.Command {
AuthCommandGroup(f, ioStream),
KubeCommandGroup(f, ioStream),
// Config management
ConfigCommandGroup(f, ioStream),
TemplateCommandGroup(f, ioStream),
// System
NewInstallCommand(commandArgs, "1", ioStream),
NewUnInstallCommand(commandArgs, "2", ioStream),

631
references/cli/config.go Normal file
View File

@ -0,0 +1,631 @@
/*
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 cli
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"strings"
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/strvals"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
"sigs.k8s.io/yaml"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/types"
velacmd "github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/pkg/config"
pkgUtils "github.com/oam-dev/kubevela/pkg/utils"
"github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/docgen"
)
// ConfigCommandGroup commands for the config
func ConfigCommandGroup(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "config",
Short: i18n.T("Manage the configs."),
Long: i18n.T("Manage the configs, such as the terraform provider, image registry, helm repository, etc."),
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
},
}
cmd.AddCommand(NewListConfigCommand(f, streams))
cmd.AddCommand(NewCreateConfigCommand(f, streams))
cmd.AddCommand(NewDistributeConfigCommand(f, streams))
cmd.AddCommand(NewDeleteConfigCommand(f, streams))
return cmd
}
// TemplateCommandGroup commands for the template of the config
func TemplateCommandGroup(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "config-template",
Aliases: []string{"ct"},
Short: i18n.T("Manage the template of config."),
Annotations: map[string]string{
types.TagCommandType: types.TypeExtension,
},
}
cmd.AddCommand(NewTemplateApplyCommand(f, streams))
cmd.AddCommand(NewTemplateListCommand(f, streams))
cmd.AddCommand(NewTemplateDeleteCommand(f, streams))
cmd.AddCommand(NewTemplateShowCommand(f, streams))
return cmd
}
// TemplateApplyCommandOptions the options of the command that apply the config template.
type TemplateApplyCommandOptions struct {
File string
Namespace string
Name string
}
// TemplateCommandOptions the options of the command that delete or show the config template.
type TemplateCommandOptions struct {
Namespace string
Name string
}
// TemplateListCommandOptions the options of the command that list the config templates.
type TemplateListCommandOptions struct {
Namespace string
AllNamespace bool
}
// NewTemplateApplyCommand command for creating and updating the config template
func NewTemplateApplyCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options TemplateApplyCommandOptions
cmd := &cobra.Command{
Use: "apply",
Short: i18n.T("Apply a config template."),
Annotations: map[string]string{
types.TagCommandType: types.TypeExtension,
},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
body, err := pkgUtils.ReadRemoteOrLocalPath(options.File, false)
if err != nil {
return err
}
inf := config.NewConfigFactory(f.Client())
template, err := inf.ParseTemplate(options.Name, body)
if err != nil {
return err
}
if err := inf.CreateOrUpdateConfigTemplate(context.Background(), options.Namespace, template); err != nil {
return err
}
streams.Infof("the config template %s applied successfully\n", template.Name)
return nil
},
}
cmd.Flags().StringVarP(&options.File, "file", "f", "", "specify the template file name")
cmd.Flags().StringVarP(&options.Name, "name", "", "", "specify the config template name")
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the template")
return cmd
}
// NewTemplateListCommand command for listing the config templates
func NewTemplateListCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options TemplateListCommandOptions
cmd := &cobra.Command{
Use: "list",
Short: i18n.T("List the config templates."),
Example: "vela config template list [-A]",
Annotations: map[string]string{
types.TagCommandType: types.TypeExtension,
},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
inf := config.NewConfigFactory(f.Client())
if options.AllNamespace {
options.Namespace = ""
}
templates, err := inf.ListTemplates(context.Background(), options.Namespace, "")
if err != nil {
return err
}
table := newUITable()
header := []interface{}{"NAME", "ALIAS", "SCOPE", "SENSITIVE", "CREATED-TIME"}
if options.AllNamespace {
header = append([]interface{}{"NAMESPACE"}, header...)
}
table.AddRow(header...)
for _, t := range templates {
row := []interface{}{t.Name, t.Alias, t.Scope, t.Sensitive, t.CreateTime}
if options.AllNamespace {
row = append([]interface{}{t.Namespace}, row...)
}
table.AddRow(row...)
}
if _, err := streams.Out.Write(table.Bytes()); err != nil {
return err
}
if _, err := streams.Out.Write([]byte("\n")); err != nil {
return err
}
return nil
},
}
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the template")
cmd.Flags().BoolVarP(&options.AllNamespace, "all-namespaces", "A", false, "If true, check the specified action in all namespaces.")
return cmd
}
// NewTemplateShowCommand command for show the properties document
func NewTemplateShowCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options TemplateCommandOptions
cmd := &cobra.Command{
Use: "show",
Short: i18n.T("Show the documents of the template properties"),
Example: "vela config-template show <name> [-n]",
Annotations: map[string]string{
types.TagCommandType: types.TypeExtension,
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.Name = args[0]
if options.Name == "" {
return fmt.Errorf("can not show the properties without the template")
}
inf := config.NewConfigFactory(f.Client())
template, err := inf.LoadTemplate(context.Background(), options.Name, options.Namespace)
if err != nil {
return err
}
doc, err := docgen.GenerateConsoleDocument(template.Schema.Title, template.Schema)
if err != nil {
return err
}
if _, err := streams.Out.Write([]byte(doc)); err != nil {
return err
}
return nil
},
}
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the template")
return cmd
}
// NewTemplateDeleteCommand command for deleting the config template
func NewTemplateDeleteCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options TemplateCommandOptions
cmd := &cobra.Command{
Use: "delete",
Short: i18n.T("Delete a config template."),
Example: fmt.Sprintf("vela config template delete <name> [-n %s]", types.DefaultKubeVelaNS),
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return fmt.Errorf("please must provides the template name")
}
options.Name = args[0]
userInput := &UserInput{
Writer: streams.Out,
Reader: bufio.NewReader(streams.In),
}
if !assumeYes {
userConfirmation := userInput.AskBool("Do you want to delete this template", &UserInputOptions{assumeYes})
if !userConfirmation {
return fmt.Errorf("stopping deleting")
}
}
inf := config.NewConfigFactory(f.Client())
if err := inf.DeleteTemplate(context.Background(), options.Namespace, options.Name); err != nil {
return err
}
streams.Infof("the config template %s deleted successfully\n", options.Name)
return nil
},
}
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the template")
return cmd
}
// DistributeConfigCommandOptions the options of the command that distribute the config.
type DistributeConfigCommandOptions struct {
Targets []string
Config string
Namespace string
Recalled bool
}
// CreateConfigCommandOptions the options of the command that create the config.
type CreateConfigCommandOptions struct {
Template string
Namespace string
Name string
File string
Properties map[string]interface{}
DryRun bool
Targets []string
Description string
Alias string
}
// Validate validate the options
func (i CreateConfigCommandOptions) Validate() error {
if i.Name == "" {
return fmt.Errorf("the config name must be specified")
}
if len(i.Targets) > 0 && i.DryRun {
return fmt.Errorf("can not set the distribution in dry-run mode")
}
return nil
}
func (i *CreateConfigCommandOptions) parseProperties(args []string) error {
if i.File != "" {
body, err := pkgUtils.ReadRemoteOrLocalPath(i.File, false)
if err != nil {
return err
}
var properties = map[string]interface{}{}
if err := yaml.Unmarshal(body, &properties); err != nil {
return err
}
i.Properties = properties
return nil
}
res := map[string]interface{}{}
for _, arg := range args {
if err := strvals.ParseInto(arg, res); err != nil {
return err
}
}
i.Properties = res
return nil
}
// ConfigListCommandOptions the options of the command that list configs.
type ConfigListCommandOptions struct {
Namespace string
Template string
AllNamespace bool
}
// NewListConfigCommand command for listing the config secrets
func NewListConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options ConfigListCommandOptions
cmd := &cobra.Command{
Use: "list",
Short: i18n.T("List the configs."),
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
},
Args: cobra.ExactArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
name := options.Template
if strings.Contains(options.Template, "/") {
namespacedName := strings.SplitN(options.Template, "/", 2)
name = namespacedName[1]
}
if options.AllNamespace {
options.Namespace = ""
}
inf := config.NewConfigFactory(f.Client())
configs, err := inf.ListConfigs(context.Background(), options.Namespace, name, "", true)
if err != nil {
return err
}
table := newUITable()
header := []interface{}{"NAME", "ALIAS", "DISTRIBUTION", "TEMPLATE", "CREATED-TIME", "DESCRIPTION"}
if options.AllNamespace {
header = append([]interface{}{"NAMESPACE"}, header...)
}
table.AddRow(header...)
for _, t := range configs {
var targetShow = ""
for _, target := range t.Targets {
if targetShow != "" {
targetShow += " "
}
switch target.Status {
case string(workflowv1alpha1.WorkflowStepPhaseSucceeded):
targetShow += green.Sprintf("%s/%s", target.ClusterName, target.Namespace)
case string(workflowv1alpha1.WorkflowStepPhaseFailed):
targetShow += red.Sprintf("%s/%s", target.ClusterName, target.Namespace)
default:
targetShow += yellow.Sprintf("%s/%s", target.ClusterName, target.Namespace)
}
}
row := []interface{}{t.Name, t.Alias, targetShow, fmt.Sprintf("%s/%s", t.Template.Namespace, t.Template.Name), t.CreateTime, t.Description}
if options.AllNamespace {
row = append([]interface{}{t.Namespace}, row...)
}
table.AddRow(row...)
}
if _, err := streams.Out.Write(table.Bytes()); err != nil {
return err
}
if _, err := streams.Out.Write([]byte("\n")); err != nil {
return err
}
return nil
},
}
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the config")
cmd.Flags().StringVarP(&options.Template, "template", "t", "", "specify the template of the config")
cmd.Flags().BoolVarP(&options.AllNamespace, "all-namespaces", "A", false, "If true, check the specified action in all namespaces.")
return cmd
}
// NewCreateConfigCommand command for creating the config
func NewCreateConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options CreateConfigCommandOptions
createConfigExample := templates.Examples(i18n.T(`
# Generate a config with the args
vela config create test-registry --template=image-registry registry=index.docker.io auth.username=test auth.password=test
# Generate a config with the file
vela config create test-config --template=image-registry -f config.yaml
# Generate a config without the template
vela config create --name test-vela -f config.yaml
`))
cmd := &cobra.Command{
Use: "create",
Aliases: []string{"c"},
Short: i18n.T("Create a config."),
Example: createConfigExample,
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
},
RunE: func(cmd *cobra.Command, args []string) error {
inf := config.NewConfigFactory(f.Client())
options.Name = args[0]
if err := options.Validate(); err != nil {
return err
}
name := options.Template
namespace := types.DefaultKubeVelaNS
if strings.Contains(options.Template, "/") {
namespacedName := strings.SplitN(options.Template, "/", 2)
namespace = namespacedName[0]
name = namespacedName[1]
}
if err := options.parseProperties(args[1:]); err != nil {
return err
}
configItem, err := inf.ParseConfig(context.Background(), config.NamespacedName{
Name: name,
Namespace: namespace,
}, config.Metadata{
NamespacedName: config.NamespacedName{
Name: options.Name,
Namespace: options.Namespace,
},
Properties: options.Properties,
Alias: options.Alias,
Description: options.Description,
})
if err != nil {
return err
}
if options.DryRun {
var outBuilder = bytes.NewBuffer(nil)
out, err := yaml.Marshal(configItem.Secret)
if err != nil {
return err
}
_, err = outBuilder.Write(out)
if err != nil {
return err
}
if configItem.OutputObjects != nil {
for k, object := range configItem.OutputObjects {
_, err = outBuilder.WriteString("# Object: \n ---" + k)
if err != nil {
return err
}
out, err := yaml.Marshal(object)
if err != nil {
return err
}
if _, err := outBuilder.Write(out); err != nil {
return err
}
}
}
_, err = streams.Out.Write(outBuilder.Bytes())
return err
}
if err := inf.CreateOrUpdateConfig(context.Background(), configItem, options.Namespace); err != nil {
return err
}
if len(options.Targets) > 0 {
ads := &config.CreateDistributionSpec{
Targets: []*config.ClusterTarget{},
Configs: []*config.NamespacedName{
&configItem.NamespacedName,
},
}
for _, t := range options.Targets {
ti := strings.Split(t, "/")
if len(ti) == 2 {
ads.Targets = append(ads.Targets, &config.ClusterTarget{
ClusterName: ti[0],
Namespace: ti[1],
})
} else {
ads.Targets = append(ads.Targets, &config.ClusterTarget{
ClusterName: types.ClusterLocalName,
Namespace: ti[0],
})
}
}
name := config.DefaultDistributionName(options.Name)
if err := inf.CreateOrUpdateDistribution(context.Background(), options.Namespace, name, ads); err != nil {
return err
}
}
streams.Infof("the config %s applied successfully\n", options.Name)
return nil
},
}
cmd.Flags().StringVarP(&options.Template, "template", "t", "", "specify the config template name and namespace")
cmd.Flags().StringVarP(&options.File, "file", "f", "", "specify the file name of the config properties")
cmd.Flags().StringArrayVarP(&options.Targets, "target", "", []string{}, "this config will be distributed if this flag is set")
cmd.Flags().BoolVarP(&options.DryRun, "dry-run", "", false, "Dry run to apply the config")
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the config")
cmd.Flags().StringVarP(&options.Description, "description", "", "", "specify the description of the config")
cmd.Flags().StringVarP(&options.Alias, "alias", "", "", "specify the alias of the config")
return cmd
}
// NewDistributeConfigCommand command for distributing the config
func NewDistributeConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options DistributeConfigCommandOptions
distributionExample := templates.Examples(i18n.T(`
# distribute the config(test-registry) from the vela-system namespace to the default namespace in the local cluster.
vela config d test-registry -t default
# distribute the config(test-registry) from the vela-system namespace to the other clusters.
vela config d test-registry -t cluster1/default -t cluster2/default
# recall the config
vela config d test-registry --recall
`))
cmd := &cobra.Command{
Use: "distribute",
Aliases: []string{"d"},
Short: i18n.T("Distribute a config"),
Example: distributionExample,
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
inf := config.NewConfigFactory(f.Client())
options.Config = args[0]
name := config.DefaultDistributionName(options.Config)
if options.Recalled {
userInput := &UserInput{
Writer: streams.Out,
Reader: bufio.NewReader(streams.In),
}
if !assumeYes {
userConfirmation := userInput.AskBool("Do you want to recall this config", &UserInputOptions{assumeYes})
if !userConfirmation {
return fmt.Errorf("recalling stopped")
}
}
if err := inf.DeleteDistribution(context.Background(), options.Namespace, name); err != nil {
return err
}
streams.Infof("the distribution %s deleted successfully\n", name)
return nil
}
ads := &config.CreateDistributionSpec{
Targets: []*config.ClusterTarget{},
Configs: []*config.NamespacedName{
{
Name: options.Config,
Namespace: options.Namespace,
},
},
}
for _, t := range options.Targets {
ti := strings.Split(t, "/")
if len(ti) == 2 {
ads.Targets = append(ads.Targets, &config.ClusterTarget{
ClusterName: ti[0],
Namespace: ti[1],
})
} else {
ads.Targets = append(ads.Targets, &config.ClusterTarget{
ClusterName: types.ClusterLocalName,
Namespace: ti[0],
})
}
}
if err := inf.CreateOrUpdateDistribution(context.Background(), options.Namespace, name, ads); err != nil {
return err
}
streams.Infof("the distribution %s applied successfully\n", name)
return nil
},
}
cmd.Flags().StringArrayVarP(&options.Targets, "target", "t", []string{}, "specify the targets that want to distribute,the format is: <clusterName>/<namespace>")
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the distribution")
cmd.Flags().BoolVarP(&options.Recalled, "recall", "r", false, "this field means recalling the configs from all targets.")
return cmd
}
// ConfigDeleteCommandOptions the options of the command that delete the config.
type ConfigDeleteCommandOptions struct {
Namespace string
Name string
NotRecall bool
}
// NewDeleteConfigCommand command for deleting the config secret
func NewDeleteConfigCommand(f velacmd.Factory, streams util.IOStreams) *cobra.Command {
var options ConfigDeleteCommandOptions
cmd := &cobra.Command{
Use: "delete",
Short: i18n.T("Delete a config."),
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
},
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.Name = args[0]
inf := config.NewConfigFactory(f.Client())
userInput := &UserInput{
Writer: streams.Out,
Reader: bufio.NewReader(streams.In),
}
if !assumeYes {
userConfirmation := userInput.AskBool("Do you want to delete this config", &UserInputOptions{assumeYes})
if !userConfirmation {
return fmt.Errorf("deleting stopped")
}
}
if !options.NotRecall {
if err := inf.DeleteDistribution(context.Background(), options.Namespace, config.DefaultDistributionName(options.Name)); err != nil && !errors.Is(err, config.ErrNotFoundDistribution) {
return err
}
}
if err := inf.DeleteConfig(context.Background(), options.Namespace, options.Name); err != nil {
return err
}
streams.Infof("the config %s deleted successfully\n", options.Name)
return nil
},
}
cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", types.DefaultKubeVelaNS, "specify the namespace of the config")
cmd.Flags().BoolVarP(&options.NotRecall, "not-recall", "", false, "means only deleting the config from the local and do not recall from targets.")
return cmd
}

View File

@ -0,0 +1,195 @@
/*
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 cli
import (
"bytes"
"os"
"strings"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
v1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/oam-dev/kubevela/pkg/cmd"
"github.com/oam-dev/kubevela/pkg/utils/util"
)
var _ = Describe("Test the commands of the config", func() {
var arg cmd.Factory
BeforeEach(func() {
arg = cmd.NewTestFactory(cfg, k8sClient)
})
It("Test apply a template", func() {
buffer := bytes.NewBuffer(nil)
cmd := TemplateCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"apply", "-f", "./test-data/config-templates/image-registry.cue", "--name", "test"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the config template test applied successfully\n"))
})
It("Test apply a new template", func() {
buffer := bytes.NewBuffer(nil)
cmd := TemplateCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"apply", "-f", "./test-data/config-templates/image-registry.cue", "--name", "test2"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the config template test2 applied successfully\n"))
})
It("Test list the templates", func() {
buffer := bytes.NewBuffer(nil)
cmd := TemplateCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"list", "-A"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(strings.Contains(buffer.String(), "vela-system")).Should(Equal(true))
Expect(strings.Contains(buffer.String(), "test")).Should(Equal(true))
Expect(strings.Contains(buffer.String(), "\n")).Should(Equal(true))
})
It("Test show the templates", func() {
buffer := bytes.NewBuffer(nil)
cmd := TemplateCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"show", "test2"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(line(buffer.String())).Should(Equal(24))
})
It("Test create the config with the args", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"create", "test", "--template=test", "registry=test.kubevela.net", "auth.username=yueda", "auth.password=yueda123", "useHTTP=true"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the config test applied successfully\n"))
})
It("Test create the config with the file", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"create", "testfile", "--template=test2", "--namespace=default", "-f", "./test-data/config/registry.yaml"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the config testfile applied successfully\n"))
})
It("Test create the config without the template", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"create", "without-template", "--namespace=default", "-f", "./test-data/config/registry.yaml"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the config without-template applied successfully\n"))
})
It("Test creating and distributing the config", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"create", "distribution", "--namespace=default", "-f", "./test-data/config/registry.yaml", "--target", "test"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the config distribution applied successfully\n"))
})
It("Test list the configs", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"list", "-A"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(line(buffer.String())).Should(Equal(5))
})
It("Test list the configs with the namespace filter", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"list", "-n", "default"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(line(buffer.String())).Should(Equal(4))
})
It("Test list the configs with the template filter", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"list", "-A", "-t", "test2"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(line(buffer.String())).Should(Equal(2))
})
It("Test dry run the config", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"create", "testfile", "--template=test", "-f", "./test-data/config/registry.yaml", "--dry-run"})
err := cmd.Execute()
Expect(err).Should(BeNil())
var secret v1.Secret
Expect(yaml.Unmarshal(buffer.Bytes(), &secret)).Should(BeNil())
Expect(secret.Name).Should(Equal("testfile"))
Expect(secret.Labels["config.oam.dev/type"]).Should(Equal("test"))
Expect(secret.Labels["config.oam.dev/catalog"]).Should(Equal("velacore-config"))
Expect(string(secret.Type)).Should(Equal("kubernetes.io/dockerconfigjson"))
})
It("Distribute a config", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: os.Stdin, Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"distribute", "testfile", "-t", "test"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("the distribution distribute-testfile applied successfully\n"))
})
It("Recall a config", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: strings.NewReader("y\n"), Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"distribute", "testfile", "--recall"})
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("Do you want to recall this config (y/n)the distribution distribute-testfile deleted successfully\n"))
})
It("Test delete a config", func() {
buffer := bytes.NewBuffer(nil)
cmd := ConfigCommandGroup(arg, util.IOStreams{In: strings.NewReader("y\n"), Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"delete", "distribution", "-n", "default"})
assumeYes = false
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("Do you want to delete this config (y/n)the config distribution deleted successfully\n"))
})
It("Test delete a template", func() {
buffer := bytes.NewBuffer(nil)
cmd := TemplateCommandGroup(arg, util.IOStreams{In: strings.NewReader("y\n"), Out: buffer, ErrOut: buffer})
cmd.SetArgs([]string{"delete", "test"})
assumeYes = false
err := cmd.Execute()
Expect(err).Should(BeNil())
Expect(buffer.String()).Should(Equal("Do you want to delete this template (y/n)the config template test deleted successfully\n"))
})
})
func line(data string) int {
return len(strings.Split(strings.TrimRight(data, "\n"), "\n"))
}

View File

@ -18,36 +18,19 @@ package cli
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"time"
"github.com/gosuri/uitable"
tcv1beta1 "github.com/oam-dev/terraform-controller/api/v1beta1"
"github.com/pkg/errors"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"github.com/spf13/cobra"
kerrors "k8s.io/apimachinery/pkg/api/errors"
k8stypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils/common"
"github.com/oam-dev/kubevela/pkg/utils/config"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/references/docgen"
)
const (
providerNameParam = "name"
errAuthenticateProvider = "failed to authenticate Terraform cloud provider %s err: %w"
)
// NewProviderCommand create `addon` command
// +Deprecated
func NewProviderCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "provider",
@ -61,8 +44,8 @@ func NewProviderCommand(c common.Args, order string, ioStreams cmdutil.IOStreams
cmd.AddCommand(
NewProviderListCommand(c, ioStreams),
)
cmd.AddCommand(prepareProviderAddCommand(c, ioStreams))
cmd.AddCommand(prepareProviderDeleteCommand(c, ioStreams))
cmd.AddCommand(prepareProviderAddCommand())
cmd.AddCommand(prepareProviderDeleteCommand())
return cmd
}
@ -87,345 +70,35 @@ func NewProviderListCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.C
}
}
func prepareProviderAddCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
func prepareProviderAddCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Authenticate Terraform Cloud Provider",
Long: "Authenticate Terraform Cloud Provider by creating a credential secret and a Terraform Controller Provider",
Example: "vela provider add <provider-type>",
Use: "add",
Deprecated: "Please use the vela config command: \n vela config create <name> --template <provider-type> [Properties]",
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
defer cancel()
k8sClient, err := c.GetClient()
if err != nil {
return err
}
defs, err := getTerraformProviderTypes(ctx, k8sClient)
if len(args) < 1 {
errMsg := "must specify a Terraform Cloud Provider type"
if err == nil {
if len(defs) > 0 {
providerDefNames := make([]string, len(defs))
for i, def := range defs {
providerDefNames[i] = def.Name
}
names := strings.Join(providerDefNames, ", ")
errMsg += fmt.Sprintf(": select one from %s", names)
} else {
errMsg += "\nNo Terraform Cloud Provider types exist. Please run `vela addon enable` first"
}
}
return errors.New(errMsg)
} else if err == nil {
var found bool
for _, def := range defs {
if def.Name == args[0] {
found = true
}
}
if !found {
return fmt.Errorf("%s is not valid", args[0])
}
}
return nil
}
addSubCommands, err := prepareProviderAddSubCommand(c, ioStreams)
if err != nil {
ioStreams.Errorf("Fail to prepare the sub commands for the add command:%s \n", err.Error())
}
cmd.AddCommand(addSubCommands...)
return cmd
}
func prepareProviderAddSubCommand(c common.Args, ioStreams cmdutil.IOStreams) ([]*cobra.Command, error) {
if len(os.Args) < 2 || os.Args[1] != "provider" {
return nil, nil
}
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
defer cancel()
k8sClient, err := c.GetClient()
if err != nil {
return nil, err
}
defs, err := getTerraformProviderTypes(timeoutCtx, k8sClient)
if err == nil {
cmds := make([]*cobra.Command, len(defs))
for i, d := range defs {
providerType := d.Name
cmd := &cobra.Command{
Use: providerType,
Short: fmt.Sprintf("Authenticate Terraform Cloud Provider %s", providerType),
Long: fmt.Sprintf("Authenticate Terraform Cloud Provider %s by creating a credential secret and a Terraform Controller Provider", providerType),
Example: fmt.Sprintf("vela provider add %s", providerType),
}
parameters, err := getParameters(context.Background(), k8sClient, providerType)
if err != nil {
return nil, err
}
for _, p := range parameters {
// TODO(wonderflow): make the provider default name to be unique but keep the compatiblility as some Application didn't specify the name,
// now it's “default” for every one, the name will conflict if we have more than one cloud provider.
cmd.Flags().String(p.Name, fmt.Sprint(p.Default), p.Usage)
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
newContext := context.Background()
name, err := cmd.Flags().GetString(providerNameParam)
if err != nil || name == "" {
return fmt.Errorf("must specify a name for the Terraform Cloud Provider %s", providerType)
}
var properties = make(map[string]string, len(parameters))
for _, p := range parameters {
value, err := cmd.Flags().GetString(p.Name)
if err != nil {
return err
}
if value == "" && p.Required {
return fmt.Errorf("must specify a value for %s", p.Name)
}
properties[p.Name] = value
}
data, err := json.Marshal(properties)
if err != nil {
return fmt.Errorf(errAuthenticateProvider, providerType, err)
}
if err := config.CreateApplication(newContext, k8sClient, name, providerType, string(data), config.UIParam{}); err != nil {
return fmt.Errorf(errAuthenticateProvider, providerType, err)
}
ioStreams.Infof("Successfully authenticate provider %s for %s\n", name, providerType)
return nil
}
cmds[i] = cmd
}
return cmds, nil
}
return nil, nil
}
// getParameters gets parameter from a Terraform Cloud Provider, ie the ComponentDefinition
func getParameters(ctx context.Context, k8sClient client.Client, providerType string) ([]types.Parameter, error) {
def, err := getTerraformProviderType(ctx, k8sClient, providerType)
if err != nil {
return nil, err
}
cap, err := docgen.GetCapabilityByComponentDefinitionObject(*def, "")
if err != nil {
return nil, err
}
return cap.Parameters, nil
}
// ProviderMeta is the metadata for a Terraform Cloud Provider
type ProviderMeta struct {
Type string
Name string
Age string
}
func listProviders(ctx context.Context, k8sClient client.Client, ioStreams cmdutil.IOStreams) error {
var (
providers []ProviderMeta
currentProviders []tcv1beta1.Provider
legacyProviders []tcv1beta1.Provider
)
l, err := config.ListTerraformProviders(ctx, k8sClient)
if err != nil {
return errors.Wrap(err, "failed to retrieve providers")
}
for _, p := range l {
// The first condition matches the providers created by `vela provider` in 1.3.2 and earlier.
if p.Labels[types.LabelConfigType] == types.TerraformProvider || p.Labels[types.LabelConfigCatalog] == types.VelaCoreConfig {
currentProviders = append(currentProviders, p)
} else {
// if not labeled, the provider is manually created or created by `vela addon enable`.
legacyProviders = append(legacyProviders, p)
}
}
defs, err := getTerraformProviderTypes(ctx, k8sClient)
if err != nil {
if kerrors.IsNotFound(err) {
ioStreams.Info("no Terraform Cloud Provider found, please run `vela addon enable` first")
}
return errors.Wrap(err, "failed to retrieve providers")
}
for _, d := range defs {
var found bool
for _, p := range currentProviders {
if p.Labels[oam.WorkloadTypeLabel] == d.Name {
found = true
providers = append(providers, ProviderMeta{
Type: p.Labels[oam.WorkloadTypeLabel],
Name: p.Name,
Age: p.CreationTimestamp.String(),
})
}
}
if !found {
providers = append(providers, ProviderMeta{
Type: d.Name,
})
}
}
for _, p := range legacyProviders {
providers = append(providers, ProviderMeta{
Type: "-",
Name: p.Name + "(legacy)",
Age: p.CreationTimestamp.String(),
})
}
if len(providers) == 0 {
return errors.New("no Terraform Cloud Provider found, please run `vela addon enable` first")
l := &terraformapi.ProviderList{}
if err := k8sClient.List(ctx, l, client.InNamespace(types.ProviderNamespace)); err != nil {
return err
}
table := uitable.New()
table.AddRow("TYPE", "NAME", "CREATED-TIME")
table.AddRow("TYPE", "PROVIDER", "NAME", "REGION", "CREATED-TIME")
for _, p := range providers {
table.AddRow(p.Type, p.Name, p.Age)
for _, p := range l.Items {
table.AddRow(p.Labels["config.oam.dev/provider"], p.Spec.Provider, p.Name, p.Spec.Region, p.CreationTimestamp)
}
ioStreams.Info(table.String())
return nil
}
// getTerraformProviderTypes retrieves all ComponentDefinition for Terraform Cloud Providers which are delivered by
// Terraform Cloud provider addons
func getTerraformProviderTypes(ctx context.Context, k8sClient client.Client) ([]v1beta1.ComponentDefinition, error) {
// keep compatibility with old version of terraform-xxx provider definition
legacyDefs := &v1beta1.ComponentDefinitionList{}
if err := k8sClient.List(ctx, legacyDefs, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{definition.UserPrefix + definition.DefinitionType: types.TerraformProvider}); err != nil {
return nil, err
}
defs := &v1beta1.ComponentDefinitionList{}
if err := k8sClient.List(ctx, defs, client.InNamespace(types.DefaultKubeVelaNS),
client.MatchingLabels{definition.DefinitionType: types.TerraformProvider}); err != nil {
return nil, err
}
var result []v1beta1.ComponentDefinition
result = append(result, legacyDefs.Items...)
result = append(result, defs.Items...)
if len(result) == 0 {
return nil, errors.New("no Terraform Cloud Provider ComponentDefinition found")
}
return result, nil
}
// getTerraformProviderType retrieves the ComponentDefinition for a Terraform Cloud Provider which is delivered by
// the Terraform Cloud provider addon
func getTerraformProviderType(ctx context.Context, k8sClient client.Client, name string) (*v1beta1.ComponentDefinition, error) {
def := &v1beta1.ComponentDefinition{}
if err := k8sClient.Get(ctx, k8stypes.NamespacedName{Namespace: types.DefaultKubeVelaNS, Name: name}, def); err != nil {
return nil, err
}
return def, nil
}
func prepareProviderDeleteCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
func prepareProviderDeleteCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Aliases: []string{"rm", "del"},
Short: "Delete Terraform Cloud Provider",
Long: "Delete Terraform Cloud Provider",
Example: "vela provider delete <provider-type> --name <provider-name>",
}
deleteSubCommands, err := prepareProviderDeleteSubCommand(c, ioStreams)
if err != nil {
ioStreams.Errorf("Fail to prepare the sub commands for the delete command:%s \n", err.Error())
}
cmd.AddCommand(deleteSubCommands...)
cmd.RunE = func(cmd *cobra.Command, args []string) error {
k8sClient, err := c.GetClient()
if err != nil {
return err
}
timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Minute*1)
defer cancel()
defs, err := getTerraformProviderTypes(timeoutCtx, k8sClient)
if len(args) < 1 {
errMsg := "must specify a Terraform Cloud Provider type"
if err == nil {
if len(defs) > 0 {
providerDefNames := make([]string, len(defs))
for i, def := range defs {
providerDefNames[i] = def.Name
}
names := strings.Join(providerDefNames, ", ")
errMsg += fmt.Sprintf(": select one from %s", names)
} else {
errMsg += "\nNo Terraform Cloud Provider types exist. Please run `vela addon enable` first"
}
}
return errors.New(errMsg)
} else if err == nil {
var found bool
for _, def := range defs {
if def.Name == args[0] {
found = true
}
}
if !found {
return fmt.Errorf("%s is not valid", args[0])
}
}
return nil
Use: "delete",
Aliases: []string{"rm", "del"},
Deprecated: "Please use the vela config command: \n vela config delete <provider-name>",
}
return cmd
}
func prepareProviderDeleteSubCommand(c common.Args, ioStreams cmdutil.IOStreams) ([]*cobra.Command, error) {
if len(os.Args) < 2 || os.Args[1] != "provider" {
return nil, nil
}
timeoutContext, cancel := context.WithTimeout(context.Background(), time.Minute*1)
defer cancel()
k8sClient, err := c.GetClient()
if err != nil {
return nil, err
}
defs, err := getTerraformProviderTypes(timeoutContext, k8sClient)
if err == nil {
cmds := make([]*cobra.Command, len(defs))
for i, d := range defs {
providerType := d.Name
cmd := &cobra.Command{
Use: providerType,
Short: fmt.Sprintf("Delete Terraform Cloud Provider %s", providerType),
Long: fmt.Sprintf("Delete Terraform Cloud Provider %s", providerType),
Example: fmt.Sprintf("vela provider delete %s", providerType),
}
parameters, err := getParameters(context.Background(), k8sClient, providerType)
if err != nil {
return nil, err
}
for _, p := range parameters {
if p.Name == providerNameParam {
cmd.Flags().String(p.Name, fmt.Sprint(p.Default), p.Usage)
}
}
cmd.RunE = func(cmd *cobra.Command, args []string) error {
name, err := cmd.Flags().GetString(providerNameParam)
if err != nil || name == "" {
return fmt.Errorf("must specify a name for the Terraform Cloud Provider %s", providerType)
}
if err := config.DeleteApplication(context.Background(), k8sClient, name, true); err != nil {
return errors.Wrapf(err, "failed to delete Terraform Cloud Provider %s", name)
}
ioStreams.Infof("Successfully delete provider %s for %s\n", name, providerType)
return nil
}
cmds[i] = cmd
}
return cmds, nil
}
return nil, nil
}

View File

@ -22,7 +22,6 @@ import (
"testing"
terraformapi "github.com/oam-dev/terraform-controller/api/v1beta1"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -32,7 +31,6 @@ import (
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/definition"
"github.com/oam-dev/kubevela/pkg/utils/util"
)
@ -95,63 +93,3 @@ func TestListProviders(t *testing.T) {
})
}
}
func TestGetProviderTypes(t *testing.T) {
s := runtime.NewScheme()
v1beta1.AddToScheme(s)
corev1.AddToScheme(s)
terraformapi.AddToScheme(s)
p1 := &v1beta1.ComponentDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "p1",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{definition.DefinitionType: types.TerraformProvider},
},
}
p2 := &v1beta1.ComponentDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "p2",
Namespace: types.DefaultKubeVelaNS,
Labels: map[string]string{definition.UserPrefix + definition.DefinitionType: types.TerraformProvider},
},
}
testCases := map[string]struct {
objects []client.Object
gotTypes map[string]bool
wantErr error
}{
"success": {
objects: []client.Object{p1},
gotTypes: map[string]bool{
"p1": true,
},
},
"success with legacy provider": {
objects: []client.Object{p2},
gotTypes: map[string]bool{
"p2": true,
},
},
"not found": {
objects: []client.Object{},
wantErr: errors.New("no Terraform Cloud Provider ComponentDefinition found"),
},
}
ctx := context.Background()
for _, tc := range testCases {
k8sClient = fake.NewClientBuilder().WithScheme(s).WithObjects(tc.objects...).Build()
providerTypes, err := getTerraformProviderTypes(ctx, k8sClient)
if tc.wantErr != nil {
assert.Contains(t, err.Error(), tc.wantErr.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, len(tc.gotTypes), len(providerTypes))
for _, gotType := range providerTypes {
_, found := tc.gotTypes[gotType.Name]
assert.True(t, found)
}
}
}
}

View File

@ -4,19 +4,12 @@ import (
"strconv"
)
"config-image-registry": {
type: "component"
annotations: {
"alias.config.oam.dev": "Image Registry"
}
labels: {
"catalog.config.oam.dev": "velacore-config"
"type.config.oam.dev": "image-registry"
"multi-cluster.config.oam.dev": "true"
"ui-hidden": "true"
}
metadata: {
name: "image-registry"
alias: "Image Registry"
scope: "project"
description: "Config information to authenticate image registry"
attributes: workload: type: "autodetects.core.oam.dev"
sensitive: false
}
template: {
@ -27,11 +20,8 @@ template: {
name: context.name
namespace: context.namespace
labels: {
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "image-registry"
"config.oam.dev/multi-cluster": "true"
"config.oam.dev/identifier": parameter.registry
"config.oam.dev/sub-type": "auth"
"config.oam.dev/catalog": "velacore-config"
"config.oam.dev/type": "image-registry"
}
}
if parameter.auth != _|_ {
@ -43,7 +33,7 @@ template: {
stringData: {
if parameter.auth != _|_ && parameter.auth.username != _|_ {
".dockerconfigjson": json.Marshal({
"auths": (parameter.registry): {
"auths": "\(parameter.registry)": {
"username": parameter.auth.username
"password": parameter.auth.password
if parameter.auth.email != _|_ {

View File

@ -0,0 +1,5 @@
registry: docker.io
auth:
username: zhangsan
password: lisi
useHTTP: true

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