Feat: component replication (#4449)

This commit is contained in:
qiaozp
2022-08-17 14:04:57 +08:00
committed by GitHub
parent bcb3550025
commit 924d55381e
27 changed files with 1345 additions and 45 deletions
+4
View File
@@ -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]
}
+3 -3
View File
@@ -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 != "" {
+2 -2
View File
@@ -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())
+1
View File
@@ -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
+3
View File
@@ -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
+2
View File
@@ -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"
)
+8
View File
@@ -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 {
+2
View File
@@ -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
+2 -18
View File
@@ -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{}
+95
View File
@@ -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
}
+167
View File
@@ -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)
}
})
}
}
+35
View File
@@ -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
}
+55
View File
@@ -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)
}
+5 -20
View File
@@ -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
+2 -2
View File
@@ -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
```
+39
View File
@@ -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
View File
@@ -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]
}
}