k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/patches/patches.go (about) 1 /* 2 Copyright 2020 The Kubernetes 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 patches 18 19 import ( 20 "bufio" 21 "bytes" 22 "fmt" 23 "io" 24 "os" 25 "path/filepath" 26 "regexp" 27 "strings" 28 "sync" 29 30 jsonpatch "github.com/evanphx/json-patch" 31 "github.com/pkg/errors" 32 33 "k8s.io/apimachinery/pkg/types" 34 "k8s.io/apimachinery/pkg/util/strategicpatch" 35 utilyaml "k8s.io/apimachinery/pkg/util/yaml" 36 kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" 37 "sigs.k8s.io/yaml" 38 ) 39 40 // PatchTarget defines a target to be patched, such as a control-plane static Pod. 41 type PatchTarget struct { 42 // Name must be the name of a known target. In the case of Kubernetes objects 43 // this is likely to match the ObjectMeta.Name of a target. 44 Name string 45 46 // StrategicMergePatchObject is only used for strategic merge patches. 47 // It represents the underlying object type that is patched - e.g. "v1.Pod" 48 StrategicMergePatchObject interface{} 49 50 // Data must contain the bytes that will be patched. 51 Data []byte 52 } 53 54 // PatchManager defines an object that can apply patches. 55 type PatchManager struct { 56 patchSets []*patchSet 57 knownTargets []string 58 output io.Writer 59 } 60 61 // patchSet defines a set of patches of a certain type that can patch a PatchTarget. 62 type patchSet struct { 63 targetName string 64 patchType types.PatchType 65 patches []string 66 } 67 68 // String() is used for unit-testing. 69 func (ps *patchSet) String() string { 70 return fmt.Sprintf( 71 "{%q, %q, %#v}", 72 ps.targetName, 73 ps.patchType, 74 ps.patches, 75 ) 76 } 77 78 const ( 79 // KubeletConfiguration defines the kubeletconfiguration patch target. 80 KubeletConfiguration = "kubeletconfiguration" 81 // CoreDNSDeployment defines the corednsdeployment patch target. 82 CoreDNSDeployment = "corednsdeployment" 83 ) 84 85 var ( 86 pathLock = &sync.RWMutex{} 87 pathCache = map[string]*PatchManager{} 88 89 patchTypes = map[string]types.PatchType{ 90 "json": types.JSONPatchType, 91 "merge": types.MergePatchType, 92 "strategic": types.StrategicMergePatchType, 93 "": types.StrategicMergePatchType, // Default 94 } 95 patchTypeList = []string{"json", "merge", "strategic"} 96 patchTypesJoined = strings.Join(patchTypeList, "|") 97 knownExtensions = []string{"json", "yaml"} 98 99 regExtension = regexp.MustCompile(`.+\.(` + strings.Join(knownExtensions, "|") + `)$`) 100 101 knownTargets = []string{ 102 kubeadmconstants.Etcd, 103 kubeadmconstants.KubeAPIServer, 104 kubeadmconstants.KubeControllerManager, 105 kubeadmconstants.KubeScheduler, 106 KubeletConfiguration, 107 CoreDNSDeployment, 108 } 109 ) 110 111 // KnownTargets returns the locally defined knownTargets. 112 func KnownTargets() []string { 113 return knownTargets 114 } 115 116 // GetPatchManagerForPath creates a patch manager that can be used to apply patches to "knownTargets". 117 // "path" should contain patches that can be used to patch the "knownTargets". 118 // If "output" is non-nil, messages about actions performed by the manager would go on this io.Writer. 119 func GetPatchManagerForPath(path string, knownTargets []string, output io.Writer) (*PatchManager, error) { 120 pathLock.RLock() 121 if pm, known := pathCache[path]; known { 122 pathLock.RUnlock() 123 return pm, nil 124 } 125 pathLock.RUnlock() 126 127 if output == nil { 128 output = io.Discard 129 } 130 131 fmt.Fprintf(output, "[patches] Reading patches from path %q\n", path) 132 133 // Get the files in the path. 134 patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(path, knownTargets, output) 135 if err != nil { 136 return nil, err 137 } 138 139 if len(patchFiles) > 0 { 140 fmt.Fprintf(output, "[patches] Found the following patch files: %v\n", patchFiles) 141 } 142 if len(ignoredFiles) > 0 { 143 fmt.Fprintf(output, "[patches] Ignored the following files: %v\n", ignoredFiles) 144 } 145 146 pm := &PatchManager{ 147 patchSets: patchSets, 148 knownTargets: knownTargets, 149 output: output, 150 } 151 pathLock.Lock() 152 pathCache[path] = pm 153 pathLock.Unlock() 154 155 return pm, nil 156 } 157 158 // ApplyPatchesToTarget takes a patch target and patches its "Data" using the patches 159 // stored in the patch manager. The resulted "Data" is always converted to JSON. 160 func (pm *PatchManager) ApplyPatchesToTarget(patchTarget *PatchTarget) error { 161 var err error 162 var patchedData []byte 163 164 var found bool 165 for _, pt := range pm.knownTargets { 166 if pt == patchTarget.Name { 167 found = true 168 break 169 } 170 } 171 if !found { 172 return errors.Errorf("unknown patch target name %q, must be one of %v", patchTarget.Name, pm.knownTargets) 173 } 174 175 // Always convert the target data to JSON. 176 patchedData, err = yaml.YAMLToJSON(patchTarget.Data) 177 if err != nil { 178 return err 179 } 180 181 // Iterate over the patchSets. 182 for _, patchSet := range pm.patchSets { 183 if patchSet.targetName != patchTarget.Name { 184 continue 185 } 186 187 // Iterate over the patches in the patchSets. 188 for _, patch := range patchSet.patches { 189 patchBytes := []byte(patch) 190 191 // Patch based on the patch type. 192 switch patchSet.patchType { 193 194 // JSON patch. 195 case types.JSONPatchType: 196 var patchObj jsonpatch.Patch 197 patchObj, err = jsonpatch.DecodePatch(patchBytes) 198 if err == nil { 199 patchedData, err = patchObj.Apply(patchedData) 200 } 201 202 // Merge patch. 203 case types.MergePatchType: 204 patchedData, err = jsonpatch.MergePatch(patchedData, patchBytes) 205 206 // Strategic merge patch. 207 case types.StrategicMergePatchType: 208 patchedData, err = strategicpatch.StrategicMergePatch( 209 patchedData, 210 patchBytes, 211 patchTarget.StrategicMergePatchObject, 212 ) 213 } 214 215 if err != nil { 216 return errors.Wrapf(err, "could not apply the following patch of type %q to target %q:\n%s\n", 217 patchSet.patchType, 218 patchTarget.Name, 219 patch) 220 } 221 fmt.Fprintf(pm.output, "[patches] Applied patch of type %q to target %q\n", patchSet.patchType, patchTarget.Name) 222 } 223 224 // Update the data for this patch target. 225 patchTarget.Data = patchedData 226 } 227 228 return nil 229 } 230 231 // parseFilename validates a file name and retrieves the encoded target name and patch type. 232 // - On unknown extension or target name it returns a warning 233 // - On unknown patch type it returns an error 234 // - On success it returns a target name and patch type 235 func parseFilename(fileName string, knownTargets []string) (string, types.PatchType, error, error) { 236 // Return a warning if the extension cannot be matched. 237 if !regExtension.MatchString(fileName) { 238 return "", "", errors.Errorf("the file extension must be one of %v", knownExtensions), nil 239 } 240 241 regFileNameSplit := regexp.MustCompile( 242 fmt.Sprintf(`^(%s)([^.+\n]*)?(\+)?(%s)?`, strings.Join(knownTargets, "|"), patchTypesJoined), 243 ) 244 // Extract the target name and patch type. The resulting sub-string slice would look like this: 245 // [full-match, targetName, suffix, +, patchType] 246 sub := regFileNameSplit.FindStringSubmatch(fileName) 247 if sub == nil { 248 return "", "", errors.Errorf("unknown target, must be one of %v", knownTargets), nil 249 } 250 targetName := sub[1] 251 252 if len(sub[3]) > 0 && len(sub[4]) == 0 { 253 return "", "", nil, errors.Errorf("unknown or missing patch type after '+', must be one of %v", patchTypeList) 254 } 255 patchType := patchTypes[sub[4]] 256 257 return targetName, patchType, nil, nil 258 } 259 260 // createPatchSet creates a patchSet object, by splitting the given "data" by "\n---". 261 func createPatchSet(targetName string, patchType types.PatchType, data string) (*patchSet, error) { 262 var patches []string 263 264 // Split the patches and convert them to JSON. 265 // Data that is already JSON will not cause an error. 266 buf := bytes.NewBuffer([]byte(data)) 267 reader := utilyaml.NewYAMLReader(bufio.NewReader(buf)) 268 for { 269 patch, err := reader.Read() 270 if err == io.EOF { 271 break 272 } else if err != nil { 273 return nil, errors.Wrapf(err, "could not split patches for data:\n%s\n", data) 274 } 275 276 patch = bytes.TrimSpace(patch) 277 if len(patch) == 0 { 278 continue 279 } 280 281 patchJSON, err := yaml.YAMLToJSON(patch) 282 if err != nil { 283 return nil, errors.Wrapf(err, "could not convert patch to JSON:\n%s\n", patch) 284 } 285 patches = append(patches, string(patchJSON)) 286 } 287 288 return &patchSet{ 289 targetName: targetName, 290 patchType: patchType, 291 patches: patches, 292 }, nil 293 } 294 295 // getPatchSetsFromPath walks a path, ignores sub-directories and non-patch files, and 296 // returns a list of patchFile objects. 297 func getPatchSetsFromPath(targetPath string, knownTargets []string, output io.Writer) ([]*patchSet, []string, []string, error) { 298 patchFiles := []string{} 299 ignoredFiles := []string{} 300 patchSets := []*patchSet{} 301 302 // Check if targetPath is a directory. 303 info, err := os.Lstat(targetPath) 304 if err != nil { 305 goto return_path_error 306 } 307 if !info.IsDir() { 308 err = &os.PathError{ 309 Op: "getPatchSetsFromPath", 310 Path: info.Name(), 311 Err: errors.New("not a directory"), 312 } 313 goto return_path_error 314 } 315 316 err = filepath.Walk(targetPath, func(path string, info os.FileInfo, err error) error { 317 if err != nil { 318 return err 319 } 320 321 // Sub-directories and "." are ignored. 322 if info.IsDir() { 323 return nil 324 } 325 326 baseName := info.Name() 327 328 // Parse the filename and retrieve the target and patch type 329 targetName, patchType, warn, err := parseFilename(baseName, knownTargets) 330 if err != nil { 331 return err 332 } 333 if warn != nil { 334 fmt.Fprintf(output, "[patches] Ignoring file %q: %v\n", baseName, warn) 335 ignoredFiles = append(ignoredFiles, baseName) 336 return nil 337 } 338 339 // Read the patch file. 340 data, err := os.ReadFile(path) 341 if err != nil { 342 return errors.Wrapf(err, "could not read the file %q", path) 343 } 344 345 if len(data) == 0 { 346 fmt.Fprintf(output, "[patches] Ignoring empty file: %q\n", baseName) 347 ignoredFiles = append(ignoredFiles, baseName) 348 return nil 349 } 350 351 // Create a patchSet object. 352 patchSet, err := createPatchSet(targetName, patchType, string(data)) 353 if err != nil { 354 return err 355 } 356 357 patchFiles = append(patchFiles, baseName) 358 patchSets = append(patchSets, patchSet) 359 return nil 360 }) 361 362 return_path_error: 363 if err != nil { 364 return nil, nil, nil, errors.Wrapf(err, "could not list patch files for path %q", targetPath) 365 } 366 367 return patchSets, patchFiles, ignoredFiles, nil 368 }