Skip to content

Commit fd2eebf

Browse files
committed
feat: create merge patch from diff of two machine configs
Added `configdiff.Patch` function to talos machinery. This function takes two machine configs in the form of `config.Provider` and returns a slice of Patches capable of converting the original config to the modified config when applied in order. Signed-off-by: Oguz Kilcan <oguz.kilcan@siderolabs.com>
1 parent eadbdda commit fd2eebf

File tree

5 files changed

+1055
-0
lines changed

5 files changed

+1055
-0
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
package configdiff
6+
7+
import (
8+
"bytes"
9+
"fmt"
10+
"reflect"
11+
12+
"gopkg.in/yaml.v3"
13+
14+
configcore "github.com/siderolabs/talos/pkg/machinery/config"
15+
"github.com/siderolabs/talos/pkg/machinery/config/config"
16+
"github.com/siderolabs/talos/pkg/machinery/config/configloader"
17+
"github.com/siderolabs/talos/pkg/machinery/config/configpatcher"
18+
"github.com/siderolabs/talos/pkg/machinery/config/container"
19+
"github.com/siderolabs/talos/pkg/machinery/config/encoder"
20+
"github.com/siderolabs/talos/pkg/machinery/config/internal/documentid"
21+
)
22+
23+
type unstructured map[string]any
24+
25+
// Patch will return a slice of Patches capable of converting the original config to the modified config when applied in order.
26+
func Patch(original, modified configcore.Provider) ([]configpatcher.Patch, error) {
27+
patches := make([]configpatcher.Patch, 0, 2)
28+
29+
firstPass, err := patch(original, modified, true)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
if firstPass != nil {
35+
firstPatch, ok := (*firstPass).(configpatcher.Patch)
36+
if !ok {
37+
return nil, fmt.Errorf("expected Patch, got %T", *firstPass)
38+
}
39+
40+
patches = append(patches, firstPatch)
41+
}
42+
43+
secondPass, err := patch(original, modified, false)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
if secondPass != nil {
49+
secondPatch, ok := (*secondPass).(configpatcher.Patch)
50+
if !ok {
51+
return nil, fmt.Errorf("expected Patch, got %T", *secondPass)
52+
}
53+
54+
patches = append(patches, secondPatch)
55+
}
56+
57+
return patches, nil
58+
}
59+
60+
// nolint: gocyclo
61+
func patch(original, modified configcore.Provider, firstPass bool) (*configpatcher.StrategicMergePatch, error) {
62+
originalIDToDoc := documentsToMap(original.Documents())
63+
modifiedIDToDoc := documentsToMap(modified.Documents())
64+
65+
var removed, added, common []documentid.DocumentID // nolint:prealloc
66+
67+
for id := range originalIDToDoc {
68+
if _, ok := modifiedIDToDoc[id]; !ok {
69+
removed = append(removed, id)
70+
71+
continue
72+
}
73+
74+
common = append(common, id)
75+
}
76+
77+
for id := range modifiedIDToDoc {
78+
if _, ok := originalIDToDoc[id]; !ok {
79+
added = append(added, id)
80+
}
81+
}
82+
83+
unstructuredPatches := make([]unstructured, 0, len(removed)+len(added)+len(common))
84+
85+
if firstPass {
86+
for _, removedID := range removed {
87+
meta := removedID.Meta()
88+
meta["$patch"] = "delete"
89+
90+
unstructuredPatches = append(unstructuredPatches, meta)
91+
}
92+
93+
for _, addedID := range added {
94+
addedDoc := modifiedIDToDoc[addedID]
95+
96+
addedUnstructured, err := documentToUnstructured(addedDoc)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
unstructuredPatches = append(unstructuredPatches, addedUnstructured)
102+
}
103+
}
104+
105+
for _, commonID := range common {
106+
originalDoc := originalIDToDoc[commonID]
107+
modifiedDoc := modifiedIDToDoc[commonID]
108+
109+
originalUnstructured, err := documentToUnstructured(originalDoc)
110+
if err != nil {
111+
return nil, err
112+
}
113+
114+
modifiedUnstructured, err := documentToUnstructured(modifiedDoc)
115+
if err != nil {
116+
return nil, err
117+
}
118+
119+
mergePatch, err := createMergePatch(originalUnstructured, modifiedUnstructured, &commonID, firstPass)
120+
if err != nil {
121+
return nil, err
122+
}
123+
124+
if len(mergePatch) == 0 {
125+
continue
126+
}
127+
128+
unstructuredPatches = append(unstructuredPatches, mergePatch)
129+
}
130+
131+
if len(unstructuredPatches) == 0 {
132+
return nil, nil
133+
}
134+
135+
patchYAML, err := encodeToYAML(unstructuredPatches)
136+
if err != nil {
137+
return nil, err
138+
}
139+
140+
cfg, err := configloader.NewFromBytes(patchYAML, configloader.WithAllowPatchDelete())
141+
if err != nil {
142+
return nil, err
143+
}
144+
145+
mergePatch := configpatcher.NewStrategicMergePatch(cfg)
146+
147+
return &mergePatch, nil
148+
}
149+
150+
func encodeToYAML(docs []unstructured) ([]byte, error) {
151+
var buf bytes.Buffer
152+
153+
enc := yaml.NewEncoder(&buf)
154+
enc.SetIndent(2)
155+
156+
for _, doc := range docs {
157+
if err := enc.Encode(doc); err != nil {
158+
return nil, err
159+
}
160+
}
161+
162+
if err := enc.Close(); err != nil {
163+
return nil, err
164+
}
165+
166+
return buf.Bytes(), nil
167+
}
168+
169+
func documentsToMap(docs []config.Document) map[documentid.DocumentID]config.Document {
170+
out := make(map[documentid.DocumentID]config.Document, len(docs))
171+
172+
for _, doc := range docs {
173+
out[documentid.Extract(doc)] = doc
174+
}
175+
176+
return out
177+
}
178+
179+
func documentToUnstructured(doc config.Document) (unstructured, error) {
180+
c, err := container.New(doc)
181+
if err != nil {
182+
return nil, err
183+
}
184+
185+
unstructuredBytes, err := c.EncodeBytes(encoder.WithComments(encoder.CommentsDisabled))
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
var out unstructured
191+
if err = yaml.Unmarshal(unstructuredBytes, &out); err != nil {
192+
return nil, err
193+
}
194+
195+
return out, nil
196+
}
197+
198+
// nolint: gocyclo,cyclop
199+
func createMergePatch(original, modified unstructured, documentID *documentid.DocumentID, firstPass bool) (unstructured, error) {
200+
meta := unstructured{}
201+
mergePatch := unstructured{}
202+
203+
// First, handle all modified and added values
204+
for key, modV := range modified {
205+
// Discover and keep meta fields obtained from documentId
206+
if documentID != nil {
207+
if metaV, ok := documentID.Meta()[key]; ok && metaV != "" {
208+
meta[key] = metaV
209+
210+
continue
211+
}
212+
}
213+
214+
origV, ok := original[key]
215+
if !ok {
216+
setValue(&mergePatch, key, modV, firstPass)
217+
218+
continue
219+
}
220+
221+
if reflect.TypeOf(origV) != reflect.TypeOf(modV) {
222+
setValue(&mergePatch, key, modV, firstPass)
223+
224+
continue
225+
}
226+
227+
switch origT := origV.(type) {
228+
case unstructured:
229+
modT := modV.(unstructured)
230+
231+
patchV, err := createMergePatch(origT, modT, nil, firstPass)
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
if len(patchV) > 0 {
237+
mergePatch[key] = patchV
238+
}
239+
case []any:
240+
modT := modV.([]any)
241+
if !reflect.DeepEqual(origT, modT) {
242+
if firstPass {
243+
mergePatch[key] = map[string]string{"$patch": "delete"}
244+
} else {
245+
setValue(&mergePatch, key, modV, true)
246+
}
247+
}
248+
case string, int, uint, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, bool:
249+
if !reflect.DeepEqual(origV, modV) {
250+
setValue(&mergePatch, key, modV, firstPass)
251+
}
252+
case nil:
253+
switch modV.(type) {
254+
case nil:
255+
// Both nil, fine.
256+
default:
257+
setValue(&mergePatch, key, modV, firstPass)
258+
}
259+
default:
260+
return nil, fmt.Errorf("unknown type:%T in key %s", origV, key)
261+
}
262+
}
263+
264+
// Now handle all deleted keys
265+
for key := range original {
266+
if _, ok := modified[key]; !ok {
267+
setValue(&mergePatch, key, map[string]string{"$patch": "delete"}, firstPass)
268+
269+
continue
270+
}
271+
}
272+
273+
// Finally, merge meta into mergePatch
274+
if len(mergePatch) > 0 && len(meta) > 0 {
275+
for key, metaV := range meta {
276+
mergePatch[key] = metaV
277+
}
278+
}
279+
280+
return mergePatch, nil
281+
}
282+
283+
func setValue(mergePatch *unstructured, key string, value any, shouldSet bool) {
284+
if shouldSet {
285+
(*mergePatch)[key] = value
286+
}
287+
}

0 commit comments

Comments
 (0)