Skip to content

Commit

Permalink
chore: additional analyzers
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
  • Loading branch information
AlexsJones committed Apr 13, 2023
1 parent 5dcc190 commit 23071fd
Show file tree
Hide file tree
Showing 11 changed files with 527 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ require (
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/rubenv/sql-migrate v1.3.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.37.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand Down
2 changes: 2 additions & 0 deletions pkg/analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

var coreAnalyzerMap = map[string]common.IAnalyzer{
"Pod": PodAnalyzer{},
"Deployment": DeploymentAnalyzer{},
"ReplicaSet": ReplicaSetAnalyzer{},
"PersistentVolumeClaim": PvcAnalyzer{},
"Service": ServiceAnalyzer{},
Expand All @@ -21,6 +22,7 @@ var coreAnalyzerMap = map[string]common.IAnalyzer{
var additionalAnalyzerMap = map[string]common.IAnalyzer{
"HorizontalPodAutoScaler": HpaAnalyzer{},
"PodDisruptionBudget": PdbAnalyzer{},
"NetworkPolicy": NetworkPolicyAnalyzer{},
}

func ListFilters() ([]string, []string, []string) {
Expand Down
76 changes: 76 additions & 0 deletions pkg/analyzer/cronjob.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package analyzer

import (
"fmt"
"time"

"github.com/k8sgpt-ai/k8sgpt/pkg/common"
cron "github.com/robfig/cron/v3"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type CronJobAnalyzer struct{}

func (analyzer CronJobAnalyzer) Analyze(config common.Analyzer) ([]common.Result, error) {
var results []common.Result

cronJobList, err := config.Client.GetClient().BatchV1().CronJobs("").List(config.Context, v1.ListOptions{})
if err != nil {
return results, err
}

for _, cronJob := range cronJobList.Items {
result := common.Result{
Kind: "CronJob",
Name: cronJob.Name,
}

if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend {
result.Error = append(result.Error, common.Failure{
Text: fmt.Sprintf("CronJob %s is suspended", cronJob.Name),
Sensitive: []common.Sensitive{},
})
} else {
// check the schedule format
if _, err := CheckCronScheduleIsValid(cronJob.Spec.Schedule); err != nil {
result.Error = append(result.Error, common.Failure{
Text: fmt.Sprintf("CronJob %s has an invalid schedule: %s", cronJob.Name, cronJob.Spec.Schedule),
Sensitive: []common.Sensitive{},
})
}

// check the starting deadline
if cronJob.Spec.StartingDeadlineSeconds != nil {
deadline := time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second
if deadline < 0 {

result = common.Result{
Kind: "CronJob",
Name: cronJob.Name,
Error: []common.Failure{
{
Text: fmt.Sprintf("CronJob %s has a negative starting deadline: %d seconds", cronJob.Name, *cronJob.Spec.StartingDeadlineSeconds),
Sensitive: []common.Sensitive{},
},
},
}

}
}

}
results = append(results, result)
}

return results, nil
}

// Check CRON schedule format
func CheckCronScheduleIsValid(schedule string) (bool, error) {
_, err := cron.ParseStandard(schedule)
if err != nil {
return false, err
}

return true, nil
}
130 changes: 130 additions & 0 deletions pkg/analyzer/cronjob_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package analyzer

import (
"context"
"testing"

"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/magiconair/properties/assert"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestCronJobSuccess(t *testing.T) {
clientset := fake.NewSimpleClientset(&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "example-cronjob",
Namespace: "default",
Annotations: map[string]string{
"analysisDate": "2022-04-01",
},
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*/1 * * * *",
ConcurrencyPolicy: "Allow",
JobTemplate: batchv1.JobTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
},
},
})

config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}

analyzer := CronJobAnalyzer{}
analysisResults, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}

assert.Equal(t, len(analysisResults), 0)
assert.Equal(t, analysisResults[0].Name, "example-cronjob")
assert.Equal(t, analysisResults[0].Kind, "CronJob")
assert.Equal(t, analysisResults[0].Error, "CronJob 'example-cronjob' has an annotation 'analysisDate', indicating it may need to be reviewed.")

}

func TestCronJobBroken(t *testing.T) {
clientset := fake.NewSimpleClientset(&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "example-cronjob",
Namespace: "default",
Annotations: map[string]string{
"analysisDate": "2022-04-01",
},
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.CronJobSpec{
Schedule: "*** * * * *",
ConcurrencyPolicy: "Allow",
JobTemplate: batchv1.JobTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "example-app",
},
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
},
},
})

config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}

analyzer := CronJobAnalyzer{}
analysisResults, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}

assert.Equal(t, len(analysisResults), 1)
assert.Equal(t, analysisResults[0].Name, "example-cronjob")
assert.Equal(t, analysisResults[0].Kind, "CronJob")
}
46 changes: 46 additions & 0 deletions pkg/analyzer/deployment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package analyzer

import (
"context"
"fmt"

v1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/k8sgpt-ai/k8sgpt/pkg/common"
)

// DeploymentAnalyzer is an analyzer that checks for misconfigured Deployments
type DeploymentAnalyzer struct {
}

// Analyze scans all namespaces for Deployments with misconfigurations
func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {

var results []common.Result
deployments, err := a.Client.GetClient().AppsV1().Deployments("").List(context.Background(), v1.ListOptions{})
if err != nil {
return nil, err
}

for _, deployment := range deployments.Items {
if *deployment.Spec.Replicas != deployment.Status.Replicas {
failureDetails := []common.Failure{
{
Text: fmt.Sprintf("Deployment %s has a mismatch between the desired and actual replicas", deployment.Name),
Sensitive: []common.Sensitive{},
},
}

result := common.Result{
Kind: "Deployment",
Name: fmt.Sprintf("%s/%s", deployment.Namespace, deployment.Name),
Error: failureDetails,
ParentObject: "",
}

results = append(results, result)
}
}

return results, nil
}
64 changes: 64 additions & 0 deletions pkg/analyzer/deployment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package analyzer

import (
"context"
"testing"

"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/magiconair/properties/assert"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestDeploymentAnalyzer(t *testing.T) {
clientset := fake.NewSimpleClientset(&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Replicas: func() *int32 { i := int32(3); return &i }(),
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "example-container",
Image: "nginx",
Ports: []v1.ContainerPort{
{
ContainerPort: 80,
},
},
},
},
},
},
},
Status: appsv1.DeploymentStatus{
Replicas: 2,
AvailableReplicas: 1,
},
})

config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}

deploymentAnalyzer := DeploymentAnalyzer{}
analysisResults, err := deploymentAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
assert.Equal(t, analysisResults[0].Kind, "Deployment")
assert.Equal(t, analysisResults[0].Name, "default/example")
assert.Equal(t, len(analysisResults[0].Error), 1)
assert.Equal(t, analysisResults[0].Error[0].Text, "Deployment example has a mismatch between the desired and actual replicas")
}
Loading

0 comments on commit 23071fd

Please sign in to comment.