diff --git a/config/crds/troubleshoot.sh_analyzers.yaml b/config/crds/troubleshoot.sh_analyzers.yaml index 57d12ec7..afeceb48 100644 --- a/config/crds/troubleshoot.sh_analyzers.yaml +++ b/config/crds/troubleshoot.sh_analyzers.yaml @@ -85,6 +85,52 @@ spec: - namespace - outcomes type: object + clusterPodStatuses: + properties: + checkName: + type: string + exclude: + type: BoolString + namespaces: + items: + type: string + type: array + 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: + - namespaces + - outcomes + type: object clusterVersion: properties: checkName: diff --git a/config/crds/troubleshoot.sh_preflights.yaml b/config/crds/troubleshoot.sh_preflights.yaml index 95c0f37d..50972b0a 100644 --- a/config/crds/troubleshoot.sh_preflights.yaml +++ b/config/crds/troubleshoot.sh_preflights.yaml @@ -85,6 +85,52 @@ spec: - namespace - outcomes type: object + clusterPodStatuses: + properties: + checkName: + type: string + exclude: + type: BoolString + namespaces: + items: + type: string + type: array + 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: + - namespaces + - outcomes + type: object clusterVersion: properties: checkName: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index ce340d0d..b0ac84f8 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -116,6 +116,52 @@ spec: - namespace - outcomes type: object + clusterPodStatuses: + properties: + checkName: + type: string + exclude: + type: BoolString + namespaces: + items: + type: string + type: array + 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: + - namespaces + - outcomes + type: object clusterVersion: properties: checkName: diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index e1d48b92..a223200c 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -205,6 +205,20 @@ func Analyze(analyzer *troubleshootv1beta2.Analyze, getFile getCollectedFileCont } return []*AnalyzeResult{result}, nil } + if analyzer.ClusterPodStatuses != nil { + isExcluded, err := isExcluded(analyzer.ClusterPodStatuses.Exclude) + if err != nil { + return nil, err + } + if isExcluded { + return nil, nil + } + results, err := clusterPodStatuses(analyzer.ClusterPodStatuses, findFiles) + if err != nil { + return nil, err + } + return results, nil + } if analyzer.ContainerRuntime != nil { isExcluded, err := isExcluded(analyzer.ContainerRuntime.Exclude) if err != nil { diff --git a/pkg/analyze/cluster_pod_statuses.go b/pkg/analyze/cluster_pod_statuses.go new file mode 100644 index 00000000..e6572c0f --- /dev/null +++ b/pkg/analyze/cluster_pod_statuses.go @@ -0,0 +1,130 @@ +package analyzer + +import ( + "bytes" + "encoding/json" + "fmt" + "path/filepath" + "strings" + "text/template" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + corev1 "k8s.io/api/core/v1" +) + +func clusterPodStatuses(analyzer *troubleshootv1beta2.ClusterPodStatuses, getChildCollectedFileContents func(string) (map[string][]byte, error)) ([]*AnalyzeResult, error) { + collected, err := getChildCollectedFileContents(filepath.Join("cluster-resources", "pods", "*.json")) + if err != nil { + return nil, errors.Wrap(err, "failed to read collected pods") + } + + var pods []corev1.Pod + for fileName, fileContent := range collected { + podsNs := strings.TrimSuffix(fileName, ".json") + include := len(analyzer.Namespaces) == 0 + for _, ns := range analyzer.Namespaces { + if ns == podsNs { + include = true + break + } + } + if include { + var nsPods []corev1.Pod + if err := json.Unmarshal(fileContent, &nsPods); err != nil { + return nil, errors.Wrapf(err, "failed to unmarshal pods list for namespace %s", podsNs) + } + pods = append(pods, nsPods...) + } + } + + allResults := []*AnalyzeResult{} + + for _, pod := range pods { + podResults := []*AnalyzeResult{} + + for _, outcome := range analyzer.Outcomes { + r := AnalyzeResult{} + when := "" + + if outcome.Fail != nil { + r.IsFail = true + r.Message = outcome.Fail.Message + r.URI = outcome.Fail.URI + when = outcome.Fail.When + } else if outcome.Warn != nil { + r.IsWarn = true + r.Message = outcome.Warn.Message + r.URI = outcome.Warn.URI + when = outcome.Warn.When + } else if outcome.Pass != nil { + r.IsPass = true + r.Message = outcome.Pass.Message + r.URI = outcome.Pass.URI + when = outcome.Pass.When + } else { + fmt.Println("error: found an empty outcome in a clusterPodStatuses analyzer") // don't stop + continue + } + + parts := strings.Split(strings.TrimSpace(when), " ") + if len(parts) < 2 { + fmt.Printf("invalid 'when' format: %s\n", when) // don't stop + continue + } + + match := false + switch parts[0] { + case "=", "==", "===": + match = parts[1] == string(pod.Status.Phase) + case "!=", "!==": + match = parts[1] != string(pod.Status.Phase) + } + + if !match { + continue + } + + r.Title = analyzer.CheckName + if r.Title == "" { + r.Title = "Pod {{ .Name }} status" + } + + if r.Message == "" { + r.Message = "Pod {{ .Name }} status is {{ .Status.Phase }}" + } + + tmpl := template.New("pod") + + // template the title + titleTmpl, err := tmpl.Parse(r.Title) + if err != nil { + return nil, errors.Wrap(err, "failed to create new title template") + } + var t bytes.Buffer + err = titleTmpl.Execute(&t, pod) + if err != nil { + return nil, errors.Wrap(err, "failed to execute template") + } + r.Title = t.String() + + // template the message + msgTmpl, err := tmpl.Parse(r.Message) + if err != nil { + return nil, errors.Wrap(err, "failed to create new title template") + } + var m bytes.Buffer + err = msgTmpl.Execute(&m, pod) + if err != nil { + return nil, errors.Wrap(err, "failed to execute template") + } + r.Message = m.String() + + podResults = append(podResults, &r) + } + + allResults = append(allResults, podResults...) + } + + return allResults, nil +} diff --git a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go index 9249d9f9..7dda948d 100644 --- a/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/analyzer_shared.go @@ -63,6 +63,12 @@ type StatefulsetStatus struct { Name string `json:"name" yaml:"name"` } +type ClusterPodStatuses struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` + Namespaces []string `json:"namespaces" yaml:"namespaces"` +} + type ContainerRuntime struct { AnalyzeMeta `json:",inline" yaml:",inline"` Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` @@ -162,6 +168,7 @@ 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"` + ClusterPodStatuses *ClusterPodStatuses `json:"clusterPodStatuses,omitempty" yaml:"clusterPodStatuses,omitempty"` ContainerRuntime *ContainerRuntime `json:"containerRuntime,omitempty" yaml:"containerRuntime,omitempty"` Distribution *Distribution `json:"distribution,omitempty" yaml:"distribution,omitempty"` NodeResources *NodeResources `json:"nodeResources,omitempty" yaml:"nodeResources,omitempty"` diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 4e1721de..b2b8b4a7 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -97,6 +97,11 @@ func (in *Analyze) DeepCopyInto(out *Analyze) { *out = new(StatefulsetStatus) (*in).DeepCopyInto(*out) } + if in.ClusterPodStatuses != nil { + in, out := &in.ClusterPodStatuses, &out.ClusterPodStatuses + *out = new(ClusterPodStatuses) + (*in).DeepCopyInto(*out) + } if in.ContainerRuntime != nil { in, out := &in.ContainerRuntime, &out.ContainerRuntime *out = new(ContainerRuntime) @@ -511,6 +516,38 @@ func (in *ClusterInfo) DeepCopy() *ClusterInfo { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterPodStatuses) DeepCopyInto(out *ClusterPodStatuses) { + *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) + } + } + } + if in.Namespaces != nil { + in, out := &in.Namespaces, &out.Namespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterPodStatuses. +func (in *ClusterPodStatuses) DeepCopy() *ClusterPodStatuses { + if in == nil { + return nil + } + out := new(ClusterPodStatuses) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterResources) DeepCopyInto(out *ClusterResources) { *out = *in diff --git a/schemas/analyzer-troubleshoot-v1beta2.json b/schemas/analyzer-troubleshoot-v1beta2.json index 5289eecc..e6c2a8f0 100644 --- a/schemas/analyzer-troubleshoot-v1beta2.json +++ b/schemas/analyzer-troubleshoot-v1beta2.json @@ -93,6 +93,77 @@ } } }, + "clusterPodStatuses": { + "type": "object", + "required": [ + "namespaces", + "outcomes" + ], + "properties": { + "checkName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcomes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fail": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "pass": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "warn": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + } + } + } + } + } + }, "clusterVersion": { "type": "object", "required": [ diff --git a/schemas/preflight-troubleshoot-v1beta2.json b/schemas/preflight-troubleshoot-v1beta2.json index e2a81f11..b2f32204 100644 --- a/schemas/preflight-troubleshoot-v1beta2.json +++ b/schemas/preflight-troubleshoot-v1beta2.json @@ -93,6 +93,77 @@ } } }, + "clusterPodStatuses": { + "type": "object", + "required": [ + "namespaces", + "outcomes" + ], + "properties": { + "checkName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcomes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fail": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "pass": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "warn": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + } + } + } + } + } + }, "clusterVersion": { "type": "object", "required": [ diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 1deba44a..59c8101b 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -139,6 +139,77 @@ } } }, + "clusterPodStatuses": { + "type": "object", + "required": [ + "namespaces", + "outcomes" + ], + "properties": { + "checkName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcomes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fail": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "pass": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + }, + "warn": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "when": { + "type": "string" + } + } + } + } + } + } + } + }, "clusterVersion": { "type": "object", "required": [