From e35ff5290a43b994b476d5fc6f9905139e7a01f5 Mon Sep 17 00:00:00 2001 From: Madhu RAJAGOPAL Date: Thu, 9 Oct 2025 14:02:22 +1300 Subject: [PATCH 01/11] Fix: Include platform info in the manifest for iHealth --- pkg/data_collector/data_collector.go | 49 +++++++++++++++------ pkg/jobs/common_job_list.go | 39 +++++++++++++++++ pkg/jobs/nic_job_list.go | 65 ++++++++++++++++++++++++++-- 3 files changed, 137 insertions(+), 16 deletions(-) diff --git a/pkg/data_collector/data_collector.go b/pkg/data_collector/data_collector.go index 340caba..6592a53 100644 --- a/pkg/data_collector/data_collector.go +++ b/pkg/data_collector/data_collector.go @@ -34,7 +34,6 @@ import ( helmClient "github.com/mittwald/go-helm-client" "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds" - "github.com/nginxinc/nginx-k8s-supportpkg/pkg/version" corev1 "k8s.io/api/core/v1" crdClient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -94,12 +93,14 @@ type CommandTiming struct { type ProductInfo struct { Product string `json:"product"` Version string `json:"version"` + Build string `json:"build"` } type PlatformInfo struct { // Add platform-specific fields as needed - K8sVersion string `json:"k8s_version,omitempty"` - Namespaces []string `json:"namespaces,omitempty"` + PlatformType string `json:"platform_type,omitempty"` + Hostname string `json:"hostname,omitempty"` + SerialNumber string `json:"serial_number,omitempty"` } type SubPackage struct { @@ -328,22 +329,44 @@ func (c *DataCollector) AllNamespacesExist() bool { } func (c *DataCollector) GenerateManifest(product string, startTime time.Time, jobsRun, jobsFailed int, jobTimings []JobInfo) ([]byte, error) { + // Read and parse product_info.json + filename := filepath.Join(c.BaseDir, "product_info.json") + file, err := os.Open(filename) + if err != nil { + log.Fatalf("failed to open file: %v", err) + } + defer file.Close() + + var info ProductInfo + decoder := json.NewDecoder(file) + if err := decoder.Decode(&info); err != nil { + log.Fatalf("failed to decode JSON: %v", err) + } + + filename = filepath.Join(c.BaseDir, "platform_info.json") + file, err = os.Open(filename) + if err != nil { + log.Fatalf("failed to open file: %v", err) + } + defer file.Close() + + var platformInfo PlatformInfo + decoder = json.NewDecoder(file) + if err = decoder.Decode(&platformInfo); err != nil { + log.Fatalf("failed to decode JSON: %v", err) + } + manifest := Manifest{ Version: "1.2", // Match the schema version Timestamp: TimestampInfo{ Start: startTime.UTC().Format(time.RFC3339Nano), Stop: time.Now().UTC().Format(time.RFC3339Nano), }, - PackageType: "root", // As defined in schema enum - RootDir: ".", - ProductInfo: ProductInfo{ - Product: product, - Version: version.Version, - }, - PlatformInfo: PlatformInfo{ - Namespaces: c.Namespaces, - }, - Commands: []Command{}, + PackageType: "root", // As defined in schema enum + RootDir: ".", + ProductInfo: info, + PlatformInfo: platformInfo, + Commands: []Command{}, } // Convert job timings to commands format diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index f2e0aa5..b97a93d 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -433,6 +433,45 @@ func CommonJobList() []Job { } else { jsonResult, _ := json.MarshalIndent(result, "", " ") jobResult.Files[filepath.Join(dc.BaseDir, "k8s", "nodes.json")] = jsonResult + var nodeList corev1.NodeList + err := json.Unmarshal(jsonResult, &nodeList) + if err != nil { + // handle error + } + + var hostnames []string + var platformType string + for _, node := range nodeList.Items { + labels := node.ObjectMeta.Labels + // If the node does NOT have the control-plane label, include its name + if labels["node-role.kubernetes.io/control-plane"] == "" { + hostnames = append(hostnames, node.ObjectMeta.Name) + osImage := node.Status.NodeInfo.OSImage + osType := node.Status.NodeInfo.OperatingSystem + osArch := node.Status.NodeInfo.Architecture + + platformType = fmt.Sprintf("%s %s/%s", osImage, osType, osArch) + } + } + const platformInfoFilename = "platform_info.json" + versionInfo, err := dc.K8sCoreClientSet.Discovery().ServerVersion() + k8sVersion := "" + if err == nil && versionInfo != nil { + k8sVersion = versionInfo.GitVersion + } + platformInfo := data_collector.PlatformInfo{ + PlatformType: fmt.Sprintf("%s, k8s version: %s", platformType, k8sVersion), + Hostname: hostnames[0], + SerialNumber: "N/A", + } + + platformInfoBytes, err := json.MarshalIndent(platformInfo, "", " ") + if err != nil { + dc.Logger.Printf("\tCould not marshal platformInfo: %v\n", err) + } else { + jobResult.Files[filepath.Join(dc.BaseDir, platformInfoFilename)] = platformInfoBytes + dc.Logger.Printf("\tPlatform Info: %s\n", platformInfoBytes) + } } ch <- jobResult }, diff --git a/pkg/jobs/nic_job_list.go b/pkg/jobs/nic_job_list.go index 3ea2a71..e71205f 100644 --- a/pkg/jobs/nic_job_list.go +++ b/pkg/jobs/nic_job_list.go @@ -23,14 +23,37 @@ import ( "context" "encoding/json" "fmt" - "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds" - "github.com/nginxinc/nginx-k8s-supportpkg/pkg/data_collector" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "path/filepath" + "regexp" "strings" "time" + + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/crds" + "github.com/nginxinc/nginx-k8s-supportpkg/pkg/data_collector" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +// Extracts ProductInfo from nginx-ingress --version output +func ParseNginxIngressProductInfo(res []byte) data_collector.ProductInfo { + productInfo := data_collector.ProductInfo{ + Version: "unknown", + Product: "NGINX Ingress Controller", + Build: "unknown", + } + + re := regexp.MustCompile(`Version=([^\s]+)`) + matches := re.FindSubmatch(res) + if len(matches) > 1 { + productInfo.Version = string(matches[1]) + } + re = regexp.MustCompile(`Commit=([^\s]+)`) + matches = re.FindSubmatch(res) + if len(matches) > 1 { + productInfo.Build = string(matches[1]) + } + return productInfo +} + func NICJobList() []Job { jobList := []Job{ { @@ -174,6 +197,42 @@ func NICJobList() []Job { ch <- jobResult }, }, + { + Name: "collect-product-platform-info", + 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} + command := []string{"./nginx-ingress", "--version"} + for _, namespace := range dc.Namespaces { + pods, err := dc.K8sCoreClientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + dc.Logger.Printf("\tCould not retrieve pod list for namespace %s: %v\n", namespace, err) + } else { + for _, pod := range pods.Items { + if strings.Contains(pod.Name, "ingress") { + for _, container := range pod.Spec.Containers { + res, err := dc.PodExecutor(namespace, pod.Name, container.Name, command, ctx) + if err != nil { + jobResult.Error = err + dc.Logger.Printf("\tCommand execution %s failed for pod %s in namespace %s: %v\n", command, pod.Name, namespace, err) + } else { + product_info := ParseNginxIngressProductInfo(res) + fileName := "product_info.json" + jsonBytes, err := json.MarshalIndent(product_info, "", " ") + if err != nil { + jobResult.Error = err + } else { + jobResult.Files[filepath.Join(dc.BaseDir, fileName)] = jsonBytes + } + ch <- jobResult + } + } + } + } + } + } + }, + }, } return jobList } From 362aaaec13a9aabe985cd30a7d6797bc5ff6ab89 Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Thu, 9 Oct 2025 14:16:41 +1300 Subject: [PATCH 02/11] Update pkg/jobs/common_job_list.go Potential panic if hostnames slice is empty. Should check len(hostnames) > 0 before accessing hostnames[0]. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/jobs/common_job_list.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index b97a93d..692ed65 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -459,9 +459,13 @@ func CommonJobList() []Job { if err == nil && versionInfo != nil { k8sVersion = versionInfo.GitVersion } + hostname := "N/A" + if len(hostnames) > 0 { + hostname = hostnames[0] + } platformInfo := data_collector.PlatformInfo{ PlatformType: fmt.Sprintf("%s, k8s version: %s", platformType, k8sVersion), - Hostname: hostnames[0], + Hostname: hostname, SerialNumber: "N/A", } From 5c99fdfc4ff64ddc3e48a9606ab0b535923f806d Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Thu, 9 Oct 2025 14:18:03 +1300 Subject: [PATCH 03/11] Update pkg/jobs/common_job_list.go Empty error handling with only a comment. Should either handle the error appropriately or remove the empty block. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/jobs/common_job_list.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index 692ed65..79df294 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -436,7 +436,9 @@ func CommonJobList() []Job { var nodeList corev1.NodeList err := json.Unmarshal(jsonResult, &nodeList) if err != nil { - // handle error + dc.Logger.Printf("\tCould not unmarshal nodes information: %v\n", err) + ch <- jobResult + return } var hostnames []string From 74f64465912f2477a0c0a2339f96101d715dafb0 Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Thu, 9 Oct 2025 14:28:01 +1300 Subject: [PATCH 04/11] Update pkg/data_collector/data_collector.go Using log.Fatalf will terminate the entire program if product_info.json doesn't exist. Should handle the error gracefully and continue with default values instead of crashing. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/data_collector/data_collector.go | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/pkg/data_collector/data_collector.go b/pkg/data_collector/data_collector.go index 6592a53..ae790f3 100644 --- a/pkg/data_collector/data_collector.go +++ b/pkg/data_collector/data_collector.go @@ -332,30 +332,29 @@ func (c *DataCollector) GenerateManifest(product string, startTime time.Time, jo // Read and parse product_info.json filename := filepath.Join(c.BaseDir, "product_info.json") file, err := os.Open(filename) - if err != nil { - log.Fatalf("failed to open file: %v", err) - } - defer file.Close() - var info ProductInfo - decoder := json.NewDecoder(file) - if err := decoder.Decode(&info); err != nil { - log.Fatalf("failed to decode JSON: %v", err) + if err != nil { + c.Logger.Printf("Warning: failed to open product_info.json: %v. Using default values.", err) + } else { + defer file.Close() + decoder := json.NewDecoder(file) + if err := decoder.Decode(&info); err != nil { + c.Logger.Printf("Warning: failed to decode product_info.json: %v. Using default values.", err) + } } filename = filepath.Join(c.BaseDir, "platform_info.json") file, err = os.Open(filename) - if err != nil { - log.Fatalf("failed to open file: %v", err) - } - defer file.Close() - var platformInfo PlatformInfo - decoder = json.NewDecoder(file) - if err = decoder.Decode(&platformInfo); err != nil { - log.Fatalf("failed to decode JSON: %v", err) + if err != nil { + c.Logger.Printf("Warning: failed to open platform_info.json: %v. Using default values.", err) + } else { + defer file.Close() + decoder = json.NewDecoder(file) + if err = decoder.Decode(&platformInfo); err != nil { + c.Logger.Printf("Warning: failed to decode platform_info.json: %v. Using default values.", err) + } } - manifest := Manifest{ Version: "1.2", // Match the schema version Timestamp: TimestampInfo{ From cce3fde2c57d4d941a8ce9d3faf4635e93acd160 Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Thu, 9 Oct 2025 14:36:20 +1300 Subject: [PATCH 05/11] Update pkg/jobs/common_job_list.go The jsonResult variable contains marshaled data from the result variable, but then it's being unmarshaled back into a NodeList. This is unnecessary - the original result should be used directly since it's already a NodeList type. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/jobs/common_job_list.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index 79df294..ea70ec7 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -433,13 +433,7 @@ func CommonJobList() []Job { } else { jsonResult, _ := json.MarshalIndent(result, "", " ") jobResult.Files[filepath.Join(dc.BaseDir, "k8s", "nodes.json")] = jsonResult - var nodeList corev1.NodeList - err := json.Unmarshal(jsonResult, &nodeList) - if err != nil { - dc.Logger.Printf("\tCould not unmarshal nodes information: %v\n", err) - ch <- jobResult - return - } + nodeList := result var hostnames []string var platformType string From 59b51ef83186c0a4c98c5cfa47b0f4597f982200 Mon Sep 17 00:00:00 2001 From: Madhu RAJAGOPAL Date: Thu, 9 Oct 2025 14:45:23 +1300 Subject: [PATCH 06/11] Fix: initialize json decoder before use --- pkg/data_collector/data_collector.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/data_collector/data_collector.go b/pkg/data_collector/data_collector.go index ae790f3..8fbec5a 100644 --- a/pkg/data_collector/data_collector.go +++ b/pkg/data_collector/data_collector.go @@ -350,7 +350,7 @@ func (c *DataCollector) GenerateManifest(product string, startTime time.Time, jo c.Logger.Printf("Warning: failed to open platform_info.json: %v. Using default values.", err) } else { defer file.Close() - decoder = json.NewDecoder(file) + decoder := json.NewDecoder(file) if err = decoder.Decode(&platformInfo); err != nil { c.Logger.Printf("Warning: failed to decode platform_info.json: %v. Using default values.", err) } From 7f5aa42645a122dd8c7f7daeb581f9b1b44802e0 Mon Sep 17 00:00:00 2001 From: Madhu RAJAGOPAL Date: Thu, 9 Oct 2025 14:56:04 +1300 Subject: [PATCH 07/11] FIx: break out of the for loop once platform info of control node has been collected --- pkg/jobs/common_job_list.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index ea70ec7..00e938f 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -435,18 +435,19 @@ func CommonJobList() []Job { jobResult.Files[filepath.Join(dc.BaseDir, "k8s", "nodes.json")] = jsonResult nodeList := result - var hostnames []string + var hostname string var platformType string for _, node := range nodeList.Items { labels := node.ObjectMeta.Labels // If the node does NOT have the control-plane label, include its name if labels["node-role.kubernetes.io/control-plane"] == "" { - hostnames = append(hostnames, node.ObjectMeta.Name) + hostname = node.ObjectMeta.Name osImage := node.Status.NodeInfo.OSImage osType := node.Status.NodeInfo.OperatingSystem osArch := node.Status.NodeInfo.Architecture platformType = fmt.Sprintf("%s %s/%s", osImage, osType, osArch) + break } } const platformInfoFilename = "platform_info.json" @@ -455,10 +456,7 @@ func CommonJobList() []Job { if err == nil && versionInfo != nil { k8sVersion = versionInfo.GitVersion } - hostname := "N/A" - if len(hostnames) > 0 { - hostname = hostnames[0] - } + platformInfo := data_collector.PlatformInfo{ PlatformType: fmt.Sprintf("%s, k8s version: %s", platformType, k8sVersion), Hostname: hostname, From 91e48fb8fd243da19a8d7d072a876cc5d454b762 Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Fri, 10 Oct 2025 11:15:39 +1300 Subject: [PATCH 08/11] Update pkg/jobs/common_job_list.go Remove superfluous log message introduced by co-pilot review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/jobs/common_job_list.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index 00e938f..3a14b4f 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -468,7 +468,6 @@ func CommonJobList() []Job { dc.Logger.Printf("\tCould not marshal platformInfo: %v\n", err) } else { jobResult.Files[filepath.Join(dc.BaseDir, platformInfoFilename)] = platformInfoBytes - dc.Logger.Printf("\tPlatform Info: %s\n", platformInfoBytes) } } ch <- jobResult From d3b2523dec8d8593e4461800a623b7e16007220d Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Fri, 10 Oct 2025 11:38:56 +1300 Subject: [PATCH 09/11] Update pkg/jobs/common_job_list.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Check the existence of the `control-plane` label to confirm that it is the control node. Co-authored-by: Daniel Aresté <5310624+dareste@users.noreply.github.com> --- pkg/jobs/common_job_list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/jobs/common_job_list.go b/pkg/jobs/common_job_list.go index 3a14b4f..6b6ce23 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -440,7 +440,7 @@ func CommonJobList() []Job { for _, node := range nodeList.Items { labels := node.ObjectMeta.Labels // If the node does NOT have the control-plane label, include its name - if labels["node-role.kubernetes.io/control-plane"] == "" { + if _, exists := labels["node-role.kubernetes.io/control-plane"]; exists { hostname = node.ObjectMeta.Name osImage := node.Status.NodeInfo.OSImage osType := node.Status.NodeInfo.OperatingSystem From 5c6db73809a77bd70285f0b2b8af5e3389bd88bd Mon Sep 17 00:00:00 2001 From: Madhu Rajagopal Date: Fri, 10 Oct 2025 11:41:11 +1300 Subject: [PATCH 10/11] Update pkg/jobs/nic_job_list.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minor camel case fix Co-authored-by: Daniel Aresté <5310624+dareste@users.noreply.github.com> --- pkg/jobs/nic_job_list.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/jobs/nic_job_list.go b/pkg/jobs/nic_job_list.go index e16682b..75f2fe5 100644 --- a/pkg/jobs/nic_job_list.go +++ b/pkg/jobs/nic_job_list.go @@ -223,9 +223,9 @@ func NICJobList() []Job { jobResult.Error = err dc.Logger.Printf("\tCommand execution %s failed for pod %s in namespace %s: %v\n", command, pod.Name, namespace, err) } else { - product_info := ParseNginxIngressProductInfo(res) + productInfo := ParseNginxIngressProductInfo(res) fileName := "product_info.json" - jsonBytes, err := json.MarshalIndent(product_info, "", " ") + jsonBytes, err := json.MarshalIndent(productInfo, "", " ") if err != nil { jobResult.Error = err } else { From 1548b8076bef7d2347be00e0dc101a312262abe9 Mon Sep 17 00:00:00 2001 From: Madhu RAJAGOPAL Date: Fri, 10 Oct 2025 12:38:16 +1300 Subject: [PATCH 11/11] Fix: run exec command only in nginx-ingress controller for the new job of product-platform-info --- pkg/jobs/nic_job_list.go | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/pkg/jobs/nic_job_list.go b/pkg/jobs/nic_job_list.go index 75f2fe5..f28556e 100644 --- a/pkg/jobs/nic_job_list.go +++ b/pkg/jobs/nic_job_list.go @@ -218,20 +218,22 @@ func NICJobList() []Job { for _, pod := range pods.Items { if strings.Contains(pod.Name, "ingress") { for _, container := range pod.Spec.Containers { - res, err := dc.PodExecutor(namespace, pod.Name, container.Name, command, ctx) - if err != nil { - jobResult.Error = err - dc.Logger.Printf("\tCommand execution %s failed for pod %s in namespace %s: %v\n", command, pod.Name, namespace, err) - } else { - productInfo := ParseNginxIngressProductInfo(res) - fileName := "product_info.json" - jsonBytes, err := json.MarshalIndent(productInfo, "", " ") + if container.Name == "nginx-ingress" { + res, err := dc.PodExecutor(namespace, pod.Name, container.Name, command, ctx) if err != nil { jobResult.Error = err + dc.Logger.Printf("\tCommand execution %s failed for pod %s in namespace %s: %v\n", command, pod.Name, namespace, err) } else { - jobResult.Files[filepath.Join(dc.BaseDir, fileName)] = jsonBytes + productInfo := ParseNginxIngressProductInfo(res) + fileName := "product_info.json" + jsonBytes, err := json.MarshalIndent(productInfo, "", " ") + if err != nil { + jobResult.Error = err + } else { + jobResult.Files[filepath.Join(dc.BaseDir, fileName)] = jsonBytes + } + ch <- jobResult } - ch <- jobResult } } }