diff --git a/pkg/features/controller_features.go b/pkg/features/controller_features.go index 95ed23c04..5ffb0c6c8 100644 --- a/pkg/features/controller_features.go +++ b/pkg/features/controller_features.go @@ -43,6 +43,17 @@ const ( // DisableReferObjectsFromURL if set, the url ref objects will be disallowed DisableReferObjectsFromURL featuregate.Feature = "DisableReferObjectsFromURL" + // ApplyResourceByUpdate enforces the modification of resource through update 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 + // potential conflicts. + // If set, KubeVela controller will enforce strong restriction on the managed resource that external + // 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" + // Edge Features // AuthenticateApplication enable the authentication for application @@ -58,6 +69,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}, AuthenticateApplication: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/pkg/utils/apply/apply.go b/pkg/utils/apply/apply.go index 0bc111912..c65262007 100644 --- a/pkg/utils/apply/apply.go +++ b/pkg/utils/apply/apply.go @@ -170,15 +170,26 @@ func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ... return nil } - loggingApply("patching object", desired) - patch, err := a.patcher.patch(existing, desired, applyAct) - if err != nil { - return errors.Wrap(err, "cannot calculate patch by computing a three way diff") + switch { + case utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByUpdate) && isUpdatableResource(desired): + loggingApply("updating object", desired) + 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") + default: + loggingApply("patching object", desired) + patch, err := a.patcher.patch(existing, desired, applyAct) + if err != nil { + return errors.Wrap(err, "cannot calculate patch by computing a three way diff") + } + if applyAct.dryRun { + return errors.Wrapf(a.c.Patch(ctx, desired, patch, client.DryRunAll), "cannot patch object") + } + return errors.Wrapf(a.c.Patch(ctx, desired, patch), "cannot patch object") } - if applyAct.dryRun { - return errors.Wrapf(a.c.Patch(ctx, desired, patch, client.DryRunAll), "cannot patch object") - } - return errors.Wrapf(a.c.Patch(ctx, desired, patch), "cannot patch object") } func generateRenderHash(desired client.Object) (string, error) { @@ -397,3 +408,14 @@ func DryRunAll() ApplyOption { return nil } } + +// isUpdatableResource check whether the resource is updatable +// Resource like v1.Service cannot unset the spec field (the ip spec is filled by service controller) +func isUpdatableResource(desired client.Object) bool { + // nolint + switch desired.GetObjectKind().GroupVersionKind() { + case corev1.SchemeGroupVersion.WithKind("Service"): + return false + } + return true +} diff --git a/pkg/utils/apply/apply_resource_test.go b/pkg/utils/apply/apply_resource_test.go index e127b1103..6d2688fd6 100644 --- a/pkg/utils/apply/apply_resource_test.go +++ b/pkg/utils/apply/apply_resource_test.go @@ -18,6 +18,8 @@ package apply import ( "context" + "encoding/json" + "fmt" "strings" . "github.com/onsi/ginkgo" @@ -25,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + utilfeature "k8s.io/apiserver/pkg/util/feature" "sigs.k8s.io/controller-runtime/pkg/client" appsv1 "k8s.io/api/apps/v1" @@ -34,6 +37,7 @@ import ( "k8s.io/utils/pointer" "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/features" "github.com/oam-dev/kubevela/pkg/oam" oamutil "github.com/oam-dev/kubevela/pkg/oam/util" ) @@ -166,6 +170,40 @@ var _ = Describe("Test apply", func() { })) Expect(rawClient.Delete(ctx, cm2)).Should(Succeed()) }) + + It("Test apply resources with external modifier", func() { + deploy.SetGroupVersionKind(appsv1.SchemeGroupVersion.WithKind("Deployment")) + originalDeploy := deploy.DeepCopy() + bs, err := json.Marshal(deploy) + Expect(err).Should(Succeed()) + deploy.SetAnnotations(map[string]string{oam.AnnotationLastAppliedConfig: string(bs)}) + modifiedDeploy := deploy.DeepCopy() + modifiedDeploy.Spec.Template.Spec.Containers = append(modifiedDeploy.Spec.Template.Spec.Containers, corev1.Container{ + Name: "added-by-external-modifier", + Image: "busybox", + }) + Expect(rawClient.Update(ctx, modifiedDeploy)).Should(Succeed()) + + By("Test patch") + Expect(utilfeature.DefaultMutableFeatureGate.Set(fmt.Sprintf("%s=false", features.ApplyResourceByUpdate))).Should(Succeed()) + Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed()) + copy1 := originalDeploy.DeepCopy() + copy1.SetResourceVersion(deploy.ResourceVersion) + Expect(k8sApplicator.Apply(ctx, copy1)).Should(Succeed()) + Expect(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed()) + 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(rawClient.Get(ctx, client.ObjectKeyFromObject(deploy), deploy)).Should(Succeed()) + copy2 := originalDeploy.DeepCopy() + copy2.SetResourceVersion(deploy.ResourceVersion) + Expect(k8sApplicator.Apply(ctx, copy2)).Should(Succeed()) + 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()) + }) }) })