Skip to content

Commit

Permalink
Merge pull request #127 from replicatedhq/node-analyzers
Browse files Browse the repository at this point in the history
Node analyzers
  • Loading branch information
marccampbell committed Jan 30, 2020
2 parents 3982e20 + 879c3a6 commit 4472f10
Show file tree
Hide file tree
Showing 9 changed files with 545 additions and 7 deletions.
38 changes: 38 additions & 0 deletions examples/preflight/node-resources.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
apiVersion: troubleshoot.replicated.com/v1beta1
kind: Preflight
metadata:
name: sample
spec:
analyzers:
- nodeResources:
checkName: Must have at least 3 nodes in the cluster
outcomes:
- fail:
when: "< 3"
message: This application requires at least 3 nodes
- warn:
when: "< 5"
message: This application recommends at last 5 nodes.
- pass:
message: This cluster has enough nodes.
- nodeResources:
checkName: Must have 3 nodes with at least 6 cores
filters:
cpuCapacity: "6"
outcomes:
- fail:
when: "< 3"
message: This application requires at least 3 nodes with 6 cores each
- pass:
message: This cluster has enough nodes with enough codes
- nodeResources:
checkName: Must have 1 node with 16 GB (available) memory and 5 cores (on a single node)
filters:
allocatableMemory: 16Gi
cpuCapacity: "5"
outcomes:
- fail:
when: "< 1"
message: This application requires at least 1 node with 16GB available memory
- pass:
message: This cluster has a node with enough memory.
11 changes: 5 additions & 6 deletions ffi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ package main
import "C"

import (
"fmt"
"encoding/json"

"gopkg.in/yaml.v2"
"fmt"

analyzer "github.com/replicatedhq/troubleshoot/pkg/analyze"
"github.com/replicatedhq/troubleshoot/pkg/logger"
"github.com/replicatedhq/troubleshoot/pkg/convert"

"github.com/replicatedhq/troubleshoot/pkg/logger"
"gopkg.in/yaml.v2"
)

//export Analyze
func Analyze(bundleURL string, analyzers string, outputFormat string, compatibility string) *C.char {
func Analyze(bundleURL string, analyzers string, outputFormat string, compatibility string) *C.char {
logger.SetQuiet(true)

result, err := analyzer.DownloadAndAnalyze(bundleURL, analyzers)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ require (
github.com/hashicorp/go-getter v1.3.1-0.20190627223108-da0323b9545e
github.com/hashicorp/go-multierror v1.0.0
github.com/imdario/mergo v0.3.7 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mholt/archiver v3.1.1+incompatible
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/nwaples/rardecode v1.0.0 // indirect
Expand All @@ -36,7 +35,9 @@ require (
k8s.io/apimachinery v0.17.0
k8s.io/cli-runtime v0.17.0
k8s.io/client-go v0.17.0
k8s.io/code-generator v0.16.5-beta.1 // indirect
sigs.k8s.io/controller-runtime v0.4.0
sigs.k8s.io/controller-tools v0.2.4 // indirect
)

replace github.com/appscode/jsonpatch => github.com/gomodules/jsonpatch v2.0.1+incompatible
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh
github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gobuffalo/flect v0.1.5 h1:xpKq9ap8MbYfhuPCF0dBH854Gp9CxZjr/IocxELFflo=
github.com/gobuffalo/flect v0.1.5/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
Expand Down Expand Up @@ -564,6 +566,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac h1:MQEvx39qSf8vyrx3XRaOe+j1UDIzKwkYOVObRgGPVqI=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down Expand Up @@ -625,6 +628,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6CtBzjclw4pgm9gg8Ys0=
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand All @@ -646,8 +651,11 @@ k8s.io/client-go v0.0.0-20190918160344-1fbdaa4c8d90/go.mod h1:J69/JveO6XESwVgG53
k8s.io/client-go v0.17.0 h1:8QOGvUGdqDMFrm9sD6IUFl256BcffynGoe80sxgTEDg=
k8s.io/client-go v0.17.0/go.mod h1:TYgR6EUHs6k45hb6KWjVD6jFZvJV4gHDikv/It0xz+k=
k8s.io/code-generator v0.0.0-20190912054826-cd179ad6a269/go.mod h1:V5BD6M4CyaN5m+VthcclXWsVcT1Hu+glwa1bi3MIsyE=
k8s.io/code-generator v0.16.5-beta.1 h1:+zWxMQH3a6fd8lZe6utWyW/V7nmG2ZMXwtovSJI2p+0=
k8s.io/code-generator v0.16.5-beta.1/go.mod h1:mJUgkl06XV4kstAnLHAIzJPVCOzVR+ZcfPIv4fUsFCY=
k8s.io/component-base v0.0.0-20190918160511-547f6c5d7090/go.mod h1:933PBGtQFJky3TEwYx4aEPZ4IxqhWh3R6DCmzqIn1hA=
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/gengo v0.0.0-20190822140433-26a664648505 h1:ZY6yclUKVbZ+SdWnkfY+Je5vrMpKOxmGeKRbsXVmqYM=
k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
Expand All @@ -667,6 +675,8 @@ modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs
modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
sigs.k8s.io/controller-runtime v0.4.0 h1:wATM6/m+3w8lj8FXNaO6Fs/rq/vqoOjO1Q116Z9NPsg=
sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns=
sigs.k8s.io/controller-tools v0.2.4 h1:la1h46EzElvWefWLqfsXrnsO3lZjpkI0asTpX6h8PLA=
sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA=
sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
Expand Down
6 changes: 6 additions & 0 deletions pkg/analyze/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ func Analyze(analyzer *troubleshootv1beta1.Analyze, getFile getCollectedFileCont
}
return analyzeDistribution(analyzer.Distribution, getFile)
}
if analyzer.NodeResources != nil {
if analyzer.NodeResources.Exclude {
return nil, nil
}
return analyzeNodeResources(analyzer.NodeResources, getFile)
}
if analyzer.TextAnalyze != nil {
return analyzeTextAnalyze(analyzer.TextAnalyze, getFile)
}
Expand Down
218 changes: 218 additions & 0 deletions pkg/analyze/node_resources.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package analyzer

import (
"encoding/json"
"strconv"
"strings"

"github.com/pkg/errors"
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)

func analyzeNodeResources(analyzer *troubleshootv1beta1.NodeResources, 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")
}

matchingNodeCount := 0

for _, node := range nodes {
isMatch, err := nodeMatchesFilters(node, analyzer.Filters)
if err != nil {
return nil, errors.Wrap(err, "failed to check if node matches filter")
}

if isMatch {
matchingNodeCount++
}
}

title := analyzer.CheckName
if title == "" {
title = "Node Resources"
}

result := &AnalyzeResult{
Title: title,
}

for _, outcome := range analyzer.Outcomes {
if outcome.Fail != nil {
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Fail.When, matchingNodeCount)
if err != nil {
return nil, errors.Wrap(err, "failed to parse when")
}

if isWhenMatch {
result.IsFail = true
result.Message = outcome.Fail.Message
result.URI = outcome.Fail.URI

return result, nil
}
} else if outcome.Warn != nil {
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Warn.When, matchingNodeCount)
if err != nil {
return nil, errors.Wrap(err, "failed to parse when")
}

if isWhenMatch {
result.IsWarn = true
result.Message = outcome.Warn.Message
result.URI = outcome.Warn.URI

return result, nil
}
} else if outcome.Pass != nil {
isWhenMatch, err := compareNodeResourceConditionalToActual(outcome.Pass.When, matchingNodeCount)
if err != nil {
return nil, errors.Wrap(err, "failed to parse when")
}

if isWhenMatch {
result.IsPass = true
result.Message = outcome.Pass.Message
result.URI = outcome.Pass.URI

return result, nil
}
}
}

return result, nil
}

func compareNodeResourceConditionalToActual(conditional string, actual int) (bool, error) {
if conditional == "" {
return true, nil
}

parts := strings.Split(strings.TrimSpace(conditional), " ")

if len(parts) != 2 {
return false, errors.New("unable to parse nodeResources conditional")
}

operator := parts[0]
desiredValue, err := strconv.Atoi(parts[1])
if err != nil {
return false, errors.Wrap(err, "failed to parse nodeResource value")
}

switch operator {
case "=", "==", "===":
return desiredValue == actual, nil
case "<":
return actual < desiredValue, nil
case "<=":
return actual <= desiredValue, nil
case ">":
return actual > desiredValue, nil
case ">=":
return actual >= desiredValue, nil
}

return false, errors.New("unexpected conditional in nodeResources")
}

func nodeMatchesFilters(node corev1.Node, filters *troubleshootv1beta1.NodeResourceFilters) (bool, error) {
if filters == nil {
return true, nil
}

// all filters must pass for this to pass

if filters.CPUCapacity != "" {
parsed, err := resource.ParseQuantity(filters.CPUCapacity)
if err != nil {
return false, errors.Wrap(err, "failed to parse cpu capacity")
}

if node.Status.Capacity.Cpu().Cmp(parsed) == -1 {
return false, nil
}
}
if filters.CPUAllocatable != "" {
parsed, err := resource.ParseQuantity(filters.CPUAllocatable)
if err != nil {
return false, errors.Wrap(err, "failed to parse cpu allocatable")
}

if node.Status.Allocatable.Cpu().Cmp(parsed) == -1 {
return false, nil
}
}

if filters.MemoryCapacity != "" {
parsed, err := resource.ParseQuantity(filters.MemoryCapacity)
if err != nil {
return false, errors.Wrap(err, "failed to parse memory capacity")
}

if node.Status.Capacity.Memory().Cmp(parsed) == -1 {
return false, nil
}
}
if filters.MemoryAllocatable != "" {
parsed, err := resource.ParseQuantity(filters.MemoryAllocatable)
if err != nil {
return false, errors.Wrap(err, "failed to parse memory allocatable")
}

if node.Status.Allocatable.Memory().Cmp(parsed) == -1 {
return false, nil
}
}

if filters.PodCapacity != "" {
parsed, err := resource.ParseQuantity(filters.PodCapacity)
if err != nil {
return false, errors.Wrap(err, "failed to parse pod capacity")
}

if node.Status.Capacity.Pods().Cmp(parsed) == -1 {
return false, nil
}
}
if filters.PodAllocatable != "" {
parsed, err := resource.ParseQuantity(filters.PodAllocatable)
if err != nil {
return false, errors.Wrap(err, "failed to parse pod allocatable")
}

if node.Status.Allocatable.Pods().Cmp(parsed) == -1 {
return false, nil
}
}

if filters.EphemeralStorageCapacity != "" {
parsed, err := resource.ParseQuantity(filters.EphemeralStorageCapacity)
if err != nil {
return false, errors.Wrap(err, "failed to parse ephemeralstorage capacity")
}

if node.Status.Capacity.StorageEphemeral().Cmp(parsed) == -1 {
return false, nil
}
}
if filters.EphemeralStorageAllocatable != "" {
parsed, err := resource.ParseQuantity(filters.EphemeralStorageAllocatable)
if err != nil {
return false, errors.Wrap(err, "failed to parse ephemeralstorage allocatable")
}

if node.Status.Allocatable.StorageEphemeral().Cmp(parsed) == -1 {
return false, nil
}
}

return true, nil
}

0 comments on commit 4472f10

Please sign in to comment.