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 }