mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
@@ -433,6 +433,45 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
containerRuntime:
|
||||
properties:
|
||||
checkName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
customResourceDefinition:
|
||||
properties:
|
||||
checkName:
|
||||
@@ -520,6 +559,45 @@ spec:
|
||||
- namespace
|
||||
- name
|
||||
type: object
|
||||
distribution:
|
||||
properties:
|
||||
checkName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
imagePullSecret:
|
||||
properties:
|
||||
checkName:
|
||||
|
||||
@@ -433,6 +433,45 @@ spec:
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
containerRuntime:
|
||||
properties:
|
||||
checkName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
customResourceDefinition:
|
||||
properties:
|
||||
checkName:
|
||||
@@ -520,6 +559,45 @@ spec:
|
||||
- namespace
|
||||
- name
|
||||
type: object
|
||||
distribution:
|
||||
properties:
|
||||
checkName:
|
||||
type: string
|
||||
outcomes:
|
||||
items:
|
||||
properties:
|
||||
fail:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
pass:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
warn:
|
||||
properties:
|
||||
message:
|
||||
type: string
|
||||
uri:
|
||||
type: string
|
||||
when:
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
type: array
|
||||
required:
|
||||
- outcomes
|
||||
type: object
|
||||
imagePullSecret:
|
||||
properties:
|
||||
checkName:
|
||||
|
||||
@@ -76,6 +76,16 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = new(StatefulsetStatus)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ContainerRuntime != nil {
|
||||
in, out := &in.ContainerRuntime, &out.ContainerRuntime
|
||||
*out = new(ContainerRuntime)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Distribution != nil {
|
||||
in, out := &in.Distribution, &out.Distribution
|
||||
*out = new(Distribution)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
|
||||
@@ -677,6 +687,33 @@ func (in *CollectorStatus) DeepCopy() *CollectorStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ContainerRuntime) DeepCopyInto(out *ContainerRuntime) {
|
||||
*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 ContainerRuntime.
|
||||
func (in *ContainerRuntime) DeepCopy() *ContainerRuntime {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ContainerRuntime)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Copy) DeepCopyInto(out *Copy) {
|
||||
*out = *in
|
||||
@@ -752,6 +789,33 @@ func (in *DeploymentStatus) DeepCopy() *DeploymentStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Distribution) DeepCopyInto(out *Distribution) {
|
||||
*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 Distribution.
|
||||
func (in *Distribution) DeepCopy() *Distribution {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Distribution)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Exec) DeepCopyInto(out *Exec) {
|
||||
*out = *in
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
apiVersion: troubleshoot.replicated.com/v1beta1
|
||||
kind: Analyzer
|
||||
metadata:
|
||||
name: defaultAnalyzers
|
||||
name: a
|
||||
spec:
|
||||
analyzers:
|
||||
- clusterVersion:
|
||||
- distribution:
|
||||
outcomes:
|
||||
- fail:
|
||||
when: "< 1.13.0"
|
||||
message: The application requires at Kubernetes 1.13.0 or later, and recommends 1.15.0.
|
||||
uri: https://www.kubernetes.io
|
||||
when: "= docker desktop"
|
||||
message: "docker for desktop is not allowed"
|
||||
- fail:
|
||||
when: "microk8s"
|
||||
message: "mickrk8s is not prod"
|
||||
- warn:
|
||||
when: "< 1.15.0"
|
||||
message: Your cluster meets the minimum version of Kubernetes, but we recommend you update to 1.15.0 or later.
|
||||
uri: https://kubernetes.io
|
||||
when: "!= eks"
|
||||
message: "YMMV on not eks"
|
||||
- pass:
|
||||
when: ">= 1.15.0"
|
||||
message: Your cluster meets the recommended and required versions of Kubernetes.
|
||||
message: "good work"
|
||||
|
||||
|
||||
|
||||
@@ -3,27 +3,4 @@ kind: Collector
|
||||
metadata:
|
||||
name: collector-sample
|
||||
spec:
|
||||
collectors:
|
||||
- secret:
|
||||
name: myapp-postgres
|
||||
namespace: default
|
||||
key: uri
|
||||
includeValue: false
|
||||
- logs:
|
||||
selector:
|
||||
- name=cilium-operator
|
||||
namespace: kube-system
|
||||
limits:
|
||||
maxAge: 30d
|
||||
maxLines: 10000
|
||||
- run:
|
||||
collectorName: ping-google
|
||||
namespace: default
|
||||
image: flungo/netutils
|
||||
command: ["ping"]
|
||||
args: ["www.google.com"]
|
||||
timeout: 5s
|
||||
- http:
|
||||
collectorName: echo-ip
|
||||
get:
|
||||
url: https://api.replicated.com/market/v1/echo/ip
|
||||
collectors: []
|
||||
|
||||
@@ -38,10 +38,16 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont
|
||||
return analyzeImagePullSecret(analyzer.ImagePullSecret, findFiles)
|
||||
}
|
||||
if analyzer.DeploymentStatus != nil {
|
||||
return deploymentStatus(analyzer.DeploymentStatus, getFile)
|
||||
return analyzeDeploymentStatus(analyzer.DeploymentStatus, getFile)
|
||||
}
|
||||
if analyzer.StatefulsetStatus != nil {
|
||||
return statefulsetStatus(analyzer.StatefulsetStatus, getFile)
|
||||
return analyzeStatefulsetStatus(analyzer.StatefulsetStatus, getFile)
|
||||
}
|
||||
if analyzer.ContainerRuntime != nil {
|
||||
return analyzeContainerRuntime(analyzer.ContainerRuntime, getFile)
|
||||
}
|
||||
if analyzer.Distribution != nil {
|
||||
return analyzeDistribution(analyzer.Distribution, getFile)
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid analyzer")
|
||||
|
||||
132
pkg/analyze/container_runtime.go
Normal file
132
pkg/analyze/container_runtime.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func analyzeContainerRuntime(analyzer *troubleshootv1beta1.ContainerRuntime, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
collected, err := getCollectedFileContents("cluster-resources/nodes.json")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
|
||||
}
|
||||
|
||||
var nodes []corev1.Node
|
||||
if err := json.Unmarshal(collected, &nodes); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal node list")
|
||||
}
|
||||
|
||||
foundRuntimes := []string{}
|
||||
for _, node := range nodes {
|
||||
foundRuntimes = append(foundRuntimes, node.Status.NodeInfo.ContainerRuntimeVersion)
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: "Container Runtime",
|
||||
}
|
||||
|
||||
// ordering is important for passthrough
|
||||
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
|
||||
}
|
||||
|
||||
for _, foundRuntime := range foundRuntimes {
|
||||
isMatch, err := compareRuntimeConditionalToActual(outcome.Fail.When, foundRuntime)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to compare runtime 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
|
||||
}
|
||||
|
||||
for _, foundRuntime := range foundRuntimes {
|
||||
isMatch, err := compareRuntimeConditionalToActual(outcome.Warn.When, foundRuntime)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to compare runtime 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
|
||||
}
|
||||
|
||||
for _, foundRuntime := range foundRuntimes {
|
||||
isMatch, err := compareRuntimeConditionalToActual(outcome.Pass.When, foundRuntime)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to compare runtime conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func compareRuntimeConditionalToActual(conditional string, actual string) (bool, error) {
|
||||
parts := strings.Split(strings.TrimSpace(conditional), " ")
|
||||
|
||||
// we can make this a lot more flexible
|
||||
if len(parts) != 2 {
|
||||
return false, errors.New("unable to parse conditional")
|
||||
}
|
||||
|
||||
parsedRuntime, err := url.Parse(actual)
|
||||
if err != nil {
|
||||
return false, errors.New("unable to parse url")
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "=":
|
||||
fallthrough
|
||||
case "==":
|
||||
fallthrough
|
||||
case "===":
|
||||
return parts[1] == parsedRuntime.Scheme, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
121
pkg/analyze/container_runtime_test.go
Normal file
121
pkg/analyze/container_runtime_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_compareRuntimeConditionalToActual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditional string
|
||||
actual string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "containerd://1.2.5 = containerd",
|
||||
conditional: "= containerd",
|
||||
actual: "containerd://1.2.5",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "containerd://1.2.5 == containerd",
|
||||
conditional: "== containerd",
|
||||
actual: "containerd://1.2.5",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "containerd://1.2.5 === containerd",
|
||||
conditional: "=== containerd",
|
||||
actual: "containerd://1.2.5",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "containerd://1.2.5 != containerd",
|
||||
conditional: "!= containerd",
|
||||
actual: "containerd://1.2.5",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "containerd://1.2.5 !== containerd",
|
||||
conditional: "!== containerd",
|
||||
actual: "containerd://1.2.5",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "containerd://1.2.5 !== containerd",
|
||||
conditional: "!=== containerd",
|
||||
actual: "containerd://1.2.5",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
actual, err := compareRuntimeConditionalToActual(test.conditional, test.actual)
|
||||
req.NoError(err)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_containerRuntime(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
analyzer troubleshootv1beta1.ContainerRuntime
|
||||
expectResult AnalyzeResult
|
||||
files map[string][]byte
|
||||
}{
|
||||
{
|
||||
name: "no containerd, when it's containerd",
|
||||
analyzer: troubleshootv1beta1.ContainerRuntime{
|
||||
Outcomes: []*troubleshootv1beta1.Outcome{
|
||||
{
|
||||
Pass: &troubleshootv1beta1.SingleOutcome{
|
||||
When: "!= containerd",
|
||||
Message: "pass",
|
||||
},
|
||||
},
|
||||
{
|
||||
Fail: &troubleshootv1beta1.SingleOutcome{
|
||||
Message: "containerd detected",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectResult: AnalyzeResult{
|
||||
IsPass: false,
|
||||
IsWarn: false,
|
||||
IsFail: true,
|
||||
Title: "Container Runtime",
|
||||
Message: "containerd detected",
|
||||
},
|
||||
files: map[string][]byte{
|
||||
"cluster-resources/nodes.json": []byte(collectedNodes),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
getFiles := func(n string) ([]byte, error) {
|
||||
return test.files[n], nil
|
||||
}
|
||||
|
||||
actual, err := analyzeContainerRuntime(&test.analyzer, getFiles)
|
||||
req.NoError(err)
|
||||
|
||||
assert.Equal(t, &test.expectResult, actual)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -452,3 +452,123 @@ var collectedDeployments = `[
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
var collectedNodes = `[
|
||||
{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Node",
|
||||
"metadata": {
|
||||
"annotations": {
|
||||
"node.alpha.kubernetes.io/ttl": "0",
|
||||
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
|
||||
},
|
||||
"creationTimestamp": "2019-10-23T18:16:43Z",
|
||||
"labels": {
|
||||
"beta.kubernetes.io/arch": "amd64",
|
||||
"beta.kubernetes.io/os": "linux",
|
||||
"kubernetes.io/arch": "amd64",
|
||||
"kubernetes.io/hostname": "repldev-marc",
|
||||
"kubernetes.io/os": "linux",
|
||||
"microk8s.io/cluster": "true"
|
||||
},
|
||||
"name": "repldev-marc",
|
||||
"resourceVersion": "1769699",
|
||||
"selfLink": "/api/v1/nodes/repldev-marc",
|
||||
"uid": "cd30c57f-b445-437f-9473-f13343124030"
|
||||
},
|
||||
"spec": {},
|
||||
"status": {
|
||||
"addresses": [
|
||||
{
|
||||
"address": "10.168.0.26",
|
||||
"type": "InternalIP"
|
||||
},
|
||||
{
|
||||
"address": "repldev-marc",
|
||||
"type": "Hostname"
|
||||
}
|
||||
],
|
||||
"allocatable": {
|
||||
"cpu": "8",
|
||||
"ephemeral-storage": "1015018628Ki",
|
||||
"hugepages-1Gi": "0",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "30770604Ki",
|
||||
"pods": "110"
|
||||
},
|
||||
"capacity": {
|
||||
"cpu": "8",
|
||||
"ephemeral-storage": "1016067204Ki",
|
||||
"hugepages-1Gi": "0",
|
||||
"hugepages-2Mi": "0",
|
||||
"memory": "30873004Ki",
|
||||
"pods": "110"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
|
||||
"lastTransitionTime": "2019-10-31T21:28:36Z",
|
||||
"message": "kubelet has sufficient memory available",
|
||||
"reason": "KubeletHasSufficientMemory",
|
||||
"status": "False",
|
||||
"type": "MemoryPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
|
||||
"lastTransitionTime": "2019-10-31T21:28:36Z",
|
||||
"message": "kubelet has no disk pressure",
|
||||
"reason": "KubeletHasNoDiskPressure",
|
||||
"status": "False",
|
||||
"type": "DiskPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
|
||||
"lastTransitionTime": "2019-10-31T21:28:36Z",
|
||||
"message": "kubelet has sufficient PID available",
|
||||
"reason": "KubeletHasSufficientPID",
|
||||
"status": "False",
|
||||
"type": "PIDPressure"
|
||||
},
|
||||
{
|
||||
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
|
||||
"lastTransitionTime": "2019-10-31T21:28:36Z",
|
||||
"message": "kubelet is posting ready status. AppArmor enabled",
|
||||
"reason": "KubeletReady",
|
||||
"status": "True",
|
||||
"type": "Ready"
|
||||
}
|
||||
],
|
||||
"daemonEndpoints": {
|
||||
"kubeletEndpoint": {
|
||||
"Port": 10250
|
||||
}
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"names": [
|
||||
"localhost:32000/kotsadm-api@sha256:d4821b65869454dfac53ad01f295740df6fcd52711f0dcf6aa9d7e515f7ebe3c"
|
||||
],
|
||||
"sizeBytes": 755312372
|
||||
},
|
||||
{
|
||||
"names": [
|
||||
"localhost:32000/kotsadm-api@sha256:fc3c971facc9dbd1b07e19c1ebb33c6361dd219af8efed0616afd1278f81fa4e"
|
||||
],
|
||||
"sizeBytes": 755312032
|
||||
}
|
||||
],
|
||||
"nodeInfo": {
|
||||
"architecture": "amd64",
|
||||
"bootID": "3401cdf2-129c-473d-a50c-723afd7378d3",
|
||||
"containerRuntimeVersion": "containerd://1.2.5",
|
||||
"kernelVersion": "5.0.0-1021-gcp",
|
||||
"kubeProxyVersion": "v1.16.2",
|
||||
"kubeletVersion": "v1.16.2",
|
||||
"machineID": "97f4a34d2aa9e26785177a6b64fb9108",
|
||||
"operatingSystem": "linux",
|
||||
"osImage": "Ubuntu 18.04.2 LTS",
|
||||
"systemUUID": "9dc594e5-ac7b-c649-e61f-cad715a28f79"
|
||||
}
|
||||
}
|
||||
}
|
||||
]`
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
)
|
||||
|
||||
func deploymentStatus(analyzer *troubleshootv1beta1.DeploymentStatus, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
func analyzeDeploymentStatus(analyzer *troubleshootv1beta1.DeploymentStatus, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
collected, err := getCollectedFileContents(path.Join("cluster-resources", "deployments", fmt.Sprintf("%s.json", analyzer.Namespace)))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read collected deployments from namespace")
|
||||
@@ -33,7 +33,7 @@ func deploymentStatus(analyzer *troubleshootv1beta1.DeploymentStatus, getCollect
|
||||
return &AnalyzeResult{
|
||||
Title: fmt.Sprintf("%s Deployment Status", analyzer.Name),
|
||||
IsFail: true,
|
||||
Message: "not found",
|
||||
Message: fmt.Sprintf("The deployment %q was not found", analyzer.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ func Test_deploymentStatus(t *testing.T) {
|
||||
return test.files[n], nil
|
||||
}
|
||||
|
||||
actual, err := deploymentStatus(&test.analyzer, getFiles)
|
||||
actual, err := analyzeDeploymentStatus(&test.analyzer, getFiles)
|
||||
req.NoError(err)
|
||||
|
||||
assert.Equal(t, &test.expectResult, actual)
|
||||
|
||||
199
pkg/analyze/distribution.go
Normal file
199
pkg/analyze/distribution.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
type providers struct {
|
||||
microk8s bool
|
||||
dockerDesktop bool
|
||||
eks bool
|
||||
gke bool
|
||||
digitalOcean bool
|
||||
}
|
||||
|
||||
type Provider int
|
||||
|
||||
const (
|
||||
unknown Provider = iota
|
||||
microk8s Provider = iota
|
||||
dockerDesktop Provider = iota
|
||||
eks Provider = iota
|
||||
gke Provider = iota
|
||||
digitalOcean Provider = iota
|
||||
)
|
||||
|
||||
func analyzeDistribution(analyzer *troubleshootv1beta1.Distribution, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
collected, err := getCollectedFileContents("cluster-resources/nodes.json")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
|
||||
}
|
||||
|
||||
var nodes []corev1.Node
|
||||
if err := json.Unmarshal(collected, &nodes); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to unmarshal node list")
|
||||
}
|
||||
|
||||
foundProviders := providers{}
|
||||
|
||||
for _, node := range nodes {
|
||||
for k, v := range node.ObjectMeta.Labels {
|
||||
if k == "microk8s.io/cluster" && v == "true" {
|
||||
foundProviders.microk8s = true
|
||||
}
|
||||
}
|
||||
|
||||
if node.Status.NodeInfo.OSImage == "Docker Desktop" {
|
||||
foundProviders.dockerDesktop = true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(node.Spec.ProviderID, "digitalocean:") {
|
||||
foundProviders.digitalOcean = true
|
||||
}
|
||||
if strings.HasPrefix(node.Spec.ProviderID, "aws:") {
|
||||
foundProviders.eks = true
|
||||
}
|
||||
}
|
||||
|
||||
result := &AnalyzeResult{
|
||||
Title: "Kubernetes Distribution",
|
||||
}
|
||||
|
||||
// ordering is important for passthrough
|
||||
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 := compareDistributionConditionalToActual(outcome.Fail.When, foundProviders)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare distribution 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 := compareDistributionConditionalToActual(outcome.Warn.When, foundProviders)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare distribution 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 := compareDistributionConditionalToActual(outcome.Pass.When, foundProviders)
|
||||
if err != nil {
|
||||
return result, errors.Wrap(err, "failed to compare distribution conditional")
|
||||
}
|
||||
|
||||
if isMatch {
|
||||
result.IsPass = true
|
||||
result.Message = outcome.Pass.Message
|
||||
result.URI = outcome.Pass.URI
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func compareDistributionConditionalToActual(conditional string, actual providers) (bool, error) {
|
||||
parts := strings.Split(strings.TrimSpace(conditional), " ")
|
||||
|
||||
// we can make this a lot more flexible
|
||||
if len(parts) == 1 {
|
||||
parts = []string{
|
||||
"=",
|
||||
parts[0],
|
||||
}
|
||||
}
|
||||
|
||||
if len(parts) != 2 {
|
||||
return false, errors.New("unable to parse conditional")
|
||||
}
|
||||
|
||||
normalizedName := mustNormalizeDistributionName(parts[1])
|
||||
|
||||
if normalizedName == unknown {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
isMatch := false
|
||||
switch normalizedName {
|
||||
case microk8s:
|
||||
isMatch = actual.microk8s
|
||||
case dockerDesktop:
|
||||
isMatch = actual.dockerDesktop
|
||||
case eks:
|
||||
isMatch = actual.eks
|
||||
case gke:
|
||||
isMatch = actual.gke
|
||||
case digitalOcean:
|
||||
isMatch = actual.digitalOcean
|
||||
}
|
||||
|
||||
switch parts[0] {
|
||||
case "=", "==", "===":
|
||||
return isMatch, nil
|
||||
case "!=", "!==":
|
||||
return !isMatch, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func mustNormalizeDistributionName(raw string) Provider {
|
||||
switch strings.ReplaceAll(strings.TrimSpace(strings.ToLower(raw)), "-", "") {
|
||||
case "microk8s":
|
||||
return microk8s
|
||||
case "dockerdesktop":
|
||||
return dockerDesktop
|
||||
case "eks":
|
||||
return eks
|
||||
case "gke":
|
||||
return gke
|
||||
case "digitalocean":
|
||||
return digitalOcean
|
||||
}
|
||||
|
||||
return unknown
|
||||
}
|
||||
85
pkg/analyze/distribution_test.go
Normal file
85
pkg/analyze/distribution_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_compareDistributionConditionalToActual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
conditional string
|
||||
input providers
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "== microk8s when microk8s is found",
|
||||
conditional: "== microk8s",
|
||||
input: providers{
|
||||
microk8s: true,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "!= microk8s when microk8s is found",
|
||||
conditional: "!= microk8s",
|
||||
input: providers{
|
||||
microk8s: true,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "!== eks when gke is found",
|
||||
conditional: "!== eks",
|
||||
input: providers{
|
||||
gke: true,
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
actual, err := compareDistributionConditionalToActual(test.conditional, test.input)
|
||||
req.NoError(err)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func Test_mustNormalizeDistributionName(t *testing.T) {
|
||||
tests := []struct {
|
||||
raw string
|
||||
expected Provider
|
||||
}{
|
||||
{
|
||||
raw: "microk8s",
|
||||
expected: microk8s,
|
||||
},
|
||||
{
|
||||
raw: "MICROK8S",
|
||||
expected: microk8s,
|
||||
},
|
||||
{
|
||||
raw: " microk8s ",
|
||||
expected: microk8s,
|
||||
},
|
||||
{
|
||||
raw: "Docker-Desktop",
|
||||
expected: dockerDesktop,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.raw, func(t *testing.T) {
|
||||
actual := mustNormalizeDistributionName(test.raw)
|
||||
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
)
|
||||
|
||||
func statefulsetStatus(analyzer *troubleshootv1beta1.StatefulsetStatus, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
func analyzeStatefulsetStatus(analyzer *troubleshootv1beta1.StatefulsetStatus, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
|
||||
collected, err := getCollectedFileContents(path.Join("cluster-resources", "statefulsets", fmt.Sprintf("%s.json", analyzer.Namespace)))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read collected deployments from namespace")
|
||||
@@ -33,7 +33,7 @@ func statefulsetStatus(analyzer *troubleshootv1beta1.StatefulsetStatus, getColle
|
||||
return &AnalyzeResult{
|
||||
Title: fmt.Sprintf("%s Statefulset Status", analyzer.Name),
|
||||
IsFail: true,
|
||||
Message: "not found",
|
||||
Message: fmt.Sprintf("The statefulset %q was not found", analyzer.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,16 @@ type StatefulsetStatus struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
}
|
||||
|
||||
type ContainerRuntime struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type Distribution struct {
|
||||
AnalyzeMeta `json:",inline" yaml:",inline"`
|
||||
Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"`
|
||||
}
|
||||
|
||||
type AnalyzeMeta struct {
|
||||
CheckName string `json:"checkName,omitempty" yaml:"checkName,omitempty"`
|
||||
}
|
||||
@@ -77,4 +87,6 @@ type Analyze struct {
|
||||
ImagePullSecret *ImagePullSecret `json:"imagePullSecret,omitempty" yaml:"imagePullSecret,omitempty"`
|
||||
DeploymentStatus *DeploymentStatus `json:"deploymentStatus,omitempty" yaml:"deploymentStatus,omitempty"`
|
||||
StatefulsetStatus *StatefulsetStatus `json:"statefulsetStatus,omitempty" yaml:"statefulsetStatus,omitempty"`
|
||||
ContainerRuntime *ContainerRuntime `json:"containerRuntime,omitempty" yaml:"containerRuntime,omitempty"`
|
||||
Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"`
|
||||
}
|
||||
|
||||
@@ -92,6 +92,16 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*out = new(StatefulsetStatus)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.ContainerRuntime != nil {
|
||||
in, out := &in.ContainerRuntime, &out.ContainerRuntime
|
||||
*out = new(ContainerRuntime)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Distribution != nil {
|
||||
in, out := &in.Distribution, &out.Distribution
|
||||
*out = new(Distribution)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Analyze.
|
||||
@@ -693,6 +703,33 @@ func (in *CollectorStatus) DeepCopy() *CollectorStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ContainerRuntime) DeepCopyInto(out *ContainerRuntime) {
|
||||
*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 ContainerRuntime.
|
||||
func (in *ContainerRuntime) DeepCopy() *ContainerRuntime {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ContainerRuntime)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Copy) DeepCopyInto(out *Copy) {
|
||||
*out = *in
|
||||
@@ -768,6 +805,33 @@ func (in *DeploymentStatus) DeepCopy() *DeploymentStatus {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Distribution) DeepCopyInto(out *Distribution) {
|
||||
*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 Distribution.
|
||||
func (in *Distribution) DeepCopy() *Distribution {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Distribution)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Exec) DeepCopyInto(out *Exec) {
|
||||
*out = *in
|
||||
|
||||
@@ -30,6 +30,8 @@ type ClusterResourcesOutput struct {
|
||||
CustomResourceDefinitionsErrors []byte `json:"cluster-resources/custom-resource-definitions-errors.json,omitempty"`
|
||||
ImagePullSecrets map[string][]byte `json:"cluster-resources/image-pull-secrets,omitempty"`
|
||||
ImagePullSecretsErrors []byte `json:"cluster-resources/image-pull-secrets-errors.json,omitempty"`
|
||||
Nodes []byte `json:"cluster-resources/nodes.json,omitempty"`
|
||||
NodesErrors []byte `json:"cluster-resources/nodes-errors.json,omitempty"`
|
||||
}
|
||||
|
||||
func ClusterResources(ctx *Context) ([]byte, error) {
|
||||
@@ -113,6 +115,14 @@ func ClusterResources(ctx *Context) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// nodes
|
||||
nodes, nodeErrors := nodes(client)
|
||||
clusterResourcesOutput.Nodes = nodes
|
||||
clusterResourcesOutput.NodesErrors, err = marshalNonNil(nodeErrors)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ctx.Redact {
|
||||
clusterResourcesOutput, err = clusterResourcesOutput.Redact()
|
||||
if err != nil {
|
||||
@@ -314,11 +324,29 @@ func imagePullSecrets(client *kubernetes.Clientset, namespaces []string) (map[st
|
||||
return imagePullSecrets, errors
|
||||
}
|
||||
|
||||
func nodes(client *kubernetes.Clientset) ([]byte, []string) {
|
||||
nodes, err := client.CoreV1().Nodes().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
b, err := json.MarshalIndent(nodes.Items, "", " ")
|
||||
if err != nil {
|
||||
return nil, []string{err.Error()}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
|
||||
namespaces, err := redact.Redact(c.Namespaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes, err := redact.Redact(c.Nodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pods, err := redactMap(c.Pods)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -346,6 +374,8 @@ func (c *ClusterResourcesOutput) Redact() (*ClusterResourcesOutput, error) {
|
||||
return &ClusterResourcesOutput{
|
||||
Namespaces: namespaces,
|
||||
NamespacesErrors: c.NamespacesErrors,
|
||||
Nodes: nodes,
|
||||
NodesErrors: c.NodesErrors,
|
||||
Pods: pods,
|
||||
PodsErrors: c.PodsErrors,
|
||||
Services: services,
|
||||
|
||||
Reference in New Issue
Block a user