Feat: support resource update policy (#6003)

This commit is contained in:
Somefive
2023-05-17 16:11:06 +08:00
committed by GitHub
parent eaa7f5821e
commit 530d7c5bd6
21 changed files with 467 additions and 16 deletions

View File

@@ -2,7 +2,7 @@
// +build !ignore_autogenerated // +build !ignore_autogenerated
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -2,7 +2,7 @@
// +build !ignore_autogenerated // +build !ignore_autogenerated
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View 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
}

View File

@@ -2,7 +2,7 @@
// +build !ignore_autogenerated // +build !ignore_autogenerated
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with 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 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. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SharedResourcePolicyRule) DeepCopyInto(out *SharedResourcePolicyRule) { func (in *SharedResourcePolicyRule) DeepCopyInto(out *SharedResourcePolicyRule) {
*out = *in *out = *in

View File

@@ -2,7 +2,7 @@
// +build !ignore_autogenerated // +build !ignore_autogenerated
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -2,7 +2,7 @@
// +build !ignore_autogenerated // +build !ignore_autogenerated
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -2,7 +2,7 @@
// +build !ignore_autogenerated // +build !ignore_autogenerated
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -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]
}

View File

@@ -1,5 +1,5 @@
/* /*
Copyright 2021 The KubeVela Authors. Copyright 2023 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.

View File

@@ -397,6 +397,7 @@ func (p *Parser) parsePoliciesFromRevision(ctx context.Context, af *Appfile) (er
case v1alpha1.SharedResourcePolicyType: case v1alpha1.SharedResourcePolicyType:
case v1alpha1.TakeOverPolicyType: case v1alpha1.TakeOverPolicyType:
case v1alpha1.ReadOnlyPolicyType: case v1alpha1.ReadOnlyPolicyType:
case v1alpha1.ResourceUpdatePolicyType:
case v1alpha1.EnvBindingPolicyType: case v1alpha1.EnvBindingPolicyType:
case v1alpha1.TopologyPolicyType: case v1alpha1.TopologyPolicyType:
case v1alpha1.OverridePolicyType: case v1alpha1.OverridePolicyType:
@@ -428,6 +429,7 @@ func (p *Parser) parsePolicies(ctx context.Context, af *Appfile) (err error) {
case v1alpha1.SharedResourcePolicyType: case v1alpha1.SharedResourcePolicyType:
case v1alpha1.TakeOverPolicyType: case v1alpha1.TakeOverPolicyType:
case v1alpha1.ReadOnlyPolicyType: case v1alpha1.ReadOnlyPolicyType:
case v1alpha1.ResourceUpdatePolicyType:
case v1alpha1.EnvBindingPolicyType: case v1alpha1.EnvBindingPolicyType:
case v1alpha1.TopologyPolicyType: case v1alpha1.TopologyPolicyType:
case v1alpha1.ReplicationPolicyType: case v1alpha1.ReplicationPolicyType:

View File

@@ -41,7 +41,7 @@ const (
// DisableReferObjectsFromURL if set, the url ref objects will be disallowed // DisableReferObjectsFromURL if set, the url ref objects will be disallowed
DisableReferObjectsFromURL featuregate.Feature = "DisableReferObjectsFromURL" 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). // 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 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 // 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, // 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 // no merge for modifications from multiple sources. Only KubeVela keeps the Source-of-Truth for the
// resource. // resource.
ApplyResourceByUpdate featuregate.Feature = "ApplyResourceByUpdate" ApplyResourceByReplace featuregate.Feature = "ApplyResourceByReplace"
// Edge Features // Edge Features
@@ -123,7 +123,7 @@ var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
LegacyComponentRevision: {Default: false, PreRelease: featuregate.Alpha}, LegacyComponentRevision: {Default: false, PreRelease: featuregate.Alpha},
LegacyResourceOwnerValidation: {Default: false, PreRelease: featuregate.Alpha}, LegacyResourceOwnerValidation: {Default: false, PreRelease: featuregate.Alpha},
DisableReferObjectsFromURL: {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}, AuthenticateApplication: {Default: false, PreRelease: featuregate.Alpha},
GzipResourceTracker: {Default: false, PreRelease: featuregate.Alpha}, GzipResourceTracker: {Default: false, PreRelease: featuregate.Alpha},
ZstdResourceTracker: {Default: false, PreRelease: featuregate.Alpha}, ZstdResourceTracker: {Default: false, PreRelease: featuregate.Alpha},

View File

@@ -158,6 +158,9 @@ func (h *resourceKeeper) dispatch(ctx context.Context, manifests []*unstructured
if h.canTakeOver(manifest) { if h.canTakeOver(manifest) {
ao = append([]apply.ApplyOption{apply.TakeOver()}, ao...) 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) manifest, err := ApplyStrategies(applyCtx, h, manifest, v1alpha1.ApplyOnceStrategyOnAppUpdate)
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to apply once policy for application %s,%s", h.app.Name, err.Error()) return errors.Wrapf(err, "failed to apply once policy for application %s,%s", h.app.Name, err.Error())

View File

@@ -63,6 +63,7 @@ type resourceKeeper struct {
sharedResourcePolicy *v1alpha1.SharedResourcePolicySpec sharedResourcePolicy *v1alpha1.SharedResourcePolicySpec
takeOverPolicy *v1alpha1.TakeOverPolicySpec takeOverPolicy *v1alpha1.TakeOverPolicySpec
readOnlyPolicy *v1alpha1.ReadOnlyPolicySpec readOnlyPolicy *v1alpha1.ReadOnlyPolicySpec
resourceUpdatePolicy *v1alpha1.ResourceUpdatePolicySpec
cache *resourceCache cache *resourceCache
} }
@@ -113,6 +114,9 @@ func (h *resourceKeeper) parseApplicationResourcePolicy() (err error) {
if h.readOnlyPolicy, err = policy.ParsePolicy[v1alpha1.ReadOnlyPolicySpec](h.app); err != nil { if h.readOnlyPolicy, err = policy.ParsePolicy[v1alpha1.ReadOnlyPolicySpec](h.app); err != nil {
return errors.Wrapf(err, "failed to parse read-only policy") 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 return nil
} }

View File

@@ -90,6 +90,9 @@ func (h *resourceKeeper) StateKeep(ctx context.Context) error {
if h.canTakeOver(manifest) { if h.canTakeOver(manifest) {
ao = append([]apply.ApplyOption{apply.TakeOver()}, ao...) 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 { 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) return errors.Wrapf(err, "failed to re-apply resource %s from resourcetracker %s", mr.ResourceKey(), rt.Name)
} }

View File

@@ -20,6 +20,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/strings/slices" "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/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/oam"
"github.com/oam-dev/kubevela/pkg/utils" "github.com/oam-dev/kubevela/pkg/utils"
@@ -55,6 +56,13 @@ func (h *resourceKeeper) isReadOnly(manifest *unstructured.Unstructured) bool {
return h.readOnlyPolicy.FindStrategy(manifest) 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 // hasOrphanFinalizer checks if the target application should orphan child resources
func hasOrphanFinalizer(app *v1beta1.Application) bool { func hasOrphanFinalizer(app *v1beta1.Application) bool {
return slices.Contains(app.GetFinalizers(), oam.FinalizerOrphanResource) return slices.Contains(app.GetFinalizers(), oam.FinalizerOrphanResource)

View File

@@ -20,19 +20,23 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
"github.com/pkg/errors" "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors" kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature" utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil" "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/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/controller/utils" "github.com/oam-dev/kubevela/pkg/controller/utils"
"github.com/oam-dev/kubevela/pkg/features" "github.com/oam-dev/kubevela/pkg/features"
@@ -63,6 +67,7 @@ type applyAction struct {
updateAnnotation bool updateAnnotation bool
dryRun bool dryRun bool
quiet bool quiet bool
updateStrategy v1alpha1.ResourceUpdateStrategy
} }
// ApplyOption is called before applying state to the object. // ApplyOption is called before applying state to the object.
@@ -153,6 +158,29 @@ func trimLastAppliedConfigurationForSpecialResources(desired client.Object) bool
return true 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 // 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 { func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...ApplyOption) error {
_, err := generateRenderHash(desired) _, err := generateRenderHash(desired)
@@ -178,15 +206,43 @@ func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...
return nil return nil
} }
switch { strategy := applyAct.updateStrategy
case utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByUpdate) && isUpdatableResource(desired): if strategy.Op == "" {
loggingApply("updating object", desired, applyAct.quiet) 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()) desired.SetResourceVersion(existing.GetResourceVersion())
var options []client.UpdateOption var options []client.UpdateOption
if applyAct.dryRun { if applyAct.dryRun {
options = append(options, client.DryRunAll) options = append(options, client.DryRunAll)
} }
return errors.Wrapf(a.c.Update(ctx, desired, options...), "cannot update object") return errors.Wrapf(a.c.Update(ctx, desired, options...), "cannot update object")
case v1alpha1.ResourceUpdateStrategyPatch:
fallthrough
default: default:
loggingApply("patching object", desired, applyAct.quiet) loggingApply("patching object", desired, applyAct.quiet)
patch, err := a.patcher.patch(existing, desired, applyAct) 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 // MustBeControllableBy requires that the new object is controllable by an
// object with the supplied UID. An object is controllable if its controller // object with the supplied UID. An object is controllable if its controller
// reference includes the supplied UID. // reference includes the supplied UID.

View File

@@ -185,7 +185,7 @@ var _ = Describe("Test apply", func() {
Expect(rawClient.Update(ctx, modifiedDeploy)).Should(Succeed()) Expect(rawClient.Update(ctx, modifiedDeploy)).Should(Succeed())
By("Test patch") 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()) Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed())
copy1 := originalDeploy.DeepCopy() copy1 := originalDeploy.DeepCopy()
copy1.SetResourceVersion(deploy.ResourceVersion) copy1.SetResourceVersion(deploy.ResourceVersion)
@@ -194,7 +194,7 @@ var _ = Describe("Test apply", func() {
Expect(len(deploy.Spec.Template.Spec.Containers)).Should(Equal(2)) Expect(len(deploy.Spec.Template.Spec.Containers)).Should(Equal(2))
By("Test update") 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()) Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed())
copy2 := originalDeploy.DeepCopy() copy2 := originalDeploy.DeepCopy()
copy2.SetResourceVersion(deploy.ResourceVersion) copy2.SetResourceVersion(deploy.ResourceVersion)
@@ -202,7 +202,7 @@ var _ = Describe("Test apply", func() {
Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed()) Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed())
Expect(len(deploy.Spec.Template.Spec.Containers)).Should(Equal(1)) 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())
}) })
}) })
}) })

View 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*.

View File

@@ -1166,5 +1166,55 @@ var _ = Describe("Test multicluster scenario", func() {
g.Expect(len(_revs.Items)).Should(Equal(1)) g.Expect(len(_revs.Items)).Should(Equal(1))
}).WithPolling(2 * time.Second).WithTimeout(20 * time.Second).Should(Succeed()) }).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())
})
}) })
}) })

View 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

View File

@@ -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]
}
}