From ed8453ce6bd2f2cf72ac552cc2ac7f11a9c51ac5 Mon Sep 17 00:00:00 2001 From: Marc Campbell Date: Fri, 8 Nov 2019 22:35:04 +0000 Subject: [PATCH] Distribution analyzer --- ...troubleshoot.replicated.com_analyzers.yaml | 78 +++++++ ...roubleshoot.replicated.com_preflights.yaml | 78 +++++++ config/crds/zz_generated.deepcopy.go | 64 ++++++ examples/troubleshoot/sample-analyzers.yaml | 22 +- pkg/analyze/analyzer.go | 3 + pkg/analyze/deployment_status.go | 2 +- pkg/analyze/distribution.go | 199 ++++++++++++++++++ pkg/analyze/distribution_test.go | 85 ++++++++ pkg/analyze/statefulset_status.go | 2 +- .../troubleshoot/v1beta1/analyzer_shared.go | 6 + .../v1beta1/zz_generated.deepcopy.go | 32 +++ 11 files changed, 559 insertions(+), 12 deletions(-) create mode 100644 pkg/analyze/distribution.go create mode 100644 pkg/analyze/distribution_test.go diff --git a/config/crds/troubleshoot.replicated.com_analyzers.yaml b/config/crds/troubleshoot.replicated.com_analyzers.yaml index 02113e4d5..552de1422 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 c0856d79b..2146cf2c4 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 b77842a04..8dbd570b8 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 58a3453fc..a0e726b46 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/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index 14a1c3a7b..cc4aa4078 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -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") } diff --git a/pkg/analyze/deployment_status.go b/pkg/analyze/deployment_status.go index f90679f90..8a05b8d18 100644 --- a/pkg/analyze/deployment_status.go +++ b/pkg/analyze/deployment_status.go @@ -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 } diff --git a/pkg/analyze/distribution.go b/pkg/analyze/distribution.go new file mode 100644 index 000000000..54225a90f --- /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 000000000..d6353748b --- /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 db0d74d32..748935411 100644 --- a/pkg/analyze/statefulset_status.go +++ b/pkg/analyze/statefulset_status.go @@ -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 } diff --git a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go index 91859a76e..f7706fa01 100644 --- a/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta1/analyzer_shared.go @@ -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"` } diff --git a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go index 0cbcd1566..3b2950e86 100644 --- a/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go @@ -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