@@ -3,12 +3,15 @@ package jobs
33import (
44 "bytes"
55 "context"
6+ "encoding/json"
67 "fmt"
78 "io"
89 "log"
10+ "path/filepath"
911 "strings"
1012 "testing"
1113
14+ "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds"
1215 "github.com/nginxinc/nginx-k8s-supportpkg/pkg/data_collector"
1316 "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock"
1417 "github.com/stretchr/testify/assert"
@@ -352,3 +355,367 @@ func TestNGFJobList_PodListError_LogFormat(t *testing.T) {
352355 assert .Contains (t , logContent , "test-ns" )
353356 assert .Contains (t , logContent , "specific error message for testing" )
354357}
358+
359+ func TestNGFJobList_PodListFailure (t * testing.T ) {
360+ tests := []struct {
361+ name string
362+ jobName string
363+ jobIndex int
364+ }{
365+ {
366+ name : "exec-nginx-gateway-version pod list failure" ,
367+ jobName : "exec-nginx-gateway-version" ,
368+ jobIndex : 0 ,
369+ },
370+ {
371+ name : "exec-nginx-t pod list failure" ,
372+ jobName : "exec-nginx-t" ,
373+ jobIndex : 1 ,
374+ },
375+ }
376+
377+ for _ , tt := range tests {
378+ t .Run (tt .name , func (t * testing.T ) {
379+ tmpDir := t .TempDir ()
380+ var logOutput bytes.Buffer
381+
382+ // Create a fake client that will return an error for pod listing
383+ client := fake .NewSimpleClientset ()
384+ client .PrependReactor ("list" , "pods" , func (action k8stesting.Action ) (handled bool , ret runtime.Object , err error ) {
385+ return true , nil , fmt .Errorf ("failed to retrieve pod list" )
386+ })
387+
388+ dc := & data_collector.DataCollector {
389+ BaseDir : tmpDir ,
390+ Namespaces : []string {"default" , "nginx-gateway" },
391+ Logger : log .New (& logOutput , "" , 0 ),
392+ K8sCoreClientSet : client ,
393+ PodExecutor : func (namespace , podName , containerName string , command []string , ctx context.Context ) ([]byte , error ) {
394+ return []byte ("mock output" ), nil
395+ },
396+ }
397+
398+ // Get the specific job
399+ jobs := NGFJobList ()
400+ job := jobs [tt .jobIndex ]
401+ assert .Equal (t , tt .jobName , job .Name , "Job name should match expected" )
402+
403+ // Execute the job
404+ ctx := context .Background ()
405+ ch := make (chan JobResult , 1 )
406+ job .Execute (dc , ctx , ch )
407+
408+ result := <- ch
409+ logContent := logOutput .String ()
410+
411+ // Verify the error was logged for each namespace
412+ assert .Contains (t , logContent , "Could not retrieve pod list for namespace default: failed to retrieve pod list" )
413+ assert .Contains (t , logContent , "Could not retrieve pod list for namespace nginx-gateway: failed to retrieve pod list" )
414+
415+ // Verify no files were created since pod listing failed
416+ assert .Empty (t , result .Files , "No files should be created when pod list fails" )
417+ assert .Nil (t , result .Error , "Job should not fail, just log the error" )
418+ })
419+ }
420+ }
421+
422+ func TestNGFJobList_PodListFailure_MultipleNamespaces (t * testing.T ) {
423+ tmpDir := t .TempDir ()
424+ var logOutput bytes.Buffer
425+
426+ // Create a fake client that returns different errors for different namespaces
427+ client := fake .NewSimpleClientset ()
428+ client .PrependReactor ("list" , "pods" , func (action k8stesting.Action ) (handled bool , ret runtime.Object , err error ) {
429+ listAction := action .(k8stesting.ListAction )
430+ namespace := listAction .GetNamespace ()
431+
432+ switch namespace {
433+ case "error-ns1" :
434+ return true , nil , fmt .Errorf ("network timeout" )
435+ case "error-ns2" :
436+ return true , nil , fmt .Errorf ("permission denied" )
437+ case "error-ns3" :
438+ return true , nil , fmt .Errorf ("resource not found" )
439+ default :
440+ // Let other namespaces succeed (but with no nginx-gateway pods)
441+ return false , nil , nil
442+ }
443+ })
444+
445+ dc := & data_collector.DataCollector {
446+ BaseDir : tmpDir ,
447+ Namespaces : []string {"error-ns1" , "error-ns2" , "error-ns3" , "success-ns" },
448+ Logger : log .New (& logOutput , "" , 0 ),
449+ K8sCoreClientSet : client ,
450+ PodExecutor : func (namespace , podName , containerName string , command []string , ctx context.Context ) ([]byte , error ) {
451+ return []byte ("mock output" ), nil
452+ },
453+ }
454+
455+ // Test both jobs that have the same error handling pattern
456+ jobs := NGFJobList ()
457+
458+ for _ , jobName := range []string {"exec-nginx-gateway-version" , "exec-nginx-t" } {
459+ t .Run (jobName , func (t * testing.T ) {
460+ var targetJob Job
461+ for _ , job := range jobs {
462+ if job .Name == jobName {
463+ targetJob = job
464+ break
465+ }
466+ }
467+
468+ // Clear log output for this subtest
469+ logOutput .Reset ()
470+
471+ ctx := context .Background ()
472+ ch := make (chan JobResult , 1 )
473+ targetJob .Execute (dc , ctx , ch )
474+
475+ result := <- ch
476+ logContent := logOutput .String ()
477+
478+ // Verify errors are logged for the failing namespaces
479+ assert .Contains (t , logContent , "Could not retrieve pod list for namespace error-ns1: network timeout" )
480+ assert .Contains (t , logContent , "Could not retrieve pod list for namespace error-ns2: permission denied" )
481+ assert .Contains (t , logContent , "Could not retrieve pod list for namespace error-ns3: resource not found" )
482+
483+ // success-ns should not have error logs
484+ assert .NotContains (t , logContent , "Could not retrieve pod list for namespace success-ns" )
485+
486+ // No files should be created since no nginx-gateway pods exist in success-ns
487+ assert .Empty (t , result .Files )
488+ assert .Nil (t , result .Error )
489+ })
490+ }
491+ }
492+
493+ func TestNGFJobList_CommandExecutionFailure (t * testing.T ) {
494+ tests := []struct {
495+ name string
496+ jobName string
497+ jobIndex int
498+ expectedCommand []string
499+ expectedContainer string
500+ expectedFileExt string
501+ }{
502+ {
503+ name : "exec-nginx-gateway-version command failure" ,
504+ jobName : "exec-nginx-gateway-version" ,
505+ jobIndex : 0 ,
506+ expectedCommand : []string {"/usr/bin/gateway" , "--help" },
507+ expectedContainer : "nginx-gateway" ,
508+ expectedFileExt : "__nginx-gateway-version.txt" ,
509+ },
510+ {
511+ name : "exec-nginx-t command failure" ,
512+ jobName : "exec-nginx-t" ,
513+ jobIndex : 1 ,
514+ expectedCommand : []string {"/usr/sbin/nginx" , "-T" },
515+ expectedContainer : "nginx" ,
516+ expectedFileExt : "__nginx-t.txt" ,
517+ },
518+ }
519+
520+ for _ , tt := range tests {
521+ t .Run (tt .name , func (t * testing.T ) {
522+ tmpDir := t .TempDir ()
523+ var logOutput bytes.Buffer
524+
525+ // Create nginx-gateway pod for testing
526+ nginxGatewayPod := & corev1.Pod {
527+ ObjectMeta : metav1.ObjectMeta {
528+ Name : "nginx-gateway-deployment-123" ,
529+ Namespace : "default" ,
530+ },
531+ Spec : corev1.PodSpec {
532+ Containers : []corev1.Container {
533+ {Name : "nginx-gateway" , Image : "nginx-gateway:latest" },
534+ {Name : "nginx" , Image : "nginx:latest" },
535+ },
536+ },
537+ }
538+
539+ client := fake .NewSimpleClientset (nginxGatewayPod )
540+
541+ dc := & data_collector.DataCollector {
542+ BaseDir : tmpDir ,
543+ Namespaces : []string {"default" },
544+ Logger : log .New (& logOutput , "" , 0 ),
545+ K8sCoreClientSet : client ,
546+ PodExecutor : func (namespace , podName , containerName string , command []string , ctx context.Context ) ([]byte , error ) {
547+ // Verify correct parameters are passed
548+ assert .Equal (t , "default" , namespace )
549+ assert .Equal (t , "nginx-gateway-deployment-123" , podName )
550+ assert .Equal (t , tt .expectedContainer , containerName )
551+ assert .Equal (t , tt .expectedCommand , command )
552+
553+ // Return error to test failure path
554+ return nil , fmt .Errorf ("command execution failed: %v" , command )
555+ },
556+ }
557+
558+ // Execute the specific job
559+ jobs := NGFJobList ()
560+ job := jobs [tt .jobIndex ]
561+ assert .Equal (t , tt .jobName , job .Name )
562+
563+ ctx := context .Background ()
564+ ch := make (chan JobResult , 1 )
565+ job .Execute (dc , ctx , ch )
566+
567+ result := <- ch
568+ logContent := logOutput .String ()
569+
570+ // Verify the error was set
571+ assert .NotNil (t , result .Error , "Job should have error when command execution fails" )
572+ assert .Contains (t , result .Error .Error (), "command execution failed" )
573+
574+ // Verify the error was logged
575+ expectedLogMessage := fmt .Sprintf ("Command execution %s failed for pod nginx-gateway-deployment-123 in namespace default" , tt .expectedCommand )
576+ assert .Contains (t , logContent , expectedLogMessage )
577+ assert .Contains (t , logContent , "command execution failed" )
578+
579+ // Verify no files were created when command execution fails
580+ assert .Empty (t , result .Files , "No files should be created when command execution fails" )
581+ })
582+ }
583+ }
584+
585+ func TestNGFJobList_CRDObjects_Success (t * testing.T ) {
586+ tmpDir := t .TempDir ()
587+ var logOutput bytes.Buffer
588+
589+ dc := & data_collector.DataCollector {
590+ BaseDir : tmpDir ,
591+ Namespaces : []string {"default" , "nginx-gateway" },
592+ Logger : log .New (& logOutput , "" , 0 ),
593+ QueryCRD : func (crd crds.Crd , namespace string , ctx context.Context ) ([]byte , error ) {
594+ // Mock successful CRD query
595+ mockData := map [string ]interface {}{
596+ "apiVersion" : crd .Group + "/" + crd .Version ,
597+ "kind" : crd .Resource ,
598+ "items" : []map [string ]interface {}{
599+ {
600+ "metadata" : map [string ]interface {}{
601+ "name" : "test-" + crd .Resource ,
602+ "namespace" : namespace ,
603+ },
604+ "spec" : map [string ]interface {}{
605+ "host" : "example.com" ,
606+ },
607+ },
608+ },
609+ }
610+ return json .Marshal (mockData )
611+ },
612+ }
613+
614+ // Get the crd-objects job
615+ jobs := NGFJobList ()
616+ crdJob := jobs [2 ] // crd-objects is at index 2
617+ assert .Equal (t , "crd-objects" , crdJob .Name )
618+
619+ ctx := context .Background ()
620+ ch := make (chan JobResult , 1 )
621+ crdJob .Execute (dc , ctx , ch )
622+
623+ result := <- ch
624+
625+ // Verify no errors
626+ assert .Nil (t , result .Error , "Should not have errors for successful CRD collection" )
627+
628+ // Get the expected CRDs from GetNGFCRDList()
629+ expectedCRDs := crds .GetNGFCRDList ()
630+ expectedFileCount := len (expectedCRDs ) * len (dc .Namespaces )
631+
632+ // Verify expected number of files created
633+ assert .Len (t , result .Files , expectedFileCount ,
634+ "Should create files for each CRD in each namespace" )
635+
636+ // Verify file paths and content for each CRD and namespace
637+ for _ , namespace := range dc .Namespaces {
638+ for _ , crd := range expectedCRDs {
639+ expectedPath := filepath .Join (tmpDir , "crds" , namespace , crd .Resource + ".json" )
640+ content , exists := result .Files [expectedPath ]
641+
642+ assert .True (t , exists , "File should exist for CRD %s in namespace %s" , crd .Resource , namespace )
643+ assert .NotEmpty (t , content , "File content should not be empty" )
644+
645+ // Verify JSON structure
646+ var jsonData map [string ]interface {}
647+ err := json .Unmarshal (content , & jsonData )
648+ assert .NoError (t , err , "Content should be valid JSON" )
649+ assert .Contains (t , jsonData , "items" , "Should contain items field" )
650+ }
651+ }
652+
653+ // Verify no error messages in logs
654+ logContent := logOutput .String ()
655+ assert .NotContains (t , logContent , "could not be collected" , "Should not have CRD collection errors" )
656+ }
657+
658+ func TestNGFJobList_CRDObjects_QueryFailure (t * testing.T ) {
659+ tmpDir := t .TempDir ()
660+ var logOutput bytes.Buffer
661+
662+ dc := & data_collector.DataCollector {
663+ BaseDir : tmpDir ,
664+ Namespaces : []string {"default" , "test-ns" },
665+ Logger : log .New (& logOutput , "" , 0 ),
666+ QueryCRD : func (crd crds.Crd , namespace string , ctx context.Context ) ([]byte , error ) {
667+ // Return different errors based on CRD and namespace
668+ if namespace == "test-ns" && crd .Resource == "nginxgateways" {
669+ return nil , fmt .Errorf ("permission denied" )
670+ }
671+ if namespace == "default" && crd .Resource == "clientsettingspolicies" {
672+ return nil , fmt .Errorf ("resource not found" )
673+ }
674+
675+ // Success for other combinations
676+ mockData := map [string ]interface {}{
677+ "apiVersion" : crd .Group + "/" + crd .Version ,
678+ "kind" : crd .Resource ,
679+ "items" : []interface {}{},
680+ }
681+ return json .Marshal (mockData )
682+ },
683+ }
684+
685+ jobs := NGFJobList ()
686+ crdJob := jobs [2 ]
687+
688+ ctx := context .Background ()
689+ ch := make (chan JobResult , 1 )
690+ crdJob .Execute (dc , ctx , ch )
691+
692+ result := <- ch
693+ logContent := logOutput .String ()
694+
695+ // Verify error logging for specific failures
696+ assert .Contains (t , logContent , "CRD nginxgateways.gateway.nginx.org/v1alpha1 could not be collected in namespace test-ns: permission denied" )
697+ assert .Contains (t , logContent , "CRD clientsettingspolicies.gateway.nginx.org/v1alpha1 could not be collected in namespace default: resource not found" )
698+
699+ // Verify successful CRDs still created files (only failures are logged)
700+ expectedCRDs := crds .GetNGFCRDList ()
701+ successfulFiles := 0
702+
703+ for _ , namespace := range dc .Namespaces {
704+ for _ , crd := range expectedCRDs {
705+ // Skip the ones we know should fail
706+ if (namespace == "test-ns" && crd .Resource == "gateways" ) ||
707+ (namespace == "default" && crd .Resource == "httproutes" ) {
708+ continue
709+ }
710+
711+ expectedPath := filepath .Join (tmpDir , "crds" , namespace , crd .Resource + ".json" )
712+ _ , exists := result .Files [expectedPath ]
713+ if exists {
714+ successfulFiles ++
715+ }
716+ }
717+ }
718+
719+ assert .Greater (t , successfulFiles , 0 , "Should have some successful CRD files" )
720+ assert .Nil (t , result .Error , "Job should not fail even if some CRDs fail to collect" )
721+ }
0 commit comments