Files
troubleshoot/pkg/collect/host_collector.go
Diamon Wiggins 8105fa00e9 Refactor Remote Host Collection (#1633)
* refactor remote collectors

* add remotecollect params struct

* remove commented checkrbac function

* removed unused function

* add temp comments

* refactor to not require RemoteCollect method per collector

* removed unneeded param

* removed unneeded param

* more refactor

* more refactor

* remove unneeded function

* remove debug print

* fix analyzer results

* move rbac to separate file

* be more specific with rbac function name

* fix imports

* fix node list file

* make k8s rest client config consistent with in cluster collection

* add ctx and otel tracing

* add test for allCollectedData

* move runHostCollectorsInPod to spec instead of metadata

* make generate

* fix broken references to supportbundle metadata

* add e2e tests

* update loader tests

* fix tests

* fix hostos remote collector spec

* update remoteHostCollectrs.yaml

---------

Co-authored-by: Dexter Yan <yanshaocong@gmail.com>
2024-10-09 18:38:49 +13:00

241 lines
8.4 KiB
Go

package collect
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"golang.org/x/sync/errgroup"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
type HostCollector interface {
Title() string
IsExcluded() (bool, error)
Collect(progressChan chan<- interface{}) (map[string][]byte, error)
}
type RemoteCollectParams struct {
ProgressChan chan<- interface{}
HostCollector *troubleshootv1beta2.HostCollect
BundlePath string
ClientConfig *rest.Config // specify actual type
Image string
PullPolicy string // specify actual type if needed
Timeout time.Duration // specify duration type if needed
LabelSelector string
NamePrefix string
Namespace string
Title string
}
func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath string) (HostCollector, bool) {
switch {
case collector.CPU != nil:
return &CollectHostCPU{collector.CPU, bundlePath}, true
case collector.Memory != nil:
return &CollectHostMemory{collector.Memory, bundlePath}, true
case collector.TCPLoadBalancer != nil:
return &CollectHostTCPLoadBalancer{collector.TCPLoadBalancer, bundlePath}, true
case collector.HTTPLoadBalancer != nil:
return &CollectHostHTTPLoadBalancer{collector.HTTPLoadBalancer, bundlePath}, true
case collector.DiskUsage != nil:
return &CollectHostDiskUsage{collector.DiskUsage, bundlePath}, true
case collector.TCPPortStatus != nil:
return &CollectHostTCPPortStatus{collector.TCPPortStatus, bundlePath}, true
case collector.UDPPortStatus != nil:
return &CollectHostUDPPortStatus{collector.UDPPortStatus, bundlePath}, true
case collector.HTTP != nil:
return &CollectHostHTTP{collector.HTTP, bundlePath}, true
case collector.Time != nil:
return &CollectHostTime{collector.Time, bundlePath}, true
case collector.BlockDevices != nil:
return &CollectHostBlockDevices{collector.BlockDevices, bundlePath}, true
case collector.SystemPackages != nil:
return &CollectHostSystemPackages{collector.SystemPackages, bundlePath}, true
case collector.KernelModules != nil:
return &CollectHostKernelModules{
hostCollector: collector.KernelModules,
BundlePath: bundlePath,
loadable: kernelModulesLoadable{},
loaded: kernelModulesLoaded{},
}, true
case collector.TCPConnect != nil:
return &CollectHostTCPConnect{collector.TCPConnect, bundlePath}, true
case collector.IPV4Interfaces != nil:
return &CollectHostIPV4Interfaces{collector.IPV4Interfaces, bundlePath}, true
case collector.SubnetAvailable != nil:
return &CollectHostSubnetAvailable{collector.SubnetAvailable, bundlePath}, true
case collector.FilesystemPerformance != nil:
return &CollectHostFilesystemPerformance{collector.FilesystemPerformance, bundlePath}, true
case collector.Certificate != nil:
return &CollectHostCertificate{collector.Certificate, bundlePath}, true
case collector.CertificatesCollection != nil:
return &CollectHostCertificatesCollection{collector.CertificatesCollection, bundlePath}, true
case collector.HostServices != nil:
return &CollectHostServices{collector.HostServices, bundlePath}, true
case collector.HostOS != nil:
return &CollectHostOS{collector.HostOS, bundlePath}, true
case collector.HostRun != nil:
return &CollectHostRun{collector.HostRun, bundlePath}, true
case collector.HostCopy != nil:
return &CollectHostCopy{collector.HostCopy, bundlePath}, true
case collector.HostKernelConfigs != nil:
return &CollectHostKernelConfigs{collector.HostKernelConfigs, bundlePath}, true
case collector.HostJournald != nil:
return &CollectHostJournald{collector.HostJournald, bundlePath}, true
case collector.HostCGroups != nil:
return &CollectHostCGroups{collector.HostCGroups, bundlePath}, true
case collector.HostDNS != nil:
return &CollectHostDNS{collector.HostDNS, bundlePath}, true
default:
return nil, false
}
}
func hostCollectorTitleOrDefault(meta troubleshootv1beta2.HostCollectorMeta, defaultTitle string) string {
if meta.CollectorName != "" {
return meta.CollectorName
}
return defaultTitle
}
func RemoteHostCollect(ctx context.Context, params RemoteCollectParams) (map[string][]byte, error) {
scheme := runtime.NewScheme()
if err := corev1.AddToScheme(scheme); err != nil {
return nil, errors.Wrap(err, "failed to add runtime scheme")
}
client, err := kubernetes.NewForConfig(params.ClientConfig)
if err != nil {
return nil, err
}
runner := &podRunner{
client: client,
scheme: scheme,
image: params.Image,
pullPolicy: params.PullPolicy,
waitInterval: remoteCollectorDefaultInterval,
}
// Get all the nodes where we should run.
nodes, err := listNodesNamesInSelector(ctx, client, params.LabelSelector)
if err != nil {
return nil, errors.Wrap(err, "failed to get the list of nodes matching a nodeSelector")
}
if params.NamePrefix == "" {
params.NamePrefix = remoteCollectorNamePrefix
}
result, err := runRemote(ctx, runner, nodes, params.HostCollector, names.SimpleNameGenerator, params.NamePrefix, params.Namespace)
if err != nil {
return nil, errors.Wrap(err, "failed to run collector remotely")
}
allCollectedData := mapCollectorResultToOutput(result, params)
output := NewResult()
// save the first result we find in the node and save it
for node, result := range allCollectedData {
var nodeResult map[string]string
if err := json.Unmarshal(result, &nodeResult); err != nil {
return nil, errors.Wrap(err, "failed to marshal node results")
}
for file, collectorResult := range nodeResult {
directory := filepath.Dir(file)
fileName := filepath.Base(file)
// expected file name for remote collectors will be the normal path separated by / and the node name
output.SaveResult(params.BundlePath, fmt.Sprintf("%s/%s/%s", directory, node, fileName), bytes.NewBufferString(collectorResult))
}
}
// check if NODE_LIST_FILE exists
_, err = os.Stat(constants.NODE_LIST_FILE)
// if it not exists, save the nodes list
if err != nil {
nodesBytes, err := json.MarshalIndent(HostOSInfoNodes{Nodes: nodes}, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal host os info nodes")
}
output.SaveResult(params.BundlePath, constants.NODE_LIST_FILE, bytes.NewBuffer(nodesBytes))
}
return output, nil
}
func runRemote(ctx context.Context, runner runner, nodes []string, collector *troubleshootv1beta2.HostCollect, nameGenerator names.NameGenerator, namePrefix string, namespace string) (map[string][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make(chan map[string][]byte, len(nodes))
for _, node := range nodes {
node := node
g.Go(func() error {
// May need to evaluate error and log warning. Otherwise any error
// here will cancel the context of other goroutines and no results
// will be returned.
return runner.run(ctx, collector, namespace, nameGenerator.GenerateName(namePrefix+"-"), node, results)
})
}
// Wait for all collectors to complete or return the first error.
if err := g.Wait(); err != nil {
return nil, errors.Wrap(err, "failed remote collection")
}
close(results)
output := make(map[string][]byte)
for result := range results {
r := result
for k, v := range r {
output[k] = v
}
}
return output, nil
}
func mapCollectorResultToOutput(result map[string][]byte, params RemoteCollectParams) map[string][]byte {
allCollectedData := make(map[string][]byte)
for k, v := range result {
if curBytes, ok := allCollectedData[k]; ok {
var curResults map[string]string
if err := json.Unmarshal(curBytes, &curResults); err != nil {
params.ProgressChan <- errors.Errorf("failed to read existing results for collector %s: %v\n", params.Title, err)
continue
}
var newResults map[string]string
if err := json.Unmarshal(v, &newResults); err != nil {
params.ProgressChan <- errors.Errorf("failed to read new results for collector %s: %v\n", params.Title, err)
continue
}
for file, data := range newResults {
curResults[file] = data
}
combinedResults, err := json.Marshal(curResults)
if err != nil {
params.ProgressChan <- errors.Errorf("failed to combine results for collector %s: %v\n", params.Title, err)
continue
}
allCollectedData[k] = combinedResults
} else {
allCollectedData[k] = v
}
}
return allCollectedData
}