diff --git a/config/crds/troubleshoot.replicated.com_analyzers.yaml b/config/crds/troubleshoot.replicated.com_analyzers.yaml index 02113e4d..552de142 100644 --- a/config/crds/troubleshoot.replicated.com_analyzers.yaml +++ b/config/crds/troubleshoot.replicated.com_analyzers.yaml @@ -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: diff --git a/config/crds/troubleshoot.replicated.com_preflights.yaml b/config/crds/troubleshoot.replicated.com_preflights.yaml index c0856d79..2146cf2c 100644 --- a/config/crds/troubleshoot.replicated.com_preflights.yaml +++ b/config/crds/troubleshoot.replicated.com_preflights.yaml @@ -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: diff --git a/config/crds/zz_generated.deepcopy.go b/config/crds/zz_generated.deepcopy.go index b77842a0..8dbd570b 100644 --- a/config/crds/zz_generated.deepcopy.go +++ b/config/crds/zz_generated.deepcopy.go @@ -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 diff --git a/examples/troubleshoot/sample-analyzers.yaml b/examples/troubleshoot/sample-analyzers.yaml index 58a3453f..a0e726b4 100644 --- a/examples/troubleshoot/sample-analyzers.yaml +++ b/examples/troubleshoot/sample-analyzers.yaml @@ -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" + + diff --git a/examples/troubleshoot/sample-troubleshoot.yaml b/examples/troubleshoot/sample-troubleshoot.yaml index e7abd28c..0d57c3c1 100644 --- a/examples/troubleshoot/sample-troubleshoot.yaml +++ b/examples/troubleshoot/sample-troubleshoot.yaml @@ -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: [] diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index 20add774..cc4aa407 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -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") diff --git a/pkg/analyze/container_runtime.go b/pkg/analyze/container_runtime.go new file mode 100644 index 00000000..e7ae3467 --- /dev/null +++ b/pkg/analyze/container_runtime.go @@ -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 +} diff --git a/pkg/analyze/container_runtime_test.go b/pkg/analyze/container_runtime_test.go new file mode 100644 index 00000000..1119e921 --- /dev/null +++ b/pkg/analyze/container_runtime_test.go @@ -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) + + }) + } +} diff --git a/pkg/analyze/data_test.go b/pkg/analyze/data_test.go index b0464421..b847c3c6 100644 --- a/pkg/analyze/data_test.go +++ b/pkg/analyze/data_test.go @@ -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" + } + } + } +]` diff --git a/pkg/analyze/deployment_status.go b/pkg/analyze/deployment_status.go index 0314cc02..8a05b8d1 100644 --- a/pkg/analyze/deployment_status.go +++ b/pkg/analyze/deployment_status.go @@ -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 } diff --git a/pkg/analyze/deployment_status_test.go b/pkg/analyze/deployment_status_test.go index 33c84fb3..6b58f034 100644 --- a/pkg/analyze/deployment_status_test.go +++ b/pkg/analyze/deployment_status_test.go @@ -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) diff --git a/pkg/analyze/distribution.go b/pkg/analyze/distribution.go new file mode 100644 index 00000000..54225a90 --- /dev/null +++ b/pkg/analyze/distribution.go @@ -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 +} diff --git a/pkg/analyze/distribution_test.go b/pkg/analyze/distribution_test.go new file mode 100644 index 00000000..d6353748 --- /dev/null +++ b/pkg/analyze/distribution_test.go @@ -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) + }) + } +} diff --git a/pkg/analyze/statefulset_status.go b/pkg/analyze/statefulset_status.go index 0c7b419c..74893541 100644 --- a/pkg/analyze/statefulset_status.go +++ b/pkg/analyze/statefulset_status.go @@ -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 } diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go index c9d28d3a..f7706fa0 100644 --- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go @@ -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"` } diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go index 443d258b..3b2950e8 100644 --- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go @@ -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 diff --git a/pkg/collect/cluster_resources.go b/pkg/collect/cluster_resources.go index db8bd7cc..c7ac1da1 100644 --- a/pkg/collect/cluster_resources.go +++ b/pkg/collect/cluster_resources.go @@ -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,