Skip to content

Commit 5a45f44

Browse files
authored
Merge pull request #11 from appuio/feat/disable-warning
Add annotation to disable request ration warnings per namespace
2 parents abebdcb + d1016f1 commit 5a45f44

File tree

5 files changed

+134
-25
lines changed

5 files changed

+134
-25
lines changed

.codeclimate.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
version: "2"
2+
checks:
3+
return-statements:
4+
enabled: true
5+
config:
6+
threshold: 8
27
plugins:
38
shellcheck:
49
enabled: true

config/rbac/role.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ metadata:
55
creationTimestamp: null
66
name: appuio-cloud-agent
77
rules:
8+
- apiGroups:
9+
- ""
10+
resources:
11+
- namespaces
12+
verbs:
13+
- get
14+
- list
15+
- watch
816
- apiGroups:
917
- ""
1018
resources:

main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ var (
2222
commit = "-dirty-"
2323
date = time.Now().Format("2006-01-02")
2424

25-
// TODO: Adjust app name
2625
appName = "appuio-cloud-agent"
2726
appLongName = "agent running on every APPUiO Cloud Zone"
2827

2928
scheme = runtime.NewScheme()
30-
setupLog = ctrl.Log.WithName("setup")
29+
setupLog = ctrl.Log.WithName("setup").WithValues("version", version, "commit", commit, "date", date)
3130
)
3231

3332
//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths="./..."

webhooks/ratio_validator.go

Lines changed: 63 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import (
44
"context"
55
"fmt"
66
"net/http"
7+
"strconv"
78
"strings"
89

910
admissionv1 "k8s.io/api/admission/v1"
1011
appsv1 "k8s.io/api/apps/v1"
1112
corev1 "k8s.io/api/core/v1"
13+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1214
"k8s.io/apimachinery/pkg/api/resource"
1315
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1416

@@ -18,6 +20,7 @@ import (
1820
)
1921

2022
// +kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch
23+
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=get;list;watch
2124

2225
// RatioValidator checks for every action in a namespace whether the Memory to CPU ratio limit is exceeded and will return a warning if it is.
2326
type RatioValidator struct {
@@ -27,6 +30,9 @@ type RatioValidator struct {
2730
RatioLimit *resource.Quantity
2831
}
2932

33+
// RatioValidatiorDisableAnnotation is the key for an annotion on a namespace to disable request ratio warnings
34+
var RatioValidatiorDisableAnnotation = "validate-request-ratio.appuio.io/disable"
35+
3036
// Handle handles the admission requests
3137
func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
3238
l := log.FromContext(ctx).
@@ -40,6 +46,19 @@ func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admi
4046
return admission.Allowed("system user")
4147
}
4248

49+
disabled, err := v.isNamespaceDisabled(ctx, req.Namespace)
50+
if err != nil {
51+
l.Error(err, "failed to get namespace")
52+
if apierrors.IsNotFound(err) {
53+
return errored(http.StatusNotFound, err)
54+
}
55+
return errored(http.StatusInternalServerError, err)
56+
}
57+
if disabled {
58+
l.V(1).Info("allowed: warning disabled")
59+
return admission.Allowed("system user")
60+
}
61+
4362
r, err := v.getRatio(ctx, req.Namespace)
4463
if err != nil {
4564
l.Error(err, "failed to get ratio")
@@ -50,28 +69,10 @@ func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admi
5069
// If we are creating an object with resource requests, we add them to the current ratio
5170
// We cannot easily do this when updating resources.
5271
if req.Operation == admissionv1.Create {
53-
switch req.Kind.Kind {
54-
case "Pod":
55-
pod := corev1.Pod{}
56-
if err := v.decoder.Decode(req, &pod); err != nil {
57-
l.Error(err, "failed to decode pod")
58-
return errored(http.StatusBadRequest, err)
59-
}
60-
r = r.RecordPod(pod)
61-
case "Deployment":
62-
deploy := appsv1.Deployment{}
63-
if err := v.decoder.Decode(req, &deploy); err != nil {
64-
l.Error(err, "failed to decode deployment")
65-
return errored(http.StatusBadRequest, err)
66-
}
67-
r = r.RecordDeployment(deploy)
68-
case "StatefulSet":
69-
sts := appsv1.StatefulSet{}
70-
if err := v.decoder.Decode(req, &sts); err != nil {
71-
l.Error(err, "failed to decode statefulset")
72-
return errored(http.StatusBadRequest, err)
73-
}
74-
r = r.RecordStatefulSet(sts)
72+
r, err = v.recordObject(ctx, r, req)
73+
if err != nil {
74+
l.Error(err, "failed to record object")
75+
return errored(http.StatusBadRequest, err)
7576
}
7677
}
7778
l = l.WithValues("ratio", r.String())
@@ -91,6 +92,46 @@ func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admi
9192
return admission.Allowed("ok")
9293
}
9394

95+
func (v *RatioValidator) recordObject(ctx context.Context, r *Ratio, req admission.Request) (*Ratio, error) {
96+
switch req.Kind.Kind {
97+
case "Pod":
98+
pod := corev1.Pod{}
99+
if err := v.decoder.Decode(req, &pod); err != nil {
100+
return r, err
101+
}
102+
r = r.RecordPod(pod)
103+
case "Deployment":
104+
deploy := appsv1.Deployment{}
105+
if err := v.decoder.Decode(req, &deploy); err != nil {
106+
return r, err
107+
}
108+
r = r.RecordDeployment(deploy)
109+
case "StatefulSet":
110+
sts := appsv1.StatefulSet{}
111+
if err := v.decoder.Decode(req, &sts); err != nil {
112+
return r, err
113+
}
114+
r = r.RecordStatefulSet(sts)
115+
}
116+
return r, nil
117+
}
118+
119+
func (v *RatioValidator) isNamespaceDisabled(ctx context.Context, nsName string) (bool, error) {
120+
ns := corev1.Namespace{}
121+
err := v.client.Get(ctx, client.ObjectKey{
122+
Name: nsName,
123+
}, &ns)
124+
if err != nil {
125+
return false, err
126+
}
127+
128+
disabled, ok := ns.Annotations[RatioValidatiorDisableAnnotation]
129+
if !ok {
130+
return false, nil
131+
}
132+
return strconv.ParseBool(disabled)
133+
}
134+
94135
func (v *RatioValidator) getRatio(ctx context.Context, ns string) (*Ratio, error) {
95136
r := NewRatio()
96137
pods := corev1.PodList{}

webhooks/ratio_validator_test.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,29 @@ func TestRatioValidator_Handle(t *testing.T) {
8383
limit: "1Gi",
8484
warn: true,
8585
},
86+
"Allow_DisabledUnfairNamespace": {
87+
user: "appuio#foo",
88+
namespace: "disabled-foo",
89+
resources: []client.Object{
90+
podFromResources("unfair", "disabled-foo", podResource{
91+
{cpu: "8", memory: "1Gi"},
92+
}),
93+
},
94+
limit: "1Gi",
95+
warn: false,
96+
},
97+
"Allow_LowercaseDisabledUnfairNamespace": {
98+
user: "appuio#foo",
99+
namespace: "disabled-bar",
100+
resources: []client.Object{
101+
podFromResources("unfair", "disabled-bar", podResource{
102+
{cpu: "8", memory: "1Gi"},
103+
}),
104+
},
105+
limit: "1Gi",
106+
warn: false,
107+
},
108+
86109
"Allow_ServiceAccount": {
87110
user: "system:serviceaccount:bar",
88111
namespace: "bar",
@@ -104,13 +127,14 @@ func TestRatioValidator_Handle(t *testing.T) {
104127
user: "bar",
105128
namespace: "fail-bar",
106129
resources: []client.Object{
130+
testNamespace("fail-bar"),
107131
podFromResources("pod1", "foo", podResource{
108132
{cpu: "100m", memory: "3G"},
109133
}),
110134
podFromResources("pod2", "foo", podResource{
111135
{cpu: "50m", memory: "1Gi"},
112136
}),
113-
podFromResources("unfair", "bar", podResource{
137+
podFromResources("unfair", "fail-bar", podResource{
114138
{cpu: "8", memory: "1Gi"},
115139
}),
116140
},
@@ -119,6 +143,16 @@ func TestRatioValidator_Handle(t *testing.T) {
119143
fail: true,
120144
statusCode: http.StatusInternalServerError,
121145
},
146+
"NamespaceNotExists": {
147+
user: "bar",
148+
namespace: "notexits",
149+
resources: []client.Object{},
150+
limit: "1Gi",
151+
warn: false,
152+
fail: true,
153+
statusCode: http.StatusNotFound,
154+
},
155+
122156
"Warn_ConsiderNewPod": {
123157
user: "appuio#foo",
124158
namespace: "foo",
@@ -242,7 +276,21 @@ func prepareTest(t *testing.T, initObjs ...client.Object) *RatioValidator {
242276

243277
decoder, err := admission.NewDecoder(scheme)
244278
require.NoError(t, err)
279+
barNs := testNamespace("bar")
280+
barNs.Annotations = map[string]string{
281+
RatioValidatiorDisableAnnotation: "False",
282+
}
245283

284+
disabledNs := testNamespace("disabled-foo")
285+
disabledNs.Annotations = map[string]string{
286+
RatioValidatiorDisableAnnotation: "True",
287+
}
288+
otherDisabledNs := testNamespace("disabled-bar")
289+
otherDisabledNs.Annotations = map[string]string{
290+
RatioValidatiorDisableAnnotation: "true",
291+
}
292+
293+
initObjs = append(initObjs, testNamespace("foo"), barNs, disabledNs, otherDisabledNs)
246294
client := fake.NewClientBuilder().
247295
WithScheme(scheme).
248296
WithObjects(initObjs...).
@@ -256,6 +304,14 @@ func prepareTest(t *testing.T, initObjs ...client.Object) *RatioValidator {
256304
return uv
257305
}
258306

307+
func testNamespace(name string) *corev1.Namespace {
308+
return &corev1.Namespace{
309+
ObjectMeta: metav1.ObjectMeta{
310+
Name: name,
311+
},
312+
}
313+
}
314+
259315
func podFromResources(name, namespace string, res podResource) *corev1.Pod {
260316
p := corev1.Pod{
261317
TypeMeta: metav1.TypeMeta{

0 commit comments

Comments
 (0)