Skip to content

Commit

Permalink
filters/builtin: add annotate filter (#3022)
Browse files Browse the repository at this point in the history
Add `annotate` filter to annotate routes and a helper to retrieve annotations.

Signed-off-by: Alexander Yastrebov <alexander.yastrebov@zalando.de>
  • Loading branch information
AlexanderYastrebov authored Apr 18, 2024
1 parent 36f8db4 commit f17057c
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 0 deletions.
19 changes: 19 additions & 0 deletions docs/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ route1: *
-> <shunt>;
```

## annotate

Annotate the route, subsequent annotations using the same key will overwrite the value.
Other subsequent filters can use annotations to make decisions and should document the key and value they use.

Parameters:

* key (string)
* value (string)

Example:

```
route1: *
-> annotate("never", "gonna give you up")
-> annotate("never", "gonna let you down")
-> <shunt>;
```

## HTTP Headers
### preserveHost

Expand Down
62 changes: 62 additions & 0 deletions filters/builtin/annotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package builtin

import (
"fmt"

"github.com/zalando/skipper/filters"
)

type (
annotateSpec struct{}

annotateFilter struct {
key, value string
}
)

const annotateStateBagKey = "filter." + filters.AnnotateName

// NewAnnotate is a filter to annotate a filter chain.
// It stores its key and value arguments into the filter context.
// Use [GetAnnotations] to retrieve the annotations from the context.
func NewAnnotate() filters.Spec {
return annotateSpec{}
}

func (annotateSpec) Name() string {
return filters.AnnotateName
}

func (as annotateSpec) CreateFilter(args []interface{}) (filters.Filter, error) {
if len(args) != 2 {
return nil, fmt.Errorf("requires string key and value arguments")
}

af, ok := &annotateFilter{}, false
if af.key, ok = args[0].(string); !ok {
return nil, fmt.Errorf("key argument must be a string")
}

if af.value, ok = args[1].(string); !ok {
return nil, fmt.Errorf("value argument must be a string")
}

return af, nil
}

func (af *annotateFilter) Request(ctx filters.FilterContext) {
if v, ok := ctx.StateBag()[annotateStateBagKey]; ok {
v.(map[string]string)[af.key] = af.value
} else {
ctx.StateBag()[annotateStateBagKey] = map[string]string{af.key: af.value}
}
}

func (af *annotateFilter) Response(filters.FilterContext) {}

func GetAnnotations(ctx filters.FilterContext) map[string]string {
if v, ok := ctx.StateBag()[annotateStateBagKey]; ok {
return v.(map[string]string)
}
return nil
}
87 changes: 87 additions & 0 deletions filters/builtin/annotate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package builtin_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zalando/skipper/eskip"
"github.com/zalando/skipper/filters/builtin"
"github.com/zalando/skipper/filters/filtertest"
)

func TestAnnotate(t *testing.T) {
spec := builtin.NewAnnotate()

for _, tc := range []struct {
name string
def string
expected map[string]string
}{
{
name: "key and value",
def: `annotate("akey", "avalue")`,
expected: map[string]string{"akey": "avalue"},
},
{
name: "multiple annotations",
def: `annotate("akey1", "avalue1") -> annotate("akey2", "avalue2")`,
expected: map[string]string{"akey1": "avalue1", "akey2": "avalue2"},
},
{
name: "overwrite annotation",
def: `annotate("akey1", "avalue1") -> annotate("akey1", "avalue2")`,
expected: map[string]string{"akey1": "avalue2"},
},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := &filtertest.Context{
FStateBag: make(map[string]interface{}),
}

for _, f := range eskip.MustParseFilters(tc.def) {
filter, err := spec.CreateFilter(f.Args)
require.NoError(t, err)

filter.Request(ctx)
}

assert.Equal(t, tc.expected, builtin.GetAnnotations(ctx))
})
}
}

func TestAnnotateArgs(t *testing.T) {
spec := builtin.NewAnnotate()

t.Run("valid", func(t *testing.T) {
for _, def := range []string{
`annotate("akey", "avalue")`,
} {
t.Run(def, func(t *testing.T) {
args := eskip.MustParseFilters(def)[0].Args

_, err := spec.CreateFilter(args)
assert.NoError(t, err)
})
}
})

t.Run("invalid", func(t *testing.T) {
for _, def := range []string{
`annotate()`,
`annotate("akey")`,
`annotate(1)`,
`annotate("akey", 1)`,
`annotate("akey", "avalue", "anextra")`,
`annotate("akey", "avalue", 1)`,
} {
t.Run(def, func(t *testing.T) {
args := eskip.MustParseFilters(def)[0].Args

_, err := spec.CreateFilter(args)
assert.Error(t, err)
})
}
})
}
1 change: 1 addition & 0 deletions filters/builtin/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func Filters() []filters.Spec {
return []filters.Spec{
NewBackendIsProxy(),
NewComment(),
NewAnnotate(),
NewRequestHeader(),
NewSetRequestHeader(),
NewAppendRequestHeader(),
Expand Down
1 change: 1 addition & 0 deletions filters/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ func (r Registry) Register(s Spec) {
const (
BackendIsProxyName = "backendIsProxy"
CommentName = "comment"
AnnotateName = "annotate"
ModRequestHeaderName = "modRequestHeader"
SetRequestHeaderName = "setRequestHeader"
AppendRequestHeaderName = "appendRequestHeader"
Expand Down

0 comments on commit f17057c

Please sign in to comment.