|  | 
|  | 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