forked from kubernetes/kubernetes
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
kubeadm: add logic for patching components in util/patches
- Loading branch information
Showing
4 changed files
with
796 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = ["patches.go"], | ||
importpath = "k8s.io/kubernetes/cmd/kubeadm/app/util/patches", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", | ||
"//staging/src/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library", | ||
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library", | ||
"//vendor/github.com/evanphx/json-patch:go_default_library", | ||
"//vendor/github.com/pkg/errors:go_default_library", | ||
"//vendor/sigs.k8s.io/yaml:go_default_library", | ||
], | ||
) | ||
|
||
go_test( | ||
name = "go_default_test", | ||
srcs = ["patches_test.go"], | ||
embed = [":go_default_library"], | ||
deps = [ | ||
"//staging/src/k8s.io/api/core/v1:go_default_library", | ||
"//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", | ||
], | ||
) | ||
|
||
filegroup( | ||
name = "package-srcs", | ||
srcs = glob(["**"]), | ||
tags = ["automanaged"], | ||
visibility = ["//visibility:private"], | ||
) | ||
|
||
filegroup( | ||
name = "all-srcs", | ||
srcs = [":package-srcs"], | ||
tags = ["automanaged"], | ||
visibility = ["//visibility:public"], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,342 @@ | ||
/* | ||
Copyright 2020 The Kubernetes Authors. | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
|
||
package patches | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
"sync" | ||
|
||
jsonpatch "github.com/evanphx/json-patch" | ||
"github.com/pkg/errors" | ||
"k8s.io/apimachinery/pkg/types" | ||
"k8s.io/apimachinery/pkg/util/strategicpatch" | ||
utilyaml "k8s.io/apimachinery/pkg/util/yaml" | ||
"sigs.k8s.io/yaml" | ||
) | ||
|
||
// PatchTarget defines a target to be patched, such as a control-plane static Pod. | ||
type PatchTarget struct { | ||
// Name must be the name of a known target. In the case of Kubernetes objects | ||
// this is likely to match the ObjectMeta.Name of a target. | ||
Name string | ||
|
||
// StrategicMergePatchObject is only used for strategic merge patches. | ||
// It represents the underlying object type that is patched - e.g. "v1.Pod" | ||
StrategicMergePatchObject interface{} | ||
|
||
// Data must contain the bytes that will be patched. | ||
Data []byte | ||
} | ||
|
||
// PatchManager defines an object that can apply patches. | ||
type PatchManager struct { | ||
patchSets []*patchSet | ||
knownTargets []string | ||
output io.Writer | ||
} | ||
|
||
// patchSet defines a set of patches of a certain type that can patch a PatchTarget. | ||
type patchSet struct { | ||
targetName string | ||
patchType types.PatchType | ||
patches []string | ||
} | ||
|
||
// String() is used for unit-testing. | ||
func (ps *patchSet) String() string { | ||
return fmt.Sprintf( | ||
"{%q, %q, %#v}", | ||
ps.targetName, | ||
ps.patchType, | ||
ps.patches, | ||
) | ||
} | ||
|
||
var ( | ||
pathLock = &sync.RWMutex{} | ||
pathCache = map[string]*PatchManager{} | ||
|
||
patchTypes = map[string]types.PatchType{ | ||
"json": types.JSONPatchType, | ||
"merge": types.MergePatchType, | ||
"strategic": types.StrategicMergePatchType, | ||
"": types.StrategicMergePatchType, // Default | ||
} | ||
patchTypeList = []string{"json", "merge", "strategic"} | ||
patchTypesJoined = strings.Join(patchTypeList, "|") | ||
knownExtensions = []string{"json", "yaml"} | ||
|
||
regExtension = regexp.MustCompile(`.+\.(` + strings.Join(knownExtensions, "|") + `)$`) | ||
) | ||
|
||
// GetPatchManagerForPath creates a patch manager that can be used to apply patches to "knownTargets". | ||
// "path" should contain patches that can be used to patch the "knownTargets". | ||
// If "output" is non-nil, messages about actions performed by the manager would go on this io.Writer. | ||
func GetPatchManagerForPath(path string, knownTargets []string, output io.Writer) (*PatchManager, error) { | ||
pathLock.RLock() | ||
if pm, known := pathCache[path]; known { | ||
pathLock.RUnlock() | ||
return pm, nil | ||
} | ||
pathLock.RUnlock() | ||
|
||
if output == nil { | ||
output = ioutil.Discard | ||
} | ||
|
||
fmt.Fprintf(output, "[patches] Reading patches from path %q\n", path) | ||
|
||
// Get the files in the path. | ||
patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(path, knownTargets, output) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(patchFiles) > 0 { | ||
fmt.Fprintf(output, "[patches] Found the following patch files: %v\n", patchFiles) | ||
} | ||
if len(ignoredFiles) > 0 { | ||
fmt.Fprintf(output, "[patches] Ignored the following files: %v\n", ignoredFiles) | ||
} | ||
|
||
pm := &PatchManager{ | ||
patchSets: patchSets, | ||
knownTargets: knownTargets, | ||
output: output, | ||
} | ||
pathLock.Lock() | ||
pathCache[path] = pm | ||
pathLock.Unlock() | ||
|
||
return pm, nil | ||
} | ||
|
||
// ApplyPatchesToTarget takes a patch target and patches its "Data" using the patches | ||
// stored in the patch manager. The resulted "Data" is always converted to JSON. | ||
func (pm *PatchManager) ApplyPatchesToTarget(patchTarget *PatchTarget) error { | ||
var err error | ||
var patchedData []byte | ||
|
||
var found bool | ||
for _, pt := range pm.knownTargets { | ||
if pt == patchTarget.Name { | ||
found = true | ||
break | ||
} | ||
} | ||
if !found { | ||
return errors.Errorf("unknown patch target name %q, must be one of %v", patchTarget.Name, pm.knownTargets) | ||
} | ||
|
||
// Always convert the target data to JSON. | ||
patchedData, err = yaml.YAMLToJSON(patchTarget.Data) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Iterate over the patchSets. | ||
for _, patchSet := range pm.patchSets { | ||
if patchSet.targetName != patchTarget.Name { | ||
continue | ||
} | ||
|
||
// Iterate over the patches in the patchSets. | ||
for _, patch := range patchSet.patches { | ||
patchBytes := []byte(patch) | ||
|
||
// Patch based on the patch type. | ||
switch patchSet.patchType { | ||
|
||
// JSON patch. | ||
case types.JSONPatchType: | ||
var patchObj jsonpatch.Patch | ||
patchObj, err = jsonpatch.DecodePatch(patchBytes) | ||
if err == nil { | ||
patchedData, err = patchObj.Apply(patchedData) | ||
} | ||
|
||
// Merge patch. | ||
case types.MergePatchType: | ||
patchedData, err = jsonpatch.MergePatch(patchedData, patchBytes) | ||
|
||
// Strategic merge patch. | ||
case types.StrategicMergePatchType: | ||
patchedData, err = strategicpatch.StrategicMergePatch( | ||
patchedData, | ||
patchBytes, | ||
patchTarget.StrategicMergePatchObject, | ||
) | ||
} | ||
|
||
if err != nil { | ||
return errors.Wrapf(err, "could not apply the following patch of type %q to target %q:\n%s\n", | ||
patchSet.patchType, | ||
patchTarget.Name, | ||
patch) | ||
} | ||
fmt.Fprintf(pm.output, "[patches] Applied patch of type %q to target %q\n", patchSet.patchType, patchTarget.Name) | ||
} | ||
|
||
// Update the data for this patch target. | ||
patchTarget.Data = patchedData | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// parseFilename validates a file name and retrieves the encoded target name and patch type. | ||
// - On unknown extension or target name it returns a warning | ||
// - On unknown patch type it returns an error | ||
// - On success it returns a target name and patch type | ||
func parseFilename(fileName string, knownTargets []string) (string, types.PatchType, error, error) { | ||
// Return a warning if the extension cannot be matched. | ||
if !regExtension.MatchString(fileName) { | ||
return "", "", errors.Errorf("the file extension must be one of %v", knownExtensions), nil | ||
} | ||
|
||
regFileNameSplit := regexp.MustCompile( | ||
fmt.Sprintf(`^(%s)([^.+\n]*)?(\+)?(%s)?`, strings.Join(knownTargets, "|"), patchTypesJoined), | ||
) | ||
// Extract the target name and patch type. The resulting sub-string slice would look like this: | ||
// [full-match, targetName, suffix, +, patchType] | ||
sub := regFileNameSplit.FindStringSubmatch(fileName) | ||
if sub == nil { | ||
return "", "", errors.Errorf("unknown target, must be one of %v", knownTargets), nil | ||
} | ||
targetName := sub[1] | ||
|
||
if len(sub[3]) > 0 && len(sub[4]) == 0 { | ||
return "", "", nil, errors.Errorf("unknown or missing patch type after '+', must be one of %v", patchTypeList) | ||
} | ||
patchType := patchTypes[sub[4]] | ||
|
||
return targetName, patchType, nil, nil | ||
} | ||
|
||
// createPatchSet creates a patchSet object, by splitting the given "data" by "\n---". | ||
func createPatchSet(targetName string, patchType types.PatchType, data string) (*patchSet, error) { | ||
var patches []string | ||
|
||
// Split the patches and convert them to JSON. | ||
// Data that is already JSON will not cause an error. | ||
buf := bytes.NewBuffer([]byte(data)) | ||
reader := utilyaml.NewYAMLReader(bufio.NewReader(buf)) | ||
for { | ||
patch, err := reader.Read() | ||
if err == io.EOF { | ||
break | ||
} else if err != nil { | ||
return nil, errors.Wrapf(err, "could not split patches for data:\n%s\n", data) | ||
} | ||
|
||
patch = bytes.TrimSpace(patch) | ||
if len(patch) == 0 { | ||
continue | ||
} | ||
|
||
patchJSON, err := yaml.YAMLToJSON(patch) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "could not convert patch to JSON:\n%s\n", patch) | ||
} | ||
patches = append(patches, string(patchJSON)) | ||
} | ||
|
||
return &patchSet{ | ||
targetName: targetName, | ||
patchType: patchType, | ||
patches: patches, | ||
}, nil | ||
} | ||
|
||
// getPatchSetsFromPath walks a path, ignores sub-directories and non-patch files, and | ||
// returns a list of patchFile objects. | ||
func getPatchSetsFromPath(targetPath string, knownTargets []string, output io.Writer) ([]*patchSet, []string, []string, error) { | ||
patchFiles := []string{} | ||
ignoredFiles := []string{} | ||
patchSets := []*patchSet{} | ||
|
||
// Check if targetPath is a directory. | ||
info, err := os.Lstat(targetPath) | ||
if err != nil { | ||
goto return_path_error | ||
} | ||
if !info.IsDir() { | ||
err = errors.New("not a directory") | ||
goto return_path_error | ||
} | ||
|
||
err = filepath.Walk(targetPath, func(path string, info os.FileInfo, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Sub-directories and "." are ignored. | ||
if info.IsDir() { | ||
return nil | ||
} | ||
|
||
baseName := info.Name() | ||
|
||
// Parse the filename and retrieve the target and patch type | ||
targetName, patchType, warn, err := parseFilename(baseName, knownTargets) | ||
if err != nil { | ||
return err | ||
} | ||
if warn != nil { | ||
fmt.Fprintf(output, "[patches] Ignoring file %q: %v\n", baseName, warn) | ||
ignoredFiles = append(ignoredFiles, baseName) | ||
return nil | ||
} | ||
|
||
// Read the patch file. | ||
data, err := ioutil.ReadFile(path) | ||
if err != nil { | ||
return errors.Wrapf(err, "could not read the file %q", path) | ||
} | ||
|
||
if len(data) == 0 { | ||
fmt.Fprintf(output, "[patches] Ignoring empty file: %q\n", baseName) | ||
ignoredFiles = append(ignoredFiles, baseName) | ||
return nil | ||
} | ||
|
||
// Create a patchSet object. | ||
patchSet, err := createPatchSet(targetName, patchType, string(data)) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
patchFiles = append(patchFiles, baseName) | ||
patchSets = append(patchSets, patchSet) | ||
return nil | ||
}) | ||
|
||
return_path_error: | ||
if err != nil { | ||
return nil, nil, nil, errors.Wrapf(err, "could not list patch files for path %q", targetPath) | ||
} | ||
|
||
return patchSets, patchFiles, ignoredFiles, nil | ||
} |
Oops, something went wrong.