mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
* feat(analyzer): make sure ReplicaSetStatus has valid result --------- Co-authored-by: Gerard Nguyen <gerard@replicated.com>
309 lines
9.1 KiB
Go
309 lines
9.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"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
)
|
|
|
|
type AnalyzeReplicaSetStatus struct {
|
|
analyzer *troubleshootv1beta2.ReplicaSetStatus
|
|
}
|
|
|
|
func (a *AnalyzeReplicaSetStatus) Title() string {
|
|
if a.analyzer.CheckName != "" {
|
|
return a.analyzer.CheckName
|
|
}
|
|
|
|
if a.analyzer.Name != "" && a.analyzer.Namespace != "" {
|
|
return fmt.Sprintf("%s/%s ReplicaSet Status", a.analyzer.Namespace, a.analyzer.Name)
|
|
}
|
|
|
|
if a.analyzer.Name != "" {
|
|
return fmt.Sprintf("%s ReplicaSet Status", a.analyzer.Name)
|
|
}
|
|
if a.analyzer.Namespace != "" {
|
|
return fmt.Sprintf("%s ReplicaSet Status", a.analyzer.Namespace)
|
|
}
|
|
|
|
return "ReplicaSet Status"
|
|
}
|
|
|
|
func (a *AnalyzeReplicaSetStatus) IsExcluded() (bool, error) {
|
|
return isExcluded(a.analyzer.Exclude)
|
|
}
|
|
|
|
func (a *AnalyzeReplicaSetStatus) Analyze(getFile getCollectedFileContents, findFiles getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
results, err := analyzeReplicaSetStatus(a.analyzer, findFiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range results {
|
|
results[i].Strict = a.analyzer.Strict.BoolOrDefaultFalse()
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
func analyzeReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
if analyzer.Name == "" {
|
|
return analyzeAllReplicaSetStatuses(analyzer, getFileContents)
|
|
} else {
|
|
return analyzeOneReplicaSetStatus(analyzer, getFileContents)
|
|
}
|
|
}
|
|
|
|
func analyzeOneReplicaSetStatus(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
excludeFiles := []string{}
|
|
files, err := getFileContents(filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, fmt.Sprintf("%s.json", analyzer.Namespace)), excludeFiles)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read collected replicasets from namespace")
|
|
}
|
|
|
|
var result *AnalyzeResult
|
|
for _, collected := range files { // only 1 file here
|
|
var replicasets appsv1.ReplicaSetList
|
|
if err := json.Unmarshal(collected, &replicasets); err != nil {
|
|
return nil, errors.Wrap(err, "failed to unmarshal deployment list")
|
|
}
|
|
|
|
var replicaset *appsv1.ReplicaSet
|
|
for _, r := range replicasets.Items {
|
|
if r.Name == analyzer.Name {
|
|
replicaset = r.DeepCopy()
|
|
break
|
|
}
|
|
}
|
|
|
|
if replicaset == nil {
|
|
// there's not an error, but maybe the requested deployment is not even deployed
|
|
result = &AnalyzeResult{
|
|
Title: fmt.Sprintf("%s ReplicaSet 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 replicaset %q was not found", analyzer.Name),
|
|
}
|
|
} else if len(analyzer.Outcomes) > 0 {
|
|
result, err = replicasetStatus(analyzer.Outcomes, replicaset)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to process status")
|
|
}
|
|
} else {
|
|
result = getDefaultReplicaSetResult(replicaset)
|
|
}
|
|
}
|
|
|
|
if result == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
return []*AnalyzeResult{result}, nil
|
|
}
|
|
|
|
func analyzeAllReplicaSetStatuses(analyzer *troubleshootv1beta2.ReplicaSetStatus, getFileContents getChildCollectedFileContents) ([]*AnalyzeResult, error) {
|
|
fileNames := make([]string, 0)
|
|
if analyzer.Namespace != "" {
|
|
fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, fmt.Sprintf("%s.json", analyzer.Namespace)))
|
|
}
|
|
for _, ns := range analyzer.Namespaces {
|
|
fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, fmt.Sprintf("%s.json", ns)))
|
|
}
|
|
|
|
if len(fileNames) == 0 {
|
|
fileNames = append(fileNames, filepath.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICASETS, "*.json"))
|
|
}
|
|
|
|
results := []*AnalyzeResult{}
|
|
excludeFiles := []string{}
|
|
for _, fileName := range fileNames {
|
|
files, err := getFileContents(fileName, excludeFiles)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read collected replicaset from file")
|
|
}
|
|
|
|
labelSelector, err := labels.Parse(strings.Join(analyzer.Selector, ","))
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse selector")
|
|
}
|
|
|
|
for _, collected := range files {
|
|
var replicasets appsv1.ReplicaSetList
|
|
if err := json.Unmarshal(collected, &replicasets); err != nil {
|
|
return nil, errors.Wrap(err, "failed to unmarshal replicaset list")
|
|
}
|
|
|
|
for _, replicaset := range replicasets.Items {
|
|
if !labelSelector.Matches(labels.Set(replicaset.Labels)) {
|
|
continue
|
|
}
|
|
|
|
var result *AnalyzeResult
|
|
if len(analyzer.Outcomes) > 0 {
|
|
result, err = replicasetStatus(analyzer.Outcomes, &replicaset)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to process status")
|
|
}
|
|
} else {
|
|
result = getDefaultReplicaSetResult(&replicaset)
|
|
}
|
|
|
|
if result != nil {
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func replicasetStatus(outcomes []*troubleshootv1beta2.Outcome, replicaset *appsv1.ReplicaSet) (*AnalyzeResult, error) {
|
|
result := &AnalyzeResult{
|
|
Title: fmt.Sprintf("%s Status", replicaset.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 := compareReplicaSetStatusToWhen(outcome.Fail.When, replicaset)
|
|
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 := compareReplicaSetStatusToWhen(outcome.Warn.When, replicaset)
|
|
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 := compareReplicaSetStatusToWhen(outcome.Pass.When, replicaset)
|
|
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 getDefaultReplicaSetResult(replicaset *appsv1.ReplicaSet) *AnalyzeResult {
|
|
if replicaset.Spec.Replicas == nil && replicaset.Status.AvailableReplicas == 1 { // default is 1
|
|
return nil
|
|
}
|
|
if replicaset.Spec.Replicas != nil && *replicaset.Spec.Replicas == replicaset.Status.AvailableReplicas {
|
|
return nil
|
|
}
|
|
|
|
return &AnalyzeResult{
|
|
Title: fmt.Sprintf("%s/%s ReplicaSet Status", replicaset.Namespace, replicaset.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 replicaset %s/%s is not ready", replicaset.Namespace, replicaset.Name),
|
|
}
|
|
}
|
|
|
|
func compareReplicaSetStatusToWhen(when string, job *appsv1.ReplicaSet) (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 "ready":
|
|
actual = job.Status.ReadyReplicas
|
|
case "available":
|
|
actual = job.Status.AvailableReplicas
|
|
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])
|
|
}
|