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
|
// +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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
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
|
// +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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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");
|
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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
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))
|
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())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
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