From 6247a1c0f3c2ead6a59661afed06973c29e57eca Mon Sep 17 00:00:00 2001 From: Dominik Augustin Date: Fri, 14 Apr 2023 15:08:47 +0200 Subject: [PATCH] feat: add node analyzer (#272) Signed-off-by: Dominik Augustin --- README.md | 1 + pkg/analyzer/analyzer.go | 1 + pkg/analyzer/node.go | 74 +++++++++++++++++++++++++ pkg/analyzer/node_test.go | 111 ++++++++++++++++++++++++++++++++++++++ pkg/common/types.go | 1 + 5 files changed, 188 insertions(+) create mode 100644 pkg/analyzer/node.go create mode 100644 pkg/analyzer/node_test.go diff --git a/README.md b/README.md index 784ea502ab..c6e2eda9a4 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ you will be able to write your own analyzers. - [x] statefulSetAnalyzer - [x] deploymentAnalyzer - [x] cronJobAnalyzer +- [x] nodeAnalyzer #### Optional diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index d5b2d3fb87..d553caa64c 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -18,6 +18,7 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{ "Ingress": IngressAnalyzer{}, "StatefulSet": StatefulSetAnalyzer{}, "CronJob": CronJobAnalyzer{}, + "Node": NodeAnalyzer{}, } var additionalAnalyzerMap = map[string]common.IAnalyzer{ diff --git a/pkg/analyzer/node.go b/pkg/analyzer/node.go new file mode 100644 index 0000000000..e13b932919 --- /dev/null +++ b/pkg/analyzer/node.go @@ -0,0 +1,74 @@ +package analyzer + +import ( + "fmt" + v1 "k8s.io/api/core/v1" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type NodeAnalyzer struct{} + +func (NodeAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + list, err := a.Client.GetClient().CoreV1().Nodes().List(a.Context, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + var preAnalysis = map[string]common.PreAnalysis{} + + for _, node := range list.Items { + var failures []common.Failure + for _, nodeCondition := range node.Status.Conditions { + // https://kubernetes.io/docs/concepts/architecture/nodes/#condition + switch nodeCondition.Type { + case v1.NodeReady: + if nodeCondition.Status == v1.ConditionTrue { + break + } + failures = addNodeConditionFailure(failures, node.Name, nodeCondition) + default: + if nodeCondition.Status != v1.ConditionFalse { + failures = addNodeConditionFailure(failures, node.Name, nodeCondition) + } + } + } + + if len(failures) > 0 { + preAnalysis[fmt.Sprintf("%s", node.Name)] = common.PreAnalysis{ + Node: node, + FailureDetails: failures, + } + } + } + + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: "Node", + Name: key, + Error: value.FailureDetails, + } + + parent, _ := util.GetParent(a.Client, value.Node.ObjectMeta) + currentAnalysis.ParentObject = parent + a.Results = append(a.Results, currentAnalysis) + } + + return a.Results, err +} + +func addNodeConditionFailure(failures []common.Failure, nodeName string, nodeCondition v1.NodeCondition) []common.Failure { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("%s has condition of type %s, reason %s: %s", nodeName, nodeCondition.Type, nodeCondition.Reason, nodeCondition.Message), + Sensitive: []common.Sensitive{ + { + Unmasked: nodeName, + Masked: util.MaskString(nodeName), + }, + }, + }) + return failures +} diff --git a/pkg/analyzer/node_test.go b/pkg/analyzer/node_test.go new file mode 100644 index 0000000000..88eefd4da5 --- /dev/null +++ b/pkg/analyzer/node_test.go @@ -0,0 +1,111 @@ +package analyzer + +import ( + "context" + "testing" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/magiconair/properties/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestNodeAnalyzerNodeReady(t *testing.T) { + clientset := fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeReady, + Status: v1.ConditionTrue, + Reason: "KubeletReady", + Message: "kubelet is posting ready status", + }, + }, + }, + }) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + }, + Context: context.Background(), + } + nodeAnalyzer := NodeAnalyzer{} + var analysisResults []common.Result + analysisResults, err := nodeAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 0) +} + +func TestNodeAnalyzerNodeDiskPressure(t *testing.T) { + clientset := fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: v1.NodeDiskPressure, + Status: v1.ConditionTrue, + Reason: "KubeletHasDiskPressure", + Message: "kubelet has disk pressure", + }, + }, + }, + }) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + }, + Context: context.Background(), + } + nodeAnalyzer := NodeAnalyzer{} + var analysisResults []common.Result + analysisResults, err := nodeAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) +} + +// A cloud provider may set their own condition and/or a new status might be introduced +// In such cases a failure is assumed and the code shouldn't break, although it might be a false positive +func TestNodeAnalyzerNodeUnknownType(t *testing.T) { + clientset := fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "node1", + }, + Status: v1.NodeStatus{ + Conditions: []v1.NodeCondition{ + { + Type: "UnknownNodeConditionType", + Status: "CompletelyUnknown", + Reason: "KubeletHasTheUnknown", + Message: "kubelet has the unknown", + }, + }, + }, + }) + + config := common.Analyzer{ + Client: &kubernetes.Client{ + Client: clientset, + }, + Context: context.Background(), + } + nodeAnalyzer := NodeAnalyzer{} + var analysisResults []common.Result + analysisResults, err := nodeAnalyzer.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 3fbf4ed296..0968ba25b3 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -38,6 +38,7 @@ type PreAnalysis struct { PodDisruptionBudget policyv1.PodDisruptionBudget StatefulSet appsv1.StatefulSet NetworkPolicy networkv1.NetworkPolicy + Node v1.Node // Integrations TrivyVulnerabilityReport trivy.VulnerabilityReport }