From 0ee3e2aa61258d9fd0488ed3f012fabec2a51e88 Mon Sep 17 00:00:00 2001 From: Rucciva Date: Fri, 3 Nov 2023 14:56:02 +0700 Subject: [PATCH] first realease --- .dockerignore | 5 + .github/workflows/docker.yaml | 57 ++++++++++ .github/workflows/go.yaml | 27 +++++ .gitignore | 1 + .goreleaser.yaml | 5 + Dockerfile | 13 +++ README.md | 15 +++ config.go | 54 +++++++++ config_test.go | 77 +++++++++++++ cosign.pub | 4 + datasource-http.go | 204 ++++++++++++++++++++++++++++++++++ datasource-http_test.go | 44 ++++++++ datasource-kong.go | 127 +++++++++++++++++++++ datasource-static.go | 34 ++++++ datasource.go | 60 ++++++++++ examples/docker-compose.yml | 18 +++ examples/kong.yaml | 38 +++++++ go.mod | 18 ++- go.sum | 31 +++++- injector.go | 55 +++++++++ kong.conf | 6 + main.go | 4 +- omniplug.go | 51 +++++++++ reqenrich.go | 153 ------------------------- reqenrich_test.go | 51 --------- template.go | 45 ++++++++ testdata/config.yaml | 55 +++++++++ 27 files changed, 1039 insertions(+), 213 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker.yaml create mode 100644 .github/workflows/go.yaml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 config.go create mode 100644 config_test.go create mode 100644 cosign.pub create mode 100644 datasource-http.go create mode 100644 datasource-http_test.go create mode 100644 datasource-kong.go create mode 100644 datasource-static.go create mode 100644 datasource.go create mode 100644 examples/docker-compose.yml create mode 100644 examples/kong.yaml create mode 100644 injector.go create mode 100644 kong.conf create mode 100644 omniplug.go delete mode 100644 reqenrich.go delete mode 100644 reqenrich_test.go create mode 100644 template.go create mode 100644 testdata/config.yaml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..66a0b56 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +** +!*.go +!go.mod +!go.sum +!kong.conf \ No newline at end of file diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..a23e05e --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,57 @@ +name: docker + +on: + push: + tags: + - "v*" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: docker_meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - id: docker_build + name: Build and push Docker image + uses: docker/build-push-action@v3 + with: + platforms: linux/amd64 + push: true + tags: ${{ steps.docker_meta.outputs.tags }} + labels: ${{ steps.docker_meta.outputs.labels }} + + - name: Install Cosign + uses: sigstore/cosign-installer@main + + - name: Sign image with a key + run: | + cosign sign --key env://COSIGN_PRIVATE_KEY ${IMAGE_NAME,,}@${IMAGE_DIGEST} --yes + env: + IMAGE_NAME: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + IMAGE_DIGEST: ${{ steps.docker_build.outputs.digest }} + COSIGN_PRIVATE_KEY: ${{secrets.COSIGN_PRIVATE_KEY}} + COSIGN_PASSWORD: ${{secrets.COSIGN_PASSWORD}} diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml new file mode 100644 index 0000000..a2d30b4 --- /dev/null +++ b/.github/workflows/go.yaml @@ -0,0 +1,27 @@ +name: go + +on: + push: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: "^1.20.0" + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser + version: latest + args: release --rm-dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..17138ab --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,5 @@ +version: 1 +builds: + - goarch: + - amd64 + - arm64 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9173e99 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM golang:1.20 AS builder +WORKDIR /src +COPY . . +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + GOOS=linux go build -o omniplug + + +FROM kong:3.4.2 + +COPY --from=builder /src/omniplug /usr/local/bin/omniplug +COPY kong.conf /tmp +RUN cat /tmp/kong.conf >> /etc/kong/kong.conf \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5dfe11d --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Kong Plugin Omniplug + +Omniplug is a Kong plugin to inject proxied HTTP request with data obtained from various source. + +Currently supported data source includes: + +- [static json/yaml](./testdata/config.yaml#L2-L6) strings. +- [Kong context](./testdata/config.yaml#L7-L20) data. +- [HTTP response](./testdata/config.yaml#L21-L34) data. + +Currently supported target of injection includes: + +- Kong [context](./testdata/config.yaml#L37-L43). +- Upstream HTTP request [Headers](./testdata/config.yaml#L44-L49). +- Upstream HTTP request [Query Parameters](./testdata/config.yaml#L50-L55). diff --git a/config.go b/config.go new file mode 100644 index 0000000..ece59e3 --- /dev/null +++ b/config.go @@ -0,0 +1,54 @@ +package main + +import ( + "encoding/json" + "sync" + + "github.com/Kong/go-pdk" +) + +type Config struct { + Datasources []configDatasource `json:"datasources" yaml:"datasources"` + Injector configInjector `json:"injector" yaml:"injector"` + + omniplug *Omniplug + mut sync.Mutex +} + +func (conf *Config) Init() (err error) { + if conf.omniplug != nil { + return + } + + conf.mut.Lock() + defer conf.mut.Unlock() + if conf.omniplug == nil { + v, err := json.Marshal(conf) + if err != nil { + return err + } + + conf.omniplug = &Omniplug{} + if err = json.Unmarshal(v, conf.omniplug); err != nil { + return err + } + + if err = conf.omniplug.Init(); err != nil { + return err + } + } + return +} + +func (conf *Config) Access(kong *pdk.PDK) { + if err := conf.Init(); err != nil { + kong.Log.Err(err.Error()) + kong.Response.ExitStatus(500) + } + + conf.omniplug.Access(kong) +} + +func NewConfig() interface{} { + return &Config{} +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..d0cc6d1 --- /dev/null +++ b/config_test.go @@ -0,0 +1,77 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + "time" + + "github.com/Kong/go-pdk/test" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func TestPlugin(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]interface{}{ + "headers": r.Header, + "queryString": r.URL.Query(), + "startedDateTime": time.Now().Format(time.RFC3339Nano), + }) + })) + s := os.Getenv("TEST_SERVER_URL") + os.Setenv("TEST_SERVER_URL", svr.URL) + defer os.Setenv("TEST_SERVER_URL", s) + + defer svr.Close() + env, err := test.New(t, test.Request{ + Method: "GET", + Url: "http://example.com?x_foo=bar&expire_at=" + url.QueryEscape(time.Now().Add(time.Hour).Format(time.RFC3339Nano)), + Headers: http.Header{ + "x-foo": []string{"bar"}, + }, + }) + assert.NoError(t, err) + + f, err := os.Open("testdata/config.yaml") + assert.NoError(t, err) + + config := &Config{} + err = yaml.NewDecoder(f).Decode(config) + assert.NoError(t, err) + + var mockbinDate string + for i := 1; i <= 10; i++ { + t.Run(fmt.Sprintf("loop-%d", i), func(t *testing.T) { + env.DoHttps(config) + t.Log(env.ServiceReq.Url) + t.Log(string(env.ClientRes.Body)) + + assert.Equal(t, 200, env.ClientRes.Status) + assert.Equal(t, 200, env.ClientRes.Status) + assert.Equal(t, "Jon Doe", env.ServiceReq.Headers.Get("username")) + assert.Equal(t, "world", env.ServiceReq.Headers.Get("hello")) + assert.Equal(t, "bar", env.ServiceReq.Headers.Get("foo")) + assert.Equal(t, "bar", env.ServiceReq.Headers.Get("foo-ex")) + date := env.ServiceReq.Headers.Get("date") + if mockbinDate == "" { + mockbinDate = date + } else { + assert.Equal(t, mockbinDate, date) + } + t.Log(date) + + u, err := url.Parse(env.ServiceReq.Url) + assert.NoError(t, err) + assert.Equal(t, "Jon Doe", u.Query().Get("username")) + assert.Equal(t, "world", u.Query().Get("hello")) + assert.Equal(t, "bar", u.Query().Get("foo")) + assert.Equal(t, "bar", u.Query().Get("foo-ex")) + assert.Equal(t, date, u.Query().Get("date")) + }) + } +} diff --git a/cosign.pub b/cosign.pub new file mode 100644 index 0000000..b7a67ab --- /dev/null +++ b/cosign.pub @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE8YShccTf0mlSyO1uGlU/YygGnS+L +O98pv5hEmkw//fL6Q1OgfYpmb0CCY36Bfe936IXMzCCi31YPynQggU48sw== +-----END PUBLIC KEY----- diff --git a/datasource-http.go b/datasource-http.go new file mode 100644 index 0000000..d5e7e36 --- /dev/null +++ b/datasource-http.go @@ -0,0 +1,204 @@ +package main + +import ( + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + "time" + + "github.com/Kong/go-pdk" + "github.com/dgraph-io/ristretto" + "github.com/spf13/cast" +) + +type Duration time.Duration + +func (t *Duration) UnmarshalText(text []byte) (err error) { + d, err := time.ParseDuration(string(text)) + if err != nil { + return err + } + *t = Duration(d) + return +} +func (t Duration) MarshalText() (text []byte, err error) { + return []byte(time.Duration(t).String()), nil +} + +type DatasourceHTTP struct { + URL Template `json:"url" yaml:"url"` + Method Template `json:"method" yaml:"method"` + QueryParams map[string]Template `json:"query_params" yaml:"query_params"` + Headers map[string]Template `json:"headers" yaml:"headers"` + Body Template `json:"body" yaml:"body"` + Timeout *Duration `json:"timeout" yaml:"timeout"` + CacheKey Template `json:"cache-key" yaml:"cache-key"` + TTL Template `json:"ttl" yaml:"ttl"` + + hc *http.Client + + rcache *ristretto.Cache +} + +type configDatasourceHTTP struct { + URL string `json:"url" yaml:"url"` + Method string `json:"method" yaml:"method"` + QueryParams map[string]string `json:"query_params" yaml:"query_params"` + Headers map[string]string `json:"headers" yaml:"headers"` + Body string `json:"body" yaml:"body"` + Timeout *string `json:"timeout" yaml:"timeout"` + CacheKey string `json:"cache-key" yaml:"cache-key"` + TTL string `json:"ttl" yaml:"ttl"` +} + +func (dh *DatasourceHTTP) Init() (err error) { + t := 10 * time.Second + if dh.Timeout != nil { + t = time.Duration(*dh.Timeout) + } + dh.hc = &http.Client{Timeout: t} + + dh.rcache, err = ristretto.NewCache(&ristretto.Config{ + NumCounters: 1e7, + MaxCost: 1 << 30, + BufferItems: 64, + }) + return +} + +func (dh *DatasourceHTTP) ToData(kong *pdk.PDK, data Data) (d map[string]interface{}, err error) { + d, cacheKey, err := dh.getCache(data) + if d != nil || err != nil { + return + } + + ur, err := dh.URL.ToString(data) + if err != nil { + return + } + u, err := url.Parse(ur) + if err != nil { + return + } + + m, err := dh.Method.ToString(data) + if err != nil { + return + } + if m == "" { + m = http.MethodGet + } + + q := u.Query() + for k, v := range dh.QueryParams { + s, err := v.ToString(data) + if err != nil { + return nil, err + } + q.Add(k, s) + } + u.RawQuery = q.Encode() + + b, err := dh.Body.ToString(data) + if err != nil { + return + } + + req, err := http.NewRequest(m, u.String(), strings.NewReader(b)) + if err != nil { + return + } + for h, t := range dh.Headers { + v, err := t.ToString(data) + if err != nil { + return nil, err + } + req.Header.Add(h, v) + } + + res, err := dh.hc.Do(req) + if err != nil { + return + } + + h := map[string]interface{}{} + for k, v := range res.Header { + h[strings.ReplaceAll(strings.ToLower(k), "-", "_")] = v + } + rs, err := io.ReadAll(res.Body) + if err != nil { + return + } + var rj interface{} = map[string]interface{}{} + if err := json.Unmarshal(rs, &rj); err != nil { + rj = string(rs) + } + + d = map[string]interface{}{} + d["response"] = map[string]interface{}{ + "status_code": res.StatusCode, + "headers": h, + "body": rj, + } + err = dh.cache(cacheKey, d, data) + + return +} + +func (dh *DatasourceHTTP) getCache(data Data) (d map[string]interface{}, key string, err error) { + if dh.TTL.Empty() { + return + } + + key, err = dh.CacheKey.ToString(data) + if err != nil { + return + } + v, ok := dh.rcache.Get(key) + if !ok { + return + } + if d, ok = v.(map[string]interface{}); !ok { + return nil, "", nil + } + return +} + +func (dh *DatasourceHTTP) cache(key string, d map[string]interface{}, data map[string]interface{}) (err error) { + if dh.TTL.Empty() { + return + } + + m := map[string]interface{}{"__self__": d} + for k, v := range data { + m[k] = v + } + s, err := dh.TTL.ToString(m) + if err != nil { + return + } + + ttl, err := time.ParseDuration(s) + if err != nil { + d, err := cast.StringToDate(s) + if err != nil { + return err + } + + now := time.Now() + if d.Before(now) { + ttl = 0 + } else { + ttl = d.Sub(now) + } + } + err = nil + if ttl <= 0 { + return + } + + dh.rcache.SetWithTTL(key, d, 1, ttl) + return +} diff --git a/datasource-http_test.go b/datasource-http_test.go new file mode 100644 index 0000000..1a94ac8 --- /dev/null +++ b/datasource-http_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func mustTemplate(t *testing.T, s string) Template { + tmpl, err := NewTemplate(s) + require.NoErrorf(t, err, "invalid template %s", s) + return tmpl +} + +func TestHTTP(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]interface{}{ + "expire_at": time.Now().Add(3 * time.Second).Format(time.RFC3339Nano), + }) + })) + + ds := &DatasourceHTTP{ + URL: mustTemplate(t, svr.URL), + TTL: mustTemplate(t, "{{.__self__.response.body.expire_at}}"), + } + err := ds.Init() + require.NoErrorf(t, err, "error initiating datasource: %s", err) + + _, err = ds.ToData(nil, map[string]interface{}{}) + require.NoErrorf(t, err, "error generating data: %s", err) + + <-time.After(time.Second) + _, ok := ds.rcache.Get("") + assert.True(t, ok, "response should be cached") + + <-time.After(3 * time.Second) + _, ok = ds.rcache.Get("") + assert.False(t, ok, "cache should no longer exist") +} diff --git a/datasource-kong.go b/datasource-kong.go new file mode 100644 index 0000000..b6f1adf --- /dev/null +++ b/datasource-kong.go @@ -0,0 +1,127 @@ +package main + +import ( + "strings" + + "github.com/Kong/go-pdk" +) + +type DatasourceKong struct { + Consumer bool `json:"consumer" yaml:"consumer"` + Context []string `json:"context" yaml:"context"` + RequestHeaders *[]string `json:"request_headers" yaml:"request_headers"` + RequestQueryParams *[]string `json:"request_query_params" yaml:"request_query_params"` +} + +type configDatasourceKong DatasourceKong + +func (dk DatasourceKong) Init() (err error) { + return +} + +func (dk DatasourceKong) ToData(kong *pdk.PDK, data Data) (m map[string]interface{}, err error) { + m = map[string]interface{}{} + + m["consumer"], err = dk.getConsumer(kong) + if err != nil { + return + } + + m["context"], err = dk.getContext(kong) + if err != nil { + return + } + + m["request_headers"], err = dk.getHeaders(kong) + if err != nil { + return + } + + m["request_query_params"], err = dk.getQueryParams(kong) + return +} + +func (dk DatasourceKong) getConsumer(kong *pdk.PDK) (user map[string]interface{}, err error) { + if !dk.Consumer { + return + } + + c, err := kong.Client.GetConsumer() + if err != nil { + return + } + user = map[string]interface{}{ + "id": c.Id, + "custom_id": c.CustomId, + "username": c.Username, + "created_at": c.CreatedAt, + "tags": c.Tags, + } + return +} +func (dk DatasourceKong) getContext(kong *pdk.PDK) (ctx map[string]interface{}, err error) { + if len(dk.Context) == 0 { + return + } + + ctx = map[string]interface{}{} + for _, k := range dk.Context { + ctx[k], err = kong.Ctx.GetSharedAny(k) + if err != nil { + return + } + } + return +} + +func (dk DatasourceKong) getHeaders(kong *pdk.PDK) (h map[string][]string, err error) { + if dk.RequestHeaders == nil { + return + } + + if len(*dk.RequestHeaders) == 0 { + h, err = kong.Request.GetHeaders(-1) + if err != nil { + return + } + for k, v := range h { + delete(h, k) + h[strings.ReplaceAll(strings.ToLower(k), "-", "_")] = v + } + return + } + + h = map[string][]string{} + for _, k := range *dk.RequestHeaders { + v, err := kong.Request.GetHeader(k) + if err != nil { + return nil, err + } + h[strings.ReplaceAll(strings.ToLower(k), "-", "_")] = []string{v} + } + return +} + +func (dk DatasourceKong) getQueryParams(kong *pdk.PDK) (q map[string][]string, err error) { + if dk.RequestQueryParams == nil { + return + } + + if len(*dk.RequestQueryParams) == 0 { + q, err = kong.Request.GetQuery(-1) + if err != nil { + return + } + return + } + + q = map[string][]string{} + for _, k := range *dk.RequestQueryParams { + v, err := kong.Request.GetQueryArg(k) + if err != nil { + return nil, err + } + q[k] = []string{v} + } + return +} diff --git a/datasource-static.go b/datasource-static.go new file mode 100644 index 0000000..bd8fab9 --- /dev/null +++ b/datasource-static.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/Kong/go-pdk" + "gopkg.in/yaml.v3" +) + +type DatasourceStatic struct { + i map[string]interface{} + text []byte +} + +type configDatasourceStatic string + +func (ds DatasourceStatic) Init() (err error) { + return +} + +func (ds DatasourceStatic) ToData(kong *pdk.PDK, data Data) (k map[string]interface{}, err error) { + return ds.i, nil +} + +func (ds *DatasourceStatic) UnmarshalText(text []byte) (err error) { + ds.i = map[string]interface{}{} + if err = yaml.Unmarshal(text, &ds.i); err != nil { + return err + } + ds.text = text + return +} + +func (ds DatasourceStatic) MarshalText() (text []byte, err error) { + return ds.text, nil +} diff --git a/datasource.go b/datasource.go new file mode 100644 index 0000000..4337dd1 --- /dev/null +++ b/datasource.go @@ -0,0 +1,60 @@ +package main + +import ( + "errors" + + "github.com/Kong/go-pdk" +) + +var ( + ErrDatasourceNoID = errors.New("datasource's id can't be empty") + ErrDatasourceIDNotAllowed = errors.New("datasource's id is invalid because the keyword is not allowed") +) + +type Datasource struct { + ID string `json:"id" yaml:"id"` + Type string `json:"type" yaml:"type"` + Static DatasourceStatic `json:"static" yaml:"static"` + Kong DatasourceKong `json:"kong" yaml:"kong"` + HTTP DatasourceHTTP `json:"http" yaml:"http"` +} + +type configDatasource struct { + ID string `json:"id" yaml:"id"` + Type string `json:"type" yaml:"type"` + + Static configDatasourceStatic `json:"static" yaml:"static"` + Kong configDatasourceKong `json:"kong" yaml:"kong"` + HTTP configDatasourceHTTP `json:"http" yaml:"http"` +} + +func (d *Datasource) Init() (err error) { + if d.ID == "" { + return ErrDatasourceNoID + } + if d.ID == "__self__" { + return ErrDatasourceIDNotAllowed + } + + switch d.Type { + case "static": + return d.Static.Init() + case "kong": + return d.Kong.Init() + case "http": + return d.HTTP.Init() + } + return +} + +func (d *Datasource) ToData(kong *pdk.PDK, data Data) (m map[string]interface{}, err error) { + switch d.Type { + case "static": + return d.Static.ToData(kong, data) + case "kong": + return d.Kong.ToData(kong, data) + case "http": + return d.HTTP.ToData(kong, data) + } + return +} diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml new file mode 100644 index 0000000..1704524 --- /dev/null +++ b/examples/docker-compose.yml @@ -0,0 +1,18 @@ +services: + kong: + build: .. + environment: + KONG_DATABASE: off + KONG_DECLARATIVE_CONFIG: /kong/declarative/kong.yaml + KONG_PROXY_ACCESS_LOG: /dev/stdout + KONG_ADMIN_ACCESS_LOG: /dev/stdout + KONG_PROXY_ERROR_LOG: /dev/stderr + KONG_ADMIN_ERROR_LOG: /dev/stderr + KONG_PLUGINS: pre-function,omniplug,post-function + volumes: + - ./kong.yaml:/kong/declarative/kong.yaml + ports: + - 8000:8000 + + echo: + image: ealen/echo-server \ No newline at end of file diff --git a/examples/kong.yaml b/examples/kong.yaml new file mode 100644 index 0000000..7f5edae --- /dev/null +++ b/examples/kong.yaml @@ -0,0 +1,38 @@ +_format_version: "3.0" +_transform: true +services: + - host: mockbin.org + name: example_service + port: 80 + protocol: http + routes: + - name: example_route + paths: + - / + strip_path: true + plugins: + - name: pre-function + config: + access: + - kong.ctx.shared.foo = "bar" + - name: omniplug + enabled: true + config: + datasources: + - id: kong + type: kong + kong: + context: + - foo + injector: + context: + omniplug: + foo: baz + headers: + foo: "{{ .kong.context.foo }}" + query_params: + foo: "{{ .kong.context.foo }}" + - name: post-function + config: + access: + - kong.response.set_header("foo", kong.ctx.shared.omniplug.foo) diff --git a/go.mod b/go.mod index cbaf6e4..1ceae15 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,39 @@ -module github.com/telkomindonesia/kong-plugin-reqenrich +module github.com/telkomindonesia/kong-plugin-omniplug go 1.20 -require github.com/Kong/go-pdk v0.10.0 +require ( + github.com/Kong/go-pdk v0.10.0 + github.com/spf13/cast v1.5.1 + gopkg.in/yaml.v3 v3.0.1 +) require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.0 // indirect github.com/alecthomas/repr v0.3.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/google/uuid v1.1.1 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/huandu/xstrings v1.3.3 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/spf13/cast v1.3.1 // indirect golang.org/x/crypto v0.3.0 // indirect + golang.org/x/sys v0.2.0 // indirect ) require ( github.com/Masterminds/sprig/v3 v3.2.3 github.com/alecthomas/assert/v2 v2.4.0 + github.com/stretchr/testify v1.8.4 github.com/ugorji/go/codec v1.2.7 // indirect google.golang.org/protobuf v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index b991a37..29e70a7 100644 --- a/go.sum +++ b/go.sum @@ -10,12 +10,22 @@ github.com/alecthomas/assert/v2 v2.4.0 h1:/ZiZ0NnriAWPYYO+4eOjgzNELrFQLaHNr92mHS github.com/alecthomas/assert/v2 v2.4.0/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= @@ -24,20 +34,29 @@ github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4 github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= @@ -58,6 +77,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -70,12 +91,14 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/injector.go b/injector.go new file mode 100644 index 0000000..0936834 --- /dev/null +++ b/injector.go @@ -0,0 +1,55 @@ +package main + +import "github.com/Kong/go-pdk" + +type Injector struct { + Context map[string]map[string]Template `json:"context" yaml:"contexts"` + Headers map[string]Template `json:"headers" yaml:"headers"` + QueryParams map[string]Template `json:"query_params" yaml:"query_params"` +} + +type configInjector struct { + Context map[string]map[string]string `json:"context" yaml:"context"` + Headers map[string]string `json:"headers" yaml:"headers"` + QueryParams map[string]string `json:"query_params" yaml:"query_params"` +} + +func (i Injector) Inject(kong *pdk.PDK, data Data) (err error) { + for k, v := range i.Context { + m := map[string]interface{}{} + for k1, v1 := range v { + s, err := v1.ToString(data) + if err != nil { + return err + } + m[k1] = s + } + kong.Ctx.SetShared(k, m) + } + + h := map[string][]string{} + for k, v := range i.Headers { + s, err := v.ToString(data) + if err != nil { + return err + } + h[k] = []string{s} + } + if len(h) > 0 { + kong.ServiceRequest.SetHeaders(h) + } + + q := map[string][]string{} + for k, v := range i.QueryParams { + s, err := v.ToString(data) + if err != nil { + return err + } + q[k] = []string{s} + } + if len(q) > 0 { + kong.ServiceRequest.SetQuery(q) + } + + return +} diff --git a/kong.conf b/kong.conf new file mode 100644 index 0000000..611d726 --- /dev/null +++ b/kong.conf @@ -0,0 +1,6 @@ +plugins = omniplug + +pluginserver_names = omniplug +pluginserver_omniplug_socket = /usr/local/kong/omniplug.socket +pluginserver_omniplug_start_cmd = /usr/local/bin/omniplug +pluginserver_omniplug_query_cmd = /usr/local/bin/omniplug -dump \ No newline at end of file diff --git a/main.go b/main.go index 6a22545..92883e5 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( var version = "0.1.0" var priority = func() int { - s := os.Getenv("KONG_PLUGIN_REQENRICH_PRIORITY") + s := os.Getenv("KONG_PLUGIN_OMNIPLUG_PRIORITY") i, err := strconv.Atoi(s) if err != nil { return 810 @@ -18,5 +18,5 @@ var priority = func() int { }() func main() { - server.StartServer(New, version, priority) + server.StartServer(NewConfig, version, priority) } diff --git a/omniplug.go b/omniplug.go new file mode 100644 index 0000000..794bb39 --- /dev/null +++ b/omniplug.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + + "github.com/Kong/go-pdk" +) + +type Data map[string]interface{} + +type Omniplug struct { + Datasources []*Datasource `json:"datasources" yaml:"datasources"` + Injector Injector `json:"injector" yaml:"injector"` +} + +func (r *Omniplug) Init() (err error) { + m := map[string]struct{}{} + for _, d := range r.Datasources { + if err = d.Init(); err != nil { + return + } + if _, ok := m[d.ID]; ok { + return fmt.Errorf("duplicate datasource id: %s", d.ID) + } + m[d.ID] = struct{}{} + } + return +} + +func (r *Omniplug) Access(kong *pdk.PDK) { + data, err := r.ResolvData(kong) + if err != nil { + kong.Response.Exit(500, []byte(err.Error()), nil) + } + + err = r.Injector.Inject(kong, data) + if err != nil { + kong.Response.Exit(500, []byte(err.Error()), nil) + } +} + +func (r *Omniplug) ResolvData(kong *pdk.PDK) (data Data, err error) { + data = map[string]interface{}{} + for _, d := range r.Datasources { + data[d.ID], err = d.ToData(kong, data) + if err != nil { + return data, err + } + } + return +} diff --git a/reqenrich.go b/reqenrich.go deleted file mode 100644 index c42c47e..0000000 --- a/reqenrich.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "bytes" - "strings" - "text/template" - - "github.com/Kong/go-pdk" - "github.com/Masterminds/sprig/v3" -) - -type Data []map[string]interface{} - -type ReqEnrich struct { - Datasource []Datasource `json:"datasource"` - Injector Injector `json:"injector"` -} - -func New() interface{} { - return &ReqEnrich{} -} - -func (conf *ReqEnrich) Access(kong *pdk.PDK) { - data, err := conf.ResolvData(kong) - if err != nil { - kong.Response.Exit(500, []byte(err.Error()), nil) - } - - err = conf.Injector.Inject(kong, data) - if err != nil { - kong.Response.Exit(500, []byte(err.Error()), nil) - } -} - -func (conf *ReqEnrich) ResolvData(kong *pdk.PDK) (data Data, err error) { - for _, d := range conf.Datasource { - switch d.Type { - case "static": - data = append(data, d.Static) - case "kong": - k, err := d.Kong.ToData(kong) - if err != nil { - return nil, err - } - data = append(data, k) - } - } - return -} - -type Datasource struct { - Type string `json:"type"` - Static map[string]interface{} `json:"static"` - Kong DatasourceKong `json:"kong"` -} - -type DatasourceKong struct { - Consumer bool `json:"consumer"` - RequestHeaders bool `json:"request_headers"` - RequestQueryParams bool `json:"request_query_params"` -} - -func (dk DatasourceKong) ToData(kong *pdk.PDK) (k map[string]interface{}, err error) { - k = map[string]interface{}{} - if dk.Consumer { - k["consumer"], err = kong.Client.GetConsumer() - if err != nil { - return - } - } - if dk.RequestHeaders { - m, err := kong.Request.GetHeaders(-1) - if err != nil { - return nil, err - } - for k, v := range m { - delete(m, k) - m[strings.ReplaceAll(strings.ToLower(k), "-", "_")] = v - } - k["request_headers"] = m - } - if dk.RequestQueryParams { - k["request_query_params"], err = kong.Request.GetQuery(-1) - if err != nil { - return - } - } - return -} - -type Injector struct { - Headers map[string]Template `json:"headers"` - QueryParams map[string]Template `json:"query_params"` -} - -func (i Injector) Inject(kong *pdk.PDK, data Data) (err error) { - d := map[string]interface{}{"data": data} - - h := map[string][]string{} - for k, v := range i.Headers { - s, err := v.ToString(d) - if err != nil { - return err - } - h[k] = []string{s} - } - if len(h) > 0 { - kong.ServiceRequest.SetHeaders(h) - } - - q := map[string][]string{} - for k, v := range i.QueryParams { - s, err := v.ToString(d) - if err != nil { - return err - } - h[k] = []string{s} - } - if len(q) > 0 { - kong.ServiceRequest.SetQuery(q) - } - - return -} - -type Template struct { - *template.Template - - text []byte -} - -func NewTemplate(s string) (Template, error) { - t := Template{} - return t, t.UnmarshalText([]byte(s)) -} - -func (t *Template) UnmarshalText(text []byte) (err error) { - t.text = text - t.Template, err = template.New("template").Funcs(sprig.FuncMap()).Parse(string(text)) - return -} - -func (t Template) MarshalText() (text []byte, err error) { - return t.text, nil -} - -func (t Template) ToString(data interface{}) (s string, err error) { - buf := new(bytes.Buffer) - if err = t.Execute(buf, data); err != nil { - return - } - return buf.String(), nil -} diff --git a/reqenrich_test.go b/reqenrich_test.go deleted file mode 100644 index 45c2705..0000000 --- a/reqenrich_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "net/http" - "testing" - - "github.com/Kong/go-pdk/test" - "github.com/alecthomas/assert/v2" -) - -func TestPlugin(t *testing.T) { - env, err := test.New(t, test.Request{ - Method: "GET", - Url: "http://example.com?q=search&x=9", - Headers: http.Header{ - "X-Test": []string{"test"}, - }, - }) - assert.NoError(t, err) - - tmpl1, err := NewTemplate("{{ (index (index .data 0) (index (index .data 1).request_headers.x_test 0)).message }}") - assert.NoError(t, err) - - re := &ReqEnrich{ - Datasource: []Datasource{ - { - Type: "static", - Static: map[string]interface{}{ - "test": map[string]interface{}{ - "message": "world", - }, - }, - }, - { - Type: "kong", - Kong: DatasourceKong{ - RequestHeaders: true, - }, - }, - }, - Injector: Injector{ - Headers: map[string]Template{ - "hello": tmpl1, - }, - }, - } - env.DoHttps(re) - t.Log(string(env.ClientRes.Body)) - assert.Equal(t, 200, env.ClientRes.Status) - assert.Equal(t, "world", env.ServiceReq.Headers.Get("hello")) -} diff --git a/template.go b/template.go new file mode 100644 index 0000000..29edf8e --- /dev/null +++ b/template.go @@ -0,0 +1,45 @@ +package main + +import ( + "bytes" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +type Template struct { + *template.Template + + text []byte +} + +func NewTemplate(s string) (Template, error) { + t := Template{} + return t, t.UnmarshalText([]byte(s)) +} + +func (t Template) ToString(data interface{}) (s string, err error) { + if t.Empty() { + return string(t.text), nil + } + + buf := new(bytes.Buffer) + if err = t.Execute(buf, data); err != nil { + return + } + return buf.String(), nil +} + +func (t Template) Empty() bool { + return t.Template == nil +} + +func (t *Template) UnmarshalText(text []byte) (err error) { + t.Template, err = template.New("template").Funcs(sprig.FuncMap()).Parse(string(text)) + t.text = text + return +} + +func (t Template) MarshalText() (text []byte, err error) { + return t.text, nil +} diff --git a/testdata/config.yaml b/testdata/config.yaml new file mode 100644 index 0000000..67fe40d --- /dev/null +++ b/testdata/config.yaml @@ -0,0 +1,55 @@ +datasources: + - id: static_data + type: static + static: | + message: + hello: world + - id: kong_data + type: kong + kong: + consumer: true + request_headers: [] + request_query_params: [] + - id: kong_data_explicit + type: kong + kong: + consumer: true + request_headers: + - x-foo + request_query_params: + - x_foo + - id: mockbin_data + type: http + # All attributes support go templating with + http: + url: '{{ env "TEST_SERVER_URL" }}' + method: POST + query_params: + expire_at: "{{ index .kong_data.request_query_params.expire_at 0}}" # Reference previous data source + headers: + content-type: application/json + body: '{"hello": "world"}' + timeout: 30s + cache-key: "{{ .static_data.message.hello }}" + ttl: "{{ index .__self__.response.body.queryString.expire_at 0 }}" # `__self__` can only be used here since `ttl` is used after the HTTP Response is received +injector: + # unfortunately, this is not testable with current SDK, so it is commented outs + # context: + # key1: + # subkey1: string1 + # subkey2: string2 + # key2: + # subkey3: string1 + # subkey4: string2 + headers: + hello: "{{ .static_data.message.hello }}" + username: "{{ .kong_data.consumer.username }}" + foo: "{{ index .kong_data.request_headers.x_foo 0 }}" + foo-ex: "{{ index .kong_data_explicit.request_headers.x_foo 0 }}" + date: "{{ index .mockbin_data.response.body.startedDateTime 0 }}" + query_params: + hello: "{{ .static_data.message.hello }}" + username: "{{ .kong_data.consumer.username }}" + foo: "{{ index .kong_data_explicit.request_query_params.x_foo 0 }}" + foo-ex: "{{ index .kong_data_explicit.request_query_params.x_foo 0 }}" + date: "{{ index .mockbin_data.response.body.startedDateTime 0 }}"