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 }