Skip to content

Commit 810be65

Browse files
ReuDazegl
authored andcommitted
score/hpa: added a check for hpa min replicas > 1
Fixes #581
1 parent c51991e commit 810be65

12 files changed

+164
-8
lines changed

domain/kube-score.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package domain
22

33
import (
44
"io"
5+
autoscalingv1 "k8s.io/api/autoscaling/v1"
56

67
appsv1 "k8s.io/api/apps/v1"
7-
autoscalingv1 "k8s.io/api/autoscaling/v1"
88
corev1 "k8s.io/api/core/v1"
99
networkingv1 "k8s.io/api/networking/v1"
1010
policyv1 "k8s.io/api/policy/v1"
@@ -49,6 +49,7 @@ type FileLocationer interface {
4949
type HpaTargeter interface {
5050
GetTypeMeta() metav1.TypeMeta
5151
GetObjectMeta() metav1.ObjectMeta
52+
MinReplicas() *int32
5253
HpaTarget() autoscalingv1.CrossVersionObjectReference
5354
FileLocationer
5455
}

parser/internal/hpa.go

+15
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ func (d HPAv1) GetTypeMeta() metav1.TypeMeta {
2626
func (d HPAv1) GetObjectMeta() metav1.ObjectMeta {
2727
return d.ObjectMeta
2828
}
29+
func (d HPAv1) MinReplicas() *int32 {
30+
return d.Spec.MinReplicas
31+
}
2932

3033
func (d HPAv1) HpaTarget() autoscalingv1.CrossVersionObjectReference {
3134
return d.Spec.ScaleTargetRef
@@ -48,6 +51,10 @@ func (d HPAv2beta1) GetObjectMeta() metav1.ObjectMeta {
4851
return d.ObjectMeta
4952
}
5053

54+
func (d HPAv2beta1) MinReplicas() *int32 {
55+
return d.Spec.MinReplicas
56+
}
57+
5158
func (d HPAv2beta1) HpaTarget() autoscalingv1.CrossVersionObjectReference {
5259
return autoscalingv1.CrossVersionObjectReference(d.Spec.ScaleTargetRef)
5360
}
@@ -69,6 +76,10 @@ func (d HPAv2beta2) GetObjectMeta() metav1.ObjectMeta {
6976
return d.ObjectMeta
7077
}
7178

79+
func (d HPAv2beta2) MinReplicas() *int32 {
80+
return d.Spec.MinReplicas
81+
}
82+
7283
func (d HPAv2beta2) HpaTarget() autoscalingv1.CrossVersionObjectReference {
7384
return autoscalingv1.CrossVersionObjectReference(d.Spec.ScaleTargetRef)
7485
}
@@ -90,6 +101,10 @@ func (d HPAv2) GetObjectMeta() metav1.ObjectMeta {
90101
return d.ObjectMeta
91102
}
92103

104+
func (d HPAv2) MinReplicas() *int32 {
105+
return d.Spec.MinReplicas
106+
}
107+
93108
func (d HPAv2) HpaTarget() autoscalingv1.CrossVersionObjectReference {
94109
return autoscalingv1.CrossVersionObjectReference(d.Spec.ScaleTargetRef)
95110
}

score/apps/apps_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,10 @@ func (d hpav1) GetObjectMeta() metav1.ObjectMeta {
355355
return d.ObjectMeta
356356
}
357357

358+
func (d hpav1) MinReplicas() *int32 {
359+
return d.Spec.MinReplicas
360+
}
361+
358362
func (d hpav1) HpaTarget() autoscalingv1.CrossVersionObjectReference {
359363
return d.Spec.ScaleTargetRef
360364
}

score/deployment/deployment.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ func deploymentReplicas(svcs []ks.Service, hpas []ks.HpaTargeter) func(deploymen
9191
}
9292
}
9393

94-
if !referencedByService || hasHPA {
94+
if !referencedByService {
9595
score.Skipped = true
96-
score.AddComment("", "Skipped as the Deployment is not targeted by service or is controlled by a HorizontalPodAutoscaler", "")
96+
score.AddComment("", "Skipped as the Deployment is not targeted by service", "")
97+
} else if hasHPA {
98+
score.Skipped = true
99+
score.AddComment("", "Skipped as the Deployment is controlled by a HorizontalPodAutoscaler", "")
97100
} else {
98101
if ptr.Deref(deployment.Spec.Replicas, 1) >= 2 {
99102
score.Grade = scorecard.GradeAllOK

score/deployment_test.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,14 @@ func TestServiceTargetsDeploymentReplicasOk(t *testing.T) {
3939

4040
func TestServiceNotTargetsDeploymentReplicasNotRelevant(t *testing.T) {
4141
t.Parallel()
42-
skipped := wasSkipped(t, config.Configuration{
42+
assert.True(t, wasSkipped(t, config.Configuration{
43+
AllFiles: []ks.NamedReader{testFile("service-not-target-deployment.yaml")},
44+
}, "Deployment Replicas"))
45+
46+
summaries := getSummaries(t, config.Configuration{
4347
AllFiles: []ks.NamedReader{testFile("service-not-target-deployment.yaml")},
4448
}, "Deployment Replicas")
45-
assert.True(t, skipped)
49+
assert.Contains(t, summaries, "Skipped as the Deployment is not targeted by service")
4650
}
4751

4852
func TestServiceTargetsDeploymentReplicasNok(t *testing.T) {
@@ -52,8 +56,12 @@ func TestServiceTargetsDeploymentReplicasNok(t *testing.T) {
5256

5357
func TestHPATargetsDeployment(t *testing.T) {
5458
t.Parallel()
55-
skipped := wasSkipped(t, config.Configuration{
59+
assert.True(t, wasSkipped(t, config.Configuration{
60+
AllFiles: []ks.NamedReader{testFile("hpa-target-deployment.yaml")},
61+
}, "Deployment Replicas"))
62+
63+
summaries := getSummaries(t, config.Configuration{
5664
AllFiles: []ks.NamedReader{testFile("hpa-target-deployment.yaml")},
5765
}, "Deployment Replicas")
58-
assert.True(t, skipped)
66+
assert.Contains(t, summaries, "Skipped as the Deployment is controlled by a HorizontalPodAutoscaler")
5967
}

score/hpa/hpa.go

+14
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import (
44
"github.com/zegl/kube-score/domain"
55
"github.com/zegl/kube-score/score/checks"
66
"github.com/zegl/kube-score/scorecard"
7+
"k8s.io/utils/ptr"
78
)
89

910
func Register(allChecks *checks.Checks, allTargetableObjs []domain.BothMeta) {
1011
allChecks.RegisterHorizontalPodAutoscalerCheck("HorizontalPodAutoscaler has target", `Makes sure that the HPA targets a valid object`, hpaHasTarget(allTargetableObjs))
12+
allChecks.RegisterHorizontalPodAutoscalerCheck("HorizontalPodAutoscaler Replicas", `Makes sure that the HPA has multiple replicas`, hpaHasMultipleReplicas())
1113
}
1214

1315
func hpaHasTarget(allTargetableObjs []domain.BothMeta) func(hpa domain.HpaTargeter) (scorecard.TestScore, error) {
@@ -33,3 +35,15 @@ func hpaHasTarget(allTargetableObjs []domain.BothMeta) func(hpa domain.HpaTarget
3335
return
3436
}
3537
}
38+
39+
func hpaHasMultipleReplicas() func(hpa domain.HpaTargeter) (scorecard.TestScore, error) {
40+
return func(hpa domain.HpaTargeter) (score scorecard.TestScore, err error) {
41+
if ptr.Deref(hpa.MinReplicas(), 1) >= 2 {
42+
score.Grade = scorecard.GradeAllOK
43+
} else {
44+
score.Grade = scorecard.GradeWarning
45+
score.AddComment("", "HPA few replicas", "HorizontalPodAutoscalers are recommended to have at least 2 replicas to prevent unwanted downtime.")
46+
}
47+
return
48+
}
49+
}

score/hpa/hpa_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ func (d hpav1) GetObjectMeta() metav1.ObjectMeta {
177177
return d.ObjectMeta
178178
}
179179

180+
func (d hpav1) MinReplicas() *int32 {
181+
return d.Spec.MinReplicas
182+
}
183+
180184
func (d hpav1) HpaTarget() v1.CrossVersionObjectReference {
181185
return d.Spec.ScaleTargetRef
182186
}

score/hpa_test.go

+10
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ func TestHorizontalPodAutoscalerHasNoTarget(t *testing.T) {
2020
t.Parallel()
2121
testExpectedScore(t, "hpa-has-no-target.yaml", "HorizontalPodAutoscaler has target", scorecard.GradeCritical)
2222
}
23+
24+
func TestHorizontalPodAutoscalerMinReplicasOk(t *testing.T) {
25+
t.Parallel()
26+
testExpectedScore(t, "hpa-min-replicas-ok.yaml", "HorizontalPodAutoscaler Replicas", scorecard.GradeAllOK)
27+
}
28+
29+
func TestHorizontalPodAutoscalerMinReplicasNok(t *testing.T) {
30+
t.Parallel()
31+
testExpectedScore(t, "hpa-min-replicas-nok.yaml", "HorizontalPodAutoscaler Replicas", scorecard.GradeWarning)
32+
}

score/score_test.go

+19
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,25 @@ func wasSkipped(t *testing.T, config config.Configuration, testcase string) bool
5454
return false
5555
}
5656

57+
func getSummaries(t *testing.T, config config.Configuration, testcase string) []string {
58+
sc, err := testScore(config)
59+
assert.NoError(t, err)
60+
var summaries []string
61+
for _, objectScore := range sc {
62+
for _, s := range objectScore.Checks {
63+
if s.Check.Name == testcase {
64+
for _, c := range s.Comments {
65+
summaries = append(summaries, c.Summary)
66+
}
67+
return summaries
68+
}
69+
}
70+
}
71+
72+
assert.Fail(t, "test was not run")
73+
return summaries
74+
}
75+
5776
func testScore(config config.Configuration) (scorecard.Scorecard, error) {
5877
p, err := parser.New()
5978
if err != nil {
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apiVersion: autoscaling/v2
2+
kind: HorizontalPodAutoscaler
3+
metadata:
4+
name: php-apache
5+
namespace: default
6+
spec:
7+
scaleTargetRef:
8+
apiVersion: apps/v1
9+
kind: Deployment
10+
name: php-apache
11+
minReplicas: 1
12+
maxReplicas: 10
13+
metrics:
14+
- type: Resource
15+
resource:
16+
name: cpu
17+
target:
18+
type: Utilization
19+
averageUtilization: 50
20+
---
21+
apiVersion: apps/v1
22+
kind: Deployment
23+
metadata:
24+
name: php-apache
25+
namespace: default
26+
spec:
27+
template:
28+
spec:
29+
containers:
30+
- name: foo
31+
image: foo:latest
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
apiVersion: autoscaling/v2
2+
kind: HorizontalPodAutoscaler
3+
metadata:
4+
name: php-apache
5+
namespace: default
6+
spec:
7+
scaleTargetRef:
8+
apiVersion: apps/v1
9+
kind: Deployment
10+
name: php-apache
11+
minReplicas: 2
12+
maxReplicas: 10
13+
metrics:
14+
- type: Resource
15+
resource:
16+
name: cpu
17+
target:
18+
type: Utilization
19+
averageUtilization: 50
20+
---
21+
apiVersion: apps/v1
22+
kind: Deployment
23+
metadata:
24+
name: php-apache
25+
namespace: default
26+
spec:
27+
template:
28+
spec:
29+
containers:
30+
- name: foo
31+
image: foo:latest

score/testdata/hpa-target-deployment.yaml

+17-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,23 @@ metadata:
2626
spec:
2727
replicas: 1
2828
template:
29+
metadata:
30+
labels:
31+
app: my-app
2932
spec:
3033
containers:
3134
- name: foo
32-
image: foo:latest
35+
image: foo:latest
36+
---
37+
kind: Service
38+
apiVersion: v1
39+
metadata:
40+
name: my-service
41+
namespace: default
42+
spec:
43+
selector:
44+
app: my-app
45+
ports:
46+
- protocol: TCP
47+
port: 80
48+
targetPort: 8080

0 commit comments

Comments
 (0)