mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
Distribution analyzer
This commit is contained in:
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -46,6 +46,9 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont
|
||||
if analyzer.ContainerRuntime != nil {
|
||||
return analyzeContainerRuntime(analyzer.ContainerRuntime, getFile)
|
||||
}
|
||||
if analyzer.Distribution != nil {
|
||||
return analyzeDistribution(analyzer.Distribution, getFile)
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid analyzer")
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func analyzeDeploymentStatus(analyzer *troubleshootv1beta1.DeploymentStatus, get
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ func analyzeStatefulsetStatus(analyzer *troubleshootv1beta1.StatefulsetStatus, g
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@ type ContainerRuntime struct {
|
||||
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"`
|
||||
}
|
||||
@@ -83,4 +88,5 @@ type Analyze struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -97,6 +97,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) {
|
||||
*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.
|
||||
@@ -800,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
|
||||
|
||||
Reference in New Issue
Block a user