Skip to content

Commit 1559db3

Browse files
committed
Init
1 parent a2ff60d commit 1559db3

19 files changed

Lines changed: 1157 additions & 0 deletions

.editorconfig

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# editorconfig.org
2+
3+
root = true
4+
5+
[*]
6+
charset = utf-8
7+
end_of_line = lf
8+
insert_final_newline = true
9+
indent_style = space
10+
indent_size = 4
11+
trim_trailing_whitespace = true
12+
13+
[*.md]
14+
trim_trailing_whitespace = false

.github/workflows/build.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: build
2+
3+
on:
4+
push:
5+
branches-ignore:
6+
- master
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Setup Go
13+
uses: actions/setup-go@v4
14+
with:
15+
go-version: '1.25.7'
16+
cache: false
17+
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Golangci Lint
22+
uses: golangci/golangci-lint-action@v9
23+
24+
- name: Run tests
25+
run: go test -race
26+
27+
- name: Check plugin
28+
run: docker run --rm -v "$PWD/go.sum:/app/go.sum" -w /app krakend:2.13.1 krakend check-plugin -g 1.25.7 -l "MUSL-1.2.5_(alpine-3.23.3)" -s ./go.sum

.github/workflows/release.yml

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: release
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Setup Go
13+
uses: actions/setup-go@v4
14+
with:
15+
go-version: '1.25.7'
16+
cache: false
17+
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
21+
- name: Golangci Lint
22+
uses: golangci/golangci-lint-action@v9
23+
24+
- name: Run tests
25+
run: go test -race
26+
27+
- name: Build project
28+
run: |
29+
docker run --rm -v "$PWD:/app" -w /app krakend/builder:2.13.1 go build -buildmode=plugin -o krakend-fallback.so .
30+
docker run --rm -v "$PWD/krakend-fallback.so:/app/krakend-fallback.so" -w /app krakend:2.13.1 krakend test-plugin -sc krakend-fallback.so
31+
zip ./krakend-fallback.zip ./krakend-fallback.so
32+
33+
- name: Bump version and push tag
34+
id: bump_tag
35+
uses: anothrNick/github-tag-action@1.61.0
36+
env:
37+
GITHUB_TOKEN: ${{ secrets.JENKSY_GITHUB_TOKEN }}
38+
WITH_V: true
39+
40+
- name: Create Release
41+
id: create_release
42+
uses: actions/create-release@v1.0.0
43+
env:
44+
GITHUB_TOKEN: ${{ secrets.JENKSY_GITHUB_TOKEN }}
45+
with:
46+
tag_name: ${{ steps.bump_tag.outputs.new_tag }}
47+
release_name: Release ${{ steps.bump_tag.outputs.new_tag }}
48+
draft: false
49+
prerelease: false
50+
51+
- name: Upload Release Asset
52+
id: upload-release-asset
53+
uses: actions/upload-release-asset@v1.0.1
54+
env:
55+
GITHUB_TOKEN: ${{ secrets.JENKSY_GITHUB_TOKEN }}
56+
with:
57+
upload_url: ${{ steps.create_release.outputs.upload_url }}
58+
asset_path: ./krakend-fallback.zip
59+
asset_name: krakend-fallback.zip
60+
asset_content_type: application/zip

backend_error.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"io"
6+
"net/http"
7+
"sort"
8+
"strings"
9+
10+
"github.com/mitchellh/mapstructure"
11+
)
12+
13+
type BackendError struct {
14+
Status int `mapstructure:"http_status_code"`
15+
Body string `mapstructure:"http_body"`
16+
Encoding string `mapstructure:"http_body_encoding"`
17+
}
18+
19+
func (e BackendError) ToResponse() *http.Response {
20+
enc := e.Encoding
21+
if enc == "" {
22+
enc = "text/plain"
23+
}
24+
return &http.Response{
25+
StatusCode: e.Status,
26+
Header: http.Header{"Content-Type": []string{enc}},
27+
Body: io.NopCloser(bytes.NewBufferString(e.Body)),
28+
ContentLength: int64(len(e.Body)),
29+
}
30+
}
31+
32+
func FindBackendError(body map[string]interface{}) (*http.Response, bool) {
33+
keys := make([]string, 0, len(body))
34+
for k := range body {
35+
if strings.HasPrefix(k, "error_") {
36+
keys = append(keys, k)
37+
}
38+
}
39+
if len(keys) == 0 {
40+
return nil, false
41+
}
42+
43+
sort.Strings(keys)
44+
v, ok := body[keys[0]]
45+
if !ok {
46+
return nil, false
47+
}
48+
49+
var berr BackendError
50+
51+
if err := mapstructure.WeakDecode(v, &berr); err != nil {
52+
return nil, false
53+
}
54+
55+
return berr.ToResponse(), true
56+
}

backend_error_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package main
2+
3+
import (
4+
"io"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func readString(t *testing.T, rc io.ReadCloser) string {
11+
t.Helper()
12+
defer func(rc io.ReadCloser) {
13+
err := rc.Close()
14+
assert.NoError(t, err)
15+
}(rc)
16+
b, err := io.ReadAll(rc)
17+
assert.NoError(t, err)
18+
return string(b)
19+
}
20+
21+
func TestFindBackendError_NoKeys(t *testing.T) {
22+
resp, ok := FindBackendError(map[string]interface{}{
23+
"product": map[string]interface{}{"id": 1},
24+
})
25+
26+
assert.False(t, ok)
27+
assert.Nil(t, resp)
28+
}
29+
30+
func TestFindBackendError_PicksFirstSortedKey(t *testing.T) {
31+
body := map[string]interface{}{
32+
"error_2": map[string]interface{}{
33+
"http_status_code": 502,
34+
"http_body": `{"message":"second"}`,
35+
"http_body_encoding": "application/json",
36+
},
37+
"error_1": map[string]interface{}{
38+
"http_status_code": 500,
39+
"http_body": `{"message":"first"}`,
40+
"http_body_encoding": "application/json; charset=utf-8",
41+
},
42+
"product": map[string]interface{}{"id": 1},
43+
}
44+
45+
resp, ok := FindBackendError(body)
46+
assert.True(t, ok)
47+
assert.NotNil(t, resp)
48+
49+
assert.Equal(t, 500, resp.StatusCode)
50+
assert.Equal(t, "application/json; charset=utf-8", resp.Header.Get("Content-Type"))
51+
52+
gotBody := readString(t, resp.Body)
53+
assert.Equal(t, `{"message":"first"}`, gotBody)
54+
assert.Equal(t, int64(len(gotBody)), resp.ContentLength)
55+
}
56+
57+
func TestFindBackendError_InvalidShape_ReturnsFalse(t *testing.T) {
58+
body := map[string]interface{}{
59+
"error_1": "not an object",
60+
}
61+
62+
resp, ok := FindBackendError(body)
63+
assert.False(t, ok)
64+
assert.Nil(t, resp)
65+
}
66+
67+
func TestFindBackendErrorMissingEncoding(t *testing.T) {
68+
body := map[string]interface{}{
69+
"error_1": map[string]interface{}{
70+
"http_status_code": 503,
71+
"http_body": "service unavailable",
72+
},
73+
}
74+
75+
resp, ok := FindBackendError(body)
76+
assert.True(t, ok)
77+
assert.NotNil(t, resp)
78+
assert.Equal(t, 503, resp.StatusCode)
79+
assert.Equal(t, "text/plain", resp.Header.Get("Content-Type"))
80+
81+
got := readString(t, resp.Body)
82+
assert.Equal(t, `service unavailable`, got)
83+
}

config.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
6+
"github.com/mitchellh/mapstructure"
7+
)
8+
9+
const Namespace = "onliner/krakend-fallback"
10+
11+
type Config struct {
12+
Routes []Route `mapstructure:"routes"`
13+
}
14+
15+
type Route struct {
16+
Path string `mapstructure:"path"`
17+
Required []string `mapstructure:"required"`
18+
Default map[string]interface{} `mapstructure:"default"`
19+
}
20+
21+
func NewConfig(input map[string]interface{}) (*Config, error) {
22+
raw, ok := input[Namespace].(map[string]interface{})
23+
if !ok {
24+
return nil, errors.New("configuration not found")
25+
}
26+
27+
var cfg Config
28+
29+
if err := mapstructure.WeakDecode(raw, &cfg); err != nil {
30+
return nil, err
31+
}
32+
33+
return &cfg, nil
34+
}
35+
36+
func (cfg *Config) MatchRoute(path string) (Route, bool) {
37+
for _, r := range cfg.Routes {
38+
if MatchPathTemplate(r.Path, path) {
39+
return r, true
40+
}
41+
}
42+
43+
return Route{}, false
44+
}

config_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestNewConfig_ConfigNotFound(t *testing.T) {
12+
_, err := NewConfig(map[string]interface{}{})
13+
assert.Error(t, errors.New("configuration not found"), err)
14+
}
15+
16+
func TestNewConfig_DecodeOK(t *testing.T) {
17+
raw := []byte(`{
18+
"onliner/krakend-fallback": {
19+
"routes": [
20+
{
21+
"path": "/products/{product}/positions",
22+
"required": ["product"],
23+
"default": {"positions": [], "shops": null}
24+
},
25+
{
26+
"path": "/health",
27+
"required": [],
28+
"default": {}
29+
}
30+
]
31+
}
32+
}`)
33+
34+
var input map[string]interface{}
35+
err := json.Unmarshal(raw, &input)
36+
assert.NoError(t, err)
37+
38+
cfg, err := NewConfig(input)
39+
assert.NoError(t, err)
40+
assert.NotNil(t, cfg)
41+
assert.Equal(t, 2, len(cfg.Routes))
42+
43+
r0 := cfg.Routes[0]
44+
assert.Equal(t, "/products/{product}/positions", r0.Path)
45+
assert.Equal(t, len(r0.Required), 1)
46+
assert.Equal(t, "product", r0.Required[0])
47+
48+
_, ok := r0.Default["positions"]
49+
assert.True(t, ok)
50+
_, ok = r0.Default["shops"]
51+
assert.True(t, ok)
52+
}
53+
54+
func TestConfig_MatchRoute_MatchesTemplate(t *testing.T) {
55+
cfg := &Config{
56+
Routes: []Route{
57+
{
58+
Path: "/products/{product}/positions",
59+
Required: []string{"product"},
60+
Default: map[string]interface{}{"positions": []interface{}{}},
61+
},
62+
{
63+
Path: "/health",
64+
Required: nil,
65+
Default: nil,
66+
},
67+
},
68+
}
69+
70+
route, ok := cfg.MatchRoute("/products/iphonex64s/positions")
71+
assert.True(t, ok)
72+
assert.Equal(t, "/products/{product}/positions", route.Path)
73+
}
74+
75+
func TestConfig_MatchRoute_NoMatch(t *testing.T) {
76+
cfg := &Config{
77+
Routes: []Route{
78+
{Path: "/a/{x}/b"},
79+
},
80+
}
81+
82+
_, ok := cfg.MatchRoute("/a/1/c")
83+
assert.False(t, ok)
84+
}

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/onliner/krakend-fallback
2+
3+
go 1.25.7
4+
5+
require (
6+
github.com/mitchellh/mapstructure v1.5.0
7+
github.com/stretchr/testify v1.11.1
8+
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
12+
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
gopkg.in/yaml.v3 v3.0.1 // indirect
14+
)

0 commit comments

Comments
 (0)