mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 10:00:06 +00:00
Feat: support resource update policy (#6003)
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
|
||||
70
apis/core.oam.dev/v1alpha1/resource_update_policy_types.go
Normal file
70
apis/core.oam.dev/v1alpha1/resource_update_policy_types.go
Normal file
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2023 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 v1alpha1
|
||||
|
||||
import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
const (
|
||||
// ResourceUpdatePolicyType refers to the type of resource-update policy
|
||||
ResourceUpdatePolicyType = "resource-update"
|
||||
)
|
||||
|
||||
// ResourceUpdatePolicySpec defines the spec of resource-update policy
|
||||
type ResourceUpdatePolicySpec struct {
|
||||
Rules []ResourceUpdatePolicyRule `json:"rules"`
|
||||
}
|
||||
|
||||
// Type the type name of the policy
|
||||
func (in *ResourceUpdatePolicySpec) Type() string {
|
||||
return ResourceUpdatePolicyType
|
||||
}
|
||||
|
||||
// ResourceUpdatePolicyRule defines the rule for resource-update resources
|
||||
type ResourceUpdatePolicyRule struct {
|
||||
// Selector picks which resources should be affected
|
||||
Selector ResourcePolicyRuleSelector `json:"selector"`
|
||||
// Strategy the strategy for updating resources
|
||||
Strategy ResourceUpdateStrategy `json:"strategy,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceUpdateStrategy the update strategy for resource
|
||||
type ResourceUpdateStrategy struct {
|
||||
// Op the update op for selected resources
|
||||
Op ResourceUpdateOp `json:"op,omitempty"`
|
||||
// RecreateFields the field path which will trigger recreate if changed
|
||||
RecreateFields []string `json:"recreateFields,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceUpdateOp update op for resource
|
||||
type ResourceUpdateOp string
|
||||
|
||||
const (
|
||||
// ResourceUpdateStrategyPatch patch the target resource (three-way patch)
|
||||
ResourceUpdateStrategyPatch ResourceUpdateOp = "patch"
|
||||
// ResourceUpdateStrategyReplace update the target resource
|
||||
ResourceUpdateStrategyReplace ResourceUpdateOp = "replace"
|
||||
)
|
||||
|
||||
// FindStrategy return if the target resource is read-only
|
||||
func (in *ResourceUpdatePolicySpec) FindStrategy(manifest *unstructured.Unstructured) *ResourceUpdateStrategy {
|
||||
for _, rule := range in.Rules {
|
||||
if rule.Selector.Match(manifest) {
|
||||
return &rule.Strategy
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
@@ -725,6 +725,65 @@ func (in *ResourcePolicyRuleSelector) DeepCopy() *ResourcePolicyRuleSelector {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceUpdatePolicyRule) DeepCopyInto(out *ResourceUpdatePolicyRule) {
|
||||
*out = *in
|
||||
in.Selector.DeepCopyInto(&out.Selector)
|
||||
in.Strategy.DeepCopyInto(&out.Strategy)
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceUpdatePolicyRule.
|
||||
func (in *ResourceUpdatePolicyRule) DeepCopy() *ResourceUpdatePolicyRule {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceUpdatePolicyRule)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceUpdatePolicySpec) DeepCopyInto(out *ResourceUpdatePolicySpec) {
|
||||
*out = *in
|
||||
if in.Rules != nil {
|
||||
in, out := &in.Rules, &out.Rules
|
||||
*out = make([]ResourceUpdatePolicyRule, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceUpdatePolicySpec.
|
||||
func (in *ResourceUpdatePolicySpec) DeepCopy() *ResourceUpdatePolicySpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceUpdatePolicySpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceUpdateStrategy) DeepCopyInto(out *ResourceUpdateStrategy) {
|
||||
*out = *in
|
||||
if in.RecreateFields != nil {
|
||||
in, out := &in.RecreateFields, &out.RecreateFields
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceUpdateStrategy.
|
||||
func (in *ResourceUpdateStrategy) DeepCopy() *ResourceUpdateStrategy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceUpdateStrategy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SharedResourcePolicyRule) DeepCopyInto(out *SharedResourcePolicyRule) {
|
||||
*out = *in
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Code generated by KubeVela templates. DO NOT EDIT. Please edit the original cue file.
|
||||
# Definition source cue file: vela-templates/definitions/internal/resource-update.cue
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: PolicyDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
definition.oam.dev/description: Configure the update strategy for selected resources.
|
||||
name: resource-update
|
||||
namespace: {{ include "systemDefinitionNamespace" . }}
|
||||
spec:
|
||||
schematic:
|
||||
cue:
|
||||
template: |
|
||||
#PolicyRule: {
|
||||
// +usage=Specify how to select the targets of the rule
|
||||
selector: #RuleSelector
|
||||
// +usage=The update strategy for the target resources
|
||||
strategy: #Strategy
|
||||
}
|
||||
|
||||
#Strategy: {
|
||||
// +usage=Specify the op for updating target resources
|
||||
op: *"patch" | "replace"
|
||||
// +usage=Specify which fields would trigger recreation when updated
|
||||
recreateFields?: [...string]
|
||||
}
|
||||
|
||||
#RuleSelector: {
|
||||
// +usage=Select resources by component names
|
||||
componentNames?: [...string]
|
||||
// +usage=Select resources by component types
|
||||
componentTypes?: [...string]
|
||||
// +usage=Select resources by oamTypes (COMPONENT or TRAIT)
|
||||
oamTypes?: [...string]
|
||||
// +usage=Select resources by trait types
|
||||
traitTypes?: [...string]
|
||||
// +usage=Select resources by resource types (like Deployment)
|
||||
resourceTypes?: [...string]
|
||||
// +usage=Select resources by their names
|
||||
resourceNames?: [...string]
|
||||
}
|
||||
|
||||
parameter: {
|
||||
// +usage=Specify the list of rules to control resource update strategy at resource level.
|
||||
rules?: [...#PolicyRule]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2021 The KubeVela Authors.
|
||||
Copyright 2023 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.
|
||||
|
||||
@@ -397,6 +397,7 @@ func (p *Parser) parsePoliciesFromRevision(ctx context.Context, af *Appfile) (er
|
||||
case v1alpha1.SharedResourcePolicyType:
|
||||
case v1alpha1.TakeOverPolicyType:
|
||||
case v1alpha1.ReadOnlyPolicyType:
|
||||
case v1alpha1.ResourceUpdatePolicyType:
|
||||
case v1alpha1.EnvBindingPolicyType:
|
||||
case v1alpha1.TopologyPolicyType:
|
||||
case v1alpha1.OverridePolicyType:
|
||||
@@ -428,6 +429,7 @@ func (p *Parser) parsePolicies(ctx context.Context, af *Appfile) (err error) {
|
||||
case v1alpha1.SharedResourcePolicyType:
|
||||
case v1alpha1.TakeOverPolicyType:
|
||||
case v1alpha1.ReadOnlyPolicyType:
|
||||
case v1alpha1.ResourceUpdatePolicyType:
|
||||
case v1alpha1.EnvBindingPolicyType:
|
||||
case v1alpha1.TopologyPolicyType:
|
||||
case v1alpha1.ReplicationPolicyType:
|
||||
|
||||
@@ -41,7 +41,7 @@ const (
|
||||
// DisableReferObjectsFromURL if set, the url ref objects will be disallowed
|
||||
DisableReferObjectsFromURL featuregate.Feature = "DisableReferObjectsFromURL"
|
||||
|
||||
// ApplyResourceByUpdate enforces the modification of resource through update requests.
|
||||
// ApplyResourceByReplace enforces the modification of resource through PUT requests.
|
||||
// If not set, the resource modification will use patch requests (three-way-strategy-merge-patch).
|
||||
// The side effect of enabling this feature is that the request traffic will increase due to
|
||||
// the increase of bytes transferred and the more frequent resource mutation failure due to the
|
||||
@@ -50,7 +50,7 @@ const (
|
||||
// system would be unable to make modifications to the KubeVela managed resource. In other words,
|
||||
// no merge for modifications from multiple sources. Only KubeVela keeps the Source-of-Truth for the
|
||||
// resource.
|
||||
ApplyResourceByUpdate featuregate.Feature = "ApplyResourceByUpdate"
|
||||
ApplyResourceByReplace featuregate.Feature = "ApplyResourceByReplace"
|
||||
|
||||
// Edge Features
|
||||
|
||||
@@ -123,7 +123,7 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
|
||||
LegacyComponentRevision: {Default: false, PreRelease: featuregate.Alpha},
|
||||
LegacyResourceOwnerValidation: {Default: false, PreRelease: featuregate.Alpha},
|
||||
DisableReferObjectsFromURL: {Default: false, PreRelease: featuregate.Alpha},
|
||||
ApplyResourceByUpdate: {Default: false, PreRelease: featuregate.Alpha},
|
||||
ApplyResourceByReplace: {Default: false, PreRelease: featuregate.Alpha},
|
||||
AuthenticateApplication: {Default: false, PreRelease: featuregate.Alpha},
|
||||
GzipResourceTracker: {Default: false, PreRelease: featuregate.Alpha},
|
||||
ZstdResourceTracker: {Default: false, PreRelease: featuregate.Alpha},
|
||||
|
||||
@@ -158,6 +158,9 @@ func (h *resourceKeeper) dispatch(ctx context.Context, manifests []*unstructured
|
||||
if h.canTakeOver(manifest) {
|
||||
ao = append([]apply.ApplyOption{apply.TakeOver()}, ao...)
|
||||
}
|
||||
if strategy := h.getUpdateStrategy(manifest); strategy != nil {
|
||||
ao = append([]apply.ApplyOption{apply.WithUpdateStrategy(*strategy)}, ao...)
|
||||
}
|
||||
manifest, err := ApplyStrategies(applyCtx, h, manifest, v1alpha1.ApplyOnceStrategyOnAppUpdate)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to apply once policy for application %s,%s", h.app.Name, err.Error())
|
||||
|
||||
@@ -63,6 +63,7 @@ type resourceKeeper struct {
|
||||
sharedResourcePolicy *v1alpha1.SharedResourcePolicySpec
|
||||
takeOverPolicy *v1alpha1.TakeOverPolicySpec
|
||||
readOnlyPolicy *v1alpha1.ReadOnlyPolicySpec
|
||||
resourceUpdatePolicy *v1alpha1.ResourceUpdatePolicySpec
|
||||
|
||||
cache *resourceCache
|
||||
}
|
||||
@@ -113,6 +114,9 @@ func (h *resourceKeeper) parseApplicationResourcePolicy() (err error) {
|
||||
if h.readOnlyPolicy, err = policy.ParsePolicy[v1alpha1.ReadOnlyPolicySpec](h.app); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse read-only policy")
|
||||
}
|
||||
if h.resourceUpdatePolicy, err = policy.ParsePolicy[v1alpha1.ResourceUpdatePolicySpec](h.app); err != nil {
|
||||
return errors.Wrapf(err, "failed to parse resource-update policy")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,9 @@ func (h *resourceKeeper) StateKeep(ctx context.Context) error {
|
||||
if h.canTakeOver(manifest) {
|
||||
ao = append([]apply.ApplyOption{apply.TakeOver()}, ao...)
|
||||
}
|
||||
if strategy := h.getUpdateStrategy(manifest); strategy != nil {
|
||||
ao = append([]apply.ApplyOption{apply.WithUpdateStrategy(*strategy)}, ao...)
|
||||
}
|
||||
if err = h.applicator.Apply(applyCtx, manifest, ao...); err != nil {
|
||||
return errors.Wrapf(err, "failed to re-apply resource %s from resourcetracker %s", mr.ResourceKey(), rt.Name)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/utils/strings/slices"
|
||||
|
||||
"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"
|
||||
"github.com/oam-dev/kubevela/pkg/utils"
|
||||
@@ -55,6 +56,13 @@ func (h *resourceKeeper) isReadOnly(manifest *unstructured.Unstructured) bool {
|
||||
return h.readOnlyPolicy.FindStrategy(manifest)
|
||||
}
|
||||
|
||||
func (h *resourceKeeper) getUpdateStrategy(manifest *unstructured.Unstructured) *v1alpha1.ResourceUpdateStrategy {
|
||||
if h.resourceUpdatePolicy == nil {
|
||||
return nil
|
||||
}
|
||||
return h.resourceUpdatePolicy.FindStrategy(manifest)
|
||||
}
|
||||
|
||||
// hasOrphanFinalizer checks if the target application should orphan child resources
|
||||
func hasOrphanFinalizer(app *v1beta1.Application) bool {
|
||||
return slices.Contains(app.GetFinalizers(), oam.FinalizerOrphanResource)
|
||||
|
||||
@@ -20,19 +20,23 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
|
||||
"github.com/pkg/errors"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
|
||||
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
|
||||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||||
"github.com/oam-dev/kubevela/pkg/controller/utils"
|
||||
"github.com/oam-dev/kubevela/pkg/features"
|
||||
@@ -63,6 +67,7 @@ type applyAction struct {
|
||||
updateAnnotation bool
|
||||
dryRun bool
|
||||
quiet bool
|
||||
updateStrategy v1alpha1.ResourceUpdateStrategy
|
||||
}
|
||||
|
||||
// ApplyOption is called before applying state to the object.
|
||||
@@ -153,6 +158,29 @@ func trimLastAppliedConfigurationForSpecialResources(desired client.Object) bool
|
||||
return true
|
||||
}
|
||||
|
||||
func needRecreate(recreateFields []string, existing, desired client.Object) (bool, error) {
|
||||
if len(recreateFields) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
_existing, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(existing)
|
||||
_desired, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(desired)
|
||||
flag := false
|
||||
for _, field := range recreateFields {
|
||||
ve, err := fieldpath.Pave(_existing).GetValue(field)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to get path %s from existing object: %w", field, err)
|
||||
}
|
||||
vd, err := fieldpath.Pave(_desired).GetValue(field)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to get path %s from desired object: %w", field, err)
|
||||
}
|
||||
if !reflect.DeepEqual(ve, vd) {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
return flag, nil
|
||||
}
|
||||
|
||||
// Apply applies new state to an object or create it if not exist
|
||||
func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...ApplyOption) error {
|
||||
_, err := generateRenderHash(desired)
|
||||
@@ -178,15 +206,43 @@ func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByUpdate) && isUpdatableResource(desired):
|
||||
loggingApply("updating object", desired, applyAct.quiet)
|
||||
strategy := applyAct.updateStrategy
|
||||
if strategy.Op == "" {
|
||||
if utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByReplace) && isUpdatableResource(desired) {
|
||||
strategy.Op = v1alpha1.ResourceUpdateStrategyReplace
|
||||
} else {
|
||||
strategy.Op = v1alpha1.ResourceUpdateStrategyPatch
|
||||
}
|
||||
}
|
||||
|
||||
shouldRecreate, err := needRecreate(strategy.RecreateFields, existing, desired)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to evaluate recreateFields: %w", err)
|
||||
}
|
||||
if shouldRecreate {
|
||||
loggingApply("recreating object", desired, applyAct.quiet)
|
||||
if applyAct.dryRun { // recreate does not support dryrun
|
||||
return nil
|
||||
}
|
||||
if existing.GetDeletionTimestamp() == nil { // check if recreation needed
|
||||
if err = a.c.Delete(ctx, existing); err != nil {
|
||||
return errors.Wrap(err, "cannot delete object")
|
||||
}
|
||||
}
|
||||
return errors.Wrap(a.c.Create(ctx, desired), "cannot recreate object")
|
||||
}
|
||||
|
||||
switch strategy.Op {
|
||||
case v1alpha1.ResourceUpdateStrategyReplace:
|
||||
loggingApply("replacing object", desired, applyAct.quiet)
|
||||
desired.SetResourceVersion(existing.GetResourceVersion())
|
||||
var options []client.UpdateOption
|
||||
if applyAct.dryRun {
|
||||
options = append(options, client.DryRunAll)
|
||||
}
|
||||
return errors.Wrapf(a.c.Update(ctx, desired, options...), "cannot update object")
|
||||
case v1alpha1.ResourceUpdateStrategyPatch:
|
||||
fallthrough
|
||||
default:
|
||||
loggingApply("patching object", desired, applyAct.quiet)
|
||||
patch, err := a.patcher.patch(existing, desired, applyAct)
|
||||
@@ -322,6 +378,14 @@ func TakeOver() ApplyOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithUpdateStrategy set the update strategy for the apply operation
|
||||
func WithUpdateStrategy(strategy v1alpha1.ResourceUpdateStrategy) ApplyOption {
|
||||
return func(act *applyAction, _, _ client.Object) error {
|
||||
act.updateStrategy = strategy
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MustBeControllableBy requires that the new object is controllable by an
|
||||
// object with the supplied UID. An object is controllable if its controller
|
||||
// reference includes the supplied UID.
|
||||
|
||||
@@ -185,7 +185,7 @@ var _ = Describe("Test apply", func() {
|
||||
Expect(rawClient.Update(ctx, modifiedDeploy)).Should(Succeed())
|
||||
|
||||
By("Test patch")
|
||||
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=false", features.ApplyResourceByUpdate))).Should(Succeed())
|
||||
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=false", features.ApplyResourceByReplace))).Should(Succeed())
|
||||
Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed())
|
||||
copy1 := originalDeploy.DeepCopy()
|
||||
copy1.SetResourceVersion(deploy.ResourceVersion)
|
||||
@@ -194,7 +194,7 @@ var _ = Describe("Test apply", func() {
|
||||
Expect(len(deploy.Spec.Template.Spec.Containers)).Should(Equal(2))
|
||||
|
||||
By("Test update")
|
||||
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=true", features.ApplyResourceByUpdate))).Should(Succeed())
|
||||
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=true", features.ApplyResourceByReplace))).Should(Succeed())
|
||||
Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed())
|
||||
copy2 := originalDeploy.DeepCopy()
|
||||
copy2.SetResourceVersion(deploy.ResourceVersion)
|
||||
@@ -202,7 +202,7 @@ var _ = Describe("Test apply", func() {
|
||||
Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed())
|
||||
Expect(len(deploy.Spec.Template.Spec.Containers)).Should(Equal(1))
|
||||
|
||||
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=false", features.ApplyResourceByUpdate))).Should(Succeed())
|
||||
Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=false", features.ApplyResourceByReplace))).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
61
references/docgen/def-doc/policy/resource-update.eg.md
Normal file
61
references/docgen/def-doc/policy/resource-update.eg.md
Normal file
@@ -0,0 +1,61 @@
|
||||
`resource-update` policy can allow users to customize the update behavior for selected resources.
|
||||
|
||||
|
||||
```yaml
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: recreate
|
||||
spec:
|
||||
components:
|
||||
- type: k8s-objects
|
||||
name: recreate
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: recreate
|
||||
data:
|
||||
key: dgo=
|
||||
immutable: true
|
||||
policies:
|
||||
- type: resource-update
|
||||
name: resource-update
|
||||
properties:
|
||||
rules:
|
||||
- selector:
|
||||
resourceTypes: ["Secret"]
|
||||
strategy:
|
||||
recreateFields: ["data.key"]
|
||||
```
|
||||
By specifying `recreateFields`, the application will recreate the target resource (**Secret** here) when the field changes (`data.key` here). If the field is not changed, the application will use the normal update (**patch** here).
|
||||
|
||||
```yaml
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: recreate
|
||||
spec:
|
||||
components:
|
||||
- type: k8s-objects
|
||||
name: recreate
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: recreate
|
||||
data:
|
||||
key: val
|
||||
policies:
|
||||
- type: resource-update
|
||||
name: resource-update
|
||||
properties:
|
||||
rules:
|
||||
- selector:
|
||||
resourceTypes: ["ConfigMap"]
|
||||
strategy:
|
||||
op: replace
|
||||
```
|
||||
By specifying `op` to `replace`, the application will update the given resource (ConfigMap here) by replace. Compared to **patch**, which leverages three-way merge patch to only modify the fields managed by KubeVela application, "replace" will update the object as a whole and wipe out other fields even if it is not managed by the KubeVela application. It can be seen as an "application-level" *ApplyResourceByReplace*.
|
||||
@@ -1166,5 +1166,55 @@ var _ = Describe("Test multicluster scenario", func() {
|
||||
g.Expect(len(_revs.Items)).Should(Equal(1))
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
})
|
||||
|
||||
It("Test application with resource-update policy", func() {
|
||||
ctx := context.Background()
|
||||
app := &v1beta1.Application{}
|
||||
bs, err := os.ReadFile("./testdata/app/app-recreate-test.yaml")
|
||||
Expect(err).Should(Succeed())
|
||||
Expect(yaml.Unmarshal(bs, app)).Should(Succeed())
|
||||
app.SetNamespace(namespace)
|
||||
Eventually(func(g Gomega) {
|
||||
g.Expect(k8sClient.Create(ctx, app)).Should(Succeed())
|
||||
}).WithPolling(2 * time.Second).WithTimeout(5 * time.Second).Should(Succeed())
|
||||
appKey := client.ObjectKeyFromObject(app)
|
||||
Eventually(func(g Gomega) {
|
||||
_app := &v1beta1.Application{}
|
||||
g.Expect(k8sClient.Get(ctx, appKey, _app)).Should(Succeed())
|
||||
g.Expect(_app.Status.Phase).Should(Equal(common.ApplicationRunning))
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
|
||||
By("update configmap")
|
||||
Eventually(func(g Gomega) {
|
||||
cm := &corev1.ConfigMap{}
|
||||
g.Expect(k8sClient.Get(ctx, appKey, cm)).Should(Succeed())
|
||||
cm.Data["extra"] = "extra-val"
|
||||
g.Expect(k8sClient.Update(ctx, cm)).Should(Succeed())
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
|
||||
By("update application")
|
||||
Expect(yaml.Unmarshal([]byte(strings.ReplaceAll(strings.ReplaceAll(string(bs), "key: dgo=", "key: dnZ2Cg=="), "key: val", "key: val2")), app)).Should(Succeed())
|
||||
Eventually(func(g Gomega) {
|
||||
_app := &v1beta1.Application{}
|
||||
g.Expect(k8sClient.Get(ctx, appKey, _app)).Should(Succeed())
|
||||
app.ResourceVersion = _app.ResourceVersion
|
||||
g.Expect(k8sClient.Update(ctx, app)).Should(Succeed())
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
Eventually(func(g Gomega) {
|
||||
_app := &v1beta1.Application{}
|
||||
g.Expect(k8sClient.Get(ctx, appKey, _app)).Should(Succeed())
|
||||
g.Expect(_app.Status.Phase).Should(Equal(common.ApplicationRunning))
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
|
||||
By("validate updated result")
|
||||
Eventually(func(g Gomega) {
|
||||
cm := &corev1.ConfigMap{}
|
||||
g.Expect(k8sClient.Get(ctx, appKey, cm)).Should(Succeed())
|
||||
g.Expect(len(cm.Data)).Should(Equal(1))
|
||||
secret := &corev1.Secret{}
|
||||
g.Expect(k8sClient.Get(ctx, appKey, secret)).Should(Succeed())
|
||||
g.Expect(string(secret.Data["key"])).Should(Equal("vvv\n"))
|
||||
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed())
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
37
test/e2e-multicluster-test/testdata/app/app-recreate-test.yaml
vendored
Normal file
37
test/e2e-multicluster-test/testdata/app/app-recreate-test.yaml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
apiVersion: core.oam.dev/v1beta1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: recreate
|
||||
spec:
|
||||
components:
|
||||
- type: k8s-objects
|
||||
name: recreate
|
||||
properties:
|
||||
objects:
|
||||
- apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: recreate
|
||||
data:
|
||||
key: dgo=
|
||||
value: dgo=
|
||||
immutable: true
|
||||
- apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: recreate
|
||||
data:
|
||||
key: val
|
||||
policies:
|
||||
- type: resource-update
|
||||
name: resource-update
|
||||
properties:
|
||||
rules:
|
||||
- selector:
|
||||
resourceTypes: ["Secret"]
|
||||
strategy:
|
||||
recreateFields: ["data.key"]
|
||||
- selector:
|
||||
resourceTypes: ["ConfigMap"]
|
||||
strategy:
|
||||
op: replace
|
||||
@@ -0,0 +1,43 @@
|
||||
"resource-update": {
|
||||
annotations: {}
|
||||
description: "Configure the update strategy for selected resources."
|
||||
labels: {}
|
||||
attributes: {}
|
||||
type: "policy"
|
||||
}
|
||||
|
||||
template: {
|
||||
#PolicyRule: {
|
||||
// +usage=Specify how to select the targets of the rule
|
||||
selector: #RuleSelector
|
||||
// +usage=The update strategy for the target resources
|
||||
strategy: #Strategy
|
||||
}
|
||||
|
||||
#Strategy: {
|
||||
// +usage=Specify the op for updating target resources
|
||||
op: *"patch" | "replace"
|
||||
// +usage=Specify which fields would trigger recreation when updated
|
||||
recreateFields?: [...string]
|
||||
}
|
||||
|
||||
#RuleSelector: {
|
||||
// +usage=Select resources by component names
|
||||
componentNames?: [...string]
|
||||
// +usage=Select resources by component types
|
||||
componentTypes?: [...string]
|
||||
// +usage=Select resources by oamTypes (COMPONENT or TRAIT)
|
||||
oamTypes?: [...string]
|
||||
// +usage=Select resources by trait types
|
||||
traitTypes?: [...string]
|
||||
// +usage=Select resources by resource types (like Deployment)
|
||||
resourceTypes?: [...string]
|
||||
// +usage=Select resources by their names
|
||||
resourceNames?: [...string]
|
||||
}
|
||||
|
||||
parameter: {
|
||||
// +usage=Specify the list of rules to control resource update strategy at resource level.
|
||||
rules?: [...#PolicyRule]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user