diff --git a/go.mod b/go.mod index 3fad585..80239bf 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/nginxinc/nginx-k8s-supportpkg go 1.24.3 require ( - github.com/mittwald/go-helm-client v0.12.17 + github.com/mittwald/go-helm-client v0.12.18 github.com/spf13/cobra v1.9.1 - k8s.io/client-go v0.33.1 + go.uber.org/mock v0.5.0 + helm.sh/helm/v3 v3.18.4 + k8s.io/client-go v0.33.2 ) require ( @@ -84,17 +86,15 @@ require ( go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/sdk v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect - golang.org/x/crypto v0.38.0 // indirect - golang.org/x/sync v0.14.0 // indirect - golang.org/x/tools v0.33.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/sync v0.15.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect google.golang.org/grpc v1.72.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect - helm.sh/helm/v3 v3.18.0 // indirect - k8s.io/apiserver v0.33.1 // indirect - k8s.io/cli-runtime v0.33.1 // indirect - k8s.io/component-base v0.33.1 // indirect - k8s.io/kubectl v0.33.1 // indirect + k8s.io/apiserver v0.33.2 // indirect + k8s.io/cli-runtime v0.33.2 // indirect + k8s.io/component-base v0.33.2 // indirect + k8s.io/kubectl v0.33.2 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/kustomize/api v0.19.0 // indirect sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect @@ -122,17 +122,17 @@ require ( golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/term v0.32.0 // indirect - golang.org/x/text v0.25.0 // indirect + golang.org/x/text v0.26.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.1 - k8s.io/apiextensions-apiserver v0.33.1 - k8s.io/apimachinery v0.33.1 + k8s.io/api v0.33.2 + k8s.io/apiextensions-apiserver v0.33.2 + k8s.io/apimachinery v0.33.2 k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/metrics v0.33.1 + k8s.io/metrics v0.33.2 k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect diff --git a/go.sum b/go.sum index 4bd68de..669833e 100644 --- a/go.sum +++ b/go.sum @@ -189,8 +189,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mittwald/go-helm-client v0.12.17 h1:PncoE1u3fXuHWLineNDQ4hI5J4uVbMW3JWrtdBR86TI= -github.com/mittwald/go-helm-client v0.12.17/go.mod h1:GQxuPspUcMsxWWDtYzjRdxOAjh3LKADIfgqtUf9mjHk= +github.com/mittwald/go-helm-client v0.12.18 h1:i9cJNv/YC3ZPKUKVNYTlrOO7ZO6YFKE/ak3J5TeYHPU= +github.com/mittwald/go-helm-client v0.12.18/go.mod h1:dLl5NkdKCvwKvLIdZzg4MDbxhSKmuimdmM3WpsAzS0I= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -330,15 +330,17 @@ go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9f go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= -golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -350,8 +352,8 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -364,8 +366,8 @@ golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -399,30 +401,30 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -helm.sh/helm/v3 v3.18.0 h1:ItOAm3Quo0dus3NUHjs+lluqWWEIO7xrSW+zKWCrvlw= -helm.sh/helm/v3 v3.18.0/go.mod h1:43QHS1W97RcoFJRk36ZBhHdTfykqBlJdsWp3yhzdq8w= -k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= -k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= -k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= -k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= -k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= -k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= -k8s.io/cli-runtime v0.33.1 h1:TvpjEtF71ViFmPeYMj1baZMJR4iWUEplklsUQ7D3quA= -k8s.io/cli-runtime v0.33.1/go.mod h1:9dz5Q4Uh8io4OWCLiEf/217DXwqNgiTS/IOuza99VZE= -k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= -k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= -k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= -k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ= +helm.sh/helm/v3 v3.18.4/go.mod h1:WVnwKARAw01iEdjpEkP7Ii1tT1pTPYfM1HsakFKM3LI= +k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= +k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/apiextensions-apiserver v0.33.2 h1:6gnkIbngnaUflR3XwE1mCefN3YS8yTD631JXQhsU6M8= +k8s.io/apiextensions-apiserver v0.33.2/go.mod h1:IvVanieYsEHJImTKXGP6XCOjTwv2LUMos0YWc9O+QP8= +k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= +k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4= +k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M= +k8s.io/cli-runtime v0.33.2 h1:koNYQKSDdq5AExa/RDudXMhhtFasEg48KLS2KSAU74Y= +k8s.io/cli-runtime v0.33.2/go.mod h1:gnhsAWpovqf1Zj5YRRBBU7PFsRc6NkEkwYNQE+mXL88= +k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= +k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0= +k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A= -k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0= -k8s.io/metrics v0.33.1 h1:Ypd5ITCf+fM+LDNFk7hESXTc3vh02CQYGiwRoVRaGsM= -k8s.io/metrics v0.33.1/go.mod h1:wK8cFTK5ykBdhL0Wy4RZwLH28XM7j/Klc+NQrMRWVxg= +k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y= +k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI= +k8s.io/metrics v0.33.2 h1:gNCBmtnUMDMCRg9Ly5ehxP3OdKISMsOnh1vzk01iCgE= +k8s.io/metrics v0.33.2/go.mod h1:yxoAosKGRsZisv3BGekC5W6T1J8XSV+PoUEevACRv7c= k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= diff --git a/pkg/crds/crd_test.go b/pkg/crds/crd_test.go new file mode 100644 index 0000000..d4de52c --- /dev/null +++ b/pkg/crds/crd_test.go @@ -0,0 +1,82 @@ +package crds + +import ( + "reflect" + "testing" +) + +func TestGetNICCRDList(t *testing.T) { + tests := []struct { + name string + want []Crd + }{ + { + name: "Correct CRD list", + want: []Crd{ + { + Resource: "apdoslogconfs", + Group: "appprotectdos.f5.com", + Version: "v1beta1", + }, + { + Resource: "apdospolicies", + Group: "appprotectdos.f5.com", + Version: "v1beta1", + }, + { + Resource: "dosprotectedresources", + Group: "appprotectdos.f5.com", + Version: "v1beta1", + }, + { + Resource: "aplogconfs", + Group: "appprotect.f5.com", + Version: "v1beta1", + }, + { + Resource: "appolicies", + Group: "appprotect.f5.com", + Version: "v1beta1", + }, + { + Resource: "apusersigs", + Group: "appprotect.f5.com", + Version: "v1beta1", + }, + { + Resource: "globalconfigurations", + Group: "k8s.nginx.org", + Version: "v1", + }, + { + Resource: "policies", + Group: "k8s.nginx.org", + Version: "v1", + }, + { + Resource: "transportservers", + Group: "k8s.nginx.org", + Version: "v1", + }, + { + Resource: "virtualserverroutes", + Group: "k8s.nginx.org", + Version: "v1", + }, + { + Resource: "virtualservers", + Group: "k8s.nginx.org", + Version: "v1", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GetNICCRDList(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetNICCRDList() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/data_collector/data_collector.go b/pkg/data_collector/data_collector.go index 340caba..8ec7365 100644 --- a/pkg/data_collector/data_collector.go +++ b/pkg/data_collector/data_collector.go @@ -36,6 +36,7 @@ import ( "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds" "github.com/nginxinc/nginx-k8s-supportpkg/pkg/version" corev1 "k8s.io/api/core/v1" + apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" crdClient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -54,12 +55,14 @@ type DataCollector struct { Logger *log.Logger LogFile *os.File K8sRestConfig *rest.Config - K8sCoreClientSet *kubernetes.Clientset - K8sCrdClientSet *crdClient.Clientset - K8sMetricsClientSet *metricsClient.Clientset + K8sCoreClientSet kubernetes.Interface + K8sCrdClientSet apiextensionsclientset.Interface + K8sMetricsClientSet metricsClient.Interface K8sHelmClientSet map[string]helmClient.Client ExcludeDBData bool ExcludeTimeSeriesData bool + PodExecutor func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) + QueryCRD func(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error) } type Manifest struct { @@ -147,6 +150,8 @@ func NewDataCollector(collector *DataCollector) error { collector.LogFile = logFile collector.Logger = log.New(logFile, "", log.LstdFlags|log.LUTC|log.Lmicroseconds|log.Lshortfile) collector.K8sHelmClientSet = make(map[string]helmClient.Client) + collector.PodExecutor = collector.RealPodExecutor + collector.QueryCRD = collector.RealQueryCRD //Initialize clients collector.K8sRestConfig = config @@ -259,7 +264,7 @@ func (c *DataCollector) WrapUp(product string) (string, error) { return tarballName, nil } -func (c *DataCollector) PodExecutor(namespace string, pod string, container string, command []string, ctx context.Context) ([]byte, error) { +func (c *DataCollector) RealPodExecutor(namespace string, pod string, container string, command []string, ctx context.Context) ([]byte, error) { req := c.K8sCoreClientSet.CoreV1().RESTClient().Post(). Namespace(namespace). Resource("pods"). @@ -292,7 +297,7 @@ func (c *DataCollector) PodExecutor(namespace string, pod string, container stri } } -func (c *DataCollector) QueryCRD(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error) { +func (c *DataCollector) RealQueryCRD(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error) { schemeGroupVersion := schema.GroupVersion{Group: crd.Group, Version: crd.Version} negotiatedSerializer := scheme.Codecs.WithoutConversion() diff --git a/pkg/data_collector/data_collector_test.go b/pkg/data_collector/data_collector_test.go new file mode 100644 index 0000000..badc500 --- /dev/null +++ b/pkg/data_collector/data_collector_test.go @@ -0,0 +1,133 @@ +package data_collector + +import ( + "bytes" + "context" + "io" + "log" + "os" + "path/filepath" + "testing" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" +) + +func TestNewDataCollector_Success(t *testing.T) { + dc := &DataCollector{Namespaces: []string{"default"}} + err := NewDataCollector(dc) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if dc.BaseDir == "" { + t.Error("BaseDir should be set") + } + if dc.Logger == nil { + t.Error("Logger should be set") + } + if dc.LogFile == nil { + t.Error("LogFile should be set") + } + if dc.K8sCoreClientSet == nil { + t.Error("K8sCoreClientSet should be set") + } + if dc.K8sCrdClientSet == nil { + t.Error("K8sCrdClientSet should be set") + } + if dc.K8sMetricsClientSet == nil { + t.Error("K8sMetricsClientSet should be set") + } + if dc.K8sHelmClientSet == nil { + t.Error("K8sHelmClientSet should be set") + } +} + +func TestWrapUp_CreatesTarball(t *testing.T) { + tmpDir := t.TempDir() + logFile, _ := os.Create(filepath.Join(tmpDir, "supportpkg.log")) + dc := &DataCollector{ + BaseDir: tmpDir, + LogFile: logFile, + Logger: log.New(io.Discard, "", 0), + } + product := "nginx" + tarball, err := dc.WrapUp(product) + if err != nil { + t.Fatalf("WrapUp failed: %v", err) + } + if _, err := os.Stat(tarball); err != nil { + t.Errorf("tarball not created: %v", err) + } + _ = os.Remove(tarball) +} + +func TestRealPodExecutor_ReturnsOutput(t *testing.T) { + dc := &DataCollector{ + K8sCoreClientSet: fake.NewSimpleClientset(), + K8sRestConfig: &rest.Config{}, + } + // Replace RealPodExecutor with a mock for testing + dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte("output"), nil + } + out, err := dc.PodExecutor("default", "pod", "container", []string{"echo", "hello"}, context.TODO()) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !bytes.Contains(out, []byte("output")) { + t.Errorf("expected output, got %s", string(out)) + } +} + +func TestRealQueryCRD_ReturnsErrorOnInvalidConfig(t *testing.T) { + dc := &DataCollector{ + K8sRestConfig: &rest.Config{}, + } + crd := crds.Crd{Group: "test", Version: "v1", Resource: "foos"} + _, err := dc.RealQueryCRD(crd, "default", context.TODO()) + if err == nil { + t.Error("expected error for invalid config") + } +} + +func TestAllNamespacesExist_AllExist(t *testing.T) { + client := fake.NewSimpleClientset(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}}) + dc := &DataCollector{ + Namespaces: []string{"default"}, + K8sCoreClientSet: client, + Logger: log.New(io.Discard, "", 0), + } + if !dc.AllNamespacesExist() { + t.Error("expected all namespaces to exist") + } +} + +func TestAllNamespacesExist_NotExist(t *testing.T) { + client := fake.NewSimpleClientset() + dc := &DataCollector{ + Namespaces: []string{"missing"}, + K8sCoreClientSet: client, + Logger: log.New(io.Discard, "", 0), + } + if dc.AllNamespacesExist() { + t.Error("expected namespaces to not exist") + } +} + +func TestWrapUp_ErrorOnLogFileClose(t *testing.T) { + tmpDir := t.TempDir() + logFile, _ := os.Create(filepath.Join(tmpDir, "supportpkg.log")) + logFile.Close() // Already closed + dc := &DataCollector{ + BaseDir: tmpDir, + LogFile: logFile, + Logger: log.New(io.Discard, "", 0), + } + _, err := dc.WrapUp("nginx") + if err == nil { + t.Error("expected error on closing already closed log file") + } +} diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index f2e0aa5..a59be6e 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -146,7 +146,7 @@ func CommonJobList() []Job { Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { jobResult := JobResult{Files: make(map[string][]byte), Error: nil} for _, namespace := range dc.Namespaces { - result, err := dc.K8sCoreClientSet.DiscoveryClient.ServerPreferredResources() + result, err := dc.K8sCoreClientSet.Discovery().ServerPreferredResources() if err != nil { dc.Logger.Printf("\tCould not retrieve API resources list %s: %v\n", namespace, err) } else { @@ -163,7 +163,7 @@ func CommonJobList() []Job { Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { jobResult := JobResult{Files: make(map[string][]byte), Error: nil} for _, namespace := range dc.Namespaces { - result, err := dc.K8sCoreClientSet.DiscoveryClient.ServerGroups() + result, err := dc.K8sCoreClientSet.Discovery().ServerGroups() if err != nil { dc.Logger.Printf("\tCould not retrieve API versions list %s: %v\n", namespace, err) } else { @@ -367,7 +367,7 @@ func CommonJobList() []Job { Timeout: time.Second * 10, Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { jobResult := JobResult{Files: make(map[string][]byte), Error: nil} - result, err := dc.K8sCoreClientSet.ServerVersion() + result, err := dc.K8sCoreClientSet.Discovery().ServerVersion() if err != nil { dc.Logger.Printf("\tCould not retrieve server version: %v\n", err) } else { diff --git a/pkg/jobs/common_job_list_test.go b/pkg/jobs/common_job_list_test.go new file mode 100644 index 0000000..4e10ac2 --- /dev/null +++ b/pkg/jobs/common_job_list_test.go @@ -0,0 +1,83 @@ +package jobs + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock" +) + +func TestCommonJobList_SelectedJobsProduceFiles(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + jobList := CommonJobList() + + for _, job := range jobList { + + ch := make(chan JobResult, 1) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + go job.Execute(dc, ctx, ch) + + select { + case res := <-ch: + if res.Error != nil { + t.Fatalf("job %s returned unexpected error: %v", job.Name, res.Error) + } + if len(res.Files) == 0 { + t.Fatalf("job %s produced no files", job.Name) + } + // Basic path sanity + non-empty content + for path, content := range res.Files { + if len(content) == 0 { + t.Fatalf("job %s file %s has empty content", job.Name, path) + } + if !strings.HasPrefix(filepath.ToSlash(path), filepath.ToSlash(dc.BaseDir)) { + t.Fatalf("job %s file path %s does not start with basedir %s", job.Name, path, dc.BaseDir) + } + } + case <-ctx.Done(): + t.Fatalf("job %s timed out", job.Name) + } + } +} + +func TestCommonJobList_PodListJSONKeyPresence(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + var podListJob *Job + jobs := CommonJobList() + for i, j := range jobs { + if j.Name == "pod-list" { + podListJob = &jobs[i] + break + } + } + if podListJob == nil { + t.Fatalf("pod-list job not found") + } + + ch := make(chan JobResult, 1) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + go podListJob.Execute(dc, ctx, ch) + + res := <-ch + if res.Error != nil { + t.Fatalf("pod-list job returned error: %v", res.Error) + } + if len(res.Files) != 1 { + t.Fatalf("expected 1 file from pod-list job, got %d", len(res.Files)) + } + for path, content := range res.Files { + if filepath.Base(path) != "pods.json" { + t.Fatalf("expected pods.json file, got %s", path) + } + // Quick check JSON starts with '{' (marshaled list) or '[' depending on structure (PodList marshals to object) + if len(content) == 0 || content[0] != '{' { + t.Fatalf("unexpected JSON content in %s", path) + } + } +} diff --git a/pkg/jobs/job_test.go b/pkg/jobs/job_test.go new file mode 100644 index 0000000..b1aeda7 --- /dev/null +++ b/pkg/jobs/job_test.go @@ -0,0 +1,102 @@ +package jobs + +import ( + "context" + "errors" + "os" + "path/filepath" + "testing" + "time" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/data_collector" + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock" +) + +// Test successful job execution and file writing +func TestJobCollect_Success(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + job := Job{ + Name: "test-job", + Timeout: time.Second, + Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { + files := map[string][]byte{ + filepath.Join(dc.BaseDir, "output.txt"): []byte("hello world"), + } + ch <- JobResult{Files: files} + }, + } + + err, skipped, _ := job.Collect(dc) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if skipped { + t.Fatalf("expected not skipped") + } + // Check file was written + content, err := os.ReadFile(filepath.Join(dc.BaseDir, "output.txt")) + if err != nil { + t.Fatalf("file not written: %v", err) + } + if string(content) != "hello world" { + t.Fatalf("unexpected file content: %s", string(content)) + } +} + +// Test job skipped scenario +func TestJobCollect_Skipped(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + job := Job{ + Name: "skip-job", + Timeout: time.Second, + Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { + ch <- JobResult{Skipped: true} + }, + } + err, skipped, _ := job.Collect(dc) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if !skipped { + t.Fatalf("expected skipped") + } +} + +// Test job error scenario +func TestJobCollect_Error(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + job := Job{ + Name: "error-job", + Timeout: time.Second, + Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { + ch <- JobResult{Error: errors.New("fail")} + }, + } + err, skipped, _ := job.Collect(dc) + if err == nil || err.Error() != "fail" { + t.Fatalf("expected error 'fail', got %v", err) + } + if skipped { + t.Fatalf("expected not skipped") + } +} + +// Test job timeout scenario +func TestJobCollect_Timeout(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + job := Job{ + Name: "timeout-job", + Timeout: time.Millisecond * 10, + Execute: func(dc *data_collector.DataCollector, ctx context.Context, ch chan JobResult) { + time.Sleep(time.Second) + ch <- JobResult{} + }, + } + err, skipped, _ := job.Collect(dc) + if err == nil { + t.Fatalf("expected timeout error, got nil") + } + if skipped { + t.Fatalf("expected not skipped") + } +} diff --git a/pkg/jobs/ngf_job_list_test.go b/pkg/jobs/ngf_job_list_test.go new file mode 100644 index 0000000..735604b --- /dev/null +++ b/pkg/jobs/ngf_job_list_test.go @@ -0,0 +1,148 @@ +package jobs + +import ( + "context" + "io" + "log" + "strings" + "testing" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNGFJobList(t *testing.T) { + jobs := NGFJobList() + if len(jobs) == 0 { + t.Error("expected jobs to be returned") + } + + expectedJobs := []string{"exec-nginx-gateway-version", "exec-nginx-t", "crd-objects"} + if len(jobs) != len(expectedJobs) { + t.Errorf("expected %d jobs, got %d", len(expectedJobs), len(jobs)) + } + + for i, job := range jobs { + if job.Name != expectedJobs[i] { + t.Errorf("expected job name %s, got %s", expectedJobs[i], job.Name) + } + if job.Execute == nil { + t.Errorf("job %s should have Execute function", job.Name) + } + if job.Timeout == 0 { + t.Errorf("job %s should have timeout set", job.Name) + } + } +} + +func TestNGFJobExecNginxGatewayVersion(t *testing.T) { + client := fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-gateway-test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "nginx-gateway"}, + }, + }, + }) + + dc := mock.SetupMockDataCollector(t) + dc.K8sCoreClientSet = client + dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte("gateway version output"), nil + } + + jobs := NGFJobList() + var versionJob Job + for _, job := range jobs { + if job.Name == "exec-nginx-gateway-version" { + versionJob = job + break + } + } + + ch := make(chan JobResult, 1) + ctx := context.Background() + + versionJob.Execute(dc, ctx, ch) + result := <-ch + + if result.Error != nil { + t.Errorf("expected no error, got %v", result.Error) + } + + if len(result.Files) == 0 { + t.Error("expected files to be created") + } + + found := false + for filename := range result.Files { + if strings.Contains(filename, "nginx-gateway-version.txt") { + found = true + break + } + } + if !found { + t.Error("expected nginx-gateway-version.txt file to be created") + } +} + +func TestNGFJobExecNginxT(t *testing.T) { + client := fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-gateway-test-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "nginx"}, + }, + }, + }) + + dc := mock.SetupMockDataCollector(t) + dc.Namespaces = []string{"default"} + dc.K8sCoreClientSet = client + dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte("nginx -t output"), nil + } + dc.Logger = log.New(io.Discard, "", 0) + + jobs := NGFJobList() + var tJob Job + for _, job := range jobs { + if job.Name == "exec-nginx-t" { + tJob = job + break + } + } + + ch := make(chan JobResult, 1) + ctx := context.Background() + + tJob.Execute(dc, ctx, ch) + result := <-ch + + if result.Error != nil { + t.Errorf("expected no error, got %v", result.Error) + } + + if len(result.Files) == 0 { + t.Error("expected files to be created") + } + + found := false + for filename := range result.Files { + if strings.Contains(filename, "nginx-t.txt") { + found = true + break + } + } + if !found { + t.Error("expected nginx-t.txt file to be created") + } +} diff --git a/pkg/jobs/ngx_job_list_test.go b/pkg/jobs/ngx_job_list_test.go new file mode 100644 index 0000000..8f85f64 --- /dev/null +++ b/pkg/jobs/ngx_job_list_test.go @@ -0,0 +1,75 @@ +package jobs + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNGXJobList_ExecNginxT(t *testing.T) { + // dc := &data_collector.DataCollector{ + // BaseDir: tmpDir, + // Logger: log.New(io.Discard, "", 0), + // Namespaces: []string{"default"}, + // } + + dc := mock.SetupMockDataCollector(t) + + // Create a fake pod named "nginx-123" in the "default" namespace + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-123", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "nginx"}, + }, + }, + } + dc.K8sCoreClientSet = fake.NewSimpleClientset(pod) + + // Mock PodExecutor + dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte("nginx -T output"), nil + } + + jobList := NGXJobList() + if len(jobList) != 1 { + t.Fatalf("expected 1 job, got %d", len(jobList)) + } + job := jobList[0] + ch := make(chan JobResult, 1) + go job.Execute(dc, context.Background(), ch) + select { + case result := <-ch: + if result.Error != nil { + t.Fatalf("unexpected error: %v", result.Error) + } + found := false + for file, content := range result.Files { + if !strings.HasSuffix(file, "__nginx-t.txt") { + t.Errorf("unexpected file name: %s", file) + } + if string(content) != "nginx -T output" { + t.Errorf("unexpected file content: %s", string(content)) + } + if !strings.HasPrefix(filepath.ToSlash(file), filepath.ToSlash(dc.BaseDir)) { + t.Errorf("file path %s does not start with tmpDir %s", file, dc.BaseDir) + } + found = true + } + if !found { + t.Errorf("no output file created by job") + } + case <-time.After(time.Second): + t.Fatal("job execution timed out") + } +} diff --git a/pkg/jobs/nic_job_list_test.go b/pkg/jobs/nic_job_list_test.go new file mode 100644 index 0000000..766a002 --- /dev/null +++ b/pkg/jobs/nic_job_list_test.go @@ -0,0 +1,103 @@ +package jobs + +import ( + "context" + "encoding/json" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds" + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +// mockPodExecutor simulates PodExecutor for testing +func mockPodExecutor(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte("mock-output"), nil +} + +// mockQueryCRD simulates QueryCRD for testing +func mockQueryCRD(crd crds.Crd, namespace string, ctx context.Context) ([]byte, error) { + return json.Marshal(map[string]string{"kind": crd.Resource}) +} + +func TestNICJobList_ExecJobs(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + dc.Namespaces = []string{"test-ns"} + + // Mock PodExecutor and QueryCRD + dc.PodExecutor = mockPodExecutor + dc.QueryCRD = mockQueryCRD + + // Use a real or fake clientset (kubernetes.Interface) + dc.K8sCoreClientSet = fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "ingress-pod", Namespace: "test-ns"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + {Name: "container1"}, + }, + }, + }) + + jobList := NICJobList() + for _, job := range jobList { + ch := make(chan JobResult, 1) + go job.Execute(dc, context.Background(), ch) + select { + case result := <-ch: + if result.Error != nil { + t.Errorf("Job %s returned error: %v", job.Name, result.Error) + } + for file, content := range result.Files { + if !strings.HasPrefix(filepath.ToSlash(file), filepath.ToSlash(dc.BaseDir)) { + t.Errorf("File path %s does not start with tmpDir", file) + } + if len(content) == 0 { + t.Errorf("File %s has empty content", file) + } + } + case <-time.After(time.Second): + t.Errorf("Job %s timed out", job.Name) + } + } +} + +func TestNICJobList_CRDObjects(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + dc.Namespaces = []string{"test-ns"} + dc.QueryCRD = mockQueryCRD + + jobList := NICJobList() + var found bool + for _, job := range jobList { + if job.Name == "crd-objects" { + found = true + ch := make(chan JobResult, 1) + go job.Execute(dc, context.Background(), ch) + select { + case result := <-ch: + if result.Error != nil { + t.Errorf("CRD job returned error: %v", result.Error) + } + for file, content := range result.Files { + if !strings.HasPrefix(filepath.ToSlash(file), filepath.ToSlash(dc.BaseDir)) { + t.Errorf("File path %s does not start with tmpDir", file) + } + var out map[string]interface{} + if err := json.Unmarshal(content, &out); err != nil { + t.Errorf("Invalid JSON in file %s: %v", file, err) + } + } + case <-time.After(time.Second): + t.Errorf("CRD job timed out") + } + } + } + if !found { + t.Errorf("crd-objects job not found in NICJobList") + } +} diff --git a/pkg/jobs/nim_job_list_test.go b/pkg/jobs/nim_job_list_test.go new file mode 100644 index 0000000..e4eed6e --- /dev/null +++ b/pkg/jobs/nim_job_list_test.go @@ -0,0 +1,157 @@ +package jobs + +import ( + "context" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/mock" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNIMJobList_ExecJobs(t *testing.T) { + // dc := &data_collector.DataCollector{ + // BaseDir: tmpDir, + // Logger: log.New(io.Discard, "", 0), + // Namespaces: []string{"default"}, + // } + dc := mock.SetupMockDataCollector(t) + dc.Namespaces = []string{"default"} + + // Create fake pods for each job type + pods := []*corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "apigw-123", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "apigw"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "clickhouse-456", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "clickhouse-server"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "core-789", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "core"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "dpm-101", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "dpm"}}, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "integrations-102", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "integrations"}}, + }, + }, + } + + objects := make([]runtime.Object, len(pods)) + for i, pod := range pods { + objects[i] = pod + } + dc.K8sCoreClientSet = fake.NewSimpleClientset(objects...) + + // Mock PodExecutor to return predictable output + dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte(strings.Join(command, " ")), nil + } + + // Run all jobs in NIMJobList + for _, job := range NIMJobList() { + ch := make(chan JobResult, 1) + go job.Execute(dc, context.Background(), ch) + select { + case result := <-ch: + if result.Error != nil { + t.Errorf("Job %s returned error: %v", job.Name, result.Error) + } + for file, content := range result.Files { + if !strings.HasPrefix(filepath.ToSlash(file), filepath.ToSlash(dc.BaseDir)) { + t.Errorf("File path %s does not start with tmpDir %s", file, dc.BaseDir) + } + if len(content) == 0 { + t.Errorf("File %s has empty content", file) + } + } + case <-time.After(2 * time.Second): + t.Errorf("Job %s timed out", job.Name) + } + } +} + +func TestNIMJobList_ExcludeFlags(t *testing.T) { + dc := mock.SetupMockDataCollector(t) + dc.Namespaces = []string{"default"} + dc.K8sCoreClientSet = fake.NewSimpleClientset(&corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "clickhouse-456", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "clickhouse-server"}}, + }, + }) + dc.PodExecutor = func(namespace, pod, container string, command []string, ctx context.Context) ([]byte, error) { + return []byte("output"), nil + } + // Test ExcludeTimeSeriesData for exec-clickhouse-data + dc.ExcludeTimeSeriesData = true + for _, job := range NIMJobList() { + if job.Name == "exec-clickhouse-data" { + ch := make(chan JobResult, 1) + go job.Execute(dc, context.Background(), ch) + select { + case result := <-ch: + if !result.Skipped { + t.Errorf("Expected job to be skipped when ExcludeTimeSeriesData is true") + } + case <-time.After(time.Second): + t.Fatal("Job exec-clickhouse-data timed out") + } + } + } + + // Test ExcludeDBData for exec-dqlite-dump + dc.ExcludeDBData = true + for _, job := range NIMJobList() { + if job.Name == "exec-dqlite-dump" { + ch := make(chan JobResult, 1) + go job.Execute(dc, context.Background(), ch) + select { + case result := <-ch: + if !result.Skipped { + t.Errorf("Expected job to be skipped when ExcludeDBData is true") + } + case <-time.After(time.Second): + t.Fatal("Job exec-dqlite-dump timed out") + } + } + } +} diff --git a/pkg/mock/mock_data_collector.go b/pkg/mock/mock_data_collector.go new file mode 100644 index 0000000..b1b0f3f --- /dev/null +++ b/pkg/mock/mock_data_collector.go @@ -0,0 +1,121 @@ +package mock + +import ( + "io" + "log" + "testing" + + helmclient "github.com/mittwald/go-helm-client" + mockHelmClient "github.com/mittwald/go-helm-client/mock" + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/data_collector" + "go.uber.org/mock/gomock" + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/rest" + metricsfake "k8s.io/metrics/pkg/client/clientset/versioned/fake" +) + +// helper creates int32 ptr +func i32(v int32) *int32 { return &v } + +func SetupMockDataCollector(t *testing.T) *data_collector.DataCollector { + t.Helper() + + tmpDir := t.TempDir() + + // Seed fake objects (namespace default implied by metadata) + objs := []runtime.Object{ + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-1", Namespace: "default"}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "c1", Image: "nginx:latest"}}, + }, + }, + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "svc-1", Namespace: "default"}, + Spec: corev1.ServiceSpec{Selector: map[string]string{"app": "demo"}}, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{Name: "dep-1", Namespace: "default"}, + Spec: appsv1.DeploymentSpec{ + Replicas: i32(1), + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "demo"}}, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "demo"}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "dep-c1", Image: "nginx:latest"}}, + }, + }, + }, + }, + &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{Name: "role-1", Namespace: "default"}, + Rules: []rbacv1.PolicyRule{{APIGroups: []string{""}, Resources: []string{"pods"}, Verbs: []string{"get"}}}, + }, + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: "cm-1", Namespace: "default"}, + Data: map[string]string{"k": "v"}, + }, + } + + client := fake.NewSimpleClientset(objs...) + // Mock rest.Config + restConfig := &rest.Config{ + Host: "https://mock-k8s-server", + } + + // Create a CRD clientset (using the real clientset, but not actually connecting) + crd := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testcrd.example.com", + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "example.com", + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Kind: "TestCRD", + Plural: "testcrds", + Singular: "testcrd", + }, + Scope: apiextensionsv1.NamespaceScoped, + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1", + Served: true, + Storage: true, + }, + }, + }, + } + + crdClient := apiextensionsfake.NewSimpleClientset(crd) + metricsClient := metricsfake.NewSimpleClientset() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + helmClient := mockHelmClient.NewMockClient(ctrl) + if helmClient == nil { + t.Fail() + } + helmClient.EXPECT().GetSettings().Return(&cli.EnvSettings{}).AnyTimes() + var mockedRelease = release.Release{Name: "test", Namespace: "test", Manifest: "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: example-config\n namespace: default\ndata:\n key: value\n"} + helmClient.EXPECT().ListDeployedReleases().Return([]*release.Release{&mockedRelease}, nil).AnyTimes() + + return &data_collector.DataCollector{ + BaseDir: tmpDir, + Namespaces: []string{"default"}, + Logger: log.New(io.Discard, "", 0), + K8sCoreClientSet: client, + K8sCrdClientSet: crdClient, + K8sRestConfig: restConfig, + K8sMetricsClientSet: metricsClient, + K8sHelmClientSet: map[string]helmclient.Client{"default": helmClient}, + } +}