diff --git a/pkg/data_collector/data_collector.go b/pkg/data_collector/data_collector.go index 340caba..8fbec5a 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,43 @@ 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) + var info ProductInfo + 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) + var platformInfo PlatformInfo + 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{ 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..6b6ce23 100644 --- a/pkg/jobs/common_job_list.go +++ b/pkg/jobs/common_job_list.go @@ -433,6 +433,42 @@ func CommonJobList() []Job { } else { jsonResult, _ := json.MarshalIndent(result, "", " ") jobResult.Files[filepath.Join(dc.BaseDir, "k8s", "nodes.json")] = jsonResult + nodeList := result + + 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 _, exists := labels["node-role.kubernetes.io/control-plane"]; exists { + 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" + 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: hostname, + 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 + } } ch <- jobResult }, diff --git a/pkg/jobs/nic_job_list.go b/pkg/jobs/nic_job_list.go index 4538191..f28556e 100644 --- a/pkg/jobs/nic_job_list.go +++ b/pkg/jobs/nic_job_list.go @@ -24,6 +24,7 @@ import ( "encoding/json" "fmt" "path/filepath" + "regexp" "strings" "time" @@ -32,6 +33,27 @@ import ( 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{ { @@ -182,6 +204,44 @@ 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 { + 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 { + 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 + } + } + } + } + } + } + } + }, + }, } return jobList }