Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

Commit

Permalink
Cache ingress + pod exec decoding
Browse files Browse the repository at this point in the history
  • Loading branch information
alpe committed Jan 2, 2020
1 parent 9998663 commit dc9eec0
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 42 deletions.
2 changes: 1 addition & 1 deletion policies/ingress/require_ingress_exemption.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (p PolicyRequireIngressExemption) Validate(ctx context.Context, config poli

resourceViolations := []policies.ResourceViolation{}

ingressResource := resource.GetIngressResource(ar)
ingressResource := resource.GetIngressResource(ctx, ar)
if ingressResource == nil {
return resourceViolations, nil
}
Expand Down
2 changes: 1 addition & 1 deletion policies/pod/no_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (p PolicyNoExec) Validate(ctx context.Context, config policies.Config, ar *

resourceViolations := []policies.ResourceViolation{}

podExecResource := resource.GetPodExecResource(ar)
podExecResource := resource.GetPodExecResource(ctx, ar)
if podExecResource == nil {
return resourceViolations, nil
}
Expand Down
33 changes: 29 additions & 4 deletions resource/context.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package resource

import "context"
import (
"context"
"sync"
)

type contextKey int // local to the resource module

Expand All @@ -13,17 +16,39 @@ const (
cacheKeyIngress
)

type cache struct {
l sync.Mutex
m map[int]interface{}
}

// getOrSet returns the cached value for the given key. When none exists the passed function is called once to create
// the initial value. When cache is nil no caching happens and the create function is always called.
// Calls are executed thread safe.
func (c *cache) getOrSet(cacheKey int, f func() interface{}) interface{} {
if c == nil {
return f()
}
c.l.Lock()
defer c.l.Unlock()
if p, ok := c.m[cacheKey]; ok {
return p
}
v := f()
c.m[cacheKey] = v
return v
}

// WithResourceCache adds a resource cache to the context returned.
func WithResourceCache(ctx context.Context) context.Context {
c := make(map[int]interface{}, 1)
c := &cache{m: make(map[int]interface{}, 1)}
return context.WithValue(ctx, ctxKeyCache, c)
}

// GetResourceCache returns the cache from the context. Result will return nil when none exists.
func GetResourceCache(ctx context.Context) map[int]interface{} {
func GetResourceCache(ctx context.Context) *cache {
c := ctx.Value(ctxKeyCache)
if c == nil {
return nil
}
return c.(map[int]interface{})
return c.(*cache)
}
128 changes: 128 additions & 0 deletions resource/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package resource

import (
"context"
"reflect"
"sync"
"testing"
"time"
)

func TestResourceCacheContext(t *testing.T) {
const testRunCount = 2
const testCacheKey = 999
specs := map[string]struct {
srcCtx func() context.Context
srcKey int
srcFactoryFunc *creatorMock
expResp interface{}
expCalls int
}{
"with cache in ctx ": {
srcCtx: func() context.Context {
return WithResourceCache(context.TODO())
},
srcKey: testCacheKey,
srcFactoryFunc: &creatorMock{respValue: "myValue"},
expCalls: 1,
expResp: "myValue",
},
"with cache filled": {
srcCtx: func() context.Context {
ctx := WithResourceCache(context.TODO())
GetResourceCache(ctx).getOrSet(testCacheKey, func() interface{} {
return "myValue"
})
return ctx
},
srcKey: testCacheKey,
srcFactoryFunc: &creatorMock{respValue: "otherValue"},
expCalls: 0,
expResp: "myValue",
},
"with empty ctx": {
srcCtx: context.TODO,
srcKey: testCacheKey,
srcFactoryFunc: &creatorMock{respValue: "foo"},
expCalls: testRunCount,
expResp: "foo",
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
ctx := spec.srcCtx()
mock := spec.srcFactoryFunc
for i := 0; i < testRunCount; i++ {
resp := GetResourceCache(ctx).getOrSet(spec.srcKey, mock.CountCall)
if exp, got := spec.expResp, resp; !reflect.DeepEqual(exp, got) {
t.Errorf("expected %v but got %v", exp, got)
}
}
if exp, got := spec.expCalls, mock.called; exp != got {
t.Errorf("expected %d but got %d", exp, got)
}
})
}
}

func TestCacheWithConcurrentAccess(t *testing.T) {
const testCacheKey = 999
const actorCount = 10

var awaitStart sync.WaitGroup
awaitStart.Add(actorCount)
var awaitCompleted sync.WaitGroup
awaitCompleted.Add(actorCount)

actors := make([]*creatorMock, actorCount)
rsp := make(chan interface{}, actorCount)

c := &cache{m: make(map[int]interface{}, 1)}
for i := 0; i < actorCount; i++ {
actors[i] = &creatorMock{respValue: i}
go func(i int) {
awaitStart.Done()
awaitStart.Wait() // wait for all actors to start sync
rsp <- c.getOrSet(testCacheKey, actors[i].CountCall)
awaitCompleted.Done()
}(i)
}
awaitCompleted.Wait()

// then only 1 create function should be called
var active *creatorMock
var expResult int
for i, a := range actors {
if a.called != 0 {
if active != nil {
t.Fatal("more than 1 create function called")
}
active = a
expResult = i
}
}
// and all should see the same result
for i := 0; i < actorCount; i++ {
select {
case r := <-rsp:
if exp, got := expResult, r; exp != got {
t.Errorf("expected %v but got %v", exp, got)
}
case <-time.After(time.Millisecond):
t.Fatal("test timeout")
}
}
}

type creatorMock struct {
l sync.Mutex
called int
respValue interface{}
}

func (m *creatorMock) CountCall() interface{} {
m.l.Lock()
m.called++
m.l.Unlock()
return m.respValue
}
11 changes: 10 additions & 1 deletion resource/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
package resource

import (
"context"

admissionv1beta1 "k8s.io/api/admission/v1beta1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
Expand All @@ -28,7 +30,14 @@ type IngressResource struct {
}

// GetIngressResource extracts and IngressResource from an AdmissionRequest
func GetIngressResource(ar *admissionv1beta1.AdmissionRequest) *IngressResource {
func GetIngressResource(ctx context.Context, ar *admissionv1beta1.AdmissionRequest) *IngressResource {
c := GetResourceCache(ctx)
return c.getOrSet(cacheKeyIngress, func() interface{} {
return decodeIngressResource(ar)
}).(*IngressResource)
}

func decodeIngressResource(ar *admissionv1beta1.AdmissionRequest) *IngressResource {
switch ar.Resource {
case metav1.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "ingresses"}:
ing := extensionsv1beta1.Ingress{}
Expand Down
10 changes: 2 additions & 8 deletions resource/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,9 @@ type PodResource struct {
// GetPodResource extracts a PodResource from an AdmissionRequest
func GetPodResource(ar *admissionv1beta1.AdmissionRequest, ctx context.Context) *PodResource {
c := GetResourceCache(ctx)
if c == nil {
return c.getOrSet(cacheKeyPod, func() interface{} {
return decodePodResource(ar)
}
if p, ok := c[cacheKeyPod]; ok {
return p.(*PodResource)
}
r := decodePodResource(ar)
c[cacheKeyPod] = r
return r
}).(*PodResource)
}

func decodePodResource(ar *admissionv1beta1.AdmissionRequest) *PodResource {
Expand Down
10 changes: 9 additions & 1 deletion resource/pod_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
package resource

import (
"context"
"encoding/json"
"strings"

Expand All @@ -29,7 +30,14 @@ type PodExecResource struct {
}

// GetPodExecResource extracts and PodExecResource from an AdmissionRequest
func GetPodExecResource(ar *admissionv1beta1.AdmissionRequest) *PodExecResource {
func GetPodExecResource(ctx context.Context, ar *admissionv1beta1.AdmissionRequest) *PodExecResource {
c := GetResourceCache(ctx)
return c.getOrSet(cacheKeyPodExec, func() interface{} {
return decodePodExecResource(ar)
}).(*PodExecResource)
}

func decodePodExecResource(ar *admissionv1beta1.AdmissionRequest) *PodExecResource {
switch ar.Kind {
case metav1.GroupVersionKind{Group: "", Version: "v1", Kind: "PodExecOptions"}:
podExecOptions := corev1.PodExecOptions{}
Expand Down
26 changes: 0 additions & 26 deletions resource/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,29 +54,3 @@ spec:
- name: http
containerPort: 8080
`

const deploymentExample = `
kind: Deployment
apiVersion: apps/v1
metadata:
name: foo
namespace: testing
labels:
app: foo
spec:
replicas: 1
selector:
matchLabels:
app: foo
template:
metadata:
labels:
app: foo
spec:
containers:
- name: foo
image: foo
ports:
- name: http
containerPort: 8080
`

0 comments on commit dc9eec0

Please sign in to comment.