Merge pull request #81 from replicatedhq/collector-node

Collector node
This commit is contained in:
Marc Campbell
2019-11-08 15:29:03 -08:00
committed by GitHub
17 changed files with 1009 additions and 41 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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

View File

@@ -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"

View File

@@ -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: []

View File

@@ -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")

View 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
}

View 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)
})
}
}

View File

@@ -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"
}
}
}
]`

View File

@@ -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
}

View File

@@ -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
View 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
}

View 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)
})
}
}

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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

View File

@@ -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,