Compare commits

...
67 Commits
Author SHA1 Message Date
divolgin ae63ccba8d Merge pull request #350 from replicatedhq/divolgin/so
remove troubleshoot.so from builds
2021-04-12 11:53:18 -07:00
divolgin 5c5e30a9a5 remove troubleshoot.so from builds 2021-04-12 18:50:58 +00:00
Salah Aldeen Al Saleh ac2a0484e2 add build flags to ffi (#349) 2021-04-12 11:23:46 -07:00
divolgin bc91151883 Merge pull request #348 from replicatedhq/divolgin/goreleaser
fix ffi build
2021-04-12 10:57:34 -07:00
divolgin de8feb167e fix ffi build 2021-04-12 17:22:21 +00:00
divolgin 0a6ef1c2c3 Merge pull request #347 from replicatedhq/divolgin/goreleaser
fix ffi build
2021-04-12 10:10:08 -07:00
divolgin 4a89067912 fix ffi build 2021-04-12 17:07:09 +00:00
divolgin 1e17c5c4c2 Merge pull request #346 from replicatedhq/divolgin/goreleaser
fix go-releaser build
2021-04-12 09:40:23 -07:00
divolgin 32cca1215a fix go-releaser build 2021-04-12 16:34:53 +00:00
divolgin 052d10fec7 Merge pull request #345 from replicatedhq/divolgin/images
use containers package instead of go-containerregistry
2021-04-12 08:42:46 -07:00
divolgin 7a0c6e5383 use containers package instead of go-containerregistry 2021-04-11 21:39:44 +00:00
divolgin 1dc03db254 Merge pull request #343 from replicatedhq/divolgin/images
Docker registry collector/analyzer
2021-04-09 09:25:29 -07:00
divolgin fe414af556 Docker registry collector/analyzer 2021-04-09 16:17:15 +00:00
Andrew Lavery f8dca39899 Merge pull request #342 from replicatedhq/laverya/improve-systemctl-service-analyzer
expand systemctl service analyzer to also match service sub/load
2021-04-02 15:33:02 -04:00
Andrew Lavery 19aef8a02f expand systemctl service analyzer to also match service sub/load 2021-04-02 14:48:24 -04:00
Andrew Lavery 4b6606e323 Merge pull request #341 from replicatedhq/laverya/systemctl-services-collector
systemctl services collector
2021-03-30 16:41:30 -04:00
Andrew Lavery 559e18d996 lowercase errors 2021-03-30 16:32:19 -04:00
Andrew Lavery bf4d26acd2 add host_services analyzer 2021-03-30 16:15:18 -04:00
Andrew Lavery f3b599c19a collect host systemctl services 2021-03-30 16:15:17 -04:00
Salah Aldeen Al Saleh 810b3cbf78 support non-interactive mode in support bundle (#340)
* support non-interactive mode in support bundle
2021-03-23 18:01:39 -07:00
Andrew Lavery 81789181ed Merge pull request #339 from replicatedhq/laverya/host-unmounted-partitions
added two parameters to the eligible block device check
2021-03-18 19:36:56 -04:00
Andrew Lavery 256c68feca added two parameters to the eligible block device check
whether to accept unmounted partitions (default false) and minimum acceptable device size (default 0)
2021-03-18 19:03:39 -04:00
divolgin b01e6ef578 Merge pull request #338 from replicatedhq/divolgin/preogress2
Add progress percentage
2021-03-18 16:02:11 -07:00
divolgin 62afc87af8 Add progress percentage 2021-03-18 22:29:27 +00:00
divolgin 17bff4b2bd Merge pull request #337 from replicatedhq/divolgin/progress
Report back some basic progress
2021-03-18 12:11:54 -07:00
divolgin 5f2525b663 Report back some basic progress 2021-03-18 18:56:27 +00:00
Salah Aldeen Al Saleh afa0bc56d4 fix custom redactors file selectors in support bundle subdirectory (#336)
* fix custom redactors file selectors in support bundle subdirectory
2021-03-11 08:45:20 -08:00
Salah Aldeen Al Saleh c1f9b3906d support reading redactors from configmaps (#335) 2021-03-10 15:25:55 -08:00
DexandSalah Aldeen Al Saleh 0a19d35073 hide spinner if interactive false (#328)
* hide preflight spinner if interactive is false

Co-authored-by: Salah Aldeen Al Saleh <salahalsaleh1993@gmail.com>
2021-03-09 09:42:38 -08:00
emosbaugh 7c4135d563 Merge pull request #334 from replicatedhq/host-preflgihts-ux
Host preflight ux improvements
2021-03-02 12:03:17 -08:00
Ethan Mosbaugh 4b78c430ca Host preflight ux improvements 2021-03-02 17:27:01 +00:00
Ethan Mosbaugh 09d16ff185 Host preflights exclude 2021-03-01 22:45:16 +00:00
Andrew Lavery bdf843f84d Merge pull request #332 from replicatedhq/laverya/add-regex-case-insensitive-test
add a test that uses a case-insensitive regex analyzer
2021-03-01 13:13:35 -05:00
Andrew Lavery 47f7d98907 add a test that uses a case-insensitive regex analyzer 2021-03-01 13:02:30 -05:00
emosbaugh a1e0813d55 Merge pull request #331 from replicatedhq/condition-analyzers 2021-02-26 07:22:38 -08:00
Ethan Mosbaugh d6acd6d906 Conditional Analyzers 2021-02-26 04:33:11 +00:00
Jalaja Ganapathy ce53dbcd29 Merge pull request #330 from replicatedhq/pv-pvc
add pv and pvcs to support bundle
2021-02-25 20:18:44 -08:00
Jalaja 3b10d9c9e1 add pv and pvcs to support bundle 2021-02-26 01:02:33 +00:00
emosbaugh 7589b4fed0 Merge pull request #329 from replicatedhq/host-diskUsage-traverse
diskUsage host collector traverse file tree for directory exists
2021-02-24 16:26:34 -08:00
Ethan Mosbaugh a459120516 diskUsage host collector traverse file tree for directory exists 2021-02-24 23:38:57 +00:00
Andrew Reed b02338762e Fix build for darwin and windows (#327)
* Fix build for darwin and windows

* Remove unexpected dependencies
2021-02-19 18:31:18 -05:00
Andrew Reed 76419237d4 Merge pull request #326 from areed/host-cert
Analyze TLS certificate
2021-02-19 16:30:27 -05:00
Andrew Reed 87b4c12274 Analyze TLS certificate 2021-02-19 20:55:16 +00:00
Dan Stough 7647c039e9 Merge pull request #325 from replicatedhq/feat/rke3-k3s-anaylzer
feat(analyzer): rke2 and k3s distro support
2021-02-19 14:52:22 -05:00
Dan Stough c26824a619 feat(analyzer): rke2 and k3s distro support 2021-02-19 19:06:02 +00:00
Andrew Reed 617e0d55de Merge pull request #324 from areed/fs-read-perf
Analyze random read IOPS for a directory
2021-02-19 13:34:28 -05:00
Andrew Reed b418334a46 Analyze random read IOPS for a directory
The random IOPS benchmark attempts to replicate the results of this
fio command:

fio --ioengine=psync --direct=1 --bs=4k --size=1Gi --readwrite=randread --serialize_overlap=1

Across three tests the fio command reported 1877 IOPS and the preflight
1822 IOPS with the same block and file size.
2021-02-18 23:56:51 +00:00
Andrew Reed b78d7d04d9 Merge pull request #323 from areed/host-fio
Analyze fs sequential write performance
2021-02-17 20:19:08 -05:00
Andrew Reed 989d5f7dbd Analyze fs write performance
The included example found P99 latency of 2.6ms.
Fio reported P99 latency of 2.5ms with this command:
fio --rw=write --ioengine=sync --fdatasync=1 --directory=/var/lib/etcd
--size=220m --bs=2300
2021-02-17 23:20:38 +00:00
Andrew Reed 8aca23199c Merge pull request #322 from areed/host-preflight-examples
Move host preflights examples into separate directory
2021-02-15 18:18:56 -05:00
Andrew Reed fe4db40b43 Move host preflights examples into separate directory
Add all supported analyzers to host preflight sample.
Don't log transient errors waiting for TCP connection.
Begin human stdout results on new line after spinner.
2021-02-15 22:46:12 +00:00
Andrew Reed 5eef177965 Merge pull request #321 from areed/host-interfaces
Analyze ipv4 interfaces
2021-02-15 16:05:11 -05:00
Andrew ReedandAndrew Lavery 6498c34da5 Analyze ipv4 interfaces
Co-authored-by: Andrew Lavery <laverya@umich.edu>
2021-02-15 20:54:53 +00:00
Andrew Reed b0a005796c Merge pull request #317 from areed/host-remote-port
Analyze TCP connection
2021-02-15 15:18:11 -05:00
Andrew Reed 1311064d21 Merge pull request #320 from areed/host-http-load-balancer
Analyze HTTP load balancer
2021-02-15 12:32:22 -05:00
Andrew Reed 450d7570eb Analyze HTTP load balancer 2021-02-15 17:22:42 +00:00
Andrew Reed 40af0f8a9c Analyze TCP connection 2021-02-12 21:45:57 +00:00
divolgin 529fe8cdbf Merge pull request #319 from replicatedhq/divolgin/cani
log not logs is the subresource
2021-02-12 11:40:36 -08:00
divolgin 105a718bbb log not logs is the subresource 2021-02-12 18:38:46 +00:00
divolgin c2b53036f9 Merge pull request #318 from replicatedhq/divolgin/cani
Fix can-i checks
2021-02-12 09:42:18 -08:00
divolgin ba22fe9f22 Fix can-i checks 2021-02-12 17:27:48 +00:00
Andrew Reed 8df8a5c792 Merge pull request #316 from areed/host-block-device
Analyze block devices
2021-02-11 14:50:22 -05:00
Andrew Reed 0bcd5183f5 Analyze block devices 2021-02-11 19:19:45 +00:00
Andrew Reed e2c0c722ae Merge pull request #315 from areed/host-time
Get time info from systemd
2021-02-10 15:30:49 -05:00
Andrew Reed 9984fe2caa Get time info from timedated 2021-02-10 20:01:15 +00:00
Andrew Reed f2e4127111 Merge pull request #314 from areed/host-preflight-urls
Host HTTP request analyzer
2021-02-10 10:56:54 -05:00
Andrew Reed f25149f45c Host HTTP request analyzer 2021-02-09 20:31:28 +00:00
90 changed files with 7003 additions and 595 deletions
+8 -6
View File
@@ -35,27 +35,29 @@ define LDFLAGS
"
endef
BUILDFLAGS = -tags "netgo containers_image_ostree_stub exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp" -installsuffix netgo
all: test
.PHONY: ffi
ffi: fmt vet
go build ${LDFLAGS} -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/troubleshoot.so -buildmode=c-shared ffi/main.go
# Run tests
test: generate fmt vet
go test ./pkg/... ./cmd/... -coverprofile cover.out
go test ${BUILDFLAGS} ./pkg/... ./cmd/... -coverprofile cover.out
.PHONY: support-bundle
support-bundle: generate fmt vet
go build -tags netgo ${LDFLAGS} -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/support-bundle github.com/replicatedhq/troubleshoot/cmd/troubleshoot
.PHONY: preflight
preflight: generate fmt vet
go build -tags netgo ${LDFLAGS} -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/preflight github.com/replicatedhq/troubleshoot/cmd/preflight
.PHONY: analyze
analyze: generate fmt vet
go build -tags netgo ${LDFLAGS} -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
go build ${BUILDFLAGS} ${LDFLAGS} -o bin/analyze github.com/replicatedhq/troubleshoot/cmd/analyze
.PHONY: fmt
fmt:
@@ -63,7 +65,7 @@ fmt:
.PHONY: vet
vet:
go vet ./pkg/... ./cmd/...
go vet ${BUILDFLAGS} ./pkg/... ./cmd/...
.PHONY: generate
generate: controller-gen client-gen
+43 -22
View File
@@ -91,30 +91,51 @@ func runPreflights(v *viper.Viper, arg string) error {
finishedCh := make(chan bool, 1)
progressCh := make(chan interface{}, 0) // non-zero buffer will result in missed messages
s := spin.New()
go func() {
for {
select {
case msg, ok := <-progressCh:
if !ok {
continue
if v.GetBool("interactive") {
s := spin.New()
go func() {
lastMsg := ""
for {
select {
case msg, ok := <-progressCh:
if !ok {
continue
}
switch msg := msg.(type) {
case error:
c := color.New(color.FgHiRed)
c.Println(fmt.Sprintf("%s\r * %v", cursor.ClearEntireLine(), msg))
case string:
if lastMsg == msg {
break
}
lastMsg = msg
c := color.New(color.FgCyan)
c.Println(fmt.Sprintf("%s\r * %s", cursor.ClearEntireLine(), msg))
}
case <-time.After(time.Millisecond * 100):
fmt.Printf("\r \033[36mRunning Preflight checks\033[m %s ", s.Next())
case <-finishedCh:
fmt.Printf("\r%s\r", cursor.ClearEntireLine())
return
}
switch msg := msg.(type) {
case error:
c := color.New(color.FgHiRed)
c.Println(fmt.Sprintf("%s\r * %v", cursor.ClearEntireLine(), msg))
case string:
c := color.New(color.FgCyan)
c.Println(fmt.Sprintf("%s\r * %s", cursor.ClearEntireLine(), msg))
}
case <-time.After(time.Millisecond * 100):
fmt.Printf("\r \033[36mRunning Preflight checks\033[m %s ", s.Next())
case <-finishedCh:
fmt.Printf("\r%s\r", cursor.ClearEntireLine())
return
}
}
}()
}()
} else {
// make sure we don't block any senders
go func() {
for {
select {
case _, ok := <-progressCh:
if !ok {
return
}
case <-finishedCh:
return
}
}
}()
}
defer func() {
close(finishedCh)
+1
View File
@@ -19,6 +19,7 @@ func showStdoutResults(format string, preflightName string, analyzeResults []*an
}
func showStdoutResultsHuman(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult) error {
fmt.Println("")
var failed bool
for _, analyzeResult := range analyzeResults {
testResultfailed := outputResult(analyzeResult)
+1
View File
@@ -38,6 +38,7 @@ from a server that can be used to assist when troubleshooting a Kubernetes clust
cmd.Flags().StringSlice("redactors", []string{}, "names of the additional redactors to use")
cmd.Flags().Bool("redact", true, "enable/disable default redactions")
cmd.Flags().Bool("interactive", true, "enable/disable interactive mode")
cmd.Flags().Bool("collect-without-permissions", false, "always generate a support bundle, even if it some require additional permissions")
cmd.Flags().String("since-time", "", "force pod logs collectors to return logs after a specific date (RFC3339)")
cmd.Flags().String("since", "", "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.")
+43 -9
View File
@@ -55,7 +55,7 @@ func runTroubleshoot(v *viper.Viper, arg string) error {
httpClient = http.DefaultClient
}
collectorContent, err := loadSpec(v, arg)
collectorContent, err := loadSupportBundleSpec(v, arg)
if err != nil {
return errors.Wrap(err, "failed to load collector spec")
}
@@ -73,7 +73,7 @@ func runTroubleshoot(v *viper.Viper, arg string) error {
additionalRedactors := &troubleshootv1beta2.Redactor{}
for idx, redactor := range v.GetStringSlice("redactors") {
redactorContent, err := loadSpec(v, redactor)
redactorContent, err := loadRedactorSpec(v, redactor)
if err != nil {
return errors.Wrapf(err, "failed to load redactor spec #%d", idx)
}
@@ -200,7 +200,7 @@ func runTroubleshoot(v *viper.Viper, arg string) error {
c.Printf("%s\r * Failed to analyze support bundle: %v\n", cursor.ClearEntireLine(), err)
}
interactive := isatty.IsTerminal(os.Stdout.Fd())
interactive := v.GetBool("interactive") && isatty.IsTerminal(os.Stdout.Fd())
if interactive {
close(finishedCh) // this removes the spinner
@@ -247,13 +247,12 @@ the %s Admin Console to begin analysis.`
return nil
}
func loadSpec(v *viper.Viper, arg string) ([]byte, error) {
var err error
func loadSupportBundleSpec(v *viper.Viper, arg string) ([]byte, error) {
if strings.HasPrefix(arg, "secret/") {
// format secret/namespace-name/secret-name
pathParts := strings.Split(arg, "/")
if len(pathParts) != 3 {
return nil, errors.Errorf("path %s must have 3 components", arg)
return nil, errors.Errorf("secret path %s must have 3 components", arg)
}
spec, err := specs.LoadFromSecret(pathParts[1], pathParts[2], "support-bundle-spec")
@@ -264,6 +263,43 @@ func loadSpec(v *viper.Viper, arg string) ([]byte, error) {
return spec, nil
}
return loadSpec(v, arg)
}
func loadRedactorSpec(v *viper.Viper, arg string) ([]byte, error) {
if strings.HasPrefix(arg, "configmap/") {
// format configmap/namespace-name/configmap-name[/data-key]
pathParts := strings.Split(arg, "/")
if len(pathParts) > 4 {
return nil, errors.Errorf("configmap path %s must have at most 4 components", arg)
}
if len(pathParts) < 3 {
return nil, errors.Errorf("configmap path %s must have at least 3 components", arg)
}
dataKey := "redactor-spec"
if len(pathParts) == 4 {
dataKey = pathParts[3]
}
spec, err := specs.LoadFromConfigMap(pathParts[1], pathParts[2], dataKey)
if err != nil {
return nil, errors.Wrap(err, "failed to get spec from configmap")
}
return spec, nil
}
spec, err := loadSpec(v, arg)
if err != nil {
return nil, errors.Wrap(err, "failed to load spec")
}
return spec, nil
}
func loadSpec(v *viper.Viper, arg string) ([]byte, error) {
var err error
if _, err = os.Stat(arg); err == nil {
b, err := ioutil.ReadFile(arg)
if err != nil {
@@ -413,7 +449,6 @@ func runCollectors(v *viper.Viper, collectors []*troubleshootv1beta2.Collect, ad
Collect: desiredCollector,
ClientConfig: config,
Namespace: v.GetString("namespace"),
PathPrefix: filepath.Base(bundlePath),
}
cleanedCollectors = append(cleanedCollectors, &collector)
}
@@ -464,8 +499,7 @@ func runCollectors(v *viper.Viper, collectors []*troubleshootv1beta2.Collect, ad
}
if result != nil {
// results already contain the bundle dir name in their paths
err = saveCollectorOutput(result, filepath.Dir(bundlePath), collector)
err = saveCollectorOutput(result, bundlePath, collector)
if err != nil {
progressChan <- fmt.Errorf("failed to parse collector spec %q: %v", collector.GetDisplayName(), err)
continue
+2 -19
View File
@@ -4,18 +4,6 @@ release:
owner: replicatedhq
name: troubleshoot
builds:
- id: so
goos:
- linux
goarch:
- amd64
env:
- CGO_ENABLED=1
- GO111MODULE=on
main: ffi/main.go
flags: -buildmode=c-shared
binary: troubleshoot.so
hooks: {}
- id: preflight
goos:
- linux
@@ -31,7 +19,7 @@ builds:
-X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
-X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
-extldflags "-static"
flags: -tags netgo -installsuffix netgo
flags: -tags netgo -tags containers_image_ostree_stub -tags exclude_graphdriver_devicemapper -tags exclude_graphdriver_btrfs -tags containers_image_openpgp -installsuffix netgo
binary: preflight
hooks: {}
- id: support-bundle
@@ -49,15 +37,10 @@ builds:
-X github.com/replicatedhq/troubleshoot/pkg/version.gitSHA={{.Commit}}
-X github.com/replicatedhq/troubleshoot/pkg/version.buildTime={{.Date}}
-extldflags "-static"
flags: -tags netgo -installsuffix netgo
flags: -tags netgo -tags containers_image_ostree_stub -tags exclude_graphdriver_devicemapper -tags exclude_graphdriver_btrfs -tags containers_image_openpgp -installsuffix netgo
binary: support-bundle
hooks: {}
archives:
- id: so
builds:
- so
format: tar.gz
name_template: 'troubleshoot.so_{{ .Os }}_{{ .Arch }}'
- id: preflight
builds:
- preflight
@@ -0,0 +1,18 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: block
spec:
collectors:
- blockDevices: {}
analyzers:
- blockDevices:
outcomes:
- pass:
when: ".* == 1"
message: One available block device
- pass:
when: ".* > 1"
message: Multiple available block devices
- fail:
message: No available block devices
+30
View File
@@ -0,0 +1,30 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: certificate
spec:
collectors:
- certificate:
certificatePath: /etc/ssl/corp.crt
keyPath: /etc/ssl/corp.key
analyzers:
- certificate:
outcomes:
- fail:
when: "key-pair-missing"
message: Certificate key pair not found in /etc/ssl
- fail:
when: "key-pair-switched"
message: Cert and key pair are switched
- fail:
when: "key-pair-encrypted"
message: Private key is encrypted
- fail:
when: "key-pair-mismatch"
message: Cert and key do not match
- fail:
when: "key-pair-invalid"
message: Certificate key pair is invalid
- pass:
when: "key-pair-valid"
message: Certificate key pair is valid
@@ -0,0 +1,31 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: fsperf
spec:
collectors:
- filesystemPerformance:
collectorName: etcd-perf
directory: /var/lib/etcd
fileSize: 22Mi
operationSizeBytes: 2300
datasync: true
analyzers:
- filesystemPerformance:
collectorName: etcd-perf
outcomes:
- fail:
when: "iops < 50"
message: Insufficient random read IOPS
- pass:
when: "p99 < 3ms"
message: Write latency is great!
- pass:
when: "p99 < 5ms"
message: Write latency is ok
- warn:
when: "p99 < 8ms"
message: Write latency is high
- fail:
when: "p99 >= 8ms"
message: Write latency is too high
@@ -0,0 +1,33 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: httploadbalancer
spec:
collectors:
- httpLoadBalancer:
collectorName: httploadbalancer
port: 80
address: http://app.corporate.internal
timeout: 10s
analyzers:
- httpLoadBalancer:
collectorName: httploadbalancer
outcomes:
- fail:
when: "connection-refused"
message: Connection to port 80 via load balancer was refused.
- fail:
when: "address-in-use"
message: Another process was already listening on port 80.
- fail:
when: "connection-timeout"
message: Timed out connecting to port 80 via load balancer. Check your firewall.
- fail:
when: "bind-permission-denied"
message: Bind permission denied. Try running as root.
- fail:
when: "error"
message: Failed to connect to port 80 via load balancer.
- pass:
when: "connected"
message: Successfully connected to port 80 via load balancer.
+22
View File
@@ -0,0 +1,22 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: http
spec:
collectors:
- http:
collectorName: registry
get:
url: https://registry.replicated.com
analyzers:
- http:
collectorName: registry
outcomes:
- fail:
when: "error"
message: Error connecting to registry
- pass:
when: "statusCode == 404"
message: Connected to registry
- fail:
message: "Unexpected response"
@@ -0,0 +1,19 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: ipv4Interfaces
spec:
collectors:
- ipv4Interfaces: {}
analyzers:
- ipv4Interfaces:
outcomes:
- fail:
when: "count == 0"
message: No IPv4 interfaces detected
- warn:
when: "count >= 2"
message: Multiple IPv4 interfaces detected
- pass:
when: "count == 1"
message: IPv4 interface detected
+22
View File
@@ -0,0 +1,22 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: ntp
spec:
collectors:
- time: {}
analyzers:
- time:
outcomes:
- fail:
when: "ntp == unsynchronized+inactive"
message: System clock not synchronized
- warn:
when: "ntp == unsynchronized+active"
message: System clock not yet synchronized
- warn:
when: "ntp == synchronized+inactive"
message: NTP not active
- pass:
when: "ntp == synchronized+active"
message: System clock is synchronized
+238
View File
@@ -0,0 +1,238 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: example
spec:
collectors:
- blockDevices: {}
- certificate:
certificatePath: /etc/ssl/corp.crt
keyPath: /etc/ssl/corp.key
- cpu: {}
- diskUsage:
collectorName: ephemeral
path: /var/lib/kubelet
- filesystemPerformance:
collectorName: etcd-perf
directory: /var/lib/etcd
fileSize: 22Mi
operationSizeBytes: 2300
datasync: true
- httpLoadBalancer:
collectorName: httploadbalancer
port: 80
address: http://app.corporate.internal
timeout: 10s
- http:
collectorName: registry
get:
url: https://registry.replicated.com
- ipv4Interfaces: {}
- memory: {}
- time: {}
- tcpConnect:
collectorName: weave host 1
address: 10.128.0.2:6783
timeout: 2s
- tcpLoadBalancer:
collectorName: LB1
address: 10.128.0.20:6443
port: 6443
timeout: 5000ms
- tcpPortStatus:
collectorName: k8s
port: 6443
analyzers:
- blockDevices:
outcomes:
- pass:
when: ".* == 1"
message: One available block device
- pass:
when: ".* > 1"
message: Multiple available block devices
- fail:
message: No available block devices
- certificate:
outcomes:
- fail:
when: "key-pair-missing"
message: Certificate key pair not found in /etc/ssl
- fail:
when: "key-pair-switched"
message: Cert and key pair are switched
- fail:
when: "key-pair-encrypted"
message: Private key is encrypted
- fail:
when: "key-pair-mismatch"
message: Cert and key do not match
- fail:
when: "key-pair-invalid"
message: Certificate key pair is invalid
- pass:
when: "key-pair-valid"
message: Certificate key pair is valid
- 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
- 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
- filesystemPerformance:
collectorName: etcd-perf
outcomes:
- fail:
when: "iops < 50"
message: Insufficient random read IOPS
- pass:
when: "p99 < 3ms"
message: Write latency is great!
- pass:
when: "p99 < 5ms"
message: Write latency is ok
- warn:
when: "p99 < 8ms"
message: Write latency is high
- fail:
when: "p99 >= 8ms"
message: Write latency is too high
- httpLoadBalancer:
collectorName: httploadbalancer
outcomes:
- fail:
when: "connection-refused"
message: Connection to port 80 via load balancer was refused.
- fail:
when: "address-in-use"
message: Another process was already listening on port 80.
- fail:
when: "connection-timeout"
message: Timed out connecting to port 80 via load balancer. Check your firewall.
- fail:
when: "bind-permission-denied"
message: Bind permission denied. Try running as root.
- fail:
when: "error"
message: Failed to connect to port 80 via load balancer.
- pass:
when: "connected"
message: Successfully connected to port 80 via load balancer.
- http:
collectorName: registry
outcomes:
- fail:
when: "error"
message: Error connecting to registry
- pass:
when: "statusCode == 404"
message: Connected to registry
- fail:
message: "Unexpected response"
- ipv4Interfaces:
outcomes:
- fail:
when: "count == 0"
message: No IPv4 interfaces detected
- warn:
when: "count >= 2"
message: Multiple IPv4 interfaces detected
- pass:
when: "count == 1"
message: IPv4 interface detected
- 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
- time:
outcomes:
- fail:
when: "ntp == unsynchronized+inactive"
message: System clock not synchronized
- warn:
when: "ntp == unsynchronized+active"
message: System clock not yet synchronized
- warn:
when: "ntp == synchronized+inactive"
message: NTP not active
- pass:
when: "ntp == synchronized+active"
message: System clock is synchronized
- tcpConnect:
collectorName: weave host 1
outcomes:
- fail:
when: "connection-refused"
message: Connection to weave on host 1 was refused
- fail:
when: "connection-timeout"
message: Timed out connecting to weave on host 1
- fail:
when: "error"
message: Unexpected error connecting to weave on host 1
- pass:
when: "connected"
message: Successfully connected to weave on host 1
- 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.
- 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
- time:
outcomes:
- pass:
when: "timezone == UTC"
message: Timezone is UTC
- fail:
message: Timezone is not UTC
+25
View File
@@ -0,0 +1,25 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: connect
spec:
collectors:
- tcpConnect:
collectorName: weave host 1
address: 10.128.0.2:6783
analyzers:
- tcpConnect:
collectorName: weave host 1
outcomes:
- fail:
when: "connection-refused"
message: Connection to weave on host 1 was refused
- fail:
when: "connection-timeout"
message: Timed out connecting to weave on host 1
- fail:
when: "error"
message: Unexpected error connecting to weave on host 1
- pass:
when: "connected"
message: Successfully connected to weave on host 1
+15
View File
@@ -0,0 +1,15 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostPreflight
metadata:
name: timezone
spec:
collectors:
- time: {}
analyzers:
- time:
outcomes:
- pass:
when: "timezone == UTC"
message: Timezone is UTC
- fail:
message: Timezone is not UTC
@@ -1,51 +0,0 @@
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
+6 -1
View File
@@ -60,13 +60,18 @@ spec:
- pass:
when: "== aks"
message: AKS is a supported distribution
# Will be supported in the future
- pass:
when: "== kurl"
message: KURL is a supported distribution
- pass:
when: "== digitalocean"
message: DigitalOcean is a supported distribution
- pass:
when: "== rke2"
message: RKE2 is a supported distribution
- pass:
when: "== k3s"
message: K3S is a supported distribution
- warn:
message: Unable to determine the distribution of Kubernetes
- nodeResources:
+23 -23
View File
@@ -3,52 +3,52 @@ module github.com/replicatedhq/troubleshoot
go 1.12
require (
cloud.google.com/go v0.57.0 // indirect
github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412
github.com/aws/aws-sdk-go v1.25.18 // indirect
github.com/blang/semver v3.5.1+incompatible
github.com/chzyer/logex v1.1.11-0.20160617073814-96a4d311aa9b // indirect
github.com/containers/image/v5 v5.10.4
github.com/docker/distribution v2.7.1+incompatible
github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect
github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect
github.com/emicklei/go-restful v2.9.6+incompatible // indirect
github.com/fatih/color v1.7.0
github.com/go-ole/go-ole v1.2.5 // indirect
github.com/go-openapi/spec v0.19.4 // indirect
github.com/go-redis/redis/v7 v7.2.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.3.1 // indirect
github.com/godbus/dbus v4.1.0+incompatible
github.com/google/gofuzz v1.1.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/hashicorp/go-getter v1.3.1-0.20190627223108-da0323b9545e
github.com/hashicorp/go-multierror v1.0.0
github.com/imdario/mergo v0.3.8 // indirect
github.com/hashicorp/go-multierror v1.1.1
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.3.0
github.com/manifoldco/promptui v0.3.2
github.com/manifoldco/promptui v0.8.0
github.com/mattn/go-colorable v0.1.4 // indirect
github.com/mattn/go-isatty v0.0.9
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/nicksnyder/go-i18n v1.10.1 // indirect
github.com/onsi/gomega v1.9.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pkg/errors v0.9.1
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.21.1+incompatible
github.com/spf13/cobra v0.0.5
github.com/spf13/cobra v1.1.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.4.0
github.com/stretchr/testify v1.5.1
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.7.0
github.com/tj/go-spin v1.1.0
github.com/ulikunitz/xz v0.5.6 // indirect
go.opencensus.io v0.22.0 // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a // indirect
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
golang.org/x/tools v0.0.0-20191010075000-0337d82405ff // indirect
gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 // indirect
gopkg.in/yaml.v2 v2.2.8
k8s.io/api v0.18.3
k8s.io/apiextensions-apiserver v0.18.2
k8s.io/apimachinery v0.18.3
k8s.io/cli-runtime v0.18.0
k8s.io/client-go v0.18.2
sigs.k8s.io/controller-runtime v0.5.1-0.20200402191424-df180accb901
golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v2 v2.3.0
k8s.io/api v0.20.2
k8s.io/apiextensions-apiserver v0.20.2
k8s.io/apimachinery v0.20.2
k8s.io/cli-runtime v0.20.2
k8s.io/client-go v0.20.2
sigs.k8s.io/controller-runtime v0.8.3
)
+595 -237
View File
File diff suppressed because it is too large Load Diff
+45 -36
View File
@@ -1,6 +1,7 @@
package analyzer
import (
"fmt"
"strconv"
"github.com/pkg/errors"
@@ -40,44 +41,37 @@ 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
func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) []*AnalyzeResult {
analyzer, ok := GetHostAnalyzer(hostAnalyzer)
if !ok {
return NewAnalyzeResultError(analyzer, errors.New("invalid analyzer"))
}
return nil, errors.New("invalid analyzer")
isExcluded, _ := analyzer.IsExcluded()
if isExcluded {
return nil
}
result, err := analyzer.Analyze(getFile)
if err != nil {
return NewAnalyzeResultError(analyzer, errors.Wrap(err, "analyze"))
}
return []*AnalyzeResult{result}
}
func NewAnalyzeResultError(analyzer HostAnalyzer, err error) []*AnalyzeResult {
if analyzer != nil {
return []*AnalyzeResult{{
IsFail: true,
Title: analyzer.Title(),
Message: fmt.Sprintf("Analyzer Failed: %v", err),
}}
}
return []*AnalyzeResult{{
IsFail: true,
Title: "nil analyzer",
Message: fmt.Sprintf("Analyzer Failed: %v", err),
}}
}
func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
@@ -306,5 +300,20 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont
return []*AnalyzeResult{result}, nil
}
if analyzer.RegistryImages != nil {
isExcluded, err := isExcluded(analyzer.RegistryImages.Exclude)
if err != nil {
return nil, err
}
if isExcluded {
return nil, nil
}
result, err := analyzeRegistry(analyzer.RegistryImages, getFile)
if err != nil {
return nil, err
}
return []*AnalyzeResult{result}, nil
}
return nil, errors.New("invalid analyzer")
}
+27
View File
@@ -22,6 +22,8 @@ type providers struct {
aks bool
ibm bool
minikube bool
rke2 bool
k3s bool
}
type Provider int
@@ -38,6 +40,8 @@ const (
aks Provider = iota
ibm Provider = iota
minikube Provider = iota
rke2 Provider = iota
k3s Provider = iota
)
func CheckOpenShift(foundProviders *providers, apiResources []*metav1.APIResourceList, provider string) string {
@@ -77,6 +81,21 @@ func ParseNodesForProviders(nodes []corev1.Node) (providers, string) {
foundProviders.minikube = true
stringProvider = "minikube"
}
if k == "node.kubernetes.io/instance-type" && v == "k3s" {
foundProviders.k3s = true
stringProvider = "k3s"
}
if k == "beta.kubernetes.io/instance-type" && v == "k3s" {
foundProviders.k3s = true
stringProvider = "k3s"
}
}
for k := range node.ObjectMeta.Annotations {
if k == "rke2.io/node-args" {
foundProviders.rke2 = true
stringProvider = "rke2"
}
}
if node.Status.NodeInfo.OSImage == "Docker Desktop" {
@@ -270,6 +289,10 @@ func compareDistributionConditionalToActual(conditional string, actual providers
isMatch = actual.ibm
case minikube:
isMatch = actual.minikube
case rke2:
isMatch = actual.rke2
case k3s:
isMatch = actual.k3s
}
switch parts[0] {
@@ -304,6 +327,10 @@ func mustNormalizeDistributionName(raw string) Provider {
return ibm
case "minikube":
return minikube
case "rke2":
return rke2
case "k3s":
return k3s
}
return unknown
+51
View File
@@ -0,0 +1,51 @@
package analyzer
import troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
type HostAnalyzer interface {
Title() string
IsExcluded() (bool, error)
Analyze(getFile func(string) ([]byte, error)) (*AnalyzeResult, error)
}
func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, bool) {
switch {
case analyzer.CPU != nil:
return &AnalyzeHostCPU{analyzer.CPU}, true
case analyzer.Memory != nil:
return &AnalyzeHostMemory{analyzer.Memory}, true
case analyzer.TCPLoadBalancer != nil:
return &AnalyzeHostTCPLoadBalancer{analyzer.TCPLoadBalancer}, true
case analyzer.HTTPLoadBalancer != nil:
return &AnalyzeHostHTTPLoadBalancer{analyzer.HTTPLoadBalancer}, true
case analyzer.DiskUsage != nil:
return &AnalyzeHostDiskUsage{analyzer.DiskUsage}, true
case analyzer.TCPPortStatus != nil:
return &AnalyzeHostTCPPortStatus{analyzer.TCPPortStatus}, true
case analyzer.HTTP != nil:
return &AnalyzeHostHTTP{analyzer.HTTP}, true
case analyzer.Time != nil:
return &AnalyzeHostTime{analyzer.Time}, true
case analyzer.BlockDevices != nil:
return &AnalyzeHostBlockDevices{analyzer.BlockDevices}, true
case analyzer.TCPConnect != nil:
return &AnalyzeHostTCPConnect{analyzer.TCPConnect}, true
case analyzer.IPV4Interfaces != nil:
return &AnalyzeHostIPV4Interfaces{analyzer.IPV4Interfaces}, true
case analyzer.FilesystemPerformance != nil:
return &AnalyzeHostFilesystemPerformance{analyzer.FilesystemPerformance}, true
case analyzer.Certificate != nil:
return &AnalyzeHostCertificate{analyzer.Certificate}, true
case analyzer.HostServices != nil:
return &AnalyzeHostServices{analyzer.HostServices}, true
default:
return nil, false
}
}
func hostAnalyzerTitleOrDefault(meta troubleshootv1beta2.AnalyzeMeta, defaultTitle string) string {
if meta.CheckName != "" {
return meta.CheckName
}
return defaultTitle
}
+205
View File
@@ -0,0 +1,205 @@
package analyzer
import (
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeHostBlockDevices struct {
hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze
}
func (a *AnalyzeHostBlockDevices) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Block Devices")
}
func (a *AnalyzeHostBlockDevices) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostBlockDevices) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents("system/block_devices.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
var devices []collect.BlockDeviceInfo
if err := json.Unmarshal(contents, &devices); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal block devices info")
}
result := AnalyzeResult{}
result.Title = a.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 := compareHostBlockDevicesConditionalToActual(outcome.Fail.When, hostAnalyzer.MinimumAcceptableSize, hostAnalyzer.IncludeUnmountedPartitions, devices)
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 := compareHostBlockDevicesConditionalToActual(outcome.Warn.When, hostAnalyzer.MinimumAcceptableSize, hostAnalyzer.IncludeUnmountedPartitions, devices)
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 := compareHostBlockDevicesConditionalToActual(outcome.Pass.When, hostAnalyzer.MinimumAcceptableSize, hostAnalyzer.IncludeUnmountedPartitions, devices)
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
}
// <regexp> <op> <count>
// example: sdb > 0
func compareHostBlockDevicesConditionalToActual(conditional string, minimumAcceptableSize uint64, includeUnmountedPartitions bool, devices []collect.BlockDeviceInfo) (res bool, err error) {
parts := strings.Split(conditional, " ")
if len(parts) != 3 {
return false, fmt.Errorf("Expected exactly 3 parts, got %d", len(parts))
}
rx, err := regexp.Compile(parts[0])
if err != nil {
return false, errors.Wrapf(err, "failed to compile regex %q", parts[0])
}
count := countEligibleBlockDevices(rx, minimumAcceptableSize, includeUnmountedPartitions, devices)
desiredInt, err := strconv.Atoi(parts[2])
if err != nil {
return false, errors.Wrapf(err, "failed to parse desired quantity %q", parts[2])
}
switch parts[1] {
case ">":
return count > desiredInt, nil
case ">=":
return count >= desiredInt, nil
case "<":
return count < desiredInt, nil
case "<=":
return count <= desiredInt, nil
case "=", "==", "===":
return count == desiredInt, nil
}
return false, fmt.Errorf("Unexpected operator %q", parts[1])
}
func countEligibleBlockDevices(rx *regexp.Regexp, minimumAcceptableSize uint64, includeUnmountedPartitions bool, devices []collect.BlockDeviceInfo) int {
count := 0
for _, device := range devices {
if isEligibleBlockDevice(rx, minimumAcceptableSize, includeUnmountedPartitions, device, devices) {
count++
}
}
return count
}
func isEligibleBlockDevice(rx *regexp.Regexp, minimumAcceptableSize uint64, includeUnmountedPartitions bool, device collect.BlockDeviceInfo, devices []collect.BlockDeviceInfo) bool {
if !rx.MatchString(device.Name) {
return false
}
if includeUnmountedPartitions {
if device.Type != "disk" && device.Type != "part" {
return false
}
} else {
if device.Type != "disk" {
return false
}
}
if minimumAcceptableSize != 0 {
if device.Size < minimumAcceptableSize {
return false
}
}
if device.Mountpoint != "" {
return false
}
if device.FilesystemType != "" {
return false
}
if device.ReadOnly {
return false
}
if device.Removable {
return false
}
for _, d := range devices {
if d.ParentKernelName == device.KernelName {
return false
}
}
return true
}
+266
View File
@@ -0,0 +1,266 @@
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 TestAnalyzeBlockDevices(t *testing.T) {
tests := []struct {
name string
devices []collect.BlockDeviceInfo
hostAnalyzer *troubleshootv1beta2.BlockDevicesAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "sdb == 1, pass when there is an empty /dev/sdb",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "sdb == 1",
Message: "Block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsPass: true,
Message: "Block device available",
},
},
{
name: "sdb == 1, fail when partitioned",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
},
{
Name: "sdb1",
KernelName: "sdb1",
ParentKernelName: "sdb",
Type: "part",
Major: 8,
Minor: 1,
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "sdb == 1",
Message: "Block device available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "No block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsFail: true,
Message: "No block device available",
},
},
{
name: "sdb == 1, fail when it has a filesystem",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
FilesystemType: "ext4",
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "sdb == 1",
Message: "Block device available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "No block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsFail: true,
Message: "No block device available",
},
},
{
name: ".* > 0, fail when only loop devices are found",
devices: []collect.BlockDeviceInfo{
{
Name: "loop0",
KernelName: "loop0",
Type: "loop",
Major: 7,
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: ".* > 0",
Message: "Block device available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "No block device available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsFail: true,
Message: "No block device available",
},
},
{
name: ".* > 1, pass with unmounted partition",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
},
{
Name: "sdb1",
KernelName: "sdb1",
ParentKernelName: "sdb",
Type: "part",
Major: 8,
Minor: 1,
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
IncludeUnmountedPartitions: true,
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: ".* >= 1",
Message: "Block device or partition available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsPass: true,
Message: "Block device or partition available",
},
},
{
name: ".* = 2, pass with two unmounted partitions/devices of at least 10gb in size",
devices: []collect.BlockDeviceInfo{
{
Name: "sdb",
KernelName: "sdb",
Type: "disk",
Major: 8,
Serial: "disk1",
Size: 1024 * 1024 * 1024 * 128,
},
{
Name: "sdb1",
KernelName: "sdb1",
ParentKernelName: "sdb",
Type: "part",
Major: 8,
Minor: 1,
Size: 1024 * 1024 * 1024 * 128,
},
{
Name: "sdc",
KernelName: "sdc",
Type: "disk",
Major: 8,
Serial: "disk2",
Size: 1024 * 1024 * 1024 * 16,
},
{
Name: "sdd",
KernelName: "sdd",
Type: "disk",
Major: 8,
Serial: "disk3",
Size: 1024 * 1024 * 1024 * 8,
},
},
hostAnalyzer: &troubleshootv1beta2.BlockDevicesAnalyze{
IncludeUnmountedPartitions: true,
MinimumAcceptableSize: 1024 * 1024 * 1024 * 10,
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: ".* = 2",
Message: "Two block devices or partitions of >10gb size available",
},
},
},
},
result: &AnalyzeResult{
Title: "Block Devices",
IsPass: true,
Message: "Two block devices or partitions of >10gb size available",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.devices)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostBlockDevices{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+69
View File
@@ -0,0 +1,69 @@
package analyzer
import (
"path/filepath"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type AnalyzeHostCertificate struct {
hostAnalyzer *troubleshootv1beta2.CertificateAnalyze
}
func (a *AnalyzeHostCertificate) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Certificate Key Pair")
}
func (a *AnalyzeHostCertificate) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostCertificate) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "certificate"
}
name := filepath.Join("certificate", collectorName+".json")
contents, err := getCollectedFileContents(name)
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
status := string(contents)
result := AnalyzeResult{}
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
if outcome.Fail.When == "" || outcome.Fail.When == status {
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 == "" || outcome.Warn.When == status {
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 == "" || outcome.Pass.When == status {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return &result, nil
}
}
}
return &result, nil
}
+333
View File
@@ -0,0 +1,333 @@
package analyzer
import (
"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 TestAnalyzeCertificate(t *testing.T) {
tests := []struct {
name string
status string
hostAnalyzer *troubleshootv1beta2.CertificateAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "key-pair-valid",
status: collect.KeyPairValid,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsPass: true,
Message: "Certificate key pair is valid",
},
},
{
name: "key-pair-invalid",
status: collect.KeyPairInvalid,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Certificate key pair is invalid",
},
},
{
name: "key-pair-missing",
status: collect.KeyPairMissing,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Certificate key pair not found",
},
},
{
name: "key-pair-switched",
status: collect.KeyPairSwitched,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Public and private keys switched",
},
},
{
name: "key-pair-encrypted",
status: collect.KeyPairEncrypted,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is encrypted",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Private key is encrypted",
},
},
{
name: "key-pair-mismatch",
status: collect.KeyPairMismatch,
hostAnalyzer: &troubleshootv1beta2.CertificateAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-missing",
Message: "Certificate key pair not found",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-switched",
Message: "Public and private keys switched",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-encrypted",
Message: "Private key is mismatch",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-mismatch",
Message: "Public and private keys don't match",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-invalid",
Message: "Certificate key pair is invalid",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "key-pair-valid",
Message: "Certificate key pair is valid",
},
},
},
},
result: &AnalyzeResult{
Title: "Certificate Key Pair",
IsFail: true,
Message: "Public and private keys don't match",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
getCollectedFileContents := func(filename string) ([]byte, error) {
return []byte(test.status), nil
}
result, err := (&AnalyzeHostCertificate{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+16 -6
View File
@@ -10,7 +10,21 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
func analyzeHostCPU(hostAnalyzer *troubleshootv1beta2.CPUAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
type AnalyzeHostCPU struct {
hostAnalyzer *troubleshootv1beta2.CPUAnalyze
}
func (a *AnalyzeHostCPU) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Number of CPUs")
}
func (a *AnalyzeHostCPU) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostCPU) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents("system/cpu.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
@@ -23,11 +37,7 @@ func analyzeHostCPU(hostAnalyzer *troubleshootv1beta2.CPUAnalyze, getCollectedFi
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "Number of CPUs"
}
result.Title = title
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
+16 -6
View File
@@ -12,7 +12,21 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
)
func analyzeHostDiskUsage(hostAnalyzer *troubleshootv1beta2.DiskUsageAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
type AnalyzeHostDiskUsage struct {
hostAnalyzer *troubleshootv1beta2.DiskUsageAnalyze
}
func (a *AnalyzeHostDiskUsage) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, fmt.Sprintf("Disk Usage %s", a.hostAnalyzer.CollectorName))
}
func (a *AnalyzeHostDiskUsage) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostDiskUsage) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
key := collect.HostDiskUsageKey(hostAnalyzer.CollectorName)
contents, err := getCollectedFileContents(key)
if err != nil {
@@ -26,11 +40,7 @@ func analyzeHostDiskUsage(hostAnalyzer *troubleshootv1beta2.DiskUsageAnalyze, ge
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = fmt.Sprintf("Disk Usage %s", hostAnalyzer.CollectorName)
}
result.Title = title
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
+1 -1
View File
@@ -367,7 +367,7 @@ func TestAnalyzeHostDiskUsage(t *testing.T) {
return b, nil
}
result, err := analyzeHostDiskUsage(test.hostAnalyzer, getCollectedFileContents)
result, err := (&AnalyzeHostDiskUsage{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
+219
View File
@@ -0,0 +1,219 @@
package analyzer
import (
"encoding/json"
"fmt"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeHostFilesystemPerformance struct {
hostAnalyzer *troubleshootv1beta2.FilesystemPerformanceAnalyze
}
func (a *AnalyzeHostFilesystemPerformance) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Filesystem Performance")
}
func (a *AnalyzeHostFilesystemPerformance) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostFilesystemPerformance) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "filesystemPerformance"
}
name := filepath.Join("filesystemPerformance", collectorName+".json")
contents, err := getCollectedFileContents(name)
if err != nil {
return nil, errors.Wrapf(err, "failed to get collected file %s", name)
}
fsPerf := collect.FSPerfResults{}
if err := json.Unmarshal(contents, &fsPerf); err != nil {
return nil, errors.Wrapf(err, "failed to unmarshal filesystem performance results from %s", name)
}
result := AnalyzeResult{}
result.Title = a.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 := compareHostFilesystemPerformanceConditionalToActual(outcome.Fail.When, fsPerf)
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 := compareHostFilesystemPerformanceConditionalToActual(outcome.Warn.When, fsPerf)
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 := compareHostFilesystemPerformanceConditionalToActual(outcome.Pass.When, fsPerf)
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 compareHostFilesystemPerformanceConditionalToActual(conditional string, fsPerf collect.FSPerfResults) (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))
}
keyword := strings.ToLower(parts[0])
comparator := parts[1]
if keyword == "iops" {
desiredInt, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return false, errors.Wrapf(err, "failed to parse desired iops %q as integer", parts[2])
}
return doCompareHostFilesystemIOPS(comparator, fsPerf.IOPS, int(desiredInt))
}
desiredDuration, err := time.ParseDuration(parts[2])
if err != nil {
return false, errors.Wrapf(err, "failed to parse duration %q", parts[2])
}
switch keyword {
case "min":
return doCompareHostFilesystemPerformance(comparator, fsPerf.Min, desiredDuration)
case "max":
return doCompareHostFilesystemPerformance(comparator, fsPerf.Max, desiredDuration)
case "average":
return doCompareHostFilesystemPerformance(comparator, fsPerf.Average, desiredDuration)
case "p1":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P1, desiredDuration)
case "p5":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P5, desiredDuration)
case "p10":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P10, desiredDuration)
case "p20":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P20, desiredDuration)
case "p30":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P30, desiredDuration)
case "p40":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P40, desiredDuration)
case "p50":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P50, desiredDuration)
case "p60":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P60, desiredDuration)
case "p70":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P70, desiredDuration)
case "p80":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P80, desiredDuration)
case "p90":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P90, desiredDuration)
case "p95":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P95, desiredDuration)
case "p99":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P99, desiredDuration)
case "p995":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P995, desiredDuration)
case "p999":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P999, desiredDuration)
case "p9995":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P9995, desiredDuration)
case "p9999":
return doCompareHostFilesystemPerformance(comparator, fsPerf.P9999, desiredDuration)
}
return false, fmt.Errorf("Unknown filesystem performance keyword %q", keyword)
}
func doCompareHostFilesystemPerformance(operator string, actual time.Duration, desired time.Duration) (bool, error) {
switch operator {
case "<":
return actual < desired, nil
case "<=":
return actual <= desired, nil
case ">":
return actual > desired, nil
case ">=":
return actual >= desired, nil
case "=", "==", "===":
return actual == desired, nil
}
return false, fmt.Errorf("Unknown filesystem performance operator %q", operator)
}
func doCompareHostFilesystemIOPS(operator string, actual int, desired int) (bool, error) {
switch operator {
case "<":
return actual < desired, nil
case "<=":
return actual <= desired, nil
case ">":
return actual > desired, nil
case ">=":
return actual >= desired, nil
case "=", "==", "===":
return actual == desired, nil
}
return false, fmt.Errorf("Unknown filesystem IOPS operator %q", operator)
}
@@ -0,0 +1,365 @@
package analyzer
import (
"encoding/json"
"testing"
"time"
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 TestAnalyzeHostFilesystemPerformance(t *testing.T) {
tests := []struct {
name string
fsPerf *collect.FSPerfResults
hostAnalyzer *troubleshootv1beta2.FilesystemPerformanceAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "IOPS",
fsPerf: &collect.FSPerfResults{
IOPS: 50,
},
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "iops < 20",
Message: "IOPS < 20",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "iops <= 20",
Message: "IOPS <= 30",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "iops > 70",
Message: "IOPS > 70",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "iops >= 100",
Message: "IOPS >= 100",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "iops == 50",
Message: "IOPS == 50",
},
},
},
},
result: &AnalyzeResult{
Title: "Filesystem Performance",
IsPass: true,
Message: "IOPS == 50",
},
},
{
name: "Cover",
fsPerf: &collect.FSPerfResults{
Min: 200 * time.Nanosecond,
Max: time.Second,
Average: 55 * time.Microsecond,
P1: 1 * time.Microsecond,
P5: 5 * time.Microsecond,
P10: 10 * time.Microsecond,
P20: 20 * time.Microsecond,
P30: 30 * time.Microsecond,
P40: 40 * time.Microsecond,
P50: 50 * time.Microsecond,
P60: 60 * time.Microsecond,
P70: 70 * time.Microsecond,
P80: 80 * time.Microsecond,
P90: 90 * time.Microsecond,
P95: 95 * time.Microsecond,
P99: 99 * time.Microsecond,
P995: 995 * time.Microsecond,
P999: 999 * time.Microsecond,
P9995: 5 * time.Millisecond,
P9999: 9 * time.Millisecond,
},
hostAnalyzer: &troubleshootv1beta2.FilesystemPerformanceAnalyze{
CollectorName: "etcd",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "min == 0",
Message: "min not working",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "min <= 50ns",
Message: "lte operator not working",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "max == 0",
Message: "max not working",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "max >= 1m",
Message: "gte operator not working",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "average == 0",
Message: "average not working",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p1 < 1us",
Message: "P1 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p1 > 1us",
Message: "P1 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p5 < 5us",
Message: "P5 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p5 > 5us",
Message: "P5 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p10 < 10us",
Message: "P10 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p10 > 10us",
Message: "P10 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p20 < 20us",
Message: "P20 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p20 > 20us",
Message: "P20 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p30 < 30us",
Message: "P30 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p30 > 30us",
Message: "P30 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p40 < 40us",
Message: "P40 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p40 > 40us",
Message: "P40 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p50 < 50us",
Message: "P50 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p50 > 50us",
Message: "P50 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p60 < 60us",
Message: "P60 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p60 > 60us",
Message: "P60 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p70 < 70us",
Message: "P70 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p70 > 70us",
Message: "P70 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p80 < 80us",
Message: "P80 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p80 > 80us",
Message: "P80 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p90 < 90us",
Message: "P90 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p90 > 90us",
Message: "P90 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p95 < 95us",
Message: "P95 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p95 > 95us",
Message: "P95 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p99 < 99us",
Message: "P99 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p99 > 99us",
Message: "P99 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p995 < 995us",
Message: "P995 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p995 > 995us",
Message: "P995 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p999 < 999us",
Message: "P999 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p999 > 999us",
Message: "P999 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p9995 < 5ms",
Message: "P9995 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p9995 > 5ms",
Message: "P9995 too high",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p9999 < 9ms",
Message: "P9999 too low",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "p9999 > 9ms",
Message: "P9999 too high",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "p9999 < 10ms",
Message: "Acceptable write latency",
},
},
},
},
result: &AnalyzeResult{
Title: "Filesystem Performance",
IsPass: true,
Message: "Acceptable write latency",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.fsPerf)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostFilesystemPerformance{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+150
View File
@@ -0,0 +1,150 @@
package analyzer
import (
"encoding/json"
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type httpResult struct {
Error *collect.HTTPError
Response *collect.HTTPResponse
}
type AnalyzeHostHTTP struct {
hostAnalyzer *troubleshootv1beta2.HTTPAnalyze
}
func (a *AnalyzeHostHTTP) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "HTTP Request")
}
func (a *AnalyzeHostHTTP) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostHTTP) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
name := filepath.Join("http", "result.json")
if hostAnalyzer.CollectorName != "" {
name = filepath.Join("http", hostAnalyzer.CollectorName+".json")
}
contents, err := getCollectedFileContents(name)
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
httpInfo := &httpResult{}
if err := json.Unmarshal(contents, httpInfo); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal http result")
}
result := AnalyzeResult{}
result.Title = a.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 := compareHostHTTPConditionalToActual(outcome.Fail.When, httpInfo)
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 := compareHostHTTPConditionalToActual(outcome.Warn.When, httpInfo)
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 := compareHostHTTPConditionalToActual(outcome.Pass.When, httpInfo)
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 compareHostHTTPConditionalToActual(conditional string, result *httpResult) (res bool, err error) {
if conditional == "error" {
return result.Error != nil, nil
}
parts := strings.Split(conditional, " ")
if len(parts) != 3 {
return false, fmt.Errorf("Failed to parse conditional: got %d parts", len(parts))
}
if parts[0] != "statusCode" {
return false, errors.New(`Conditional must begin with keyword "statusCode"`)
}
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" {
return false, errors.New(`Only supported operator is "=="`)
}
i, err := strconv.Atoi(parts[2])
if err != nil {
return false, err
}
if result.Response == nil {
return false, err
}
return result.Response.Status == i, nil
}
+104
View File
@@ -0,0 +1,104 @@
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 TestAnalyzeHostHTTP(t *testing.T) {
tests := []struct {
name string
httpResult *httpResult
hostAnalyzer *troubleshootv1beta2.HTTPAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "error",
httpResult: &httpResult{
Error: &collect.HTTPError{
Message: "i/o timeout",
},
},
hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{
CollectorName: "registry",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "error",
Message: "Failed to reach replicated.registry.com",
},
},
},
},
result: &AnalyzeResult{
Title: "HTTP Request",
IsFail: true,
Message: "Failed to reach replicated.registry.com",
},
},
{
name: "200",
httpResult: &httpResult{
Response: &collect.HTTPResponse{
Status: 200,
},
},
hostAnalyzer: &troubleshootv1beta2.HTTPAnalyze{
CollectorName: "registry",
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "error",
Message: "Failed to reach replicated.registry.com",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "statusCode == 204",
Message: "No content",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "statusCode == 200",
Message: "Successfully reached registry",
},
},
},
},
result: &AnalyzeResult{
Title: "HTTP Request",
IsPass: true,
Message: "Successfully reached registry",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.httpResult)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostHTTP{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+100
View File
@@ -0,0 +1,100 @@
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"
)
type AnalyzeHostHTTPLoadBalancer struct {
hostAnalyzer *troubleshootv1beta2.HTTPLoadBalancerAnalyze
}
func (a *AnalyzeHostHTTPLoadBalancer) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "HTTP Load Balancer")
}
func (a *AnalyzeHostHTTPLoadBalancer) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostHTTPLoadBalancer) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "httpLoadBalancer"
}
fullPath := path.Join("httpLoadBalancer", 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{}
result.Title = a.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
}
+148
View File
@@ -0,0 +1,148 @@
package analyzer
import (
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type AnalyzeHostIPV4Interfaces struct {
hostAnalyzer *troubleshootv1beta2.IPV4InterfacesAnalyze
}
func (a *AnalyzeHostIPV4Interfaces) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "IPv4 Interfaces")
}
func (a *AnalyzeHostIPV4Interfaces) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostIPV4Interfaces) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents("system/ipv4Interfaces.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
var ipv4Interfaces []net.Interface
if err := json.Unmarshal(contents, &ipv4Interfaces); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal ipv4Interfaces")
}
result := AnalyzeResult{}
result.Title = a.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 := compareHostIPV4InterfacesConditionalToActual(outcome.Fail.When, ipv4Interfaces)
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 := compareHostIPV4InterfacesConditionalToActual(outcome.Warn.When, ipv4Interfaces)
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 := compareHostIPV4InterfacesConditionalToActual(outcome.Pass.When, ipv4Interfaces)
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 compareHostIPV4InterfacesConditionalToActual(conditional string, ipv4Interfaces []net.Interface) (res bool, err error) {
parts := strings.Split(conditional, " ")
if len(parts) != 3 {
return false, fmt.Errorf("Expected exactly 3 parts in conditional, got %d", len(parts))
}
keyword := parts[0]
operator := parts[1]
desired := parts[2]
if keyword != "count" {
return false, fmt.Errorf(`Only supported keyword is "count", got %q`, keyword)
}
desiredInt, err := strconv.ParseInt(desired, 10, 64)
if err != nil {
return false, errors.Wrapf(err, "failed to parse %q as int", desired)
}
actualCount := len(ipv4Interfaces)
switch operator {
case "<":
return actualCount < int(desiredInt), nil
case "<=":
return actualCount <= int(desiredInt), nil
case ">":
return actualCount > int(desiredInt), nil
case ">=":
return actualCount >= int(desiredInt), nil
case "=", "==", "===":
return actualCount == int(desiredInt), nil
}
return false, fmt.Errorf("Unknown operator %q. Supported operators are: <, <=, ==, >=, >", operator)
}
+101
View File
@@ -0,0 +1,101 @@
package analyzer
import (
"encoding/json"
"net"
"testing"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyzeIPV4Interfaces(t *testing.T) {
tests := []struct {
name string
interfaces []net.Interface
hostAnalyzer *troubleshootv1beta2.IPV4InterfacesAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "fail when no ipv4 interfaces detected",
interfaces: nil,
hostAnalyzer: &troubleshootv1beta2.IPV4InterfacesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "count > 0",
Message: "IPv4 interface available",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "count == 0",
Message: "No IPv4 interfaces detected",
},
},
},
},
result: &AnalyzeResult{
Title: "IPv4 Interfaces",
IsFail: true,
Message: "No IPv4 interfaces detected",
},
},
{
name: "pass when ipv4 interfaces detected",
interfaces: []net.Interface{
{
Index: 1,
MTU: 1460,
HardwareAddr: net.HardwareAddr("42010a80001d"),
Name: "ens4",
},
},
hostAnalyzer: &troubleshootv1beta2.IPV4InterfacesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "count == 0",
Message: "No IPv4 interfaces detected",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "count > 0",
Message: "IPv4 interface available",
},
},
},
},
result: &AnalyzeResult{
Title: "IPv4 Interfaces",
IsPass: true,
Message: "IPv4 interface available",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.interfaces)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostIPV4Interfaces{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+16 -6
View File
@@ -11,7 +11,21 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
)
func analyzeHostMemory(hostAnalyzer *troubleshootv1beta2.MemoryAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
type AnalyzeHostMemory struct {
hostAnalyzer *troubleshootv1beta2.MemoryAnalyze
}
func (a *AnalyzeHostMemory) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Amount of Memory")
}
func (a *AnalyzeHostMemory) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostMemory) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents("system/memory.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
@@ -24,11 +38,7 @@ func analyzeHostMemory(hostAnalyzer *troubleshootv1beta2.MemoryAnalyze, getColle
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "Amount of Memory"
}
result.Title = title
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
+1 -1
View File
@@ -164,7 +164,7 @@ func TestAnalyzeHostMemory(t *testing.T) {
return b, nil
}
result, err := analyzeHostMemory(test.hostAnalyzer, getCollectedFileContents)
result, err := (&AnalyzeHostMemory{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
+183
View File
@@ -0,0 +1,183 @@
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"
)
type AnalyzeHostServices struct {
hostAnalyzer *troubleshootv1beta2.HostServicesAnalyze
}
func (a *AnalyzeHostServices) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Host Services")
}
func (a *AnalyzeHostServices) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostServices) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents(collect.HostServicesPath)
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
var services []collect.ServiceInfo
if err := json.Unmarshal(contents, &services); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal systemctl service info")
}
result := AnalyzeResult{}
result.Title = a.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 := compareHostServicesConditionalToActual(outcome.Fail.When, services)
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 := compareHostServicesConditionalToActual(outcome.Warn.When, services)
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 := compareHostServicesConditionalToActual(outcome.Pass.When, services)
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
}
// <service> <op> <state>
// example: ufw.service = active
func compareHostServicesConditionalToActual(conditional string, services []collect.ServiceInfo) (res bool, err error) {
parts := strings.Split(conditional, " ")
if len(parts) != 3 {
return false, fmt.Errorf("expected exactly 3 parts, got %d", len(parts))
}
matchParams := strings.Split(parts[2], ",")
activeMatch := matchParams[0]
subMatch := ""
loadMatch := ""
if len(matchParams) > 1 {
subMatch = matchParams[1]
}
if len(matchParams) > 2 {
loadMatch = matchParams[2]
}
switch parts[1] {
case "=", "==":
for _, service := range services {
if isServiceMatch(service.Unit, parts[0]) {
isMatch := true
if activeMatch != "" && activeMatch != "*" {
isMatch = isMatch && (activeMatch == service.Active)
}
if subMatch != "" && subMatch != "*" {
isMatch = isMatch && (subMatch == service.Sub)
}
if loadMatch != "" && loadMatch != "*" {
isMatch = isMatch && (loadMatch == service.Load)
}
return isMatch, nil
}
}
return false, nil
case "!=", "<>":
for _, service := range services {
if isServiceMatch(service.Unit, parts[0]) {
isMatch := false
if activeMatch != "" && activeMatch != "*" {
isMatch = isMatch || (activeMatch != service.Active)
}
if subMatch != "" && subMatch != "*" {
isMatch = isMatch || (subMatch != service.Sub)
}
if loadMatch != "" && loadMatch != "*" {
isMatch = isMatch || (loadMatch != service.Load)
}
return isMatch, nil
}
}
return false, nil
}
return false, fmt.Errorf("unexpected operator %q", parts[1])
}
func isServiceMatch(serviceName string, matchName string) bool {
if serviceName == matchName {
return true
}
if strings.HasPrefix(serviceName, matchName) {
return true
}
return false
}
+212
View File
@@ -0,0 +1,212 @@
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 TestAnalyzeHostServices(t *testing.T) {
tests := []struct {
name string
info []collect.ServiceInfo
hostAnalyzer *troubleshootv1beta2.HostServicesAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "service 'a' is active",
info: []collect.ServiceInfo{
{
Unit: "a.service",
Active: "active",
},
},
hostAnalyzer: &troubleshootv1beta2.HostServicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "a.service == active",
Message: "the service 'a' is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Host Services",
IsFail: true,
Message: "the service 'a' is active",
},
},
{
name: "connected, fail",
info: []collect.ServiceInfo{
{
Unit: "a.service",
Active: "active",
},
{
Unit: "b.service",
Active: "inactive",
},
},
hostAnalyzer: &troubleshootv1beta2.HostServicesAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "a.service != active",
Message: "service 'a' is active",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "b.service != active",
Message: "service 'b' is not active",
},
},
},
},
result: &AnalyzeResult{
Title: "Host Services",
IsPass: true,
Message: "service 'b' is not active",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.info)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostServices{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
func Test_compareHostServicesConditionalToActual(t *testing.T) {
tests := []struct {
name string
conditional string
services []collect.ServiceInfo
wantRes bool
wantErr bool
}{
{
name: "match second item",
conditional: "abc.service = active",
services: []collect.ServiceInfo{
{
Unit: "first",
},
{
Unit: "abc.service",
Active: "active",
Sub: "running",
},
},
wantRes: true,
},
{
name: "item not in list",
conditional: "abc = active",
services: []collect.ServiceInfo{
{
Unit: "first",
},
},
wantRes: false,
},
{
name: "item does not match",
conditional: "abc = active",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "inactive",
Sub: "exited",
},
},
wantRes: false,
},
{
name: "other operator",
conditional: "abc * active",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "inactive",
Sub: "exited",
},
},
wantErr: true,
},
{
name: "item active matches but not sub",
conditional: "abc = active,running",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "active",
Sub: "exited",
},
},
wantRes: false,
},
{
name: "item active,sub,load matches",
conditional: "abc = active,*,loaded",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "active",
Sub: "exited",
Load: "loaded",
},
},
wantRes: true,
},
{
name: "one item active,sub,load does not match with !=",
conditional: "abc != active,running,loaded",
services: []collect.ServiceInfo{
{
Unit: "abc.service",
Active: "active",
Sub: "exited",
Load: "loaded",
},
},
wantRes: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := require.New(t)
gotRes, err := compareHostServicesConditionalToActual(tt.conditional, tt.services)
if tt.wantErr {
req.Error(err)
} else {
req.NoError(err)
req.Equal(tt.wantRes, gotRes)
}
})
}
}
+96
View File
@@ -0,0 +1,96 @@
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"
)
type AnalyzeHostTCPConnect struct {
hostAnalyzer *troubleshootv1beta2.TCPConnectAnalyze
}
func (a *AnalyzeHostTCPConnect) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "TCP Connection Attempt")
}
func (a *AnalyzeHostTCPConnect) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostTCPConnect) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
fullPath := path.Join("connect", 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{}
result.Title = a.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
}
+92
View File
@@ -0,0 +1,92 @@
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 TestAnalyzeTCPConnect(t *testing.T) {
tests := []struct {
name string
info *collect.NetworkStatusResult
hostAnalyzer *troubleshootv1beta2.TCPConnectAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "connection refused, fail",
info: &collect.NetworkStatusResult{
Status: collect.NetworkStatusConnectionRefused,
},
hostAnalyzer: &troubleshootv1beta2.TCPConnectAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "connection-refused",
Message: "Connection was refused",
},
},
},
},
result: &AnalyzeResult{
Title: "TCP Connection Attempt",
IsFail: true,
Message: "Connection was refused",
},
},
{
name: "connected, fail",
info: &collect.NetworkStatusResult{
Status: collect.NetworkStatusConnected,
},
hostAnalyzer: &troubleshootv1beta2.TCPConnectAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "connection-refused",
Message: "Connection was refused",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "connected",
Message: "Connection was successful",
},
},
},
},
result: &AnalyzeResult{
Title: "TCP Connection Attempt",
IsPass: true,
Message: "Connection was successful",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.info)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostTCPConnect{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+16 -6
View File
@@ -10,7 +10,21 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
func analyzeHostTCPLoadBalancer(hostAnalyzer *troubleshootv1beta2.TCPLoadBalancerAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
type AnalyzeHostTCPLoadBalancer struct {
hostAnalyzer *troubleshootv1beta2.TCPLoadBalancerAnalyze
}
func (a *AnalyzeHostTCPLoadBalancer) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "TCP Load Balancer")
}
func (a *AnalyzeHostTCPLoadBalancer) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostTCPLoadBalancer) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
collectorName := hostAnalyzer.CollectorName
if collectorName == "" {
collectorName = "tcpLoadBalancer"
@@ -29,11 +43,7 @@ func analyzeHostTCPLoadBalancer(hostAnalyzer *troubleshootv1beta2.TCPLoadBalance
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "TCP Load Balancer"
}
result.Title = title
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
+16 -6
View File
@@ -10,7 +10,21 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
func analyzeHostTCPPortStatus(hostAnalyzer *troubleshootv1beta2.TCPPortStatusAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
type AnalyzeHostTCPPortStatus struct {
hostAnalyzer *troubleshootv1beta2.TCPPortStatusAnalyze
}
func (a *AnalyzeHostTCPPortStatus) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "TCP Port Status")
}
func (a *AnalyzeHostTCPPortStatus) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostTCPPortStatus) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
fullPath := path.Join("tcpPortStatus", "tcpPortStatus.json")
if hostAnalyzer.CollectorName != "" {
fullPath = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", hostAnalyzer.CollectorName))
@@ -27,11 +41,7 @@ func analyzeHostTCPPortStatus(hostAnalyzer *troubleshootv1beta2.TCPPortStatusAna
result := AnalyzeResult{}
title := hostAnalyzer.CheckName
if title == "" {
title = "TCP Port Status"
}
result.Title = title
result.Title = a.Title()
for _, outcome := range hostAnalyzer.Outcomes {
if outcome.Fail != nil {
+154
View File
@@ -0,0 +1,154 @@
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"
)
const (
SynchronizedActive = "synchronized+active"
SynchronizedInactive = "synchronized+inactive"
UnsynchronizedActive = "unsynchronized+active"
UnsynchronizedInactive = "unsynchronized+inactive"
)
type AnalyzeHostTime struct {
hostAnalyzer *troubleshootv1beta2.TimeAnalyze
}
func (a *AnalyzeHostTime) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "Time")
}
func (a *AnalyzeHostTime) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostTime) Analyze(getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
contents, err := getCollectedFileContents("system/time.json")
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
timeInfo := collect.TimeInfo{}
if err := json.Unmarshal(contents, &timeInfo); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal time info")
}
result := AnalyzeResult{}
result.Title = a.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 := compareHostTimeStatusToActual(outcome.Fail.When, timeInfo)
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 := compareHostTimeStatusToActual(outcome.Warn.When, timeInfo)
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 := compareHostTimeStatusToActual(outcome.Pass.When, timeInfo)
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 compareHostTimeStatusToActual(status string, timeInfo collect.TimeInfo) (res bool, err error) {
parts := strings.Split(status, " ")
if len(parts) != 3 {
return false, fmt.Errorf("Expected exactly 3 parts, got %d", len(parts))
}
if parts[0] == "timezone" {
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" && parts[1] != "!=" {
return false, errors.New(`Only supported operators are "==" and "!="`)
}
if parts[1] == "!=" {
return parts[2] != timeInfo.Timezone, nil
}
return parts[2] == timeInfo.Timezone, nil
}
if parts[0] == "ntp" {
if parts[1] != "=" && parts[1] != "==" && parts[1] != "===" {
return false, errors.New(`Only supported operator is "=="`)
}
switch parts[2] {
case SynchronizedActive:
return timeInfo.NTPSynchronized && timeInfo.NTPActive, nil
case SynchronizedInactive:
return timeInfo.NTPSynchronized && !timeInfo.NTPActive, nil
case UnsynchronizedActive:
return !timeInfo.NTPSynchronized && timeInfo.NTPActive, nil
case UnsynchronizedInactive:
return !timeInfo.NTPSynchronized && !timeInfo.NTPActive, nil
default:
return false, fmt.Errorf("Unknown status %q. Allowed values are %q, %q, %q, or %q", parts[2], SynchronizedActive, SynchronizedInactive, UnsynchronizedActive, UnsynchronizedInactive)
}
}
return false, fmt.Errorf("Unknown keyword: %s", parts[0])
}
+232
View File
@@ -0,0 +1,232 @@
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 TestAnalyzeHostTime(t *testing.T) {
tests := []struct {
name string
timeInfo *collect.TimeInfo
hostAnalyzer *troubleshootv1beta2.TimeAnalyze
result *AnalyzeResult
expectErr bool
}{
{
name: "ntp == synchronized+active",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: true,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsPass: true,
Message: "System clock synchronized and NTP is active",
},
},
{
name: "ntp == unsynchronized+inactive",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: false,
NTPActive: false,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsFail: true,
Message: "System clock not synchronized",
},
},
{
name: "ntp == unsynchronized+active",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: false,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+active",
Message: "System clock not yet synchronized",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsWarn: true,
Message: "System clock not yet synchronized",
},
},
{
name: "ntp == synchronized+inactive",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: true,
NTPActive: false,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+inactive",
Message: "System clock not synchronized",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "ntp == unsynchronized+active",
Message: "System clock not yet synchronized",
},
},
{
Warn: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+inactive",
Message: "System clock synchronized for now",
},
},
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "ntp == synchronized+active",
Message: "System clock synchronized and NTP is active",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsWarn: true,
Message: "System clock synchronized for now",
},
},
{
name: "timezone",
timeInfo: &collect.TimeInfo{
Timezone: "UTC",
NTPSynchronized: true,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
When: "timezone == UTC",
Message: "Timezone is set to UTC",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "timezone not set to UTC",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsPass: true,
Message: "Timezone is set to UTC",
},
},
{
name: "timezone is not UTC",
timeInfo: &collect.TimeInfo{
Timezone: "PST",
NTPSynchronized: true,
NTPActive: true,
},
hostAnalyzer: &troubleshootv1beta2.TimeAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Fail: &troubleshootv1beta2.SingleOutcome{
When: "timezone != UTC",
Message: "Timezone is not set to UTC",
},
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "Timezone is set to UTC",
},
},
},
},
result: &AnalyzeResult{
Title: "Time",
IsFail: true,
Message: "Timezone is not set to UTC",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req := require.New(t)
b, err := json.Marshal(test.timeInfo)
if err != nil {
t.Fatal(err)
}
getCollectedFileContents := func(filename string) ([]byte, error) {
return b, nil
}
result, err := (&AnalyzeHostTime{test.hostAnalyzer}).Analyze(getCollectedFileContents)
if test.expectErr {
req.Error(err)
} else {
req.NoError(err)
}
assert.Equal(t, test.result, result)
})
}
}
+1 -1
View File
@@ -68,7 +68,7 @@ func analyzePostgres(analyzer *troubleshootv1beta2.DatabaseAnalyze, getCollected
return result, nil
}
} else if outcome.Warn != nil {
if outcome.Pass.When == "" {
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
+1 -1
View File
@@ -62,7 +62,7 @@ func analyzeRedis(analyzer *troubleshootv1beta2.DatabaseAnalyze, getCollectedFil
return result, nil
}
} else if outcome.Warn != nil {
if outcome.Pass.When == "" {
if outcome.Warn.When == "" {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI
+183
View File
@@ -0,0 +1,183 @@
package analyzer
import (
"encoding/json"
"fmt"
"path"
"strconv"
"strings"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
func analyzeRegistry(analyzer *troubleshootv1beta2.RegistryImagesAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
collectorName := analyzer.CollectorName
if collectorName == "" {
collectorName = "images"
}
fullPath := path.Join("registry", 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)
}
registryInfo := collect.RegistryInfo{}
if err := json.Unmarshal(collected, &registryInfo); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal database connection result")
}
numMissingImages := 0
numVerifiedImages := 0
numErrors := 0
for _, image := range registryInfo.Images {
if image.Error != "" {
numErrors++
} else if !image.Exists {
numMissingImages++
} else {
numVerifiedImages++
}
}
title := analyzer.CheckName
if title == "" {
title = collectorName
}
result := &AnalyzeResult{
Title: title,
IconKey: "kubernetes_registry_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/registry-analyze.svg",
}
for _, outcome := range analyzer.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 := compareRegistryConditionalToActual(outcome.Fail.When, numVerifiedImages, numMissingImages, numErrors)
if err != nil {
return result, errors.Wrap(err, "failed to compare registry conditional")
}
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 := compareRegistryConditionalToActual(outcome.Warn.When, numVerifiedImages, numMissingImages, numErrors)
if err != nil {
return result, errors.Wrap(err, "failed to compare registry conditional")
}
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 := compareRegistryConditionalToActual(outcome.Pass.When, numVerifiedImages, numMissingImages, numErrors)
if err != nil {
return result, errors.Wrap(err, "failed to compare registry conditional")
}
if isMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI
return result, nil
}
}
}
return result, nil
}
func compareRegistryConditionalToActual(conditional string, numVerifiedImages int, numMissingImages int, numErrors int) (bool, error) {
parts := strings.Split(strings.TrimSpace(conditional), " ")
if len(parts) != 3 {
return false, errors.Errorf("unable to parse conditional: %s", conditional)
}
switch parts[0] {
case "verified":
result, err := doCompareRegistryImageCount(parts[1], parts[2], numVerifiedImages)
if err != nil {
return false, errors.Wrap(err, "failed to compare number of verified images")
}
return result, nil
case "missing":
result, err := doCompareRegistryImageCount(parts[1], parts[2], numMissingImages)
if err != nil {
return false, errors.Wrap(err, "failed to compare number of missing images")
}
return result, nil
case "errors":
result, err := doCompareRegistryImageCount(parts[1], parts[2], numErrors)
if err != nil {
return false, errors.Wrap(err, "failed to compare number of errors")
}
return result, nil
}
return false, errors.Errorf("unknown term %q in conditional", parts[0])
}
func doCompareRegistryImageCount(operator string, desired string, actual int) (bool, error) {
desiredInt, err := strconv.Atoi(desired)
if err != nil {
return false, errors.Wrap(err, "failed to parse")
}
switch operator {
case "<":
return actual < desiredInt, nil
case "<=":
return actual <= desiredInt, nil
case ">":
return actual > desiredInt, nil
case ">=":
return actual >= desiredInt, nil
case "=", "==", "===":
return actual == desiredInt, nil
}
return false, errors.Errorf("unknown operator: %s", operator)
}
+68
View File
@@ -303,6 +303,74 @@ func Test_textAnalyze(t *testing.T) {
"text-collector-2/cfile-3.txt": []byte("Yes it all succeeded"),
},
},
{
name: "case insensitive failure case 1", // regexes are not case insensitive by default
analyzer: troubleshootv1beta2.TextAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "text-collector-1",
FileName: "cfile-1.txt",
RegexPattern: "succeeded",
},
expectResult: []AnalyzeResult{
{
IsPass: false,
IsWarn: false,
IsFail: true,
Title: "text-collector-1",
Message: "fail",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
},
files: map[string][]byte{
"text-collector-1/cfile-1.txt": []byte("Yes it all SUCCEEDED"),
},
},
{
name: "case insensitive success case 1",
analyzer: troubleshootv1beta2.TextAnalyze{
Outcomes: []*troubleshootv1beta2.Outcome{
{
Pass: &troubleshootv1beta2.SingleOutcome{
Message: "pass",
},
},
{
Fail: &troubleshootv1beta2.SingleOutcome{
Message: "fail",
},
},
},
CollectorName: "text-collector-1",
FileName: "cfile-1.txt",
RegexPattern: "(?i)succeeded",
},
expectResult: []AnalyzeResult{
{
IsPass: true,
IsWarn: false,
IsFail: false,
Title: "text-collector-1",
Message: "pass",
IconKey: "kubernetes_text_analyze",
IconURI: "https://troubleshoot.sh/images/analyzer-icons/text-analyze.svg",
},
},
files: map[string][]byte{
"text-collector-1/cfile-1.txt": []byte("Yes it all SUCCEEDED"),
},
},
}
for _, test := range tests {
@@ -148,7 +148,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Namespace",
Resource: "namespaces",
Subresource: "",
Name: "",
},
@@ -160,7 +160,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Node",
Resource: "nodes",
Subresource: "",
Name: "",
},
@@ -172,7 +172,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "apiextensions.k8s.io",
Version: "",
Resource: "CustomResourceDefinition",
Resource: "customresourcedefinitions",
Subresource: "",
Name: "",
},
@@ -184,7 +184,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "storage.k8s.io",
Version: "",
Resource: "StorageClasses",
Resource: "storageclasses",
Subresource: "",
Name: "",
},
@@ -197,7 +197,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Secret",
Resource: "secrets",
Subresource: "",
Name: c.Secret.SecretName,
},
@@ -210,7 +210,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -222,7 +222,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "log",
Name: "",
},
@@ -235,7 +235,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "create",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -248,7 +248,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -260,7 +260,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "exec",
Name: "",
},
@@ -273,7 +273,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -285,7 +285,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "exec",
Name: "",
},
@@ -116,6 +116,12 @@ type CephStatusAnalyze struct {
Namespace string `json:"namespace" yaml:"namespace"`
}
type RegistryImagesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
CollectorName string `json:"collectorName" yaml:"collectorName"`
}
type AnalyzeMeta struct {
CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"`
Exclude multitype.BoolOrString `json:"exclude,omitempty" yaml:"exclude,omitempty"`
@@ -138,4 +144,5 @@ type Analyze struct {
Mysql *DatabaseAnalyze `json:"mysql,omitempty" yaml:"mysql,omitempty"`
Redis *DatabaseAnalyze `json:"redis,omitempty" yaml:"redis,omitempty"`
CephStatus *CephStatusAnalyze `json:"cephStatus,omitempty" yaml:"cephStatus,omitempty"`
RegistryImages *RegistryImagesAnalyze `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
}
@@ -139,6 +139,13 @@ type Ceph struct {
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
}
type RegistryImages struct {
CollectorMeta `json:",inline" yaml:",inline"`
Images []string `json:"images" yaml:"images"`
Namespace string `json:"namespace" yaml:"namespace"`
ImagePullSecrets *ImagePullSecrets `json:"imagePullSecret,omitempty" yaml:"imagePullSecret,omitempty"`
}
type Collect struct {
ClusterInfo *ClusterInfo `json:"clusterInfo,omitempty" yaml:"clusterInfo,omitempty"`
ClusterResources *ClusterResources `json:"clusterResources,omitempty" yaml:"clusterResources,omitempty"`
@@ -154,6 +161,7 @@ type Collect struct {
Redis *Database `json:"redis,omitempty" yaml:"redis,omitempty"`
Collectd *Collectd `json:"collectd,omitempty" yaml:"collectd,omitempty"`
Ceph *Ceph `json:"ceph,omitempty" yaml:"ceph,omitempty"`
RegistryImages *RegistryImages `json:"registryImages,omitempty" yaml:"registryImages,omitempty"`
}
func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSubjectAccessReviewSpec {
@@ -168,7 +176,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Namespace",
Resource: "namespaces",
Subresource: "",
Name: "",
},
@@ -180,7 +188,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Node",
Resource: "nodes",
Subresource: "",
Name: "",
},
@@ -192,7 +200,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "apiextensions.k8s.io",
Version: "",
Resource: "CustomResourceDefinition",
Resource: "customresourcedefinitions",
Subresource: "",
Name: "",
},
@@ -204,7 +212,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "storage.k8s.io",
Version: "",
Resource: "StorageClasses",
Resource: "storageclasses",
Subresource: "",
Name: "",
},
@@ -217,7 +225,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Secret",
Resource: "secrets",
Subresource: "",
Name: c.Secret.SecretName,
},
@@ -230,7 +238,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -242,7 +250,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "log",
Name: "",
},
@@ -255,7 +263,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "create",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -268,7 +276,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -280,7 +288,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "exec",
Name: "",
},
@@ -293,7 +301,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "list",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "",
Name: "",
},
@@ -305,7 +313,7 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
Verb: "get",
Group: "",
Version: "",
Resource: "Pod",
Resource: "pods",
Subresource: "exec",
Name: "",
},
@@ -313,6 +321,21 @@ func (c *Collect) AccessReviewSpecs(overrideNS string) []authorizationv1.SelfSub
})
} else if c.HTTP != nil {
// NOOP
} else if c.RegistryImages != nil &&
c.RegistryImages.ImagePullSecrets != nil &&
c.RegistryImages.ImagePullSecrets.Data == nil {
result = append(result, authorizationv1.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authorizationv1.ResourceAttributes{
Namespace: pickNamespaceOrDefault(c.RegistryImages.Namespace, overrideNS),
Verb: "get",
Group: "",
Version: "",
Resource: "secrets",
Subresource: "",
Name: c.RegistryImages.ImagePullSecrets.Name,
},
NonResourceAttributes: nil,
})
}
return result
@@ -361,6 +384,10 @@ func (c *Collect) GetName() string {
collector = "ceph"
name = c.Ceph.CollectorName
}
if c.RegistryImages != nil {
collector = "registry-images"
name = c.RegistryImages.CollectorName
}
if collector == "" {
return "<none>"
@@ -16,6 +16,12 @@ type TCPLoadBalancerAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HTTPLoadBalancerAnalyze 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"`
@@ -28,14 +34,77 @@ type DiskUsageAnalyze struct {
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HTTPAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type TimeAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type BlockDevicesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
MinimumAcceptableSize uint64 `json:"minimumAcceptableSize" yaml:"minimumAcceptableSize"`
IncludeUnmountedPartitions bool `json:"includeUnmountedPartitions" yaml:"includeUnmountedPartitions"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type TCPConnectAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type IPV4InterfacesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type FilesystemPerformanceAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type CertificateAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
CollectorName string `json:"collectorName,omitempty" yaml:"collectorName,omitempty"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
}
type HostServicesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
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"`
TCPLoadBalancer *TCPLoadBalancerAnalyze `json:"tcpLoadBalancer,omitempty" yaml:"tcpLoadBalancer,omitempty"`
HTTPLoadBalancer *HTTPLoadBalancerAnalyze `json:"httpLoadBalancer,omitempty" yaml:"httpLoadBalancer,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"`
HTTP *HTTPAnalyze `json:"http,omitempty" yaml:"http,omitempty"`
Time *TimeAnalyze `json:"time,omitempty" yaml:"time,omitempty"`
BlockDevices *BlockDevicesAnalyze `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
TCPConnect *TCPConnectAnalyze `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
IPV4Interfaces *IPV4InterfacesAnalyze `json:"ipv4Interfaces,omitempty" yaml:"ipv4Interfaces,omitempty"`
FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"`
HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
}
@@ -52,15 +52,62 @@ type DiskUsage struct {
Path string `json:"path"`
}
type HostHTTP struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
Get *Get `json:"get,omitempty" yaml:"get,omitempty"`
Post *Post `json:"post,omitempty" yaml:"post,omitempty"`
Put *Put `json:"put,omitempty" yaml:"put,omitempty"`
}
type HostTime struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type HostBlockDevices struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
type TCPConnect struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
Address string `json:"address"`
Timeout string `json:"timeout,omitempty"`
}
type FilesystemPerformance struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
OperationSizeBytes uint64 `json:"operationSize,omitempty"`
Directory string `json:"directory,omitempty"`
FileSize string `json:"fileSize,omitempty"`
Sync bool `json:"sync,omitempty"`
Datasync bool `json:"datasync,omitempty"`
}
type Certificate struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
CertificatePath string `json:"certificatePath" yaml:"certificatepath"`
KeyPath string `json:"keyPath" yaml:"keyPath"`
}
type HostServices struct {
HostCollectorMeta `json:",inline" yaml:",inline"`
}
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"`
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"`
HTTP *HostHTTP `json:"http,omitempty" yaml:"http,omitempty"`
Time *HostTime `json:"time,omitempty" yaml:"time,omitempty"`
BlockDevices *HostBlockDevices `json:"blockDevices,omitempty" yaml:"blockDevices,omitempty"`
TCPConnect *TCPConnect `json:"tcpConnect,omitempty" yaml:"tcpConnect,omitempty"`
FilesystemPerformance *FilesystemPerformance `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *Certificate `json:"certificate,omitempty" yaml:"certificate,omitempty"`
HostServices *HostServices `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
}
func (c *HostCollect) GetName() string {
@@ -132,6 +132,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
*out = new(CephStatusAnalyze)
(*in).DeepCopyInto(*out)
}
if in.RegistryImages != nil {
in, out := &in.RegistryImages, &out.RegistryImages
*out = new(RegistryImagesAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
@@ -287,6 +292,33 @@ 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 *BlockDevicesAnalyze) DeepCopyInto(out *BlockDevicesAnalyze) {
*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 BlockDevicesAnalyze.
func (in *BlockDevicesAnalyze) DeepCopy() *BlockDevicesAnalyze {
if in == nil {
return nil
}
out := new(BlockDevicesAnalyze)
in.DeepCopyInto(out)
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
@@ -373,6 +405,49 @@ func (in *CephStatusAnalyze) DeepCopy() *CephStatusAnalyze {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Certificate) DeepCopyInto(out *Certificate) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Certificate.
func (in *Certificate) DeepCopy() *Certificate {
if in == nil {
return nil
}
out := new(Certificate)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CertificateAnalyze) DeepCopyInto(out *CertificateAnalyze) {
*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 CertificateAnalyze.
func (in *CertificateAnalyze) DeepCopy() *CertificateAnalyze {
if in == nil {
return nil
}
out := new(CertificateAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ClusterInfo) DeepCopyInto(out *ClusterInfo) {
*out = *in
@@ -505,6 +580,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
*out = new(Ceph)
**out = **in
}
if in.RegistryImages != nil {
in, out := &in.RegistryImages, &out.RegistryImages
*out = new(RegistryImages)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Collect.
@@ -974,6 +1054,49 @@ func (in *FileSelector) DeepCopy() *FileSelector {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FilesystemPerformance) DeepCopyInto(out *FilesystemPerformance) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FilesystemPerformance.
func (in *FilesystemPerformance) DeepCopy() *FilesystemPerformance {
if in == nil {
return nil
}
out := new(FilesystemPerformance)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FilesystemPerformanceAnalyze) DeepCopyInto(out *FilesystemPerformanceAnalyze) {
*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 FilesystemPerformanceAnalyze.
func (in *FilesystemPerformanceAnalyze) DeepCopy() *FilesystemPerformanceAnalyze {
if in == nil {
return nil
}
out := new(FilesystemPerformanceAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Get) DeepCopyInto(out *Get) {
*out = *in
@@ -1027,6 +1150,33 @@ 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 *HTTPAnalyze) DeepCopyInto(out *HTTPAnalyze) {
*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 HTTPAnalyze.
func (in *HTTPAnalyze) DeepCopy() *HTTPAnalyze {
if in == nil {
return nil
}
out := new(HTTPAnalyze)
in.DeepCopyInto(out)
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
@@ -1043,6 +1193,33 @@ func (in *HTTPLoadBalancer) DeepCopy() *HTTPLoadBalancer {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HTTPLoadBalancerAnalyze) DeepCopyInto(out *HTTPLoadBalancerAnalyze) {
*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 HTTPLoadBalancerAnalyze.
func (in *HTTPLoadBalancerAnalyze) DeepCopy() *HTTPLoadBalancerAnalyze {
if in == nil {
return nil
}
out := new(HTTPLoadBalancerAnalyze)
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
@@ -1056,6 +1233,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(TCPLoadBalancerAnalyze)
(*in).DeepCopyInto(*out)
}
if in.HTTPLoadBalancer != nil {
in, out := &in.HTTPLoadBalancer, &out.HTTPLoadBalancer
*out = new(HTTPLoadBalancerAnalyze)
(*in).DeepCopyInto(*out)
}
if in.DiskUsage != nil {
in, out := &in.DiskUsage, &out.DiskUsage
*out = new(DiskUsageAnalyze)
@@ -1071,6 +1253,46 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*out = new(TCPPortStatusAnalyze)
(*in).DeepCopyInto(*out)
}
if in.HTTP != nil {
in, out := &in.HTTP, &out.HTTP
*out = new(HTTPAnalyze)
(*in).DeepCopyInto(*out)
}
if in.Time != nil {
in, out := &in.Time, &out.Time
*out = new(TimeAnalyze)
(*in).DeepCopyInto(*out)
}
if in.BlockDevices != nil {
in, out := &in.BlockDevices, &out.BlockDevices
*out = new(BlockDevicesAnalyze)
(*in).DeepCopyInto(*out)
}
if in.TCPConnect != nil {
in, out := &in.TCPConnect, &out.TCPConnect
*out = new(TCPConnectAnalyze)
(*in).DeepCopyInto(*out)
}
if in.IPV4Interfaces != nil {
in, out := &in.IPV4Interfaces, &out.IPV4Interfaces
*out = new(IPV4InterfacesAnalyze)
(*in).DeepCopyInto(*out)
}
if in.FilesystemPerformance != nil {
in, out := &in.FilesystemPerformance, &out.FilesystemPerformance
*out = new(FilesystemPerformanceAnalyze)
(*in).DeepCopyInto(*out)
}
if in.Certificate != nil {
in, out := &in.Certificate, &out.Certificate
*out = new(CertificateAnalyze)
(*in).DeepCopyInto(*out)
}
if in.HostServices != nil {
in, out := &in.HostServices, &out.HostServices
*out = new(HostServicesAnalyze)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze.
@@ -1083,6 +1305,22 @@ func (in *HostAnalyze) DeepCopy() *HostAnalyze {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostBlockDevices) DeepCopyInto(out *HostBlockDevices) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostBlockDevices.
func (in *HostBlockDevices) DeepCopy() *HostBlockDevices {
if in == nil {
return nil
}
out := new(HostBlockDevices)
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
@@ -1126,6 +1364,41 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*out = new(DiskUsage)
**out = **in
}
if in.HTTP != nil {
in, out := &in.HTTP, &out.HTTP
*out = new(HostHTTP)
(*in).DeepCopyInto(*out)
}
if in.Time != nil {
in, out := &in.Time, &out.Time
*out = new(HostTime)
**out = **in
}
if in.BlockDevices != nil {
in, out := &in.BlockDevices, &out.BlockDevices
*out = new(HostBlockDevices)
**out = **in
}
if in.TCPConnect != nil {
in, out := &in.TCPConnect, &out.TCPConnect
*out = new(TCPConnect)
**out = **in
}
if in.FilesystemPerformance != nil {
in, out := &in.FilesystemPerformance, &out.FilesystemPerformance
*out = new(FilesystemPerformance)
**out = **in
}
if in.Certificate != nil {
in, out := &in.Certificate, &out.Certificate
*out = new(Certificate)
**out = **in
}
if in.HostServices != nil {
in, out := &in.HostServices, &out.HostServices
*out = new(HostServices)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect.
@@ -1154,6 +1427,37 @@ func (in *HostCollectorMeta) DeepCopy() *HostCollectorMeta {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostHTTP) DeepCopyInto(out *HostHTTP) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
if in.Get != nil {
in, out := &in.Get, &out.Get
*out = new(Get)
(*in).DeepCopyInto(*out)
}
if in.Post != nil {
in, out := &in.Post, &out.Post
*out = new(Post)
(*in).DeepCopyInto(*out)
}
if in.Put != nil {
in, out := &in.Put, &out.Put
*out = new(Put)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostHTTP.
func (in *HostHTTP) DeepCopy() *HostHTTP {
if in == nil {
return nil
}
out := new(HostHTTP)
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
@@ -1265,6 +1569,65 @@ func (in *HostPreflightStatus) DeepCopy() *HostPreflightStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostServices) DeepCopyInto(out *HostServices) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostServices.
func (in *HostServices) DeepCopy() *HostServices {
if in == nil {
return nil
}
out := new(HostServices)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostServicesAnalyze) DeepCopyInto(out *HostServicesAnalyze) {
*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 HostServicesAnalyze.
func (in *HostServicesAnalyze) DeepCopy() *HostServicesAnalyze {
if in == nil {
return nil
}
out := new(HostServicesAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HostTime) DeepCopyInto(out *HostTime) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostTime.
func (in *HostTime) DeepCopy() *HostTime {
if in == nil {
return nil
}
out := new(HostTime)
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
@@ -1281,6 +1644,33 @@ func (in *IPV4Interfaces) DeepCopy() *IPV4Interfaces {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IPV4InterfacesAnalyze) DeepCopyInto(out *IPV4InterfacesAnalyze) {
*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 IPV4InterfacesAnalyze.
func (in *IPV4InterfacesAnalyze) DeepCopy() *IPV4InterfacesAnalyze {
if in == nil {
return nil
}
out := new(IPV4InterfacesAnalyze)
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
@@ -1854,6 +2244,59 @@ func (in *Regex) DeepCopy() *Regex {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RegistryImages) DeepCopyInto(out *RegistryImages) {
*out = *in
out.CollectorMeta = in.CollectorMeta
if in.Images != nil {
in, out := &in.Images, &out.Images
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.ImagePullSecrets != nil {
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
*out = new(ImagePullSecrets)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryImages.
func (in *RegistryImages) DeepCopy() *RegistryImages {
if in == nil {
return nil
}
out := new(RegistryImages)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RegistryImagesAnalyze) DeepCopyInto(out *RegistryImagesAnalyze) {
*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 RegistryImagesAnalyze.
func (in *RegistryImagesAnalyze) DeepCopy() *RegistryImagesAnalyze {
if in == nil {
return nil
}
out := new(RegistryImagesAnalyze)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Removals) DeepCopyInto(out *Removals) {
*out = *in
@@ -2168,6 +2611,49 @@ 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 *TCPConnect) DeepCopyInto(out *TCPConnect) {
*out = *in
out.HostCollectorMeta = in.HostCollectorMeta
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TCPConnect.
func (in *TCPConnect) DeepCopy() *TCPConnect {
if in == nil {
return nil
}
out := new(TCPConnect)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TCPConnectAnalyze) DeepCopyInto(out *TCPConnectAnalyze) {
*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 TCPConnectAnalyze.
func (in *TCPConnectAnalyze) DeepCopy() *TCPConnectAnalyze {
if in == nil {
return nil
}
out := new(TCPConnectAnalyze)
in.DeepCopyInto(out)
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
@@ -2280,3 +2766,30 @@ func (in *TextAnalyze) DeepCopy() *TextAnalyze {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *TimeAnalyze) DeepCopyInto(out *TimeAnalyze) {
*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 TimeAnalyze.
func (in *TimeAnalyze) DeepCopy() *TimeAnalyze {
if in == nil {
return nil
}
out := new(TimeAnalyze)
in.DeepCopyInto(out)
return out
}
+54
View File
@@ -173,6 +173,24 @@ func ClusterResources(c *Collector) (map[string][]byte, error) {
return nil, err
}
//Persistent Volumes
pvs, pvsErrors := pvs(ctx, client)
clusterResourcesOutput["cluster-resources/pvs.json"] = pvs
clusterResourcesOutput["cluster-resources/pvs-errors.json"], err = marshalNonNil(pvsErrors)
if err != nil {
return nil, err
}
//Persistent Volume Claims
pvcs, pvcsErrors := pvcs(ctx, client, namespaceNames)
for k, v := range pvcs {
clusterResourcesOutput[path.Join("cluster-resources/pvcs", k)] = v
}
clusterResourcesOutput["cluster-resources/pvcs-errors.json"], err = marshalNonNil(pvcsErrors)
if err != nil {
return nil, err
}
return clusterResourcesOutput, nil
}
@@ -532,3 +550,39 @@ func convertToPolicyRule(status authorizationv1.SubjectRulesReviewStatus) []rbac
return ret
}
func pvs(ctx context.Context, client *kubernetes.Clientset) ([]byte, []string) {
pv, err := client.CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, []string{err.Error()}
}
b, err := json.MarshalIndent(pv.Items, "", " ")
if err != nil {
return nil, []string{err.Error()}
}
return b, nil
}
func pvcs(ctx context.Context, client *kubernetes.Clientset, namespaces []string) (map[string][]byte, map[string]string) {
pvcsByNamespace := make(map[string][]byte)
errorsByNamespace := make(map[string]string)
for _, namespace := range namespaces {
pvcs, err := client.CoreV1().PersistentVolumeClaims(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
errorsByNamespace[namespace] = err.Error()
continue
}
b, err := json.MarshalIndent(pvcs.Items, "", " ")
if err != nil {
errorsByNamespace[namespace] = err.Error()
continue
}
pvcsByNamespace[namespace+".json"] = b
}
return pvcsByNamespace, errorsByNamespace
}
+2 -11
View File
@@ -2,7 +2,6 @@ package collect
import (
"context"
"path/filepath"
"strconv"
"github.com/pkg/errors"
@@ -20,7 +19,6 @@ type Collector struct {
RBACErrors []error
ClientConfig *rest.Config
Namespace string
PathPrefix string
}
type Collectors []*Collector
@@ -201,6 +199,8 @@ func (c *Collector) RunCollectorSync(globalRedactors []*troubleshootv1beta2.Reda
result, err = Collectd(c, c.Collect.Collectd)
} else if c.Collect.Ceph != nil {
result, err = Ceph(c, c.Collect.Ceph)
} else if c.Collect.RegistryImages != nil {
result, err = Registry(c, c.Collect.RegistryImages)
} else {
err = errors.New("no spec found to run")
return
@@ -209,15 +209,6 @@ func (c *Collector) RunCollectorSync(globalRedactors []*troubleshootv1beta2.Reda
return
}
if c.PathPrefix != "" {
// prefix file paths
prefixed := map[string][]byte{}
for k, v := range result {
prefixed[filepath.Join(c.PathPrefix, k)] = v
}
result = prefixed
}
if c.Redact {
result, err = redactMap(result, globalRedactors)
}
+89
View File
@@ -0,0 +1,89 @@
package collect
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os/exec"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type BlockDeviceInfo struct {
Name string `json:"name"`
KernelName string `json:"kernel_name"`
ParentKernelName string `json:"parent_kernel_name"`
Type string `json:"type"`
Major int `json:"major"`
Minor int `json:"minor"`
Size uint64 `json:"size"`
FilesystemType string `json:"filesystem_type"`
Mountpoint string `json:"mountpoint"`
Serial string `json:"serial"`
ReadOnly bool `json:"read_only"`
Removable bool `json:"removable"`
}
const lsblkColumns = "NAME,KNAME,PKNAME,TYPE,MAJ:MIN,SIZE,FSTYPE,MOUNTPOINT,SERIAL,RO,RM"
const lsblkFormat = `NAME=%q KNAME=%q PKNAME=%q TYPE=%q MAJ:MIN="%d:%d" SIZE="%d" FSTYPE=%q MOUNTPOINT=%q SERIAL=%q RO="%d" RM="%d0"`
type CollectHostBlockDevices struct {
hostCollector *troubleshootv1beta2.HostBlockDevices
}
func (c *CollectHostBlockDevices) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Block Devices")
}
func (c *CollectHostBlockDevices) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostBlockDevices) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
var devices []BlockDeviceInfo
cmd := exec.Command("lsblk", "--noheadings", "--bytes", "--pairs", "-o", lsblkColumns)
stdout, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(err, "failed to execute lsblk")
}
buf := bytes.NewBuffer(stdout)
scanner := bufio.NewScanner(buf)
for scanner.Scan() {
bdi := BlockDeviceInfo{}
var ro int
var rm int
fmt.Sscanf(
scanner.Text(),
lsblkFormat,
&bdi.Name,
&bdi.KernelName,
&bdi.ParentKernelName,
&bdi.Type,
&bdi.Major,
&bdi.Minor,
&bdi.Size,
&bdi.FilesystemType,
&bdi.Mountpoint,
&bdi.Serial,
&ro,
&rm,
)
bdi.ReadOnly = ro == 1
bdi.Removable = rm == 1
devices = append(devices, bdi)
}
b, err := json.Marshal(devices)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal block device info")
}
return map[string][]byte{
"system/block_devices.json": b,
}, nil
}
+73
View File
@@ -0,0 +1,73 @@
package collect
import (
"bytes"
"crypto/tls"
"io/ioutil"
"path/filepath"
"strings"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
const KeyPairMissing = "key-pair-missing"
const KeyPairSwitched = "key-pair-switched"
const KeyPairEncrypted = "key-pair-encrypted"
const KeyPairMismatch = "key-pair-mismatch"
const KeyPairInvalid = "key-pair-invalid"
const KeyPairValid = "key-pair-valid"
type CollectHostCertificate struct {
hostCollector *troubleshootv1beta2.Certificate
}
func (c *CollectHostCertificate) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Certificate Key Pair")
}
func (c *CollectHostCertificate) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostCertificate) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
var result = KeyPairValid
_, err := tls.LoadX509KeyPair(c.hostCollector.CertificatePath, c.hostCollector.KeyPath)
if err != nil {
if strings.Contains(err.Error(), "no such file") {
result = KeyPairMissing
} else if strings.Contains(err.Error(), "PEM inputs may have been switched") {
result = KeyPairSwitched
} else if strings.Contains(err.Error(), "found a certificate rather than a key") {
result = KeyPairSwitched
} else if strings.Contains(err.Error(), "private key does not match public key") {
result = KeyPairMismatch
} else if strings.Contains(err.Error(), "failed to parse private key") {
if encrypted, _ := isEncryptedKey(c.hostCollector.KeyPath); encrypted {
result = KeyPairEncrypted
} else {
result = KeyPairInvalid
}
} else {
result = KeyPairInvalid
}
}
collectorName := c.hostCollector.CollectorName
if collectorName == "" {
collectorName = "certificate"
}
name := filepath.Join("certificate", collectorName+".json")
return map[string][]byte{
name: []byte(result),
}, nil
}
func isEncryptedKey(filename string) (bool, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return false, err
}
return bytes.Contains(data, []byte("ENCRYPTED")), nil
}
+42 -33
View File
@@ -1,44 +1,53 @@
package collect
import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type HostCollector struct {
Collect *troubleshootv1beta2.HostCollect
type HostCollector interface {
Title() string
IsExcluded() (bool, error)
Collect(progressChan chan<- interface{}) (map[string][]byte, error)
}
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
func GetHostCollector(collector *troubleshootv1beta2.HostCollect) (HostCollector, bool) {
switch {
case collector.CPU != nil:
return &CollectHostCPU{collector.CPU}, true
case collector.Memory != nil:
return &CollectHostMemory{collector.Memory}, true
case collector.TCPLoadBalancer != nil:
return &CollectHostTCPLoadBalancer{collector.TCPLoadBalancer}, true
case collector.HTTPLoadBalancer != nil:
return &CollectHostHTTPLoadBalancer{collector.HTTPLoadBalancer}, true
case collector.DiskUsage != nil:
return &CollectHostDiskUsage{collector.DiskUsage}, true
case collector.TCPPortStatus != nil:
return &CollectHostTCPPortStatus{collector.TCPPortStatus}, true
case collector.HTTP != nil:
return &CollectHostHTTP{collector.HTTP}, true
case collector.Time != nil:
return &CollectHostTime{collector.Time}, true
case collector.BlockDevices != nil:
return &CollectHostBlockDevices{collector.BlockDevices}, true
case collector.TCPConnect != nil:
return &CollectHostTCPConnect{collector.TCPConnect}, true
case collector.IPV4Interfaces != nil:
return &CollectHostIPV4Interfaces{collector.IPV4Interfaces}, true
case collector.FilesystemPerformance != nil:
return &CollectHostFilesystemPerformance{collector.FilesystemPerformance}, true
case collector.Certificate != nil:
return &CollectHostCertificate{collector.Certificate}, true
case collector.HostServices != nil:
return &CollectHostServices{collector.HostServices}, true
default:
return nil, false
}
if err != nil {
return
}
func hostCollectorTitleOrDefault(meta troubleshootv1beta2.HostCollectorMeta, defaultTitle string) string {
if meta.CollectorName != "" {
return meta.CollectorName
}
return
}
func (c *HostCollector) GetDisplayName() string {
return c.Collect.GetName()
return defaultTitle
}
+14 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/shirou/gopsutil/cpu"
)
@@ -12,7 +13,19 @@ type CPUInfo struct {
PhysicalCount int `json:"physicalCount"`
}
func HostCPU(c *HostCollector) (map[string][]byte, error) {
type CollectHostCPU struct {
hostCollector *troubleshootv1beta2.CPU
}
func (c *CollectHostCPU) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "CPU Info")
}
func (c *CollectHostCPU) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostCPU) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
cpuInfo := CPUInfo{}
logicalCount, err := cpu.Counts(true)
+43 -5
View File
@@ -3,8 +3,11 @@ package collect
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/shirou/gopsutil/disk"
)
@@ -13,16 +16,33 @@ type DiskUsageInfo struct {
UsedBytes uint64 `json:"used_bytes"`
}
func HostDiskUsage(c *HostCollector) (map[string][]byte, error) {
type CollectHostDiskUsage struct {
hostCollector *troubleshootv1beta2.DiskUsage
}
func (c *CollectHostDiskUsage) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, fmt.Sprintf("Disk Usage %s", c.hostCollector.CollectorName))
}
func (c *CollectHostDiskUsage) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostDiskUsage) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
result := map[string][]byte{}
if c.Collect.DiskUsage == nil {
if c.hostCollector == nil {
return result, nil
}
du, err := disk.Usage(c.Collect.DiskUsage.Path)
pathExists, err := traverseFiletreeDirExists(c.hostCollector.Path)
if err != nil {
return result, errors.Wrapf(err, "collect disk usage for %s", c.Collect.DiskUsage.Path)
return result, errors.Wrap(err, "traverse file tree")
}
du, err := disk.Usage(pathExists)
if err != nil {
return result, errors.Wrapf(err, "collect disk usage for %s", pathExists)
}
diskSpaceInfo := DiskUsageInfo{
TotalBytes: du.Total,
@@ -32,7 +52,7 @@ func HostDiskUsage(c *HostCollector) (map[string][]byte, error) {
if err != nil {
return nil, errors.Wrap(err, "failed to marshal disk space info")
}
key := HostDiskUsageKey(c.Collect.DiskUsage.CollectorName)
key := HostDiskUsageKey(c.hostCollector.CollectorName)
result[key] = b
return result, nil
@@ -41,3 +61,21 @@ func HostDiskUsage(c *HostCollector) (map[string][]byte, error) {
func HostDiskUsageKey(name string) string {
return fmt.Sprintf("diskUsage/%s.json", name)
}
func traverseFiletreeDirExists(filename string) (string, error) {
filename = filepath.Clean(filename)
for i := 0; i < 50; i++ {
_, err := os.Stat(filename)
if err == nil {
return filename, nil
} else if os.IsNotExist(err) {
filename = filepath.Dir(filename)
if filename == "/" {
return filename, nil
}
} else {
return "", err
}
}
return "", errors.New("max recursion exceeded")
}
@@ -0,0 +1,60 @@
package collect
import (
"math"
"math/rand"
"time"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type CollectHostFilesystemPerformance struct {
hostCollector *troubleshootv1beta2.FilesystemPerformance
}
func (c *CollectHostFilesystemPerformance) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Filesystem Performance")
}
func (c *CollectHostFilesystemPerformance) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostFilesystemPerformance) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
return collectHostFilesystemPerformance(c.hostCollector)
}
type FSPerfResults struct {
Min time.Duration
Max time.Duration
Average time.Duration
P1 time.Duration
P5 time.Duration
P10 time.Duration
P20 time.Duration
P30 time.Duration
P40 time.Duration
P50 time.Duration
P60 time.Duration
P70 time.Duration
P80 time.Duration
P90 time.Duration
P95 time.Duration
P99 time.Duration
P995 time.Duration
P999 time.Duration
P9995 time.Duration
P9999 time.Duration
IOPS int
}
func getPercentileIndex(p float64, items int) int {
if p >= 1 {
return items - 1
}
return int(math.Ceil(p*float64(items))) - 1
}
@@ -0,0 +1,10 @@
package collect
import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
func collectHostFilesystemPerformance(hostCollector *troubleshootv1beta2.FilesystemPerformance) (map[string][]byte, error) {
return nil, errors.New("Filesystem performance collector is only implemented for Linux")
}
@@ -0,0 +1,225 @@
package collect
import (
"encoding/json"
"log"
"math/rand"
"os"
"path/filepath"
"sort"
"sync"
"syscall"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"k8s.io/apimachinery/pkg/api/resource"
)
func init() {
rand.Seed(time.Now().UnixNano())
}
type Durations []time.Duration
func (d Durations) Len() int {
return len(d)
}
func (d Durations) Less(i, j int) bool {
return d[i] < d[j]
}
func (d Durations) Swap(i, j int) {
d[i], d[j] = d[j], d[i]
}
func collectHostFilesystemPerformance(hostCollector *troubleshootv1beta2.FilesystemPerformance) (map[string][]byte, error) {
var operationSize uint64 = 1024
if hostCollector.OperationSizeBytes != 0 {
operationSize = hostCollector.OperationSizeBytes
}
var fileSize uint64 = 10 * 1024 * 1024
if hostCollector.FileSize != "" {
quantity, err := resource.ParseQuantity(hostCollector.FileSize)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse fileSize %q", hostCollector.FileSize)
}
fileSizeInt64, ok := quantity.AsInt64()
if !ok {
return nil, errors.Wrapf(err, "failed to parse fileSize %q", hostCollector.FileSize)
}
fileSize = uint64(fileSizeInt64)
}
if hostCollector.Directory == "" {
return nil, errors.New("Directory is required to collect filesystem performance info")
}
// TODO: clean up this directory if its created
if err := os.MkdirAll(hostCollector.Directory, 0700); err != nil {
return nil, errors.Wrapf(err, "failed to mkdir %q", hostCollector.Directory)
}
filename := filepath.Join(hostCollector.Directory, "fsperf")
f, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
if err != nil {
return nil, errors.Wrapf(err, "open %s", filename)
}
defer func() {
if err := f.Close(); err != nil {
log.Println(err.Error())
}
if err := os.Remove(filename); err != nil {
log.Println(err.Error())
}
}()
// Sequential writes benchmark
var written uint64 = 0
var results Durations
for {
if written >= fileSize {
break
}
data := make([]byte, int(operationSize))
rand.Read(data)
start := time.Now()
n, err := f.Write(data)
if err != nil {
return nil, errors.Wrapf(err, "write to %s", filename)
}
if hostCollector.Sync {
if err := f.Sync(); err != nil {
return nil, errors.Wrapf(err, "sync %s", filename)
}
} else if hostCollector.Datasync {
if err := syscall.Fdatasync(int(f.Fd())); err != nil {
return nil, errors.Wrapf(err, "datasync %s", filename)
}
}
d := time.Now().Sub(start)
results = append(results, d)
written += uint64(n)
}
if len(results) == 0 {
return nil, errors.New("No filesystem performance results collected")
}
sort.Sort(results)
var sum time.Duration
for _, d := range results {
sum += d
}
fsPerf := &FSPerfResults{
Min: results[0],
Max: results[len(results)-1],
Average: sum / time.Duration(len(results)),
P1: results[getPercentileIndex(.01, len(results))],
P5: results[getPercentileIndex(.05, len(results))],
P10: results[getPercentileIndex(.1, len(results))],
P20: results[getPercentileIndex(.2, len(results))],
P30: results[getPercentileIndex(.3, len(results))],
P40: results[getPercentileIndex(.4, len(results))],
P50: results[getPercentileIndex(.5, len(results))],
P60: results[getPercentileIndex(.6, len(results))],
P70: results[getPercentileIndex(.7, len(results))],
P80: results[getPercentileIndex(.8, len(results))],
P90: results[getPercentileIndex(.9, len(results))],
P95: results[getPercentileIndex(.95, len(results))],
P99: results[getPercentileIndex(.99, len(results))],
P995: results[getPercentileIndex(.995, len(results))],
P999: results[getPercentileIndex(.999, len(results))],
P9995: results[getPercentileIndex(.9995, len(results))],
P9999: results[getPercentileIndex(.9999, len(results))],
}
// Random IOPS benchmark
// Re-open the file read+write in direct mode to prevent caching
if err := f.Close(); err != nil {
return nil, errors.Wrapf(err, "close %s", filename)
}
f, err = os.OpenFile(filename, os.O_RDWR|syscall.O_DIRECT, 0600)
if err != nil {
return nil, errors.Wrapf(err, "open direct %s", filename)
}
offsets := make([]int64, len(results))
for index, p := range rand.Perm(len(results)) {
offsets[index] = int64(p) * int64(operationSize)
}
// Use multiple workers to keep the filesystem busy. Since operations are serialized on a single
// file, more than 2 does not improve IOPS.
workers := 2
wg := sync.WaitGroup{}
m := sync.Mutex{}
errs := make(chan error, workers)
start := time.Now()
for i := 0; i < workers; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
data := make([]byte, int(operationSize))
fd := int(f.Fd())
for idx, offset := range offsets {
if idx%workers != i {
continue
}
m.Lock()
n, err := syscall.Pread(fd, data, offset)
m.Unlock()
if err != nil {
errs <- errors.Wrapf(err, "failed to pread %d bytes to %s at offset %d", len(data), filename, offset)
}
if n != len(data) {
errs <- errors.Wrapf(err, "pread %d of %d bytes to %s at offset %d", n, len(data), filename, offset)
}
}
}(i)
}
wg.Wait()
if len(errs) > 0 {
return nil, <-errs
}
d := time.Now().Sub(start)
nsPerIO := d / time.Duration(len(offsets))
iops := time.Second / nsPerIO
fsPerf.IOPS = int(iops)
collectorName := hostCollector.CollectorName
if collectorName == "" {
collectorName = "filesystemPerformance"
}
name := filepath.Join("filesystemPerformance", collectorName+".json")
b, err := json.Marshal(fsPerf)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal fs perf results")
}
return map[string][]byte{
name: b,
}, nil
}
@@ -0,0 +1,59 @@
package collect
import (
"fmt"
"testing"
)
func TestGetPercentileIndex(t *testing.T) {
tests := []struct {
length int
p float64
answer int
}{
{
length: 2,
p: 0.49,
answer: 0,
},
{
length: 2,
p: 0.5,
answer: 0,
},
{
length: 2,
p: 0.51,
answer: 1,
},
{
length: 100,
p: 0.01,
answer: 0,
},
{
length: 100,
p: 0.99,
answer: 98,
},
{
length: 100,
p: 0.995,
answer: 99,
},
{
length: 10000,
p: 0.999,
answer: 9989,
},
}
for _, test := range tests {
name := fmt.Sprintf("(%f, %d) == %d", test.p, test.length, test.answer)
t.Run(name, func(t *testing.T) {
output := getPercentileIndex(test.p, test.length)
if output != test.answer {
t.Errorf("Got %d, want %d", output, test.answer)
}
})
}
}
@@ -0,0 +1,10 @@
package collect
import (
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
func collectHostFilesystemPerformance(hostCollector *troubleshootv1beta2.FilesystemPerformance) (map[string][]byte, error) {
return nil, errors.New("Filesystem performance collector is only implemented for Linux")
}
+53
View File
@@ -0,0 +1,53 @@
package collect
import (
"net/http"
"path/filepath"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type CollectHostHTTP struct {
hostCollector *troubleshootv1beta2.HostHTTP
}
func (c *CollectHostHTTP) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "HTTP Request")
}
func (c *CollectHostHTTP) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostHTTP) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
httpCollector := c.hostCollector
var response *http.Response
var err error
if httpCollector.Get != nil {
response, err = doGet(httpCollector.Get)
} else if httpCollector.Post != nil {
response, err = doPost(httpCollector.Post)
} else if httpCollector.Put != nil {
response, err = doPut(httpCollector.Put)
} else {
return nil, errors.New("no supported http request type")
}
output, err := responseToOutput(response, err, false)
if err != nil {
return nil, err
}
fileName := "result.json"
if httpCollector.CollectorName != "" {
fileName = httpCollector.CollectorName + ".json"
}
httpOutput := map[string][]byte{
filepath.Join("http", fileName): output,
}
return httpOutput, nil
}
+178
View File
@@ -0,0 +1,178 @@
package collect
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"path"
"strings"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/debug"
"github.com/segmentio/ksuid"
)
type CollectHostHTTPLoadBalancer struct {
hostCollector *troubleshootv1beta2.HTTPLoadBalancer
}
func (c *CollectHostHTTPLoadBalancer) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "HTTP Load Balancer")
}
func (c *CollectHostHTTPLoadBalancer) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostHTTPLoadBalancer) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
listenAddress := fmt.Sprintf("0.0.0.0:%d", c.hostCollector.Port)
timeout := 60 * time.Minute
if c.hostCollector.Timeout != "" {
var err error
timeout, err = time.ParseDuration(c.hostCollector.Timeout)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse timeout %q", c.hostCollector.Timeout)
}
}
requestToken := ksuid.New().Bytes()
responseToken := ksuid.New().Bytes()
listenErr := make(chan error, 1)
go func() {
mux := http.NewServeMux()
server := http.Server{
Addr: listenAddress,
Handler: mux,
}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return
}
if !bytes.Equal(body, requestToken) {
return
}
_, err = w.Write(responseToken)
if err != nil {
return
}
server.Shutdown(context.Background())
})
err := http.ListenAndServe(listenAddress, mux)
if err != http.ErrServerClosed {
listenErr <- err
}
}()
var networkStatus NetworkStatus
stopAfter := time.Now().Add(timeout)
for {
if len(listenErr) > 0 {
err := <-listenErr
if strings.Contains(err.Error(), "address already in use") {
networkStatus = NetworkStatusAddressInUse
break
}
if strings.Contains(err.Error(), "permission denied") {
networkStatus = NetworkStatusBindPermissionDenied
break
}
debug.Println(err.Error())
networkStatus = NetworkStatusErrorOther
break
}
if time.Now().After(stopAfter) {
break
}
networkStatus = attemptPOST(c.hostCollector.Address, requestToken, responseToken)
if networkStatus == NetworkStatusErrorOther || networkStatus == NetworkStatusConnectionTimeout {
progressChan <- errors.Errorf("http post %s: network status %q", c.hostCollector.Address, networkStatus)
time.Sleep(time.Second)
continue
}
break
}
result := NetworkStatusResult{
Status: networkStatus,
}
b, err := json.Marshal(result)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal result")
}
name := path.Join("httpLoadBalancer", "httpLoadBalancer.json")
if c.hostCollector.CollectorName != "" {
name = path.Join("httpLoadBalancer", fmt.Sprintf("%s.json", c.hostCollector.CollectorName))
}
return map[string][]byte{
name: b,
}, nil
}
func attemptPOST(address string, request []byte, response []byte) NetworkStatus {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
// Create a new transport every time to ensure a new TCP connection so the load balancer does
// not forward every request to the same backend
transport := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 50 * time.Millisecond,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
}
client := http.Client{
Transport: transport,
}
buf := bytes.NewBuffer(request)
req, err := http.NewRequestWithContext(ctx, "POST", address, buf)
if err != nil {
debug.Println(err.Error())
return NetworkStatusErrorOther
}
resp, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "connection refused") {
return NetworkStatusConnectionRefused
}
if strings.Contains(err.Error(), "i/o timeout") {
return NetworkStatusConnectionTimeout
}
return NetworkStatusErrorOther
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return NetworkStatusErrorOther
}
if !bytes.Equal(body, response) {
return NetworkStatusErrorOther
}
return NetworkStatusConnected
}
+53
View File
@@ -0,0 +1,53 @@
package collect
import (
"encoding/json"
"net"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type CollectHostIPV4Interfaces struct {
hostCollector *troubleshootv1beta2.IPV4Interfaces
}
func (c *CollectHostIPV4Interfaces) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "IPv4 Interfaces")
}
func (c *CollectHostIPV4Interfaces) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostIPV4Interfaces) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
var ipv4Interfaces []net.Interface
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 {
continue
}
ipv4Interfaces = append(ipv4Interfaces, iface)
}
b, err := json.Marshal(ipv4Interfaces)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal network interfaces")
}
return map[string][]byte{
"system/ipv4Interfaces.json": b,
}, nil
}
+14 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/shirou/gopsutil/mem"
)
@@ -11,7 +12,19 @@ type MemoryInfo struct {
Total uint64 `json:"total"`
}
func HostMemory(c *HostCollector) (map[string][]byte, error) {
type CollectHostMemory struct {
hostCollector *troubleshootv1beta2.Memory
}
func (c *CollectHostMemory) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Amount of Memory")
}
func (c *CollectHostMemory) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostMemory) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
memoryInfo := MemoryInfo{}
vmstat, err := mem.VirtualMemory()
+20 -19
View File
@@ -2,30 +2,31 @@ package collect
import (
"bytes"
"fmt"
"net"
"strings"
"time"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/debug"
"github.com/segmentio/ksuid"
)
type NetworkStatus string
const (
NetworkStatusAddressInUse = "address-in-use"
NetworkStatusConnectionRefused = "connection-refused"
NetworkStatusConnectionTimeout = "connection-timeout"
NetworkStatusConnected = "connected"
NetworkStatusErrorOther = "error"
NetworkStatusAddressInUse = "address-in-use"
NetworkStatusConnectionRefused = "connection-refused"
NetworkStatusConnectionTimeout = "connection-timeout"
NetworkStatusConnected = "connected"
NetworkStatusErrorOther = "error"
NetworkStatusBindPermissionDenied = "bind-permission-denied"
)
type NetworkStatusResult struct {
Status NetworkStatus `json:"status"`
}
func checkTCPConnection(listenAddress string, dialAddress string, timeout time.Duration) (NetworkStatus, error) {
func checkTCPConnection(progressChan chan<- interface{}, 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") {
@@ -65,7 +66,8 @@ func checkTCPConnection(listenAddress string, dialAddress string, timeout time.D
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)
progressChan <- err
time.Sleep(time.Second)
continue
}
if strings.Contains(err.Error(), "connection refused") {
@@ -78,26 +80,27 @@ func checkTCPConnection(listenAddress string, dialAddress string, timeout time.D
return NetworkStatusConnected, nil
}
time.Sleep(time.Millisecond * 50)
progressChan <- errors.New("failed to verify connection to server")
time.Sleep(time.Second)
}
}
func handleTestConnection(conn net.Conn, requestToken []byte, responseToken []byte) bool {
defer func() {
if err := conn.Close(); err != nil {
fmt.Println(err.Error())
debug.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)
debug.Printf("Server failed to set read deadline: %v\n", err)
return false
}
buf := make([]byte, 1024)
_, err := conn.Read(buf)
if err != nil {
fmt.Printf("Server failed to read: %v", err)
debug.Printf("Server failed to read: %v\n", err)
return false
}
@@ -106,12 +109,12 @@ func handleTestConnection(conn net.Conn, requestToken []byte, responseToken []by
}
if err := conn.SetWriteDeadline(time.Now().Add(50 * time.Millisecond)); err != nil {
fmt.Printf("Server failed to set write deadline: %v", err)
debug.Printf("Server failed to set write deadline: %v\n", err)
return false
}
if _, err := conn.Write(responseToken); err != nil {
fmt.Printf("Server failed to write: %v", err)
debug.Printf("Server failed to write: %v\n", err)
return false
}
@@ -121,30 +124,28 @@ func handleTestConnection(conn net.Conn, requestToken []byte, responseToken []by
func verifyConnectionToServer(conn net.Conn, requestToken []byte, responseToken []byte) bool {
defer func() {
if err := conn.Close(); err != nil {
fmt.Println(err.Error())
debug.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)
debug.Printf("Client failed to set write deadline: %v\n", 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)
debug.Printf("Client failed to set read deadline: %v\n", err)
return false
}
buf := make([]byte, 1024)
_, err = conn.Read(buf)
if err != nil {
fmt.Printf("Client failed to read: %v", err)
return false
}
+69
View File
@@ -0,0 +1,69 @@
package collect
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"os/exec"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type ServiceInfo struct {
Unit string `json:"Unit"`
Load string `json:"Load"`
Active string `json:"Active"`
Sub string `json:"Sub"`
}
const systemctlFormat = `%s %s %s %s` // this leaves off the description
const HostServicesPath = `system/systemctl_services.json`
type CollectHostServices struct {
hostCollector *troubleshootv1beta2.HostServices
}
func (c *CollectHostServices) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Block Devices")
}
func (c *CollectHostServices) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostServices) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
var devices []ServiceInfo
cmd := exec.Command("systemctl", "list-units", "--type=service", "--no-legend", "--all")
stdout, err := cmd.Output()
if err != nil {
return nil, errors.Wrapf(err, "failed to execute systemctl")
}
buf := bytes.NewBuffer(stdout)
scanner := bufio.NewScanner(buf)
for scanner.Scan() {
bdi := ServiceInfo{}
fmt.Sscanf(
scanner.Text(),
systemctlFormat,
&bdi.Unit,
&bdi.Load,
&bdi.Active,
&bdi.Sub,
)
devices = append(devices, bdi)
}
b, err := json.Marshal(devices)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal systemctl service info")
}
return map[string][]byte{
HostServicesPath: b,
}, nil
}
+69
View File
@@ -0,0 +1,69 @@
package collect
import (
"encoding/json"
"fmt"
"net"
"path"
"strings"
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type CollectHostTCPConnect struct {
hostCollector *troubleshootv1beta2.TCPConnect
}
func (c *CollectHostTCPConnect) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "TCP Connection Attempt")
}
func (c *CollectHostTCPConnect) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostTCPConnect) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
address := c.hostCollector.Address
timeout := 10 * time.Second
if c.hostCollector.Timeout != "" {
var err error
timeout, err = time.ParseDuration(c.hostCollector.Timeout)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse timeout %q", c.hostCollector.Timeout)
}
}
result := NetworkStatusResult{
Status: attemptConnect(address, timeout),
}
b, err := json.Marshal(result)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal result")
}
name := path.Join("connect", fmt.Sprintf("%s.json", c.hostCollector.CollectorName))
return map[string][]byte{
name: b,
}, nil
}
func attemptConnect(address string, timeout time.Duration) NetworkStatus {
conn, err := net.DialTimeout("tcp", address, timeout)
if err != nil {
if strings.Contains(err.Error(), "i/o timeout") {
return NetworkStatusConnectionTimeout
}
if strings.Contains(err.Error(), "connection refused") {
return NetworkStatusConnectionRefused
}
return NetworkStatusErrorOther
}
conn.Close()
return NetworkStatusConnected
}
+19 -16
View File
@@ -7,32 +7,35 @@ import (
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type ConnectionResult int
type CollectHostTCPLoadBalancer struct {
hostCollector *troubleshootv1beta2.TCPLoadBalancer
}
const (
ConnectionRefused ConnectionResult = iota
Connected
ConnectionTimeout
ConnectionAddressInUse
ErrorOther
)
func (c *CollectHostTCPLoadBalancer) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "TCP Load Balancer")
}
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
func (c *CollectHostTCPLoadBalancer) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostTCPLoadBalancer) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
listenAddress := fmt.Sprintf("0.0.0.0:%d", c.hostCollector.Port)
dialAddress := c.hostCollector.Address
timeout := 60 * time.Minute
if c.Collect.TCPLoadBalancer.Timeout != "" {
if c.hostCollector.Timeout != "" {
var err error
timeout, err = time.ParseDuration(c.Collect.TCPLoadBalancer.Timeout)
timeout, err = time.ParseDuration(c.hostCollector.Timeout)
if err != nil {
return nil, errors.Wrap(err, "failed to parse durection")
}
}
networkStatus, err := checkTCPConnection(listenAddress, dialAddress, timeout)
networkStatus, err := checkTCPConnection(progressChan, listenAddress, dialAddress, timeout)
if err != nil {
return nil, err
}
@@ -47,8 +50,8 @@ func HostTCPLoadBalancer(c *HostCollector) (map[string][]byte, error) {
}
name := path.Join("tcpLoadBalancer", "tcpLoadBalancer.json")
if c.Collect.TCPLoadBalancer.CollectorName != "" {
name = path.Join("tcpLoadBalancer", fmt.Sprintf("%s.json", c.Collect.TCPLoadBalancer.CollectorName))
if c.hostCollector.CollectorName != "" {
name = path.Join("tcpLoadBalancer", fmt.Sprintf("%s.json", c.hostCollector.CollectorName))
}
return map[string][]byte{
+25 -12
View File
@@ -8,22 +8,35 @@ import (
"time"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
func HostTCPPortStatus(c *HostCollector) (map[string][]byte, error) {
dialAddress := ""
listenAddress := fmt.Sprintf("0.0.0.0:%d", c.Collect.TCPPortStatus.Port)
type CollectHostTCPPortStatus struct {
hostCollector *troubleshootv1beta2.TCPPortStatus
}
if c.Collect.TCPPortStatus.Interface != "" {
iface, err := net.InterfaceByName(c.Collect.TCPPortStatus.Interface)
func (c *CollectHostTCPPortStatus) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "TCP Port Status")
}
func (c *CollectHostTCPPortStatus) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostTCPPortStatus) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
dialAddress := ""
listenAddress := fmt.Sprintf("0.0.0.0:%d", c.hostCollector.Port)
if c.hostCollector.Interface != "" {
iface, err := net.InterfaceByName(c.hostCollector.Interface)
if err != nil {
return nil, errors.Wrapf(err, "lookup interface %s", c.Collect.TCPPortStatus.Interface)
return nil, errors.Wrapf(err, "lookup interface %s", c.hostCollector.Interface)
}
ip, err := getIPv4FromInterface(iface)
if err != nil {
return nil, errors.Wrapf(err, "get ipv4 address for interface %s", c.Collect.TCPPortStatus.Interface)
return nil, errors.Wrapf(err, "get ipv4 address for interface %s", c.hostCollector.Interface)
}
listenAddress = fmt.Sprintf("%s:%d", ip, c.Collect.TCPPortStatus.Port)
listenAddress = fmt.Sprintf("%s:%d", ip, c.hostCollector.Port)
dialAddress = listenAddress
}
@@ -32,10 +45,10 @@ func HostTCPPortStatus(c *HostCollector) (map[string][]byte, error) {
if err != nil {
return nil, err
}
dialAddress = fmt.Sprintf("%s:%d", ip, c.Collect.TCPPortStatus.Port)
dialAddress = fmt.Sprintf("%s:%d", ip, c.hostCollector.Port)
}
networkStatus, err := checkTCPConnection(listenAddress, dialAddress, 10*time.Second)
networkStatus, err := checkTCPConnection(progressChan, listenAddress, dialAddress, 10*time.Second)
if err != nil {
return nil, err
}
@@ -49,8 +62,8 @@ func HostTCPPortStatus(c *HostCollector) (map[string][]byte, error) {
}
name := path.Join("tcpPortStatus", "tcpPortStatus.json")
if c.Collect.TCPPortStatus.CollectorName != "" {
name = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", c.Collect.TCPPortStatus.CollectorName))
if c.hostCollector.CollectorName != "" {
name = path.Join("tcpPortStatus", fmt.Sprintf("%s.json", c.hostCollector.CollectorName))
}
return map[string][]byte{
name: b,
+94
View File
@@ -0,0 +1,94 @@
package collect
import (
"encoding/json"
"fmt"
"log"
"strings"
"github.com/godbus/dbus"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type NTPStatus string
type TimeInfo struct {
Timezone string `json:"timezone"`
NTPSynchronized bool `json:"ntp_synchronized"`
NTPActive bool `json:"ntp_active"`
}
type CollectHostTime struct {
hostCollector *troubleshootv1beta2.HostTime
}
func (c *CollectHostTime) Title() string {
return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "TCP Port Status")
}
func (c *CollectHostTime) IsExcluded() (bool, error) {
return isExcluded(c.hostCollector.Exclude)
}
func (c *CollectHostTime) Collect(progressChan chan<- interface{}) (map[string][]byte, error) {
timeInfo := TimeInfo{}
conn, err := dbus.SystemBus()
if err != nil {
return nil, errors.Wrap(err, "failed to connect to dbus")
}
defer func() {
if err := conn.Close(); err != nil {
log.Printf("Failed to close dbus connection: %v\n", err)
}
}()
prop := "org.freedesktop.timedate1.Timezone"
variant, err := conn.Object("org.freedesktop.timedate1", "/org/freedesktop/timedate1").GetProperty(prop)
if err != nil {
return nil, errors.Wrapf(err, "failed to read property %s", prop)
}
timeInfo.Timezone = strings.Trim(variant.String(), `"`)
// UTC is reported as Etc/UTC on Ubuntu
if strings.ToLower(timeInfo.Timezone) == "etc/utc" {
timeInfo.Timezone = "UTC"
}
prop = "org.freedesktop.timedate1.NTPSynchronized"
variant, err = conn.Object("org.freedesktop.timedate1", "/org/freedesktop/timedate1").GetProperty(prop)
if err != nil {
return nil, errors.Wrapf(err, "failed to read property %s", prop)
}
switch variant.String() {
case "true":
timeInfo.NTPSynchronized = true
case "false":
timeInfo.NTPSynchronized = false
default:
return nil, fmt.Errorf("Unexpected value for property %s: %s", prop, variant.String())
}
prop = "org.freedesktop.timedate1.NTP"
variant, err = conn.Object("org.freedesktop.timedate1", "/org/freedesktop/timedate1").GetProperty(prop)
if err != nil {
return nil, errors.Wrapf(err, "failed to read property %s", prop)
}
switch variant.String() {
case "true":
timeInfo.NTPActive = true
case "false":
timeInfo.NTPActive = false
default:
return nil, fmt.Errorf("Unexpected value for property %s: %s", prop, variant.String())
}
b, err := json.Marshal(timeInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to marshal time info")
}
return map[string][]byte{
"system/time.json": b,
}, nil
}
+4 -4
View File
@@ -12,13 +12,13 @@ import (
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
)
type httpResponse struct {
type HTTPResponse struct {
Status int `json:"status"`
Body string `json:"body"`
Headers map[string]string `json:"headers"`
}
type httpError struct {
type HTTPError struct {
Message string `json:"message"`
}
@@ -106,7 +106,7 @@ func doPut(put *troubleshootv1beta2.Put) (*http.Response, error) {
func responseToOutput(response *http.Response, err error, doRedact bool) ([]byte, error) {
output := make(map[string]interface{})
if err != nil {
output["error"] = httpError{
output["error"] = HTTPError{
Message: err.Error(),
}
} else {
@@ -120,7 +120,7 @@ func responseToOutput(response *http.Response, err error, doRedact bool) ([]byte
headers[k] = strings.Join(v, ",")
}
output["response"] = httpResponse{
output["response"] = HTTPResponse{
Status: response.StatusCode,
Body: string(body),
Headers: headers,
+255
View File
@@ -0,0 +1,255 @@
package collect
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
"time"
imagedocker "github.com/containers/image/v5/docker"
dockerref "github.com/containers/image/v5/docker/reference"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/docker/distribution/registry/api/errcode"
registryv2 "github.com/docker/distribution/registry/api/v2"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
type RegistryImage struct {
Exists bool `json:"exists"`
Error string `json:"error,omitempty"`
}
type RegistryInfo struct {
Images map[string]RegistryImage `json:"images"`
}
type registryAuthConfig struct {
username string
password string
}
func Registry(c *Collector, registryCollector *troubleshootv1beta2.RegistryImages) (map[string][]byte, error) {
registryInfo := RegistryInfo{
Images: map[string]RegistryImage{},
}
for _, image := range registryCollector.Images {
exists, err := imageExists(c, registryCollector, image)
if err != nil {
registryInfo.Images[image] = RegistryImage{
Error: err.Error(),
}
} else {
registryInfo.Images[image] = RegistryImage{
Exists: exists,
}
}
}
b, err := json.MarshalIndent(registryInfo, "", " ")
if err != nil {
return nil, errors.Wrap(err, "failed to marshal database connection")
}
collectorName := registryCollector.CollectorName
if collectorName == "" {
collectorName = "images"
}
registryOutput := map[string][]byte{
fmt.Sprintf("registry/%s.json", collectorName): b,
}
return registryOutput, nil
}
func imageExists(c *Collector, registryCollector *troubleshootv1beta2.RegistryImages, image string) (bool, error) {
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("docker://%s", image))
if err != nil {
return false, errors.Wrapf(err, "failed to parse image name %s", image)
}
authConfig, err := getImageAuthConfig(c, registryCollector, imageRef)
if err != nil {
return false, errors.Wrap(err, "failed to get auth config")
}
var lastErr error
for i := 0; i < 3; i++ {
sysCtx := types.SystemContext{
DockerDisableV1Ping: true,
DockerInsecureSkipTLSVerify: types.OptionalBoolTrue,
}
if authConfig != nil {
sysCtx.DockerAuthConfig = &types.DockerAuthConfig{
Username: authConfig.username,
Password: authConfig.password,
}
}
remoteImage, err := imageRef.NewImage(context.Background(), &sysCtx)
if err == nil {
remoteImage.Close()
return true, nil
}
if strings.Contains(err.Error(), "no image found in manifest list for architecture") {
// manifest was downloaded, but no matching architecture found in manifest
// should this count as image does not exist?
// this binary's architecture is not necessarily what will run in the cluster
return true, nil
}
if isNotFound(err) {
return false, nil
}
if strings.Contains(err.Error(), "EOF") {
lastErr = err
time.Sleep(1 * time.Second)
continue
}
return false, errors.Wrap(err, "failed to get image manifest")
}
return false, errors.Wrap(lastErr, "failed to retry")
}
func getImageAuthConfig(c *Collector, registryCollector *troubleshootv1beta2.RegistryImages, imageRef types.ImageReference) (*registryAuthConfig, error) {
if registryCollector.ImagePullSecrets == nil {
return nil, nil
}
if registryCollector.ImagePullSecrets.Data != nil {
config, err := getImageAuthConfigFromData(c, imageRef, registryCollector.ImagePullSecrets)
if err != nil {
return nil, errors.Wrap(err, "failed to get auth from data")
}
return config, nil
}
if registryCollector.ImagePullSecrets.Name != "" {
namespace := registryCollector.Namespace
if namespace == "" {
namespace = c.Namespace
}
if namespace == "" {
namespace = "default"
}
config, err := getImageAuthConfigFromSecret(c, imageRef, registryCollector.ImagePullSecrets, namespace)
if err != nil {
return nil, errors.Wrap(err, "failed to get auth from secret")
}
return config, nil
}
return nil, errors.New("image pull secret spec is not valid")
}
func getImageAuthConfigFromData(c *Collector, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets) (*registryAuthConfig, error) {
if pullSecrets.SecretType != "kubernetes.io/dockerconfigjson" {
return nil, errors.Errorf("secret type is not supported: %s", pullSecrets.SecretType)
}
configJsonBase64 := pullSecrets.Data[".dockerconfigjson"]
registry := dockerref.Domain(imageRef.DockerReference())
configJson, err := base64.StdEncoding.DecodeString(configJsonBase64)
if err != nil {
return nil, errors.Wrap(err, "failed to decode docker config string")
}
dockerCfgJSON := struct {
Auths map[string]struct {
Auth []byte `json:"auth"`
} `json:"auths"`
}{}
err = json.Unmarshal([]byte(configJson), &dockerCfgJSON)
if err != nil {
return nil, errors.Wrap(err, "failed to unmarshal config json")
}
auth, ok := dockerCfgJSON.Auths[registry]
if !ok {
// Suport a mix of public and private images
return nil, nil
}
parts := strings.Split(string(auth.Auth), ":")
if len(parts) != 2 {
return nil, errors.Errorf("expected 2 parts in the string, but found %d", len(parts))
}
authConfig := registryAuthConfig{
username: parts[0],
password: parts[1],
}
return &authConfig, nil
}
func getImageAuthConfigFromSecret(c *Collector, imageRef types.ImageReference, pullSecrets *v1beta2.ImagePullSecrets, namespace string) (*registryAuthConfig, error) {
ctx := context.Background()
client, err := kubernetes.NewForConfig(c.ClientConfig)
if err != nil {
return nil, errors.Wrap(err, "failed to create client from config")
}
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, pullSecrets.Name, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get secret")
}
foundSecrets := &v1beta2.ImagePullSecrets{
Name: secret.Name,
SecretType: string(secret.Type),
Data: map[string]string{
".dockerconfigjson": base64.StdEncoding.EncodeToString(secret.Data[".dockerconfigjson"]),
},
}
config, err := getImageAuthConfigFromData(c, imageRef, foundSecrets)
if err != nil {
return nil, errors.Wrap(err, "failed to get auth from secret data")
}
return config, nil
}
func isNotFound(err error) bool {
switch err := err.(type) {
case errcode.Errors:
for _, e := range err {
if isNotFound(e) {
return true
}
}
return false
case errcode.Error:
return err.Message == registryv2.ErrorCodeManifestUnknown.Message()
}
// this type will cause panic when compared to error type
if _, ok := err.(imagedocker.ErrUnauthorizedForCredentials); ok {
return false
}
cause := errors.Cause(err)
if cause, ok := cause.(error); ok {
if cause == err {
return false
}
}
return isNotFound(cause)
}
+28
View File
@@ -0,0 +1,28 @@
package debug
import (
"log"
"os"
"github.com/spf13/viper"
)
var logger = log.New(os.Stderr, "[debug]", log.Lshortfile)
func Print(v ...interface{}) {
if viper.GetBool("debug") {
log.Print(v...)
}
}
func Printf(format string, v ...interface{}) {
if viper.GetBool("debug") {
log.Printf(format, v...)
}
}
func Println(v ...interface{}) {
if viper.GetBool("debug") {
log.Println(v...)
}
}
+2 -14
View File
@@ -64,20 +64,8 @@ func doAnalyze(allCollectedData map[string][]byte, analyzers []*troubleshootv1be
}
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...)
}
analyzeResult := analyze.HostAnalyze(hostAnalyzer, getCollectedFileContents, getChildCollectedFileContents)
analyzeResults = append(analyzeResults, analyzeResult...)
}
return analyzeResults
}
+47 -8
View File
@@ -18,6 +18,13 @@ type CollectOpts struct {
ProgressChan chan interface{}
}
type CollectProgress struct {
CurrentName string
CurrentStatus string
CompletedCount int
TotalCount int
}
type CollectResult interface {
Analyze() []*analyze.AnalyzeResult
IsRBACAllowed() bool
@@ -36,7 +43,7 @@ func (cr ClusterCollectResult) IsRBACAllowed() bool {
type HostCollectResult struct {
AllCollectedData map[string][]byte
Collectors collect.HostCollectors
Collectors []collect.HostCollector
Spec *troubleshootv1beta2.HostPreflight
}
@@ -53,12 +60,12 @@ func CollectHost(opts CollectOpts, p *troubleshootv1beta2.HostPreflight) (Collec
allCollectedData := make(map[string][]byte)
var collectors collect.HostCollectors
var collectors []collect.HostCollector
for _, desiredCollector := range collectSpecs {
collector := collect.HostCollector{
Collect: desiredCollector,
collector, ok := collect.GetHostCollector(desiredCollector)
if ok {
collectors = append(collectors, collector)
}
collectors = append(collectors, &collector)
}
collectResult := HostCollectResult{
@@ -67,9 +74,15 @@ func CollectHost(opts CollectOpts, p *troubleshootv1beta2.HostPreflight) (Collec
}
for _, collector := range collectors {
result, err := collector.RunCollectorSync()
isExcluded, _ := collector.IsExcluded()
if isExcluded {
continue
}
opts.ProgressChan <- fmt.Sprintf("[%s] Running collector...", collector.Title())
result, err := collector.Collect(opts.ProgressChan)
if err != nil {
opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v\n", collector.GetDisplayName(), err)
opts.ProgressChan <- errors.Errorf("failed to run collector: %s: %v", collector.Title(), err)
continue
}
@@ -128,22 +141,48 @@ func Collect(opts CollectOpts, p *troubleshootv1beta2.Preflight) (CollectResult,
}
// Run preflights collectors synchronously
for _, collector := range collectors {
for i, collector := range collectors {
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
opts.ProgressChan <- fmt.Sprintf("skipping collector %s with insufficient RBAC permissions", collector.GetDisplayName())
opts.ProgressChan <- CollectProgress{
CurrentName: collector.GetDisplayName(),
CurrentStatus: "skipped",
CompletedCount: i + 1,
TotalCount: len(collectors),
}
continue
}
}
opts.ProgressChan <- CollectProgress{
CurrentName: collector.GetDisplayName(),
CurrentStatus: "running",
CompletedCount: i,
TotalCount: len(collectors),
}
result, err := collector.RunCollectorSync(nil)
if err != nil {
opts.ProgressChan <- errors.Errorf("failed to run collector %s: %v\n", collector.GetDisplayName(), err)
opts.ProgressChan <- CollectProgress{
CurrentName: collector.GetDisplayName(),
CurrentStatus: "failed",
CompletedCount: i + 1,
TotalCount: len(collectors),
}
continue
}
opts.ProgressChan <- CollectProgress{
CurrentName: collector.GetDisplayName(),
CurrentStatus: "completed",
CompletedCount: i + 1,
TotalCount: len(collectors),
}
if result != nil {
for k, v := range result {
allCollectedData[k] = v
+34
View File
@@ -0,0 +1,34 @@
package specs
import (
"context"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
func LoadFromConfigMap(namespace string, configMapName string, key string) ([]byte, error) {
config, err := k8sutil.GetRESTConfig()
if err != nil {
return nil, errors.Wrap(err, "failed to convert kube flags to rest config")
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, errors.Wrap(err, "failed to convert create k8s client")
}
foundConfigMap, err := client.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get configmap")
}
spec, ok := foundConfigMap.Data[key]
if !ok {
return nil, errors.Errorf("spec not found in configmap %s", configMapName)
}
return []byte(spec), nil
}