k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/cmd/kubeadm/app/util/patches/patches_test.go (about) 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 "bytes" 21 "io" 22 "os" 23 "path/filepath" 24 "reflect" 25 "testing" 26 27 utiltesting "k8s.io/client-go/util/testing" 28 29 "github.com/pkg/errors" 30 31 v1 "k8s.io/api/core/v1" 32 "k8s.io/apimachinery/pkg/types" 33 ) 34 35 var testKnownTargets = []string{ 36 "etcd", 37 "kube-apiserver", 38 "kube-controller-manager", 39 "kube-scheduler", 40 "kubeletconfiguration", 41 } 42 43 const testDirPattern = "patch-files" 44 45 func TestParseFilename(t *testing.T) { 46 tests := []struct { 47 name string 48 fileName string 49 expectedTargetName string 50 expectedPatchType types.PatchType 51 expectedWarning bool 52 expectedError bool 53 }{ 54 { 55 name: "valid: known target and patch type", 56 fileName: "etcd+merge.json", 57 expectedTargetName: "etcd", 58 expectedPatchType: types.MergePatchType, 59 }, 60 { 61 name: "valid: known target and default patch type", 62 fileName: "etcd0.yaml", 63 expectedTargetName: "etcd", 64 expectedPatchType: types.StrategicMergePatchType, 65 }, 66 { 67 name: "valid: known target and custom patch type", 68 fileName: "etcd0+merge.yaml", 69 expectedTargetName: "etcd", 70 expectedPatchType: types.MergePatchType, 71 }, 72 { 73 name: "invalid: unknown target", 74 fileName: "foo.yaml", 75 expectedWarning: true, 76 }, 77 { 78 name: "invalid: unknown extension", 79 fileName: "etcd.foo", 80 expectedWarning: true, 81 }, 82 { 83 name: "invalid: missing extension", 84 fileName: "etcd", 85 expectedWarning: true, 86 }, 87 { 88 name: "invalid: unknown patch type", 89 fileName: "etcd+foo.json", 90 expectedError: true, 91 }, 92 { 93 name: "invalid: missing patch type", 94 fileName: "etcd+.json", 95 expectedError: true, 96 }, 97 } 98 99 for _, tc := range tests { 100 t.Run(tc.name, func(t *testing.T) { 101 targetName, patchType, warn, err := parseFilename(tc.fileName, testKnownTargets) 102 if (err != nil) != tc.expectedError { 103 t.Errorf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) 104 } 105 if (warn != nil) != tc.expectedWarning { 106 t.Errorf("expected warning: %v, got: %v, warning: %v", tc.expectedWarning, warn != nil, warn) 107 } 108 if targetName != tc.expectedTargetName { 109 t.Errorf("expected target name: %v, got: %v", tc.expectedTargetName, targetName) 110 } 111 if patchType != tc.expectedPatchType { 112 t.Errorf("expected patch type: %v, got: %v", tc.expectedPatchType, patchType) 113 } 114 }) 115 } 116 } 117 118 func TestCreatePatchSet(t *testing.T) { 119 tests := []struct { 120 name string 121 targetName string 122 patchType types.PatchType 123 expectedPatchSet *patchSet 124 data string 125 }{ 126 { 127 128 name: "valid: YAML patches are separated and converted to JSON", 129 targetName: "etcd", 130 patchType: types.StrategicMergePatchType, 131 data: "foo: bar\n---\nfoo: baz\n", 132 expectedPatchSet: &patchSet{ 133 targetName: "etcd", 134 patchType: types.StrategicMergePatchType, 135 patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, 136 }, 137 }, 138 { 139 name: "valid: JSON patches are separated", 140 targetName: "etcd", 141 patchType: types.StrategicMergePatchType, 142 data: `{"foo":"bar"}` + "\n---\n" + `{"foo":"baz"}`, 143 expectedPatchSet: &patchSet{ 144 targetName: "etcd", 145 patchType: types.StrategicMergePatchType, 146 patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, 147 }, 148 }, 149 { 150 name: "valid: empty patches are ignored", 151 targetName: "etcd", 152 patchType: types.StrategicMergePatchType, 153 data: `{"foo":"bar"}` + "\n---\n ---\n" + `{"foo":"baz"}`, 154 expectedPatchSet: &patchSet{ 155 targetName: "etcd", 156 patchType: types.StrategicMergePatchType, 157 patches: []string{`{"foo":"bar"}`, `{"foo":"baz"}`}, 158 }, 159 }, 160 } 161 162 for _, tc := range tests { 163 t.Run(tc.name, func(t *testing.T) { 164 ps, _ := createPatchSet(tc.targetName, tc.patchType, tc.data) 165 if !reflect.DeepEqual(ps, tc.expectedPatchSet) { 166 t.Fatalf("expected patch set:\n%+v\ngot:\n%+v\n", tc.expectedPatchSet, ps) 167 } 168 }) 169 } 170 } 171 172 func TestGetPatchSetsForPathMustBeDirectory(t *testing.T) { 173 tempFile, err := os.CreateTemp("", "test-file") 174 if err != nil { 175 t.Errorf("error creating temporary file: %v", err) 176 } 177 defer utiltesting.CloseAndRemove(t, tempFile) 178 179 _, _, _, err = getPatchSetsFromPath(tempFile.Name(), testKnownTargets, io.Discard) 180 var pathErr *os.PathError 181 if !errors.As(err, &pathErr) { 182 t.Fatalf("expected os.PathError for non-directory path %q, but got %v", tempFile.Name(), err) 183 } 184 } 185 186 func TestGetPatchSetsForPath(t *testing.T) { 187 const patchData = `{"foo":"bar"}` 188 189 tests := []struct { 190 name string 191 filesToWrite []string 192 expectedPatchSets []*patchSet 193 expectedPatchFiles []string 194 expectedIgnoredFiles []string 195 expectedError bool 196 patchData string 197 }{ 198 { 199 name: "valid: patch files are sorted and non-patch files are ignored", 200 filesToWrite: []string{"kube-scheduler+merge.json", "kube-apiserver+json.yaml", "etcd.yaml", "foo", "bar.json"}, 201 patchData: patchData, 202 expectedPatchSets: []*patchSet{ 203 { 204 targetName: "etcd", 205 patchType: types.StrategicMergePatchType, 206 patches: []string{patchData}, 207 }, 208 { 209 targetName: "kube-apiserver", 210 patchType: types.JSONPatchType, 211 patches: []string{patchData}, 212 }, 213 { 214 targetName: "kube-scheduler", 215 patchType: types.MergePatchType, 216 patches: []string{patchData}, 217 }, 218 }, 219 expectedPatchFiles: []string{"etcd.yaml", "kube-apiserver+json.yaml", "kube-scheduler+merge.json"}, 220 expectedIgnoredFiles: []string{"bar.json", "foo"}, 221 }, 222 { 223 name: "valid: empty files are ignored", 224 patchData: "", 225 filesToWrite: []string{"kube-scheduler.json"}, 226 expectedPatchFiles: []string{}, 227 expectedIgnoredFiles: []string{"kube-scheduler.json"}, 228 expectedPatchSets: []*patchSet{}, 229 }, 230 { 231 name: "invalid: bad patch type in filename returns and error", 232 filesToWrite: []string{"kube-scheduler+foo.json"}, 233 expectedError: true, 234 }, 235 } 236 237 for _, tc := range tests { 238 t.Run(tc.name, func(t *testing.T) { 239 tempDir, err := os.MkdirTemp("", testDirPattern) 240 if err != nil { 241 t.Fatal(err) 242 } 243 defer os.RemoveAll(tempDir) 244 245 for _, file := range tc.filesToWrite { 246 filePath := filepath.Join(tempDir, file) 247 err := os.WriteFile(filePath, []byte(tc.patchData), 0644) 248 if err != nil { 249 t.Fatalf("could not write temporary file %q", filePath) 250 } 251 } 252 253 patchSets, patchFiles, ignoredFiles, err := getPatchSetsFromPath(tempDir, testKnownTargets, io.Discard) 254 if (err != nil) != tc.expectedError { 255 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) 256 } 257 258 if !reflect.DeepEqual(tc.expectedPatchFiles, patchFiles) { 259 t.Fatalf("expected patch files:\n%+v\ngot:\n%+v", tc.expectedPatchFiles, patchFiles) 260 } 261 if !reflect.DeepEqual(tc.expectedIgnoredFiles, ignoredFiles) { 262 t.Fatalf("expected ignored files:\n%+v\ngot:\n%+v", tc.expectedIgnoredFiles, ignoredFiles) 263 } 264 if !reflect.DeepEqual(tc.expectedPatchSets, patchSets) { 265 t.Fatalf("expected patch sets:\n%+v\ngot:\n%+v", tc.expectedPatchSets, patchSets) 266 } 267 }) 268 } 269 } 270 271 func TestGetPatchManagerForPath(t *testing.T) { 272 type file struct { 273 name string 274 data string 275 } 276 277 tests := []struct { 278 name string 279 files []*file 280 patchTarget *PatchTarget 281 expectedData []byte 282 expectedError bool 283 }{ 284 { 285 name: "valid: patch a kube-apiserver target using merge patch; json patch is applied first", 286 patchTarget: &PatchTarget{ 287 Name: "kube-apiserver", 288 StrategicMergePatchObject: v1.Pod{}, 289 Data: []byte("foo: bar\nbaz: qux\n"), 290 }, 291 expectedData: []byte(`{"baz":"qux","foo":"patched"}`), 292 files: []*file{ 293 { 294 name: "kube-apiserver+merge.yaml", 295 data: "foo: patched", 296 }, 297 { 298 name: "kube-apiserver+json.json", 299 data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, 300 }, 301 }, 302 }, 303 { 304 name: "valid: kube-apiserver target is patched with json patch", 305 patchTarget: &PatchTarget{ 306 Name: "kube-apiserver", 307 StrategicMergePatchObject: v1.Pod{}, 308 Data: []byte("foo: bar\n"), 309 }, 310 expectedData: []byte(`{"foo":"zzz"}`), 311 files: []*file{ 312 { 313 name: "kube-apiserver+json.json", 314 data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, 315 }, 316 }, 317 }, 318 { 319 name: "valid: kubeletconfiguration target is patched with json patch", 320 patchTarget: &PatchTarget{ 321 Name: "kubeletconfiguration", 322 StrategicMergePatchObject: nil, 323 Data: []byte("foo: bar\n"), 324 }, 325 expectedData: []byte(`{"foo":"zzz"}`), 326 files: []*file{ 327 { 328 name: "kubeletconfiguration+json.json", 329 data: `[{"op": "replace", "path": "/foo", "value": "zzz"}]`, 330 }, 331 }, 332 }, 333 { 334 name: "valid: kube-apiserver target is patched with strategic merge patch", 335 patchTarget: &PatchTarget{ 336 Name: "kube-apiserver", 337 StrategicMergePatchObject: v1.Pod{}, 338 Data: []byte("foo: bar\n"), 339 }, 340 expectedData: []byte(`{"foo":"zzz"}`), 341 files: []*file{ 342 { 343 name: "kube-apiserver+strategic.json", 344 data: `{"foo":"zzz"}`, 345 }, 346 }, 347 }, 348 { 349 name: "valid: etcd target is not changed because there are no patches for it", 350 patchTarget: &PatchTarget{ 351 Name: "etcd", 352 StrategicMergePatchObject: v1.Pod{}, 353 Data: []byte("foo: bar\n"), 354 }, 355 expectedData: []byte("foo: bar\n"), 356 files: []*file{ 357 { 358 name: "kube-apiserver+merge.yaml", 359 data: "foo: patched", 360 }, 361 }, 362 }, 363 { 364 name: "invalid: cannot patch etcd target due to malformed json patch", 365 patchTarget: &PatchTarget{ 366 Name: "etcd", 367 StrategicMergePatchObject: v1.Pod{}, 368 Data: []byte("foo: bar\n"), 369 }, 370 files: []*file{ 371 { 372 name: "etcd+json.json", 373 data: `{"foo":"zzz"}`, 374 }, 375 }, 376 expectedError: true, 377 }, 378 } 379 380 for _, tc := range tests { 381 t.Run(tc.name, func(t *testing.T) { 382 tempDir, err := os.MkdirTemp("", testDirPattern) 383 if err != nil { 384 t.Fatal(err) 385 } 386 defer os.RemoveAll(tempDir) 387 388 for _, file := range tc.files { 389 filePath := filepath.Join(tempDir, file.name) 390 err := os.WriteFile(filePath, []byte(file.data), 0644) 391 if err != nil { 392 t.Fatalf("could not write temporary file %q", filePath) 393 } 394 } 395 396 pm, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) 397 if err != nil { 398 t.Fatal(err) 399 } 400 401 err = pm.ApplyPatchesToTarget(tc.patchTarget) 402 if (err != nil) != tc.expectedError { 403 t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err) 404 } 405 if err != nil { 406 return 407 } 408 409 if !bytes.Equal(tc.patchTarget.Data, tc.expectedData) { 410 t.Fatalf("expected result:\n%s\ngot:\n%s", tc.expectedData, tc.patchTarget.Data) 411 } 412 }) 413 } 414 } 415 416 func TestGetPatchManagerForPathCache(t *testing.T) { 417 tempDir, err := os.MkdirTemp("", testDirPattern) 418 if err != nil { 419 t.Fatal(err) 420 } 421 defer os.RemoveAll(tempDir) 422 423 pmOld, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) 424 if err != nil { 425 t.Fatal(err) 426 } 427 pmNew, err := GetPatchManagerForPath(tempDir, testKnownTargets, nil) 428 if err != nil { 429 t.Fatal(err) 430 } 431 if pmOld != pmNew { 432 t.Logf("path %q was not cached, expected pointer: %p, got: %p", tempDir, pmOld, pmNew) 433 } 434 }