mirror of
https://github.com/kubevela/kubevela.git
synced 2026-07-28 18:01:24 +00:00
Feat: Added new deploy-components workflow step (#7213)
* feat(workflow): add deploy-components step for per-component multi-cluster targeting Adds a pure-CUE workflow-step definition that lets each component in an Application resolve its own target cluster(s) via topology policies, filling the gap between apply-component (single component, static cluster) and deploy (all components, cross-product against policies). Signed-off-by: vishal210893 <vishal210893@gmail.com> * chore(workflow): add rendered deploy-components WorkflowStepDefinition manifest Generated from vela-templates/definitions/internal/workflowstep/deploy-components.cue via make def-install, for installation as part of the vela-core Helm chart. Signed-off-by: vishal210893 <vishal210893@gmail.com> * test(e2e): add multicluster e2e for deploy-components step Covers per-component topology-policy targeting (each component lands only on its resolved cluster, no cross-product) and fast-fail validation when a referenced component name is not present in the application. Signed-off-by: vishal210893 <vishal210893@gmail.com> * docs(workflow): add example doc for deploy-components step Required by the definition-doc CI check for any new WorkflowStepDefinition. Signed-off-by: vishal210893 <vishal210893@gmail.com> * fix(workflow): normalize deploy-components import block to grouped form The rendered manifest was generated with flat "import X" lines; the project's CUE formatter (as used by CI's make reviewable) renders multi-import blocks grouped as import (...). Fixes the check-diff CI job. Signed-off-by: vishal210893 <vishal210893@gmail.com> * fix(workflow): gate deploy-components applies on validation to prevent partial rollout Previously, _missingComponents was derived from deploy's own realized keys, which meant an invalid component name could only be detected after applies for valid components had already started -- a partial rollout on failure. Compute _missingComponents directly from the loaded component set instead (via iteration, matching the pattern deploy itself already uses reliably -- an indexed lookup against the same field was empirically found to evaluate before the load task resolves, causing false positives), and wrap deploy in an if-guard so no component is applied unless every referenced name is valid. Verified against a live multicluster setup: the negative case now applies zero resources before failing, and the positive case is unaffected. Signed-off-by: vishal210893 <vishal210893@gmail.com> * style(workflow): tighten deploy-components comments Comments narrated the debugging history behind each constraint; state them as plain engineering facts instead. Signed-off-by: vishal210893 <vishal210893@gmail.com> * fix(workflow): correct mistyped #do string in multicluster.#GetPlacementsFromTopologyPolicies The exported CUE symbol's #do value ("get-placements-from-tmulticlusterology-policies") never matched the registered provider handler key ("get-placements-from-topology-policies"), making it unusable by any CUE template. Fix the string and rename the symbol to match. Unused elsewhere in the tree, so no other callers are affected. deploy-components keeps its own local copy (for backport safety) but now documents that it mirrors the corrected symbol. Signed-off-by: vishal210893 <vishal210893@gmail.com> * refactor(workflow): use the shared topology-placement symbol in deploy-components Now that multicluster.#GetPlacementsFromTopologyPolicies is fixed, drop the local #GetPlacements duplicate and reference the shared symbol directly. deploy-components is new in this release, so there's no backport scenario where it could ship without the fix landing alongside it. Verified against a live multicluster controller build: positive case resolves placements and applies correctly, negative case still gates deploy on validation with zero partial rollout. Signed-off-by: vishal210893 <vishal210893@gmail.com> --------- Signed-off-by: vishal210893 <vishal210893@gmail.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/deploy-components.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: WorkflowStepDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
custom.definition.oam.dev/category: Application Delivery
|
||||
definition.oam.dev/description: Deploy each component to the cluster(s) resolved from its own topology policies. Applies are executed sequentially, one at a time -- unlike "deploy", there is no parallelism setting to configure.
|
||||
labels:
|
||||
custom.definition.oam.dev/scope: Application
|
||||
name: deploy-components
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import (
|
||||
"list"
|
||||
"strings"
|
||||
"vela/builtin"
|
||||
"vela/multicluster"
|
||||
"vela/oam"
|
||||
)
|
||||
components: oam.#LoadComponets
|
||||
|
||||
// Iterating (not an indexed lookup) avoids evaluating before "components" resolves.
|
||||
_loadedNames: [for name, _ in components.$returns.value {name}]
|
||||
|
||||
_missingComponents: [for entry in parameter.components if !list.Contains(_loadedNames, entry.name) {entry.name}]
|
||||
|
||||
if len(_missingComponents) > 0 {
|
||||
validateComponents: builtin.#Fail & {
|
||||
$params: message: "component(s) not found in application: \(strings.Join(_missingComponents, ", "))"
|
||||
}
|
||||
}
|
||||
|
||||
// Gated so nothing is applied unless every component name is valid.
|
||||
if len(_missingComponents) == 0 {
|
||||
deploy: {
|
||||
// "comp", not "value" -- shadows the $params.value field below otherwise.
|
||||
for name, comp in components.$returns.value {
|
||||
for entry in parameter.components if entry.name == name {
|
||||
"\(name)": {
|
||||
placements: multicluster.#GetPlacementsFromTopologyPolicies & {
|
||||
$params: policies: entry.policies
|
||||
}
|
||||
apply: {
|
||||
for p in placements.$returns.placements {
|
||||
"\(p.cluster)-\(p.namespace)": oam.#ApplyComponent & {
|
||||
$params: {
|
||||
value: comp
|
||||
cluster: p.cluster
|
||||
namespace: p.namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
// +usage=Per-component mapping of which topology policies determine its target cluster(s)
|
||||
components: [...{
|
||||
// +usage=the name of the component in the application to apply
|
||||
name: string
|
||||
// +usage=names of topology policies (declared at the Application level) used to resolve this component's target cluster(s)
|
||||
policies: [...string]
|
||||
}]
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
#GetPlacementsFromTmulticlusterologyPolicies: {
|
||||
#GetPlacementsFromTopologyPolicies: {
|
||||
#provider: "multicluster"
|
||||
#do: "get-placements-from-tmulticlusterology-policies"
|
||||
#do: "get-placements-from-topology-policies"
|
||||
|
||||
$params: {
|
||||
policies: [...string]
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
```yaml
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: deploy-components-example
|
||||
namespace: examples
|
||||
spec:
|
||||
components:
|
||||
- name: web-on-local
|
||||
type: webservice
|
||||
properties:
|
||||
image: nginx
|
||||
- name: web-on-worker
|
||||
type: webservice
|
||||
properties:
|
||||
image: nginx
|
||||
policies:
|
||||
- name: topology-local
|
||||
type: topology
|
||||
properties:
|
||||
clusters: ["local"]
|
||||
- name: topology-worker
|
||||
type: topology
|
||||
properties:
|
||||
clusters: ["cluster-worker"]
|
||||
workflow:
|
||||
steps:
|
||||
- name: deploy-components
|
||||
type: deploy-components
|
||||
properties:
|
||||
components:
|
||||
- name: web-on-local
|
||||
policies: ["topology-local"]
|
||||
- name: web-on-worker
|
||||
policies: ["topology-worker"]
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
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 e2e_multicluster_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
)
|
||||
|
||||
var _ = Describe("Test deploy-components workflow step", func() {
|
||||
|
||||
var namespace string
|
||||
var hubCtx context.Context
|
||||
var workerCtx context.Context
|
||||
|
||||
BeforeEach(func() {
|
||||
hubCtx, workerCtx, namespace = initializeContextAndNamespace()
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
cleanUpNamespace(hubCtx, workerCtx, namespace)
|
||||
})
|
||||
|
||||
It("Test deploying each component to the cluster resolved from its own topology policy", func() {
|
||||
app := &v1beta1.Application{}
|
||||
bs, err := os.ReadFile("./testdata/app/app-deploy-components.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(yaml.Unmarshal(bs, app)).Should(Succeed())
|
||||
app.SetNamespace(namespace)
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Create(context.Background(), app)).Should(Succeed())
|
||||
}).WithPolling(2 * time.Second).WithTimeout(5 * time.Second).Should(Succeed())
|
||||
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
Eventually(func(g Gomega) {
|
||||
_app := &v1beta1.Application{}
|
||||
g.Expect(k8sClient.Get(context.Background(), appKey, _app)).Should(Succeed())
|
||||
g.Expect(_app.Status.Phase).Should(Equal(common.ApplicationRunning))
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
|
||||
By("component mapped to the local topology policy only lands on the hub cluster")
|
||||
Expect(k8sClient.Get(hubCtx, types.NamespacedName{Namespace: namespace, Name: "cm-on-local"}, &corev1.ConfigMap{})).Should(Succeed())
|
||||
Expect(kerrors.IsNotFound(k8sClient.Get(workerCtx, types.NamespacedName{Namespace: namespace, Name: "cm-on-local"}, &corev1.ConfigMap{}))).Should(BeTrue())
|
||||
|
||||
By("component mapped to the worker topology policy only lands on the worker cluster")
|
||||
Expect(k8sClient.Get(workerCtx, types.NamespacedName{Namespace: namespace, Name: "cm-on-worker"}, &corev1.ConfigMap{})).Should(Succeed())
|
||||
Expect(kerrors.IsNotFound(k8sClient.Get(hubCtx, types.NamespacedName{Namespace: namespace, Name: "cm-on-worker"}, &corev1.ConfigMap{}))).Should(BeTrue())
|
||||
|
||||
By("Deleting")
|
||||
_app := &v1beta1.Application{}
|
||||
Expect(k8sClient.Get(context.Background(), appKey, _app)).Should(Succeed())
|
||||
Expect(k8sClient.Delete(context.Background(), _app)).Should(Succeed())
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(kerrors.IsNotFound(k8sClient.Get(context.Background(), appKey, _app))).Should(BeTrue())
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
Expect(kerrors.IsNotFound(k8sClient.Get(hubCtx, types.NamespacedName{Namespace: namespace, Name: "cm-on-local"}, &corev1.ConfigMap{}))).Should(BeTrue())
|
||||
Expect(kerrors.IsNotFound(k8sClient.Get(workerCtx, types.NamespacedName{Namespace: namespace, Name: "cm-on-worker"}, &corev1.ConfigMap{}))).Should(BeTrue())
|
||||
})
|
||||
|
||||
It("Test deploy-components fails fast with a clear message when a component name is wrong", func() {
|
||||
app := &v1beta1.Application{}
|
||||
bs, err := os.ReadFile("./testdata/app/app-deploy-components-missing.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(yaml.Unmarshal(bs, app)).Should(Succeed())
|
||||
app.SetNamespace(namespace)
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Create(context.Background(), app)).Should(Succeed())
|
||||
}).WithPolling(2 * time.Second).WithTimeout(5 * time.Second).Should(Succeed())
|
||||
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
Eventually(func(g Gomega) {
|
||||
_app := &v1beta1.Application{}
|
||||
g.Expect(k8sClient.Get(context.Background(), appKey, _app)).Should(Succeed())
|
||||
g.Expect(_app.Status.Workflow).ShouldNot(BeNil())
|
||||
g.Expect(len(_app.Status.Workflow.Steps)).ShouldNot(Equal(0))
|
||||
g.Expect(_app.Status.Workflow.Steps[0].Phase).Should(Equal(workflowv1alpha1.WorkflowStepPhaseFailed))
|
||||
g.Expect(_app.Status.Workflow.Steps[0].Message).Should(ContainSubstring("component(s) not found in application: cm-on-lcal"))
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
})
|
||||
})
|
||||
45
test/e2e-multicluster-test/testdata/app/app-deploy-components-missing.yaml
vendored
Normal file
45
test/e2e-multicluster-test/testdata/app/app-deploy-components-missing.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: app-deploy-components-missing
|
||||
spec:
|
||||
components:
|
||||
- name: cm-on-local
|
||||
type: k8s-objects
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm-on-local
|
||||
data:
|
||||
key: "local"
|
||||
- name: cm-on-worker
|
||||
type: k8s-objects
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm-on-worker
|
||||
data:
|
||||
key: "worker"
|
||||
policies:
|
||||
- name: topology-local
|
||||
type: topology
|
||||
properties:
|
||||
clusters: ["local"]
|
||||
- name: topology-worker
|
||||
type: topology
|
||||
properties:
|
||||
clusters: ["cluster-worker"]
|
||||
workflow:
|
||||
steps:
|
||||
- name: deploy-components
|
||||
type: deploy-components
|
||||
properties:
|
||||
components:
|
||||
- name: cm-on-lcal # intentional typo, should trigger validation failure
|
||||
policies: ["topology-local"]
|
||||
- name: cm-on-worker
|
||||
policies: ["topology-worker"]
|
||||
45
test/e2e-multicluster-test/testdata/app/app-deploy-components.yaml
vendored
Normal file
45
test/e2e-multicluster-test/testdata/app/app-deploy-components.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: app-deploy-components
|
||||
spec:
|
||||
components:
|
||||
- name: cm-on-local
|
||||
type: k8s-objects
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm-on-local
|
||||
data:
|
||||
key: "local"
|
||||
- name: cm-on-worker
|
||||
type: k8s-objects
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cm-on-worker
|
||||
data:
|
||||
key: "worker"
|
||||
policies:
|
||||
- name: topology-local
|
||||
type: topology
|
||||
properties:
|
||||
clusters: ["local"]
|
||||
- name: topology-worker
|
||||
type: topology
|
||||
properties:
|
||||
clusters: ["cluster-worker"]
|
||||
workflow:
|
||||
steps:
|
||||
- name: deploy-components
|
||||
type: deploy-components
|
||||
properties:
|
||||
components:
|
||||
- name: cm-on-local
|
||||
policies: ["topology-local"]
|
||||
- name: cm-on-worker
|
||||
policies: ["topology-worker"]
|
||||
@@ -0,0 +1,69 @@
|
||||
import (
|
||||
"list"
|
||||
"strings"
|
||||
"vela/builtin"
|
||||
"vela/multicluster"
|
||||
"vela/oam"
|
||||
)
|
||||
|
||||
"deploy-components": {
|
||||
type: "workflow-step"
|
||||
annotations: {
|
||||
"category": "Application Delivery"
|
||||
}
|
||||
labels: {
|
||||
"scope": "Application"
|
||||
}
|
||||
description: "Deploy each component to the cluster(s) resolved from its own topology policies. Applies are executed sequentially, one at a time -- unlike \"deploy\", there is no parallelism setting to configure."
|
||||
}
|
||||
template: {
|
||||
components: oam.#LoadComponets
|
||||
|
||||
// Iterating (not an indexed lookup) avoids evaluating before "components" resolves.
|
||||
_loadedNames: [for name, _ in components.$returns.value {name}]
|
||||
|
||||
_missingComponents: [for entry in parameter.components if !list.Contains(_loadedNames, entry.name) {entry.name}]
|
||||
|
||||
if len(_missingComponents) > 0 {
|
||||
validateComponents: builtin.#Fail & {
|
||||
$params: message: "component(s) not found in application: \(strings.Join(_missingComponents, ", "))"
|
||||
}
|
||||
}
|
||||
|
||||
// Gated so nothing is applied unless every component name is valid.
|
||||
if len(_missingComponents) == 0 {
|
||||
deploy: {
|
||||
// "comp", not "value" -- shadows the $params.value field below otherwise.
|
||||
for name, comp in components.$returns.value {
|
||||
for entry in parameter.components if entry.name == name {
|
||||
"\(name)": {
|
||||
placements: multicluster.#GetPlacementsFromTopologyPolicies & {
|
||||
$params: policies: entry.policies
|
||||
}
|
||||
apply: {
|
||||
for p in placements.$returns.placements {
|
||||
"\(p.cluster)-\(p.namespace)": oam.#ApplyComponent & {
|
||||
$params: {
|
||||
value: comp
|
||||
cluster: p.cluster
|
||||
namespace: p.namespace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parameter: {
|
||||
// +usage=Per-component mapping of which topology policies determine its target cluster(s)
|
||||
components: [...{
|
||||
// +usage=the name of the component in the application to apply
|
||||
name: string
|
||||
// +usage=names of topology policies (declared at the Application level) used to resolve this component's target cluster(s)
|
||||
policies: [...string]
|
||||
}]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user