mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-18 03:56:36 +00:00
Feat: component replication (#4449)
This commit is contained in:
@@ -494,6 +494,10 @@ type ApplicationComponent struct {
|
||||
// scopes in ApplicationComponent defines the component-level scopes
|
||||
// the format is <scope-type:scope-instance-name> pairs, the key represents type of `ScopeDefinition` while the value represent the name of scope instance.
|
||||
Scopes map[string]string `json:"scopes,omitempty"`
|
||||
|
||||
// ReplicaKey is not empty means the component is replicated. This field is designed so that it can't be specified in application directly.
|
||||
// So we set the json tag as "-". Instead, this will be filled when using replication policy.
|
||||
ReplicaKey string `json:"-"`
|
||||
}
|
||||
|
||||
// StepOutputs defines output variable of WorkflowStep
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
DebugPolicyType = "debug"
|
||||
// SharedResourcePolicyType refers to the type of shared resource policy
|
||||
SharedResourcePolicyType = "shared-resource"
|
||||
// ReplicationPolicyType refers to the type of replication policy
|
||||
ReplicationPolicyType = "replication"
|
||||
)
|
||||
|
||||
// TopologyPolicySpec defines the spec of topology policy
|
||||
@@ -77,3 +79,11 @@ func (in SharedResourcePolicySpec) FindStrategy(manifest *unstructured.Unstructu
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ReplicationPolicySpec defines the spec of replication policy
|
||||
// Override policy should be used together with replication policy to select the deployment target components
|
||||
type ReplicationPolicySpec struct {
|
||||
Keys []string `json:"keys,omitempty"`
|
||||
// Selector is the subset of selected components which will be replicated.
|
||||
Selector []string `json:"selector,omitempty"`
|
||||
}
|
||||
|
||||
@@ -612,6 +612,31 @@ func (in *RefObjectsComponentSpec) DeepCopy() *RefObjectsComponentSpec {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ReplicationPolicySpec) DeepCopyInto(out *ReplicationPolicySpec) {
|
||||
*out = *in
|
||||
if in.Keys != nil {
|
||||
in, out := &in.Keys, &out.Keys
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Selector != nil {
|
||||
in, out := &in.Selector, &out.Selector
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ReplicationPolicySpec.
|
||||
func (in *ReplicationPolicySpec) DeepCopy() *ReplicationPolicySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ReplicationPolicySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourcePolicyRuleSelector) DeepCopyInto(out *ResourcePolicyRuleSelector) {
|
||||
*out = *in
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/replication.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: PolicyDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Describe the configuration to replicate components when deploying resources, it only works with specified `deploy` step in workflow.
|
||||
name: replication
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
parameter: {
|
||||
// +usage=Spicify the keys of replication. Every key coresponds to a replication components
|
||||
keys: [...string]
|
||||
// +usage=Specify the components which will be replicated.
|
||||
selector?: [...string]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/replication.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: PolicyDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Describe the configuration to replicate components when deploying resources, it only works with specified `deploy` step in workflow.
|
||||
name: replication
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
parameter: {
|
||||
// +usage=Spicify the keys of replication. Every key coresponds to a replication components
|
||||
keys: [...string]
|
||||
// +usage=Specify the components which will be replicated.
|
||||
selector?: [...string]
|
||||
}
|
||||
|
||||
@@ -511,7 +511,7 @@ func baseGenerateComponent(pCtx process.Context, wl *Workload, appName, ns strin
|
||||
}
|
||||
}
|
||||
}
|
||||
compManifest, err := evalWorkloadWithContext(pCtx, wl, ns, appName, wl.Name)
|
||||
compManifest, err := evalWorkloadWithContext(pCtx, wl, ns, appName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -559,7 +559,7 @@ func makeWorkloadWithContext(pCtx process.Context, wl *Workload, ns, appName str
|
||||
}
|
||||
|
||||
// evalWorkloadWithContext evaluate the workload's template to generate component manifest
|
||||
func evalWorkloadWithContext(pCtx process.Context, wl *Workload, ns, appName, compName string) (*types.ComponentManifest, error) {
|
||||
func evalWorkloadWithContext(pCtx process.Context, wl *Workload, ns, appName string) (*types.ComponentManifest, error) {
|
||||
compManifest := &types.ComponentManifest{}
|
||||
workload, err := makeWorkloadWithContext(pCtx, wl, ns, appName)
|
||||
if err != nil {
|
||||
@@ -573,7 +573,7 @@ func evalWorkloadWithContext(pCtx process.Context, wl *Workload, ns, appName, co
|
||||
for i, assist := range assists {
|
||||
tr, err := assist.Ins.Unstructured()
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "evaluate trait=%s template for component=%s app=%s", assist.Name, compName, appName)
|
||||
return nil, errors.Wrapf(err, "evaluate trait=%s template for component=%s app=%s", assist.Name, wl.Name, appName)
|
||||
}
|
||||
labels := util.MergeMapOverrideWithDst(commonLabels, map[string]string{oam.TraitTypeLabel: assist.Type})
|
||||
if assist.Name != "" {
|
||||
|
||||
@@ -814,7 +814,7 @@ var _ = Describe("Test evalWorkloadWithContext", func() {
|
||||
|
||||
args := appArgs{
|
||||
wl: &Workload{
|
||||
Name: "sample-db",
|
||||
Name: compName,
|
||||
FullTemplate: &Template{
|
||||
Terraform: &common.Terraform{
|
||||
Configuration: `
|
||||
@@ -891,7 +891,7 @@ variable "password" {
|
||||
AppRevisionName: args.revision,
|
||||
}, args.wl.Name)
|
||||
pCtx := NewBasicContext(ctxData, args.wl.Params)
|
||||
comp, err := evalWorkloadWithContext(pCtx, args.wl, ns, args.appName, compName)
|
||||
comp, err := evalWorkloadWithContext(pCtx, args.wl, ns, args.appName)
|
||||
Expect(comp.StandardWorkload).ShouldNot(BeNil())
|
||||
Expect(comp.Name).Should(Equal(""))
|
||||
Expect(err).Should(BeNil())
|
||||
|
||||
@@ -419,6 +419,7 @@ func (p *Parser) parsePolicies(ctx context.Context, af *Appfile) (err error) {
|
||||
case v1alpha1.SharedResourcePolicyType:
|
||||
case v1alpha1.EnvBindingPolicyType:
|
||||
case v1alpha1.TopologyPolicyType:
|
||||
case v1alpha1.ReplicationPolicyType:
|
||||
case v1alpha1.DebugPolicyType:
|
||||
af.Debug = true
|
||||
case v1alpha1.OverridePolicyType:
|
||||
|
||||
@@ -229,6 +229,7 @@ func (h *AppHandler) checkComponentHealth(appParser *appfile.Parser, appRev *v1b
|
||||
return func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string, env string) (bool, error) {
|
||||
ctx := multicluster.ContextWithClusterName(context.Background(), clusterName)
|
||||
ctx = contextWithComponentNamespace(ctx, overrideNamespace)
|
||||
ctx = contextWithReplicaKey(ctx, comp.ReplicaKey)
|
||||
|
||||
wl, manifest, err := h.prepareWorkloadAndManifests(ctx, appParser, comp, appRev, patcher, af)
|
||||
if err != nil {
|
||||
@@ -262,6 +263,7 @@ func (h *AppHandler) applyComponentFunc(appParser *appfile.Parser, appRev *v1bet
|
||||
|
||||
ctx := multicluster.ContextWithClusterName(context.Background(), clusterName)
|
||||
ctx = contextWithComponentNamespace(ctx, overrideNamespace)
|
||||
ctx = contextWithReplicaKey(ctx, comp.ReplicaKey)
|
||||
ctx = envbinding.ContextWithEnvName(ctx, env)
|
||||
|
||||
wl, manifest, err := h.prepareWorkloadAndManifests(ctx, appParser, comp, appRev, patcher, af)
|
||||
@@ -333,6 +335,9 @@ func (h *AppHandler) prepareWorkloadAndManifests(ctx context.Context,
|
||||
if ns := componentNamespaceFromContext(ctx); ns != "" {
|
||||
ctxData.Namespace = ns
|
||||
}
|
||||
if rk := replicaKeyFromContext(ctx); rk != "" {
|
||||
ctxData.ReplicaKey = rk
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, errors.WithMessage(err, "GenerateComponentManifest")
|
||||
|
||||
@@ -74,6 +74,8 @@ const (
|
||||
ComponentNamespaceContextKey = contextKey("component-namespace")
|
||||
// ComponentContextKey is the key in context that records the component
|
||||
ComponentContextKey = contextKey("component")
|
||||
// ReplicaKeyContextKey is the key in context that records the replica key
|
||||
ReplicaKeyContextKey = contextKey("replica-key")
|
||||
)
|
||||
|
||||
const rolloutTraitName = "rollout"
|
||||
@@ -117,6 +119,15 @@ func componentNamespaceFromContext(ctx context.Context) string {
|
||||
return ns
|
||||
}
|
||||
|
||||
func contextWithReplicaKey(ctx context.Context, key string) context.Context {
|
||||
return context.WithValue(ctx, ReplicaKeyContextKey, key)
|
||||
}
|
||||
|
||||
func replicaKeyFromContext(ctx context.Context) string {
|
||||
key, _ := ctx.Value(ReplicaKeyContextKey).(string)
|
||||
return key
|
||||
}
|
||||
|
||||
func (h *AppHandler) getComponentRevisionNamespace(ctx context.Context) string {
|
||||
if ns, ok := ctx.Value(ComponentNamespaceContextKey).(string); ok && ns != "" {
|
||||
return ns
|
||||
|
||||
@@ -396,6 +396,9 @@ func GetCommonLabels(contextLabels map[string]string) map[string]string {
|
||||
commonLabels[oam.LabelAppComponent] = v
|
||||
case model.ContextAppRevision:
|
||||
commonLabels[oam.LabelAppRevision] = v
|
||||
case model.ContextReplicaKey:
|
||||
commonLabels[oam.LabelReplicaKey] = v
|
||||
|
||||
}
|
||||
}
|
||||
return commonLabels
|
||||
|
||||
@@ -56,4 +56,6 @@ const (
|
||||
ComponentRevisionPlaceHolder = "KUBEVELA_COMPONENT_REVISION_PLACEHOLDER"
|
||||
// ContextDataArtifacts is used to store unstructured resources of components
|
||||
ContextDataArtifacts = "artifacts"
|
||||
// ContextReplicaKey is the key of replication in context
|
||||
ContextReplicaKey = "replicaKey"
|
||||
)
|
||||
|
||||
@@ -77,6 +77,8 @@ type templateContext struct {
|
||||
outputSecretName string
|
||||
// requiredSecrets is used to store all secret names which are generated by cloud resource components and required by current component
|
||||
requiredSecrets []RequiredSecrets
|
||||
// replicaKey is used to store all replica key which are generated by replication policy
|
||||
replicaKey string
|
||||
|
||||
baseHooks []BaseHook
|
||||
auxiliaryHooks []AuxiliaryHook
|
||||
@@ -109,6 +111,7 @@ type ContextData struct {
|
||||
AppRevisionName string
|
||||
WorkflowName string
|
||||
PublishVersion string
|
||||
ReplicaKey string
|
||||
|
||||
Ctx context.Context
|
||||
BaseHooks []BaseHook
|
||||
@@ -128,6 +131,7 @@ func NewContext(data ContextData) Context {
|
||||
appRevision: data.AppRevisionName,
|
||||
workflowName: data.WorkflowName,
|
||||
publishVersion: data.PublishVersion,
|
||||
replicaKey: data.ReplicaKey,
|
||||
|
||||
configs: []map[string]string{},
|
||||
auxiliaries: []Auxiliary{},
|
||||
@@ -184,6 +188,10 @@ func (ctx *templateContext) BaseContextFile() (string, error) {
|
||||
buff += fmt.Sprintf(model.ContextWorkflowName+": \"%s\"\n", ctx.workflowName)
|
||||
buff += fmt.Sprintf(model.ContextPublishVersion+": \"%s\"\n", ctx.publishVersion)
|
||||
|
||||
if ctx.replicaKey != "" {
|
||||
buff += fmt.Sprintf(model.ContextReplicaKey+": \"%s\"\n", ctx.replicaKey)
|
||||
}
|
||||
|
||||
if ctx.appLabels != nil {
|
||||
bt, err := json.Marshal(ctx.appLabels)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
LabelAppDeployment = "app.oam.dev/appDeployment"
|
||||
// LabelAppComponent records the name of Component
|
||||
LabelAppComponent = "app.oam.dev/component"
|
||||
// LabelReplicaKey records the replica key of Component
|
||||
LabelReplicaKey = "app.oam.dev/replicaKey"
|
||||
// LabelAppComponentRevision records the revision name of Component
|
||||
LabelAppComponentRevision = "app.oam.dev/revision"
|
||||
// LabelOAMResourceType whether a CR is workload or trait
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
"github.com/oam-dev/kubevela/pkg/policy/utils"
|
||||
errors2 "github.com/oam-dev/kubevela/pkg/utils/errors"
|
||||
)
|
||||
|
||||
@@ -115,23 +116,6 @@ func MergeComponent(base *common.ApplicationComponent, patch *v1alpha1.EnvCompon
|
||||
return newComponent, nil
|
||||
}
|
||||
|
||||
func filterComponents(components []string, selector []string) []string {
|
||||
if selector != nil {
|
||||
filter := map[string]bool{}
|
||||
for _, compName := range selector {
|
||||
filter[compName] = true
|
||||
}
|
||||
var _comps []string
|
||||
for _, compName := range components {
|
||||
if _, ok := filter[compName]; ok {
|
||||
_comps = append(_comps, compName)
|
||||
}
|
||||
}
|
||||
return _comps
|
||||
}
|
||||
return components
|
||||
}
|
||||
|
||||
// PatchApplication patch base application with patch and selector
|
||||
func PatchApplication(base *v1beta1.Application, patch *v1alpha1.EnvPatch, selector *v1alpha1.EnvSelector) (*v1beta1.Application, error) {
|
||||
newApp := base.DeepCopy()
|
||||
@@ -207,7 +191,7 @@ func PatchComponents(baseComponents []common.ApplicationComponent, patchComponen
|
||||
}
|
||||
|
||||
// if selector is enabled, filter
|
||||
compOrders = filterComponents(compOrders, selector)
|
||||
compOrders = utils.FilterComponents(compOrders, selector)
|
||||
|
||||
// fill in new application
|
||||
newComponents := []common.ApplicationComponent{}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
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 policy
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"k8s.io/kubectl/pkg/util/slice"
|
||||
|
||||
"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"
|
||||
pkgutils "github.com/oam-dev/kubevela/pkg/utils"
|
||||
)
|
||||
|
||||
// selectReplicateComponents will replicate the components
|
||||
func selectReplicateComponents(components []common.ApplicationComponent, selectors []string) ([]common.ApplicationComponent, error) {
|
||||
var compToReplicate = make([]common.ApplicationComponent, 0)
|
||||
for _, comp := range components {
|
||||
if slice.ContainsString(selectors, comp.Name, nil) {
|
||||
compToReplicate = append(compToReplicate, comp)
|
||||
}
|
||||
}
|
||||
if len(compToReplicate) == 0 {
|
||||
return nil, errors.New("no component selected to replicate")
|
||||
}
|
||||
return compToReplicate, nil
|
||||
}
|
||||
|
||||
// ReplicateComponents will filter the components to replicate, return the replication decisions
|
||||
func ReplicateComponents(policies []v1beta1.AppPolicy, components []common.ApplicationComponent) ([]common.ApplicationComponent, error) {
|
||||
var (
|
||||
compToRemove = make(map[string]bool)
|
||||
compToAdd = make([]common.ApplicationComponent, 0)
|
||||
)
|
||||
existReplicationPolicy := false
|
||||
for _, policy := range policies {
|
||||
if policy.Type == v1alpha1.ReplicationPolicyType {
|
||||
existReplicationPolicy = true
|
||||
replicateSpec := &v1alpha1.ReplicationPolicySpec{}
|
||||
if policy.Properties == nil {
|
||||
continue
|
||||
}
|
||||
if err := pkgutils.StrictUnmarshal(policy.Properties.Raw, replicateSpec); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to parse replicate policy %s", policy.Name)
|
||||
}
|
||||
compToRep, err := selectReplicateComponents(components, replicateSpec.Selector)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to apply replicate policy %s", policy.Name)
|
||||
}
|
||||
compToAdd = append(compToAdd, replicateComponents(compToRep, replicateSpec.Keys)...)
|
||||
for _, comp := range compToRep {
|
||||
compToRemove[comp.Name] = true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if !existReplicationPolicy {
|
||||
return components, nil
|
||||
}
|
||||
compsAfterReplicate := make([]common.ApplicationComponent, 0, len(components))
|
||||
for _, comp := range components {
|
||||
if !compToRemove[comp.Name] {
|
||||
compsAfterReplicate = append(compsAfterReplicate, comp)
|
||||
}
|
||||
}
|
||||
compsAfterReplicate = append(compsAfterReplicate, compToAdd...)
|
||||
return compsAfterReplicate, nil
|
||||
}
|
||||
|
||||
func replicateComponents(comps []common.ApplicationComponent, keys []string) []common.ApplicationComponent {
|
||||
compsAfterReplicate := make([]common.ApplicationComponent, 0, len(comps)*len(keys))
|
||||
for _, comp := range comps {
|
||||
for _, key := range keys {
|
||||
compAfterReplicate := comp.DeepCopy()
|
||||
compAfterReplicate.ReplicaKey = key
|
||||
compsAfterReplicate = append(compsAfterReplicate, *compAfterReplicate)
|
||||
}
|
||||
}
|
||||
return compsAfterReplicate
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
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 policy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
assert2 "github.com/stretchr/testify/assert"
|
||||
"gotest.tools/assert"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/oam/util"
|
||||
)
|
||||
|
||||
func TestReplicateComponents(t *testing.T) {
|
||||
comp1 := common.ApplicationComponent{Name: "comp1"}
|
||||
comp2 := common.ApplicationComponent{Name: "comp2"}
|
||||
baseComponents := []common.ApplicationComponent{
|
||||
comp1,
|
||||
comp2,
|
||||
}
|
||||
testCases := map[string]struct {
|
||||
Components []common.ApplicationComponent
|
||||
Selectors []string
|
||||
Output []common.ApplicationComponent
|
||||
WantErr error
|
||||
}{
|
||||
"nil selector, don't replicate": {
|
||||
Components: baseComponents,
|
||||
Selectors: nil,
|
||||
Output: nil,
|
||||
WantErr: fmt.Errorf("no component selected to replicate"),
|
||||
},
|
||||
"select all, replicate all": {
|
||||
Components: baseComponents,
|
||||
Selectors: []string{"comp1", "comp2"},
|
||||
Output: baseComponents,
|
||||
},
|
||||
"replicate part": {
|
||||
Components: baseComponents,
|
||||
Selectors: []string{"comp1"},
|
||||
Output: []common.ApplicationComponent{comp1},
|
||||
},
|
||||
"part invalid selector": {
|
||||
Components: baseComponents,
|
||||
Selectors: []string{"comp1", "comp3"},
|
||||
Output: []common.ApplicationComponent{comp1},
|
||||
},
|
||||
"no component selected": {
|
||||
Components: baseComponents,
|
||||
Selectors: []string{"comp3"},
|
||||
Output: []common.ApplicationComponent{},
|
||||
WantErr: fmt.Errorf("no component selected for replicate"),
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
result, err := selectReplicateComponents(tc.Components, tc.Selectors)
|
||||
if tc.WantErr != nil {
|
||||
assert2.Error(t, err)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, len(result), len(tc.Output))
|
||||
assert.DeepEqual(t, result, tc.Output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetReplicationComponents(t *testing.T) {
|
||||
baseComps := []common.ApplicationComponent{
|
||||
{Name: "comp1"},
|
||||
{Name: "comp2"},
|
||||
}
|
||||
PolicyName := "test-policy"
|
||||
testCases := map[string]struct {
|
||||
Policies []v1beta1.AppPolicy
|
||||
Components []common.ApplicationComponent
|
||||
WantErr error
|
||||
WantComps []common.ApplicationComponent
|
||||
}{
|
||||
"no replication policy, all components remain unchanged": {
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: PolicyName,
|
||||
Type: "foo",
|
||||
Properties: nil,
|
||||
},
|
||||
},
|
||||
Components: baseComps,
|
||||
WantComps: baseComps,
|
||||
},
|
||||
"one replication policy, replicate those components": {
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: PolicyName,
|
||||
Type: "replication",
|
||||
Properties: util.Object2RawExtension(v1alpha1.ReplicationPolicySpec{
|
||||
Keys: []string{"replica-1", "replica-2"},
|
||||
Selector: []string{"comp1"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Components: baseComps,
|
||||
WantComps: []common.ApplicationComponent{
|
||||
{Name: "comp2"},
|
||||
{Name: "comp1", ReplicaKey: "replica-1"},
|
||||
{Name: "comp1", ReplicaKey: "replica-2"},
|
||||
},
|
||||
},
|
||||
"replicate non-exist component": {
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: PolicyName,
|
||||
Type: "replication",
|
||||
Properties: util.Object2RawExtension(v1alpha1.ReplicationPolicySpec{
|
||||
Keys: []string{"replica-1", "replica-2"},
|
||||
Selector: []string{"comp-non-exist"},
|
||||
}),
|
||||
},
|
||||
},
|
||||
Components: baseComps,
|
||||
WantErr: fmt.Errorf("failed to apply replicate policy %s", PolicyName),
|
||||
},
|
||||
"invalid-override-policy": {
|
||||
Policies: []v1beta1.AppPolicy{
|
||||
{
|
||||
Name: PolicyName,
|
||||
Type: "replication",
|
||||
Properties: &runtime.RawExtension{Raw: []byte(`{bad value}`)},
|
||||
},
|
||||
},
|
||||
Components: baseComps,
|
||||
WantErr: fmt.Errorf("failed to parse replicate policy %s", PolicyName),
|
||||
},
|
||||
}
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
comps, err := ReplicateComponents(tc.Policies, tc.Components)
|
||||
if tc.WantErr != nil {
|
||||
assert2.Error(t, err)
|
||||
assert2.Contains(t, err.Error(), tc.WantErr.Error())
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, comps, tc.WantComps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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 utils
|
||||
|
||||
// FilterComponents select the components using the selectors
|
||||
func FilterComponents(components []string, selector []string) []string {
|
||||
if selector != nil {
|
||||
filter := map[string]bool{}
|
||||
for _, compName := range selector {
|
||||
filter[compName] = true
|
||||
}
|
||||
var _comps []string
|
||||
for _, compName := range components {
|
||||
if _, ok := filter[compName]; ok {
|
||||
_comps = append(_comps, compName)
|
||||
}
|
||||
}
|
||||
return _comps
|
||||
}
|
||||
return components
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
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 utils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFilterComponents(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Components []string
|
||||
Selector []string
|
||||
Output []string
|
||||
}{
|
||||
"normal": {
|
||||
Components: []string{"comp1", "comp2", "comp3"},
|
||||
Selector: []string{"comp1", "comp2"},
|
||||
Output: []string{"comp1", "comp2"},
|
||||
},
|
||||
"selector-empty": {
|
||||
Components: []string{"comp1", "comp2", "comp3"},
|
||||
Selector: []string{},
|
||||
Output: nil,
|
||||
},
|
||||
"selector-nil": {
|
||||
Components: []string{"comp1", "comp2", "comp3"},
|
||||
Selector: nil,
|
||||
Output: []string{"comp1", "comp2", "comp3"},
|
||||
},
|
||||
}
|
||||
for name, tt := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r := require.New(t)
|
||||
output := FilterComponents(tt.Components, tt.Selector)
|
||||
r.Equal(tt.Output, output)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -74,6 +74,8 @@ func (executor *deployWorkflowStepExecutor) Deploy(ctx context.Context, policyNa
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
|
||||
// Dealing with topology, override and replication policies in order.
|
||||
placements, err := pkgpolicy.GetPlacementsFromTopologyPolicies(ctx, executor.cli, executor.af.Namespace, policies, resourcekeeper.AllowCrossNamespaceResource)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
@@ -82,6 +84,10 @@ func (executor *deployWorkflowStepExecutor) Deploy(ctx context.Context, policyNa
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
components, err = pkgpolicy.ReplicateComponents(policies, components)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
}
|
||||
return applyComponents(executor.apply, executor.healthCheck, components, placements, parallelism)
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"cuelang.org/go/cue/cuecontext"
|
||||
"github.com/pkg/errors"
|
||||
@@ -65,7 +64,7 @@ type provider struct {
|
||||
|
||||
// RenderComponent render component
|
||||
func (p *provider) RenderComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error {
|
||||
comp, patcher, clusterName, overrideNamespace, env, err := lookUpValues(v, nil)
|
||||
comp, patcher, clusterName, overrideNamespace, env, err := lookUpCompInfo(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -92,8 +91,9 @@ func (p *provider) RenderComponent(ctx wfContext.Context, v *value.Value, act wf
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *provider) applyComponent(_ wfContext.Context, v *value.Value, act wfTypes.Action, mu *sync.Mutex) error {
|
||||
comp, patcher, clusterName, overrideNamespace, env, err := lookUpValues(v, mu)
|
||||
// ApplyComponent apply component.
|
||||
func (p *provider) ApplyComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error {
|
||||
comp, patcher, clusterName, overrideNamespace, env, err := lookUpCompInfo(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,11 +102,6 @@ func (p *provider) applyComponent(_ wfContext.Context, v *value.Value, act wfTyp
|
||||
return err
|
||||
}
|
||||
|
||||
if mu != nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
}
|
||||
|
||||
if workload != nil {
|
||||
if err := v.FillObject(workload.Object, "output"); err != nil {
|
||||
return errors.WithMessage(err, "FillOutput")
|
||||
@@ -130,20 +125,10 @@ func (p *provider) applyComponent(_ wfContext.Context, v *value.Value, act wfTyp
|
||||
if waitHealthy && !healthy {
|
||||
act.Wait("wait healthy")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyComponent apply component.
|
||||
func (p *provider) ApplyComponent(ctx wfContext.Context, v *value.Value, act wfTypes.Action) error {
|
||||
return p.applyComponent(ctx, v, act, nil)
|
||||
}
|
||||
|
||||
func lookUpValues(v *value.Value, mu *sync.Mutex) (*common.ApplicationComponent, *value.Value, string, string, string, error) {
|
||||
if mu != nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
}
|
||||
func lookUpCompInfo(v *value.Value) (*common.ApplicationComponent, *value.Value, string, string, string, error) {
|
||||
compSettings, err := v.LookupValue("value")
|
||||
if err != nil {
|
||||
return nil, nil, "", "", "", err
|
||||
|
||||
@@ -87,7 +87,7 @@ metadata: {
|
||||
func TestRenderComponent(t *testing.T) {
|
||||
r := require.New(t)
|
||||
p := &provider{
|
||||
render: func(comp common.ApplicationComponent, patcher *value.Value, clusterName string, overrideNamespace string, _ string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) {
|
||||
render: func(comp common.ApplicationComponent, patcher *value.Value, _, _, _ string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) {
|
||||
return &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "apps/v1",
|
||||
@@ -238,7 +238,7 @@ func TestLoadComponentInOrder(t *testing.T) {
|
||||
|
||||
var testHealthy bool
|
||||
|
||||
func simpleComponentApplyForTest(comp common.ApplicationComponent, _ *value.Value, _ string, _ string, _ string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
|
||||
func simpleComponentApplyForTest(comp common.ApplicationComponent, _ *value.Value, _, _, _ string) (*unstructured.Unstructured, []*unstructured.Unstructured, bool, error) {
|
||||
workload := new(unstructured.Unstructured)
|
||||
workload.UnmarshalJSON([]byte(`{
|
||||
"apiVersion": "v1",
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
### Background
|
||||
|
||||
In KubeVela, we can dispatch resources across the clusters. But projects like [OpenYurt](https://openyurt.io) have finer-grained division like node pool.
|
||||
This requires to dispatch some similar resources to the same cluster. These resources are called replication. Back to the example of OpenYurt, it can
|
||||
integrate KubeVela and replicate the resources then dispatch them to the different node pool.
|
||||
|
||||
### Usage
|
||||
|
||||
Replication is an internal policy. It can be only used with `deploy` workflow step. When using replication policy. A new field `replicaKey` will be added to context.
|
||||
User can use definitions that make use of `context.replicaKey`. For example, apply a replica-webservice ComponentDefinition.
|
||||
|
||||
In this ComponentDefinition, we can use `context.replicaKey` to distinguish the name of Deployment and Service.
|
||||
|
||||
> **NOTE**: ComponentDefinition below is trimmed for brevity. See complete YAML in [replication.yaml](https://github.com/kubevela/kubevela/tree/master/test/e2e-test/testdata/definition/replication.yaml)
|
||||
|
||||
```yaml
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Webservice, but can be replicated
|
||||
name: replica-webservice
|
||||
namespace: vela-system
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
metadata: {
|
||||
if context.replicaKey != _|_ {
|
||||
name: context.name + "-" + context.replicaKey
|
||||
}
|
||||
if context.replicaKey == _|_ {
|
||||
name: context.name
|
||||
}
|
||||
}
|
||||
spec: {
|
||||
selector: matchLabels: {
|
||||
"app.oam.dev/component": context.name
|
||||
if context.replicaKey != _|_ {
|
||||
"app.oam.dev/replicaKey": context.replicaKey
|
||||
}
|
||||
}
|
||||
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
if parameter.labels != _|_ {
|
||||
parameter.labels
|
||||
}
|
||||
if parameter.addRevisionLabel {
|
||||
"app.oam.dev/revision": context.revision
|
||||
}
|
||||
"app.oam.dev/name": context.appName
|
||||
"app.oam.dev/component": context.name
|
||||
if context.replicaKey != _|_ {
|
||||
"app.oam.dev/replicaKey": context.replicaKey
|
||||
}
|
||||
|
||||
}
|
||||
if parameter.annotations != _|_ {
|
||||
annotations: parameter.annotations
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
outputs: {
|
||||
if len(exposePorts) != 0 {
|
||||
webserviceExpose: {
|
||||
apiVersion: "v1"
|
||||
kind: "Service"
|
||||
metadata: {
|
||||
if context.replicaKey != _|_ {
|
||||
name: context.name + "-" + context.replicaKey
|
||||
}
|
||||
if context.replicaKey == _|_ {
|
||||
name: context.name
|
||||
}
|
||||
}
|
||||
spec: {
|
||||
selector: {
|
||||
"app.oam.dev/component": context.name
|
||||
if context.replicaKey != _|_ {
|
||||
"app.oam.dev/replicaKey": context.replicaKey
|
||||
}
|
||||
}
|
||||
ports: exposePorts
|
||||
type: parameter.exposeType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then user can apply application below. Replication policy is declared in `application.spec.policies`. These policies are used in `deploy-with-rep` workflow step.
|
||||
They work together to influence the `deploy` step.
|
||||
|
||||
- override: select `hello-rep` component to deploy.
|
||||
- topology: select cluster `local` to deploy.
|
||||
- replication: select `hello-rep` component to replicate.
|
||||
|
||||
As a result, there will be two Deployments and two Services:
|
||||
|
||||
```yaml
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: app-replication-policy
|
||||
spec:
|
||||
components:
|
||||
- name: hello-rep
|
||||
type: replica-webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
ports:
|
||||
- port: 80
|
||||
expose: true
|
||||
policies:
|
||||
- name: comp-to-replicate
|
||||
type: override
|
||||
properties:
|
||||
selector: [ "hello-rep" ]
|
||||
- name: target-default
|
||||
type: topology
|
||||
properties:
|
||||
clusters: [ "local" ]
|
||||
- name: replication-default
|
||||
type: replication
|
||||
properties:
|
||||
keys: ["beijing","hangzhou"]
|
||||
selector: ["hello-rep"]
|
||||
|
||||
workflow:
|
||||
steps:
|
||||
- name: deploy-with-rep
|
||||
type: deploy
|
||||
properties:
|
||||
policies: ["comp-to-replicate","target-default","replication-default"]
|
||||
```
|
||||
|
||||
```shell
|
||||
kubectl get deploy -n default
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
hello-rep-beijing 1/1 1 1 5s
|
||||
hello-rep-hangzhou 1/1 1 1 5s
|
||||
|
||||
kubectl get service -n default
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
hello-rep-hangzhou ClusterIP 10.43.23.200 <none> 80/TCP 41s
|
||||
hello-rep-beijing ClusterIP 10.43.24.116 <none> 80/TCP 12s
|
||||
```
|
||||
@@ -433,4 +433,43 @@ var _ = Describe("Application Normal tests", func() {
|
||||
By("Checking an application status")
|
||||
verifyApplicationWorkflowTerminated(newApp.Namespace, newApp.Name)
|
||||
})
|
||||
|
||||
It("Test app with replication policy", func() {
|
||||
By("Apply replica-webservice definition")
|
||||
var compDef v1beta1.ComponentDefinition
|
||||
Expect(common.ReadYamlToObject("testdata/definition/replica-webservice.yaml", &compDef)).Should(BeNil())
|
||||
Eventually(func() error {
|
||||
return k8sClient.Create(ctx, compDef.DeepCopy())
|
||||
}, 10*time.Second, 500*time.Millisecond).Should(SatisfyAny(util.AlreadyExistMatcher{}, BeNil()))
|
||||
|
||||
By("Creating an application")
|
||||
applyApp("app_replication.yaml")
|
||||
|
||||
By("Checking the replication & application status")
|
||||
verifyWorkloadRunningExpected("hello-rep-beijing", 1, "crccheck/hello-world")
|
||||
verifyWorkloadRunningExpected("hello-rep-hangzhou", 1, "crccheck/hello-world")
|
||||
By("Checking the origin component are not be dispatched")
|
||||
var workload v1.Deployment
|
||||
err := k8sClient.Get(ctx, client.ObjectKey{Namespace: namespaceName, Name: "hello-rep"}, &workload)
|
||||
Expect(err).Should(SatisfyAny(&util.NotFoundMatcher{}))
|
||||
|
||||
By("Checking the component not replicated & application status")
|
||||
verifyWorkloadRunningExpected("hello-no-rep", 1, "crccheck/hello-world")
|
||||
|
||||
var svc corev1.Service
|
||||
By("Verify Service running as expected")
|
||||
verifySeriveDispatched := func(svcName string) {
|
||||
Eventually(
|
||||
func() error {
|
||||
return k8sClient.Get(ctx, client.ObjectKey{Namespace: namespaceName, Name: svcName}, &svc)
|
||||
},
|
||||
time.Second*120, time.Millisecond*500).Should(BeNil())
|
||||
}
|
||||
verifySeriveDispatched("hello-rep-beijing")
|
||||
verifySeriveDispatched("hello-rep-hangzhou")
|
||||
|
||||
By("Checking the services not replicated & application status")
|
||||
verifyWorkloadRunningExpected("hello-no-rep", 1, "crccheck/hello-world")
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: app-replication-policy
|
||||
spec:
|
||||
components:
|
||||
- name: hello-rep
|
||||
type: replica-webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
ports:
|
||||
- port: 80
|
||||
expose: true
|
||||
- name: hello-no-rep
|
||||
type: webservice
|
||||
properties:
|
||||
image: crccheck/hello-world
|
||||
ports:
|
||||
- port: 80
|
||||
expose: true
|
||||
|
||||
policies:
|
||||
- name: comp-to-replicate
|
||||
type: override
|
||||
properties:
|
||||
selector: [ "hello-rep" ]
|
||||
- name: comp-not-replicate
|
||||
type: override
|
||||
properties:
|
||||
selector: [ "hello-no-rep" ]
|
||||
- name: target-default
|
||||
type: topology
|
||||
properties:
|
||||
clusters: [ "local" ]
|
||||
- name: replication-default
|
||||
type: replication
|
||||
properties:
|
||||
keys: ["beijing","hangzhou"]
|
||||
selector: ["hello-rep"]
|
||||
|
||||
workflow:
|
||||
steps:
|
||||
- name: deploy-with-rep
|
||||
type: deploy
|
||||
properties:
|
||||
policies: ["comp-to-replicate","target-default","replication-default"]
|
||||
- name: deploy-without-rep
|
||||
type: deploy
|
||||
properties:
|
||||
policies: ["comp-not-replicate","target-default"]
|
||||
@@ -0,0 +1,603 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: ComponentDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Webservice, but can be replicated
|
||||
name: replica-webservice
|
||||
namespace: vela-system
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
mountsArray: {
|
||||
pvc: *[
|
||||
for v in parameter.volumeMounts.pvc {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
if v.subPath != _|_ {
|
||||
subPath: v.subPath
|
||||
}
|
||||
name: v.name
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
configMap: *[
|
||||
for v in parameter.volumeMounts.configMap {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
if v.subPath != _|_ {
|
||||
subPath: v.subPath
|
||||
}
|
||||
name: v.name
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
secret: *[
|
||||
for v in parameter.volumeMounts.secret {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
if v.subPath != _|_ {
|
||||
subPath: v.subPath
|
||||
}
|
||||
name: v.name
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
emptyDir: *[
|
||||
for v in parameter.volumeMounts.emptyDir {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
if v.subPath != _|_ {
|
||||
subPath: v.subPath
|
||||
}
|
||||
name: v.name
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
hostPath: *[
|
||||
for v in parameter.volumeMounts.hostPath {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
if v.subPath != _|_ {
|
||||
subPath: v.subPath
|
||||
}
|
||||
name: v.name
|
||||
}
|
||||
},
|
||||
] | []
|
||||
}
|
||||
volumesArray: {
|
||||
pvc: *[
|
||||
for v in parameter.volumeMounts.pvc {
|
||||
{
|
||||
name: v.name
|
||||
persistentVolumeClaim: claimName: v.claimName
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
configMap: *[
|
||||
for v in parameter.volumeMounts.configMap {
|
||||
{
|
||||
name: v.name
|
||||
configMap: {
|
||||
defaultMode: v.defaultMode
|
||||
name: v.cmName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
secret: *[
|
||||
for v in parameter.volumeMounts.secret {
|
||||
{
|
||||
name: v.name
|
||||
secret: {
|
||||
defaultMode: v.defaultMode
|
||||
secretName: v.secretName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
emptyDir: *[
|
||||
for v in parameter.volumeMounts.emptyDir {
|
||||
{
|
||||
name: v.name
|
||||
emptyDir: medium: v.medium
|
||||
}
|
||||
},
|
||||
] | []
|
||||
|
||||
hostPath: *[
|
||||
for v in parameter.volumeMounts.hostPath {
|
||||
{
|
||||
name: v.name
|
||||
hostPath: path: v.path
|
||||
}
|
||||
},
|
||||
] | []
|
||||
}
|
||||
volumesList: volumesArray.pvc + volumesArray.configMap + volumesArray.secret + volumesArray.emptyDir + volumesArray.hostPath
|
||||
deDupVolumesArray: [
|
||||
for val in [
|
||||
for i, vi in volumesList {
|
||||
for j, vj in volumesList if j < i && vi.name == vj.name {
|
||||
_ignore: true
|
||||
}
|
||||
vi
|
||||
},
|
||||
] if val._ignore == _|_ {
|
||||
val
|
||||
},
|
||||
]
|
||||
output: {
|
||||
apiVersion: "apps/v1"
|
||||
kind: "Deployment"
|
||||
metadata: {
|
||||
if context.replicaKey != _|_ {
|
||||
name: context.name + "-" + context.replicaKey
|
||||
}
|
||||
if context.replicaKey == _|_ {
|
||||
name: context.name
|
||||
}
|
||||
}
|
||||
spec: {
|
||||
selector: matchLabels: {
|
||||
"app.oam.dev/component": context.name
|
||||
if context.replicaKey != _|_ {
|
||||
"app.oam.dev/replicaKey": context.replicaKey
|
||||
}
|
||||
}
|
||||
|
||||
template: {
|
||||
metadata: {
|
||||
labels: {
|
||||
if parameter.labels != _|_ {
|
||||
parameter.labels
|
||||
}
|
||||
if parameter.addRevisionLabel {
|
||||
"app.oam.dev/revision": context.revision
|
||||
}
|
||||
"app.oam.dev/name": context.appName
|
||||
"app.oam.dev/component": context.name
|
||||
if context.replicaKey != _|_ {
|
||||
"app.oam.dev/replicaKey": context.replicaKey
|
||||
}
|
||||
|
||||
}
|
||||
if parameter.annotations != _|_ {
|
||||
annotations: parameter.annotations
|
||||
}
|
||||
}
|
||||
|
||||
spec: {
|
||||
containers: [{
|
||||
name: context.name
|
||||
image: parameter.image
|
||||
if parameter["port"] != _|_ && parameter["ports"] == _|_ {
|
||||
ports: [{
|
||||
containerPort: parameter.port
|
||||
}]
|
||||
}
|
||||
if parameter["ports"] != _|_ {
|
||||
ports: [ for v in parameter.ports {
|
||||
{
|
||||
containerPort: v.port
|
||||
protocol: v.protocol
|
||||
if v.name != _|_ {
|
||||
name: v.name
|
||||
}
|
||||
if v.name == _|_ {
|
||||
name: "port-" + strconv.FormatInt(v.port, 10)
|
||||
}
|
||||
}}]
|
||||
}
|
||||
|
||||
if parameter["imagePullPolicy"] != _|_ {
|
||||
imagePullPolicy: parameter.imagePullPolicy
|
||||
}
|
||||
|
||||
if parameter["cmd"] != _|_ {
|
||||
command: parameter.cmd
|
||||
}
|
||||
|
||||
if parameter["env"] != _|_ {
|
||||
env: parameter.env
|
||||
}
|
||||
|
||||
if context["config"] != _|_ {
|
||||
env: context.config
|
||||
}
|
||||
|
||||
if parameter["cpu"] != _|_ {
|
||||
resources: {
|
||||
limits: cpu: parameter.cpu
|
||||
requests: cpu: parameter.cpu
|
||||
}
|
||||
}
|
||||
|
||||
if parameter["memory"] != _|_ {
|
||||
resources: {
|
||||
limits: memory: parameter.memory
|
||||
requests: memory: parameter.memory
|
||||
}
|
||||
}
|
||||
|
||||
if parameter["volumes"] != _|_ && parameter["volumeMounts"] == _|_ {
|
||||
volumeMounts: [ for v in parameter.volumes {
|
||||
{
|
||||
mountPath: v.mountPath
|
||||
name: v.name
|
||||
}}]
|
||||
}
|
||||
|
||||
if parameter["volumeMounts"] != _|_ {
|
||||
volumeMounts: mountsArray.pvc + mountsArray.configMap + mountsArray.secret + mountsArray.emptyDir + mountsArray.hostPath
|
||||
}
|
||||
|
||||
if parameter["livenessProbe"] != _|_ {
|
||||
livenessProbe: parameter.livenessProbe
|
||||
}
|
||||
|
||||
if parameter["readinessProbe"] != _|_ {
|
||||
readinessProbe: parameter.readinessProbe
|
||||
}
|
||||
|
||||
}]
|
||||
|
||||
if parameter["hostAliases"] != _|_ {
|
||||
// +patchKey=ip
|
||||
hostAliases: parameter.hostAliases
|
||||
}
|
||||
|
||||
if parameter["imagePullSecrets"] != _|_ {
|
||||
imagePullSecrets: [ for v in parameter.imagePullSecrets {
|
||||
name: v
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if parameter["volumes"] != _|_ && parameter["volumeMounts"] == _|_ {
|
||||
volumes: [ for v in parameter.volumes {
|
||||
{
|
||||
name: v.name
|
||||
if v.type == "pvc" {
|
||||
persistentVolumeClaim: claimName: v.claimName
|
||||
}
|
||||
if v.type == "configMap" {
|
||||
configMap: {
|
||||
defaultMode: v.defaultMode
|
||||
name: v.cmName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.type == "secret" {
|
||||
secret: {
|
||||
defaultMode: v.defaultMode
|
||||
secretName: v.secretName
|
||||
if v.items != _|_ {
|
||||
items: v.items
|
||||
}
|
||||
}
|
||||
}
|
||||
if v.type == "emptyDir" {
|
||||
emptyDir: medium: v.medium
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
if parameter["volumeMounts"] != _|_ {
|
||||
volumes: deDupVolumesArray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
exposePorts: [
|
||||
for v in parameter.ports if v.expose == true {
|
||||
port: v.port
|
||||
targetPort: v.port
|
||||
if v.name != _|_ {
|
||||
name: v.name
|
||||
}
|
||||
if v.name == _|_ {
|
||||
name: "port-" + strconv.FormatInt(v.port, 10)
|
||||
}
|
||||
},
|
||||
]
|
||||
outputs: {
|
||||
if len(exposePorts) != 0 {
|
||||
webserviceExpose: {
|
||||
apiVersion: "v1"
|
||||
kind: "Service"
|
||||
metadata: {
|
||||
if context.replicaKey != _|_ {
|
||||
name: context.name + "-" + context.replicaKey
|
||||
}
|
||||
if context.replicaKey == _|_ {
|
||||
name: context.name
|
||||
}
|
||||
}
|
||||
spec: {
|
||||
selector: {
|
||||
"app.oam.dev/component": context.name
|
||||
if context.replicaKey != _|_ {
|
||||
"app.oam.dev/replicaKey": context.replicaKey
|
||||
}
|
||||
}
|
||||
ports: exposePorts
|
||||
type: parameter.exposeType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parameter: {
|
||||
// +usage=Specify the labels in the workload
|
||||
labels?: [string]: string
|
||||
|
||||
// +usage=Specify the annotations in the workload
|
||||
annotations?: [string]: string
|
||||
|
||||
// +usage=Which image would you like to use for your service
|
||||
// +short=i
|
||||
image: string
|
||||
|
||||
// +usage=Specify image pull policy for your service
|
||||
imagePullPolicy?: "Always" | "Never" | "IfNotPresent"
|
||||
|
||||
// +usage=Specify image pull secrets for your service
|
||||
imagePullSecrets?: [...string]
|
||||
|
||||
// +ignore
|
||||
// +usage=Deprecated field, please use ports instead
|
||||
// +short=p
|
||||
port?: int
|
||||
|
||||
// +usage=Which ports do you want customer traffic sent to, defaults to 80
|
||||
ports?: [...{
|
||||
// +usage=Number of port to expose on the pod's IP address
|
||||
port: int
|
||||
// +usage=Name of the port
|
||||
name?: string
|
||||
// +usage=Protocol for port. Must be UDP, TCP, or SCTP
|
||||
protocol: *"TCP" | "UDP" | "SCTP"
|
||||
// +usage=Specify if the port should be exposed
|
||||
expose: *false | bool
|
||||
}]
|
||||
|
||||
// +ignore
|
||||
// +usage=Specify what kind of Service you want. options: "ClusterIP", "NodePort", "LoadBalancer"
|
||||
exposeType: *"ClusterIP" | "NodePort" | "LoadBalancer"
|
||||
|
||||
// +ignore
|
||||
// +usage=If addRevisionLabel is true, the revision label will be added to the underlying pods
|
||||
addRevisionLabel: *false | bool
|
||||
|
||||
// +usage=Commands to run in the container
|
||||
cmd?: [...string]
|
||||
|
||||
// +usage=Define arguments by using environment variables
|
||||
env?: [...{
|
||||
// +usage=Environment variable name
|
||||
name: string
|
||||
// +usage=The value of the environment variable
|
||||
value?: string
|
||||
// +usage=Specifies a source the value of this var should come from
|
||||
valueFrom?: {
|
||||
// +usage=Selects a key of a secret in the pod's namespace
|
||||
secretKeyRef?: {
|
||||
// +usage=The name of the secret in the pod's namespace to select from
|
||||
name: string
|
||||
// +usage=The key of the secret to select from. Must be a valid secret key
|
||||
key: string
|
||||
}
|
||||
// +usage=Selects a key of a config map in the pod's namespace
|
||||
configMapKeyRef?: {
|
||||
// +usage=The name of the config map in the pod's namespace to select from
|
||||
name: string
|
||||
// +usage=The key of the config map to select from. Must be a valid secret key
|
||||
key: string
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
// +usage=Number of CPU units for the service, like `0.5` (0.5 CPU core), `1` (1 CPU core)
|
||||
cpu?: string
|
||||
|
||||
// +usage=Specifies the attributes of the memory resource required for the container.
|
||||
memory?: string
|
||||
|
||||
volumeMounts?: {
|
||||
// +usage=Mount PVC type volume
|
||||
pvc?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
subPath?: string
|
||||
// +usage=The name of the PVC
|
||||
claimName: string
|
||||
}]
|
||||
// +usage=Mount ConfigMap type volume
|
||||
configMap?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
subPath?: string
|
||||
defaultMode: *420 | int
|
||||
cmName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}]
|
||||
// +usage=Mount Secret type volume
|
||||
secret?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
subPath?: string
|
||||
defaultMode: *420 | int
|
||||
secretName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}]
|
||||
// +usage=Mount EmptyDir type volume
|
||||
emptyDir?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
subPath?: string
|
||||
medium: *"" | "Memory"
|
||||
}]
|
||||
// +usage=Mount HostPath type volume
|
||||
hostPath?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
subPath?: string
|
||||
path: string
|
||||
}]
|
||||
}
|
||||
|
||||
// +usage=Deprecated field, use volumeMounts instead.
|
||||
volumes?: [...{
|
||||
name: string
|
||||
mountPath: string
|
||||
// +usage=Specify volume type, options: "pvc","configMap","secret","emptyDir"
|
||||
type: "pvc" | "configMap" | "secret" | "emptyDir"
|
||||
if type == "pvc" {
|
||||
claimName: string
|
||||
}
|
||||
if type == "configMap" {
|
||||
defaultMode: *420 | int
|
||||
cmName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}
|
||||
if type == "secret" {
|
||||
defaultMode: *420 | int
|
||||
secretName: string
|
||||
items?: [...{
|
||||
key: string
|
||||
path: string
|
||||
mode: *511 | int
|
||||
}]
|
||||
}
|
||||
if type == "emptyDir" {
|
||||
medium: *"" | "Memory"
|
||||
}
|
||||
}]
|
||||
|
||||
// +usage=Instructions for assessing whether the container is alive.
|
||||
livenessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Instructions for assessing whether the container is in a suitable state to serve traffic.
|
||||
readinessProbe?: #HealthProbe
|
||||
|
||||
// +usage=Specify the hostAliases to add
|
||||
hostAliases?: [...{
|
||||
ip: string
|
||||
hostnames: [...string]
|
||||
}]
|
||||
}
|
||||
#HealthProbe: {
|
||||
|
||||
// +usage=Instructions for assessing container health by executing a command. Either this attribute or the httpGet attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the httpGet attribute and the tcpSocket attribute.
|
||||
exec?: {
|
||||
// +usage=A command to be executed inside the container to assess its health. Each space delimited token of the command is a separate array element. Commands exiting 0 are considered to be successful probes, whilst all other exit codes are considered failures.
|
||||
command: [...string]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by executing an HTTP GET request. Either this attribute or the exec attribute or the tcpSocket attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the tcpSocket attribute.
|
||||
httpGet?: {
|
||||
// +usage=The endpoint, relative to the port, to which the HTTP GET request should be directed.
|
||||
path: string
|
||||
// +usage=The TCP socket within the container to which the HTTP GET request should be directed.
|
||||
port: int
|
||||
host?: string
|
||||
scheme?: *"HTTP" | string
|
||||
httpHeaders?: [...{
|
||||
name: string
|
||||
value: string
|
||||
}]
|
||||
}
|
||||
|
||||
// +usage=Instructions for assessing container health by probing a TCP socket. Either this attribute or the exec attribute or the httpGet attribute MUST be specified. This attribute is mutually exclusive with both the exec attribute and the httpGet attribute.
|
||||
tcpSocket?: {
|
||||
// +usage=The TCP socket within the container that should be probed to assess container health.
|
||||
port: int
|
||||
}
|
||||
|
||||
// +usage=Number of seconds after the container is started before the first probe is initiated.
|
||||
initialDelaySeconds: *0 | int
|
||||
|
||||
// +usage=How often, in seconds, to execute the probe.
|
||||
periodSeconds: *10 | int
|
||||
|
||||
// +usage=Number of seconds after which the probe times out.
|
||||
timeoutSeconds: *1 | int
|
||||
|
||||
// +usage=Minimum consecutive successes for the probe to be considered successful after having failed.
|
||||
successThreshold: *1 | int
|
||||
|
||||
// +usage=Number of consecutive failures required to determine the container is not alive (liveness probe) or not ready (readiness probe).
|
||||
failureThreshold: *3 | int
|
||||
}
|
||||
status:
|
||||
customStatus: |-
|
||||
ready: {
|
||||
readyReplicas: *0 | int
|
||||
} & {
|
||||
if context.output.status.readyReplicas != _|_ {
|
||||
readyReplicas: context.output.status.readyReplicas
|
||||
}
|
||||
}
|
||||
message: "Ready:\(ready.readyReplicas)/\(context.output.spec.replicas)"
|
||||
healthPolicy: |-
|
||||
ready: {
|
||||
updatedReplicas: *0 | int
|
||||
readyReplicas: *0 | int
|
||||
replicas: *0 | int
|
||||
observedGeneration: *0 | int
|
||||
} & {
|
||||
if context.output.status.updatedReplicas != _|_ {
|
||||
updatedReplicas: context.output.status.updatedReplicas
|
||||
}
|
||||
if context.output.status.readyReplicas != _|_ {
|
||||
readyReplicas: context.output.status.readyReplicas
|
||||
}
|
||||
if context.output.status.replicas != _|_ {
|
||||
replicas: context.output.status.replicas
|
||||
}
|
||||
if context.output.status.observedGeneration != _|_ {
|
||||
observedGeneration: context.output.status.observedGeneration
|
||||
}
|
||||
}
|
||||
isHealth: (context.output.spec.replicas == ready.readyReplicas) && (context.output.spec.replicas == ready.updatedReplicas) && (context.output.spec.replicas == ready.replicas) && (ready.observedGeneration == context.output.metadata.generation || ready.observedGeneration > context.output.metadata.generation)
|
||||
workload:
|
||||
definition:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
type: deployments.apps
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
"replication": {
|
||||
annotations: {}
|
||||
description: "Describe the configuration to replicate components when deploying resources, it only works with specified `deploy` step in workflow."
|
||||
labels: {}
|
||||
attributes: {}
|
||||
type: "policy"
|
||||
}
|
||||
|
||||
template: {
|
||||
parameter: {
|
||||
// +usage=Spicify the keys of replication. Every key coresponds to a replication components
|
||||
keys: [...string]
|
||||
// +usage=Specify the components which will be replicated.
|
||||
selector?: [...string]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user