mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-19 08:40:42 +00:00
kubeadm: add logic for patching components in util/patches
This commit is contained in:
parent
a8b31556c8
commit
5506049b87
@ -89,6 +89,7 @@ filegroup(
|
||||
"//cmd/kubeadm/app/util/kubeconfig:all-srcs",
|
||||
"//cmd/kubeadm/app/util/kustomize:all-srcs",
|
||||
"//cmd/kubeadm/app/util/output:all-srcs",
|
||||
"//cmd/kubeadm/app/util/patches:all-srcs",
|
||||
"//cmd/kubeadm/app/util/pkiutil:all-srcs",
|
||||
"//cmd/kubeadm/app/util/pubkeypin:all-srcs",
|
||||
"//cmd/kubeadm/app/util/runtime:all-srcs",
|
||||
|
40
cmd/kubeadm/app/util/patches/BUILD
Normal file
40
cmd/kubeadm/app/util/patches/BUILD
Normal file
@ -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"],
|
||||
)
|
342
cmd/kubeadm/app/util/patches/patches.go
Normal file
342
cmd/kubeadm/app/util/patches/patches.go
Normal file
@ -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
|
||||
}
|
413
cmd/kubeadm/app/util/patches/patches_test.go
Normal file
413
cmd/kubeadm/app/util/patches/patches_test.go
Normal file
@ -0,0 +1,413 @@
|
||||
/*
|
||||
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 (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
var testKnownTargets = []string{
|
||||
"etcd",
|
||||
"kube-apiserver",
|
||||
"kube-controller-manager",
|
||||
"kube-scheduler",
|
||||
}
|
||||
|
||||
const testDirPattern = "patch-files"
|
||||
|
||||
func TestParseFilename(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fileName string
|
||||
expectedTargetName string
|
||||
expectedPatchType types.PatchType
|
||||
expectedWarning bool
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "valid: known target and patch type",
|
||||
fileName: "etcd+merge.json",
|
||||
expectedTargetName: "etcd",
|
||||
expectedPatchType: types.MergePatchType,
|
||||
},
|
||||
{
|
||||
name: "valid: known target and default patch type",
|
||||
fileName: "etcd0.yaml",
|
||||
expectedTargetName: "etcd",
|
||||
expectedPatchType: types.StrategicMergePatchType,
|
||||
},
|
||||
{
|
||||
name: "valid: known target and custom patch type",
|
||||
fileName: "etcd0+merge.yaml",
|
||||
expectedTargetName: "etcd",
|
||||
expectedPatchType: types.MergePatchType,
|
||||
},
|
||||
{
|
||||
name: "invalid: unknown target",
|
||||
fileName: "foo.yaml",
|
||||
expectedWarning: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: unknown extension",
|
||||
fileName: "etcd.foo",
|
||||
expectedWarning: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: missing extension",
|
||||
fileName: "etcd",
|
||||
expectedWarning: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: unknown patch type",
|
||||
fileName: "etcd+foo.json",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "invalid: missing patch type",
|
||||
fileName: "etcd+.json",
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
targetName, patchType, warn, err := parseFilename(tc.fileName, testKnownTargets)
|
||||
if (err != nil) != tc.expectedError {
|
||||
t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if (warn != nil) != tc.expectedWarning {
|
||||
t.Errorf("expected warning: %v, got: %v, warning: %v", tc.expectedWarning, warn != nil, warn)
|
||||
}
|
||||
if targetName != tc.expectedTargetName {
|
||||
t.Errorf("expected target name: %v, got: %v", tc.expectedTargetName, targetName)
|
||||
}
|
||||
if patchType != tc.expectedPatchType {
|
||||
t.Errorf("expected patch type: %v, got: %v", tc.expectedPatchType, patchType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePatchSet(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
targetName string
|
||||
patchType types.PatchType
|
||||
expectedPatchSet *patchSet
|
||||
data string
|
||||
}{
|
||||
{
|
||||
|
||||
name: "valid: YAML patches are separated and converted to JSON",
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
data: "foo: bar\n---\nfoo: baz\n",
|
||||
expectedPatchSet: &patchSet{
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid: JSON patches are separated",
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
data: `{"foo":"bar"}` + "\n---\n" + `{"foo":"baz"}`,
|
||||
expectedPatchSet: &patchSet{
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid: empty patches are ignored",
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
data: `{"foo":"bar"}` + "\n---\n ---\n" + `{"foo":"baz"}`,
|
||||
expectedPatchSet: &patchSet{
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ps, _ := createPatchSet(tc.targetName, tc.patchType, tc.data)
|
||||
if !reflect.DeepEqual(ps, tc.expectedPatchSet) {
|
||||
t.Fatalf("expected patch set:\n%+v\ngot:\n%+v\n", tc.expectedPatchSet, ps)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPatchSetsForPathMustBeDirectory(t *testing.T) {
|
||||
tempFile, err := ioutil.TempFile("", "test-file")
|
||||
if err != nil {
|
||||
t.Errorf("error creating temporary file: %v", err)
|
||||
}
|
||||
defer os.Remove(tempFile.Name())
|
||||
|
||||
_, _, _, err = getPatchSetsFromPath(tempFile.Name(), testKnownTargets, ioutil.Discard)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for non-directory path %q", tempFile.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPatchSetsForPath(t *testing.T) {
|
||||
const patchData = `{"foo":"bar"}`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filesToWrite []string
|
||||
expectedPatchSets []*patchSet
|
||||
expectedPatchFiles []string
|
||||
expectedIgnoredFiles []string
|
||||
expectedError bool
|
||||
patchData string
|
||||
}{
|
||||
{
|
||||
name: "valid: patch files are sorted and non-patch files are ignored",
|
||||
filesToWrite: []string{"kube-scheduler+merge.json", "kube-apiserver+json.yaml", "etcd.yaml", "foo", "bar.json"},
|
||||
patchData: patchData,
|
||||
expectedPatchSets: []*patchSet{
|
||||
{
|
||||
targetName: "etcd",
|
||||
patchType: types.StrategicMergePatchType,
|
||||
patches: []string{patchData},
|
||||
},
|
||||
{
|
||||
targetName: "kube-apiserver",
|
||||
patchType: types.JSONPatchType,
|
||||
patches: []string{patchData},
|
||||
},
|
||||
{
|
||||
targetName: "kube-scheduler",
|
||||
patchType: types.MergePatchType,
|
||||
patches: []string{patchData},
|
||||
},
|
||||
},
|
||||
expectedPatchFiles: []string{"etcd.yaml", "kube-apiserver+json.yaml", "kube-scheduler+merge.json"},
|
||||
expectedIgnoredFiles: []string{"bar.json", "foo"},
|
||||
},
|
||||
{
|
||||
name: "valid: empty files are ignored",
|
||||
patchData: "",
|
||||
filesToWrite: []string{"kube-scheduler.json"},
|
||||
expectedPatchFiles: []string{},
|
||||
expectedIgnoredFiles: []string{"kube-scheduler.json"},
|
||||
expectedPatchSets: []*patchSet{},
|
||||
},
|
||||
{
|
||||
name: "invalid: bad patch type in filename returns and error",
|
||||
filesToWrite: []string{"kube-scheduler+foo.json"},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tempDir, err := ioutil.TempDir("", testDirPattern)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
for _, file := range tc.filesToWrite {
|
||||
filePath := filepath.Join(tempDir, file)
|
||||
err := ioutil.WriteFile(filePath, []byte(tc.patchData), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write temporary file %q", filePath)
|
||||
}
|
||||
}
|
||||
|
||||
patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(tempDir, testKnownTargets, ioutil.Discard)
|
||||
if (err != nil) != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(tc.expectedPatchFiles, patchFiles) {
|
||||
t.Fatalf("expected patch files:\n%+v\ngot:\n%+v", tc.expectedPatchFiles, patchFiles)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expectedIgnoredFiles, ignoredFiles) {
|
||||
t.Fatalf("expected ignored files:\n%+v\ngot:\n%+v", tc.expectedIgnoredFiles, ignoredFiles)
|
||||
}
|
||||
if !reflect.DeepEqual(tc.expectedPatchSets, patchSets) {
|
||||
t.Fatalf("expected patch sets:\n%+v\ngot:\n%+v", tc.expectedPatchSets, patchSets)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPatchManagerForPath(t *testing.T) {
|
||||
type file struct {
|
||||
name string
|
||||
data string
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
files []*file
|
||||
patchTarget *PatchTarget
|
||||
expectedData []byte
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "valid: patch a kube-apiserver target using merge patch; json patch is applied first",
|
||||
patchTarget: &PatchTarget{
|
||||
Name: "kube-apiserver",
|
||||
StrategicMergePatchObject: v1.Pod{},
|
||||
Data: []byte("foo: bar\nbaz: qux\n"),
|
||||
},
|
||||
expectedData: []byte(`{"baz":"qux","foo":"patched"}`),
|
||||
files: []*file{
|
||||
{
|
||||
name: "kube-apiserver+merge.yaml",
|
||||
data: "foo: patched",
|
||||
},
|
||||
{
|
||||
name: "kube-apiserver+json.json",
|
||||
data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid: kube-apiserver target is patched with json patch",
|
||||
patchTarget: &PatchTarget{
|
||||
Name: "kube-apiserver",
|
||||
StrategicMergePatchObject: v1.Pod{},
|
||||
Data: []byte("foo: bar\n"),
|
||||
},
|
||||
expectedData: []byte(`{"foo":"zzz"}`),
|
||||
files: []*file{
|
||||
{
|
||||
name: "kube-apiserver+json.json",
|
||||
data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid: kube-apiserver target is patched with strategic merge patch",
|
||||
patchTarget: &PatchTarget{
|
||||
Name: "kube-apiserver",
|
||||
StrategicMergePatchObject: v1.Pod{},
|
||||
Data: []byte("foo: bar\n"),
|
||||
},
|
||||
expectedData: []byte(`{"foo":"zzz"}`),
|
||||
files: []*file{
|
||||
{
|
||||
name: "kube-apiserver+strategic.json",
|
||||
data: `{"foo":"zzz"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid: etcd target is not changed because there are no patches for it",
|
||||
patchTarget: &PatchTarget{
|
||||
Name: "etcd",
|
||||
StrategicMergePatchObject: v1.Pod{},
|
||||
Data: []byte("foo: bar\n"),
|
||||
},
|
||||
expectedData: []byte("foo: bar\n"),
|
||||
files: []*file{
|
||||
{
|
||||
name: "kube-apiserver+merge.yaml",
|
||||
data: "foo: patched",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid: cannot patch etcd target due to malformed json patch",
|
||||
patchTarget: &PatchTarget{
|
||||
Name: "etcd",
|
||||
StrategicMergePatchObject: v1.Pod{},
|
||||
Data: []byte("foo: bar\n"),
|
||||
},
|
||||
files: []*file{
|
||||
{
|
||||
name: "etcd+json.json",
|
||||
data: `{"foo":"zzz"}`,
|
||||
},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tempDir, err := ioutil.TempDir("", testDirPattern)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
for _, file := range tc.files {
|
||||
filePath := filepath.Join(tempDir, file.name)
|
||||
err := ioutil.WriteFile(filePath, []byte(file.data), 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("could not write temporary file %q", filePath)
|
||||
}
|
||||
}
|
||||
|
||||
pm, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = pm.ApplyPatchesToTarget(tc.patchTarget)
|
||||
if (err != nil) != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(tc.patchTarget.Data, tc.expectedData) {
|
||||
t.Fatalf("expected result:\n%s\ngot:\n%s", tc.expectedData, tc.patchTarget.Data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPatchManagerForPathCache(t *testing.T) {
|
||||
tempDir, err := ioutil.TempDir("", testDirPattern)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
pmOld, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pmNew, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pmOld != pmNew {
|
||||
t.Logf("path %q was not cached, expected pointer: %p, got: %p", tempDir, pmOld, pmNew)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user