diff --git a/cmd/preflight/cli/run.go b/cmd/preflight/cli/run.go index 6fb7136fc..5a3dc58c8 100644 --- a/cmd/preflight/cli/run.go +++ b/cmd/preflight/cli/run.go @@ -86,15 +86,16 @@ func runPreflights(v *viper.Viper, arg string) error { return errors.Wrapf(err, "failed to parse %s", arg) } - preflightSpec := obj.(*troubleshootv1beta2.Preflight) + var collectResults preflight.CollectResult + preflightSpecName := "" + finishedCh := make(chan bool, 1) + progressCh := make(chan interface{}, 0) // non-zero buffer will result in missed messages s := spin.New() - finishedCh := make(chan bool, 1) - progressChan := make(chan interface{}, 0) // non-zero buffer will result in missed messages go func() { for { select { - case msg, ok := <-progressChan: + case msg, ok := <-progressCh: if !ok { continue } @@ -114,60 +115,105 @@ func runPreflights(v *viper.Viper, arg string) error { } } }() + defer func() { close(finishedCh) + close(progressCh) }() + if preflightSpec, ok := obj.(*troubleshootv1beta2.Preflight); ok { + r, err := collectInCluster(preflightSpec, finishedCh, progressCh) + if err != nil { + return errors.Wrap(err, "failed to collect in cluster") + } + collectResults = *r + preflightSpecName = preflightSpec.Name + } else if hostPreflightSpec, ok := obj.(*troubleshootv1beta2.HostPreflight); ok { + r, err := collectHost(hostPreflightSpec, finishedCh, progressCh) + if err != nil { + return errors.Wrap(err, "failed to collect from host") + } + collectResults = *r + preflightSpecName = hostPreflightSpec.Name + } + + if collectResults == nil { + return errors.New("no results") + } + + analyzeResults := collectResults.Analyze() + + if preflightSpec, ok := obj.(*troubleshootv1beta2.Preflight); ok { + if preflightSpec.Spec.UploadResultsTo != "" { + err := uploadResults(preflightSpec.Spec.UploadResultsTo, analyzeResults) + if err != nil { + progressCh <- err + } + } + } + + finishedCh <- true + + if v.GetBool("interactive") { + if len(analyzeResults) == 0 { + return errors.New("no data has been collected") + } + return showInteractiveResults(preflightSpecName, analyzeResults) + } + + return showStdoutResults(v.GetString("format"), preflightSpecName, analyzeResults) +} + +func collectInCluster(preflightSpec *troubleshootv1beta2.Preflight, finishedCh chan bool, progressCh chan interface{}) (*preflight.CollectResult, error) { + v := viper.GetViper() + restConfig, err := k8sutil.GetRESTConfig() if err != nil { - return errors.Wrap(err, "failed to convert kube flags to rest config") + return nil, errors.Wrap(err, "failed to convert kube flags to rest config") } collectOpts := preflight.CollectOpts{ Namespace: v.GetString("namespace"), IgnorePermissionErrors: v.GetBool("collect-without-permissions"), - ProgressChan: progressChan, + ProgressChan: progressCh, KubernetesRestConfig: restConfig, } if v.GetString("since") != "" || v.GetString("since-time") != "" { - err := parseTimeFlags(v, progressChan, preflightSpec.Spec.Collectors) + err := parseTimeFlags(v, progressCh, preflightSpec.Spec.Collectors) if err != nil { - return err + return nil, err } } collectResults, err := preflight.Collect(collectOpts, preflightSpec) if err != nil { - if !collectResults.IsRBACAllowed { + if !collectResults.IsRBACAllowed() { if preflightSpec.Spec.UploadResultsTo != "" { - err := uploadErrors(preflightSpec.Spec.UploadResultsTo, collectResults.Collectors) + clusterCollectResults := collectResults.(preflight.ClusterCollectResult) + err := uploadErrors(preflightSpec.Spec.UploadResultsTo, clusterCollectResults.Collectors) if err != nil { - progressChan <- err + progressCh <- err } } } - return err + return nil, err } - analyzeResults := collectResults.Analyze() - if preflightSpec.Spec.UploadResultsTo != "" { - err := uploadResults(preflightSpec.Spec.UploadResultsTo, analyzeResults) - if err != nil { - progressChan <- err - } - } + return &collectResults, nil +} - finishedCh <- true +func collectHost(hostPreflightSpec *troubleshootv1beta2.HostPreflight, finishedCh chan bool, progressCh chan interface{}) (*preflight.CollectResult, error) { + collectOpts := preflight.CollectOpts{ + ProgressChan: progressCh, + } - if v.GetBool("interactive") { - if len(analyzeResults) == 0 { - return errors.New("no data has been collected") - } - return showInteractiveResults(preflightSpec.Name, analyzeResults) + collectResults, err := preflight.CollectHost(collectOpts, hostPreflightSpec) + if err != nil { + return nil, errors.Wrap(err, "failed to collect from host") } - return showStdoutResults(v.GetString("format"), preflightSpec.Name, analyzeResults) + return &collectResults, nil } func parseTimeFlags(v *viper.Viper, progressChan chan interface{}, collectors []*troubleshootv1beta2.Collect) error { diff --git a/examples/preflight/host-cpu.yaml b/examples/preflight/host-cpu.yaml new file mode 100644 index 000000000..1cd09aae9 --- /dev/null +++ b/examples/preflight/host-cpu.yaml @@ -0,0 +1,22 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: cpu +spec: + collectors: + - cpu: {} + analyzers: + - cpu: + outcomes: + - fail: + when: "physical < 4" + message: At least 4 physical CPU cores are required + - fail: + when: "logical < 8" + message: At least 8 CPU cores are required + - warn: + when: "count < 16" + message: At least 16 CPU cores preferred + - pass: + message: This server has sufficient CPU cores + diff --git a/examples/preflight/host-disk-usage.yaml b/examples/preflight/host-disk-usage.yaml new file mode 100644 index 000000000..abd8b3d6f --- /dev/null +++ b/examples/preflight/host-disk-usage.yaml @@ -0,0 +1,33 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: diskUsage +spec: + collectors: + - diskUsage: + collectorName: ephemeral + path: /var/lib/kubelet + analyzers: + - diskUsage: + collectorName: ephemeral + outcomes: + - fail: + when: "total < 20Gi" + message: /var/lib/kubelet has less than 20Gi of total space + - fail: + when: "available < 10Gi" + message: /var/lib/kubelet has less than 10Gi of disk space available + - fail: + when: "used/total > 70%" + message: /var/lib/kubelet is more than 70% full + - warn: + when: "total < 40Gi" + message: /var/lib/kubelet has less than 40Gi of total space + - warn: + when: "used/total > 60%" + message: /var/lib/kubelet is more than 60% full + - pass: + when: "available/total >= 90%" + message: /var/lib/kubelet has more than 90% available + - pass: + message: /var/lib/kubelet has sufficient disk space available diff --git a/examples/preflight/host-memory.yaml b/examples/preflight/host-memory.yaml new file mode 100644 index 000000000..4f883efaf --- /dev/null +++ b/examples/preflight/host-memory.yaml @@ -0,0 +1,19 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: memory +spec: + collectors: + - memory: + collectorName: memory + analyzers: + - memory: + outcomes: + - fail: + when: "< 8Gi" + message: At least 8Gi of memory is required + - warn: + when: "< 32Gi" + message: At least 32Gi of memory is recommended + - pass: + message: The system has as sufficient memory diff --git a/examples/preflight/host-port.yaml b/examples/preflight/host-port.yaml new file mode 100644 index 000000000..33f75e7bb --- /dev/null +++ b/examples/preflight/host-port.yaml @@ -0,0 +1,30 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: port +spec: + collectors: + - tcpPortStatus: + collectorName: k8s + port: 7443 + analyzers: + - tcpPortStatus: + collectorName: k8s + outcomes: + - fail: + when: "connection-refused" + message: Connection to port 7443 was refused. + - fail: + when: "address-in-use" + message: Another process was already listening on port 7443. + - fail: + when: "connection-timeout" + message: Timed out connecting to port 7443. Check your firewall. + - fail: + when: "error" + message: Unexpected port status + - pass: + when: "connected" + message: Port 7443 is open + - warn: + message: Unexpected port status diff --git a/examples/preflight/host-tcp-load-balancer.yaml b/examples/preflight/host-tcp-load-balancer.yaml new file mode 100644 index 000000000..4a8dcdf2d --- /dev/null +++ b/examples/preflight/host-tcp-load-balancer.yaml @@ -0,0 +1,31 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: loadbalancer +spec: + collectors: + - tcpLoadBalancer: + collectorName: loadbalancer + port: 7443 + address: 10.128.0.29:7444 + analyzers: + - tcpLoadBalancer: + collectorName: loadbalancer + outcomes: + - fail: + when: "connection-refused" + message: Connection to port 7443 via load balancer was refused. + - fail: + when: "address-in-use" + message: Another process was already listening on port 7443. + - fail: + when: "connection-timeout" + message: Timed out connecting to port 7443 via load balancer. Check your firewall. + - fail: + when: "error" + message: Unexpected port status + - pass: + when: "connected" + message: Successfully connected to port 7443 via load balancer + - warn: + message: Unexpected port status diff --git a/examples/preflight/sample-host-preflight.yaml b/examples/preflight/sample-host-preflight.yaml new file mode 100644 index 000000000..659a35333 --- /dev/null +++ b/examples/preflight/sample-host-preflight.yaml @@ -0,0 +1,51 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: example +spec: + collectors: + - tcpLoadBalancer: + collectorName: LB1 + address: 10.1.1.1 + port: 6443 + timeout: 5000ms + - diskUsage: + collectorName: ephemeral + path: /var/lib/kubelet + analyzers: + - cpu: + outcomes: + - fail: + when: "< 4" + message: This server has less than 4 CPU cores, and we require 8, but recommend 16 + - warn: + when: "< 16" + message: This server has at least 4 CPU cores, but we recommend 16 or more + - pass: + message: This server has sufficient CPU cores + - tcpLoadBalancer: + collectorName: LB1 + outcomes: + - fail: + when: "connection-timeout" + message: The TCP Load Balancer is not forwarding traffic to this server. + - fail: + when: "address-in-use" + message: The local port is not available to validate the Load Balancer configuration. + - pass: + when: "connected" + message: The specified TCP Load Balancer appears to be properly forwarding traffic to this server. + - diskUsage: + collectorName: ephemeral + outcomes: + - fail: + when: "total < 20Gi" + message: /var/lib/kubelet has less than 20Gi of total space + - fail: + when: "available < 10Gi" + message: /var/lib/kubelet has less than 10Gi of disk space available + - fail: + when: "used/total > 70%" + message: /var/lib/kubelet is more than 70% full + - pass: + message: /var/lib/kubelet has sufficient disk space available diff --git a/go.mod b/go.mod index 1a608adf6..f7c21055c 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/prometheus/procfs v0.0.5 // indirect github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 github.com/segmentio/ksuid v1.0.3 + github.com/shirou/gopsutil v3.20.12+incompatible github.com/spf13/cobra v0.0.5 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.4.0 diff --git a/go.sum b/go.sum index 05aee0bd2..63763602e 100644 --- a/go.sum +++ b/go.sum @@ -414,6 +414,8 @@ github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY= github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shirou/gopsutil v3.20.12+incompatible h1:6VEGkOXP/eP4o2Ilk8cSsX0PhOEfX6leqAnD+urrp9M= +github.com/shirou/gopsutil v3.20.12+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index 775d3fea3..53d548b88 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -40,6 +40,46 @@ func isExcluded(excludeVal multitype.BoolOrString) (bool, error) { return parsed, nil } +func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) { + if hostAnalyzer.CPU != nil { + result, err := analyzeHostCPU(hostAnalyzer.CPU, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } + if hostAnalyzer.TCPLoadBalancer != nil { + result, err := analyzeHostTCPLoadBalancer(hostAnalyzer.TCPLoadBalancer, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } + if hostAnalyzer.DiskUsage != nil { + result, err := analyzeHostDiskUsage(hostAnalyzer.DiskUsage, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } + if hostAnalyzer.Memory != nil { + result, err := analyzeHostMemory(hostAnalyzer.Memory, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } + if hostAnalyzer.TCPPortStatus != nil { + result, err := analyzeHostTCPPortStatus(hostAnalyzer.TCPPortStatus, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } + + return nil, errors.New("invalid analyzer") +} + func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) { if analyzer.ClusterVersion != nil { isExcluded, err := isExcluded(analyzer.ClusterVersion.Exclude) @@ -265,6 +305,6 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont } return []*AnalyzeResult{result}, nil } - return nil, errors.New("invalid analyzer") + return nil, errors.New("invalid analyzer") } diff --git a/pkg/analyze/host_cpu.go b/pkg/analyze/host_cpu.go new file mode 100644 index 000000000..ba315cb64 --- /dev/null +++ b/pkg/analyze/host_cpu.go @@ -0,0 +1,165 @@ +package analyzer + +import ( + "encoding/json" + "strconv" + "strings" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +func analyzeHostCPU(hostAnalyzer *troubleshootv1beta2.CPUAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + contents, err := getCollectedFileContents("system/cpu.json") + if err != nil { + return nil, errors.Wrap(err, "failed to get collected file") + } + + cpuInfo := collect.CPUInfo{} + if err := json.Unmarshal(contents, &cpuInfo); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal cpu info") + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = "Number of CPUs" + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + isMatch, err := compareHostCPUConditionalToActual(outcome.Fail.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount) + if err != nil { + return nil, errors.Wrap(err, "failed to compare") + } + + if isMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + isMatch, err := compareHostCPUConditionalToActual(outcome.Warn.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount) + if err != nil { + return nil, errors.Wrap(err, "failed to compare") + } + + if isMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + isMatch, err := compareHostCPUConditionalToActual(outcome.Pass.When, cpuInfo.LogicalCount, cpuInfo.PhysicalCount) + if err != nil { + return nil, errors.Wrap(err, "failed to compare") + } + + if isMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} + +func compareHostCPUConditionalToActual(conditional string, logicalCount int, physicalCount int) (res bool, err error) { + compareLogical := false + comparePhysical := false + compareUnspecified := false + + comparator := "" + desired := "" + + parts := strings.Split(conditional, " ") + if len(parts) == 3 { + comparator = parts[1] + desired = parts[2] + if strings.ToLower(parts[0]) == "logical" { + compareLogical = true + } else if strings.ToLower(parts[0]) == "physical" { + comparePhysical = true + } else if strings.ToLower(parts[0]) == "count" { + compareUnspecified = true + } + } else if len(parts) == 2 { + compareUnspecified = true + comparator = parts[0] + desired = parts[1] + } + + if !compareLogical && !comparePhysical && !compareUnspecified { + return false, errors.New("unable to parse conditional") + } + + if compareLogical { + return doCompareHostCPU(comparator, desired, logicalCount) + } else if comparePhysical { + return doCompareHostCPU(comparator, desired, physicalCount) + } else { + actual := logicalCount + if physicalCount > logicalCount { + actual = physicalCount + } + + return doCompareHostCPU(comparator, desired, actual) + } +} + +func doCompareHostCPU(operator string, desired string, actual int) (bool, error) { + desiredInt, err := strconv.ParseInt(desired, 10, 64) + if err != nil { + return false, errors.Wrap(err, "failed to parse") + } + + switch operator { + case "<": + return actual < int(desiredInt), nil + case "<=": + return actual <= int(desiredInt), nil + case ">": + return actual > int(desiredInt), nil + case ">=": + return actual >= int(desiredInt), nil + case "=", "==", "===": + return actual == int(desiredInt), nil + } + + return false, errors.New("unknown operator") +} diff --git a/pkg/analyze/host_cpu_test.go b/pkg/analyze/host_cpu_test.go new file mode 100644 index 000000000..c117faaa5 --- /dev/null +++ b/pkg/analyze/host_cpu_test.go @@ -0,0 +1,150 @@ +package analyzer + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_doCompareHostCPU(t *testing.T) { + tests := []struct { + name string + operator string + desired string + actual int + expected bool + }{ + { + name: "< 16", + operator: "<", + desired: "16", + actual: 8, + expected: true, + }, + { + name: "< 8 when actual is 8", + operator: "<", + desired: "8", + actual: 8, + expected: false, + }, + { + name: "<= 8 when actual is 8", + operator: "<=", + desired: "8", + actual: 8, + expected: true, + }, + { + name: "<= 8 when actual is 16", + operator: "<=", + desired: "8", + actual: 16, + expected: false, + }, + { + name: "== 8 when actual is 16", + operator: "==", + desired: "8", + actual: 16, + expected: false, + }, + { + name: "== 8 when actual is 8", + operator: "==", + desired: "8", + actual: 8, + expected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + actual, err := doCompareHostCPU(test.operator, test.desired, test.actual) + req.NoError(err) + + assert.Equal(t, test.expected, actual) + + }) + } +} + +func Test_compareHostCPUConditionalToActual(t *testing.T) { + tests := []struct { + name string + when string + logicalCount int + physicalCount int + expected bool + }{ + { + name: "physical > 4, when physical is 8", + when: "physical > 4", + logicalCount: 0, + physicalCount: 8, + expected: true, + }, + { + name: "physical > 4, when physical is 4", + when: "physical > 4", + logicalCount: 0, + physicalCount: 4, + expected: false, + }, + { + name: "physical > 4, when physical is 3, logical is 6", + when: "physical > 4", + logicalCount: 6, + physicalCount: 3, + expected: false, + }, + { + name: "logical > 4, when physical is 4, logical is 8", + when: "logical > 4", + logicalCount: 8, + physicalCount: 4, + expected: true, + }, + { + name: ">= 4, when physical is 2, logical is 4", + when: ">= 4", + logicalCount: 4, + physicalCount: 2, + expected: true, + }, + { + name: "count < 4, when physical is 2, logical is 4", + when: "count < 4", + logicalCount: 4, + physicalCount: 2, + expected: false, + }, + { + name: "count <= 4, when physical is 2, logical is 4", + when: "count <= 4", + logicalCount: 4, + physicalCount: 2, + expected: true, + }, + { + name: "== 4, physical is 4, logical is 4", + when: "== 4", + logicalCount: 4, + physicalCount: 4, + expected: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + actual, err := compareHostCPUConditionalToActual(test.when, test.logicalCount, test.physicalCount) + req.NoError(err) + + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/pkg/analyze/host_disk_usage.go b/pkg/analyze/host_disk_usage.go new file mode 100644 index 000000000..29889fdf1 --- /dev/null +++ b/pkg/analyze/host_disk_usage.go @@ -0,0 +1,185 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "k8s.io/apimachinery/pkg/api/resource" +) + +func analyzeHostDiskUsage(hostAnalyzer *troubleshootv1beta2.DiskUsageAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + key := collect.HostDiskUsageKey(hostAnalyzer.CollectorName) + contents, err := getCollectedFileContents(key) + if err != nil { + return nil, errors.Wrapf(err, "failed to get collected file %s", key) + } + + diskUsageInfo := collect.DiskUsageInfo{} + if err := json.Unmarshal(contents, &diskUsageInfo); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal disk usage info from %s", key) + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = fmt.Sprintf("Disk Usage %s", hostAnalyzer.CollectorName) + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Fail.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %q", outcome.Fail.When) + } + + if isMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Warn.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %q", outcome.Warn.When) + } + + if isMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + isMatch, err := compareHostDiskUsageConditionalToActual(outcome.Pass.When, diskUsageInfo.TotalBytes, diskUsageInfo.UsedBytes) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %q", outcome.Pass.When) + } + + if isMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} + +func compareHostDiskUsageConditionalToActual(conditional string, totalBytes uint64, usedBytes uint64) (res bool, err error) { + parts := strings.Split(conditional, " ") + if len(parts) != 3 { + return false, fmt.Errorf("conditional must have exactly 3 parts, got %d", len(parts)) + } + stat := strings.ToLower(parts[0]) + comparator := parts[1] + desired := parts[2] + + switch stat { + case "total": + return doCompareHostDiskUsage(comparator, desired, totalBytes) + case "used": + return doCompareHostDiskUsage(comparator, desired, usedBytes) + case "available": + return doCompareHostDiskUsage(comparator, desired, totalBytes-usedBytes) + case "used/total": + used := float64(usedBytes) / float64(totalBytes) + return doCompareHostDiskUsagePercent(comparator, desired, used) + case "available/total": + available := float64(totalBytes-usedBytes) / float64(totalBytes) + return doCompareHostDiskUsagePercent(comparator, desired, available) + } + return false, fmt.Errorf("unknown disk usage statistic %q", stat) +} + +func doCompareHostDiskUsage(operator string, desired string, actual uint64) (bool, error) { + quantity, err := resource.ParseQuantity(desired) + if err != nil { + return false, fmt.Errorf("could not parse quantity %q", desired) + } + desiredInt, ok := quantity.AsInt64() + if !ok { + return false, fmt.Errorf("could not parse quantity %q", desired) + } + + switch operator { + case "<": + return actual < uint64(desiredInt), nil + case "<=": + return actual <= uint64(desiredInt), nil + case ">": + return actual > uint64(desiredInt), nil + case ">=": + return actual >= uint64(desiredInt), nil + case "=", "==", "===": + return actual == uint64(desiredInt), nil + } + + return false, errors.New("unknown operator") +} + +func doCompareHostDiskUsagePercent(operator string, desired string, actual float64) (bool, error) { + isPercent := false + if strings.HasSuffix(desired, "%") { + desired = strings.TrimSuffix(desired, "%") + isPercent = true + } + desiredPercent, err := strconv.ParseFloat(desired, 64) + if err != nil { + return false, errors.Wrap(err, "parsed desired quantity") + } + if isPercent { + desiredPercent = desiredPercent / 100.0 + } + + switch operator { + case "<": + return actual < desiredPercent, nil + case "<=": + return actual <= desiredPercent, nil + case ">": + return actual > desiredPercent, nil + case ">=": + return actual >= desiredPercent, nil + case "=", "==", "===": + return actual == desiredPercent, nil + } + + return false, errors.New("unknown operator") +} diff --git a/pkg/analyze/host_disk_usage_test.go b/pkg/analyze/host_disk_usage_test.go new file mode 100644 index 000000000..7e734fac0 --- /dev/null +++ b/pkg/analyze/host_disk_usage_test.go @@ -0,0 +1,380 @@ +package analyzer + +import ( + "encoding/json" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_doCompareHostDiskUsage(t *testing.T) { + tests := []struct { + name string + operator string + desired string + actual uint64 + expected bool + }{ + { + name: ">= 1Gi, when actual is 2Gi", + operator: ">=", + desired: "1Gi", + actual: 2147483648, + expected: true, + }, + { + name: "<= 1Gi, when actual is 1GB", + operator: "<=", + desired: "1Gi", + actual: 1000000000, + expected: true, + }, + { + name: "< 20Gi, when actual is 15Gi", + operator: "<", + desired: "20Gi", + actual: 15 * 1024 * 1024 * 1024, + expected: true, + }, + { + name: "< 20Gi, when actual is 20Gi", + operator: "<", + desired: "20Gi", + actual: 20 * 1024 * 1024 * 1024, + expected: false, + }, + { + name: "> 1073741824, when actual is 1024", + operator: ">", + desired: "1073741824", + actual: 1024, + expected: false, + }, + { + name: "= 4096, when actual is 4096", + operator: "=", + desired: "4096", + actual: 4096, + expected: true, + }, + { + name: "= 4096, when actual is 1024", + operator: "=", + desired: "4096", + actual: 1024, + expected: false, + }, + { + name: "= 4096, when actual is 5000", + operator: "=", + desired: "4096", + actual: 5000, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + actual, err := doCompareHostDiskUsage(test.operator, test.desired, test.actual) + req.NoError(err) + + assert.Equal(t, test.expected, actual) + + }) + } +} + +func Test_doCompareHostDiskUsagePercent(t *testing.T) { + tests := []struct { + name string + operator string + desired string + actual float64 + expected bool + }{ + { + name: ">= .20, when actual is .30", + operator: ">=", + desired: ".20", + actual: .30, + expected: true, + }, + { + name: ">= .20, when actual is .10", + operator: ">=", + desired: ".20", + actual: .10, + expected: false, + }, + { + name: ">= .20, when actual is .20", + operator: ">=", + desired: ".20", + actual: .20, + expected: true, + }, + { + name: "> .20, when actual is .30", + operator: ">", + desired: ".20", + actual: .30, + expected: true, + }, + { + name: "> .20, when actual is .10", + operator: ">", + desired: ".20", + actual: .10, + expected: false, + }, + { + name: "> .20, when actual is .20", + operator: ">", + desired: ".20", + actual: .20, + expected: false, + }, + { + name: "<= .20, when actual is .30", + operator: "<=", + desired: ".20", + actual: .30, + expected: false, + }, + { + name: "<= .20, when actual is .10", + operator: "<=", + desired: ".20", + actual: .10, + expected: true, + }, + { + name: "<= .20, when actual is .20", + operator: "<=", + desired: ".20", + actual: .20, + expected: true, + }, + { + name: "< .20, when actual is .30", + operator: "<", + desired: ".20", + actual: .30, + expected: false, + }, + { + name: "< .20, when actual is .10", + operator: "<", + desired: ".20", + actual: .10, + expected: true, + }, + { + name: "< .20, when actual is .20", + operator: "<", + desired: ".20", + actual: .20, + expected: false, + }, + { + name: "= .20, when actual is .30", + operator: "=", + desired: ".20", + actual: .30, + expected: false, + }, + { + name: "= .20, when actual is .10", + operator: "=", + desired: ".20", + actual: .10, + expected: false, + }, + { + name: "= .20, when actual is .20", + operator: "=", + desired: ".20", + actual: .20, + expected: true, + }, + { + name: "= 20%, when actual is .20", + operator: "=", + desired: "20%", + actual: .20, + expected: true, + }, + { + name: "= 20%, when actual is 20", + operator: "=", + desired: "20%", + actual: 20, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + actual, err := doCompareHostDiskUsagePercent(test.operator, test.desired, test.actual) + req.NoError(err) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestAnalyzeHostDiskUsage(t *testing.T) { + tests := []struct { + name string + diskUsageInfo *collect.DiskUsageInfo + hostAnalyzer *troubleshootv1beta2.DiskUsageAnalyze + result *AnalyzeResult + expectErr bool + }{ + { + name: "Fail on insuffient total ephemeral disk space", + diskUsageInfo: &collect.DiskUsageInfo{ + TotalBytes: 10 * 1024 * 1024 * 1024, + UsedBytes: 5 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{ + CollectorName: "ephemeral", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "used/total >= 80%", + Message: "/var/lib/kubelet is more than 80% full", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "total <= 10Gi", + Message: "/var/lib/kubelet requires at least 10Gi", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Disk Usage ephemeral", + IsFail: true, + Message: "/var/lib/kubelet requires at least 10Gi", + }, + }, + { + name: "Fail on insuffient available ephemeral disk space percentage", + diskUsageInfo: &collect.DiskUsageInfo{ + TotalBytes: 10 * 1024 * 1024 * 1024, + UsedBytes: 8 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{ + CollectorName: "ephemeral", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "total < 10Gi", + Message: "/var/lib/kubelet requires at least 10Gi", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "used/total >= 80%", + Message: "/var/lib/kubelet is more than 80% full", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Disk Usage ephemeral", + IsFail: true, + Message: "/var/lib/kubelet is more than 80% full", + }, + }, + { + name: "Warn on high ephemeral disk space usage", + diskUsageInfo: &collect.DiskUsageInfo{ + TotalBytes: 1024 * 1024 * 1024 * 1024, + UsedBytes: 100 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{ + CollectorName: "ephemeral", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "total < 10Gi", + Message: "/var/lib/kubelet requires at least 10Gi", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "used/total >= 80%", + Message: "/var/lib/kubelet is more than 80% full", + }, + }, + { + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "used >= 100Gi", + Message: "/var/lib/kubelet has more than 100Gi used", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Disk Usage ephemeral", + IsWarn: true, + Message: "/var/lib/kubelet has more than 100Gi used", + }, + }, + { + name: "Pass on ephemeral disk space available", + diskUsageInfo: &collect.DiskUsageInfo{ + TotalBytes: 12 * 1024 * 1024 * 1024, + UsedBytes: 1 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.DiskUsageAnalyze{ + CollectorName: "ephemeral", + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "available > 10Gi", + Message: "/var/lib/kubelet has at least 10Gi available", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Disk Usage ephemeral", + IsPass: true, + Message: "/var/lib/kubelet has at least 10Gi available", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + b, err := json.Marshal(test.diskUsageInfo) + if err != nil { + t.Fatal(err) + } + + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + + result, err := analyzeHostDiskUsage(test.hostAnalyzer, getCollectedFileContents) + if test.expectErr { + req.Error(err) + } else { + req.NoError(err) + } + + assert.Equal(t, test.result, result) + }) + } +} diff --git a/pkg/analyze/host_memory.go b/pkg/analyze/host_memory.go new file mode 100644 index 000000000..12d57b111 --- /dev/null +++ b/pkg/analyze/host_memory.go @@ -0,0 +1,134 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "k8s.io/apimachinery/pkg/api/resource" +) + +func analyzeHostMemory(hostAnalyzer *troubleshootv1beta2.MemoryAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + contents, err := getCollectedFileContents("system/memory.json") + if err != nil { + return nil, errors.Wrap(err, "failed to get collected file") + } + + memoryInfo := collect.MemoryInfo{} + if err := json.Unmarshal(contents, &memoryInfo); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal memory info") + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = "Amount of Memory" + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + isMatch, err := compareHostMemoryConditionalToActual(outcome.Fail.When, memoryInfo.Total) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When) + } + + if isMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + isMatch, err := compareHostMemoryConditionalToActual(outcome.Warn.When, memoryInfo.Total) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When) + } + + if isMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + isMatch, err := compareHostMemoryConditionalToActual(outcome.Pass.When, memoryInfo.Total) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When) + } + + if isMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} + +func compareHostMemoryConditionalToActual(conditional string, total uint64) (res bool, err error) { + parts := strings.Split(conditional, " ") + if len(parts) != 2 { + return false, fmt.Errorf("Expected 2 parts in conditional, got %d", len(parts)) + } + + operator := parts[0] + desired := parts[1] + quantity, err := resource.ParseQuantity(desired) + if err != nil { + return false, fmt.Errorf("could not parse quantity %q", desired) + } + desiredInt, ok := quantity.AsInt64() + if !ok { + return false, fmt.Errorf("could not parse quantity %q", desired) + } + + switch operator { + case "<": + return total < uint64(desiredInt), nil + case "<=": + return total <= uint64(desiredInt), nil + case ">": + return total > uint64(desiredInt), nil + case ">=": + return total >= uint64(desiredInt), nil + case "=", "==", "===": + return total == uint64(desiredInt), nil + } + + return false, errors.New("unknown operator") +} diff --git a/pkg/analyze/host_memory_test.go b/pkg/analyze/host_memory_test.go new file mode 100644 index 000000000..a0b70ccfa --- /dev/null +++ b/pkg/analyze/host_memory_test.go @@ -0,0 +1,177 @@ +package analyzer + +import ( + "encoding/json" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_doCompareHostMemory(t *testing.T) { + tests := []struct { + name string + conditional string + actual uint64 + expected bool + }{ + { + name: "< 16Gi when actual is 8Gi", + conditional: "< 16Gi", + actual: 8 * 1024 * 1024 * 1024, + expected: true, + }, + { + name: "< 8Gi when actual is 8Gi", + conditional: "< 8Gi", + actual: 8 * 1024 * 1024 * 1024, + expected: false, + }, + { + name: "<= 8Gi when actual is 8Gi", + conditional: "<= 8Gi", + actual: 8 * 1024 * 1024 * 1024, + expected: true, + }, + { + name: "<= 8Gi when actual is 16Gi", + conditional: "<= 8Gi", + actual: 16 * 1024 * 1024 * 1024, + expected: false, + }, + { + name: "== 8Gi when actual is 16Gi", + conditional: "== 8Gi", + actual: 16 * 1024 * 1024 * 1024, + expected: false, + }, + { + name: "== 8Gi when actual is 8Gi", + conditional: "== 8Gi", + actual: 8 * 1024 * 1024 * 1024, + expected: true, + }, + { + name: "== 8000000000 when actual is 8000000000", + conditional: "== 8000000000", + actual: 8 * 1000 * 1000 * 1000, + expected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + actual, err := compareHostMemoryConditionalToActual(test.conditional, test.actual) + req.NoError(err) + + assert.Equal(t, test.expected, actual) + + }) + } +} + +func TestAnalyzeHostMemory(t *testing.T) { + tests := []struct { + name string + memoryInfo *collect.MemoryInfo + hostAnalyzer *troubleshootv1beta2.MemoryAnalyze + result *AnalyzeResult + expectErr bool + }{ + { + name: "Pass on memory available", + memoryInfo: &collect.MemoryInfo{ + Total: 8 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: ">= 4Gi", + Message: "System has at least 4Gi of memory", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Amount of Memory", + IsPass: true, + Message: "System has at least 4Gi of memory", + }, + }, + { + name: "Fail on memory available", + memoryInfo: &collect.MemoryInfo{ + Total: 8 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "< 16Gi", + Message: "System requires at least 16Gi of memory", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Amount of Memory", + IsFail: true, + Message: "System requires at least 16Gi of memory", + }, + }, + { + name: "Warn on memory available", + memoryInfo: &collect.MemoryInfo{ + Total: 8 * 1024 * 1024 * 1024, + }, + hostAnalyzer: &troubleshootv1beta2.MemoryAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "< 4Gi", + Message: "System requires at least 4Gi of memory", + }, + }, + { + Warn: &troubleshootv1beta2.SingleOutcome{ + When: "<= 8Gi", + Message: "System performs best with more than 8Gi of memory", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "Amount of Memory", + IsWarn: true, + Message: "System performs best with more than 8Gi of memory", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + b, err := json.Marshal(test.memoryInfo) + if err != nil { + t.Fatal(err) + } + + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + + result, err := analyzeHostMemory(test.hostAnalyzer, getCollectedFileContents) + if test.expectErr { + req.Error(err) + } else { + req.NoError(err) + } + + assert.Equal(t, test.result, result) + }) + } +} diff --git a/pkg/analyze/host_tcploadbalancer.go b/pkg/analyze/host_tcploadbalancer.go new file mode 100644 index 000000000..e6efc102c --- /dev/null +++ b/pkg/analyze/host_tcploadbalancer.go @@ -0,0 +1,91 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +func analyzeHostTCPLoadBalancer(hostAnalyzer *troubleshootv1beta2.TCPLoadBalancerAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + collectorName := hostAnalyzer.CollectorName + if collectorName == "" { + collectorName = "tcpLoadBalancer" + } + + fullPath := path.Join("tcpLoadBalancer", fmt.Sprintf("%s.json", collectorName)) + + collected, err := getCollectedFileContents(fullPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath) + } + actual := collect.NetworkStatusResult{} + if err := json.Unmarshal(collected, &actual); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal collected") + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = "TCP Load Balancer" + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Fail.When { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Warn.When { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Pass.When { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} diff --git a/pkg/analyze/host_tcpportstatus.go b/pkg/analyze/host_tcpportstatus.go new file mode 100644 index 000000000..4d03b1fdd --- /dev/null +++ b/pkg/analyze/host_tcpportstatus.go @@ -0,0 +1,89 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "path" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/collect" +) + +func analyzeHostTCPPortStatus(hostAnalyzer *troubleshootv1beta2.TCPPortStatusAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + fullPath := path.Join("tcpPortStatus", "tcpPortStatus.json") + if hostAnalyzer.CollectorName != "" { + fullPath = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName)) + } + + collected, err := getCollectedFileContents(fullPath) + if err != nil { + return nil, errors.Wrapf(err, "failed to read collected file name: %s", fullPath) + } + actual := collect.NetworkStatusResult{} + if err := json.Unmarshal(collected, &actual); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal collected") + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = "TCP Port Status" + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Fail.When { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Warn.When { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + if string(actual.Status) == outcome.Pass.When { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go index a985823ae..0b4ef14ef 100644 --- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go @@ -4,18 +4,6 @@ import ( "github.com/replicatedhq/troubleshoot/pkg/multitype" ) -type SingleOutcome struct { - When string `json:"when,omitempty" yaml:"when,omitempty"` - Message string `json:"message,omitempty" yaml:"message,omitempty"` - URI string `json:"uri,omitempty" yaml:"uri,omitempty"` -} - -type Outcome struct { - Fail *SingleOutcome `json:"fail,omitempty" yaml:"fail,omitempty"` - Warn *SingleOutcome `json:"warn,omitempty" yaml:"warn,omitempty"` - Pass *SingleOutcome `json:"pass,omitempty" yaml:"pass,omitempty"` -} - type ClusterVersion struct { AnalyzeMeta `json:",inline" yaml:",inline"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go new file mode 100644 index 000000000..0900ee50b --- /dev/null +++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go @@ -0,0 +1,41 @@ +package v1beta2 + +type CPUAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + +type MemoryAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + +type TCPLoadBalancerAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + +type TCPPortStatusAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + +type DiskUsageAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + +type HostAnalyze struct { + CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"` + // + TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"` + + DiskUsage *DiskUsageAnalyze `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"` + + Memory *MemoryAnalyze `json:"memory,omitempty" yaml:"memory,omitempty"` + + TCPPortStatus *TCPPortStatusAnalyze `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"` +} diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go new file mode 100644 index 000000000..f654a3d2c --- /dev/null +++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go @@ -0,0 +1,80 @@ +package v1beta2 + +import ( + "github.com/replicatedhq/troubleshoot/pkg/multitype" +) + +type HostCollectorMeta struct { + CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"` + // +optional + Exclude multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"` +} + +type CPU struct { + HostCollectorMeta `json:",inline" yaml:",inline"` +} + +type Memory struct { + HostCollectorMeta `json:",inline" yaml:",inline"` +} + +type TCPLoadBalancer struct { + HostCollectorMeta `json:",inline" yaml:",inline"` + Address string `json:"address"` + Port int `json:"port"` + Timeout string `json:"timeout,omitempty"` +} + +type HTTPLoadBalancer struct { + HostCollectorMeta `json:",inline" yaml:",inline"` + Address string `json:"address"` + Port int `json:"port"` + Path string `json:"path"` + Timeout string `json:"timeout,omitempty"` +} + +type TCPPortStatus struct { + HostCollectorMeta `json:",inline" yaml:",inline"` + Interface string `json:"interface,omitempty"` + Port int `json:"port"` +} + +type Kubernetes struct { + HostCollectorMeta `json:",inline" yaml:",inline"` +} + +type IPV4Interfaces struct { + HostCollectorMeta `json:",inline" yaml:",inline"` +} + +type DiskUsage struct { + HostCollectorMeta `json:",inline" yaml:",inline"` + Path string `json:"path"` +} + +type HostCollect struct { + CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"` + Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"` + TCPLoadBalancer *TCPLoadBalancer `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"` + HTTPLoadBalancer *HTTPLoadBalancer `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,omitempty"` + TCPPortStatus *TCPPortStatus `json:"tcpPortStatus,omitempty" yaml:"tcpPortStatus,omitempty"` + Kubernetes *Kubernetes `json:"kubernetes,omitempty" yaml:"kubernetes,omitempty"` + IPV4Interfaces *IPV4Interfaces `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"` + DiskUsage *DiskUsage `json:"diskUsage,omitempty" yaml:"diskUsage,omitempty"` +} + +func (c *HostCollect) GetName() string { + var collector string + if c.CPU != nil { + collector = "cpu" + } + if c.Memory != nil { + collector = "memory" + } + + if collector == "" { + return "" + } + + return collector +} diff --git a/pkg/apis/troubleshoot/v1beta2/hostpreflight_types.go b/pkg/apis/troubleshoot/v1beta2/hostpreflight_types.go new file mode 100644 index 000000000..b38345267 --- /dev/null +++ b/pkg/apis/troubleshoot/v1beta2/hostpreflight_types.go @@ -0,0 +1,57 @@ +/* +Copyright 2019 Replicated, Inc.. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta2 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// HostPreflightSpec defines the desired state of HostPreflight +type HostPreflightSpec struct { + Collectors []*HostCollect `json:"collectors,omitempty" yaml:"collectors,omitempty"` + Analyzers []*HostAnalyze `json:"analyzers,omitempty" yaml:"analyzers,omitempty"` +} + +// HostPreflightStatus defines the observed state of HostPreflight +type HostPreflightStatus struct { +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// HostPreflight is the Schema for the hostpreflights API +// +k8s:openapi-gen=true +type HostPreflight struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"` + + Spec HostPreflightSpec `json:"spec,omitempty" yaml:"spec,omitempty"` + Status HostPreflightStatus `json:"status,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// HostPreflightList contains a list of HostPreflight +type HostPreflightList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []HostPreflight `json:"items"` +} + +func init() { + SchemeBuilder.Register(&HostPreflight{}, &HostPreflightList{}) +} diff --git a/pkg/apis/troubleshoot/v1beta2/outcome.go b/pkg/apis/troubleshoot/v1beta2/outcome.go new file mode 100644 index 000000000..4cdc572f9 --- /dev/null +++ b/pkg/apis/troubleshoot/v1beta2/outcome.go @@ -0,0 +1,13 @@ +package v1beta2 + +type SingleOutcome struct { + When string `json:"when,omitempty" yaml:"when,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + URI string `json:"uri,omitempty" yaml:"uri,omitempty"` +} + +type Outcome struct { + Fail *SingleOutcome `json:"fail,omitempty" yaml:"fail,omitempty"` + Warn *SingleOutcome `json:"warn,omitempty" yaml:"warn,omitempty"` + Pass *SingleOutcome `json:"pass,omitempty" yaml:"pass,omitempty"` +} diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 70de7ae33..dc18d28e7 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -287,6 +287,49 @@ func (in *AnalyzerStatus) DeepCopy() *AnalyzerStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CPU) DeepCopyInto(out *CPU) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPU. +func (in *CPU) DeepCopy() *CPU { + if in == nil { + return nil + } + out := new(CPU) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CPUAnalyze) DeepCopyInto(out *CPUAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CPUAnalyze. +func (in *CPUAnalyze) DeepCopy() *CPUAnalyze { + if in == nil { + return nil + } + out := new(CPUAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Ceph) DeepCopyInto(out *Ceph) { *out = *in @@ -810,6 +853,49 @@ func (in *DeploymentStatus) DeepCopy() *DeploymentStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DiskUsage) DeepCopyInto(out *DiskUsage) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiskUsage. +func (in *DiskUsage) DeepCopy() *DiskUsage { + if in == nil { + return nil + } + out := new(DiskUsage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DiskUsageAnalyze) DeepCopyInto(out *DiskUsageAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DiskUsageAnalyze. +func (in *DiskUsageAnalyze) DeepCopy() *DiskUsageAnalyze { + if in == nil { + return nil + } + out := new(DiskUsageAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Distribution) DeepCopyInto(out *Distribution) { *out = *in @@ -941,6 +1027,260 @@ func (in *HTTP) DeepCopy() *HTTP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HTTPLoadBalancer) DeepCopyInto(out *HTTPLoadBalancer) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPLoadBalancer. +func (in *HTTPLoadBalancer) DeepCopy() *HTTPLoadBalancer { + if in == nil { + return nil + } + out := new(HTTPLoadBalancer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) { + *out = *in + if in.CPU != nil { + in, out := &in.CPU, &out.CPU + *out = new(CPUAnalyze) + (*in).DeepCopyInto(*out) + } + if in.TCPLoadBalancer != nil { + in, out := &in.TCPLoadBalancer, &out.TCPLoadBalancer + *out = new(TCPLoadBalancerAnalyze) + (*in).DeepCopyInto(*out) + } + if in.DiskUsage != nil { + in, out := &in.DiskUsage, &out.DiskUsage + *out = new(DiskUsageAnalyze) + (*in).DeepCopyInto(*out) + } + if in.Memory != nil { + in, out := &in.Memory, &out.Memory + *out = new(MemoryAnalyze) + (*in).DeepCopyInto(*out) + } + if in.TCPPortStatus != nil { + in, out := &in.TCPPortStatus, &out.TCPPortStatus + *out = new(TCPPortStatusAnalyze) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze. +func (in *HostAnalyze) DeepCopy() *HostAnalyze { + if in == nil { + return nil + } + out := new(HostAnalyze) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostCollect) DeepCopyInto(out *HostCollect) { + *out = *in + if in.CPU != nil { + in, out := &in.CPU, &out.CPU + *out = new(CPU) + **out = **in + } + if in.Memory != nil { + in, out := &in.Memory, &out.Memory + *out = new(Memory) + **out = **in + } + if in.TCPLoadBalancer != nil { + in, out := &in.TCPLoadBalancer, &out.TCPLoadBalancer + *out = new(TCPLoadBalancer) + **out = **in + } + if in.HTTPLoadBalancer != nil { + in, out := &in.HTTPLoadBalancer, &out.HTTPLoadBalancer + *out = new(HTTPLoadBalancer) + **out = **in + } + if in.TCPPortStatus != nil { + in, out := &in.TCPPortStatus, &out.TCPPortStatus + *out = new(TCPPortStatus) + **out = **in + } + if in.Kubernetes != nil { + in, out := &in.Kubernetes, &out.Kubernetes + *out = new(Kubernetes) + **out = **in + } + if in.IPV4Interfaces != nil { + in, out := &in.IPV4Interfaces, &out.IPV4Interfaces + *out = new(IPV4Interfaces) + **out = **in + } + if in.DiskUsage != nil { + in, out := &in.DiskUsage, &out.DiskUsage + *out = new(DiskUsage) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect. +func (in *HostCollect) DeepCopy() *HostCollect { + if in == nil { + return nil + } + out := new(HostCollect) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostCollectorMeta) DeepCopyInto(out *HostCollectorMeta) { + *out = *in + out.Exclude = in.Exclude +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollectorMeta. +func (in *HostCollectorMeta) DeepCopy() *HostCollectorMeta { + if in == nil { + return nil + } + out := new(HostCollectorMeta) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPreflight) DeepCopyInto(out *HostPreflight) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflight. +func (in *HostPreflight) DeepCopy() *HostPreflight { + if in == nil { + return nil + } + out := new(HostPreflight) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HostPreflight) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPreflightList) DeepCopyInto(out *HostPreflightList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HostPreflight, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflightList. +func (in *HostPreflightList) DeepCopy() *HostPreflightList { + if in == nil { + return nil + } + out := new(HostPreflightList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HostPreflightList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPreflightSpec) DeepCopyInto(out *HostPreflightSpec) { + *out = *in + if in.Collectors != nil { + in, out := &in.Collectors, &out.Collectors + *out = make([]*HostCollect, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(HostCollect) + (*in).DeepCopyInto(*out) + } + } + } + if in.Analyzers != nil { + in, out := &in.Analyzers, &out.Analyzers + *out = make([]*HostAnalyze, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(HostAnalyze) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflightSpec. +func (in *HostPreflightSpec) DeepCopy() *HostPreflightSpec { + if in == nil { + return nil + } + out := new(HostPreflightSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostPreflightStatus) DeepCopyInto(out *HostPreflightStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostPreflightStatus. +func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus { + if in == nil { + return nil + } + out := new(HostPreflightStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPV4Interfaces) DeepCopyInto(out *IPV4Interfaces) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPV4Interfaces. +func (in *IPV4Interfaces) DeepCopy() *IPV4Interfaces { + if in == nil { + return nil + } + out := new(IPV4Interfaces) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImagePullSecret) DeepCopyInto(out *ImagePullSecret) { *out = *in @@ -1017,6 +1357,22 @@ func (in *Ingress) DeepCopy() *Ingress { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Kubernetes) DeepCopyInto(out *Kubernetes) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kubernetes. +func (in *Kubernetes) DeepCopy() *Kubernetes { + if in == nil { + return nil + } + out := new(Kubernetes) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LogLimits) DeepCopyInto(out *LogLimits) { *out = *in @@ -1064,6 +1420,49 @@ func (in *Logs) DeepCopy() *Logs { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Memory) DeepCopyInto(out *Memory) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memory. +func (in *Memory) DeepCopy() *Memory { + if in == nil { + return nil + } + out := new(Memory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemoryAnalyze) DeepCopyInto(out *MemoryAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemoryAnalyze. +func (in *MemoryAnalyze) DeepCopy() *MemoryAnalyze { + if in == nil { + return nil + } + out := new(MemoryAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NodeResourceFilters) DeepCopyInto(out *NodeResourceFilters) { *out = *in @@ -1769,6 +2168,92 @@ func (in *SupportBundleVersionSpec) DeepCopy() *SupportBundleVersionSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPLoadBalancer) DeepCopyInto(out *TCPLoadBalancer) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPLoadBalancer. +func (in *TCPLoadBalancer) DeepCopy() *TCPLoadBalancer { + if in == nil { + return nil + } + out := new(TCPLoadBalancer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPLoadBalancerAnalyze) DeepCopyInto(out *TCPLoadBalancerAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPLoadBalancerAnalyze. +func (in *TCPLoadBalancerAnalyze) DeepCopy() *TCPLoadBalancerAnalyze { + if in == nil { + return nil + } + out := new(TCPLoadBalancerAnalyze) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPPortStatus) DeepCopyInto(out *TCPPortStatus) { + *out = *in + out.HostCollectorMeta = in.HostCollectorMeta +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPPortStatus. +func (in *TCPPortStatus) DeepCopy() *TCPPortStatus { + if in == nil { + return nil + } + out := new(TCPPortStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TCPPortStatusAnalyze) DeepCopyInto(out *TCPPortStatusAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPPortStatusAnalyze. +func (in *TCPPortStatusAnalyze) DeepCopy() *TCPPortStatusAnalyze { + if in == nil { + return nil + } + out := new(TCPPortStatusAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TextAnalyze) DeepCopyInto(out *TextAnalyze) { *out = *in diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go new file mode 100644 index 000000000..761fd6d8c --- /dev/null +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_hostpreflight.go @@ -0,0 +1,141 @@ +/* +Copyright 2019 Replicated, Inc.. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + "context" + + v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeHostPreflights implements HostPreflightInterface +type FakeHostPreflights struct { + Fake *FakeTroubleshootV1beta2 + ns string +} + +var hostpreflightsResource = schema.GroupVersionResource{Group: "troubleshoot.sh", Version: "v1beta2", Resource: "hostpreflights"} + +var hostpreflightsKind = schema.GroupVersionKind{Group: "troubleshoot.sh", Version: "v1beta2", Kind: "HostPreflight"} + +// Get takes name of the hostPreflight, and returns the corresponding hostPreflight object, and an error if there is any. +func (c *FakeHostPreflights) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.HostPreflight, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(hostpreflightsResource, c.ns, name), &v1beta2.HostPreflight{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta2.HostPreflight), err +} + +// List takes label and field selectors, and returns the list of HostPreflights that match those selectors. +func (c *FakeHostPreflights) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.HostPreflightList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(hostpreflightsResource, hostpreflightsKind, c.ns, opts), &v1beta2.HostPreflightList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1beta2.HostPreflightList{ListMeta: obj.(*v1beta2.HostPreflightList).ListMeta} + for _, item := range obj.(*v1beta2.HostPreflightList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested hostPreflights. +func (c *FakeHostPreflights) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(hostpreflightsResource, c.ns, opts)) + +} + +// Create takes the representation of a hostPreflight and creates it. Returns the server's representation of the hostPreflight, and an error, if there is any. +func (c *FakeHostPreflights) Create(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.CreateOptions) (result *v1beta2.HostPreflight, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(hostpreflightsResource, c.ns, hostPreflight), &v1beta2.HostPreflight{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta2.HostPreflight), err +} + +// Update takes the representation of a hostPreflight and updates it. Returns the server's representation of the hostPreflight, and an error, if there is any. +func (c *FakeHostPreflights) Update(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (result *v1beta2.HostPreflight, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(hostpreflightsResource, c.ns, hostPreflight), &v1beta2.HostPreflight{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta2.HostPreflight), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeHostPreflights) UpdateStatus(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (*v1beta2.HostPreflight, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(hostpreflightsResource, "status", c.ns, hostPreflight), &v1beta2.HostPreflight{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta2.HostPreflight), err +} + +// Delete takes name of the hostPreflight and deletes it. Returns an error if one occurs. +func (c *FakeHostPreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(hostpreflightsResource, c.ns, name), &v1beta2.HostPreflight{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeHostPreflights) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(hostpreflightsResource, c.ns, listOpts) + + _, err := c.Fake.Invokes(action, &v1beta2.HostPreflightList{}) + return err +} + +// Patch applies the patch and returns the patched hostPreflight. +func (c *FakeHostPreflights) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.HostPreflight, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(hostpreflightsResource, c.ns, name, pt, data, subresources...), &v1beta2.HostPreflight{}) + + if obj == nil { + return nil, err + } + return obj.(*v1beta2.HostPreflight), err +} diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go index 7e7e28dae..3e11fbec9 100644 --- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/fake/fake_troubleshoot_client.go @@ -35,6 +35,10 @@ func (c *FakeTroubleshootV1beta2) Collectors(namespace string) v1beta2.Collector return &FakeCollectors{c, namespace} } +func (c *FakeTroubleshootV1beta2) HostPreflights(namespace string) v1beta2.HostPreflightInterface { + return &FakeHostPreflights{c, namespace} +} + func (c *FakeTroubleshootV1beta2) Preflights(namespace string) v1beta2.PreflightInterface { return &FakePreflights{c, namespace} } diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go index b2023742e..0779c12fb 100644 --- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/generated_expansion.go @@ -21,6 +21,8 @@ type AnalyzerExpansion interface{} type CollectorExpansion interface{} +type HostPreflightExpansion interface{} + type PreflightExpansion interface{} type RedactorExpansion interface{} diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/hostpreflight.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/hostpreflight.go new file mode 100644 index 000000000..f360ea8ae --- /dev/null +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/hostpreflight.go @@ -0,0 +1,194 @@ +/* +Copyright 2019 Replicated, Inc.. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by client-gen. DO NOT EDIT. + +package v1beta2 + +import ( + "context" + "time" + + v1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + scheme "github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// HostPreflightsGetter has a method to return a HostPreflightInterface. +// A group's client should implement this interface. +type HostPreflightsGetter interface { + HostPreflights(namespace string) HostPreflightInterface +} + +// HostPreflightInterface has methods to work with HostPreflight resources. +type HostPreflightInterface interface { + Create(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.CreateOptions) (*v1beta2.HostPreflight, error) + Update(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (*v1beta2.HostPreflight, error) + UpdateStatus(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (*v1beta2.HostPreflight, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*v1beta2.HostPreflight, error) + List(ctx context.Context, opts v1.ListOptions) (*v1beta2.HostPreflightList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.HostPreflight, err error) + HostPreflightExpansion +} + +// hostPreflights implements HostPreflightInterface +type hostPreflights struct { + client rest.Interface + ns string +} + +// newHostPreflights returns a HostPreflights +func newHostPreflights(c *TroubleshootV1beta2Client, namespace string) *hostPreflights { + return &hostPreflights{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the hostPreflight, and returns the corresponding hostPreflight object, and an error if there is any. +func (c *hostPreflights) Get(ctx context.Context, name string, options v1.GetOptions) (result *v1beta2.HostPreflight, err error) { + result = &v1beta2.HostPreflight{} + err = c.client.Get(). + Namespace(c.ns). + Resource("hostpreflights"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(ctx). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of HostPreflights that match those selectors. +func (c *hostPreflights) List(ctx context.Context, opts v1.ListOptions) (result *v1beta2.HostPreflightList, err error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + result = &v1beta2.HostPreflightList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("hostpreflights"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Do(ctx). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested hostPreflights. +func (c *hostPreflights) Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) { + var timeout time.Duration + if opts.TimeoutSeconds != nil { + timeout = time.Duration(*opts.TimeoutSeconds) * time.Second + } + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("hostpreflights"). + VersionedParams(&opts, scheme.ParameterCodec). + Timeout(timeout). + Watch(ctx) +} + +// Create takes the representation of a hostPreflight and creates it. Returns the server's representation of the hostPreflight, and an error, if there is any. +func (c *hostPreflights) Create(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.CreateOptions) (result *v1beta2.HostPreflight, err error) { + result = &v1beta2.HostPreflight{} + err = c.client.Post(). + Namespace(c.ns). + Resource("hostpreflights"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(hostPreflight). + Do(ctx). + Into(result) + return +} + +// Update takes the representation of a hostPreflight and updates it. Returns the server's representation of the hostPreflight, and an error, if there is any. +func (c *hostPreflights) Update(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (result *v1beta2.HostPreflight, err error) { + result = &v1beta2.HostPreflight{} + err = c.client.Put(). + Namespace(c.ns). + Resource("hostpreflights"). + Name(hostPreflight.Name). + VersionedParams(&opts, scheme.ParameterCodec). + Body(hostPreflight). + Do(ctx). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *hostPreflights) UpdateStatus(ctx context.Context, hostPreflight *v1beta2.HostPreflight, opts v1.UpdateOptions) (result *v1beta2.HostPreflight, err error) { + result = &v1beta2.HostPreflight{} + err = c.client.Put(). + Namespace(c.ns). + Resource("hostpreflights"). + Name(hostPreflight.Name). + SubResource("status"). + VersionedParams(&opts, scheme.ParameterCodec). + Body(hostPreflight). + Do(ctx). + Into(result) + return +} + +// Delete takes name of the hostPreflight and deletes it. Returns an error if one occurs. +func (c *hostPreflights) Delete(ctx context.Context, name string, opts v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("hostpreflights"). + Name(name). + Body(&opts). + Do(ctx). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *hostPreflights) DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error { + var timeout time.Duration + if listOpts.TimeoutSeconds != nil { + timeout = time.Duration(*listOpts.TimeoutSeconds) * time.Second + } + return c.client.Delete(). + Namespace(c.ns). + Resource("hostpreflights"). + VersionedParams(&listOpts, scheme.ParameterCodec). + Timeout(timeout). + Body(&opts). + Do(ctx). + Error() +} + +// Patch applies the patch and returns the patched hostPreflight. +func (c *hostPreflights) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1beta2.HostPreflight, err error) { + result = &v1beta2.HostPreflight{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("hostpreflights"). + Name(name). + SubResource(subresources...). + VersionedParams(&opts, scheme.ParameterCodec). + Body(data). + Do(ctx). + Into(result) + return +} diff --git a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go index 383b93cc5..9dc93fe14 100644 --- a/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go +++ b/pkg/client/troubleshootclientset/typed/troubleshoot/v1beta2/troubleshoot_client.go @@ -27,6 +27,7 @@ type TroubleshootV1beta2Interface interface { RESTClient() rest.Interface AnalyzersGetter CollectorsGetter + HostPreflightsGetter PreflightsGetter RedactorsGetter SupportBundlesGetter @@ -45,6 +46,10 @@ func (c *TroubleshootV1beta2Client) Collectors(namespace string) CollectorInterf return newCollectors(c, namespace) } +func (c *TroubleshootV1beta2Client) HostPreflights(namespace string) HostPreflightInterface { + return newHostPreflights(c, namespace) +} + func (c *TroubleshootV1beta2Client) Preflights(namespace string) PreflightInterface { return newPreflights(c, namespace) } diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go new file mode 100644 index 000000000..c684c9eaa --- /dev/null +++ b/pkg/collect/host_collector.go @@ -0,0 +1,44 @@ +package collect + +import ( + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) + +type HostCollector struct { + Collect *troubleshootv1beta2.HostCollect +} + +type HostCollectors []*HostCollector + +func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error) { + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("recovered rom panic: %v", r) + } + }() + + if c.Collect.CPU != nil { + result, err = HostCPU(c) + } else if c.Collect.Memory != nil { + result, err = HostMemory(c) + } else if c.Collect.TCPLoadBalancer != nil { + result, err = HostTCPLoadBalancer(c) + } else if c.Collect.DiskUsage != nil { + result, err = HostDiskUsage(c) + } else if c.Collect.TCPPortStatus != nil { + result, err = HostTCPPortStatus(c) + } else { + err = errors.New("no spec found to run") + return + } + if err != nil { + return + } + + return +} + +func (c *HostCollector) GetDisplayName() string { + return c.Collect.GetName() +} diff --git a/pkg/collect/host_cpu.go b/pkg/collect/host_cpu.go new file mode 100644 index 000000000..032a3ff7e --- /dev/null +++ b/pkg/collect/host_cpu.go @@ -0,0 +1,38 @@ +package collect + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/shirou/gopsutil/cpu" +) + +type CPUInfo struct { + LogicalCount int `json:"logicalCount"` + PhysicalCount int `json:"physicalCount"` +} + +func HostCPU(c *HostCollector) (map[string][]byte, error) { + cpuInfo := CPUInfo{} + + logicalCount, err := cpu.Counts(true) + if err != nil { + return nil, errors.Wrap(err, "failed to count logical cpus") + } + cpuInfo.LogicalCount = logicalCount + + physicalCount, err := cpu.Counts(false) + if err != nil { + return nil, errors.Wrap(err, "failed to count physical cpus") + } + cpuInfo.PhysicalCount = physicalCount + + b, err := json.Marshal(cpuInfo) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal cpu info") + } + + return map[string][]byte{ + "system/cpu.json": b, + }, nil +} diff --git a/pkg/collect/host_disk_usage.go b/pkg/collect/host_disk_usage.go new file mode 100644 index 000000000..2b751e268 --- /dev/null +++ b/pkg/collect/host_disk_usage.go @@ -0,0 +1,43 @@ +package collect + +import ( + "encoding/json" + "fmt" + + "github.com/pkg/errors" + "github.com/shirou/gopsutil/disk" +) + +type DiskUsageInfo struct { + TotalBytes uint64 `json:"total_bytes"` + UsedBytes uint64 `json:"used_bytes"` +} + +func HostDiskUsage(c *HostCollector) (map[string][]byte, error) { + result := map[string][]byte{} + + if c.Collect.DiskUsage == nil { + return result, nil + } + + du, err := disk.Usage(c.Collect.DiskUsage.Path) + if err != nil { + return result, errors.Wrapf(err, "collect disk usage for %s", c.Collect.DiskUsage.Path) + } + diskSpaceInfo := DiskUsageInfo{ + TotalBytes: du.Total, + UsedBytes: du.Used, + } + b, err := json.Marshal(diskSpaceInfo) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal disk space info") + } + key := HostDiskUsageKey(c.Collect.DiskUsage.CollectorName) + result[key] = b + + return result, nil +} + +func HostDiskUsageKey(name string) string { + return fmt.Sprintf("diskUsage/%s.json", name) +} diff --git a/pkg/collect/host_memory.go b/pkg/collect/host_memory.go new file mode 100644 index 000000000..7cf0194e7 --- /dev/null +++ b/pkg/collect/host_memory.go @@ -0,0 +1,31 @@ +package collect + +import ( + "encoding/json" + + "github.com/pkg/errors" + "github.com/shirou/gopsutil/mem" +) + +type MemoryInfo struct { + Total uint64 `json:"total"` +} + +func HostMemory(c *HostCollector) (map[string][]byte, error) { + memoryInfo := MemoryInfo{} + + vmstat, err := mem.VirtualMemory() + if err != nil { + return nil, errors.Wrap(err, "failed to read virtual memory") + } + memoryInfo.Total = vmstat.Available + + b, err := json.Marshal(memoryInfo) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal memory info") + } + + return map[string][]byte{ + "system/memory.json": b, + }, nil +} diff --git a/pkg/collect/host_network.go b/pkg/collect/host_network.go new file mode 100644 index 000000000..06cabfde4 --- /dev/null +++ b/pkg/collect/host_network.go @@ -0,0 +1,156 @@ +package collect + +import ( + "bytes" + "fmt" + "net" + "strings" + "time" + + "github.com/pkg/errors" + "github.com/segmentio/ksuid" +) + +type NetworkStatus string + +const ( + NetworkStatusAddressInUse = "address-in-use" + NetworkStatusConnectionRefused = "connection-refused" + NetworkStatusConnectionTimeout = "connection-timeout" + NetworkStatusConnected = "connected" + NetworkStatusErrorOther = "error" +) + +type NetworkStatusResult struct { + Status NetworkStatus `json:"status"` +} + +func checkTCPConnection(listenAddress string, dialAddress string, timeout time.Duration) (NetworkStatus, error) { + lstn, err := net.Listen("tcp", listenAddress) + if err != nil { + if strings.Contains(err.Error(), "address already in use") { + return NetworkStatusAddressInUse, nil + } + + return NetworkStatusErrorOther, errors.Wrap(err, "failed to create listener") + } + defer lstn.Close() + + // The server may receive requests from other clients and the client request may be forwarded to + // other servers. The client must continue to initiate new connections and send its request + // token until the server responds with its token. + requestToken := ksuid.New().Bytes() + responseToken := ksuid.New().Bytes() + + go func() { + for { + conn, err := lstn.Accept() + if err != nil { + continue + } + + if handleTestConnection(conn, requestToken, responseToken) { + return + } + } + }() + + stopAfter := time.Now().Add(timeout) + + for { + if time.Now().After(stopAfter) { + return NetworkStatusConnectionTimeout, nil + } + + conn, err := net.DialTimeout("tcp", dialAddress, 50*time.Millisecond) + if err != nil { + if strings.Contains(err.Error(), "i/o timeout") { + time.Sleep(time.Millisecond * 50) + continue + } + if strings.Contains(err.Error(), "connection refused") { + return NetworkStatusConnectionRefused, nil + } + return NetworkStatusErrorOther, errors.Wrap(err, "failed to dial") + } + + if verifyConnectionToServer(conn, requestToken, responseToken) { + return NetworkStatusConnected, nil + } + + time.Sleep(time.Millisecond * 50) + } +} + +func handleTestConnection(conn net.Conn, requestToken []byte, responseToken []byte) bool { + defer func() { + if err := conn.Close(); err != nil { + fmt.Println(err.Error()) + } + }() + + if err := conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil { + fmt.Printf("Server failed to set read deadline: %v", err) + return false + } + + buf := make([]byte, 1024) + _, err := conn.Read(buf) + if err != nil { + fmt.Printf("Server failed to read: %v", err) + return false + } + + if !bytes.Contains(buf, requestToken) { + return false + } + + if err := conn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)); err != nil { + fmt.Printf("Server failed to set write deadline: %v", err) + return false + } + + if _, err := conn.Write(responseToken); err != nil { + fmt.Printf("Server failed to write: %v", err) + return false + } + + return true +} + +func verifyConnectionToServer(conn net.Conn, requestToken []byte, responseToken []byte) bool { + defer func() { + if err := conn.Close(); err != nil { + fmt.Println(err.Error()) + } + }() + + if err := conn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)); err != nil { + fmt.Printf("Client failed to set write deadline: %v", err) + return false + } + + _, err := conn.Write(requestToken) + if err != nil { + fmt.Printf("Client failed to write: %v", err) + return false + } + + if err := conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond)); err != nil { + fmt.Printf("Client failed to set read deadline: %v", err) + return false + } + + buf := make([]byte, 1024) + _, err = conn.Read(buf) + if err != nil { + fmt.Printf("Client failed to read: %v", err) + return false + } + + if !bytes.Contains(buf, responseToken) { + return false + } + + return true +} diff --git a/pkg/collect/host_tcploadbalancer.go b/pkg/collect/host_tcploadbalancer.go new file mode 100644 index 000000000..d561620cb --- /dev/null +++ b/pkg/collect/host_tcploadbalancer.go @@ -0,0 +1,57 @@ +package collect + +import ( + "encoding/json" + "fmt" + "path" + "time" + + "github.com/pkg/errors" +) + +type ConnectionResult int + +const ( + ConnectionRefused ConnectionResult = iota + Connected + ConnectionTimeout + ConnectionAddressInUse + ErrorOther +) + +func HostTCPLoadBalancer(c *HostCollector) (map[string][]byte, error) { + listenAddress := fmt.Sprintf("0.0.0.0:%d", c.Collect.TCPLoadBalancer.Port) + dialAddress := c.Collect.TCPLoadBalancer.Address + + timeout := 60 * time.Minute + if c.Collect.TCPLoadBalancer.Timeout != "" { + var err error + timeout, err = time.ParseDuration(c.Collect.TCPLoadBalancer.Timeout) + if err != nil { + return nil, errors.Wrap(err, "failed to parse durection") + } + } + + networkStatus, err := checkTCPConnection(listenAddress, dialAddress, timeout) + if err != nil { + return nil, err + } + + result := NetworkStatusResult{ + Status: networkStatus, + } + + b, err := json.Marshal(result) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal result") + } + + name := path.Join("tcpLoadBalancer", "tcpLoadBalancer.json") + if c.Collect.TCPLoadBalancer.CollectorName != "" { + name = path.Join("tcpLoadBalancer", fmt.Sprintf("%s.json", c.Collect.TCPLoadBalancer.CollectorName)) + } + + return map[string][]byte{ + name: b, + }, nil +} diff --git a/pkg/collect/host_tcpportstatus.go b/pkg/collect/host_tcpportstatus.go new file mode 100644 index 000000000..e06bb3b7b --- /dev/null +++ b/pkg/collect/host_tcpportstatus.go @@ -0,0 +1,100 @@ +package collect + +import ( + "encoding/json" + "fmt" + "net" + "path" + "time" + + "github.com/pkg/errors" +) + +func HostTCPPortStatus(c *HostCollector) (map[string][]byte, error) { + dialAddress := "" + listenAddress := fmt.Sprintf("0.0.0.0:%d", c.Collect.TCPPortStatus.Port) + + if c.Collect.TCPPortStatus.Interface != "" { + iface, err := net.InterfaceByName(c.Collect.TCPPortStatus.Interface) + if err != nil { + return nil, errors.Wrapf(err, "lookup interface %s", c.Collect.TCPPortStatus.Interface) + } + ip, err := getIPv4FromInterface(iface) + if err != nil { + return nil, errors.Wrapf(err, "get ipv4 address for interface %s", c.Collect.TCPPortStatus.Interface) + } + listenAddress = fmt.Sprintf("%s:%d", ip, c.Collect.TCPPortStatus.Port) + dialAddress = listenAddress + } + + if dialAddress == "" { + ip, err := getLocalIPv4() + if err != nil { + return nil, err + } + dialAddress = fmt.Sprintf("%s:%d", ip, c.Collect.TCPPortStatus.Port) + } + + networkStatus, err := checkTCPConnection(listenAddress, dialAddress, 10*time.Second) + if err != nil { + return nil, err + } + + result := NetworkStatusResult{ + Status: networkStatus, + } + b, err := json.Marshal(result) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal result") + } + + name := path.Join("tcpPortStatus", "tcpPortStatus.json") + if c.Collect.TCPPortStatus.CollectorName != "" { + name = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", c.Collect.TCPPortStatus.CollectorName)) + } + return map[string][]byte{ + name: b, + }, nil +} + +func getIPv4FromInterface(iface *net.Interface) (net.IP, error) { + addrs, err := iface.Addrs() + if err != nil { + return nil, errors.Wrap(err, "list interface addresses") + } + + for _, addr := range addrs { + ip, _, err := net.ParseCIDR(addr.String()) + if err != nil { + return nil, errors.Wrapf(err, "parse interface address %q", addr.String()) + } + ip = ip.To4() + if ip != nil { + return ip, nil + } + } + + return nil, errors.New("interface does not have an ipv4 address") +} + +func getLocalIPv4() (net.IP, error) { + interfaces, err := net.Interfaces() + if err != nil { + return nil, errors.Wrap(err, "list host network interfaces") + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + if iface.Flags&net.FlagLoopback != 0 { + continue + } + ip, _ := getIPv4FromInterface(&iface) + if ip != nil { + return ip, nil + } + } + + return nil, errors.New("No network interface has an IPv4 address") +} diff --git a/pkg/preflight/analyze.go b/pkg/preflight/analyze.go index 1a84efb81..1157ab3b3 100644 --- a/pkg/preflight/analyze.go +++ b/pkg/preflight/analyze.go @@ -6,12 +6,22 @@ import ( "strings" analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" ) // Analyze runs the analyze phase of preflight checks -func (c CollectResult) Analyze() []*analyze.AnalyzeResult { +func (c ClusterCollectResult) Analyze() []*analyze.AnalyzeResult { + return doAnalyze(c.AllCollectedData, c.Spec.Spec.Analyzers, nil) +} + +// Analyze runs the analysze phase of host preflight checks +func (c HostCollectResult) Analyze() []*analyze.AnalyzeResult { + return doAnalyze(c.AllCollectedData, nil, c.Spec.Spec.Analyzers) +} + +func doAnalyze(allCollectedData map[string][]byte, analyzers []*troubleshootv1beta2.Analyze, hostAnalyzers []*troubleshootv1beta2.HostAnalyze) []*analyze.AnalyzeResult { getCollectedFileContents := func(fileName string) ([]byte, error) { - contents, ok := c.AllCollectedData[fileName] + contents, ok := allCollectedData[fileName] if !ok { return nil, fmt.Errorf("file %s was not collected", fileName) } @@ -20,13 +30,13 @@ func (c CollectResult) Analyze() []*analyze.AnalyzeResult { } getChildCollectedFileContents := func(prefix string) (map[string][]byte, error) { matching := make(map[string][]byte) - for k, v := range c.AllCollectedData { + for k, v := range allCollectedData { if strings.HasPrefix(k, prefix) { matching[k] = v } } - for k, v := range c.AllCollectedData { + for k, v := range allCollectedData { if ok, _ := filepath.Match(prefix, k); ok { matching[k] = v } @@ -36,7 +46,7 @@ func (c CollectResult) Analyze() []*analyze.AnalyzeResult { } analyzeResults := []*analyze.AnalyzeResult{} - for _, analyzer := range c.Spec.Spec.Analyzers { + for _, analyzer := range analyzers { analyzeResult, err := analyze.Analyze(analyzer, getCollectedFileContents, getChildCollectedFileContents) if err != nil { analyzeResult = []*analyze.AnalyzeResult{ @@ -53,5 +63,21 @@ func (c CollectResult) Analyze() []*analyze.AnalyzeResult { } } + for _, hostAnalyzer := range hostAnalyzers { + analyzeResult, err := analyze.HostAnalyze(hostAnalyzer, getCollectedFileContents, getChildCollectedFileContents) + if err != nil { + analyzeResult = []*analyze.AnalyzeResult{ + { + IsFail: true, + Title: "Analyzer Failed", + Message: err.Error(), + }, + } + } + + if analyzeResult != nil { + analyzeResults = append(analyzeResults, analyzeResult...) + } + } return analyzeResults } diff --git a/pkg/preflight/collect.go b/pkg/preflight/collect.go index 74943a4cc..13f4bce0a 100644 --- a/pkg/preflight/collect.go +++ b/pkg/preflight/collect.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/pkg/errors" + analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/collect" "k8s.io/client-go/rest" @@ -17,13 +18,73 @@ type CollectOpts struct { ProgressChan chan interface{} } -type CollectResult struct { +type CollectResult interface { + Analyze() []*analyze.AnalyzeResult + IsRBACAllowed() bool +} + +type ClusterCollectResult struct { AllCollectedData map[string][]byte Collectors collect.Collectors - IsRBACAllowed bool + isRBACAllowed bool Spec *troubleshootv1beta2.Preflight } +func (cr ClusterCollectResult) IsRBACAllowed() bool { + return cr.isRBACAllowed +} + +type HostCollectResult struct { + AllCollectedData map[string][]byte + Collectors collect.HostCollectors + Spec *troubleshootv1beta2.HostPreflight +} + +func (cr HostCollectResult) IsRBACAllowed() bool { + return true +} + +// CollectHost runs the collection phase of host preflight checks +func CollectHost(opts CollectOpts, p *troubleshootv1beta2.HostPreflight) (CollectResult, error) { + collectSpecs := make([]*troubleshootv1beta2.HostCollect, 0, 0) + collectSpecs = append(collectSpecs, p.Spec.Collectors...) + collectSpecs = ensureHostCollectorInList(collectSpecs, troubleshootv1beta2.HostCollect{CPU: &troubleshootv1beta2.CPU{}}) + collectSpecs = ensureHostCollectorInList(collectSpecs, troubleshootv1beta2.HostCollect{Memory: &troubleshootv1beta2.Memory{}}) + + allCollectedData := make(map[string][]byte) + + var collectors collect.HostCollectors + for _, desiredCollector := range collectSpecs { + collector := collect.HostCollector{ + Collect: desiredCollector, + } + collectors = append(collectors, &collector) + } + + collectResult := HostCollectResult{ + Collectors: collectors, + Spec: p, + } + + for _, collector := range collectors { + result, err := collector.RunCollectorSync() + if err != nil { + opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v\n", collector.GetDisplayName(), err) + continue + } + + if result != nil { + for k, v := range result { + allCollectedData[k] = v + } + } + } + + collectResult.AllCollectedData = allCollectedData + + return collectResult, nil +} + // Collect runs the collection phase of preflight checks func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, error) { collectSpecs := make([]*troubleshootv1beta2.Collect, 0, 0) @@ -44,7 +105,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, collectors = append(collectors, &collector) } - collectResult := CollectResult{ + collectResult := ClusterCollectResult{ Collectors: collectors, Spec: p, } @@ -62,7 +123,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, } if foundForbidden && !opts.IgnorePermissionErrors { - collectResult.IsRBACAllowed = false + collectResult.isRBACAllowed = false return collectResult, errors.New("insufficient permissions to run all collectors") } @@ -71,7 +132,7 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult, if len(collector.RBACErrors) > 0 { // don't skip clusterResources collector due to RBAC issues if collector.Collect.ClusterResources == nil { - collectResult.IsRBACAllowed = false // not failing, but going to report this + collectResult.isRBACAllowed = false // not failing, but going to report this opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName()) continue } @@ -106,3 +167,16 @@ func ensureCollectorInList(list []*troubleshootv1beta2.Collect, collector troubl return append(list, &collector) } + +func ensureHostCollectorInList(list []*troubleshootv1beta2.HostCollect, collector troubleshootv1beta2.HostCollect) []*troubleshootv1beta2.HostCollect { + for _, inList := range list { + if collector.CPU != nil && inList.CPU != nil { + return list + } + if collector.Memory != nil && inList.Memory != nil { + return list + } + } + + return append(list, &collector) +}