github.com/oam-dev/kubevela@v1.9.11/pkg/utils/apply/apply.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package apply
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"reflect"
    24  
    25  	"github.com/crossplane/crossplane-runtime/pkg/fieldpath"
    26  	"github.com/pkg/errors"
    27  	corev1 "k8s.io/api/core/v1"
    28  	v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
    29  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/types"
    34  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    35  	"k8s.io/klog/v2"
    36  	"sigs.k8s.io/controller-runtime/pkg/client"
    37  	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
    38  
    39  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
    40  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
    41  	"github.com/oam-dev/kubevela/pkg/controller/utils"
    42  	"github.com/oam-dev/kubevela/pkg/features"
    43  	"github.com/oam-dev/kubevela/pkg/oam"
    44  	"github.com/oam-dev/kubevela/pkg/oam/util"
    45  	"github.com/oam-dev/kubevela/pkg/utils/common"
    46  )
    47  
    48  const (
    49  	// LabelRenderHash is the label that record the hash value of the rendering resource.
    50  	LabelRenderHash = "oam.dev/render-hash"
    51  )
    52  
    53  // Applicator applies new state to an object or create it if not exist.
    54  // It uses the same mechanism as `kubectl apply`, that is, for each resource being applied,
    55  // computing a three-way diff merge in client side based on its current state, modified stated,
    56  // and last-applied-state which is tracked through an specific annotation.
    57  // If the resource doesn't exist before, Apply will create it.
    58  type Applicator interface {
    59  	Apply(context.Context, client.Object, ...ApplyOption) error
    60  }
    61  
    62  type applyAction struct {
    63  	takeOver         bool
    64  	readOnly         bool
    65  	isShared         bool
    66  	skipUpdate       bool
    67  	updateAnnotation bool
    68  	dryRun           bool
    69  	quiet            bool
    70  	updateStrategy   v1alpha1.ResourceUpdateStrategy
    71  }
    72  
    73  // ApplyOption is called before applying state to the object.
    74  // ApplyOption is still called even if the object does NOT exist.
    75  // If the object does not exist, `existing` will be assigned as `nil`.
    76  // nolint
    77  type ApplyOption func(act *applyAction, existing, desired client.Object) error
    78  
    79  // NewAPIApplicator creates an Applicator that applies state to an
    80  // object or creates the object if not exist.
    81  func NewAPIApplicator(c client.Client) *APIApplicator {
    82  	return &APIApplicator{
    83  		creator: creatorFn(createOrGetExisting),
    84  		patcher: patcherFn(threeWayMergePatch),
    85  		c:       c,
    86  	}
    87  }
    88  
    89  type creator interface {
    90  	createOrGetExisting(context.Context, *applyAction, client.Client, client.Object, ...ApplyOption) (client.Object, error)
    91  }
    92  
    93  type creatorFn func(context.Context, *applyAction, client.Client, client.Object, ...ApplyOption) (client.Object, error)
    94  
    95  func (fn creatorFn) createOrGetExisting(ctx context.Context, act *applyAction, c client.Client, o client.Object, ao ...ApplyOption) (client.Object, error) {
    96  	return fn(ctx, act, c, o, ao...)
    97  }
    98  
    99  type patcher interface {
   100  	patch(c, m client.Object, a *applyAction) (client.Patch, error)
   101  }
   102  
   103  type patcherFn func(c, m client.Object, a *applyAction) (client.Patch, error)
   104  
   105  func (fn patcherFn) patch(c, m client.Object, a *applyAction) (client.Patch, error) {
   106  	return fn(c, m, a)
   107  }
   108  
   109  // APIApplicator implements Applicator
   110  type APIApplicator struct {
   111  	creator
   112  	patcher
   113  	c client.Client
   114  }
   115  
   116  // loggingApply will record a log with desired object applied
   117  func loggingApply(msg string, desired client.Object, quiet bool) {
   118  	if quiet {
   119  		return
   120  	}
   121  	d, ok := desired.(metav1.Object)
   122  	if !ok {
   123  		klog.InfoS(msg, "resource", desired.GetObjectKind().GroupVersionKind().String())
   124  		return
   125  	}
   126  	klog.InfoS(msg, "name", d.GetName(), "resource", desired.GetObjectKind().GroupVersionKind().String())
   127  }
   128  
   129  // trimLastAppliedConfigurationForSpecialResources will filter special object that can reduce the record for "app.oam.dev/last-applied-configuration" annotation.
   130  func trimLastAppliedConfigurationForSpecialResources(desired client.Object) bool {
   131  	if desired == nil {
   132  		return false
   133  	}
   134  	gvk := desired.GetObjectKind().GroupVersionKind()
   135  	gp, kd := gvk.Group, gvk.Kind
   136  	if gp == "" {
   137  		// group is empty means it's Kubernetes core API, we won't record annotation for Secret and Configmap
   138  		if kd == "Secret" || kd == "ConfigMap" || kd == "CustomResourceDefinition" {
   139  			return false
   140  		}
   141  		if _, ok := desired.(*corev1.ConfigMap); ok {
   142  			return false
   143  		}
   144  		if _, ok := desired.(*corev1.Secret); ok {
   145  			return false
   146  		}
   147  		if _, ok := desired.(*v1.CustomResourceDefinition); ok {
   148  			return false
   149  		}
   150  	}
   151  	ann := desired.GetAnnotations()
   152  	if ann != nil {
   153  		lac := ann[oam.AnnotationLastAppliedConfig]
   154  		if lac == "-" || lac == "skip" {
   155  			return false
   156  		}
   157  	}
   158  	return true
   159  }
   160  
   161  func needRecreate(recreateFields []string, existing, desired client.Object) (bool, error) {
   162  	if len(recreateFields) == 0 {
   163  		return false, nil
   164  	}
   165  	_existing, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(existing)
   166  	_desired, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(desired)
   167  	flag := false
   168  	for _, field := range recreateFields {
   169  		ve, err := fieldpath.Pave(_existing).GetValue(field)
   170  		if err != nil {
   171  			return false, fmt.Errorf("unable to get path %s from existing object: %w", field, err)
   172  		}
   173  		vd, err := fieldpath.Pave(_desired).GetValue(field)
   174  		if err != nil {
   175  			return false, fmt.Errorf("unable to get path %s from desired object: %w", field, err)
   176  		}
   177  		if !reflect.DeepEqual(ve, vd) {
   178  			flag = true
   179  		}
   180  	}
   181  	return flag, nil
   182  }
   183  
   184  // Apply applies new state to an object or create it if not exist
   185  func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...ApplyOption) error {
   186  	_, err := generateRenderHash(desired)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	applyAct := &applyAction{updateAnnotation: trimLastAppliedConfigurationForSpecialResources(desired)}
   191  	existing, err := a.createOrGetExisting(ctx, applyAct, a.c, desired, ao...)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	if existing == nil {
   196  		return nil
   197  	}
   198  
   199  	// the object already exists, apply new state
   200  	if err := executeApplyOptions(applyAct, existing, desired, ao); err != nil {
   201  		return err
   202  	}
   203  
   204  	if applyAct.skipUpdate {
   205  		loggingApply("skip update", desired, applyAct.quiet)
   206  		return nil
   207  	}
   208  
   209  	strategy := applyAct.updateStrategy
   210  	if strategy.Op == "" {
   211  		if utilfeature.DefaultMutableFeatureGate.Enabled(features.ApplyResourceByReplace) && isUpdatableResource(desired) {
   212  			strategy.Op = v1alpha1.ResourceUpdateStrategyReplace
   213  		} else {
   214  			strategy.Op = v1alpha1.ResourceUpdateStrategyPatch
   215  		}
   216  	}
   217  
   218  	shouldRecreate, err := needRecreate(strategy.RecreateFields, existing, desired)
   219  	if err != nil {
   220  		return fmt.Errorf("failed to evaluate recreateFields: %w", err)
   221  	}
   222  	if shouldRecreate {
   223  		loggingApply("recreating object", desired, applyAct.quiet)
   224  		if applyAct.dryRun { // recreate does not support dryrun
   225  			return nil
   226  		}
   227  		if existing.GetDeletionTimestamp() == nil { // check if recreation needed
   228  			if err = a.c.Delete(ctx, existing); err != nil {
   229  				return errors.Wrap(err, "cannot delete object")
   230  			}
   231  		}
   232  		return errors.Wrap(a.c.Create(ctx, desired), "cannot recreate object")
   233  	}
   234  
   235  	switch strategy.Op {
   236  	case v1alpha1.ResourceUpdateStrategyReplace:
   237  		loggingApply("replacing object", desired, applyAct.quiet)
   238  		desired.SetResourceVersion(existing.GetResourceVersion())
   239  		var options []client.UpdateOption
   240  		if applyAct.dryRun {
   241  			options = append(options, client.DryRunAll)
   242  		}
   243  		return errors.Wrapf(a.c.Update(ctx, desired, options...), "cannot update object")
   244  	case v1alpha1.ResourceUpdateStrategyPatch:
   245  		fallthrough
   246  	default:
   247  		loggingApply("patching object", desired, applyAct.quiet)
   248  		patch, err := a.patcher.patch(existing, desired, applyAct)
   249  		if err != nil {
   250  			return errors.Wrap(err, "cannot calculate patch by computing a three way diff")
   251  		}
   252  		if isEmptyPatch(patch) {
   253  			return nil
   254  		}
   255  		if applyAct.dryRun {
   256  			return errors.Wrapf(a.c.Patch(ctx, desired, patch, client.DryRunAll), "cannot patch object")
   257  		}
   258  		return errors.Wrapf(a.c.Patch(ctx, desired, patch), "cannot patch object")
   259  	}
   260  }
   261  
   262  func generateRenderHash(desired client.Object) (string, error) {
   263  	if desired == nil {
   264  		return "", nil
   265  	}
   266  	desiredHash, err := utils.ComputeSpecHash(desired)
   267  	if err != nil {
   268  		return "", errors.Wrap(err, "compute desired hash")
   269  	}
   270  	util.AddLabels(desired, map[string]string{
   271  		LabelRenderHash: desiredHash,
   272  	})
   273  	return desiredHash, nil
   274  }
   275  
   276  func getRenderHash(existing client.Object) string {
   277  	labels := existing.GetLabels()
   278  	if labels == nil {
   279  		return ""
   280  	}
   281  	return labels[LabelRenderHash]
   282  }
   283  
   284  // createOrGetExisting will create the object if it does not exist
   285  // or get and return the existing object
   286  func createOrGetExisting(ctx context.Context, act *applyAction, c client.Client, desired client.Object, ao ...ApplyOption) (client.Object, error) {
   287  	var create = func() (client.Object, error) {
   288  		// execute ApplyOptions even the object doesn't exist
   289  		if err := executeApplyOptions(act, nil, desired, ao); err != nil {
   290  			return nil, err
   291  		}
   292  		if act.readOnly {
   293  			return nil, fmt.Errorf("%s (%s) is marked as read-only but does not exist. You should check the existence of the resource or remove the read-only policy", desired.GetObjectKind().GroupVersionKind().Kind, desired.GetName())
   294  		}
   295  		if act.updateAnnotation {
   296  			if err := addLastAppliedConfigAnnotation(desired); err != nil {
   297  				return nil, err
   298  			}
   299  		}
   300  		loggingApply("creating object", desired, act.quiet)
   301  		if act.dryRun {
   302  			return nil, errors.Wrap(c.Create(ctx, desired, client.DryRunAll), "cannot create object")
   303  		}
   304  		return nil, errors.Wrap(c.Create(ctx, desired), "cannot create object")
   305  	}
   306  
   307  	if desired.GetObjectKind().GroupVersionKind().Kind == "" {
   308  		gvk, err := apiutil.GVKForObject(desired, common.Scheme)
   309  		if err == nil {
   310  			desired.GetObjectKind().SetGroupVersionKind(gvk)
   311  		}
   312  	}
   313  
   314  	// allow to create object with only generateName
   315  	if desired.GetName() == "" && desired.GetGenerateName() != "" {
   316  		return create()
   317  	}
   318  
   319  	existing := &unstructured.Unstructured{}
   320  	existing.GetObjectKind().SetGroupVersionKind(desired.GetObjectKind().GroupVersionKind())
   321  	err := c.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, existing)
   322  	if kerrors.IsNotFound(err) {
   323  		return create()
   324  	}
   325  	if err != nil {
   326  		return nil, errors.Wrap(err, "cannot get object")
   327  	}
   328  	return existing, nil
   329  }
   330  
   331  func executeApplyOptions(act *applyAction, existing, desired client.Object, aos []ApplyOption) error {
   332  	// if existing is nil, it means the object is going to be created.
   333  	// ApplyOption function should handle this situation carefully by itself.
   334  	for _, fn := range aos {
   335  		if err := fn(act, existing, desired); err != nil {
   336  			return errors.Wrap(err, "cannot apply ApplyOption")
   337  		}
   338  	}
   339  	return nil
   340  }
   341  
   342  // NotUpdateRenderHashEqual if the render hash of new object equal to the old hash, should not apply.
   343  func NotUpdateRenderHashEqual() ApplyOption {
   344  	return func(act *applyAction, existing, desired client.Object) error {
   345  		if existing == nil || desired == nil || act.isShared {
   346  			return nil
   347  		}
   348  		newSt, ok := desired.(*unstructured.Unstructured)
   349  		if !ok {
   350  			return nil
   351  		}
   352  		oldSt, ok := existing.(*unstructured.Unstructured)
   353  		if !ok {
   354  			return nil
   355  		}
   356  		if getRenderHash(existing) == getRenderHash(desired) {
   357  			*newSt = *oldSt
   358  			act.skipUpdate = true
   359  		}
   360  		return nil
   361  	}
   362  }
   363  
   364  // ReadOnly skip apply fo the resource
   365  func ReadOnly() ApplyOption {
   366  	return func(act *applyAction, _, _ client.Object) error {
   367  		act.readOnly = true
   368  		act.skipUpdate = true
   369  		return nil
   370  	}
   371  }
   372  
   373  // TakeOver allow take over resources without app owner
   374  func TakeOver() ApplyOption {
   375  	return func(act *applyAction, _, _ client.Object) error {
   376  		act.takeOver = true
   377  		return nil
   378  	}
   379  }
   380  
   381  // WithUpdateStrategy set the update strategy for the apply operation
   382  func WithUpdateStrategy(strategy v1alpha1.ResourceUpdateStrategy) ApplyOption {
   383  	return func(act *applyAction, _, _ client.Object) error {
   384  		act.updateStrategy = strategy
   385  		return nil
   386  	}
   387  }
   388  
   389  // MustBeControllableBy requires that the new object is controllable by an
   390  // object with the supplied UID. An object is controllable if its controller
   391  // reference includes the supplied UID.
   392  func MustBeControllableBy(u types.UID) ApplyOption {
   393  	return func(_ *applyAction, existing, _ client.Object) error {
   394  		if existing == nil {
   395  			return nil
   396  		}
   397  		c := metav1.GetControllerOf(existing.(metav1.Object))
   398  		if c == nil {
   399  			return nil
   400  		}
   401  		if c.UID != u {
   402  			return errors.Errorf("existing object is not controlled by UID %q", u)
   403  		}
   404  		return nil
   405  	}
   406  }
   407  
   408  // MustBeControlledByApp requires that the new object is controllable by versioned resourcetracker
   409  func MustBeControlledByApp(app *v1beta1.Application) ApplyOption {
   410  	return func(act *applyAction, existing, _ client.Object) error {
   411  		if existing == nil || act.isShared || act.readOnly {
   412  			return nil
   413  		}
   414  		appKey, controlledBy := GetAppKey(app), GetControlledBy(existing)
   415  		// if the existing object has no resource version, it means this resource is an API response not directly from
   416  		// an etcd object but from some external services, such as vela-prism. Then the response does not necessarily
   417  		// contain the owner
   418  		if controlledBy == "" && !utilfeature.DefaultMutableFeatureGate.Enabled(features.LegacyResourceOwnerValidation) && existing.GetResourceVersion() != "" && !act.takeOver {
   419  			return fmt.Errorf("%s %s/%s exists but not managed by any application now", existing.GetObjectKind().GroupVersionKind().Kind, existing.GetNamespace(), existing.GetName())
   420  		}
   421  		if controlledBy != "" && controlledBy != appKey {
   422  			return fmt.Errorf("existing object %s %s/%s is managed by other application %s", existing.GetObjectKind().GroupVersionKind().Kind, existing.GetNamespace(), existing.GetName(), controlledBy)
   423  		}
   424  		return nil
   425  	}
   426  }
   427  
   428  // GetControlledBy extract the application that controls the current resource
   429  func GetControlledBy(existing client.Object) string {
   430  	labels := existing.GetLabels()
   431  	if labels == nil {
   432  		return ""
   433  	}
   434  	appName := labels[oam.LabelAppName]
   435  	appNs := labels[oam.LabelAppNamespace]
   436  	if appName == "" || appNs == "" {
   437  		return ""
   438  	}
   439  	return fmt.Sprintf("%s/%s", appNs, appName)
   440  }
   441  
   442  // GetAppKey construct the key for identifying the application
   443  func GetAppKey(app *v1beta1.Application) string {
   444  	ns := app.Namespace
   445  	if ns == "" {
   446  		ns = metav1.NamespaceDefault
   447  	}
   448  	return fmt.Sprintf("%s/%s", ns, app.GetName())
   449  }
   450  
   451  // MakeCustomApplyOption let user can generate applyOption that restrict change apply action.
   452  func MakeCustomApplyOption(f func(existing, desired client.Object) error) ApplyOption {
   453  	return func(act *applyAction, existing, desired client.Object) error {
   454  		return f(existing, desired)
   455  	}
   456  }
   457  
   458  // DisableUpdateAnnotation disable write last config to annotation
   459  func DisableUpdateAnnotation() ApplyOption {
   460  	return func(a *applyAction, existing, _ client.Object) error {
   461  		a.updateAnnotation = false
   462  		return nil
   463  	}
   464  }
   465  
   466  // SharedByApp let the resource be sharable
   467  func SharedByApp(app *v1beta1.Application) ApplyOption {
   468  	return func(act *applyAction, existing, desired client.Object) error {
   469  		// calculate the shared-by annotation
   470  		// if resource exists, add the current application into the resource shared-by field
   471  		var sharedBy string
   472  		if existing != nil && existing.GetAnnotations() != nil {
   473  			sharedBy = existing.GetAnnotations()[oam.AnnotationAppSharedBy]
   474  		}
   475  		sharedBy = AddSharer(sharedBy, app)
   476  		util.AddAnnotations(desired, map[string]string{oam.AnnotationAppSharedBy: sharedBy})
   477  		if existing == nil {
   478  			return nil
   479  		}
   480  
   481  		// resource exists and controlled by current application
   482  		appKey, controlledBy := GetAppKey(app), GetControlledBy(existing)
   483  		if controlledBy == "" || appKey == controlledBy {
   484  			return nil
   485  		}
   486  
   487  		// resource exists but not controlled by current application
   488  		if existing.GetAnnotations() == nil || existing.GetAnnotations()[oam.AnnotationAppSharedBy] == "" {
   489  			// if the application that controls the resource does not allow sharing, return error
   490  			return fmt.Errorf("application is controlled by %s but is not sharable", controlledBy)
   491  		}
   492  		// the application that controls the resource allows sharing, then only mutate the shared-by annotation
   493  		act.isShared = true
   494  		bs, err := json.Marshal(existing)
   495  		if err != nil {
   496  			return err
   497  		}
   498  		if err = json.Unmarshal(bs, desired); err != nil {
   499  			return err
   500  		}
   501  		util.AddAnnotations(desired, map[string]string{oam.AnnotationAppSharedBy: sharedBy})
   502  		return nil
   503  	}
   504  }
   505  
   506  // DryRunAll executing all validation, etc without persisting the change to storage.
   507  func DryRunAll() ApplyOption {
   508  	return func(a *applyAction, existing, _ client.Object) error {
   509  		a.dryRun = true
   510  		return nil
   511  	}
   512  }
   513  
   514  // Quiet means disable the logger
   515  func Quiet() ApplyOption {
   516  	return func(a *applyAction, existing, _ client.Object) error {
   517  		a.quiet = true
   518  		return nil
   519  	}
   520  }
   521  
   522  // isUpdatableResource check whether the resource is updatable
   523  // Resource like v1.Service cannot unset the spec field (the ip spec is filled by service controller)
   524  func isUpdatableResource(desired client.Object) bool {
   525  	// nolint
   526  	switch desired.GetObjectKind().GroupVersionKind() {
   527  	case corev1.SchemeGroupVersion.WithKind("Service"):
   528  		return false
   529  	}
   530  	return true
   531  }