Compare commits

...

10 Commits

Author SHA1 Message Date
github-actions[bot]
03228d8e6d chore(main): release 0.2.4 (#292)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2023-04-18 16:33:43 +02:00
Rakshit Gondwal
71732037fa feat: improve HPA analyzer to check ScaleTargetRef resources (#283)
* feat: improve HPA analyzer to check ScaleTargetRef resources

Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>

* feat: modify tests

Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>

* feat: improve all ScaleTargetRef to check for resources

Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>

* test: add test cases for all ScaleTargetRef types

Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>

* refactor: use interface to avoid dupplication

Signed-off-by: Matthis Holleville <matthish29@gmail.com>
Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>

* test: add test case for NoResourceConfiguredForScaleTargetRef

Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>

---------

Signed-off-by: Rakshit Gondwal <rakshitgondwal3@gmail.com>
Signed-off-by: Matthis Holleville <matthish29@gmail.com>
Co-authored-by: Matthis <99146727+matthisholleville@users.noreply.github.com>
2023-04-18 15:27:06 +02:00
Aris Boutselis
a962d13654 Merge pull request #296 from matthisholleville/fix/language-toggle-bug
fix: resolve language toggle bug (issue #294)
2023-04-18 10:17:10 +01:00
Matthis
0313627848 fix: resolve language toggle bug (issue #294)
Signed-off-by: Matthis <matthish29@gmail.com>
2023-04-18 06:14:11 +02:00
Matthis
3d684a2af7 fix: deployment/cronjob namespace filtering (#290)
Signed-off-by: Matthis Holleville <matthish29@gmail.com>
2023-04-17 17:37:19 +02:00
Matthis
af8b350520 fix: ensure parent directories are created in EnsureDirExists function (#293) 2023-04-17 14:38:43 +01:00
Matthis
3c4bc1a92c Merge pull request #287 from matthisholleville/feature/server-logs
feat: init logging middleware on server mode
2023-04-17 11:53:12 +02:00
Matthis
5f3244a5d4 Merge branch 'main' into feature/server-logs 2023-04-17 11:46:58 +02:00
Alex Jones
222e0873c0 Update README.md (#288)
Signed-off-by: Alex Jones <alexsimonjones@gmail.com>
2023-04-17 06:49:45 +01:00
Matthis Holleville
6742410025 feat: init logging middleware on server mode
Signed-off-by: Matthis Holleville <matthish29@gmail.com>
2023-04-16 23:40:19 +02:00
20 changed files with 935 additions and 39 deletions

View File

@@ -1 +1 @@
{".":"0.2.3"}
{".":"0.2.4"}

View File

@@ -1,5 +1,20 @@
# Changelog
## [0.2.4](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.3...v0.2.4) (2023-04-18)
### Features
* improve HPA analyzer to check ScaleTargetRef resources ([#283](https://github.com/k8sgpt-ai/k8sgpt/issues/283)) ([7173203](https://github.com/k8sgpt-ai/k8sgpt/commit/71732037fa40071cef0c2bc143736019d75eac86))
* init logging middleware on server mode ([6742410](https://github.com/k8sgpt-ai/k8sgpt/commit/6742410025d5e99c60045bb314730799f0e1e5ce))
### Bug Fixes
* deployment/cronjob namespace filtering ([#290](https://github.com/k8sgpt-ai/k8sgpt/issues/290)) ([3d684a2](https://github.com/k8sgpt-ai/k8sgpt/commit/3d684a2af7a9e1821bdb8b1bd6e85867b800d3ee))
* ensure parent directories are created in EnsureDirExists function ([#293](https://github.com/k8sgpt-ai/k8sgpt/issues/293)) ([af8b350](https://github.com/k8sgpt-ai/k8sgpt/commit/af8b350520d1a187a199482dd338db0086118db8))
* resolve language toggle bug (issue [#294](https://github.com/k8sgpt-ai/k8sgpt/issues/294)) ([0313627](https://github.com/k8sgpt-ai/k8sgpt/commit/03136278486ba12e3352580b317b9e63fa3a80f0))
## [0.2.3](https://github.com/k8sgpt-ai/k8sgpt/compare/v0.2.2...v0.2.3) (2023-04-16)

View File

@@ -12,6 +12,8 @@
It has SRE experience codified into its analyzers and helps to pull out the most relevant information to enrich it with AI.
<a href="https://www.producthunt.com/posts/k8sgpt?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-k8sgpt" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=389489&theme=light" alt="K8sGPT - K8sGPT&#0032;gives&#0032;Kubernetes&#0032;Superpowers&#0032;to&#0032;everyone | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
# Installation
@@ -28,7 +30,7 @@ brew install k8sgpt
**32 bit:**
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_386.rpm
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.4/k8sgpt_386.rpm
sudo rpm -ivh k8sgpt_386.rpm
```
<!---x-release-please-end-->
@@ -37,7 +39,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_amd64.rpm
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.4/k8sgpt_amd64.rpm
sudo rpm -ivh -i k8sgpt_amd64.rpm
```
<!---x-release-please-end-->
@@ -49,7 +51,7 @@ brew install k8sgpt
**32 bit:**
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_386.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.4/k8sgpt_386.deb
sudo dpkg -i k8sgpt_386.deb
```
<!---x-release-please-end-->
@@ -57,7 +59,7 @@ brew install k8sgpt
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_amd64.deb
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.4/k8sgpt_amd64.deb
sudo dpkg -i k8sgpt_amd64.deb
```
<!---x-release-please-end-->
@@ -70,14 +72,14 @@ brew install k8sgpt
**32 bit:**
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_386.apk
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.4/k8sgpt_386.apk
apk add k8sgpt_386.apk
```
<!---x-release-please-end-->
**64 bit:**
<!---x-release-please-start-version-->
```
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.3/k8sgpt_amd64.apk
curl -LO https://github.com/k8sgpt-ai/k8sgpt/releases/download/v0.2.4/k8sgpt_amd64.apk
apk add k8sgpt_amd64.apk
```
<!---x-release-please-end-->x

View File

@@ -19,7 +19,7 @@ spec:
containers:
- name: k8sgpt-container
imagePullPolicy: Always
image: ghcr.io/k8sgpt-ai/k8sgpt:v0.2.3 #x-release-please-version
image: ghcr.io/k8sgpt-ai/k8sgpt:v0.2.4 #x-release-please-version
ports:
- containerPort: 8080
args: ["serve"]

View File

@@ -4,10 +4,11 @@ import (
"context"
"encoding/base64"
"fmt"
"strings"
"github.com/fatih/color"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/spf13/viper"
"strings"
)
type NoOpAIClient struct {
@@ -34,7 +35,7 @@ func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, nocache bool)
inputKey := strings.Join(prompt, " ")
// Check for cached data
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))
cacheKey := util.GetCacheKey(a.GetName(), sEnc)
cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc)
response, err := a.GetCompletion(ctx, inputKey)
if err != nil {

View File

@@ -5,9 +5,10 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
"strings"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/fatih/color"
"github.com/spf13/viper"
@@ -59,7 +60,7 @@ func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, nocache bool)
inputKey := strings.Join(prompt, " ")
// Check for cached data
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))
cacheKey := util.GetCacheKey(a.GetName(), sEnc)
cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc)
// find in viper cache
if viper.IsSet(cacheKey) && !nocache {
// retrieve data from cache

View File

@@ -22,7 +22,7 @@ func (analyzer CronJobAnalyzer) Analyze(a common.Analyzer) ([]common.Result, err
var results []common.Result
cronJobList, err := a.Client.GetClient().BatchV1().CronJobs("").List(a.Context, v1.ListOptions{})
cronJobList, err := a.Client.GetClient().BatchV1().CronJobs(a.Namespace).List(a.Context, v1.ListOptions{})
if err != nil {
return results, err
}

View File

@@ -124,3 +124,97 @@ func TestCronJobBroken(t *testing.T) {
assert.Equal(t, analysisResults[0].Name, "default/example-cronjob")
assert.Equal(t, analysisResults[0].Kind, "CronJob")
}
func TestCronJobBrokenMultipleNamespaceFiltering(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,
},
},
},
},
},
},
&batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: "example-cronjob",
Namespace: "other-namespace",
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, "default/example-cronjob")
assert.Equal(t, analysisResults[0].Kind, "CronJob")
}

View File

@@ -23,7 +23,7 @@ func (d DeploymentAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error)
"analyzer_name": kind,
})
deployments, err := a.Client.GetClient().AppsV1().Deployments("").List(context.Background(), v1.ListOptions{})
deployments, err := a.Client.GetClient().AppsV1().Deployments(a.Namespace).List(context.Background(), v1.ListOptions{})
if err != nil {
return nil, err
}

View File

@@ -60,3 +60,81 @@ func TestDeploymentAnalyzer(t *testing.T) {
assert.Equal(t, analysisResults[0].Kind, "Deployment")
assert.Equal(t, analysisResults[0].Name, "default/example")
}
func TestDeploymentAnalyzerNamespaceFiltering(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,
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
},
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")
}

View File

@@ -5,6 +5,8 @@ import (
"github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@@ -30,28 +32,28 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
// check ScaleTargetRef exist
scaleTargetRef := hpa.Spec.ScaleTargetRef
scaleTargetRefNotFound := false
var podInfo PodInfo
switch scaleTargetRef.Kind {
case "Deployment":
_, err := a.Client.GetClient().AppsV1().Deployments(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err != nil {
scaleTargetRefNotFound = true
deployment, err := a.Client.GetClient().AppsV1().Deployments(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err == nil {
podInfo = DeploymentInfo{deployment}
}
case "ReplicationController":
_, err := a.Client.GetClient().CoreV1().ReplicationControllers(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err != nil {
scaleTargetRefNotFound = true
rc, err := a.Client.GetClient().CoreV1().ReplicationControllers(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err == nil {
podInfo = ReplicationControllerInfo{rc}
}
case "ReplicaSet":
_, err := a.Client.GetClient().AppsV1().ReplicaSets(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err != nil {
scaleTargetRefNotFound = true
rs, err := a.Client.GetClient().AppsV1().ReplicaSets(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err == nil {
podInfo = ReplicaSetInfo{rs}
}
case "StatefulSet":
_, err := a.Client.GetClient().AppsV1().StatefulSets(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err != nil {
scaleTargetRefNotFound = true
ss, err := a.Client.GetClient().AppsV1().StatefulSets(hpa.Namespace).Get(a.Context, scaleTargetRef.Name, metav1.GetOptions{})
if err == nil {
podInfo = StatefulSetInfo{ss}
}
default:
failures = append(failures, common.Failure{
@@ -60,7 +62,7 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
})
}
if scaleTargetRefNotFound {
if podInfo == nil {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("HorizontalPodAutoscaler uses %s/%s as ScaleTargetRef which does not exist.", scaleTargetRef.Kind, scaleTargetRef.Name),
Sensitive: []common.Sensitive{
@@ -70,6 +72,26 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
},
},
})
} else {
containers := len(podInfo.GetPodSpec().Containers)
for _, container := range podInfo.GetPodSpec().Containers {
if container.Resources.Requests == nil || container.Resources.Limits == nil {
containers--
}
}
if containers <= 0 {
failures = append(failures, common.Failure{
Text: fmt.Sprintf("%s %s/%s does not have resource configured.", scaleTargetRef.Kind, a.Namespace, scaleTargetRef.Name),
Sensitive: []common.Sensitive{
{
Unmasked: scaleTargetRef.Name,
Masked: util.MaskString(scaleTargetRef.Name),
},
},
})
}
}
if len(failures) > 0 {
@@ -96,3 +118,43 @@ func (HpaAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) {
return a.Results, nil
}
type PodInfo interface {
GetPodSpec() corev1.PodSpec
}
type DeploymentInfo struct {
*appsv1.Deployment
}
func (d DeploymentInfo) GetPodSpec() corev1.PodSpec {
return d.Spec.Template.Spec
}
// define a structure for ReplicationController
type ReplicationControllerInfo struct {
*corev1.ReplicationController
}
func (rc ReplicationControllerInfo) GetPodSpec() corev1.PodSpec {
return rc.Spec.Template.Spec
}
// define a structure for ReplicaSet
type ReplicaSetInfo struct {
*appsv1.ReplicaSet
}
func (rs ReplicaSetInfo) GetPodSpec() corev1.PodSpec {
return rs.Spec.Template.Spec
}
// define a structure for StatefulSet
type StatefulSetInfo struct {
*appsv1.StatefulSet
}
// implement PodInfo for StatefulSetInfo
func (ss StatefulSetInfo) GetPodSpec() corev1.PodSpec {
return ss.Spec.Template.Spec
}

View File

@@ -10,6 +10,8 @@ import (
"github.com/magiconair/properties/assert"
appsv1 "k8s.io/api/apps/v1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
@@ -163,7 +165,7 @@ func TestHPAAnalyzerWithNonExistentScaleTargetRef(t *testing.T) {
}
}
func TestHPAAnalyzerWithExistingScaleTargetRef(t *testing.T) {
func TestHPAAnalyzerWithExistingScaleTargetRefAsDeployment(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv1.HorizontalPodAutoscaler{
@@ -185,6 +187,28 @@ func TestHPAAnalyzerWithExistingScaleTargetRef(t *testing.T) {
Namespace: "default",
Annotations: map[string]string{},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "example",
Image: "nginx",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"cpu": resource.MustParse("100m"),
"memory": resource.MustParse("128Mi"),
},
Limits: corev1.ResourceList{
"cpu": resource.MustParse("200m"),
"memory": resource.MustParse("256Mi"),
},
},
},
},
},
},
},
},
)
hpaAnalyzer := HpaAnalyzer{}
@@ -204,3 +228,293 @@ func TestHPAAnalyzerWithExistingScaleTargetRef(t *testing.T) {
assert.Equal(t, len(analysis.Error), 0)
}
}
func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicationController(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
Kind: "ReplicationController",
Name: "example",
},
},
},
&corev1.ReplicationController{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: corev1.ReplicationControllerSpec{
Template: &corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "example",
Image: "nginx",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"cpu": resource.MustParse("100m"),
"memory": resource.MustParse("128Mi"),
},
Limits: corev1.ResourceList{
"cpu": resource.MustParse("200m"),
"memory": resource.MustParse("256Mi"),
},
},
},
},
},
},
},
},
)
hpaAnalyzer := HpaAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := hpaAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
for _, analysis := range analysisResults {
assert.Equal(t, len(analysis.Error), 0)
}
}
func TestHPAAnalyzerWithExistingScaleTargetRefAsReplicaSet(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
Kind: "ReplicaSet",
Name: "example",
},
},
},
&appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: appsv1.ReplicaSetSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "example",
Image: "nginx",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"cpu": resource.MustParse("100m"),
"memory": resource.MustParse("128Mi"),
},
Limits: corev1.ResourceList{
"cpu": resource.MustParse("200m"),
"memory": resource.MustParse("256Mi"),
},
},
},
},
},
},
},
},
)
hpaAnalyzer := HpaAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := hpaAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
for _, analysis := range analysisResults {
assert.Equal(t, len(analysis.Error), 0)
}
}
func TestHPAAnalyzerWithExistingScaleTargetRefAsStatefulSet(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
Kind: "StatefulSet",
Name: "example",
},
},
},
&appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: appsv1.StatefulSetSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "example",
Image: "nginx",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
"cpu": resource.MustParse("100m"),
"memory": resource.MustParse("128Mi"),
},
Limits: corev1.ResourceList{
"cpu": resource.MustParse("200m"),
"memory": resource.MustParse("256Mi"),
},
},
},
},
},
},
},
},
)
hpaAnalyzer := HpaAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := hpaAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
for _, analysis := range analysisResults {
assert.Equal(t, len(analysis.Error), 0)
}
}
func TestHPAAnalyzerWithExistingScaleTargetRefWithoutSpecifyingResources(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: autoscalingv1.HorizontalPodAutoscalerSpec{
ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{
Kind: "Deployment",
Name: "example",
},
},
},
&appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: appsv1.DeploymentSpec{
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "example",
Image: "nginx",
},
},
},
},
},
},
)
hpaAnalyzer := HpaAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := hpaAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
var errorFound bool
for _, analysis := range analysisResults {
for _, err := range analysis.Error {
if strings.Contains(err.Text, "does not have resource configured."){
errorFound = true
break
}
if errorFound {
break
}
}
if !errorFound {
t.Error("expected error 'does not have resource configured.' not found in analysis results")
}
}
}
func TestHPAAnalyzerNamespaceFiltering(t *testing.T) {
clientset := fake.NewSimpleClientset(
&autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
},
&autoscalingv1.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
Annotations: map[string]string{},
},
})
hpaAnalyzer := HpaAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := hpaAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
}

View File

@@ -113,3 +113,35 @@ func TestIngressAnalyzerWithoutIngressClassAnnotation(t *testing.T) {
t.Error("expected error 'does not specify an Ingress class' not found in analysis results")
}
}
func TestIngressAnalyzerNamespaceFiltering(t *testing.T) {
clientset := fake.NewSimpleClientset(
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
},
&networkingv1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
Annotations: map[string]string{},
},
})
ingressAnalyzer := IngressAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := ingressAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
}

View File

@@ -120,3 +120,77 @@ func TestNetpolWithPod(t *testing.T) {
assert.Equal(t, len(results), 0)
}
func TestNetpolNoPodsNamespaceFiltering(t *testing.T) {
clientset := fake.NewSimpleClientset(
&networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
},
Spec: networkingv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "example",
},
},
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{
PodSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "database",
},
},
},
},
},
},
},
},
&networkingv1.NetworkPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
},
Spec: networkingv1.NetworkPolicySpec{
PodSelector: metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "example",
},
},
Ingress: []networkingv1.NetworkPolicyIngressRule{
{
From: []networkingv1.NetworkPolicyPeer{
{
PodSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "database",
},
},
},
},
},
},
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analyzer := NetworkPolicyAnalyzer{}
results, err := analyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(results), 1)
assert.Equal(t, results[0].Kind, "NetworkPolicy")
}

View File

@@ -47,3 +47,57 @@ func TestPodAnalyzer(t *testing.T) {
}
assert.Equal(t, len(analysisResults), 1)
}
func TestPodAnalyzerNamespaceFiltering(t *testing.T) {
clientset := fake.NewSimpleClientset(
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Status: v1.PodStatus{
Phase: v1.PodPending,
Conditions: []v1.PodCondition{
{
Type: v1.PodScheduled,
Reason: "Unschedulable",
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
},
},
},
},
&v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
Annotations: map[string]string{},
},
Status: v1.PodStatus{
Phase: v1.PodPending,
Conditions: []v1.PodCondition{
{
Type: v1.PodScheduled,
Reason: "Unschedulable",
Message: "0/1 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate.",
},
},
},
})
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
podAnalyzer := PodAnalyzer{}
var analysisResults []common.Result
analysisResults, err := podAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
}

View File

@@ -48,3 +48,62 @@ func TestServiceAnalyzer(t *testing.T) {
}
assert.Equal(t, len(analysisResults), 1)
}
func TestServiceAnalyzerNamespaceFiltering(t *testing.T) {
clientset := fake.NewSimpleClientset(
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
Annotations: map[string]string{},
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "example",
},
},
},
&v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
Annotations: map[string]string{},
},
},
&v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
Annotations: map[string]string{},
},
Spec: v1.ServiceSpec{
Selector: map[string]string{
"app": "example",
},
},
},
)
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
serviceAnalyzer := ServiceAnalyzer{}
analysisResults, err := serviceAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
}

View File

@@ -145,3 +145,33 @@ func TestStatefulSetAnalyzerMissingStorageClass(t *testing.T) {
}
}
func TestStatefulSetAnalyzerNamespaceFiltering(t *testing.T) {
clientset := fake.NewSimpleClientset(
&appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "default",
},
},
&appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "example",
Namespace: "other-namespace",
},
})
statefulSetAnalyzer := StatefulSetAnalyzer{}
config := common.Analyzer{
Client: &kubernetes.Client{
Client: clientset,
},
Context: context.Background(),
Namespace: "default",
}
analysisResults, err := statefulSetAnalyzer.Analyze(config)
if err != nil {
t.Error(err)
}
assert.Equal(t, len(analysisResults), 1)
}

77
pkg/server/log.go Normal file
View File

@@ -0,0 +1,77 @@
package server
import (
"bytes"
"net/http"
"time"
"go.uber.org/zap"
)
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
buf *bytes.Buffer
}
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
return &loggingResponseWriter{
w,
http.StatusOK,
&bytes.Buffer{},
}
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
return lrw.buf.Write(b)
}
func (lrw *loggingResponseWriter) Flush() {
if f, ok := lrw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
lrw.ResponseWriter.Write(lrw.buf.Bytes())
}
func logRequest(logger *zap.Logger, fields []zap.Field, statusCode int, message string) {
if statusCode >= 400 {
logger.Error(message, fields...)
} else {
logger.Info("request completed", fields...)
}
}
func loggingMiddleware(next http.Handler) http.Handler {
config := zap.NewProductionConfig()
config.DisableCaller = true
logger, err := config.Build()
if err != nil {
panic(err)
}
defer logger.Sync()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
lrw := NewLoggingResponseWriter(w)
start := time.Now()
defer func() {
duration := time.Since(start).Milliseconds()
fields := []zap.Field{
zap.Int64("duration_ms", duration),
zap.String("method", r.Method),
zap.String("remote_addr", r.RemoteAddr),
zap.Int("status_code", lrw.statusCode),
zap.String("url", r.URL.Path),
}
logRequest(logger, fields, lrw.statusCode, lrw.buf.String())
}()
next.ServeHTTP(lrw, r)
lrw.Flush()
})
}

View File

@@ -51,30 +51,32 @@ func (s *Config) analyzeHandler(w http.ResponseWriter, r *http.Request) {
config, err := analysis.NewAnalysis(s.Backend, language, []string{}, namespace, nocache, explain)
if err != nil {
health.Failure++
fmt.Fprintf(w, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = config.RunAnalysis()
if err != nil {
color.Red("Error: %v", err)
health.Failure++
fmt.Fprintf(w, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if explain {
err := config.GetAIResults(s.Output, anonymize)
if err != nil {
color.Red("Error: %v", err)
health.Failure++
fmt.Fprintf(w, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
out, err := config.PrintOutput(s.Output)
if err != nil {
color.Red("Error: %v", err)
health.Failure++
fmt.Fprintf(w, err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
health.Success++
@@ -82,11 +84,12 @@ func (s *Config) analyzeHandler(w http.ResponseWriter, r *http.Request) {
}
func (s *Config) Serve() error {
handler := loggingMiddleware(http.DefaultServeMux)
http.Handle("/metrics", promhttp.Handler())
http.HandleFunc("/analyze", s.analyzeHandler)
http.HandleFunc("/healthz", s.healthzHandler)
color.Green("Starting server on port %s", s.Port)
err := http.ListenAndServe(":"+s.Port, nil)
err := http.ListenAndServe(":"+s.Port, handler)
if err != nil {
fmt.Printf("error starting server: %s\n", err)
return err

View File

@@ -134,8 +134,8 @@ func ReplaceIfMatch(text string, pattern string, replacement string) string {
return text
}
func GetCacheKey(provider string, sEnc string) string {
return fmt.Sprintf("%s-%s", provider, sEnc)
func GetCacheKey(provider string, language string, sEnc string) string {
return fmt.Sprintf("%s-%s-%s", provider, language, sEnc)
}
func GetPodListByLabels(client k.Interface,
@@ -164,7 +164,7 @@ func FileExists(path string) (bool, error) {
}
func EnsureDirExists(dir string) error {
err := os.Mkdir(dir, 0755)
err := os.MkdirAll(dir, 0755)
if errors.Is(err, os.ErrExist) {
return nil