github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/cli/patch/patch.go (about)

     1  /*
     2  Copyright (C) 2022-2023 ApeCloud Co., Ltd
     3  
     4  This file is part of KubeBlocks project
     5  
     6  This program is free software: you can redistribute it and/or modify
     7  it under the terms of the GNU Affero General Public License as published by
     8  the Free Software Foundation, either version 3 of the License, or
     9  (at your option) any later version.
    10  
    11  This program is distributed in the hope that it will be useful
    12  but WITHOUT ANY WARRANTY; without even the implied warranty of
    13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    14  GNU Affero General Public License for more details.
    15  
    16  You should have received a copy of the GNU Affero General Public License
    17  along with this program.  If not, see <http://www.gnu.org/licenses/>.
    18  */
    19  
    20  package patch
    21  
    22  import (
    23  	"fmt"
    24  	"reflect"
    25  	"strings"
    26  
    27  	jsonpatch "github.com/evanphx/json-patch"
    28  	"github.com/pkg/errors"
    29  	"github.com/spf13/cobra"
    30  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    31  	"k8s.io/apimachinery/pkg/api/meta"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    34  	"k8s.io/apimachinery/pkg/runtime"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/apimachinery/pkg/types"
    37  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    38  	"k8s.io/apimachinery/pkg/util/yaml"
    39  	"k8s.io/cli-runtime/pkg/genericclioptions"
    40  	"k8s.io/cli-runtime/pkg/genericiooptions"
    41  	"k8s.io/cli-runtime/pkg/printers"
    42  	"k8s.io/cli-runtime/pkg/resource"
    43  	"k8s.io/client-go/kubernetes/scheme"
    44  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    45  
    46  	"github.com/1aal/kubeblocks/pkg/cli/edit"
    47  	"github.com/1aal/kubeblocks/pkg/cli/util"
    48  )
    49  
    50  type OutputOperation func(bool) string
    51  
    52  type Options struct {
    53  	Factory cmdutil.Factory
    54  
    55  	// resource names
    56  	Names           []string
    57  	GVR             schema.GroupVersionResource
    58  	OutputOperation OutputOperation
    59  
    60  	// following fields are similar to kubectl patch
    61  	PrintFlags  *genericclioptions.PrintFlags
    62  	ToPrinter   func(string) (printers.ResourcePrinter, error)
    63  	Patch       string
    64  	Subresource string
    65  
    66  	namespace                    string
    67  	enforceNamespace             bool
    68  	dryRunStrategy               cmdutil.DryRunStrategy
    69  	args                         []string
    70  	builder                      *resource.Builder
    71  	unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error)
    72  	fieldManager                 string
    73  
    74  	EditBeforeUpdate bool
    75  
    76  	genericiooptions.IOStreams
    77  }
    78  
    79  func NewOptions(f cmdutil.Factory, streams genericiooptions.IOStreams, gvr schema.GroupVersionResource) *Options {
    80  	return &Options{
    81  		Factory:         f,
    82  		GVR:             gvr,
    83  		PrintFlags:      genericclioptions.NewPrintFlags("").WithTypeSetter(scheme.Scheme),
    84  		IOStreams:       streams,
    85  		OutputOperation: patchOperation,
    86  	}
    87  }
    88  
    89  func (o *Options) AddFlags(cmd *cobra.Command) {
    90  	o.PrintFlags.AddFlags(cmd)
    91  	cmdutil.AddDryRunFlag(cmd)
    92  	cmd.Flags().BoolVar(&o.EditBeforeUpdate, "edit", o.EditBeforeUpdate, "Edit the API resource")
    93  }
    94  
    95  func (o *Options) complete(cmd *cobra.Command) error {
    96  	var err error
    97  	if len(o.Names) == 0 {
    98  		return fmt.Errorf("missing %s name", o.GVR.Resource)
    99  	}
   100  
   101  	o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
   102  	if err != nil {
   103  		return err
   104  	}
   105  
   106  	cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.dryRunStrategy)
   107  	o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) {
   108  		o.PrintFlags.NamePrintFlags.Operation = operation
   109  		return o.PrintFlags.ToPrinter()
   110  	}
   111  
   112  	o.namespace, o.enforceNamespace, err = o.Factory.ToRawKubeConfigLoader().Namespace()
   113  	if err != nil {
   114  		return err
   115  	}
   116  	o.args = append([]string{util.GVRToString(o.GVR)}, o.Names...)
   117  	o.builder = o.Factory.NewBuilder()
   118  	o.unstructuredClientForMapping = o.Factory.UnstructuredClientForMapping
   119  	return nil
   120  }
   121  
   122  func (o *Options) Run(cmd *cobra.Command) error {
   123  	if err := o.complete(cmd); err != nil {
   124  		return err
   125  	}
   126  
   127  	if len(o.Patch) == 0 {
   128  		return fmt.Errorf("the contents of the patch is empty")
   129  	}
   130  
   131  	// for CRD, we always use Merge patch type
   132  	patchType := types.MergePatchType
   133  	patchBytes, err := yaml.ToJSON([]byte(o.Patch))
   134  	if err != nil {
   135  		return fmt.Errorf("unable to parse %q: %v", o.Patch, err)
   136  	}
   137  
   138  	r := o.builder.
   139  		Unstructured().
   140  		ContinueOnError().
   141  		NamespaceParam(o.namespace).DefaultNamespace().
   142  		Subresource(o.Subresource).
   143  		ResourceTypeOrNameArgs(false, o.args...).
   144  		Flatten().
   145  		Do()
   146  	err = r.Err()
   147  	if err != nil {
   148  		return err
   149  	}
   150  	count := 0
   151  	err = r.Visit(func(info *resource.Info, err error) error {
   152  		if err != nil {
   153  			return err
   154  		}
   155  		count++
   156  		name, namespace := info.Name, info.Namespace
   157  		if o.dryRunStrategy != cmdutil.DryRunClient {
   158  			mapping := info.ResourceMapping()
   159  			client, err := o.unstructuredClientForMapping(mapping)
   160  			if err != nil {
   161  				return err
   162  			}
   163  
   164  			helper := resource.
   165  				NewHelper(client, mapping).
   166  				DryRun(o.dryRunStrategy == cmdutil.DryRunServer).
   167  				WithFieldManager(o.fieldManager).
   168  				WithSubresource(o.Subresource).
   169  				WithFieldValidation(metav1.FieldValidationStrict)
   170  			patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil)
   171  			if err != nil {
   172  				if apierrors.IsUnsupportedMediaType(err) {
   173  					return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind))
   174  				}
   175  				return fmt.Errorf("unable to update %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err)
   176  			}
   177  
   178  			if o.EditBeforeUpdate {
   179  				customEdit := edit.NewCustomEditOptions(o.Factory, o.IOStreams, "patched")
   180  				if err = customEdit.Run(patchedObj); err != nil {
   181  					return fmt.Errorf("unable to edit %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err)
   182  				}
   183  				patchedObj = &unstructured.Unstructured{
   184  					Object: map[string]interface{}{
   185  						"metadata": patchedObj.(*unstructured.Unstructured).Object["metadata"],
   186  						"spec":     patchedObj.(*unstructured.Unstructured).Object["spec"],
   187  					},
   188  				}
   189  				patchBytes, err = patchedObj.(*unstructured.Unstructured).MarshalJSON()
   190  				if err != nil {
   191  					return fmt.Errorf("unable to marshal %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err)
   192  				}
   193  				patchedObj, err = helper.Patch(namespace, name, patchType, patchBytes, nil)
   194  				if err != nil {
   195  					if apierrors.IsUnsupportedMediaType(err) {
   196  						return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind))
   197  					}
   198  					return fmt.Errorf("unable to update %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err)
   199  				}
   200  			}
   201  
   202  			didPatch := !reflect.DeepEqual(info.Object, patchedObj)
   203  			printer, err := o.ToPrinter(o.OutputOperation(didPatch))
   204  			if err != nil {
   205  				return err
   206  			}
   207  			return printer.PrintObj(patchedObj, o.Out)
   208  		}
   209  
   210  		originalObjJS, err := runtime.Encode(unstructured.UnstructuredJSONScheme, info.Object)
   211  		if err != nil {
   212  			return err
   213  		}
   214  
   215  		originalPatchedObjJS, err := getPatchedJSON(patchType, originalObjJS, patchBytes, info.Object.GetObjectKind().GroupVersionKind(), scheme.Scheme)
   216  		if err != nil {
   217  			return err
   218  		}
   219  
   220  		targetObj, err := runtime.Decode(unstructured.UnstructuredJSONScheme, originalPatchedObjJS)
   221  		if err != nil {
   222  			return err
   223  		}
   224  
   225  		didPatch := !reflect.DeepEqual(info.Object, targetObj)
   226  		printer, err := o.ToPrinter(o.OutputOperation(didPatch))
   227  		if err != nil {
   228  			return err
   229  		}
   230  		return printer.PrintObj(targetObj, o.Out)
   231  	})
   232  	if err != nil {
   233  		return err
   234  	}
   235  	if count == 0 {
   236  		return fmt.Errorf("no objects passed to patch")
   237  	}
   238  	return nil
   239  }
   240  
   241  func getPatchedJSON(patchType types.PatchType, originalJS, patchJS []byte, gvk schema.GroupVersionKind, oc runtime.ObjectCreater) ([]byte, error) {
   242  	switch patchType {
   243  	case types.JSONPatchType:
   244  		patchObj, err := jsonpatch.DecodePatch(patchJS)
   245  		if err != nil {
   246  			return nil, err
   247  		}
   248  		bytes, err := patchObj.Apply(originalJS)
   249  		// TODO: This is pretty hacky, we need a better structured error from the json-patch
   250  		if err != nil && strings.Contains(err.Error(), "doc is missing key") {
   251  			msg := err.Error()
   252  			ix := strings.Index(msg, "key:")
   253  			key := msg[ix+5:]
   254  			return bytes, fmt.Errorf("object to be patched is missing field (%s)", key)
   255  		}
   256  		return bytes, err
   257  
   258  	case types.MergePatchType:
   259  		return jsonpatch.MergePatch(originalJS, patchJS)
   260  
   261  	case types.StrategicMergePatchType:
   262  		// get a typed object for this GVK if we need to apply a strategic merge patch
   263  		obj, err := oc.New(gvk)
   264  		if err != nil {
   265  			return nil, fmt.Errorf("strategic merge patch is not supported for %s locally, try --type merge", gvk.String())
   266  		}
   267  		return strategicpatch.StrategicMergePatch(originalJS, patchJS, obj)
   268  
   269  	default:
   270  		// only here as a safety net - go-restful filters content-type
   271  		return nil, fmt.Errorf("unknown Content-Type header for patch: %v", patchType)
   272  	}
   273  }
   274  
   275  func patchOperation(didPatch bool) string {
   276  	if didPatch {
   277  		return "patched"
   278  	}
   279  	return "patched (no change)"
   280  }