Skip to content

Commit

Permalink
Merge pull request #11 from appuio/feat/disable-warning
Browse files Browse the repository at this point in the history
Add annotation to disable request ration warnings per namespace
  • Loading branch information
glrf authored Apr 7, 2022
2 parents abebdcb + d1016f1 commit 5a45f44
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 25 deletions.
5 changes: 5 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
version: "2"
checks:
return-statements:
enabled: true
config:
threshold: 8
plugins:
shellcheck:
enabled: true
Expand Down
8 changes: 8 additions & 0 deletions config/rbac/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ metadata:
creationTimestamp: null
name: appuio-cloud-agent
rules:
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
Expand Down
3 changes: 1 addition & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ var (
commit = "-dirty-"
date = time.Now().Format("2006-01-02")

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

scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
setupLog = ctrl.Log.WithName("setup").WithValues("version", version, "commit", commit, "date", date)
)

//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths="./..."
Expand Down
85 changes: 63 additions & 22 deletions webhooks/ratio_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"strings"

admissionv1 "k8s.io/api/admission/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

Expand All @@ -18,6 +20,7 @@ import (
)

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

// 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.
type RatioValidator struct {
Expand All @@ -27,6 +30,9 @@ type RatioValidator struct {
RatioLimit *resource.Quantity
}

// RatioValidatiorDisableAnnotation is the key for an annotion on a namespace to disable request ratio warnings
var RatioValidatiorDisableAnnotation = "validate-request-ratio.appuio.io/disable"

// Handle handles the admission requests
func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
l := log.FromContext(ctx).
Expand All @@ -40,6 +46,19 @@ func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admi
return admission.Allowed("system user")
}

disabled, err := v.isNamespaceDisabled(ctx, req.Namespace)
if err != nil {
l.Error(err, "failed to get namespace")
if apierrors.IsNotFound(err) {
return errored(http.StatusNotFound, err)
}
return errored(http.StatusInternalServerError, err)
}
if disabled {
l.V(1).Info("allowed: warning disabled")
return admission.Allowed("system user")
}

r, err := v.getRatio(ctx, req.Namespace)
if err != nil {
l.Error(err, "failed to get ratio")
Expand All @@ -50,28 +69,10 @@ func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admi
// If we are creating an object with resource requests, we add them to the current ratio
// We cannot easily do this when updating resources.
if req.Operation == admissionv1.Create {
switch req.Kind.Kind {
case "Pod":
pod := corev1.Pod{}
if err := v.decoder.Decode(req, &pod); err != nil {
l.Error(err, "failed to decode pod")
return errored(http.StatusBadRequest, err)
}
r = r.RecordPod(pod)
case "Deployment":
deploy := appsv1.Deployment{}
if err := v.decoder.Decode(req, &deploy); err != nil {
l.Error(err, "failed to decode deployment")
return errored(http.StatusBadRequest, err)
}
r = r.RecordDeployment(deploy)
case "StatefulSet":
sts := appsv1.StatefulSet{}
if err := v.decoder.Decode(req, &sts); err != nil {
l.Error(err, "failed to decode statefulset")
return errored(http.StatusBadRequest, err)
}
r = r.RecordStatefulSet(sts)
r, err = v.recordObject(ctx, r, req)
if err != nil {
l.Error(err, "failed to record object")
return errored(http.StatusBadRequest, err)
}
}
l = l.WithValues("ratio", r.String())
Expand All @@ -91,6 +92,46 @@ func (v *RatioValidator) Handle(ctx context.Context, req admission.Request) admi
return admission.Allowed("ok")
}

func (v *RatioValidator) recordObject(ctx context.Context, r *Ratio, req admission.Request) (*Ratio, error) {
switch req.Kind.Kind {
case "Pod":
pod := corev1.Pod{}
if err := v.decoder.Decode(req, &pod); err != nil {
return r, err
}
r = r.RecordPod(pod)
case "Deployment":
deploy := appsv1.Deployment{}
if err := v.decoder.Decode(req, &deploy); err != nil {
return r, err
}
r = r.RecordDeployment(deploy)
case "StatefulSet":
sts := appsv1.StatefulSet{}
if err := v.decoder.Decode(req, &sts); err != nil {
return r, err
}
r = r.RecordStatefulSet(sts)
}
return r, nil
}

func (v *RatioValidator) isNamespaceDisabled(ctx context.Context, nsName string) (bool, error) {
ns := corev1.Namespace{}
err := v.client.Get(ctx, client.ObjectKey{
Name: nsName,
}, &ns)
if err != nil {
return false, err
}

disabled, ok := ns.Annotations[RatioValidatiorDisableAnnotation]
if !ok {
return false, nil
}
return strconv.ParseBool(disabled)
}

func (v *RatioValidator) getRatio(ctx context.Context, ns string) (*Ratio, error) {
r := NewRatio()
pods := corev1.PodList{}
Expand Down
58 changes: 57 additions & 1 deletion webhooks/ratio_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@ func TestRatioValidator_Handle(t *testing.T) {
limit: "1Gi",
warn: true,
},
"Allow_DisabledUnfairNamespace": {
user: "appuio#foo",
namespace: "disabled-foo",
resources: []client.Object{
podFromResources("unfair", "disabled-foo", podResource{
{cpu: "8", memory: "1Gi"},
}),
},
limit: "1Gi",
warn: false,
},
"Allow_LowercaseDisabledUnfairNamespace": {
user: "appuio#foo",
namespace: "disabled-bar",
resources: []client.Object{
podFromResources("unfair", "disabled-bar", podResource{
{cpu: "8", memory: "1Gi"},
}),
},
limit: "1Gi",
warn: false,
},

"Allow_ServiceAccount": {
user: "system:serviceaccount:bar",
namespace: "bar",
Expand All @@ -104,13 +127,14 @@ func TestRatioValidator_Handle(t *testing.T) {
user: "bar",
namespace: "fail-bar",
resources: []client.Object{
testNamespace("fail-bar"),
podFromResources("pod1", "foo", podResource{
{cpu: "100m", memory: "3G"},
}),
podFromResources("pod2", "foo", podResource{
{cpu: "50m", memory: "1Gi"},
}),
podFromResources("unfair", "bar", podResource{
podFromResources("unfair", "fail-bar", podResource{
{cpu: "8", memory: "1Gi"},
}),
},
Expand All @@ -119,6 +143,16 @@ func TestRatioValidator_Handle(t *testing.T) {
fail: true,
statusCode: http.StatusInternalServerError,
},
"NamespaceNotExists": {
user: "bar",
namespace: "notexits",
resources: []client.Object{},
limit: "1Gi",
warn: false,
fail: true,
statusCode: http.StatusNotFound,
},

"Warn_ConsiderNewPod": {
user: "appuio#foo",
namespace: "foo",
Expand Down Expand Up @@ -242,7 +276,21 @@ func prepareTest(t *testing.T, initObjs ...client.Object) *RatioValidator {

decoder, err := admission.NewDecoder(scheme)
require.NoError(t, err)
barNs := testNamespace("bar")
barNs.Annotations = map[string]string{
RatioValidatiorDisableAnnotation: "False",
}

disabledNs := testNamespace("disabled-foo")
disabledNs.Annotations = map[string]string{
RatioValidatiorDisableAnnotation: "True",
}
otherDisabledNs := testNamespace("disabled-bar")
otherDisabledNs.Annotations = map[string]string{
RatioValidatiorDisableAnnotation: "true",
}

initObjs = append(initObjs, testNamespace("foo"), barNs, disabledNs, otherDisabledNs)
client := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(initObjs...).
Expand All @@ -256,6 +304,14 @@ func prepareTest(t *testing.T, initObjs ...client.Object) *RatioValidator {
return uv
}

func testNamespace(name string) *corev1.Namespace {
return &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
}

func podFromResources(name, namespace string, res podResource) *corev1.Pod {
p := corev1.Pod{
TypeMeta: metav1.TypeMeta{
Expand Down

0 comments on commit 5a45f44

Please sign in to comment.