...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/util/patches/patches.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/util/patches

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

View as plain text