Skip to content

Commit 15c3932

Browse files
authored
Merge pull request #192 from nginx/mrajagopal-unit-tests
Implement unit-tests for the codebase
2 parents 39e33e7 + fc8e92c commit 15c3932

17 files changed

+3637
-15
lines changed

.github/workflows/unit-tests.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Unit Tests
2+
permissions:
3+
contents: read
4+
5+
on:
6+
push:
7+
8+
jobs:
9+
unit-tests:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- name: Checkout code
14+
uses: actions/checkout@v5
15+
16+
- name: Set up Go
17+
uses: actions/setup-go@v6
18+
with:
19+
go-version: "1.24"
20+
21+
- name: Create fake kube config
22+
run: |
23+
mkdir -p /home/runner/.kube
24+
cat <<EOF > /home/runner/.kube/config
25+
apiVersion: v1
26+
kind: Config
27+
preferences: {}
28+
clusters:
29+
- cluster:
30+
server: https://fake-server
31+
certificate-authority-data: FAKE
32+
name: fake-cluster
33+
contexts:
34+
- context:
35+
cluster: fake-cluster
36+
user: fake-user
37+
name: fake-context
38+
current-context: fake-context
39+
users:
40+
- name: fake-user
41+
user:
42+
token: FAKE
43+
EOF
44+
- name: Run Unit Tests
45+
run: |
46+
go clean -testcache && go test -v ./... -coverprofile=coverage.out
47+
go tool cover -func=coverage.out
48+
go tool cover -html=coverage.out -o coverage.html

Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,12 @@ nginx-utils:
66
docker buildx build --build-context project=nginx-utils --platform linux/amd64 -t nginx-utils -f nginx-utils/Dockerfile .
77

88
install: build
9-
sudo cp cmd/kubectl-nginx_supportpkg /usr/local/bin
9+
sudo cp cmd/kubectl-nginx_supportpkg /usr/local/bin
10+
11+
clean:
12+
rm -f cmd/kubectl-nginx_supportpkg
13+
14+
test:
15+
go clean -testcache && go test -v ./... -coverprofile=coverage.out
16+
go tool cover -func=coverage.out
17+
go tool cover -html=coverage.out -o coverage.html

go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ module github.com/nginxinc/nginx-k8s-supportpkg
33
go 1.24.3
44

55
require (
6-
github.com/mittwald/go-helm-client v0.12.17
6+
github.com/mittwald/go-helm-client v0.12.18
77
github.com/spf13/cobra v1.10.1
8+
github.com/stretchr/testify v1.10.0
9+
go.uber.org/mock v0.5.0
10+
helm.sh/helm/v3 v3.18.5
811
k8s.io/client-go v0.34.0
912
)
1013

@@ -90,7 +93,6 @@ require (
9093
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
9194
google.golang.org/grpc v1.72.1 // indirect
9295
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
93-
helm.sh/helm/v3 v3.18.5 // indirect
9496
k8s.io/apiserver v0.34.0 // indirect
9597
k8s.io/cli-runtime v0.33.3 // indirect
9698
k8s.io/component-base v0.34.0 // indirect
@@ -136,5 +138,5 @@ require (
136138
k8s.io/metrics v0.33.3
137139
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
138140
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
139-
sigs.k8s.io/yaml v1.6.0 // indirect
141+
sigs.k8s.io/yaml v1.6.0
140142
)

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
189189
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
190190
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
191191
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
192-
github.com/mittwald/go-helm-client v0.12.17 h1:PncoE1u3fXuHWLineNDQ4hI5J4uVbMW3JWrtdBR86TI=
193-
github.com/mittwald/go-helm-client v0.12.17/go.mod h1:GQxuPspUcMsxWWDtYzjRdxOAjh3LKADIfgqtUf9mjHk=
192+
github.com/mittwald/go-helm-client v0.12.18 h1:i9cJNv/YC3ZPKUKVNYTlrOO7ZO6YFKE/ak3J5TeYHPU=
193+
github.com/mittwald/go-helm-client v0.12.18/go.mod h1:dLl5NkdKCvwKvLIdZzg4MDbxhSKmuimdmM3WpsAzS0I=
194194
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
195195
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
196196
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
@@ -326,6 +326,8 @@ go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9f
326326
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
327327
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
328328
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
329+
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
330+
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
329331
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
330332
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
331333
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=

pkg/data_collector/data_collector.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
helmClient "github.com/mittwald/go-helm-client"
3636
"github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds"
3737
corev1 "k8s.io/api/core/v1"
38+
apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
3839
crdClient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
3940
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4041
"k8s.io/apimachinery/pkg/runtime/schema"
@@ -53,12 +54,14 @@ type DataCollector struct {
5354
Logger *log.Logger
5455
LogFile *os.File
5556
K8sRestConfig *rest.Config
56-
K8sCoreClientSet *kubernetes.Clientset
57-
K8sCrdClientSet *crdClient.Clientset
58-
K8sMetricsClientSet *metricsClient.Clientset
57+
K8sCoreClientSet kubernetes.Interface
58+
K8sCrdClientSet apiextensionsclientset.Interface
59+
K8sMetricsClientSet metricsClient.Interface
5960
K8sHelmClientSet map[string]helmClient.Client
6061
ExcludeDBData bool
6162
ExcludeTimeSeriesData bool
63+
PodExecutor func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error)
64+
QueryCRD func(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error)
6265
}
6366

6467
type Manifest struct {
@@ -148,6 +151,8 @@ func NewDataCollector(collector *DataCollector) error {
148151
collector.LogFile = logFile
149152
collector.Logger = log.New(logFile, "", log.LstdFlags|log.LUTC|log.Lmicroseconds|log.Lshortfile)
150153
collector.K8sHelmClientSet = make(map[string]helmClient.Client)
154+
collector.PodExecutor = collector.RealPodExecutor
155+
collector.QueryCRD = collector.RealQueryCRD
151156

152157
//Initialize clients
153158
collector.K8sRestConfig = config
@@ -260,7 +265,7 @@ func (c *DataCollector) WrapUp(product string) (string, error) {
260265
return tarballName, nil
261266
}
262267

263-
func (c *DataCollector) PodExecutor(namespace string, pod string, container string, command []string, ctx context.Context) ([]byte, error) {
268+
func (c *DataCollector) RealPodExecutor(namespace string, pod string, container string, command []string, ctx context.Context) ([]byte, error) {
264269
req := c.K8sCoreClientSet.CoreV1().RESTClient().Post().
265270
Namespace(namespace).
266271
Resource("pods").
@@ -293,7 +298,7 @@ func (c *DataCollector) PodExecutor(namespace string, pod string, container stri
293298
}
294299
}
295300

296-
func (c *DataCollector) QueryCRD(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error) {
301+
func (c *DataCollector) RealQueryCRD(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error) {
297302

298303
schemeGroupVersion := schema.GroupVersion{Group: crd.Group, Version: crd.Version}
299304
negotiatedSerializer := scheme.Codecs.WithoutConversion()
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package data_collector
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"io"
7+
"log"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
12+
"github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds"
13+
corev1 "k8s.io/api/core/v1"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"k8s.io/client-go/kubernetes/fake"
16+
"k8s.io/client-go/rest"
17+
)
18+
19+
func TestNewDataCollector_Success(t *testing.T) {
20+
dc := &DataCollector{Namespaces: []string{"default"}}
21+
err := NewDataCollector(dc)
22+
if err != nil {
23+
t.Fatalf("expected no error, got %v", err)
24+
}
25+
if dc.BaseDir == "" {
26+
t.Error("BaseDir should be set")
27+
}
28+
if dc.Logger == nil {
29+
t.Error("Logger should be set")
30+
}
31+
if dc.LogFile == nil {
32+
t.Error("LogFile should be set")
33+
}
34+
if dc.K8sCoreClientSet == nil {
35+
t.Error("K8sCoreClientSet should be set")
36+
}
37+
if dc.K8sCrdClientSet == nil {
38+
t.Error("K8sCrdClientSet should be set")
39+
}
40+
if dc.K8sMetricsClientSet == nil {
41+
t.Error("K8sMetricsClientSet should be set")
42+
}
43+
if dc.K8sHelmClientSet == nil {
44+
t.Error("K8sHelmClientSet should be set")
45+
}
46+
}
47+
48+
func TestWrapUp_CreatesTarball(t *testing.T) {
49+
tmpDir := t.TempDir()
50+
logFile, _ := os.Create(filepath.Join(tmpDir, "supportpkg.log"))
51+
dc := &DataCollector{
52+
BaseDir: tmpDir,
53+
LogFile: logFile,
54+
Logger: log.New(io.Discard, "", 0),
55+
}
56+
product := "nginx"
57+
tarball, err := dc.WrapUp(product)
58+
if err != nil {
59+
t.Fatalf("WrapUp failed: %v", err)
60+
}
61+
if _, err := os.Stat(tarball); err != nil {
62+
t.Errorf("tarball not created: %v", err)
63+
}
64+
_ = os.Remove(tarball)
65+
}
66+
67+
func TestRealPodExecutor_ReturnsOutput(t *testing.T) {
68+
dc := &DataCollector{
69+
K8sCoreClientSet: fake.NewClientset(),
70+
K8sRestConfig: &rest.Config{},
71+
}
72+
// Replace RealPodExecutor with a mock for testing
73+
dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) {
74+
return []byte("output"), nil
75+
}
76+
out, err := dc.PodExecutor("default", "pod", "container", []string{"echo", "hello"}, context.TODO())
77+
if err != nil {
78+
t.Fatalf("expected no error, got %v", err)
79+
}
80+
if !bytes.Contains(out, []byte("output")) {
81+
t.Errorf("expected output, got %s", string(out))
82+
}
83+
}
84+
85+
func TestRealQueryCRD_ReturnsErrorOnInvalidConfig(t *testing.T) {
86+
dc := &DataCollector{
87+
K8sRestConfig: &rest.Config{},
88+
}
89+
crd := crds.Crd{Group: "test", Version: "v1", Resource: "foos"}
90+
_, err := dc.RealQueryCRD(crd, "default", context.TODO())
91+
if err == nil {
92+
t.Error("expected error for invalid config")
93+
}
94+
}
95+
96+
func TestAllNamespacesExist_AllExist(t *testing.T) {
97+
client := fake.NewClientset(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}})
98+
dc := &DataCollector{
99+
Namespaces: []string{"default"},
100+
K8sCoreClientSet: client,
101+
Logger: log.New(io.Discard, "", 0),
102+
}
103+
if !dc.AllNamespacesExist() {
104+
t.Error("expected all namespaces to exist")
105+
}
106+
}
107+
108+
func TestAllNamespacesExist_NotExist(t *testing.T) {
109+
client := fake.NewClientset()
110+
dc := &DataCollector{
111+
Namespaces: []string{"missing"},
112+
K8sCoreClientSet: client,
113+
Logger: log.New(io.Discard, "", 0),
114+
}
115+
if dc.AllNamespacesExist() {
116+
t.Error("expected namespaces to not exist")
117+
}
118+
}
119+
120+
func TestWrapUp_ErrorOnLogFileClose(t *testing.T) {
121+
tmpDir := t.TempDir()
122+
logFile, _ := os.Create(filepath.Join(tmpDir, "supportpkg.log"))
123+
logFile.Close() // Already closed
124+
dc := &DataCollector{
125+
BaseDir: tmpDir,
126+
LogFile: logFile,
127+
Logger: log.New(io.Discard, "", 0),
128+
}
129+
_, err := dc.WrapUp("nginx")
130+
if err == nil {
131+
t.Error("expected error on closing already closed log file")
132+
}
133+
}

pkg/jobs/common_job_list.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,14 @@ func CommonJobList() []Job {
146146
Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) {
147147
jobResult := JobResult{Files: make(map[string][]byte), Error: nil}
148148
for _, namespace := range dc.Namespaces {
149-
result, err := dc.K8sCoreClientSet.DiscoveryClient.ServerPreferredResources()
149+
discoveryClient, ok := dc.K8sCoreClientSet.Discovery().(interface {
150+
ServerPreferredResources() ([]*metav1.APIResourceList, error)
151+
})
152+
if !ok {
153+
dc.Logger.Printf("\tDiscovery() does not implement ServerPreferredResources for namespace %s\n", namespace)
154+
continue
155+
}
156+
result, err := discoveryClient.ServerPreferredResources()
150157
if err != nil {
151158
dc.Logger.Printf("\tCould not retrieve API resources list %s: %v\n", namespace, err)
152159
} else {
@@ -163,7 +170,7 @@ func CommonJobList() []Job {
163170
Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) {
164171
jobResult := JobResult{Files: make(map[string][]byte), Error: nil}
165172
for _, namespace := range dc.Namespaces {
166-
result, err := dc.K8sCoreClientSet.DiscoveryClient.ServerGroups()
173+
result, err := dc.K8sCoreClientSet.Discovery().ServerGroups()
167174
if err != nil {
168175
dc.Logger.Printf("\tCould not retrieve API versions list %s: %v\n", namespace, err)
169176
} else {
@@ -367,7 +374,7 @@ func CommonJobList() []Job {
367374
Timeout: time.Second * 10,
368375
Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) {
369376
jobResult := JobResult{Files: make(map[string][]byte), Error: nil}
370-
result, err := dc.K8sCoreClientSet.ServerVersion()
377+
result, err := dc.K8sCoreClientSet.Discovery().ServerVersion()
371378
if err != nil {
372379
dc.Logger.Printf("\tCould not retrieve server version: %v\n", err)
373380
} else {

0 commit comments

Comments
 (0)