1
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
41 type PatchTarget struct {
42
43
44 Name string
45
46
47
48 StrategicMergePatchObject interface{}
49
50
51 Data []byte
52 }
53
54
55 type PatchManager struct {
56 patchSets []*patchSet
57 knownTargets []string
58 output io.Writer
59 }
60
61
62 type patchSet struct {
63 targetName string
64 patchType types.PatchType
65 patches []string
66 }
67
68
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
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,
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
107 func KnownTargets() []string {
108 return knownTargets
109 }
110
111
112
113
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
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
154
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
171 patchedData, err = yaml.YAMLToJSON(patchTarget.Data)
172 if err != nil {
173 return err
174 }
175
176
177 for _, patchSet := range pm.patchSets {
178 if patchSet.targetName != patchTarget.Name {
179 continue
180 }
181
182
183 for _, patch := range patchSet.patches {
184 patchBytes := []byte(patch)
185
186
187 switch patchSet.patchType {
188
189
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
198 case types.MergePatchType:
199 patchedData, err = jsonpatch.MergePatch(patchedData, patchBytes)
200
201
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
220 patchTarget.Data = patchedData
221 }
222
223 return nil
224 }
225
226
227
228
229
230 func parseFilename(fileName string, knownTargets []string) (string, types.PatchType, error, error) {
231
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
240
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
256 func createPatchSet(targetName string, patchType types.PatchType, data string) (*patchSet, error) {
257 var patches []string
258
259
260
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
291
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
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
317 if info.IsDir() {
318 return nil
319 }
320
321 baseName := info.Name()
322
323
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
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
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