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  }