github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/app_resource_test.go (about) 1 package commands 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "strings" 7 "testing" 8 "text/tabwriter" 9 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 14 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 15 ) 16 17 func TestPrintTreeViewAppResources(t *testing.T) { 18 var nodes [3]v1alpha1.ResourceNode 19 nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"} 20 nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}} 21 nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} 22 nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} 23 nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} 24 nodeMapping := make(map[string]v1alpha1.ResourceNode) 25 mapParentToChild := make(map[string][]string) 26 parentNode := make(map[string]struct{}) 27 for _, node := range nodes { 28 nodeMapping[node.UID] = node 29 if len(node.ParentRefs) > 0 { 30 _, ok := mapParentToChild[node.ParentRefs[0].UID] 31 if !ok { 32 var temp []string 33 mapParentToChild[node.ParentRefs[0].UID] = temp 34 } 35 mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) 36 } else { 37 parentNode[node.UID] = struct{}{} 38 } 39 } 40 buf := &bytes.Buffer{} 41 w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) 42 43 printTreeViewAppResourcesNotOrphaned(nodeMapping, mapParentToChild, parentNode, w) 44 require.NoError(t, w.Flush()) 45 output := buf.String() 46 47 assert.Contains(t, output, "Rollout") 48 assert.Contains(t, output, "argoproj.io") 49 } 50 51 func TestPrintTreeViewDetailedAppResources(t *testing.T) { 52 var nodes [3]v1alpha1.ResourceNode 53 nodes[0].ResourceRef = v1alpha1.ResourceRef{Group: "", Version: "v1", Kind: "Pod", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5-6trpt", UID: "92c3a5fe-d13e-4ae2-b8ec-c10dd3543b28"} 54 nodes[0].ParentRefs = []v1alpha1.ResourceRef{{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"}} 55 nodes[1].ResourceRef = v1alpha1.ResourceRef{Group: "apps", Version: "v1", Kind: "ReplicaSet", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo-5dcd5457d5", UID: "75c30dce-1b66-414f-a86c-573a74be0f40"} 56 nodes[1].ParentRefs = []v1alpha1.ResourceRef{{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"}} 57 nodes[2].ResourceRef = v1alpha1.ResourceRef{Group: "argoproj.io", Version: "", Kind: "Rollout", Namespace: "sandbox-rollout-numalogic-demo", Name: "numalogic-rollout-demo", UID: "87f3aab0-f634-4b2c-959a-7ddd30675ed0"} 58 nodes[2].Health = &v1alpha1.HealthStatus{ 59 Status: "Degraded", 60 Message: "Readiness Gate failed", 61 } 62 63 nodeMapping := make(map[string]v1alpha1.ResourceNode) 64 mapParentToChild := make(map[string][]string) 65 parentNode := make(map[string]struct{}) 66 for _, node := range nodes { 67 nodeMapping[node.UID] = node 68 if len(node.ParentRefs) > 0 { 69 _, ok := mapParentToChild[node.ParentRefs[0].UID] 70 if !ok { 71 var temp []string 72 mapParentToChild[node.ParentRefs[0].UID] = temp 73 } 74 mapParentToChild[node.ParentRefs[0].UID] = append(mapParentToChild[node.ParentRefs[0].UID], node.UID) 75 } else { 76 parentNode[node.UID] = struct{}{} 77 } 78 } 79 buf := &bytes.Buffer{} 80 w := tabwriter.NewWriter(buf, 0, 0, 2, ' ', 0) 81 82 printDetailedTreeViewAppResourcesNotOrphaned(nodeMapping, mapParentToChild, parentNode, w) 83 require.NoError(t, w.Flush()) 84 output := buf.String() 85 86 assert.Contains(t, output, "Rollout") 87 assert.Contains(t, output, "Degraded") 88 assert.Contains(t, output, "Readiness Gate failed") 89 } 90 91 func TestPrintResourcesTree(t *testing.T) { 92 tree := v1alpha1.ApplicationTree{ 93 Nodes: []v1alpha1.ResourceNode{ 94 { 95 ResourceRef: v1alpha1.ResourceRef{ 96 Group: "group", 97 Kind: "kind", 98 Namespace: "ns", 99 Name: "rs1", 100 }, 101 }, 102 }, 103 OrphanedNodes: []v1alpha1.ResourceNode{ 104 { 105 ResourceRef: v1alpha1.ResourceRef{ 106 Group: "group2", 107 Kind: "kind2", 108 Namespace: "ns2", 109 Name: "rs2", 110 }, 111 }, 112 }, 113 } 114 output, _ := captureOutput(func() error { 115 printResources(true, false, &tree, "") 116 return nil 117 }) 118 119 expectation := "GROUP KIND NAMESPACE NAME ORPHANED\ngroup kind ns rs1 No\ngroup2 kind2 ns2 rs2 Yes\n" 120 121 assert.Equal(t, expectation, output) 122 } 123 124 func TestFilterFieldsFromObject(t *testing.T) { 125 tests := []struct { 126 name string 127 obj unstructured.Unstructured 128 filteredFields []string 129 expectedFields []string 130 unexpectedFields []string 131 }{ 132 { 133 name: "filter nested field", 134 obj: unstructured.Unstructured{ 135 Object: map[string]any{ 136 "apiVersion": "vX", 137 "kind": "kind", 138 "metadata": map[string]any{ 139 "name": "test", 140 }, 141 "spec": map[string]any{ 142 "testfield": map[string]any{ 143 "nestedtest": "test", 144 }, 145 "testfield2": "test", 146 }, 147 }, 148 }, 149 filteredFields: []string{"spec.testfield.nestedtest"}, 150 expectedFields: []string{"spec.testfield.nestedtest"}, 151 unexpectedFields: []string{"spec.testfield2"}, 152 }, 153 { 154 name: "filter multiple fields", 155 obj: unstructured.Unstructured{ 156 Object: map[string]any{ 157 "apiVersion": "vX", 158 "kind": "kind", 159 "metadata": map[string]any{ 160 "name": "test", 161 }, 162 "spec": map[string]any{ 163 "testfield": map[string]any{ 164 "nestedtest": "test", 165 }, 166 "testfield2": "test", 167 "testfield3": "deleteme", 168 }, 169 }, 170 }, 171 filteredFields: []string{"spec.testfield.nestedtest", "spec.testfield3"}, 172 expectedFields: []string{"spec.testfield.nestedtest"}, 173 unexpectedFields: []string{"spec.testfield2"}, 174 }, 175 { 176 name: "filter nested list object", 177 obj: unstructured.Unstructured{ 178 Object: map[string]any{ 179 "apiVersion": "vX", 180 "kind": "kind", 181 "metadata": map[string]any{ 182 "name": "test", 183 }, 184 "spec": map[string]any{ 185 "testfield": map[string]any{ 186 "nestedtest": "test", 187 }, 188 "testfield2": "test", 189 }, 190 }, 191 }, 192 filteredFields: []string{"spec.testfield.nestedtest"}, 193 expectedFields: []string{"spec.testfield.nestedtest"}, 194 unexpectedFields: []string{"spec.testfield2"}, 195 }, 196 } 197 198 for _, tt := range tests { 199 t.Run(tt.name, func(t *testing.T) { 200 tt.obj.SetName("test-object") 201 202 filtered := filterFieldsFromObject(&tt.obj, tt.filteredFields) 203 204 for _, field := range tt.expectedFields { 205 fieldPath := strings.Split(field, ".") 206 _, exists, err := unstructured.NestedFieldCopy(filtered.Object, fieldPath...) 207 require.NoError(t, err) 208 assert.True(t, exists, "Expected field %s to exist", field) 209 } 210 211 for _, field := range tt.unexpectedFields { 212 fieldPath := strings.Split(field, ".") 213 _, exists, err := unstructured.NestedFieldCopy(filtered.Object, fieldPath...) 214 require.NoError(t, err) 215 assert.False(t, exists, "Expected field %s to not exist", field) 216 } 217 218 assert.Equal(t, tt.obj.GetName(), filtered.GetName()) 219 }) 220 } 221 } 222 223 func TestExtractNestedItem(t *testing.T) { 224 tests := []struct { 225 name string 226 obj map[string]any 227 fields []string 228 depth int 229 expected map[string]any 230 }{ 231 { 232 name: "extract simple nested item", 233 obj: map[string]any{ 234 "listofitems": []any{ 235 map[string]any{ 236 "extract": "123", 237 "dontextract": "abc", 238 }, 239 map[string]any{ 240 "extract": "456", 241 "dontextract": "def", 242 }, 243 map[string]any{ 244 "extract": "789", 245 "dontextract": "ghi", 246 }, 247 }, 248 }, 249 fields: []string{"listofitems", "extract"}, 250 depth: 0, 251 expected: map[string]any{ 252 "listofitems": []any{ 253 map[string]any{ 254 "extract": "123", 255 }, 256 map[string]any{ 257 "extract": "456", 258 }, 259 map[string]any{ 260 "extract": "789", 261 }, 262 }, 263 }, 264 }, 265 { 266 name: "double nested list of objects", 267 obj: map[string]any{ 268 "listofitems": []any{ 269 map[string]any{ 270 "doublenested": []any{ 271 map[string]any{ 272 "extract": "123", 273 }, 274 }, 275 "dontextract": "abc", 276 }, 277 map[string]any{ 278 "doublenested": []any{ 279 map[string]any{ 280 "extract": "456", 281 }, 282 }, 283 "dontextract": "def", 284 }, 285 map[string]any{ 286 "doublenested": []any{ 287 map[string]any{ 288 "extract": "789", 289 }, 290 }, 291 "dontextract": "ghi", 292 }, 293 }, 294 }, 295 fields: []string{"listofitems", "doublenested", "extract"}, 296 depth: 0, 297 expected: map[string]any{ 298 "listofitems": []any{ 299 map[string]any{ 300 "doublenested": []any{ 301 map[string]any{ 302 "extract": "123", 303 }, 304 }, 305 }, 306 map[string]any{ 307 "doublenested": []any{ 308 map[string]any{ 309 "extract": "456", 310 }, 311 }, 312 }, 313 map[string]any{ 314 "doublenested": []any{ 315 map[string]any{ 316 "extract": "789", 317 }, 318 }, 319 }, 320 }, 321 }, 322 }, 323 { 324 name: "depth is greater then list of field size", 325 obj: map[string]any{"test1": "1234567890"}, 326 fields: []string{"test1"}, 327 depth: 4, 328 expected: nil, 329 }, 330 } 331 332 for _, tt := range tests { 333 t.Run(tt.name, func(t *testing.T) { 334 filteredObj := extractNestedItem(tt.obj, tt.fields, tt.depth) 335 assert.Equal(t, tt.expected, filteredObj, "Did not get the correct filtered obj") 336 }) 337 } 338 } 339 340 func TestExtractItemsFromList(t *testing.T) { 341 tests := []struct { 342 name string 343 list []any 344 fields []string 345 expected []any 346 }{ 347 { 348 name: "test simple field", 349 list: []any{ 350 map[string]any{"extract": "value1", "dontextract": "valueA"}, 351 map[string]any{"extract": "value2", "dontextract": "valueB"}, 352 map[string]any{"extract": "value3", "dontextract": "valueC"}, 353 }, 354 fields: []string{"extract"}, 355 expected: []any{ 356 map[string]any{"extract": "value1"}, 357 map[string]any{"extract": "value2"}, 358 map[string]any{"extract": "value3"}, 359 }, 360 }, 361 { 362 name: "test simple field with some depth", 363 list: []any{ 364 map[string]any{ 365 "test1": map[string]any{ 366 "test2": map[string]any{ 367 "extract": "123", 368 "dontextract": "abc", 369 }, 370 }, 371 }, 372 map[string]any{ 373 "test1": map[string]any{ 374 "test2": map[string]any{ 375 "extract": "456", 376 "dontextract": "def", 377 }, 378 }, 379 }, 380 map[string]any{ 381 "test1": map[string]any{ 382 "test2": map[string]any{ 383 "extract": "789", 384 "dontextract": "ghi", 385 }, 386 }, 387 }, 388 }, 389 fields: []string{"test1", "test2", "extract"}, 390 expected: []any{ 391 map[string]any{ 392 "test1": map[string]any{ 393 "test2": map[string]any{ 394 "extract": "123", 395 }, 396 }, 397 }, 398 map[string]any{ 399 "test1": map[string]any{ 400 "test2": map[string]any{ 401 "extract": "456", 402 }, 403 }, 404 }, 405 map[string]any{ 406 "test1": map[string]any{ 407 "test2": map[string]any{ 408 "extract": "789", 409 }, 410 }, 411 }, 412 }, 413 }, 414 { 415 name: "test a missing field", 416 list: []any{ 417 map[string]any{"test1": "123"}, 418 map[string]any{"test1": "456"}, 419 map[string]any{"test1": "789"}, 420 }, 421 fields: []string{"test2"}, 422 expected: nil, 423 }, 424 { 425 name: "test getting an object", 426 list: []any{ 427 map[string]any{ 428 "extract": map[string]any{ 429 "keyA": "valueA", 430 "keyB": "valueB", 431 "keyC": "valueC", 432 }, 433 "dontextract": map[string]any{ 434 "key1": "value1", 435 "key2": "value2", 436 "key3": "value3", 437 }, 438 }, 439 map[string]any{ 440 "extract": map[string]any{ 441 "keyD": "valueD", 442 "keyE": "valueE", 443 "keyF": "valueF", 444 }, 445 "dontextract": map[string]any{ 446 "key4": "value4", 447 "key5": "value5", 448 "key6": "value6", 449 }, 450 }, 451 }, 452 fields: []string{"extract"}, 453 expected: []any{ 454 map[string]any{ 455 "extract": map[string]any{ 456 "keyA": "valueA", 457 "keyB": "valueB", 458 "keyC": "valueC", 459 }, 460 }, 461 map[string]any{ 462 "extract": map[string]any{ 463 "keyD": "valueD", 464 "keyE": "valueE", 465 "keyF": "valueF", 466 }, 467 }, 468 }, 469 }, 470 } 471 472 for _, tt := range tests { 473 t.Run(tt.name, func(t *testing.T) { 474 extractedList := extractItemsFromList(tt.list, tt.fields) 475 assert.Equal(t, tt.expected, extractedList, "Lists were not equal") 476 }) 477 } 478 } 479 480 func TestReconstructObject(t *testing.T) { 481 tests := []struct { 482 name string 483 extracted []any 484 fields []string 485 depth int 486 expected map[string]any 487 }{ 488 { 489 name: "simple single field at depth 0", 490 extracted: []any{"value1", "value2"}, 491 fields: []string{"items"}, 492 depth: 0, 493 expected: map[string]any{ 494 "items": []any{"value1", "value2"}, 495 }, 496 }, 497 { 498 name: "object nested at depth 1", 499 extracted: []any{map[string]any{"key": "value"}}, 500 fields: []string{"test1", "test2"}, 501 depth: 1, 502 expected: map[string]any{ 503 "test1": map[string]any{ 504 "test2": []any{map[string]any{"key": "value"}}, 505 }, 506 }, 507 }, 508 { 509 name: "empty list of extracted items", 510 extracted: []any{}, 511 fields: []string{"test1"}, 512 depth: 0, 513 expected: map[string]any{ 514 "test1": []any{}, 515 }, 516 }, 517 { 518 name: "complex object nesteed at depth 2", 519 extracted: []any{map[string]any{ 520 "obj1": map[string]any{ 521 "key1": "value1", 522 "key2": "value2", 523 }, 524 "obj2": map[string]any{ 525 "keyA": "valueA", 526 "keyB": "valueB", 527 }, 528 }}, 529 fields: []string{"test1", "test2", "test3"}, 530 depth: 2, 531 expected: map[string]any{ 532 "test1": map[string]any{ 533 "test2": map[string]any{ 534 "test3": []any{ 535 map[string]any{ 536 "obj1": map[string]any{ 537 "key1": "value1", 538 "key2": "value2", 539 }, 540 "obj2": map[string]any{ 541 "keyA": "valueA", 542 "keyB": "valueB", 543 }, 544 }, 545 }, 546 }, 547 }, 548 }, 549 }, 550 } 551 552 for _, tt := range tests { 553 t.Run(tt.name, func(t *testing.T) { 554 filteredObj := reconstructObject(tt.extracted, tt.fields, tt.depth) 555 assert.Equal(t, tt.expected, filteredObj, "objects were not equal") 556 }) 557 } 558 } 559 560 func TestPrintManifests(t *testing.T) { 561 obj := unstructured.Unstructured{ 562 Object: map[string]any{ 563 "apiVersion": "vX", 564 "kind": "test", 565 "metadata": map[string]any{ 566 "name": "unit-test", 567 }, 568 "spec": map[string]any{ 569 "testfield": "testvalue", 570 }, 571 }, 572 } 573 574 expectedYAML := `apiVersion: vX 575 kind: test 576 metadata: 577 name: unit-test 578 spec: 579 testfield: testvalue 580 ` 581 582 output, _ := captureOutput(func() error { 583 printManifests(&[]unstructured.Unstructured{obj}, false, true, "yaml") 584 return nil 585 }) 586 assert.Equal(t, expectedYAML+"\n", output, "Incorrect yaml output for printManifests") 587 588 output, _ = captureOutput(func() error { 589 printManifests(&[]unstructured.Unstructured{obj, obj}, false, true, "yaml") 590 return nil 591 }) 592 assert.Equal(t, expectedYAML+"\n---\n"+expectedYAML+"\n", output, "Incorrect yaml output with multiple objs.") 593 594 expectedJSON := `{ 595 "apiVersion": "vX", 596 "kind": "test", 597 "metadata": { 598 "name": "unit-test" 599 }, 600 "spec": { 601 "testfield": "testvalue" 602 } 603 }` 604 605 output, _ = captureOutput(func() error { 606 printManifests(&[]unstructured.Unstructured{obj}, false, true, "json") 607 return nil 608 }) 609 assert.Equal(t, expectedJSON+"\n", output, "Incorrect json output.") 610 611 output, _ = captureOutput(func() error { 612 printManifests(&[]unstructured.Unstructured{obj, obj}, false, true, "json") 613 return nil 614 }) 615 assert.Equal(t, expectedJSON+"\n---\n"+expectedJSON+"\n", output, "Incorrect json output with multiple objs.") 616 617 output, _ = captureOutput(func() error { 618 printManifests(&[]unstructured.Unstructured{obj}, true, true, "wide") 619 return nil 620 }) 621 assert.Contains(t, output, "FIELD RESOURCE NAME VALUE", "Missing or incorrect header line for table print with showing names.") 622 assert.Contains(t, output, "apiVersion unit-test vX", "Missing or incorrect row in table related to apiVersion with showing names.") 623 assert.Contains(t, output, "kind unit-test test", "Missing or incorrect line in the table related to kind with showing names.") 624 assert.Contains(t, output, "spec.testfield unit-test testvalue", "Missing or incorrect line in the table related to spec.testfield with showing names.") 625 assert.NotContains(t, output, "metadata.name unit-test testvalue", "Missing or incorrect line in the table related to metadata.name with showing names.") 626 627 output, _ = captureOutput(func() error { 628 printManifests(&[]unstructured.Unstructured{obj}, true, false, "wide") 629 return nil 630 }) 631 assert.Contains(t, output, "FIELD VALUE", "Missing or incorrect header line for table print with not showing names.") 632 assert.Contains(t, output, "apiVersion vX", "Missing or incorrect row in table related to apiVersion with not showing names.") 633 assert.Contains(t, output, "kind test", "Missing or incorrect row in the table related to kind with not showing names.") 634 assert.Contains(t, output, "spec.testfield testvalue", "Missing or incorrect row in the table related to spec.testefield with not showing names.") 635 assert.NotContains(t, output, "metadata.name testvalue", "Missing or incorrect row in the tbale related to metadata.name with not showing names.") 636 } 637 638 func TestPrintManifests_FilterNestedListObject_Wide(t *testing.T) { 639 obj := unstructured.Unstructured{ 640 Object: map[string]any{ 641 "apiVersion": "vX", 642 "kind": "kind", 643 "metadata": map[string]any{ 644 "name": "unit-test", 645 }, 646 "status": map[string]any{ 647 "podIPs": []map[string]any{ 648 { 649 "IP": "127.0.0.1", 650 }, 651 { 652 "IP": "127.0.0.2", 653 }, 654 }, 655 }, 656 }, 657 } 658 659 output, _ := captureOutput(func() error { 660 v, err := json.Marshal(&obj) 661 if err != nil { 662 return nil 663 } 664 665 var obj2 *unstructured.Unstructured 666 err = json.Unmarshal([]byte(v), &obj2) 667 if err != nil { 668 return nil 669 } 670 printManifests(&[]unstructured.Unstructured{*obj2}, false, true, "wide") 671 return nil 672 }) 673 674 // Verify table header 675 assert.Contains(t, output, "FIELD RESOURCE NAME VALUE", "Missing a line in the table") 676 assert.Contains(t, output, "apiVersion unit-test vX", "Test for apiVersion field failed for wide output") 677 assert.Contains(t, output, "kind unit-test kind", "Test for kind field failed for wide output") 678 assert.Contains(t, output, "status.podIPs[0].IP unit-test 127.0.0.1", "Test for podIP array index 0 field failed for wide output") 679 assert.Contains(t, output, "status.podIPs[1].IP unit-test 127.0.0.2", "Test for podIP array index 1 field failed for wide output") 680 }