mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
Have all in-cluster analysers implement the same interface. This will help with the implementation of code that requires making calls to all analysers Fixes #995
296 lines
8.1 KiB
Go
296 lines
8.1 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
|
|
"github.com/replicatedhq/troubleshoot/pkg/constants"
|
|
batchv1 "k8s.io/api/batch/v1"
|
|
)
|
|
|
|
type AnalyzeJobStatus struct {
|
|
analyzer *troubleshootv1beta2.JobStatus
|
|
}
|
|
|
|
func (a *AnalyzeJobStatus) Title() string {
|
|
if a.analyzer.CheckName != "" {
|
|
return a.analyzer.CheckName
|
|
}
|
|
|
|
if a.analyzer.Name != "" && a.analyzer.Namespace != "" {
|
|
return fmt.Sprintf("%s/%s Job Status", a.analyzer.Namespace, a.analyzer.Name)
|
|
}
|
|
|
|
if a.analyzer.Name != "" {
|
|
return fmt.Sprintf("%s Job Status", a.analyzer.Name)
|
|
}
|
|
if a.analyzer.Namespace != "" {
|
|
return fmt.Sprintf("%s Job Status", a.analyzer.Namespace)
|
|
}
|
|
|
|
return "Job Status"
|
|
}
|
|
|
|
func (a *AnalyzeJobStatus) IsExcluded() (bool, error) {
|
|
return isExcluded(a.analyzer.Exclude)
|
|
}
|
|
|
|
func (a *AnalyzeJobStatus) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
results, err := analyzeJobStatus(a.analyzer, findFiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range results {
|
|
results[i].Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func analyzeJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
if analyzer.Name == "" {
|
|
return analyzeAllJobStatuses(analyzer, getFileContents)
|
|
} else {
|
|
return analyzeOneJobStatus(analyzer, getFileContents)
|
|
}
|
|
}
|
|
|
|
func analyzeOneJobStatus(analyzer *troubleshootv1beta2.JobStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
excludeFiles := []string{}
|
|
files, err := getFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, fmt.Sprintf("%s.json", analyzer.Namespace)), excludeFiles)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read collected jobs from namespace")
|
|
}
|
|
|
|
var result *AnalyzeResult
|
|
for _, collected := range files { // only 1 file here
|
|
var jobs batchv1.JobList
|
|
if err := json.Unmarshal(collected, &jobs); err != nil {
|
|
return nil, errors.Wrap(err, "failed to unmarshal job list")
|
|
}
|
|
|
|
var job *batchv1.Job
|
|
for _, j := range jobs.Items {
|
|
if j.Name == analyzer.Name {
|
|
job = j.DeepCopy()
|
|
break
|
|
}
|
|
}
|
|
|
|
if job == nil {
|
|
// there's not an error, but maybe the requested deployment is not even deployed
|
|
result = &AnalyzeResult{
|
|
Title: fmt.Sprintf("%s Job Status", analyzer.Name),
|
|
IconKey: "kubernetes_deployment_status", // TODO: need new icon
|
|
IconURI: "https://troubleshoot.sh/images/analyzer-icons/deployment-status.svg?w=17&h=17", // TODO: need new icon
|
|
IsFail: true,
|
|
Message: fmt.Sprintf("The job %q was not found", analyzer.Name),
|
|
}
|
|
} else if len(analyzer.Outcomes) > 0 {
|
|
result, err = jobStatus(analyzer.Outcomes, job)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to process status")
|
|
}
|
|
} else {
|
|
result = getDefaultJobResult(job)
|
|
}
|
|
}
|
|
|
|
if result == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return []*AnalyzeResult{result}, nil
|
|
}
|
|
|
|
func analyzeAllJobStatuses(analyzer *troubleshootv1beta2.JobStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
fileNames := make([]string, 0)
|
|
if analyzer.Namespace != "" {
|
|
fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, fmt.Sprintf("%s.json", analyzer.Namespace)))
|
|
}
|
|
for _, ns := range analyzer.Namespaces {
|
|
fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, fmt.Sprintf("%s.json", ns)))
|
|
}
|
|
|
|
// no namespace specified, so we need to analyze all jobs
|
|
if len(analyzer.Namespaces) == 0 {
|
|
fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_JOBS, "*.json"))
|
|
}
|
|
|
|
excludeFiles := []string{}
|
|
results := []*AnalyzeResult{}
|
|
for _, fileName := range fileNames {
|
|
files, err := getFileContents(fileName, excludeFiles)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read collected jobs from file")
|
|
}
|
|
|
|
for _, collected := range files {
|
|
var jobs batchv1.JobList
|
|
if err := json.Unmarshal(collected, &jobs); err != nil {
|
|
return nil, errors.Wrap(err, "failed to unmarshal job list")
|
|
}
|
|
|
|
for _, job := range jobs.Items {
|
|
result := getDefaultJobResult(&job)
|
|
if result != nil {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func jobStatus(outcomes []*troubleshootv1beta2.Outcome, job *batchv1.Job) (*AnalyzeResult, error) {
|
|
result := &AnalyzeResult{
|
|
Title: fmt.Sprintf("%s Status", job.Name),
|
|
IconKey: "kubernetes_deployment_status", // TODO: needs new icon
|
|
IconURI: "https://troubleshoot.sh/images/analyzer-icons/deployment-status.svg?w=17&h=17", // TODO: needs new icon
|
|
}
|
|
|
|
// ordering from the spec is important, the first one that matches returns
|
|
for _, outcome := range 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
|
|
}
|
|
|
|
match, err := compareJobStatusToWhen(outcome.Fail.When, job)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse fail range")
|
|
}
|
|
|
|
if match {
|
|
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
|
|
}
|
|
|
|
match, err := compareJobStatusToWhen(outcome.Warn.When, job)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse warn range")
|
|
}
|
|
|
|
if match {
|
|
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
|
|
}
|
|
|
|
match, err := compareJobStatusToWhen(outcome.Pass.When, job)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse pass range")
|
|
}
|
|
|
|
if match {
|
|
result.IsPass = true
|
|
result.Message = outcome.Pass.Message
|
|
result.URI = outcome.Pass.URI
|
|
|
|
return result, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func getDefaultJobResult(job *batchv1.Job) *AnalyzeResult {
|
|
if job.Spec.Completions == nil && job.Status.Succeeded > 1 {
|
|
return nil
|
|
}
|
|
|
|
if job.Spec.Completions != nil && *job.Spec.Completions == job.Status.Succeeded {
|
|
return nil
|
|
}
|
|
|
|
if job.Status.Failed == 0 {
|
|
return nil
|
|
}
|
|
|
|
return &AnalyzeResult{
|
|
Title: fmt.Sprintf("%s/%s Job Status", job.Namespace, job.Name),
|
|
IconKey: "kubernetes_deployment_status",
|
|
IconURI: "https://troubleshoot.sh/images/analyzer-icons/deployment-status.svg?w=17&h=17",
|
|
IsFail: true,
|
|
Message: fmt.Sprintf("The job %s/%s is not complete", job.Namespace, job.Name),
|
|
}
|
|
}
|
|
|
|
func compareJobStatusToWhen(when string, job *batchv1.Job) (bool, error) {
|
|
parts := strings.Split(strings.TrimSpace(when), " ")
|
|
|
|
// we can make this a lot more flexible
|
|
if len(parts) != 3 {
|
|
return false, errors.Errorf("unable to parse when range: %s", when)
|
|
}
|
|
|
|
value, err := strconv.Atoi(parts[2])
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to parse when value: %s", parts[2])
|
|
}
|
|
|
|
var actual int32
|
|
switch parts[0] {
|
|
case "succeeded":
|
|
actual = job.Status.Succeeded
|
|
case "failed":
|
|
actual = job.Status.Failed
|
|
default:
|
|
return false, errors.Errorf("unknown when value: %s", parts[0])
|
|
}
|
|
|
|
switch parts[1] {
|
|
case "=":
|
|
fallthrough
|
|
case "==":
|
|
fallthrough
|
|
case "===":
|
|
return actual == int32(value), nil
|
|
|
|
case "<":
|
|
return actual < int32(value), nil
|
|
|
|
case ">":
|
|
return actual > int32(value), nil
|
|
|
|
case "<=":
|
|
return actual <= int32(value), nil
|
|
|
|
case ">=":
|
|
return actual >= int32(value), nil
|
|
}
|
|
|
|
return false, errors.Errorf("unknown comparator: %q", parts[1])
|
|
}
|