From 34aa74ff480479ec00d274471f1b61cbcad5ca01 Mon Sep 17 00:00:00 2001 From: Somefive Date: Sat, 20 Nov 2021 13:07:27 +0800 Subject: [PATCH] Feat: envbinding support cloud resource deploy and share (#2734) * Feat: envbinding support cloud resource deploy and bind * Fix: change bind-cloud-resource to share-cloud-resource --- .../deploy-cloud-resource.yaml | 32 ++ .../templates/defwithtemplate/deploy2env.yaml | 4 +- .../defwithtemplate/share-cloud-resource.yaml | 39 +++ .../defwithtemplate/bind-cloud-resource.yaml | 39 +++ .../deploy-cloud-resource.yaml | 32 ++ .../defwithtemplate/share-cloud-resource.yaml | 39 +++ .../deploy-and-share-cloud-resource.yaml | 78 +++++ .../application/application_controller.go | 3 +- .../v1alpha2/application/generator.go | 4 + pkg/stdlib/op.cue | 4 + pkg/stdlib/pkgs/multicluster.cue | 89 ++++-- pkg/stdlib/pkgs/terraform.cue | 280 ++++++++++++++++++ pkg/workflow/providers/kube/handle.go | 3 +- pkg/workflow/providers/terraform/terraform.go | 80 +++++ .../providers/terraform/terraform_test.go | 146 +++++++++ .../internal/deploy-cloud-resource.cue | 27 ++ .../definitions/internal/deploy2env.cue | 4 +- .../internal/share-cloud-resource.cue | 34 +++ 18 files changed, 911 insertions(+), 26 deletions(-) create mode 100644 charts/vela-core/templates/defwithtemplate/deploy-cloud-resource.yaml create mode 100644 charts/vela-core/templates/defwithtemplate/share-cloud-resource.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/bind-cloud-resource.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/deploy-cloud-resource.yaml create mode 100644 charts/vela-minimal/templates/defwithtemplate/share-cloud-resource.yaml create mode 100644 docs/examples/envbinding/deploy-and-share-cloud-resource.yaml create mode 100644 pkg/stdlib/pkgs/terraform.cue create mode 100644 pkg/workflow/providers/terraform/terraform.go create mode 100644 pkg/workflow/providers/terraform/terraform_test.go create mode 100644 vela-templates/definitions/internal/deploy-cloud-resource.cue create mode 100644 vela-templates/definitions/internal/share-cloud-resource.cue diff --git a/charts/vela-core/templates/defwithtemplate/deploy-cloud-resource.yaml b/charts/vela-core/templates/defwithtemplate/deploy-cloud-resource.yaml new file mode 100644 index 000000000..8a884ddf9 --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/deploy-cloud-resource.yaml @@ -0,0 +1,32 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/deploy-cloud-resource.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Deploy cloud resource and bind secret to clusters + name: deploy-cloud-resource + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#DeployCloudResource & { + env: parameter.env + policy: parameter.policy + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + parameter: { + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } + diff --git a/charts/vela-core/templates/defwithtemplate/deploy2env.yaml b/charts/vela-core/templates/defwithtemplate/deploy2env.yaml index 84f9fd929..ac97f8fd4 100644 --- a/charts/vela-core/templates/defwithtemplate/deploy2env.yaml +++ b/charts/vela-core/templates/defwithtemplate/deploy2env.yaml @@ -23,8 +23,8 @@ spec: namespace: context.namespace } parameter: { - // +usage=Declare the name of the policy - policy: string + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string // +usage=Declare the name of the env in policy env: string } diff --git a/charts/vela-core/templates/defwithtemplate/share-cloud-resource.yaml b/charts/vela-core/templates/defwithtemplate/share-cloud-resource.yaml new file mode 100644 index 000000000..7c1b6b178 --- /dev/null +++ b/charts/vela-core/templates/defwithtemplate/share-cloud-resource.yaml @@ -0,0 +1,39 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/share-cloud-resource.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Sync secrets created by terraform component to runtime clusters so that runtime clusters can share the created cloud resource. + name: share-cloud-resource + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#ShareCloudResource & { + env: parameter.env + policy: parameter.policy + placements: parameter.placements + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + parameter: { + env: string + // +usage=Declare the location to bind + placements: [...{ + namespace?: string + cluster?: string + }] + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/bind-cloud-resource.yaml b/charts/vela-minimal/templates/defwithtemplate/bind-cloud-resource.yaml new file mode 100644 index 000000000..0f56cf427 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/bind-cloud-resource.yaml @@ -0,0 +1,39 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/bind-cloud-resource.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Sync secrets created by terraform component to runtime clusters + name: bind-cloud-resource + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#BindCloudResource & { + env: parameter.env + policy: parameter.policy + placements: parameter.placements + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + parameter: { + env: string + // +usage=Declare the location to bind + placements: [...{ + namespace?: string + cluster?: string + }] + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/deploy-cloud-resource.yaml b/charts/vela-minimal/templates/defwithtemplate/deploy-cloud-resource.yaml new file mode 100644 index 000000000..8a884ddf9 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/deploy-cloud-resource.yaml @@ -0,0 +1,32 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/deploy-cloud-resource.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Deploy cloud resource and bind secret to clusters + name: deploy-cloud-resource + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#DeployCloudResource & { + env: parameter.env + policy: parameter.policy + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + parameter: { + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } + diff --git a/charts/vela-minimal/templates/defwithtemplate/share-cloud-resource.yaml b/charts/vela-minimal/templates/defwithtemplate/share-cloud-resource.yaml new file mode 100644 index 000000000..7c1b6b178 --- /dev/null +++ b/charts/vela-minimal/templates/defwithtemplate/share-cloud-resource.yaml @@ -0,0 +1,39 @@ +# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file. +# Definition source cue file: vela-templates/definitions/internal/share-cloud-resource.cue +apiVersion: core.oam.dev/v1beta1 +kind: WorkflowStepDefinition +metadata: + annotations: + definition.oam.dev/description: Sync secrets created by terraform component to runtime clusters so that runtime clusters can share the created cloud resource. + name: share-cloud-resource + namespace: {{.Values.systemDefinitionNamespace}} +spec: + schematic: + cue: + template: | + import ( + "vela/op" + ) + + app: op.#ShareCloudResource & { + env: parameter.env + policy: parameter.policy + placements: parameter.placements + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + parameter: { + env: string + // +usage=Declare the location to bind + placements: [...{ + namespace?: string + cluster?: string + }] + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } + diff --git a/docs/examples/envbinding/deploy-and-share-cloud-resource.yaml b/docs/examples/envbinding/deploy-and-share-cloud-resource.yaml new file mode 100644 index 000000000..596937b3c --- /dev/null +++ b/docs/examples/envbinding/deploy-and-share-cloud-resource.yaml @@ -0,0 +1,78 @@ +apiVersion: core.oam.dev/v1beta1 +kind: Application +metadata: + name: rds-app + namespace: project-1 +spec: + components: + - name: db + type: alibaba-rds + properties: + instance_name: db + account_name: kubevela + password: my-password + writeConnectionSecretToRef: + name: project-1-rds-conn-credential + policies: + - name: env-policy + type: env-binding + properties: + envs: + # 部署 RDS 给杭州集群 + - name: hangzhou + placement: + clusterSelector: + name: cluster-hangzhou + patch: + components: + - name: db + type: alibaba-rds + properties: + # region: hangzhou + instance_name: hangzhou_db + # 部署 RDS 给香港集群 + - name: hongkong + placement: + clusterSelector: + name: cluster-hongkong + namespaceSelector: + name: hk-project-1 + patch: + components: + - name: db + type: alibaba-rds + properties: + # region: hongkong + instance_name: hongkong_db + writeConnectionSecretToRef: + name: hk-project-rds-credential + + workflow: + steps: + # 部署 RDS 给杭州区用 + - name: deploy-hangzhou-rds + type: deploy-cloud-resource + properties: + env: hangzhou + # 将给杭州区用的 RDS 共享给北京区 + - name: share-hangzhou-rds-to-beijing + type: share-cloud-resource + properties: + env: hangzhou + placements: + - cluster: cluster-beijing + # 部署 RDS 给香港区用 + - name: deploy-hongkong-rds + type: deploy-cloud-resource + properties: + env: hongkong + # 将给香港区用的 RDS 共享给香港区其他项目用 + - name: share-hongkong-rds-to-other-namespace + type: share-cloud-resource + properties: + env: hongkong + placements: + - cluster: cluster-hongkong + namespace: hk-project-2 + - cluster: cluster-hongkong + namespace: hk-project-3 diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go index 807376c64..7c6937852 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go @@ -41,6 +41,7 @@ import ( "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" velatypes "github.com/oam-dev/kubevela/apis/types" "github.com/oam-dev/kubevela/pkg/appfile" + common2 "github.com/oam-dev/kubevela/pkg/controller/common" core "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev" "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1alpha2/application/assemble" "github.com/oam-dev/kubevela/pkg/cue/packages" @@ -88,7 +89,7 @@ type Reconciler struct { // Reconcile process app event // nolint:gocyclo func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - ctx, cancel := context.WithTimeout(ctx, time.Minute) + ctx, cancel := context.WithTimeout(ctx, common2.ReconcileTimeout) defer cancel() logCtx := monitorContext.NewTraceContext(ctx, "").AddTag("application", req.String(), "controller", "application") diff --git a/pkg/controller/core.oam.dev/v1alpha2/application/generator.go b/pkg/controller/core.oam.dev/v1alpha2/application/generator.go index fe1c812a5..3de532bdc 100644 --- a/pkg/controller/core.oam.dev/v1alpha2/application/generator.go +++ b/pkg/controller/core.oam.dev/v1alpha2/application/generator.go @@ -37,6 +37,7 @@ import ( "github.com/oam-dev/kubevela/pkg/workflow/providers/kube" multiclusterProvider "github.com/oam-dev/kubevela/pkg/workflow/providers/multicluster" oamProvider "github.com/oam-dev/kubevela/pkg/workflow/providers/oam" + terraformProvider "github.com/oam-dev/kubevela/pkg/workflow/providers/terraform" "github.com/oam-dev/kubevela/pkg/workflow/tasks" wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" ) @@ -54,6 +55,9 @@ func (h *AppHandler) GenerateApplicationSteps(ctx context.Context, appParser, appRev, af), h.renderComponentFunc(appParser, appRev, af)) taskDiscover := tasks.NewTaskDiscover(handlerProviders, h.r.pd, h.r.Client, h.r.dm) multiclusterProvider.Install(handlerProviders, h.r.Client, app) + terraformProvider.Install(handlerProviders, app, func(comp common.ApplicationComponent) (*appfile.Workload, error) { + return appParser.ParseWorkloadFromRevision(comp, appRev) + }) var tasks []wfTypes.TaskRunner for _, step := range af.WorkflowSteps { options := &wfTypes.GeneratorOptions{ diff --git a/pkg/stdlib/op.cue b/pkg/stdlib/op.cue index 641883aac..428ea7352 100644 --- a/pkg/stdlib/op.cue +++ b/pkg/stdlib/op.cue @@ -107,6 +107,10 @@ import ( #ApplyEnvBindApp: multicluster.#ApplyEnvBindApp +#DeployCloudResource: terraform.#DeployCloudResource + +#ShareCloudResource: terraform.#ShareCloudResource + #LoadPolicies: oam.#LoadPolicies #ListClusters: multicluster.#ListClusters diff --git a/pkg/stdlib/pkgs/multicluster.cue b/pkg/stdlib/pkgs/multicluster.cue index 119c54231..4783d4ff5 100644 --- a/pkg/stdlib/pkgs/multicluster.cue +++ b/pkg/stdlib/pkgs/multicluster.cue @@ -68,35 +68,64 @@ ... } -#ApplyEnvBindApp: { - #do: "steps" - - env: string - policy: string - app: string - namespace: string +#LoadEnvBindingEnv: #Steps & { + inputs: { + env: string + policy: string + } loadPolicies: oam.#LoadPolicies @step(1) - loadPolicy: loadPolicies.value["\(policy)"] + policy_: string + if inputs.policy == "" { + envBindingPolicies: [ for k, v in loadPolicies.value if v.type == "env-binding" {k}] + policy_: envBindingPolicies[0] + } + if inputs.policy != "" { + policy_: inputs.policy + } + + loadPolicy: loadPolicies.value["\(policy_)"] envMap: { for ev in loadPolicy.properties.envs { "\(ev.name)": ev } ... } - envConfig: envMap["\(env)"] + envConfig_: envMap["\(inputs.env)"] - placementDecisions: multicluster.#MakePlacementDecisions & { + outputs: { + policy: policy_ + envConfig: envConfig_ + } +} + +#PrepareEnvBinding: #Steps & { + inputs: { + env: string + policy: string + } + env_: inputs.env + policy_: inputs.policy + + loadEnv: #LoadEnvBindingEnv & { inputs: { - policyName: policy - envName: env + env: env_ + policy: policy_ + } + } @step(1) + envConfig: loadEnv.outputs.envConfig + + placementDecisions: #MakePlacementDecisions & { + inputs: { + policyName: loadEnv.outputs.policy + envName: env_ placement: envConfig.placement } } @step(2) - patchedApp: multicluster.#PatchApplication & { + patchedApp: #PatchApplication & { inputs: { - envName: env + envName: env_ if envConfig.selector != _|_ { selector: envConfig.selector } @@ -106,10 +135,32 @@ } } @step(3) - components: patchedApp.outputs.spec.components - apply: #Steps & { - for decision in placementDecisions.outputs.decisions { - for key, comp in components { + outputs: { + components: patchedApp.outputs.spec.components + decisions: placementDecisions.outputs.decisions + } +} + +#ApplyEnvBindApp: { + #do: "steps" + + env: string + policy: string + app: string + namespace: string + + env_: env + policy_: policy + prepare: #PrepareEnvBinding & { + inputs: { + env: env_ + policy: policy_ + } + } @step(1) + + apply: #Steps & { + for decision in prepare.outputs.decisions { + for key, comp in prepare.outputs.components { "\(decision.cluster)-\(decision.namespace)-\(key)": #ApplyComponent & { value: comp if decision.cluster != _|_ { @@ -118,7 +169,7 @@ if decision.namespace != _|_ { namespace: decision.namespace } - } @step(4) + } @step(2) } } } diff --git a/pkg/stdlib/pkgs/terraform.cue b/pkg/stdlib/pkgs/terraform.cue new file mode 100644 index 000000000..1e800d085 --- /dev/null +++ b/pkg/stdlib/pkgs/terraform.cue @@ -0,0 +1,280 @@ +#LoadTerraformComponents: { + #provider: "terraform" + #do: "load-terraform-components" + + outputs: { + components: [...multicluster.#Component] + } +} + +#GetConnectionStatus: { + #provider: "terraform" + #do: "get-connection-status" + + inputs: { + componentName: string + } + + outputs: { + healthy?: bool + } +} + +#PrepareTerraformEnvBinding: #Steps & { + inputs: { + env: string + policy: string + } + env_: inputs.env + policy_: inputs.policy + + prepare: multicluster.#PrepareEnvBinding & { + inputs: { + env: env_ + policy: policy_ + } + } @step(1) + loadTerraformComponents: #LoadTerraformComponents @step(2) + terraformComponentMap: { + for _, comp in loadTerraformComponents.outputs.components { + "\(comp.name)": comp + } + ... + } + components_: [ for comp in prepare.outputs.components if terraformComponentMap["\(comp.name)"] != _|_ {comp}] + outputs: { + components: components_ + decisions: prepare.outputs.decisions + } +} + +#loadSecretInfo: { + component: {...} + appNamespace: string + name: string + namespace: string + env: string + if component.properties != _|_ && component.properties.writeConnectionSecretToRef != _|_ { + if component.properties.writeConnectionSecretToRef.name != _|_ { + name: component.properties.writeConnectionSecretToRef.name + } + if component.properties.writeConnectionSecretToRef.name == _|_ { + name: component.name + } + if component.properties.writeConnectionSecretToRef.namespace != _|_ { + namespace: component.properties.writeConnectionSecretToRef.namespace + } + if component.properties.writeConnectionSecretToRef.namespace == _|_ { + namespace: appNamespace + } + } + envName: "\(name)-\(env)" +} + +#bindTerraformComponentToCluster: #Steps & { + comp: {...} + secret: {...} + env: string + decisions: [...{...}] + + status: terraform.#GetConnectionStatus & { + inputs: componentName: "\(comp.name)-\(env)" + } @step(1) + + read: kube.#Read & { + value: { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: secret.envName + namespace: secret.namespace + ... + } + ... + } + } @step(2) + + wait: { + #do: "wait" + continue: status.outputs.healthy && read.err == _|_ + } @step(3) + + sync: #Steps & { + for decision in decisions { + "\(decision.cluster)-\(decision.namespace)": kube.#Apply & { + cluster: decision.cluster + value: { + apiVersion: "v1" + kind: "Secret" + metadata: { + name: secret.name + if decision.namespace != _|_ && decision.namespace != "" { + namespace: decision.namespace + } + if decision.namespace == _|_ || decision.namespace == "" { + namespace: secret.namespace + } + ... + } + type: "Opaque" + data: read.value.data + ... + } + } + } + } @step(4) +} + +#DeployCloudResource: { + #do: "steps" + + env: string + name: string + policy: string + namespace: string + + env_: env + policy_: policy + prepareDeploy: #PrepareTerraformEnvBinding & { + inputs: { + env: env_ + policy: policy_ + } + } @step(1) + + deploy: #Steps & { + for comp in prepareDeploy.outputs.components { + "\(comp.name)": #Steps & { + + secretMeta: #loadSecretInfo & { + component: comp + env: env_ + appNamespace: namespace + } + + apply: #ApplyComponent & { + value: { + name: "\(comp.name)-\(env)" + properties: { + writeConnectionSecretToRef: { + name: secretMeta.envName + namespace: secretMeta.namespace + } + if comp.properties != _|_ { + for k, v in comp.properties { + if k != "writeConnectionSecretToRef" { + "\(k)": v + } + } + } + ... + } + for k, v in comp { + if k != "name" && k != "properties" { + "\(k)": v + } + } + ... + } + } @step(1) + + comp_: comp + bind: #bindTerraformComponentToCluster & { + comp: comp_ + secret: secretMeta + env: env_ + decisions: prepareDeploy.outputs.decisions + } @step(2) + + secret: bind.read.value + + update: kube.#Apply & { + value: { + metadata: { + for k, v in secret.metadata { + if k != "labels" { + "\(k)": v + } + } + labels: { + "app.oam.dev/name": name + "app.oam.dev/namespace": namespace + "app.oam.dev/component": comp.name + "app.oam.dev/env-name": env + "app.oam.dev/sync-alias": secretMeta.name + if secret.metadata.labels != _|_ { + for k, v in secret.metadata.labels { + if k != "app.oam.dev/name" && k != "app.oam.dev/sync-alias" && k != "app.oam.dev/env-name" { + "\(k)": v + } + } + } + ... + } + } + for k, v in secret { + if k != "metadata" { + "\(k)": v + } + } + ... + } + } @step(6) + } + } + ... + } @step(2) +} + +#ShareCloudResource: { + #do: "steps" + + env: string + name: string + policy: string + namespace: string + namespace_: namespace + placements: [...multicluster.#PlacementDecision] + + env_: env + policy_: policy + prepareBind: #PrepareTerraformEnvBinding & { + inputs: { + env: env_ + policy: policy_ + } + } @step(1) + + decisions_: [ for placement in placements { + namespace: *"" | string + if placement.namespace != _|_ { + namespace: placement.namespace + } + if placement.namespace == _|_ { + namespace: namespace_ + } + cluster: *"local" | string + if placement.cluster != _|_ { + cluster: placement.cluster + } + }] + + deploy: #Steps & { + for comp in prepareBind.outputs.components { + "\(comp.name)": #Steps & { + secretMeta: #loadSecretInfo & { + component: comp + env: env_ + appNamespace: namespace + } + comp_: comp + bind: #bindTerraformComponentToCluster & { + comp: comp_ + secret: secretMeta + env: env_ + decisions: decisions_ + } @step(1) + } + } + } @step(2) +} diff --git a/pkg/workflow/providers/kube/handle.go b/pkg/workflow/providers/kube/handle.go index 606933ff1..7d6f15cbb 100644 --- a/pkg/workflow/providers/kube/handle.go +++ b/pkg/workflow/providers/kube/handle.go @@ -19,12 +19,11 @@ package kube import ( "context" - "github.com/oam-dev/kubevela/apis/core.oam.dev/common" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" "github.com/oam-dev/kubevela/pkg/cue/model" "github.com/oam-dev/kubevela/pkg/cue/model/value" "github.com/oam-dev/kubevela/pkg/multicluster" diff --git a/pkg/workflow/providers/terraform/terraform.go b/pkg/workflow/providers/terraform/terraform.go new file mode 100644 index 000000000..f1cc3b7a2 --- /dev/null +++ b/pkg/workflow/providers/terraform/terraform.go @@ -0,0 +1,80 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package terraform + +import ( + "github.com/pkg/errors" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + wfContext "github.com/oam-dev/kubevela/pkg/workflow/context" + "github.com/oam-dev/kubevela/pkg/workflow/providers" + wfTypes "github.com/oam-dev/kubevela/pkg/workflow/types" +) + +const ( + // ProviderName is provider name for install. + ProviderName = "terraform" +) + +// WorkloadRenderer renderer to render application component into workload +type WorkloadRenderer func(comp common.ApplicationComponent) (*appfile.Workload, error) + +type provider struct { + app *v1beta1.Application + renderer WorkloadRenderer +} + +func (p *provider) LoadTerraformComponents(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + var components []common.ApplicationComponent + for _, comp := range p.app.Spec.Components { + wl, err := p.renderer(comp) + if err != nil { + return errors.Wrapf(err, "failed to render component into workload") + } + if wl.CapabilityCategory != types.TerraformCategory { + continue + } + components = append(components, comp) + } + return v.FillObject(components, "outputs", "components") +} + +func (p *provider) GetConnectionStatus(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error { + componentName, err := v.GetString("inputs", "componentName") + if err != nil { + return errors.Wrapf(err, "failed to get component name") + } + for _, svc := range p.app.Status.Services { + if svc.Name == componentName { + return v.FillObject(svc.Healthy, "outputs", "healthy") + } + } + return v.FillObject(false, "outputs", "healthy") +} + +// Install register handlers to provider discover. +func Install(p providers.Providers, app *v1beta1.Application, renderer WorkloadRenderer) { + prd := &provider{app: app, renderer: renderer} + p.Register(ProviderName, map[string]providers.Handler{ + "load-terraform-components": prd.LoadTerraformComponents, + "get-connection-status": prd.GetConnectionStatus, + }) +} diff --git a/pkg/workflow/providers/terraform/terraform_test.go b/pkg/workflow/providers/terraform/terraform_test.go new file mode 100644 index 000000000..eda0e0960 --- /dev/null +++ b/pkg/workflow/providers/terraform/terraform_test.go @@ -0,0 +1,146 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package terraform + +import ( + "strings" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + + apicommon "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" + "github.com/oam-dev/kubevela/pkg/appfile" + "github.com/oam-dev/kubevela/pkg/cue/model/value" + "github.com/oam-dev/kubevela/pkg/workflow/providers/mock" +) + +func fakeWorkloadRenderer(comp apicommon.ApplicationComponent) (*appfile.Workload, error) { + if strings.HasPrefix(comp.Name, "error") { + return nil, errors.New(comp.Name) + } + if strings.HasPrefix(comp.Name, "terraform") { + return &appfile.Workload{CapabilityCategory: types.TerraformCategory}, nil + } + return &appfile.Workload{CapabilityCategory: types.CUECategory}, nil +} + +func TestLoadTerraformComponents(t *testing.T) { + r := require.New(t) + testCases := []struct { + Inputs []apicommon.ApplicationComponent + HasError bool + Outputs []apicommon.ApplicationComponent + }{{ + Inputs: []apicommon.ApplicationComponent{{Name: "error"}}, + HasError: true, + }, { + Inputs: []apicommon.ApplicationComponent{{Name: "terraform-1"}, {Name: "cue"}, {Name: "terraform-2"}}, + Outputs: []apicommon.ApplicationComponent{{Name: "terraform-1"}, {Name: "terraform-2"}}, + }, { + Inputs: []apicommon.ApplicationComponent{{Name: "cue"}}, + Outputs: []apicommon.ApplicationComponent{}, + }} + for _, testCase := range testCases { + app := &v1beta1.Application{} + app.Spec.Components = testCase.Inputs + p := &provider{ + app: app, + renderer: fakeWorkloadRenderer, + } + act := &mock.Action{} + v, err := value.NewValue("", nil, "") + r.NoError(err) + err = p.LoadTerraformComponents(nil, v, act) + if testCase.HasError { + r.Error(err) + continue + } + r.NoError(err) + outputs, err := v.LookupValue("outputs", "components") + r.NoError(err) + var comps []apicommon.ApplicationComponent + r.NoError(outputs.UnmarshalTo(&comps)) + r.Equal(testCase.Outputs, comps) + } +} + +func TestGetConnectionStatus(t *testing.T) { + r := require.New(t) + testCases := []struct { + ComponentName string + Services []apicommon.ApplicationComponentStatus + Healthy bool + Error string + }{{ + ComponentName: "", + Error: "failed to get component name", + }, { + ComponentName: "comp", + Services: []apicommon.ApplicationComponentStatus{{ + Name: "not-comp", + Healthy: true, + }}, + Healthy: false, + }, { + ComponentName: "comp", + Services: []apicommon.ApplicationComponentStatus{{ + Name: "not-comp", + Healthy: true, + }, { + Name: "comp", + Healthy: true, + }}, + Healthy: true, + }, { + ComponentName: "comp", + Services: []apicommon.ApplicationComponentStatus{{ + Name: "not-comp", + Healthy: true, + }, { + Name: "comp", + Healthy: false, + }}, + Healthy: false, + }} + for _, testCase := range testCases { + app := &v1beta1.Application{} + app.Status.Services = testCase.Services + p := &provider{ + app: app, + renderer: fakeWorkloadRenderer, + } + act := &mock.Action{} + v, err := value.NewValue("", nil, "") + r.NoError(err) + if testCase.ComponentName != "" { + r.NoError(v.FillObject(map[string]string{"componentName": testCase.ComponentName}, "inputs")) + } + err = p.GetConnectionStatus(nil, v, act) + if testCase.Error != "" { + r.Error(err) + r.Contains(err.Error(), testCase.Error) + continue + } + r.NoError(err) + healthy, err := v.GetBool("outputs", "healthy") + r.NoError(err) + r.Equal(testCase.Healthy, healthy) + } +} diff --git a/vela-templates/definitions/internal/deploy-cloud-resource.cue b/vela-templates/definitions/internal/deploy-cloud-resource.cue new file mode 100644 index 000000000..f87a05166 --- /dev/null +++ b/vela-templates/definitions/internal/deploy-cloud-resource.cue @@ -0,0 +1,27 @@ +import ( + "vela/op" +) + +"deploy-cloud-resource": { + type: "workflow-step" + annotations: {} + labels: {} + description: "Deploy cloud resource and bind secret to clusters" +} +template: { + app: op.#DeployCloudResource & { + env: parameter.env + policy: parameter.policy + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + + parameter: { + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } +} diff --git a/vela-templates/definitions/internal/deploy2env.cue b/vela-templates/definitions/internal/deploy2env.cue index 23fc1268e..f71d18d33 100644 --- a/vela-templates/definitions/internal/deploy2env.cue +++ b/vela-templates/definitions/internal/deploy2env.cue @@ -18,8 +18,8 @@ template: { } parameter: { - // +usage=Declare the name of the policy - policy: string + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string // +usage=Declare the name of the env in policy env: string } diff --git a/vela-templates/definitions/internal/share-cloud-resource.cue b/vela-templates/definitions/internal/share-cloud-resource.cue new file mode 100644 index 000000000..6869ea6d8 --- /dev/null +++ b/vela-templates/definitions/internal/share-cloud-resource.cue @@ -0,0 +1,34 @@ +import ( + "vela/op" +) + +"share-cloud-resource": { + type: "workflow-step" + annotations: {} + labels: {} + description: "Sync secrets created by terraform component to runtime clusters so that runtime clusters can share the created cloud resource." +} +template: { + app: op.#ShareCloudResource & { + env: parameter.env + policy: parameter.policy + placements: parameter.placements + // context.namespace indicates the namespace of the app + namespace: context.namespace + // context.namespace indicates the name of the app + name: context.name + } + + parameter: { + env: string + // +usage=Declare the location to bind + placements: [...{ + namespace?: string + cluster?: string + }] + // +usage=Declare the name of the env-binding policy, if empty, the first env-binding policy will be used + policy: *"" | string + // +usage=Declare the name of the env in policy + env: string + } +}