Skip to content

Commit 5506049

Browse files
committed
kubeadm: add logic for patching components in util/patches
1 parent a8b3155 commit 5506049

File tree

4 files changed

+796
-0
lines changed

4 files changed

+796
-0
lines changed

cmd/kubeadm/app/util/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ filegroup(
8989
"//cmd/kubeadm/app/util/kubeconfig:all-srcs",
9090
"//cmd/kubeadm/app/util/kustomize:all-srcs",
9191
"//cmd/kubeadm/app/util/output:all-srcs",
92+
"//cmd/kubeadm/app/util/patches:all-srcs",
9293
"//cmd/kubeadm/app/util/pkiutil:all-srcs",
9394
"//cmd/kubeadm/app/util/pubkeypin:all-srcs",
9495
"//cmd/kubeadm/app/util/runtime:all-srcs",

cmd/kubeadm/app/util/patches/BUILD

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

0 commit comments

Comments
 (0)