mirror of
https://github.com/eliasstepanik/caddy-ingess.git
synced 2026-01-10 20:18:28 +00:00
feat: Add experimental "smart" sort plugin (#90)
* feat: Add experimental "smart" sort plugin * Add tests and ci for tests * fix main test error
This commit is contained in:
parent
cce8e52ddd
commit
16312f5480
24
.github/workflows/tests.yml
vendored
Normal file
24
.github/workflows/tests.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: Go
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-test:
|
||||||
|
name: Build and Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.16
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v ./...
|
||||||
@ -46,7 +46,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get client to access the kubernetes service api
|
// get client to access the kubernetes service api
|
||||||
kubeClient, err := createApiserverClient(logger)
|
kubeClient, _, err := createApiserverClient(logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Could not establish a connection to the Kubernetes API Server. %v", err)
|
logger.Fatalf("Could not establish a connection to the Kubernetes API Server. %v", err)
|
||||||
}
|
}
|
||||||
@ -71,10 +71,10 @@ func main() {
|
|||||||
|
|
||||||
// createApiserverClient creates a new Kubernetes REST client. We assume the
|
// createApiserverClient creates a new Kubernetes REST client. We assume the
|
||||||
// controller runs inside Kubernetes and use the in-cluster config.
|
// controller runs inside Kubernetes and use the in-cluster config.
|
||||||
func createApiserverClient(logger *zap.SugaredLogger) (*kubernetes.Clientset, error) {
|
func createApiserverClient(logger *zap.SugaredLogger) (*kubernetes.Clientset, *version.Info, error) {
|
||||||
cfg, err := clientcmd.BuildConfigFromFlags("", "")
|
cfg, err := clientcmd.BuildConfigFromFlags("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Creating API client for %s", cfg.Host)
|
logger.Infof("Creating API client for %s", cfg.Host)
|
||||||
@ -84,7 +84,7 @@ func createApiserverClient(logger *zap.SugaredLogger) (*kubernetes.Clientset, er
|
|||||||
cfg.ContentType = "application/vnd.kubernetes.protobuf"
|
cfg.ContentType = "application/vnd.kubernetes.protobuf"
|
||||||
client, err := kubernetes.NewForConfig(cfg)
|
client, err := kubernetes.NewForConfig(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The client may fail to connect to the API server on the first request
|
// The client may fail to connect to the API server on the first request
|
||||||
@ -113,12 +113,12 @@ func createApiserverClient(logger *zap.SugaredLogger) (*kubernetes.Clientset, er
|
|||||||
|
|
||||||
// err is returned in case of timeout in the exponential backoff (ErrWaitTimeout)
|
// err is returned in case of timeout in the exponential backoff (ErrWaitTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, lastErr
|
return nil, nil, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if retries > 0 {
|
if retries > 0 {
|
||||||
logger.Warnf("Initial connection to the Kubernetes API server was retried %d times.", retries)
|
logger.Warnf("Initial connection to the Kubernetes API server was retried %d times.", retries)
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
return client, v, nil
|
||||||
}
|
}
|
||||||
|
|||||||
76
internal/caddy/global/ingress_sort.go
Normal file
76
internal/caddy/global/ingress_sort.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package global
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"github.com/caddyserver/ingress/pkg/converter"
|
||||||
|
"github.com/caddyserver/ingress/pkg/store"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IngressSortPlugin struct{}
|
||||||
|
|
||||||
|
func (p IngressSortPlugin) IngressPlugin() converter.PluginInfo {
|
||||||
|
return converter.PluginInfo{
|
||||||
|
Name: "ingress_sort",
|
||||||
|
// Must go after ingress are configured
|
||||||
|
Priority: -2,
|
||||||
|
New: func() converter.Plugin { return new(IngressSortPlugin) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
converter.RegisterPlugin(IngressSortPlugin{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFirstItemFromJSON(data json.RawMessage) string {
|
||||||
|
var arr []string
|
||||||
|
err := json.Unmarshal(data, &arr)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return arr[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortRoutes(routes caddyhttp.RouteList) {
|
||||||
|
sort.SliceStable(routes, func(i, j int) bool {
|
||||||
|
iPath := getFirstItemFromJSON(routes[i].MatcherSetsRaw[0]["path"])
|
||||||
|
jPath := getFirstItemFromJSON(routes[j].MatcherSetsRaw[0]["path"])
|
||||||
|
iPrefixed := strings.HasSuffix(iPath, "*")
|
||||||
|
jPrefixed := strings.HasSuffix(jPath, "*")
|
||||||
|
|
||||||
|
// If both same type check by length
|
||||||
|
if iPrefixed == jPrefixed {
|
||||||
|
return len(jPath) < len(iPath)
|
||||||
|
}
|
||||||
|
// Empty path will be moved last
|
||||||
|
if jPath == "" || iPath == "" {
|
||||||
|
return jPath == ""
|
||||||
|
}
|
||||||
|
// j path is exact so should go first
|
||||||
|
return jPrefixed
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalHandler in IngressSortPlugin tries to sort routes to have the less conflict.
|
||||||
|
//
|
||||||
|
// It only supports basic conflicts for now. It doesn't support multiple matchers in the same route
|
||||||
|
// nor multiple path/host in the matcher. It shouldn't be an issue with the ingress.matcher plugin.
|
||||||
|
// Sort will prioritize exact paths then prefix paths and finally empty paths.
|
||||||
|
// When 2 exacts paths or 2 prefixed paths are on the same host, we choose the longer first.
|
||||||
|
func (p IngressSortPlugin) GlobalHandler(config *converter.Config, store *store.Store) error {
|
||||||
|
if !store.ConfigMap.ExperimentalSmartSort {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
routes := config.GetHTTPServer().Routes
|
||||||
|
|
||||||
|
sortRoutes(routes)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface guards
|
||||||
|
var (
|
||||||
|
_ = converter.GlobalMiddleware(IngressSortPlugin{})
|
||||||
|
)
|
||||||
108
internal/caddy/global/ingress_sort_test.go
Normal file
108
internal/caddy/global/ingress_sort_test.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package global
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIngressSort(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
routes []struct {
|
||||||
|
id int
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
expect []int
|
||||||
|
}{
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "multiple exact paths",
|
||||||
|
routes: []struct {
|
||||||
|
id int
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{id: 0, path: "/path/a"},
|
||||||
|
{id: 1, path: "/path/"},
|
||||||
|
{id: 2, path: "/other"},
|
||||||
|
},
|
||||||
|
expect: []int{0, 1, 2},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple prefix paths",
|
||||||
|
routes: []struct {
|
||||||
|
id int
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{id: 0, path: "/path/*"},
|
||||||
|
{id: 1, path: "/path/auth/*"},
|
||||||
|
{id: 2, path: "/other/*"},
|
||||||
|
{id: 3, path: "/login/*"},
|
||||||
|
},
|
||||||
|
expect: []int{1, 2, 3, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed exact and prefixed",
|
||||||
|
routes: []struct {
|
||||||
|
id int
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{id: 0, path: "/path/*"},
|
||||||
|
{id: 1, path: "/path/auth/"},
|
||||||
|
{id: 2, path: "/path/v2/*"},
|
||||||
|
{id: 3, path: "/path/new"},
|
||||||
|
},
|
||||||
|
expect: []int{1, 3, 2, 0},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed exact, prefix and empty",
|
||||||
|
routes: []struct {
|
||||||
|
id int
|
||||||
|
path string
|
||||||
|
}{
|
||||||
|
{id: 0, path: "/path/*"},
|
||||||
|
{id: 1, path: ""},
|
||||||
|
{id: 2, path: "/path/v2/*"},
|
||||||
|
{id: 3, path: "/path/new"},
|
||||||
|
{id: 4, path: ""},
|
||||||
|
},
|
||||||
|
expect: []int{3, 2, 0, 1, 4},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
routes := []caddyhttp.Route{}
|
||||||
|
|
||||||
|
for _, route := range test.routes {
|
||||||
|
match := caddy.ModuleMap{}
|
||||||
|
match["id"] = caddyconfig.JSON(route.id, nil)
|
||||||
|
|
||||||
|
if route.path != "" {
|
||||||
|
match["path"] = caddyconfig.JSON(caddyhttp.MatchPath{route.path}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := caddyhttp.Route{MatcherSetsRaw: []caddy.ModuleMap{match}}
|
||||||
|
routes = append(routes, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
sortRoutes(routes)
|
||||||
|
|
||||||
|
var got []int
|
||||||
|
for i := range test.expect {
|
||||||
|
var currentId int
|
||||||
|
err := json.Unmarshal(routes[i].MatcherSetsRaw[0]["id"], ¤tId)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error unmarshaling id for i %v, %v", i, err)
|
||||||
|
}
|
||||||
|
got = append(got, currentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(test.expect, got) {
|
||||||
|
t.Errorf("expected order to match: got %v, expected %v, %s", got, test.expect, routes[1].MatcherSetsRaw)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ type ConfigMapOptions struct {
|
|||||||
Debug bool `json:"debug,omitempty"`
|
Debug bool `json:"debug,omitempty"`
|
||||||
AcmeCA string `json:"acmeCA,omitempty"`
|
AcmeCA string `json:"acmeCA,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
|
ExperimentalSmartSort bool `json:"experimentalSmartSort,omitempty"`
|
||||||
ProxyProtocol bool `json:"proxyProtocol,omitempty"`
|
ProxyProtocol bool `json:"proxyProtocol,omitempty"`
|
||||||
Metrics bool `json:"metrics,omitempty"`
|
Metrics bool `json:"metrics,omitempty"`
|
||||||
OnDemandTLS bool `json:"onDemandTLS,omitempty"`
|
OnDemandTLS bool `json:"onDemandTLS,omitempty"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user