Files
Evans Mungai e156b8c215 chore: Refactor in cluster analysers (#999)
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
2023-02-03 15:26:36 +00:00

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])
}