Distribution analyzer

This commit is contained in:
Marc Campbell
2019-11-08 23:15:58 +00:00
parent 5744d608a4
commit ed8453ce6b
11 changed files with 559 additions and 12 deletions
@@ -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:
+64
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
+12 -10
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"
+3
View File
@@ -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")
}
+1 -1
View File
@@ -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
}
+199
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
}
+85
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)
})
}
}
+1 -1
View File
@@ -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