Skip to content

Commit c684282

Browse files
committed
Review: Add tests for failure paths when querying the k8s API for NGF job list
1 parent f757b84 commit c684282

File tree

1 file changed

+367
-0
lines changed

1 file changed

+367
-0
lines changed

pkg/jobs/ngf_job_list_test.go

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package jobs
33
import (
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

Comments
 (0)