Files
troubleshoot/pkg/analyze/host_system_packages.go
Salah Al Saleh 97dcae9fc7 Ability to use sprig functions in analyzer templates (#1745)
* Ability to use sprig functions in analyzer templates
2025-02-21 08:10:46 -08:00

234 lines
5.7 KiB
Go

package analyzer
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"unicode"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/util"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeHostSystemPackages struct {
hostAnalyzer *troubleshootv1beta2.SystemPackagesAnalyze
}
func (a *AnalyzeHostSystemPackages) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "System Packages")
}
func (a *AnalyzeHostSystemPackages) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostSystemPackages) Analyze(
getCollectedFileContents func(string) ([]byte, error), findFiles getChildCollectedFileContents,
) ([]*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "packages"
}
localPath := fmt.Sprintf("%s/%s-packages.json", collect.NodeInfoBaseDir, collectorName)
packagesFileName := fmt.Sprintf("%s-packages.json", collectorName)
collectedContents, err := retrieveCollectedContents(
getCollectedFileContents,
localPath,
collect.NodeInfoBaseDir,
packagesFileName,
)
if err != nil {
return []*AnalyzeResult{{Title: a.Title()}}, err
}
var results []*AnalyzeResult
for _, content := range collectedContents {
currentTitle := a.Title()
if content.NodeName != "" {
currentTitle = fmt.Sprintf("%s - Node %s", a.Title(), content.NodeName)
}
result, err := a.analyzeSingleNode(content, currentTitle)
if err != nil {
return nil, errors.Wrapf(err, "failed to analyze system packages for %s", currentTitle)
}
if result != nil {
results = append(results, result...)
}
}
return results, nil
}
func getSystemPackageTemplateMap(pkg collect.SystemPackage, osName string, osVersion string) map[string]interface{} {
osVersionParts := strings.Split(osVersion, ".")
osVersionMajor, _ := strconv.ParseInt(osVersionParts[0], 10, 64)
var osVersionMinor int64
if len(osVersionParts) > 1 {
osVersionMinor, _ = strconv.ParseInt(osVersionParts[1], 10, 64)
}
m := map[string]interface{}{
"OS": osName,
"OSVersion": osVersion,
"OSVersionMajor": osVersionMajor,
"OSVersionMinor": osVersionMinor,
"Name": pkg.Name,
"Error": pkg.Error,
"ExitCode": pkg.ExitCode,
"IsInstalled": isSystemPackageInstalled(pkg),
}
for k, v := range getSystemPackageDetailsMap(pkg) {
m[k] = v
}
return m
}
func isSystemPackageInstalled(pkg collect.SystemPackage) bool {
if pkg.Error != "" {
return false
}
if pkg.ExitCode != "0" {
return false
}
if strings.Contains(pkg.Details, "not installed") {
return false
}
if strings.Contains(pkg.Details, "No matching Packages") {
return false
}
return true
}
func getSystemPackageDetailsMap(pkg collect.SystemPackage) map[string]string {
// TODO: handle multiline values
m := map[string]string{}
buffer := bytes.NewBuffer([]byte(pkg.Details))
scanner := bufio.NewScanner(buffer)
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
key := parts[0]
// key should start with an upper case
if !unicode.IsUpper([]rune(key)[0]) {
continue
}
// key shouldn't start with a space
if unicode.IsSpace([]rune(key)[0]) {
continue
}
// sanitize the key and value
key = strings.ReplaceAll(key, " ", "")
key = strings.ReplaceAll(key, "-", "")
value := strings.TrimSpace(parts[1])
if key == "" || value == "" {
continue
}
m[key] = value
}
return m
}
func compareSystemPackagesConditionalToActual(conditional string, templateMap map[string]interface{}) (res bool, err error) {
if conditional == "" {
return true, nil
}
when, err := util.RenderTemplate(conditional, templateMap)
if err != nil {
return false, errors.Wrap(err, "failed to render when template")
}
t, err := strconv.ParseBool(when)
if err != nil {
return false, errors.Wrap(err, "failed to parse templated when expression as a boolean")
}
return t, nil
}
func (a *AnalyzeHostSystemPackages) analyzeSingleNode(content collectedContent, currentTitle string) ([]*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
var info collect.SystemPackagesInfo
if err := json.Unmarshal(content.Data, &info); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal system packages info from %s", currentTitle)
}
allResults := []*AnalyzeResult{}
for _, pkg := range info.Packages {
templateMap := getSystemPackageTemplateMap(pkg, info.OS, info.OSVersion)
for _, outcome := range hostAnalyzer.Outcomes {
r := AnalyzeResult{}
when := ""
switch {
case outcome.Fail != nil:
r.IsFail = true
r.Message = outcome.Fail.Message
r.URI = outcome.Fail.URI
when = outcome.Fail.When
case outcome.Warn != nil:
r.IsWarn = true
r.Message = outcome.Warn.Message
r.URI = outcome.Warn.URI
when = outcome.Warn.When
case outcome.Pass != nil:
r.IsPass = true
r.Message = outcome.Pass.Message
r.URI = outcome.Pass.URI
when = outcome.Pass.When
default:
return nil, errors.New("invalid outcome")
}
match, err := compareSystemPackagesConditionalToActual(when, templateMap)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", when)
}
if !match {
continue
}
r.Title = currentTitle
// template the message
renderedMsg, err := util.RenderTemplate(r.Message, templateMap)
if err != nil {
return nil, errors.Wrap(err, "failed to create new message template")
}
r.Message = renderedMsg
// add to results, break and check the next pod
allResults = append(allResults, &r)
break
}
}
return allResults, nil
}