From 49ed837f97e411ce27a659cf22a03415ce25f65c Mon Sep 17 00:00:00 2001 From: barnettZQG Date: Mon, 17 Oct 2022 17:15:45 +0800 Subject: [PATCH] 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 * Fix: remove the xml Signed-off-by: barnettZQG * Fix: add the unit test for the nacos writer Signed-off-by: barnettZQG * Feat: add the integration API Signed-off-by: barnettZQG * Feat: make the provider commands to be deprecated Signed-off-by: barnettZQG * Fix: make the unit test work Signed-off-by: barnettZQG * Feat: rename the integration to the config Signed-off-by: barnettZQG * Fix: make the unit test cases work Signed-off-by: barnettZQG * Feat: refactor the config commands Signed-off-by: barnettZQG * Feat: add the distribution status for the config Signed-off-by: barnettZQG * Fix: sort the import packages Signed-off-by: barnettZQG * Fix: refine the code style Signed-off-by: barnettZQG * Fix: refine the code style Signed-off-by: barnettZQG * Fix: get the content format before render the content Signed-off-by: barnettZQG * Feat: add some examples Signed-off-by: barnettZQG * Fix: the command test cases Signed-off-by: barnettZQG * Feat: add the definitions of the workflow step Signed-off-by: barnettZQG * Fix: add some tests Signed-off-by: barnettZQG * Fix: add some tests Signed-off-by: barnettZQG * Fix: change the name Signed-off-by: barnettZQG * Fix: retry the CI Signed-off-by: barnettZQG * Fix: refine some words Signed-off-by: barnettZQG Signed-off-by: barnettZQG --- apis/types/types.go | 20 +- .../config-image-registry.yaml | 86 - .../defwithtemplate/create-config.yaml | 44 + .../defwithtemplate/delete-config.yaml | 31 + .../defwithtemplate/list-config.yaml | 33 + .../defwithtemplate/read-config.yaml | 31 + .../defwithtemplate/create-config.yaml | 44 + .../defwithtemplate/delete-config.yaml | 31 + .../defwithtemplate/list-config.yaml | 33 + .../defwithtemplate/read-config.yaml | 31 + docs/apidoc/swagger.json | 1622 +++++++++++++---- docs/examples/config/dex-connector/README.md | 37 + .../app-config-dex-connector-github.yaml | 21 - .../examples/config/helm-repository/README.md | 47 + docs/examples/config/image-registry/README.md | 102 -- ...pp-config-image-registry-account-auth.yaml | 20 - .../image-registry/app-deliever-secret.yaml | 22 - .../config/image-registry/app-sample.yaml | 14 - .../app-validate-imagePullSecret.yaml | 14 - docs/examples/config/nacos-config/README.md | 38 + .../config/private-image-registry/README.md | 45 + go.mod | 20 +- go.sum | 30 +- pkg/addon/addon.go | 98 +- pkg/addon/addon_test.go | 3 + pkg/addon/testdata/example/parameter.cue | 5 +- pkg/addon/type.go | 4 + pkg/apiserver/domain/model/project.go | 7 + pkg/apiserver/domain/service/application.go | 44 - .../domain/service/authentication_test.go | 5 +- pkg/apiserver/domain/service/config.go | 672 +++---- pkg/apiserver/domain/service/config_test.go | 894 ++------- pkg/apiserver/domain/service/definition.go | 6 +- pkg/apiserver/domain/service/helm.go | 32 +- pkg/apiserver/domain/service/helm_test.go | 65 +- pkg/apiserver/domain/service/image.go | 86 +- pkg/apiserver/domain/service/project.go | 131 +- pkg/apiserver/domain/service/project_test.go | 28 - pkg/apiserver/domain/service/rbac.go | 18 +- pkg/apiserver/domain/service/suite_test.go | 2 +- pkg/apiserver/domain/service/system_info.go | 3 +- .../service/testdata/crds/provider-crd.yaml | 105 ++ pkg/apiserver/interfaces/api/config.go | 162 +- pkg/apiserver/interfaces/api/dto/v1/types.go | 129 +- pkg/apiserver/interfaces/api/interfaces.go | 1 + .../interfaces/api/interfaces_test.go | 2 +- pkg/apiserver/interfaces/api/project.go | 285 ++- pkg/apiserver/server.go | 6 + pkg/apiserver/utils/bcode/016_config.go | 40 + pkg/apiserver/utils/dex.go | 23 +- pkg/apiserver/utils/dex_test.go | 2 +- pkg/cmd/factory.go | 18 + pkg/config/config_suite_test.go | 72 + pkg/config/context/context.go | 36 + pkg/config/factory.go | 889 +++++++++ pkg/config/factory_test.go | 168 ++ pkg/config/testdata/helm-repo.cue | 49 + pkg/config/testdata/mysql-db-nacos.cue | 49 + pkg/config/testdata/nacos-server.cue | 18 + pkg/config/writer/nacos.go | 203 +++ pkg/config/writer/nacos_test.go | 146 ++ pkg/config/writer/writer.go | 165 ++ pkg/config/writer/writter_test.go | 88 + .../componentdefinition_controller.go | 6 +- .../componentdefinition_suit_test.go | 5 - .../policydefinition_controller.go | 6 +- .../policies/policydefinition/suite_test.go | 5 - .../traitdefinition_controller.go | 6 +- .../traitdefinition_suite_test.go | 5 - .../workflowstepdefinition/suite_test.go | 5 - .../workflowstepdefinition_controller.go | 6 +- pkg/controller/utils/capability.go | 145 +- .../utils/capability_integrate_test.go | 2 +- pkg/controller/utils/capability_test.go | 37 +- pkg/cue/base_template.go | 1 + pkg/cue/convert.go | 5 + pkg/cue/script/schema.go | 96 + pkg/cue/script/schema_test.go | 61 + pkg/cue/script/template.go | 228 +++ pkg/cue/script/template_test.go | 222 +++ .../testdata}/arrayWithoutItemsSchema.json | 0 .../arrayWithoutItemsSchemaFixed.json | 0 .../script/testdata}/shortTagSchema.json | 0 .../script/testdata}/shortTagSchemaFixed.json | 0 .../script/testdata}/webservice.json | 0 .../script/testdata}/webserviceFixed.json | 0 pkg/stdlib/op.cue | 5 + pkg/stdlib/pkgs/config.cue | 40 + pkg/utils/apply/apply.go | 31 +- pkg/utils/common/common.go | 4 +- pkg/utils/config/application.go | 159 -- pkg/utils/config/project.go | 31 - pkg/utils/config/project_test.go | 97 - references/cli/cli.go | 4 + references/cli/config.go | 631 +++++++ references/cli/config_test.go | 195 ++ references/cli/provider.go | 361 +--- references/cli/provider_test.go | 62 - .../config-templates/image-registry.cue | 26 +- references/cli/test-data/config/registry.yaml | 5 + references/cli/up_test.go | 1 + references/docgen/openapi.go | 77 + test/e2e-apiserver-test/config_test.go | 284 +++ test/mock/nacos/config_client.go | 168 ++ .../internal/workflowstep/create-config.cue | 38 + .../internal/workflowstep/delete-config.cue | 25 + .../internal/workflowstep/list-config.cue | 27 + .../internal/workflowstep/read-config.cue | 25 + 108 files changed, 7162 insertions(+), 3150 deletions(-) delete mode 100644 charts/vela-core/templates/defwithtemplate/config-image-registry.yaml create mode 100644 charts/vela-core/templates/defwithtemplate/create-config.yaml create mode 100644 charts/vela-core/templates/defwithtemplate/delete-config.yaml create mode 100644 charts/vela-core/templates/defwithtemplate/list-config.yaml create mode 100644 charts/vela-core/templates/defwithtemplate/read-config.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/create-config.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/delete-config.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/list-config.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/read-config.yaml create mode 100644 docs/examples/config/dex-connector/README.md delete mode 100644 docs/examples/config/dex-connector/app-config-dex-connector-github.yaml create mode 100644 docs/examples/config/helm-repository/README.md delete mode 100644 docs/examples/config/image-registry/README.md delete mode 100644 docs/examples/config/image-registry/app-config-image-registry-account-auth.yaml delete mode 100644 docs/examples/config/image-registry/app-deliever-secret.yaml delete mode 100644 docs/examples/config/image-registry/app-sample.yaml delete mode 100644 docs/examples/config/image-registry/app-validate-imagePullSecret.yaml create mode 100644 docs/examples/config/nacos-config/README.md create mode 100644 docs/examples/config/private-image-registry/README.md create mode 100644 pkg/apiserver/domain/service/testdata/crds/provider-crd.yaml create mode 100644 pkg/apiserver/utils/bcode/016_config.go create mode 100644 pkg/config/config_suite_test.go create mode 100644 pkg/config/context/context.go create mode 100644 pkg/config/factory.go create mode 100644 pkg/config/factory_test.go create mode 100644 pkg/config/testdata/helm-repo.cue create mode 100644 pkg/config/testdata/mysql-db-nacos.cue create mode 100644 pkg/config/testdata/nacos-server.cue create mode 100644 pkg/config/writer/nacos.go create mode 100644 pkg/config/writer/nacos_test.go create mode 100644 pkg/config/writer/writer.go create mode 100644 pkg/config/writer/writter_test.go create mode 100644 pkg/cue/script/schema.go create mode 100644 pkg/cue/script/schema_test.go create mode 100644 pkg/cue/script/template.go create mode 100644 pkg/cue/script/template_test.go rename pkg/{controller/utils/testdata/definition => cue/script/testdata}/arrayWithoutItemsSchema.json (100%) rename pkg/{controller/utils/testdata/definition => cue/script/testdata}/arrayWithoutItemsSchemaFixed.json (100%) rename pkg/{controller/utils/testdata/definition => cue/script/testdata}/shortTagSchema.json (100%) rename pkg/{controller/utils/testdata/definition => cue/script/testdata}/shortTagSchemaFixed.json (100%) rename pkg/{controller/utils/testdata/definition => cue/script/testdata}/webservice.json (100%) rename pkg/{controller/utils/testdata/definition => cue/script/testdata}/webserviceFixed.json (100%) create mode 100644 pkg/stdlib/pkgs/config.cue delete mode 100644 pkg/utils/config/application.go delete mode 100644 pkg/utils/config/project.go delete mode 100644 pkg/utils/config/project_test.go create mode 100644 references/cli/config.go create mode 100644 references/cli/config_test.go rename vela-templates/definitions/internal/component/config-image-registry.cue => references/cli/test-data/config-templates/image-registry.cue (70%) create mode 100644 references/cli/test-data/config/registry.yaml create mode 100644 references/docgen/openapi.go create mode 100644 test/e2e-apiserver-test/config_test.go create mode 100644 test/mock/nacos/config_client.go create mode 100644 vela-templates/definitions/internal/workflowstep/create-config.cue create mode 100644 vela-templates/definitions/internal/workflowstep/delete-config.cue create mode 100644 vela-templates/definitions/internal/workflowstep/list-config.cue create mode 100644 vela-templates/definitions/internal/workflowstep/read-config.cue diff --git a/apis/types/types.go b/apis/types/types.go index 4210ee489..4cd63623e 100644 --- a/apis/types/types.go +++ b/apis/types/types.go @@ -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 ( diff --git a/charts/vela-core/templates/defwithtemplate/config-image-registry.yaml b/charts/vela-core/templates/defwithtemplate/config-image-registry.yaml deleted file mode 100644 index dce359e6f..000000000 --- a/charts/vela-core/templates/defwithtemplate/config-image-registry.yaml +++ /dev/null @@ -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 - diff --git a/charts/vela-core/templates/defwithtemplate/create-config.yaml b/charts/vela-core/templates/defwithtemplate/create-config.yaml new file mode 100644 index 000000000..a678c99f5 --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/create-config.yaml @@ -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: {...} + } + diff --git a/charts/vela-core/templates/defwithtemplate/delete-config.yaml b/charts/vela-core/templates/defwithtemplate/delete-config.yaml new file mode 100644 index 000000000..be23c45c7 --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/delete-config.yaml @@ -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 + } + diff --git a/charts/vela-core/templates/defwithtemplate/list-config.yaml b/charts/vela-core/templates/defwithtemplate/list-config.yaml new file mode 100644 index 000000000..1f0079ac3 --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/list-config.yaml @@ -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 + } + diff --git a/charts/vela-core/templates/defwithtemplate/read-config.yaml b/charts/vela-core/templates/defwithtemplate/read-config.yaml new file mode 100644 index 000000000..cc84513e8 --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/read-config.yaml @@ -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 + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/create-config.yaml b/charts/vela-minimal/templates/defwithtemplate/create-config.yaml new file mode 100644 index 000000000..a678c99f5 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/create-config.yaml @@ -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: {...} + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/delete-config.yaml b/charts/vela-minimal/templates/defwithtemplate/delete-config.yaml new file mode 100644 index 000000000..be23c45c7 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/delete-config.yaml @@ -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 + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/list-config.yaml b/charts/vela-minimal/templates/defwithtemplate/list-config.yaml new file mode 100644 index 000000000..1f0079ac3 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/list-config.yaml @@ -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 + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/read-config.yaml b/charts/vela-minimal/templates/defwithtemplate/read-config.yaml new file mode 100644 index 000000000..cc84513e8 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/read-config.yaml @@ -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 + } + diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json index d13300d4e..bcac0fb3f 100644 --- a/docs/apidoc/swagger.json +++ b/docs/apidoc/swagger.json @@ -2834,6 +2834,38 @@ "description": "Bummer, something went wrong" } } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cloudshell" + ], + "summary": "destroy the user's cloud shell environment", + "operationId": "destroyCloudShell", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.SimpleResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } } }, "/api/v1/clusters": { @@ -3393,7 +3425,7 @@ } } }, - "/api/v1/config_types": { + "/api/v1/config_templates": { "get": { "consumes": [ "application/xml", @@ -3406,24 +3438,13 @@ "tags": [ "config" ], - "summary": "list all config types", - "operationId": "listConfigTypes", - "parameters": [ - { - "type": "string", - "description": "Fuzzy search based on name and description.", - "name": "query", - "in": "query" - } - ], + "summary": "List all config templates from the system namespace", + "operationId": "listConfigTemplates", "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/v1.ConfigType" - } + "$ref": "#/definitions/v1.ListConfigTemplateResponse" } }, "400": { @@ -3435,7 +3456,7 @@ } } }, - "/api/v1/config_types/{configType}": { + "/api/v1/config_templates/{templateName}": { "get": { "consumes": [ "application/xml", @@ -3448,22 +3469,67 @@ "tags": [ "config" ], - "summary": "get a config type", - "operationId": "getConfigType", + "summary": "Detail a template", + "operationId": "getConfigTemplate", "parameters": [ { "type": "string", - "description": "identifier of the config type", - "name": "configType", + "description": "identifier of the config template", + "name": "templateName", "in": "path", "required": true + }, + { + "type": "string", + "description": "the name of the namespace", + "name": "namespace", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.ConfigType" + "$ref": "#/definitions/v1.ConfigTemplateDetail" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/configs": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "config" + ], + "summary": "list all configs that belong to the system scope", + "operationId": "getConfigs", + "parameters": [ + { + "type": "string", + "description": "the name of the template", + "name": "template", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListConfigResponse" } }, "400": { @@ -3489,13 +3555,6 @@ "summary": "create or update a config", "operationId": "createConfig", "parameters": [ - { - "type": "string", - "description": "identifier of the config type", - "name": "configType", - "in": "path", - "required": true - }, { "name": "body", "in": "body", @@ -3509,7 +3568,7 @@ "200": { "description": "OK", "schema": { - "$ref": "#/definitions/v1.EmptyResponse" + "$ref": "#/definitions/v1.Config" } }, "400": { @@ -3527,7 +3586,7 @@ } } }, - "/api/v1/config_types/{configType}/configs": { + "/api/v1/configs/{configName}": { "get": { "consumes": [ "application/xml", @@ -3540,13 +3599,13 @@ "tags": [ "config" ], - "summary": "get configs from a config type", - "operationId": "getConfigs", + "summary": "detail a config", + "operationId": "getConfig", "parameters": [ { "type": "string", "description": "identifier of the config", - "name": "configType", + "name": "configName", "in": "path", "required": true } @@ -3568,10 +3627,8 @@ } } } - } - }, - "/api/v1/config_types/{configType}/configs/{name}": { - "get": { + }, + "put": { "consumes": [ "application/xml", "application/json" @@ -3583,20 +3640,13 @@ "tags": [ "config" ], - "summary": "get a config from a config type", - "operationId": "getConfig", + "summary": "update a config", + "operationId": "updateConfig", "parameters": [ - { - "type": "string", - "description": "identifier of the config type", - "name": "configType", - "in": "path", - "required": true - }, { "type": "string", "description": "identifier of the config", - "name": "name", + "name": "configName", "in": "path", "required": true } @@ -3607,7 +3657,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/definitions/*v1.Config" + "$ref": "#/definitions/*v1.UpdateConfigRequest" } } }, @@ -3634,17 +3684,10 @@ "summary": "delete a config", "operationId": "deleteConfig", "parameters": [ - { - "type": "string", - "description": "identifier of the config type", - "name": "configType", - "in": "path", - "required": true - }, { "type": "string", "description": "identifier of the config", - "name": "name", + "name": "configName", "in": "path", "required": true } @@ -3689,9 +3732,9 @@ "parameters": [ { "enum": [ - "component", "trait", - "workflowstep" + "workflowstep", + "component" ], "type": "string", "description": "query the definition type", @@ -4321,6 +4364,99 @@ } } }, + "/api/v1/projects/{projectName}/config_templates": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "get the templates which are in a project", + "operationId": "getConfigTemplates", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "the namespace of the template", + "name": "namespace", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListConfigTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/projects/{projectName}/config_templates/{templateName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "Detail a template", + "operationId": "getConfigTemplate", + "parameters": [ + { + "type": "string", + "description": "identifier of the config template", + "name": "templateName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "the name of the namespace", + "name": "namespace", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ConfigTemplateDetail" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/projects/{projectName}/configs": { "get": { "consumes": [ @@ -4339,8 +4475,8 @@ "parameters": [ { "type": "string", - "description": "config type", - "name": "configType", + "description": "the template name", + "name": "template", "in": "query" }, { @@ -4355,10 +4491,339 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/*v1.Config" - } + "$ref": "#/definitions/v1.ListConfigResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "create a config in a project", + "operationId": "createConfig", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.Config" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/projects/{projectName}/configs/{configName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "detail a config in a project", + "operationId": "detailConfig", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the config", + "name": "configName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.Config" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "update a config in a project", + "operationId": "updateConfig", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the config", + "name": "configName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateConfigRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.Config" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "delete a config from a project", + "operationId": "deleteConfig", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the config", + "name": "configName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/projects/{projectName}/distributions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "list the distribution jobs of the config", + "operationId": "listDistributions", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListConfigDistributionResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "apply the distribution job of the config", + "operationId": "applyDistribution", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateConfigDistributionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, + "/api/v1/projects/{projectName}/distributions/{distributionName}": { + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "delete a distribution job of the config", + "operationId": "deleteDistribution", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the distribution", + "name": "distributionName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.EmptyResponse" } }, "400": { @@ -4486,6 +4951,46 @@ } } }, + "/api/v1/projects/{projectName}/providers": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "project" + ], + "summary": "get providers which are in a project", + "operationId": "getProviders", + "parameters": [ + { + "type": "string", + "description": "identifier of the project", + "name": "projectName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/v1.ListTerraformProviderResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/bcode.Bcode" + } + } + } + } + }, "/api/v1/projects/{projectName}/roles": { "get": { "consumes": [ @@ -5722,6 +6227,16 @@ ], "summary": "update a user's alias or password", "operationId": "updateUser", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateUserRequest" + } + } + ], "responses": { "200": { "description": "OK", @@ -6071,6 +6586,7 @@ "*v1.ApplicationTriggerBase": {}, "*v1.Config": {}, "*v1.EmptyResponse": {}, + "*v1.UpdateConfigRequest": {}, "addon.Dependency": { "properties": { "name": { @@ -6493,7 +7009,7 @@ "inputs": { "type": "array", "items": { - "$ref": "#/definitions/common.inputItem" + "$ref": "#/definitions/v1alpha1.inputItem" } }, "name": { @@ -6502,7 +7018,7 @@ "outputs": { "type": "array", "items": { - "$ref": "#/definitions/common.outputItem" + "$ref": "#/definitions/v1alpha1.outputItem" } }, "properties": { @@ -6796,37 +7312,6 @@ } } }, - "common.StepStatus": { - "required": [ - "id" - ], - "properties": { - "firstExecuteTime": { - "type": "string" - }, - "id": { - "type": "string" - }, - "lastExecuteTime": { - "type": "string" - }, - "message": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phase": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, "common.Terraform": { "required": [ "configuration" @@ -6869,6 +7354,9 @@ "contextBackend": { "$ref": "#/definitions/v1.ObjectReference" }, + "endTime": { + "type": "string" + }, "finished": { "type": "boolean" }, @@ -6881,10 +7369,13 @@ "startTime": { "type": "string" }, + "status": { + "type": "string" + }, "steps": { "type": "array", "items": { - "$ref": "#/definitions/common.WorkflowStepStatus" + "$ref": "#/definitions/v1alpha1.WorkflowStepStatus" } }, "suspend": { @@ -6898,125 +7389,6 @@ } } }, - "common.WorkflowStepMeta": { - "properties": { - "alias": { - "type": "string" - } - } - }, - "common.WorkflowStepStatus": { - "required": [ - "id" - ], - "properties": { - "firstExecuteTime": { - "type": "string" - }, - "id": { - "type": "string" - }, - "lastExecuteTime": { - "type": "string" - }, - "message": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phase": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "subSteps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowSubStepStatus" - } - }, - "type": { - "type": "string" - } - } - }, - "common.WorkflowSubStep": { - "required": [ - "name", - "type" - ], - "properties": { - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "if": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "meta": { - "$ref": "#/definitions/common.WorkflowStepMeta" - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "type": "string" - }, - "timeout": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, - "common.WorkflowSubStepStatus": { - "required": [ - "id" - ], - "properties": { - "firstExecuteTime": { - "type": "string" - }, - "id": { - "type": "string" - }, - "lastExecuteTime": { - "type": "string" - }, - "message": { - "type": "string" - }, - "name": { - "type": "string" - }, - "phase": { - "type": "string" - }, - "reason": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, "common.WorkloadGVK": { "required": [ "apiVersion", @@ -7041,34 +7413,6 @@ } } }, - "common.inputItem": { - "required": [ - "parameterKey", - "from" - ], - "properties": { - "from": { - "type": "string" - }, - "parameterKey": { - "type": "string" - } - } - }, - "common.outputItem": { - "required": [ - "valueFrom", - "name" - ], - "properties": { - "name": { - "type": "string" - }, - "valueFrom": { - "type": "string" - } - } - }, "condition.Condition": { "required": [ "type", @@ -7104,6 +7448,101 @@ } } }, + "config.ClusterTarget": { + "required": [ + "clusterName", + "namespace" + ], + "properties": { + "clusterName": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, + "config.ClusterTargetStatus": { + "required": [ + "clusterName", + "namespace", + "status", + "application", + "message" + ], + "properties": { + "application": { + "$ref": "#/definitions/config.NamespacedName" + }, + "clusterName": { + "type": "string" + }, + "message": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "config.Distribution": { + "required": [ + "name", + "namespace", + "createdTime", + "configs", + "targets", + "application", + "status" + ], + "properties": { + "application": { + "$ref": "#/definitions/types.NamespacedName" + }, + "configs": { + "type": "array", + "items": { + "$ref": "#/definitions/config.NamespacedName" + } + }, + "createdTime": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/common.AppStatus" + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/definitions/config.ClusterTarget" + } + } + } + }, + "config.NamespacedName": { + "required": [ + "name", + "namespace" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, "map[string]interface {}": { "type": "object" }, @@ -7150,7 +7589,7 @@ "inputs": { "type": "array", "items": { - "$ref": "#/definitions/common.inputItem" + "$ref": "#/definitions/v1alpha1.inputItem" } }, "labels": { @@ -7168,7 +7607,7 @@ "outputs": { "type": "array", "items": { - "$ref": "#/definitions/common.outputItem" + "$ref": "#/definitions/v1alpha1.outputItem" } }, "properties": { @@ -7200,10 +7639,11 @@ }, "model.ApplicationRevision": { "required": [ - "updateTime", "createTime", + "updateTime", "appPrimaryKey", "version", + "revisionCRName", "status", "reason", "deployUser", @@ -7241,6 +7681,9 @@ "reason": { "type": "string" }, + "revisionCRName": { + "type": "string" + }, "rollbackVersion": { "type": "string" }, @@ -7459,6 +7902,23 @@ "model.JSONStruct": { "type": "object" }, + "model.ProjectRef": { + "required": [ + "name", + "roles" + ], + "properties": { + "name": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "model.ProviderInfo": { "required": [ "provider", @@ -7572,6 +8032,20 @@ } } }, + "types.NamespacedName": { + "required": [ + "Namespace", + "Name" + ], + "properties": { + "Name": { + "type": "string" + }, + "Namespace": { + "type": "string" + } + } + }, "types.Reference": { "required": [ "name" @@ -8063,10 +8537,10 @@ "v1.ApplicationDeployResponse": { "required": [ "version", - "note", + "status", "envName", "createTime", - "status", + "note", "triggerType" ], "properties": { @@ -8580,7 +9054,7 @@ "inputs": { "type": "array", "items": { - "$ref": "#/definitions/common.inputItem" + "$ref": "#/definitions/v1alpha1.inputItem" } }, "labels": { @@ -8598,7 +9072,7 @@ "outputs": { "type": "array", "items": { - "$ref": "#/definitions/common.outputItem" + "$ref": "#/definitions/v1alpha1.outputItem" } }, "traits": { @@ -8790,28 +9264,84 @@ } } }, - "v1.ConfigType": { + "v1.ConfigTemplate": { "required": [ - "definitions", "alias", "name", - "description" + "namespace", + "description", + "scope", + "sensitive", + "createTime" ], "properties": { "alias": { "type": "string" }, - "definitions": { - "type": "array", - "items": { - "type": "string" - } + "createTime": { + "type": "string", + "format": "date-time" }, "description": { "type": "string" }, "name": { "type": "string" + }, + "namespace": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "sensitive": { + "type": "boolean" + } + } + }, + "v1.ConfigTemplateDetail": { + "required": [ + "scope", + "sensitive", + "createTime", + "alias", + "name", + "namespace", + "description", + "schema", + "uiSchema" + ], + "properties": { + "alias": { + "type": "string" + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "schema": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "sensitive": { + "type": "boolean" + }, + "uiSchema": { + "type": "array", + "items": { + "$ref": "#/definitions/utils.UIParameter" + } } } }, @@ -9134,7 +9664,7 @@ "inputs": { "type": "array", "items": { - "$ref": "#/definitions/common.inputItem" + "$ref": "#/definitions/v1alpha1.inputItem" } }, "labels": { @@ -9149,7 +9679,7 @@ "outputs": { "type": "array", "items": { - "$ref": "#/definitions/common.outputItem" + "$ref": "#/definitions/v1alpha1.outputItem" } }, "properties": { @@ -9163,32 +9693,52 @@ } } }, + "v1.CreateConfigDistributionRequest": { + "required": [ + "name", + "configs", + "targets" + ], + "properties": { + "configs": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NamespacedName" + } + }, + "name": { + "type": "string" + }, + "targets": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterTarget" + } + } + } + }, "v1.CreateConfigRequest": { "required": [ "name", "alias", "description", - "project", - "componentType" + "template" ], "properties": { "alias": { "type": "string" }, - "componentType": { - "type": "string" - }, "description": { "type": "string" }, "name": { "type": "string" }, - "project": { - "type": "string" - }, "properties": { "type": "string" + }, + "template": { + "$ref": "#/definitions/v1.NamespacedName" } } }, @@ -9509,11 +10059,11 @@ }, "v1.DetailAddonResponse": { "required": [ + "invisible", + "version", "name", "description", "icon", - "version", - "invisible", "schema", "uiSchema", "definitions", @@ -9593,13 +10143,13 @@ }, "v1.DetailApplicationResponse": { "required": [ - "description", - "icon", "name", "alias", "project", - "createTime", "updateTime", + "description", + "createTime", + "icon", "policies", "envBindings", "resourceInfo" @@ -9656,20 +10206,20 @@ }, "v1.DetailClusterResponse": { "required": [ - "createTime", - "status", - "kubeConfig", - "updateTime", - "dashboardURL", "name", "description", - "icon", - "labels", "reason", + "createTime", "alias", "provider", - "apiServerURL", + "kubeConfig", + "labels", + "dashboardURL", "kubeConfigSecret", + "updateTime", + "icon", + "status", + "apiServerURL", "resourceInfo" ], "properties": { @@ -9727,14 +10277,14 @@ }, "v1.DetailComponentResponse": { "required": [ - "appPrimaryKey", "alias", - "createTime", - "main", - "name", "creator", - "type", "updateTime", + "createTime", + "name", + "type", + "main", + "appPrimaryKey", "definition" ], "properties": { @@ -9772,7 +10322,7 @@ "inputs": { "type": "array", "items": { - "$ref": "#/definitions/common.inputItem" + "$ref": "#/definitions/v1alpha1.inputItem" } }, "labels": { @@ -9790,7 +10340,7 @@ "outputs": { "type": "array", "items": { - "$ref": "#/definitions/common.outputItem" + "$ref": "#/definitions/v1alpha1.outputItem" } }, "properties": { @@ -9822,13 +10372,13 @@ }, "v1.DetailDefinitionResponse": { "required": [ + "status", + "ownerAddon", "name", + "alias", + "labels", "description", "icon", - "alias", - "status", - "labels", - "ownerAddon", "schema", "uiSchema" ], @@ -9885,15 +10435,15 @@ }, "v1.DetailPolicyResponse": { "required": [ - "creator", - "updateTime", - "envName", - "name", "alias", "type", - "description", "properties", - "createTime" + "name", + "description", + "creator", + "createTime", + "updateTime", + "envName" ], "properties": { "alias": { @@ -9935,17 +10485,18 @@ }, "v1.DetailRevisionResponse": { "required": [ - "updateTime", - "createTime", - "version", + "revisionCRName", "reason", - "envName", - "appPrimaryKey", "deployUser", - "note", - "status", + "triggerType", + "version", + "updateTime", + "appPrimaryKey", "workflowName", - "triggerType" + "createTime", + "note", + "envName", + "status" ], "properties": { "appPrimaryKey": { @@ -9976,6 +10527,9 @@ "reason": { "type": "string" }, + "revisionCRName": { + "type": "string" + }, "rollbackVersion": { "type": "string" }, @@ -9999,10 +10553,10 @@ }, "v1.DetailTargetResponse": { "required": [ - "updateTime", + "project", "name", "createTime", - "project" + "updateTime" ], "properties": { "alias": { @@ -10042,11 +10596,11 @@ }, "v1.DetailUserResponse": { "required": [ + "disabled", "createTime", "lastLoginTime", "name", "email", - "disabled", "projects", "roles" ], @@ -10145,12 +10699,12 @@ "v1.DetailWorkflowResponse": { "required": [ "envName", + "updateTime", "name", "alias", - "default", - "updateTime", - "description", "enable", + "default", + "description", "createTime" ], "properties": { @@ -10627,6 +11181,45 @@ } } }, + "v1.ListConfigDistributionResponse": { + "required": [ + "distributions" + ], + "properties": { + "distributions": { + "type": "array", + "items": { + "$ref": "#/definitions/config.Distribution" + } + } + } + }, + "v1.ListConfigResponse": { + "required": [ + "configs" + ], + "properties": { + "configs": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.Config" + } + } + } + }, + "v1.ListConfigTemplateResponse": { + "required": [ + "templates" + ], + "properties": { + "templates": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ConfigTemplate" + } + } + } + }, "v1.ListDefinitionResponse": { "required": [ "definitions" @@ -10774,6 +11367,19 @@ } } }, + "v1.ListTerraformProviderResponse": { + "required": [ + "providers" + ], + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.TerraformProvider" + } + } + } + }, "v1.ListUserResponse": { "required": [ "users", @@ -10954,6 +11560,19 @@ } } }, + "v1.NamespacedName": { + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + } + } + }, "v1.ObjectReference": { "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", "properties": { @@ -11139,11 +11758,15 @@ "v1.ProjectUserBase": { "required": [ "name", + "alias", "userRoles", "createTime", "updateTime" ], "properties": { + "alias": { + "type": "string" + }, "createTime": { "type": "string", "format": "date-time" @@ -11286,6 +11909,18 @@ "loginType" ], "properties": { + "dexUserDefaultPlatformRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "dexUserDefaultProjects": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ProjectRef" + } + }, "enableCollection": { "type": "boolean" }, @@ -11307,6 +11942,12 @@ "loginType" ], "properties": { + "dexUserDefaultProjects": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ProjectRef" + } + }, "enableCollection": { "type": "boolean" }, @@ -11326,6 +11967,18 @@ "systemVersion" ], "properties": { + "dexUserDefaultPlatformRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "dexUserDefaultProjects": { + "type": "array", + "items": { + "$ref": "#/definitions/model.ProjectRef" + } + }, "enableCollection": { "type": "boolean" }, @@ -11404,6 +12057,29 @@ } } }, + "v1.TerraformProvider": { + "required": [ + "name", + "region", + "provider", + "createTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "name": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "region": { + "type": "string" + } + } + }, "v1.UpdateAddonRegistryRequest": { "properties": { "git": { @@ -11489,6 +12165,23 @@ } } }, + "v1.UpdateConfigRequest": { + "required": [ + "alias", + "description" + ], + "properties": { + "alias": { + "type": "string" + }, + "description": { + "type": "string" + }, + "properties": { + "type": "string" + } + } + }, "v1.UpdateDefinitionStatusRequest": { "required": [ "type", @@ -11609,6 +12302,28 @@ } } }, + "v1.UpdateUserRequest": { + "required": [ + "roles" + ], + "properties": { + "alias": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.UpdateWorkflowRequest": { "required": [ "default" @@ -11789,7 +12504,7 @@ "inputs": { "type": "array", "items": { - "$ref": "#/definitions/common.inputItem" + "$ref": "#/definitions/v1alpha1.inputItem" } }, "name": { @@ -11798,7 +12513,7 @@ "outputs": { "type": "array", "items": { - "$ref": "#/definitions/common.outputItem" + "$ref": "#/definitions/v1alpha1.outputItem" } }, "properties": { @@ -11809,6 +12524,213 @@ } } }, + "v1alpha1.StepStatus": { + "required": [ + "id" + ], + "properties": { + "firstExecuteTime": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastExecuteTime": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1alpha1.WorkflowExecuteMode": { + "properties": { + "steps": { + "type": "string" + }, + "subSteps": { + "type": "string" + } + } + }, + "v1alpha1.WorkflowStep": { + "required": [ + "type", + "name" + ], + "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "if": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.inputItem" + } + }, + "meta": { + "$ref": "#/definitions/v1alpha1.WorkflowStepMeta" + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.outputItem" + } + }, + "properties": { + "type": "string" + }, + "subSteps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.WorkflowStepBase" + } + }, + "timeout": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1alpha1.WorkflowStepBase": { + "required": [ + "name", + "type" + ], + "properties": { + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "if": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.inputItem" + } + }, + "meta": { + "$ref": "#/definitions/v1alpha1.WorkflowStepMeta" + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.outputItem" + } + }, + "properties": { + "type": "string" + }, + "timeout": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1alpha1.WorkflowStepMeta": { + "properties": { + "alias": { + "type": "string" + } + } + }, + "v1alpha1.WorkflowStepStatus": { + "required": [ + "id" + ], + "properties": { + "firstExecuteTime": { + "type": "string" + }, + "id": { + "type": "string" + }, + "lastExecuteTime": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "subSteps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.StepStatus" + } + }, + "type": { + "type": "string" + } + } + }, + "v1alpha1.inputItem": { + "required": [ + "parameterKey", + "from" + ], + "properties": { + "from": { + "type": "string" + }, + "parameterKey": { + "type": "string" + } + } + }, + "v1alpha1.outputItem": { + "required": [ + "valueFrom", + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "valueFrom": { + "type": "string" + } + } + }, "v1beta1.AppPolicy": { "required": [ "name", @@ -11927,8 +12849,8 @@ "schematic": { "$ref": "#/definitions/common.Schematic" }, - "skipRevisionAffect": { - "type": "boolean" + "stage": { + "type": "string" }, "status": { "$ref": "#/definitions/common.Status" @@ -11941,7 +12863,7 @@ "v1beta1.Workflow": { "properties": { "mode": { - "$ref": "#/definitions/v1beta1.WorkflowExecuteMode" + "$ref": "#/definitions/v1alpha1.WorkflowExecuteMode" }, "ref": { "type": "string" @@ -11949,71 +12871,11 @@ "steps": { "type": "array", "items": { - "$ref": "#/definitions/v1beta1.WorkflowStep" + "$ref": "#/definitions/v1alpha1.WorkflowStep" } } } }, - "v1beta1.WorkflowExecuteMode": { - "properties": { - "steps": { - "type": "string" - }, - "subSteps": { - "type": "string" - } - } - }, - "v1beta1.WorkflowStep": { - "required": [ - "name", - "type" - ], - "properties": { - "dependsOn": { - "type": "array", - "items": { - "type": "string" - } - }, - "if": { - "type": "string" - }, - "inputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.inputItem" - } - }, - "meta": { - "$ref": "#/definitions/common.WorkflowStepMeta" - }, - "name": { - "type": "string" - }, - "outputs": { - "type": "array", - "items": { - "$ref": "#/definitions/common.outputItem" - } - }, - "properties": { - "type": "string" - }, - "subSteps": { - "type": "array", - "items": { - "$ref": "#/definitions/common.WorkflowSubStep" - } - }, - "timeout": { - "type": "string" - }, - "type": { - "type": "string" - } - } - }, "v1beta1.WorkflowStepDefinitionSpec": { "properties": { "definitionRef": { diff --git a/docs/examples/config/dex-connector/README.md b/docs/examples/config/dex-connector/README.md new file mode 100644 index 000000000..310118760 --- /dev/null +++ b/docs/examples/config/dex-connector/README.md @@ -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< 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) diff --git a/pkg/addon/testdata/example/parameter.cue b/pkg/addon/testdata/example/parameter.cue index 2b0222f39..d27ccfa2d 100644 --- a/pkg/addon/testdata/example/parameter.cue +++ b/pkg/addon/testdata/example/parameter.cue @@ -1,3 +1,4 @@ parameter: { - example: string -} \ No newline at end of file + //+usage=the example field + example: string +} diff --git a/pkg/addon/type.go b/pkg/addon/type.go index d39447b42..f3326babf 100644 --- a/pkg/addon/type.go +++ b/pkg/addon/type.go @@ -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"` diff --git a/pkg/apiserver/domain/model/project.go b/pkg/apiserver/domain/model/project.go index ff86f7dd5..6911b06b8 100644 --- a/pkg/apiserver/domain/model/project.go +++ b/pkg/apiserver/domain/model/project.go @@ -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" diff --git a/pkg/apiserver/domain/service/application.go b/pkg/apiserver/domain/service/application.go index df0a30404..4183b2a7b 100644 --- a/pkg/apiserver/domain/service/application.go +++ b/pkg/apiserver/domain/service/application.go @@ -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 . diff --git a/pkg/apiserver/domain/service/authentication_test.go b/pkg/apiserver/domain/service/authentication_test.go index d227f68bc..7280e90a3 100644 --- a/pkg/apiserver/domain/service/authentication_test.go +++ b/pkg/apiserver/domain/service/authentication_test.go @@ -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", }, diff --git a/pkg/apiserver/domain/service/config.go b/pkg/apiserver/domain/service/config.go index a7c6854be..0f062988a 100644 --- a/pkg/apiserver/domain/service/config.go +++ b/pkg/apiserver/domain/service/config.go @@ -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) } diff --git a/pkg/apiserver/domain/service/config_test.go b/pkg/apiserver/domain/service/config_test.go index 7e3017593..238ee8fff 100644 --- a/pkg/apiserver/domain/service/config_test.go +++ b/pkg/apiserver/domain/service/config_test.go @@ -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, ¶mErr)).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)) + }) +}) diff --git a/pkg/apiserver/domain/service/definition.go b/pkg/apiserver/domain/service/definition.go index 0e29a6358..28eaa7821 100644 --- a/pkg/apiserver/domain/service/definition.go +++ b/pkg/apiserver/domain/service/definition.go @@ -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 { diff --git a/pkg/apiserver/domain/service/helm.go b/pkg/apiserver/domain/service/helm.go index 82a791024..62f785e32 100644 --- a/pkg/apiserver/domain/service/helm.go +++ b/pkg/apiserver/domain/service/helm.go @@ -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}) } } diff --git a/pkg/apiserver/domain/service/helm_test.go b/pkg/apiserver/domain/service/helm_test.go index 290957574..c5076cf13 100644 --- a/pkg/apiserver/domain/service/helm_test.go +++ b/pkg/apiserver/domain/service/helm_test.go @@ -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 diff --git a/pkg/apiserver/domain/service/image.go b/pkg/apiserver/domain/service/image.go index fbf892623..1bec4ad32 100644 --- a/pkg/apiserver/domain/service/image.go +++ b/pkg/apiserver/domain/service/image.go @@ -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 { diff --git a/pkg/apiserver/domain/service/project.go b/pkg/apiserver/domain/service/project.go index 8f23c6238..73227e896 100644 --- a/pkg/apiserver/domain/service/project.go +++ b/pkg/apiserver/domain/service/project.go @@ -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} diff --git a/pkg/apiserver/domain/service/project_test.go b/pkg/apiserver/domain/service/project_test.go index 60ba73484..f87dbc4a7 100644 --- a/pkg/apiserver/domain/service/project_test.go +++ b/pkg/apiserver/domain/service/project_test.go @@ -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()) diff --git a/pkg/apiserver/domain/service/rbac.go b/pkg/apiserver/domain/service/rbac.go index 59c28a626..49937d7c3 100644 --- a/pkg/apiserver/domain/service/rbac.go +++ b/pkg/apiserver/domain/service/rbac.go @@ -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) diff --git a/pkg/apiserver/domain/service/suite_test.go b/pkg/apiserver/domain/service/suite_test.go index 8a2e9f909..850a910b8 100644 --- a/pkg/apiserver/domain/service/suite_test.go +++ b/pkg/apiserver/domain/service/suite_test.go @@ -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") diff --git a/pkg/apiserver/domain/service/system_info.go b/pkg/apiserver/domain/service/system_info.go index e184e2188..78d243f01 100644 --- a/pkg/apiserver/domain/service/system_info.go +++ b/pkg/apiserver/domain/service/system_info.go @@ -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) diff --git a/pkg/apiserver/domain/service/testdata/crds/provider-crd.yaml b/pkg/apiserver/domain/service/testdata/crds/provider-crd.yaml new file mode 100644 index 000000000..f0846cd10 --- /dev/null +++ b/pkg/apiserver/domain/service/testdata/crds/provider-crd.yaml @@ -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: [] \ No newline at end of file diff --git a/pkg/apiserver/interfaces/api/config.go b/pkg/apiserver/interfaces/api/config.go index e16ca3935..b963d3418 100644 --- a/pkg/apiserver/interfaces/api/config.go +++ b/pkg/apiserver/interfaces/api/config.go @@ -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 diff --git a/pkg/apiserver/interfaces/api/dto/v1/types.go b/pkg/apiserver/interfaces/api/dto/v1/types.go index 0ad09c7f3..002e79dbd 100644 --- a/pkg/apiserver/interfaces/api/dto/v1/types.go +++ b/pkg/apiserver/interfaces/api/dto/v1/types.go @@ -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"` +} diff --git a/pkg/apiserver/interfaces/api/interfaces.go b/pkg/apiserver/interfaces/api/interfaces.go index a26ae3dfb..d7fa9b7a2 100644 --- a/pkg/apiserver/interfaces/api/interfaces.go +++ b/pkg/apiserver/interfaces/api/interfaces.go @@ -71,6 +71,7 @@ func InitAPIBean() []interface{} { // Config management RegisterAPIInterface(ConfigAPIInterface()) + RegisterAPIInterface(ConfigTemplateAPIInterface()) // Resources RegisterAPIInterface(NewClusterAPIInterface()) diff --git a/pkg/apiserver/interfaces/api/interfaces_test.go b/pkg/apiserver/interfaces/api/interfaces_test.go index 7af3146d6..fe8ceb99a 100644 --- a/pkg/apiserver/interfaces/api/interfaces_test.go +++ b/pkg/apiserver/interfaces/api/interfaces_test.go @@ -23,5 +23,5 @@ import ( ) func TestInitAPIBean(t *testing.T) { - assert.Equal(t, len(InitAPIBean()), 22) + assert.Equal(t, len(InitAPIBean()), 23) } diff --git a/pkg/apiserver/interfaces/api/project.go b/pkg/apiserver/interfaces/api/project.go index b0815ce7c..8a7c548de 100644 --- a/pkg/apiserver/interfaces/api/project.go +++ b/pkg/apiserver/interfaces/api/project.go @@ -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 - } + err = res.WriteEntity(apis.ListConfigTemplateResponse{Templates: templates}) + if err != nil { + bcode.ReturnError(req, res, err) return } - err = res.WriteEntity(configs) +} + +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 diff --git a/pkg/apiserver/server.go b/pkg/apiserver/server.go index 76342fc46..6d52043b5 100644 --- a/pkg/apiserver/server.go +++ b/pkg/apiserver/server.go @@ -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) diff --git a/pkg/apiserver/utils/bcode/016_config.go b/pkg/apiserver/utils/bcode/016_config.go new file mode 100644 index 000000000..f7de5cd7a --- /dev/null +++ b/pkg/apiserver/utils/bcode/016_config.go @@ -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") +) diff --git a/pkg/apiserver/utils/dex.go b/pkg/apiserver/utils/dex.go index 6ac1cb40b..3805e43ba 100644 --- a/pkg/apiserver/utils/dex.go +++ b/pkg/apiserver/utils/dex.go @@ -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, + } } } diff --git a/pkg/apiserver/utils/dex_test.go b/pkg/apiserver/utils/dex_test.go index 8be9672ca..bc26715af 100644 --- a/pkg/apiserver/utils/dex_test.go +++ b/pkg/apiserver/utils/dex_test.go @@ -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", }, diff --git a/pkg/cmd/factory.go b/pkg/cmd/factory.go index 135b22ae1..29714e519 100644 --- a/pkg/cmd/factory.go +++ b/pkg/cmd/factory.go @@ -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 +} diff --git a/pkg/config/config_suite_test.go b/pkg/config/config_suite_test.go new file mode 100644 index 000000000..66a326d25 --- /dev/null +++ b/pkg/config/config_suite_test.go @@ -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") + +} diff --git a/pkg/config/context/context.go b/pkg/config/context/context.go new file mode 100644 index 000000000..48432bdee --- /dev/null +++ b/pkg/config/context/context.go @@ -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) diff --git a/pkg/config/factory.go b/pkg/config/factory.go new file mode 100644 index 000000000..13d39bda6 --- /dev/null +++ b/pkg/config/factory.go @@ -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) +} diff --git a/pkg/config/factory_test.go b/pkg/config/factory_test.go new file mode 100644 index 000000000..30818b2ee --- /dev/null +++ b/pkg/config/factory_test.go @@ -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()) + }) +}) diff --git a/pkg/config/testdata/helm-repo.cue b/pkg/config/testdata/helm-repo.cue new file mode 100644 index 000000000..e990813e5 --- /dev/null +++ b/pkg/config/testdata/helm-repo.cue @@ -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 + } +} diff --git a/pkg/config/testdata/mysql-db-nacos.cue b/pkg/config/testdata/mysql-db-nacos.cue new file mode 100644 index 000000000..d527e75a8 --- /dev/null +++ b/pkg/config/testdata/mysql-db-nacos.cue @@ -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 + } + } +} diff --git a/pkg/config/testdata/nacos-server.cue b/pkg/config/testdata/nacos-server.cue new file mode 100644 index 000000000..93d45e30f --- /dev/null +++ b/pkg/config/testdata/nacos-server.cue @@ -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 + } + } +} diff --git a/pkg/config/writer/nacos.go b/pkg/config/writer/nacos.go new file mode 100644 index 000000000..5d0aebefc --- /dev/null +++ b/pkg/config/writer/nacos.go @@ -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 +} diff --git a/pkg/config/writer/nacos_test.go b/pkg/config/writer/nacos_test.go new file mode 100644 index 000000000..0a938ad1d --- /dev/null +++ b/pkg/config/writer/nacos_test.go @@ -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) +} diff --git a/pkg/config/writer/writer.go b/pkg/config/writer/writer.go new file mode 100644 index 000000000..d4bcff85d --- /dev/null +++ b/pkg/config/writer/writer.go @@ -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 +} diff --git a/pkg/config/writer/writter_test.go b/pkg/config/writer/writter_test.go new file mode 100644 index 000000000..4390e2a0d --- /dev/null +++ b/pkg/config/writer/writter_test.go @@ -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}]}}}`) +} diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller.go index 0b2b4366d..d16a1e574 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_controller.go @@ -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) diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_suit_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_suit_test.go index 608482ba6..8a571fc29 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_suit_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/components/componentdefinition/componentdefinition_suit_test.go @@ -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, }, diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/policydefinition_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/policydefinition_controller.go index eab5bac43..6f94cddc0 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/policydefinition_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/policydefinition_controller.go @@ -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, diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/suite_test.go index dde074d6a..7b62ba618 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/policies/policydefinition/suite_test.go @@ -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()) diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller.go index 0a3482602..6b058b979 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_controller.go @@ -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) diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_suite_test.go index dfd50a1a7..61bf7faa4 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/traits/traitdefinition/traitdefinition_suite_test.go @@ -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, }, diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/suite_test.go b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/suite_test.go index f57720d95..dbd480c09 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/suite_test.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/suite_test.go @@ -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, }, diff --git a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go index cf9f283cb..85e294cf5 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/core/workflow/workflowstepdefinition/workflowstepdefinition_controller.go @@ -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) diff --git a/pkg/controller/utils/capability.go b/pkg/controller/utils/capability.go index 6ff51942b..acdfb37b6 100644 --- a/pkg/controller/utils/capability.go +++ b/pkg/controller/utils/capability.go @@ -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 -} diff --git a/pkg/controller/utils/capability_integrate_test.go b/pkg/controller/utils/capability_integrate_test.go index 3fc581f90..6075fa52a 100644 --- a/pkg/controller/utils/capability_integrate_test.go +++ b/pkg/controller/utils/capability_integrate_test.go @@ -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())) }) diff --git a/pkg/controller/utils/capability_test.go b/pkg/controller/utils/capability_test.go index 677765bfc..4e8ac5689 100644 --- a/pkg/controller/utils/capability_test.go +++ b/pkg/controller/utils/capability_test.go @@ -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", diff --git a/pkg/cue/base_template.go b/pkg/cue/base_template.go index 51eebc4c7..49083540c 100644 --- a/pkg/cue/base_template.go +++ b/pkg/cue/base_template.go @@ -24,5 +24,6 @@ context: { name: string value: string }] + ... } ` diff --git a/pkg/cue/convert.go b/pkg/cue/convert.go index d27c42c19..50cf6c096 100644 --- a/pkg/cue/convert.go +++ b/pkg/cue/convert.go @@ -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") +} diff --git a/pkg/cue/script/schema.go b/pkg/cue/script/schema.go new file mode 100644 index 000000000..fe86f9c2f --- /dev/null +++ b/pkg/cue/script/schema.go @@ -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 +} diff --git a/pkg/cue/script/schema_test.go b/pkg/cue/script/schema_test.go new file mode 100644 index 000000000..6451a9af2 --- /dev/null +++ b/pkg/cue/script/schema_test.go @@ -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)) + }) + } +} diff --git a/pkg/cue/script/template.go b/pkg/cue/script/template.go new file mode 100644 index 000000000..6846d1371 --- /dev/null +++ b/pkg/cue/script/template.go @@ -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 +} diff --git a/pkg/cue/script/template_test.go b/pkg/cue/script/template_test.go new file mode 100644 index 000000000..15182cb99 --- /dev/null +++ b/pkg/cue/script/template_test.go @@ -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) +} diff --git a/pkg/controller/utils/testdata/definition/arrayWithoutItemsSchema.json b/pkg/cue/script/testdata/arrayWithoutItemsSchema.json similarity index 100% rename from pkg/controller/utils/testdata/definition/arrayWithoutItemsSchema.json rename to pkg/cue/script/testdata/arrayWithoutItemsSchema.json diff --git a/pkg/controller/utils/testdata/definition/arrayWithoutItemsSchemaFixed.json b/pkg/cue/script/testdata/arrayWithoutItemsSchemaFixed.json similarity index 100% rename from pkg/controller/utils/testdata/definition/arrayWithoutItemsSchemaFixed.json rename to pkg/cue/script/testdata/arrayWithoutItemsSchemaFixed.json diff --git a/pkg/controller/utils/testdata/definition/shortTagSchema.json b/pkg/cue/script/testdata/shortTagSchema.json similarity index 100% rename from pkg/controller/utils/testdata/definition/shortTagSchema.json rename to pkg/cue/script/testdata/shortTagSchema.json diff --git a/pkg/controller/utils/testdata/definition/shortTagSchemaFixed.json b/pkg/cue/script/testdata/shortTagSchemaFixed.json similarity index 100% rename from pkg/controller/utils/testdata/definition/shortTagSchemaFixed.json rename to pkg/cue/script/testdata/shortTagSchemaFixed.json diff --git a/pkg/controller/utils/testdata/definition/webservice.json b/pkg/cue/script/testdata/webservice.json similarity index 100% rename from pkg/controller/utils/testdata/definition/webservice.json rename to pkg/cue/script/testdata/webservice.json diff --git a/pkg/controller/utils/testdata/definition/webserviceFixed.json b/pkg/cue/script/testdata/webserviceFixed.json similarity index 100% rename from pkg/controller/utils/testdata/definition/webserviceFixed.json rename to pkg/cue/script/testdata/webserviceFixed.json diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index ec9dfc95d..08339f754 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -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 diff --git a/pkg/stdlib/pkgs/config.cue b/pkg/stdlib/pkgs/config.cue new file mode 100644 index 000000000..6f037e13c --- /dev/null +++ b/pkg/stdlib/pkgs/config.cue @@ -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: [...{...}] +} diff --git a/pkg/utils/apply/apply.go b/pkg/utils/apply/apply.go index 9138df2de..720ca3067 100644 --- a/pkg/utils/apply/apply.go +++ b/pkg/utils/apply/apply.go @@ -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 { diff --git a/pkg/utils/common/common.go b/pkg/utils/common/common.go index 9a452ed0c..c3df26778 100644 --- a/pkg/utils/common/common.go +++ b/pkg/utils/common/common.go @@ -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 { diff --git a/pkg/utils/config/application.go b/pkg/utils/config/application.go deleted file mode 100644 index 7a8721cb4..000000000 --- a/pkg/utils/config/application.go +++ /dev/null @@ -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 -} diff --git a/pkg/utils/config/project.go b/pkg/utils/config/project.go deleted file mode 100644 index beb7b2751..000000000 --- a/pkg/utils/config/project.go +++ /dev/null @@ -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 -} diff --git a/pkg/utils/config/project_test.go b/pkg/utils/config/project_test.go deleted file mode 100644 index 436cbb8a2..000000000 --- a/pkg/utils/config/project_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/references/cli/cli.go b/references/cli/cli.go index a40bf5de3..b0379a383 100644 --- a/references/cli/cli.go +++ b/references/cli/cli.go @@ -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), diff --git a/references/cli/config.go b/references/cli/config.go new file mode 100644 index 000000000..a927fd21b --- /dev/null +++ b/references/cli/config.go @@ -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 [-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 [-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: /") + 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 +} diff --git a/references/cli/config_test.go b/references/cli/config_test.go new file mode 100644 index 000000000..bf7d3ef1d --- /dev/null +++ b/references/cli/config_test.go @@ -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")) +} diff --git a/references/cli/provider.go b/references/cli/provider.go index ed7c40079..880144ff2 100644 --- a/references/cli/provider.go +++ b/references/cli/provider.go @@ -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 ", + Use: "add", + Deprecated: "Please use the vela config command: \n vela config create --template [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 --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 ", } 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 -} diff --git a/references/cli/provider_test.go b/references/cli/provider_test.go index 8e41e989d..49a4a18d4 100644 --- a/references/cli/provider_test.go +++ b/references/cli/provider_test.go @@ -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) - } - } - } -} diff --git a/vela-templates/definitions/internal/component/config-image-registry.cue b/references/cli/test-data/config-templates/image-registry.cue similarity index 70% rename from vela-templates/definitions/internal/component/config-image-registry.cue rename to references/cli/test-data/config-templates/image-registry.cue index 8b3b578f7..003b859d7 100644 --- a/vela-templates/definitions/internal/component/config-image-registry.cue +++ b/references/cli/test-data/config-templates/image-registry.cue @@ -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 != _|_ { diff --git a/references/cli/test-data/config/registry.yaml b/references/cli/test-data/config/registry.yaml new file mode 100644 index 000000000..46dc1065f --- /dev/null +++ b/references/cli/test-data/config/registry.yaml @@ -0,0 +1,5 @@ +registry: docker.io +auth: + username: zhangsan + password: lisi +useHTTP: true \ No newline at end of file diff --git a/references/cli/up_test.go b/references/cli/up_test.go index 60c04bd83..bcdd0dbf1 100644 --- a/references/cli/up_test.go +++ b/references/cli/up_test.go @@ -42,6 +42,7 @@ func TestUp(t *testing.T) { app.Name = "app-up" msg := common.Info(app) assert.Contains(t, msg, "App has been deployed") + // This test case can not run in the TERM with the color. assert.Contains(t, msg, fmt.Sprintf("App status: vela status %s", app.Name)) } diff --git a/references/docgen/openapi.go b/references/docgen/openapi.go new file mode 100644 index 000000000..229d49eeb --- /dev/null +++ b/references/docgen/openapi.go @@ -0,0 +1,77 @@ +/* +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 docgen + +import ( + "bytes" + "fmt" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/olekukonko/tablewriter" +) + +// GenerateConsoleDocument generate the document shown on the console. +func GenerateConsoleDocument(title string, schema *openapi3.Schema) (string, error) { + var buffer = &bytes.Buffer{} + var printSubProperties []*openapi3.Schema + if len(schema.Properties) > 0 { + var propertiesTable = tablewriter.NewWriter(buffer) + propertiesTable.SetHeader([]string{"NAME", "TYPE", "DESCRIPTION", "REQUIRED", "OPTIONS", "DEFAULT"}) + for key, subSchema := range schema.Properties { + name := subSchema.Value.Title + if title != "" { + name = fmt.Sprintf("(%s).%s", title, name) + } + defaultValue := fmt.Sprintf("%v", subSchema.Value.Default) + if subSchema.Value.Default == nil { + defaultValue = "" + } + var options = "" + for _, enum := range subSchema.Value.Enum { + options += fmt.Sprintf("%v", enum) + } + propertiesTable.Append([]string{ + name, + subSchema.Value.Type, + subSchema.Value.Description, + fmt.Sprintf("%t", strings.Contains(strings.Join(schema.Required, "/"), subSchema.Value.Title)), + options, + defaultValue, + }) + if len(subSchema.Value.Properties) > 0 { + printSubProperties = append(printSubProperties, schema.Properties[key].Value) + } + } + buffer.WriteString(title + "\n") + propertiesTable.Render() + } + + for _, sub := range printSubProperties { + next := strings.Join([]string{title, sub.Title}, ".") + if title == "" { + next = sub.Title + } + re, err := GenerateConsoleDocument(next, sub) + if err != nil { + return "", err + } + buffer.WriteString(re) + } + + return buffer.String(), nil +} diff --git a/test/e2e-apiserver-test/config_test.go b/test/e2e-apiserver-test/config_test.go new file mode 100644 index 000000000..bdbbe9aeb --- /dev/null +++ b/test/e2e-apiserver-test/config_test.go @@ -0,0 +1,284 @@ +/* +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 e2e_apiserver_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "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/config" +) + +var tc = ` +import ( + "encoding/base64" + "encoding/json" + "strconv" +) + +metadata: { + name: "image-registry" + alias: "Image Registry" + scope: "project" + description: "Config information to authenticate image registry" + sensitive: false +} + +template: { + output: { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: context.name + namespace: context.namespace + labels: { + "config.oam.dev/catalog": "velacore-config" + "config.oam.dev/type": "image-registry" + } + } + if parameter.auth != _|_ { + type: "kubernetes.io/dockerconfigjson" + } + if parameter.auth == _|_ { + type: "Opaque" + } + stringData: { + if parameter.auth != _|_ && parameter.auth.username != _|_ { + ".dockerconfigjson": json.Marshal({ + "auths": "\(parameter.registry)": { + "username": parameter.auth.username + "password": parameter.auth.password + if parameter.auth.email != _|_ { + "email": parameter.auth.email + } + "auth": base64.Encode(null, (parameter.auth.username + ":" + parameter.auth.password)) + } + }) + } + if parameter.insecure != _|_ { + "insecure-skip-verify": strconv.FormatBool(parameter.insecure) + } + if parameter.useHTTP != _|_ { + "protocol-use-http": strconv.FormatBool(parameter.useHTTP) + } + } + } + + parameter: { + // +usage=Image registry FQDN, such as: index.docker.io + registry: 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 + } +} +` + +var _ = Describe("Test the rest api about the config", func() { + var templateName = "test-image-registry" + var projectName = "test-config" + It("Prepare a template", func() { + cf := config.NewConfigFactory(k8sClient) + it, err := cf.ParseTemplate(templateName, []byte(tc)) + Expect(err).Should(BeNil()) + err = cf.CreateOrUpdateConfigTemplate(context.TODO(), types.DefaultKubeVelaNS, it) + Expect(err).Should(BeNil()) + }) + + It("Test listing the templates", func() { + res := get("/config_templates") + var templates v1.ListConfigTemplateResponse + Expect(decodeResponseBody(res, &templates)).Should(Succeed()) + var exist bool + for _, t := range templates.Templates { + if t.Name == templateName { + exist = true + } + } + Expect(exist).Should(BeTrue()) + }) + + It("Test get a template", func() { + res := get("/config_templates/" + templateName) + var template v1.ConfigTemplateDetail + Expect(decodeResponseBody(res, &template)).Should(Succeed()) + Expect(len(template.UISchema)).Should(Equal(4)) + }) + + It("Test creating a config", func() { + req := v1.CreateConfigRequest{ + Name: "test-registry", + Alias: "Test Registry", + Description: "This is a demo config", + Template: v1.NamespacedName{Name: templateName}, + Properties: `{"registry": "kubevela.test.com"}`, + } + res := post("/configs", req) + var config v1.Config + Expect(decodeResponseBody(res, &config)).Should(Succeed()) + Expect(config.Alias).Should(Equal(req.Alias)) + Expect(config.Name).Should(Equal(req.Name)) + Expect(config.Description).Should(Equal(req.Description)) + Expect(config.Template.Name).Should(Equal(templateName)) + Expect(config.Sensitive).Should(BeFalse()) + Expect(config.Secret).Should(BeNil()) + Expect(config.Properties["registry"]).Should(Equal("kubevela.test.com")) + + By("the template is not exist") + req = v1.CreateConfigRequest{ + Name: "test-registry", + Alias: "Test Registry", + Description: "This is a demo config", + Template: v1.NamespacedName{Name: templateName + "notfound"}, + Properties: `{"registry": "kubevela.test.com"}`, + } + res = post("/configs", req) + Expect(res.StatusCode).Should(Equal(404)) + + By("without the template") + + req = v1.CreateConfigRequest{ + Name: "test-config", + Alias: "Test Config", + Description: "This is a demo config", + Properties: `{"url": "kubevela.test.com"}`, + } + res = post("/configs", req) + config = v1.Config{} + Expect(decodeResponseBody(res, &config)).Should(Succeed()) + Expect(config.Properties["url"]).Should(Equal("kubevela.test.com")) + }) + + It("Test getting a config", func() { + res := get(fmt.Sprintf("/configs/%s", "test-registry-not-found")) + Expect(res.StatusCode).Should(Equal(404)) + res = get(fmt.Sprintf("/configs/%s", "test-registry")) + var config v1.Config + Expect(decodeResponseBody(res, &config)).Should(Succeed()) + Expect(config.Alias).Should(Equal("Test Registry")) + Expect(config.Name).Should(Equal("test-registry")) + Expect(config.Description).Should(Equal("This is a demo config")) + Expect(config.Template.Name).Should(Equal(templateName)) + Expect(config.Sensitive).Should(BeFalse()) + Expect(config.Secret).Should(BeNil()) + Expect(config.Properties["registry"]).Should(Equal("kubevela.test.com")) + }) + + It("Test updating a config", func() { + req := v1.UpdateConfigRequest{ + Alias: "Test Registry Alias", + Description: "This is a test config", + Properties: `{"registry": "kubevela.test.cn"}`, + } + res := put(fmt.Sprintf("/configs/%s", "test-registry"), req) + var config v1.Config + Expect(decodeResponseBody(res, &config)).Should(Succeed()) + Expect(config.Alias).Should(Equal("Test Registry Alias")) + Expect(config.Description).Should(Equal("This is a test config")) + Expect(config.Template.Name).Should(Equal(templateName)) + Expect(config.Sensitive).Should(BeFalse()) + Expect(config.Secret).Should(BeNil()) + Expect(config.Properties["registry"]).Should(Equal("kubevela.test.cn")) + }) + + It("Test creating a config to a project", func() { + + delete(fmt.Sprintf("/projects/" + projectName)) + + createProject := v1.CreateProjectRequest{ + Name: projectName, + Alias: "Test Config", + } + res := post("/projects", createProject) + var project v1.ProjectBase + Expect(decodeResponseBody(res, &project)).Should(Succeed()) + req := v1.CreateConfigRequest{ + Name: "test-project-registry", + Alias: "Test Registry", + Description: "This is a demo config", + Template: v1.NamespacedName{Name: templateName}, + Properties: `{"registry": "kubevela.test.com"}`, + } + res = post(fmt.Sprintf("/projects/%s/configs", projectName), req) + var config v1.Config + Expect(decodeResponseBody(res, &config)).Should(Succeed()) + Expect(config.Project).Should(Equal(projectName)) + }) + + It("Test distributing a project", func() { + req := v1.CreateConfigDistributionRequest{ + Name: "distribute-test-registry", + Configs: []*v1.NamespacedName{{Name: "test-registry", Namespace: "vela-system"}}, + Targets: []*v1.ClusterTarget{{ClusterName: "local", Namespace: "test"}}, + } + res := post(fmt.Sprintf("/projects/%s/distributions", projectName), req) + Expect(res.StatusCode).Should(Equal(200)) + }) + + It("Test listing the configs from a project", func() { + res := get(fmt.Sprintf("/projects/%s/configs?template="+templateName, projectName)) + var configs v1.ListConfigResponse + Expect(decodeResponseBody(res, &configs)).Should(Succeed()) + Expect(len(configs.Configs)).Should(Equal(2)) + var targetLength = 0 + fmt.Printf("%+v \n%+v", configs.Configs[0], configs.Configs[1]) + for _, config := range configs.Configs { + if config.Name == "test-registry" { + targetLength = len(config.Targets) + } + } + Expect(targetLength).Should(Equal(1)) + }) + + It("Test listing the distribution from a project", func() { + res := get(fmt.Sprintf("/projects/%s/distributions", projectName)) + var configs v1.ListConfigDistributionResponse + Expect(decodeResponseBody(res, &configs)).Should(Succeed()) + Expect(len(configs.Distributions)).Should(Equal(1)) + }) + + It("Test delete a distribution from a project", func() { + res := delete(fmt.Sprintf("/projects/%s/distributions/distribute-test-registry", projectName)) + Expect(res.StatusCode).Should(Equal(200)) + + res = delete(fmt.Sprintf("/projects/%s/distributions/distribute-not-found", projectName)) + Expect(res.StatusCode).Should(Equal(404)) + }) + + It("Test deleting a config", func() { + res := delete(fmt.Sprintf("/configs/%s", "test-registry")) + Expect(res.StatusCode).Should(Equal(200)) + + res = delete(fmt.Sprintf("/projects/%s/configs/%s", projectName, "test-project-registry")) + Expect(res.StatusCode).Should(Equal(200)) + }) +}) diff --git a/test/mock/nacos/config_client.go b/test/mock/nacos/config_client.go new file mode 100644 index 000000000..1be038a41 --- /dev/null +++ b/test/mock/nacos/config_client.go @@ -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 nacos + +import ( + "reflect" + + "github.com/golang/mock/gomock" + "github.com/nacos-group/nacos-sdk-go/v2/model" + "github.com/nacos-group/nacos-sdk-go/v2/vo" +) + +// MockIConfigClient is a mock of IConfigClient interface +type MockIConfigClient struct { + ctrl *gomock.Controller + recorder *MockIConfigClientMockRecorder +} + +// MockIConfigClientMockRecorder is the mock recorder for MockIConfigClient +type MockIConfigClientMockRecorder struct { + mock *MockIConfigClient +} + +// NewMockIConfigClient creates a new mock instance +func NewMockIConfigClient(ctrl *gomock.Controller) *MockIConfigClient { + mock := &MockIConfigClient{ctrl: ctrl} + mock.recorder = &MockIConfigClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockIConfigClient) EXPECT() *MockIConfigClientMockRecorder { + return m.recorder +} + +// GetConfig mock getting the config +func (m *MockIConfigClient) GetConfig(param vo.ConfigParam) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfig", param) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetConfig indicates an expected call of GetConfig +func (mr *MockIConfigClientMockRecorder) GetConfig(param interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockIConfigClient)(nil).GetConfig), param) +} + +// PublishConfig use to publish config to nacos server +// dataId require +// group require +// content require +// tenant ==>nacos.namespace optional +func (m *MockIConfigClient) PublishConfig(param vo.ConfigParam) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PublishConfig", param) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PublishConfig indicates an expected call of PublishConfig +func (mr *MockIConfigClientMockRecorder) PublishConfig(param interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishConfig", reflect.TypeOf((*MockIConfigClient)(nil).PublishConfig), param) +} + +// DeleteConfig use to delete config +// dataId require +// group require +// tenant ==>nacos.namespace optional +func (m *MockIConfigClient) DeleteConfig(param vo.ConfigParam) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteConfig", param) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteConfig indicates an expected call of DeleteConfig +func (mr *MockIConfigClientMockRecorder) DeleteConfig(param interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteConfig", reflect.TypeOf((*MockIConfigClient)(nil).DeleteConfig), param) +} + +// ListenConfig use to listen config change,it will callback OnChange() when config change +// dataId require +// group require +// onchange require +// tenant ==>nacos.namespace optional +func (m *MockIConfigClient) ListenConfig(params vo.ConfigParam) (err error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListenConfig", params) + ret0, _ := ret[0].(error) + return ret0 +} + +// ListenConfig indicates an expected call of ListenConfig +func (mr *MockIConfigClientMockRecorder) ListenConfig(params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListenConfig", reflect.TypeOf((*MockIConfigClient)(nil).ListenConfig), params) +} + +// CancelListenConfig use to cancel listen config change +// dataId require +// group require +// tenant ==>nacos.namespace optional +func (m *MockIConfigClient) CancelListenConfig(params vo.ConfigParam) (err error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CancelListenConfig", params) + ret0, _ := ret[0].(error) + return ret0 +} + +// CancelListenConfig indicates an expected call of CancelListenConfig +func (mr *MockIConfigClientMockRecorder) CancelListenConfig(params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelListenConfig", reflect.TypeOf((*MockIConfigClient)(nil).CancelListenConfig), params) +} + +// SearchConfig use to search nacos config +// search require search=accurate--精确搜索 search=blur--模糊搜索 +// group option +// dataId option +// tenant ==>nacos.namespace optional +// pageNo option,default is 1 +// pageSize option,default is 10 +func (m *MockIConfigClient) SearchConfig(param vo.SearchConfigParm) (*model.ConfigPage, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SearchConfig", param) + ret0, _ := ret[0].(*model.ConfigPage) + ret1, _ := ret[0].(error) + return ret0, ret1 +} + +// SearchConfig indicates an expected call of SearchConfig +func (mr *MockIConfigClientMockRecorder) SearchConfig(params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SearchConfig", reflect.TypeOf((*MockIConfigClient)(nil).SearchConfig), params) +} + +// CloseClient Close the GRPC client +func (m *MockIConfigClient) CloseClient() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SearchConfig") +} + +// CloseClient indicates an expected call of CloseClient +func (mr *MockIConfigClientMockRecorder) CloseClient(params interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseClient", reflect.TypeOf((*MockIConfigClient)(nil).CloseClient), params) +} diff --git a/vela-templates/definitions/internal/workflowstep/create-config.cue b/vela-templates/definitions/internal/workflowstep/create-config.cue new file mode 100644 index 000000000..ae9ee6126 --- /dev/null +++ b/vela-templates/definitions/internal/workflowstep/create-config.cue @@ -0,0 +1,38 @@ +import ( + "vela/op" +) + +"create-config": { + type: "workflow-step" + annotations: {} + labels: {} + description: "Create or update a config" +} +template: { + 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: {...} + } +} diff --git a/vela-templates/definitions/internal/workflowstep/delete-config.cue b/vela-templates/definitions/internal/workflowstep/delete-config.cue new file mode 100644 index 000000000..48da16bcd --- /dev/null +++ b/vela-templates/definitions/internal/workflowstep/delete-config.cue @@ -0,0 +1,25 @@ +import ( + "vela/op" +) + +"delete-config": { + type: "workflow-step" + annotations: {} + labels: {} + description: "Delete a config" +} +template: { + 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 + } +} diff --git a/vela-templates/definitions/internal/workflowstep/list-config.cue b/vela-templates/definitions/internal/workflowstep/list-config.cue new file mode 100644 index 000000000..b419e0aaf --- /dev/null +++ b/vela-templates/definitions/internal/workflowstep/list-config.cue @@ -0,0 +1,27 @@ +import ( + "vela/op" +) + +"list-config": { + type: "workflow-step" + annotations: {} + labels: {} + description: "List the configs" +} +template: { + 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 + } +} diff --git a/vela-templates/definitions/internal/workflowstep/read-config.cue b/vela-templates/definitions/internal/workflowstep/read-config.cue new file mode 100644 index 000000000..65bf36255 --- /dev/null +++ b/vela-templates/definitions/internal/workflowstep/read-config.cue @@ -0,0 +1,25 @@ +import ( + "vela/op" +) + +"read-config": { + type: "workflow-step" + annotations: {} + labels: {} + description: "Read a config" +} +template: { + 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 + } +}