Skip to content

Commit 5744d60

Browse files
committed
Container runtime analyzer
1 parent 0e3bcd2 commit 5744d60

File tree

9 files changed

+419
-5
lines changed

9 files changed

+419
-5
lines changed

pkg/analyze/analyzer.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,13 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont
3838
return analyzeImagePullSecret(analyzer.ImagePullSecret, findFiles)
3939
}
4040
if analyzer.DeploymentStatus != nil {
41-
return deploymentStatus(analyzer.DeploymentStatus, getFile)
41+
return analyzeDeploymentStatus(analyzer.DeploymentStatus, getFile)
4242
}
4343
if analyzer.StatefulsetStatus != nil {
44-
return statefulsetStatus(analyzer.StatefulsetStatus, getFile)
44+
return analyzeStatefulsetStatus(analyzer.StatefulsetStatus, getFile)
45+
}
46+
if analyzer.ContainerRuntime != nil {
47+
return analyzeContainerRuntime(analyzer.ContainerRuntime, getFile)
4548
}
4649

4750
return nil, errors.New("invalid analyzer")

pkg/analyze/container_runtime.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package analyzer
2+
3+
import (
4+
"encoding/json"
5+
"net/url"
6+
"strings"
7+
8+
"github.com/pkg/errors"
9+
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
10+
corev1 "k8s.io/api/core/v1"
11+
)
12+
13+
func analyzeContainerRuntime(analyzer *troubleshootv1beta1.ContainerRuntime, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) {
14+
collected, err := getCollectedFileContents("cluster-resources/nodes.json")
15+
if err != nil {
16+
return nil, errors.Wrap(err, "failed to get contents of nodes.json")
17+
}
18+
19+
var nodes []corev1.Node
20+
if err := json.Unmarshal(collected, &nodes); err != nil {
21+
return nil, errors.Wrap(err, "failed to unmarshal node list")
22+
}
23+
24+
foundRuntimes := []string{}
25+
for _, node := range nodes {
26+
foundRuntimes = append(foundRuntimes, node.Status.NodeInfo.ContainerRuntimeVersion)
27+
}
28+
29+
result := &AnalyzeResult{
30+
Title: "Container Runtime",
31+
}
32+
33+
// ordering is important for passthrough
34+
for _, outcome := range analyzer.Outcomes {
35+
if outcome.Fail != nil {
36+
if outcome.Fail.When == "" {
37+
result.IsFail = true
38+
result.Message = outcome.Fail.Message
39+
result.URI = outcome.Fail.URI
40+
41+
return result, nil
42+
}
43+
44+
for _, foundRuntime := range foundRuntimes {
45+
isMatch, err := compareRuntimeConditionalToActual(outcome.Fail.When, foundRuntime)
46+
if err != nil {
47+
return nil, errors.Wrap(err, "failed to compare runtime conditional")
48+
}
49+
50+
if isMatch {
51+
result.IsFail = true
52+
result.Message = outcome.Fail.Message
53+
result.URI = outcome.Fail.URI
54+
55+
return result, nil
56+
}
57+
}
58+
} else if outcome.Warn != nil {
59+
if outcome.Warn.When == "" {
60+
result.IsWarn = true
61+
result.Message = outcome.Warn.Message
62+
result.URI = outcome.Warn.URI
63+
64+
return result, nil
65+
}
66+
67+
for _, foundRuntime := range foundRuntimes {
68+
isMatch, err := compareRuntimeConditionalToActual(outcome.Warn.When, foundRuntime)
69+
if err != nil {
70+
return nil, errors.Wrap(err, "failed to compare runtime conditional")
71+
}
72+
73+
if isMatch {
74+
result.IsWarn = true
75+
result.Message = outcome.Warn.Message
76+
result.URI = outcome.Warn.URI
77+
78+
return result, nil
79+
}
80+
}
81+
} else if outcome.Pass != nil {
82+
if outcome.Pass.When == "" {
83+
result.IsPass = true
84+
result.Message = outcome.Pass.Message
85+
result.URI = outcome.Pass.URI
86+
87+
return result, nil
88+
}
89+
90+
for _, foundRuntime := range foundRuntimes {
91+
isMatch, err := compareRuntimeConditionalToActual(outcome.Pass.When, foundRuntime)
92+
if err != nil {
93+
return nil, errors.Wrap(err, "failed to compare runtime conditional")
94+
}
95+
96+
if isMatch {
97+
result.IsPass = true
98+
result.Message = outcome.Pass.Message
99+
result.URI = outcome.Pass.URI
100+
101+
return result, nil
102+
}
103+
}
104+
}
105+
}
106+
107+
return result, nil
108+
}
109+
110+
func compareRuntimeConditionalToActual(conditional string, actual string) (bool, error) {
111+
parts := strings.Split(strings.TrimSpace(conditional), " ")
112+
113+
// we can make this a lot more flexible
114+
if len(parts) != 2 {
115+
return false, errors.New("unable to parse conditional")
116+
}
117+
118+
parsedRuntime, err := url.Parse(actual)
119+
if err != nil {
120+
return false, errors.New("unable to parse url")
121+
}
122+
123+
switch parts[0] {
124+
case "=":
125+
fallthrough
126+
case "==":
127+
fallthrough
128+
case "===":
129+
return parts[1] == parsedRuntime.Scheme, nil
130+
}
131+
return false, nil
132+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package analyzer
2+
3+
import (
4+
"testing"
5+
6+
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func Test_compareRuntimeConditionalToActual(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
conditional string
15+
actual string
16+
expected bool
17+
}{
18+
{
19+
name: "containerd://1.2.5 = containerd",
20+
conditional: "= containerd",
21+
actual: "containerd://1.2.5",
22+
expected: true,
23+
},
24+
{
25+
name: "containerd://1.2.5 == containerd",
26+
conditional: "== containerd",
27+
actual: "containerd://1.2.5",
28+
expected: true,
29+
},
30+
{
31+
name: "containerd://1.2.5 === containerd",
32+
conditional: "=== containerd",
33+
actual: "containerd://1.2.5",
34+
expected: true,
35+
},
36+
{
37+
name: "containerd://1.2.5 != containerd",
38+
conditional: "!= containerd",
39+
actual: "containerd://1.2.5",
40+
expected: false,
41+
},
42+
{
43+
name: "containerd://1.2.5 !== containerd",
44+
conditional: "!== containerd",
45+
actual: "containerd://1.2.5",
46+
expected: false,
47+
},
48+
{
49+
name: "containerd://1.2.5 !== containerd",
50+
conditional: "!=== containerd",
51+
actual: "containerd://1.2.5",
52+
expected: false,
53+
},
54+
}
55+
56+
for _, test := range tests {
57+
t.Run(test.name, func(t *testing.T) {
58+
req := require.New(t)
59+
60+
actual, err := compareRuntimeConditionalToActual(test.conditional, test.actual)
61+
req.NoError(err)
62+
63+
assert.Equal(t, test.expected, actual)
64+
65+
})
66+
}
67+
}
68+
69+
func Test_containerRuntime(t *testing.T) {
70+
tests := []struct {
71+
name string
72+
analyzer troubleshootv1beta1.ContainerRuntime
73+
expectResult AnalyzeResult
74+
files map[string][]byte
75+
}{
76+
{
77+
name: "no containerd, when it's containerd",
78+
analyzer: troubleshootv1beta1.ContainerRuntime{
79+
Outcomes: []*troubleshootv1beta1.Outcome{
80+
{
81+
Pass: &troubleshootv1beta1.SingleOutcome{
82+
When: "!= containerd",
83+
Message: "pass",
84+
},
85+
},
86+
{
87+
Fail: &troubleshootv1beta1.SingleOutcome{
88+
Message: "containerd detected",
89+
},
90+
},
91+
},
92+
},
93+
expectResult: AnalyzeResult{
94+
IsPass: false,
95+
IsWarn: false,
96+
IsFail: true,
97+
Title: "Container Runtime",
98+
Message: "containerd detected",
99+
},
100+
files: map[string][]byte{
101+
"cluster-resources/nodes.json": []byte(collectedNodes),
102+
},
103+
},
104+
}
105+
106+
for _, test := range tests {
107+
t.Run(test.name, func(t *testing.T) {
108+
req := require.New(t)
109+
110+
getFiles := func(n string) ([]byte, error) {
111+
return test.files[n], nil
112+
}
113+
114+
actual, err := analyzeContainerRuntime(&test.analyzer, getFiles)
115+
req.NoError(err)
116+
117+
assert.Equal(t, &test.expectResult, actual)
118+
119+
})
120+
}
121+
}

pkg/analyze/data_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,3 +452,123 @@ var collectedDeployments = `[
452452
}
453453
}
454454
]`
455+
456+
var collectedNodes = `[
457+
{
458+
"apiVersion": "v1",
459+
"kind": "Node",
460+
"metadata": {
461+
"annotations": {
462+
"node.alpha.kubernetes.io/ttl": "0",
463+
"volumes.kubernetes.io/controller-managed-attach-detach": "true"
464+
},
465+
"creationTimestamp": "2019-10-23T18:16:43Z",
466+
"labels": {
467+
"beta.kubernetes.io/arch": "amd64",
468+
"beta.kubernetes.io/os": "linux",
469+
"kubernetes.io/arch": "amd64",
470+
"kubernetes.io/hostname": "repldev-marc",
471+
"kubernetes.io/os": "linux",
472+
"microk8s.io/cluster": "true"
473+
},
474+
"name": "repldev-marc",
475+
"resourceVersion": "1769699",
476+
"selfLink": "/api/v1/nodes/repldev-marc",
477+
"uid": "cd30c57f-b445-437f-9473-f13343124030"
478+
},
479+
"spec": {},
480+
"status": {
481+
"addresses": [
482+
{
483+
"address": "10.168.0.26",
484+
"type": "InternalIP"
485+
},
486+
{
487+
"address": "repldev-marc",
488+
"type": "Hostname"
489+
}
490+
],
491+
"allocatable": {
492+
"cpu": "8",
493+
"ephemeral-storage": "1015018628Ki",
494+
"hugepages-1Gi": "0",
495+
"hugepages-2Mi": "0",
496+
"memory": "30770604Ki",
497+
"pods": "110"
498+
},
499+
"capacity": {
500+
"cpu": "8",
501+
"ephemeral-storage": "1016067204Ki",
502+
"hugepages-1Gi": "0",
503+
"hugepages-2Mi": "0",
504+
"memory": "30873004Ki",
505+
"pods": "110"
506+
},
507+
"conditions": [
508+
{
509+
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
510+
"lastTransitionTime": "2019-10-31T21:28:36Z",
511+
"message": "kubelet has sufficient memory available",
512+
"reason": "KubeletHasSufficientMemory",
513+
"status": "False",
514+
"type": "MemoryPressure"
515+
},
516+
{
517+
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
518+
"lastTransitionTime": "2019-10-31T21:28:36Z",
519+
"message": "kubelet has no disk pressure",
520+
"reason": "KubeletHasNoDiskPressure",
521+
"status": "False",
522+
"type": "DiskPressure"
523+
},
524+
{
525+
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
526+
"lastTransitionTime": "2019-10-31T21:28:36Z",
527+
"message": "kubelet has sufficient PID available",
528+
"reason": "KubeletHasSufficientPID",
529+
"status": "False",
530+
"type": "PIDPressure"
531+
},
532+
{
533+
"lastHeartbeatTime": "2019-11-08T17:03:39Z",
534+
"lastTransitionTime": "2019-10-31T21:28:36Z",
535+
"message": "kubelet is posting ready status. AppArmor enabled",
536+
"reason": "KubeletReady",
537+
"status": "True",
538+
"type": "Ready"
539+
}
540+
],
541+
"daemonEndpoints": {
542+
"kubeletEndpoint": {
543+
"Port": 10250
544+
}
545+
},
546+
"images": [
547+
{
548+
"names": [
549+
"localhost:32000/kotsadm-api@sha256:d4821b65869454dfac53ad01f295740df6fcd52711f0dcf6aa9d7e515f7ebe3c"
550+
],
551+
"sizeBytes": 755312372
552+
},
553+
{
554+
"names": [
555+
"localhost:32000/kotsadm-api@sha256:fc3c971facc9dbd1b07e19c1ebb33c6361dd219af8efed0616afd1278f81fa4e"
556+
],
557+
"sizeBytes": 755312032
558+
}
559+
],
560+
"nodeInfo": {
561+
"architecture": "amd64",
562+
"bootID": "3401cdf2-129c-473d-a50c-723afd7378d3",
563+
"containerRuntimeVersion": "containerd://1.2.5",
564+
"kernelVersion": "5.0.0-1021-gcp",
565+
"kubeProxyVersion": "v1.16.2",
566+
"kubeletVersion": "v1.16.2",
567+
"machineID": "97f4a34d2aa9e26785177a6b64fb9108",
568+
"operatingSystem": "linux",
569+
"osImage": "Ubuntu 18.04.2 LTS",
570+
"systemUUID": "9dc594e5-ac7b-c649-e61f-cad715a28f79"
571+
}
572+
}
573+
}
574+
]`

0 commit comments

Comments
 (0)