Compare commits

...
26 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
26 changed files with 1887 additions and 302 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
+14
View File
@@ -121,6 +121,20 @@ func runPreflights(v *viper.Viper, arg string) error {
}
}
}()
} else {
// make sure we don't block any senders
go func() {
for {
select {
case _, ok := <-progressCh:
if !ok {
return
}
case <-finishedCh:
return
}
}
}()
}
defer func() {
+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.")
+1 -1
View File
@@ -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
+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
+22 -23
View File
@@ -3,53 +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/godbus/dbus v4.1.0+incompatible
github.com/google/go-cmp v0.3.1 // indirect
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
)
+593 -237
View File
File diff suppressed because it is too large Load Diff
+23 -1
View File
@@ -60,9 +60,16 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle
}
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: analyzer.Title(),
Title: "nil analyzer",
Message: fmt.Sprintf("Analyzer Failed: %v", err),
}}
}
@@ -293,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")
}
+2
View File
@@ -36,6 +36,8 @@ func GetHostAnalyzer(analyzer *troubleshootv1beta2.HostAnalyze) (HostAnalyzer, b
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
}
+22 -10
View File
@@ -51,7 +51,7 @@ func (a *AnalyzeHostBlockDevices) Analyze(getCollectedFileContents func(string)
return &result, nil
}
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Fail.When, devices)
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)
}
@@ -72,7 +72,7 @@ func (a *AnalyzeHostBlockDevices) Analyze(getCollectedFileContents func(string)
return &result, nil
}
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Warn.When, devices)
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)
}
@@ -93,7 +93,7 @@ func (a *AnalyzeHostBlockDevices) Analyze(getCollectedFileContents func(string)
return &result, nil
}
isMatch, err := compareHostBlockDevicesConditionalToActual(outcome.Pass.When, devices)
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)
}
@@ -113,7 +113,7 @@ func (a *AnalyzeHostBlockDevices) Analyze(getCollectedFileContents func(string)
// <regexp> <op> <count>
// example: sdb > 0
func compareHostBlockDevicesConditionalToActual(conditional string, devices []collect.BlockDeviceInfo) (res bool, err error) {
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))
@@ -123,7 +123,7 @@ func compareHostBlockDevicesConditionalToActual(conditional string, devices []co
if err != nil {
return false, errors.Wrapf(err, "failed to compile regex %q", parts[0])
}
count := countEligibleBlockDevices(rx, devices)
count := countEligibleBlockDevices(rx, minimumAcceptableSize, includeUnmountedPartitions, devices)
desiredInt, err := strconv.Atoi(parts[2])
if err != nil {
@@ -146,11 +146,11 @@ func compareHostBlockDevicesConditionalToActual(conditional string, devices []co
return false, fmt.Errorf("Unexpected operator %q", parts[1])
}
func countEligibleBlockDevices(rx *regexp.Regexp, devices []collect.BlockDeviceInfo) int {
func countEligibleBlockDevices(rx *regexp.Regexp, minimumAcceptableSize uint64, includeUnmountedPartitions bool, devices []collect.BlockDeviceInfo) int {
count := 0
for _, device := range devices {
if isEligibleBlockDevice(rx, device, devices) {
if isEligibleBlockDevice(rx, minimumAcceptableSize, includeUnmountedPartitions, device, devices) {
count++
}
}
@@ -158,13 +158,25 @@ func countEligibleBlockDevices(rx *regexp.Regexp, devices []collect.BlockDeviceI
return count
}
func isEligibleBlockDevice(rx *regexp.Regexp, device collect.BlockDeviceInfo, devices []collect.BlockDeviceInfo) bool {
func isEligibleBlockDevice(rx *regexp.Regexp, minimumAcceptableSize uint64, includeUnmountedPartitions bool, device collect.BlockDeviceInfo, devices []collect.BlockDeviceInfo) bool {
if !rx.MatchString(device.Name) {
return false
}
if device.Type != "disk" {
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 != "" {
+91
View File
@@ -149,6 +149,97 @@ func TestAnalyzeBlockDevices(t *testing.T) {
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) {
+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)
}
})
}
}
+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)
}
@@ -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 {
@@ -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>"
@@ -46,8 +46,10 @@ type TimeAnalyze struct {
}
type BlockDevicesAnalyze struct {
AnalyzeMeta `json:",inline" yaml:",inline"`
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
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 {
@@ -73,6 +75,11 @@ type CertificateAnalyze struct {
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"`
//
@@ -98,4 +105,6 @@ type HostAnalyze struct {
FilesystemPerformance *FilesystemPerformanceAnalyze `json:"filesystemPerformance,omitempty" yaml:"filesystemPerformance,omitempty"`
Certificate *CertificateAnalyze `json:"certificate,omitempty" yaml:"certificate,omitempty"`
HostServices *HostServicesAnalyze `json:"hostServices,omitempty" yaml:"hostServices,omitempty"`
}
@@ -88,6 +88,10 @@ type Certificate struct {
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"`
@@ -103,6 +107,7 @@ type HostCollect struct {
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.
@@ -575,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.
@@ -1278,6 +1288,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) {
*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.
@@ -1379,6 +1394,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) {
*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.
@@ -1549,6 +1569,49 @@ 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
@@ -2181,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
+2
View File
@@ -199,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
+2
View File
@@ -38,6 +38,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect) (HostCollector
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
}
+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
}
+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)
}
+34 -1
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
@@ -134,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