istio.io/istio@v0.0.0-20240520182934-d79c90f27776/operator/pkg/patch/patch.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  /*
    16  Package patch implements a simple patching mechanism for k8s resources.
    17  Paths are specified in the form a.b.c.[key:value].d.[list_entry_value], where:
    18    - [key:value] selects a list entry in list c which contains an entry with key:value
    19    - [list_entry_value] selects a list entry in list d which is a regex match of list_entry_value.
    20  
    21  Some examples are given below. Given a resource:
    22  
    23  	kind: Deployment
    24  	metadata:
    25  	  name: istio-citadel
    26  	  namespace: istio-system
    27  	a:
    28  	  b:
    29  	  - name: n1
    30  	    value: v1
    31  	  - name: n2
    32  	    list:
    33  	    - "vv1"
    34  	    - vv2=foo
    35  
    36  values and list entries can be added, modified or deleted.
    37  
    38  # MODIFY
    39  
    40  1. set v1 to v1new
    41  
    42  	path: a.b.[name:n1].value
    43  	value: v1new
    44  
    45  2. set vv1 to vv3
    46  
    47  	// Note the lack of quotes around vv1 (see NOTES below).
    48  	path: a.b.[name:n2].list.[vv1]
    49  	value: vv3
    50  
    51  3. set vv2=foo to vv2=bar (using regex match)
    52  
    53  	path: a.b.[name:n2].list.[vv2]
    54  	value: vv2=bar
    55  
    56  4. replace a port whose port was 15010
    57  
    58    - path: spec.ports.[port:15010]
    59      value:
    60      port: 15020
    61      name: grpc-xds
    62      protocol: TCP
    63  
    64  # DELETE
    65  
    66  1. Delete container with name: n1
    67  
    68  	path: a.b.[name:n1]
    69  
    70  2. Delete list value vv1
    71  
    72  	path: a.b.[name:n2].list.[vv1]
    73  
    74  # ADD
    75  
    76  1. Add vv3 to list
    77  
    78  	path: a.b.[name:n2].list.[1000]
    79  	value: vv3
    80  
    81  Note: the value 1000 is an example. That value used in the patch should
    82  be a value greater than number of the items in the list. Choose 1000 is
    83  just an example which normally is greater than the most of the lists used.
    84  
    85  2. Add new key:value to container name: n1
    86  
    87  	path: a.b.[name:n1]
    88  	value:
    89  	  new_attr: v3
    90  
    91  *NOTES*
    92  - Due to loss of string quoting during unmarshaling, keys and values should not be string quoted, even if they appear
    93  that way in the object being patched.
    94  - [key:value] treats ':' as a special separator character. Any ':' in the key or value string must be escaped as \:.
    95  */
    96  package patch
    97  
    98  import (
    99  	"fmt"
   100  	"strings"
   101  
   102  	yaml2 "gopkg.in/yaml.v2"
   103  
   104  	"istio.io/api/operator/v1alpha1"
   105  	"istio.io/istio/operator/pkg/helm"
   106  	"istio.io/istio/operator/pkg/metrics"
   107  	"istio.io/istio/operator/pkg/object"
   108  	"istio.io/istio/operator/pkg/tpath"
   109  	"istio.io/istio/operator/pkg/util"
   110  	"istio.io/istio/pkg/log"
   111  )
   112  
   113  var scope = log.RegisterScope("patch", "patch")
   114  
   115  // overlayMatches reports whether obj matches the overlay for either the default namespace or no namespace (cluster scope).
   116  func overlayMatches(overlay *v1alpha1.K8SObjectOverlay, obj *object.K8sObject, defaultNamespace string) bool {
   117  	oh := obj.Hash()
   118  	if oh == object.Hash(overlay.Kind, defaultNamespace, overlay.Name) ||
   119  		oh == object.Hash(overlay.Kind, "", overlay.Name) {
   120  		return true
   121  	}
   122  	return false
   123  }
   124  
   125  // YAMLManifestPatch patches a base YAML in the given namespace with a list of overlays.
   126  // Each overlay has the format described in the K8SObjectOverlay definition.
   127  // It returns the patched manifest YAML.
   128  func YAMLManifestPatch(baseYAML string, defaultNamespace string, overlays []*v1alpha1.K8SObjectOverlay) (string, error) {
   129  	var ret strings.Builder
   130  	var errs util.Errors
   131  	objs, err := object.ParseK8sObjectsFromYAMLManifest(baseYAML)
   132  	if err != nil {
   133  		return "", err
   134  	}
   135  
   136  	matches := make(map[*v1alpha1.K8SObjectOverlay]object.K8sObjects)
   137  	// Try to apply the defined overlays.
   138  	for _, obj := range objs {
   139  		oy, err := obj.YAML()
   140  		if err != nil {
   141  			errs = util.AppendErr(errs, fmt.Errorf("object to YAML error (%s) for base object: \n%s", err, obj.YAMLDebugString()))
   142  			continue
   143  		}
   144  		oys := string(oy)
   145  		for _, overlay := range overlays {
   146  			if overlayMatches(overlay, obj, defaultNamespace) {
   147  				matches[overlay] = append(matches[overlay], obj)
   148  				var errs2 util.Errors
   149  				oys, errs2 = applyPatches(obj, overlay.Patches)
   150  				errs = util.AppendErrs(errs, errs2)
   151  			}
   152  		}
   153  		if _, err := ret.WriteString(oys + helm.YAMLSeparator); err != nil {
   154  			errs = util.AppendErr(errs, fmt.Errorf("writeString: %s", err))
   155  		}
   156  	}
   157  
   158  	for _, overlay := range overlays {
   159  		// Each overlay should have exactly one match in the output manifest.
   160  		switch {
   161  		case len(matches[overlay]) == 0:
   162  			errs = util.AppendErr(errs, fmt.Errorf("overlay for %s:%s does not match any object in output manifest. Available objects are:\n%s",
   163  				overlay.Kind, overlay.Name, strings.Join(objs.Keys(), "\n")))
   164  		case len(matches[overlay]) > 1:
   165  			errs = util.AppendErr(errs, fmt.Errorf("overlay for %s:%s matches multiple objects in output manifest:\n%s",
   166  				overlay.Kind, overlay.Name, strings.Join(objs.Keys(), "\n")))
   167  		}
   168  	}
   169  
   170  	return ret.String(), errs.ToError()
   171  }
   172  
   173  // applyPatches applies the given patches against the given object. It returns the resulting patched YAML if successful,
   174  // or a list of errors otherwise.
   175  func applyPatches(base *object.K8sObject, patches []*v1alpha1.K8SObjectOverlay_PathValue) (outYAML string, errs util.Errors) {
   176  	bo := make(map[any]any)
   177  	by, err := base.YAML()
   178  	if err != nil {
   179  		return "", util.NewErrs(err)
   180  	}
   181  	// Use yaml2 specifically to allow interface{} as key which WritePathContext treats specially
   182  	err = yaml2.Unmarshal(by, &bo)
   183  	if err != nil {
   184  		return "", util.NewErrs(err)
   185  	}
   186  	for _, p := range patches {
   187  		v := p.Value.AsInterface()
   188  		if strings.TrimSpace(p.Path) == "" {
   189  			scope.Warnf("value=%s has empty path, skip\n", v)
   190  			continue
   191  		}
   192  		scope.Debugf("applying path=%s, value=%s\n", p.Path, v)
   193  		inc, _, err := tpath.GetPathContext(bo, util.PathFromString(p.Path), true)
   194  		if err != nil {
   195  			errs = util.AppendErr(errs, err)
   196  			metrics.ManifestPatchErrorTotal.Increment()
   197  			continue
   198  		}
   199  
   200  		err = tpath.WritePathContext(inc, v, false)
   201  		if err != nil {
   202  			errs = util.AppendErr(errs, err)
   203  			metrics.ManifestPatchErrorTotal.Increment()
   204  		}
   205  	}
   206  	oy, err := yaml2.Marshal(bo)
   207  	if err != nil {
   208  		return "", util.AppendErr(errs, err)
   209  	}
   210  	return string(oy), errs
   211  }