Skip to content

Commit

Permalink
Add free endpoints (#90)
Browse files Browse the repository at this point in the history
* Add free endpoints

* Documentation Update

* Add backend to the server block

* Add more context to error message

no quoting due to (escaped) json log

Co-authored-by: Alex Schneider <alex.schneider@sevenval.com>
Co-authored-by: Marcel Ludwig <marcel.ludwig@avenga.com>
  • Loading branch information
3 people authored Jan 27, 2021
1 parent 4b25ece commit a23cfa7
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 71 deletions.
19 changes: 13 additions & 6 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) {

file.Server = append(file.Server, srv)

serverBodies, err := mergeBackendBodies(backends, srv)
if err != nil {
return nil, err
}
srv.Remain = MergeBodies(serverBodies)

// api block(s)
for _, apiBlock := range []*config.Api{srv.API} {
if apiBlock == nil {
Expand All @@ -129,17 +135,18 @@ func LoadConfig(body hcl.Body, src []byte) (*config.CouperFile, error) {
return nil, err
}

bodies = appendUniqueBodies(serverBodies, bodies...)

// empty bodies would be removed with a hcl.Merge.. later on.
if err = refineEndpoints(backends, bodies, apiBlock.Endpoints); err != nil {
return nil, err
}
}

// standalone endpoints
// TODO: free endpoints
//if err := refineEndpoints(file.Definitions, nil, srv.Endpoints); err != nil {
// return nil, err
//}
if err := refineEndpoints(backends, nil, srv.Endpoints); err != nil {
return nil, err
}
}

if len(file.Server) == 0 {
Expand Down Expand Up @@ -172,7 +179,7 @@ func mergeBackendBodies(backendList Backends, inlineBackend config.Inline) ([]hc
return nil, fmt.Errorf("configuration error: inlineBackend reference and inline definition")
}
// we have a reference, append to list and...
bodies = append(bodies, reference)
bodies = appendUniqueBodies(bodies, reference)
}
// ...additionally add the inline overrides.
if content != nil && len(content.Attributes) > 0 {
Expand All @@ -198,7 +205,7 @@ func mergeBackendBodies(backendList Backends, inlineBackend config.Inline) ([]hc
bodies = append([]hcl.Body{ref}, bodies...)
}

bodies = append(bodies, backends[0].Body)
bodies = appendUniqueBodies(bodies, backends[0].Body)
}

return bodies, nil
Expand Down
115 changes: 81 additions & 34 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@ type HandlerKind uint8

const (
KindAPI HandlerKind = iota
KindEndpoint
KindFiles
KindSPA
)

type endpointList map[*config.Endpoint]HandlerKind

// NewServerConfiguration sets http handler specific defaults and validates the given gateway configuration.
// Wire up all endpoints and maps them within the returned Server.
func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerConfiguration, error) {
Expand Down Expand Up @@ -125,59 +128,85 @@ func NewServerConfiguration(conf *config.CouperFile, log *logrus.Entry) (ServerC
}
}

if srvConf.API != nil {
// map backends to endpoint
endpoints := make(map[string]bool)
endpointsPatterns := make(map[string]bool)

for endpoint, epType := range getEndpointsList(srvConf) {
var basePath string
var cors *config.CORS
var errTpl *errors.Template

switch epType {
case KindAPI:
basePath = serverOptions.APIBasePath
cors = srvConf.API.CORS
errTpl = serverOptions.APIErrTpl
case KindEndpoint:
basePath = serverOptions.SrvBasePath
errTpl = serverOptions.ServerErrTpl
}

for _, endpoint := range srvConf.API.Endpoints {
pattern := utils.JoinPath("/", serverOptions.APIBasePath, endpoint.Pattern)
pattern := utils.JoinPath(basePath, endpoint.Pattern)
unique, cleanPattern := isUnique(endpointsPatterns, pattern)
if !unique {
return nil, fmt.Errorf("%s: duplicate endpoint: '%s'", endpoint.Body().MissingItemRange().String(), pattern)
}
endpointsPatterns[cleanPattern] = true

unique, cleanPattern := isUnique(endpoints, pattern)
if !unique {
return nil, fmt.Errorf("duplicate endpoint: %q", pattern)
}
endpoints[cleanPattern] = true

// setACHandlerFn individual wrap for access_control configuration per endpoint
setACHandlerFn := func(protectedHandler http.Handler) {
api[endpoint] = configureProtectedHandler(accessControls, serverOptions.APIErrTpl,
config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl).
Merge(config.NewAccessControl(srvConf.API.AccessControl, srvConf.API.DisableAccessControl)),
config.NewAccessControl(endpoint.AccessControl, endpoint.DisableAccessControl),
protectedHandler)
// setACHandlerFn individual wrap for access_control configuration per endpoint
setACHandlerFn := func(protectedHandler http.Handler) {
ac := config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl)
if epType == KindAPI {
ac = ac.Merge(config.NewAccessControl(srvConf.API.AccessControl, srvConf.API.DisableAccessControl))
}

backendConf := *DefaultBackendConf
if diags := gohcl.DecodeBody(endpoint.Remain, confCtx, &backendConf); diags.HasErrors() {
return nil, diags
}
api[endpoint] = configureProtectedHandler(accessControls, serverOptions.APIErrTpl,
ac,
config.NewAccessControl(endpoint.AccessControl, endpoint.DisableAccessControl),
protectedHandler)
}

backend, err := newProxy(confCtx, &backendConf, srvConf.API.CORS, log, serverOptions, conf.Settings.NoProxyFromEnv)
if err != nil {
return nil, err
}
backendConf := *DefaultBackendConf
if diags := gohcl.DecodeBody(endpoint.Remain, confCtx, &backendConf); diags.HasErrors() {
return nil, diags
}

setACHandlerFn(backend)
backend, err := newProxy(
confCtx, &backendConf, cors, log, serverOptions,
conf.Settings.NoProxyFromEnv, errTpl, epType,
)
if err != nil {
return nil, err
}

err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
if err != nil {
return nil, err
}
setACHandlerFn(backend)

err = setRoutesFromHosts(serverConfiguration, defaultPort, srvConf.Hosts, pattern, api[endpoint], KindAPI)
if err != nil {
return nil, err
}
}
}

return serverConfiguration, nil
}

func newProxy(
ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.CORS,
log *logrus.Entry, srvOpts *server.Options, noProxyFromEnv bool) (http.Handler, error) {
ctx *hcl.EvalContext, beConf *config.Backend, corsOpts *config.CORS, log *logrus.Entry,
srvOpts *server.Options, noProxyFromEnv bool, errTpl *errors.Template, epType HandlerKind) (http.Handler, error) {
corsOptions, err := handler.NewCORSOptions(corsOpts)
if err != nil {
return nil, err
}

proxyOptions, err := handler.NewProxyOptions(beConf, corsOptions, noProxyFromEnv)
var kind string
switch epType {
case KindAPI:
kind = "api"
case KindEndpoint:
kind = "endpoint"
}

proxyOptions, err := handler.NewProxyOptions(beConf, corsOptions, noProxyFromEnv, errTpl, kind)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -314,6 +343,8 @@ func setRoutesFromHosts(srvConf ServerConfiguration, defaultPort int, hosts []st

switch kind {
case KindAPI:
fallthrough
case KindEndpoint:
routes = srvConf[listenPort].EndpointRoutes
case KindFiles:
routes = srvConf[listenPort].FileRoutes
Expand All @@ -330,3 +361,19 @@ func setRoutesFromHosts(srvConf ServerConfiguration, defaultPort int, hosts []st
}
return nil
}

func getEndpointsList(srvConf *config.Server) endpointList {
endpoints := make(endpointList)

if srvConf.API != nil {
for _, endpoint := range srvConf.API.Endpoints {
endpoints[endpoint] = KindAPI
}
}

for _, endpoint := range srvConf.Endpoints {
endpoints[endpoint] = KindEndpoint
}

return endpoints
}
8 changes: 5 additions & 3 deletions config/runtime/server/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Options struct {
APIBasePath string
FileBasePath string
SPABasePath string
SrvBasePath string
ServerName string
}

Expand All @@ -33,6 +34,7 @@ func NewServerOptions(conf *config.Server) (*Options, error) {
return options, nil
}
options.ServerName = conf.Name
options.SrvBasePath = path.Join("/", conf.BasePath)

if conf.ErrorFile != "" {
tpl, err := errors.NewTemplateFromFile(conf.ErrorFile)
Expand All @@ -44,7 +46,7 @@ func NewServerOptions(conf *config.Server) (*Options, error) {
}

if conf.API != nil {
options.APIBasePath = path.Join("/", conf.BasePath, conf.API.BasePath)
options.APIBasePath = path.Join(options.SrvBasePath, conf.API.BasePath)

if conf.API.ErrorFile != "" {
tpl, err := errors.NewTemplateFromFile(conf.API.ErrorFile)
Expand All @@ -64,11 +66,11 @@ func NewServerOptions(conf *config.Server) (*Options, error) {
options.FileErrTpl = tpl
}

options.FileBasePath = utils.JoinPath("/", conf.BasePath, conf.Files.BasePath)
options.FileBasePath = utils.JoinPath(options.SrvBasePath, conf.Files.BasePath)
}

if conf.Spa != nil {
options.SPABasePath = utils.JoinPath("/", conf.BasePath, conf.Spa.BasePath)
options.SPABasePath = utils.JoinPath(options.SrvBasePath, conf.Spa.BasePath)
}

return options, nil
Expand Down
54 changes: 54 additions & 0 deletions config/runtime/server_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"testing"

"github.com/avenga/couper/config"
"github.com/avenga/couper/internal/seetie"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcltest"
)

func TestServer_isUnique(t *testing.T) {
Expand Down Expand Up @@ -165,3 +168,54 @@ func TestServer_splitWildcardHostPort(t *testing.T) {
t.Errorf("Expected NIL, given %s", err)
}
}

func TestServer_getEndpointsList(t *testing.T) {
getHCLBody := func(in string) hcl.Body {
return hcltest.MockBody(&hcl.BodyContent{
Attributes: hcltest.MockAttrs(map[string]hcl.Expression{
"path": hcltest.MockExprLiteral(seetie.GoToValue(in)),
}),
})
}

srvConf := &config.Server{
API: &config.Api{
Endpoints: []*config.Endpoint{
{Remain: getHCLBody("/api/1")},
{Remain: getHCLBody("/api/2")},
},
},
Endpoints: []*config.Endpoint{
{Remain: getHCLBody("/free/1")},
{Remain: getHCLBody("/free/2")},
},
}

endpoints := getEndpointsList(srvConf)
if l := len(endpoints); l != 4 {
t.Fatalf("Expected 4 endpointes, given %d", l)
}

checks := map[string]HandlerKind{
"/api/1": KindAPI,
"/api/2": KindAPI,
"/free/1": KindEndpoint,
"/free/2": KindEndpoint,
}

for e, kind := range endpoints {
a, _ := e.Remain.JustAttributes()
v, _ := a["path"].Expr.Value(nil)
path := seetie.ValueToString(v)

if v, ok := checks[path]; !ok || v != kind {
t.Fatalf("Missing an endpoint for %s", path)
}

delete(checks, path)
}

if l := len(checks); l != 0 {
t.Fatalf("Expected 0 checks, given %d", l)
}
}
56 changes: 47 additions & 9 deletions config/server.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,51 @@
package config

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
)

var _ Inline = &Server{}

// Server represents the HCL <server> block.
type Server struct {
AccessControl []string `hcl:"access_control,optional"`
DisableAccessControl []string `hcl:"disable_access_control,optional"`
API *Api `hcl:"api,block"`
BasePath string `hcl:"base_path,optional"`
ErrorFile string `hcl:"error_file,optional"`
Files *Files `hcl:"files,block"`
Hosts []string `hcl:"hosts,optional"`
Name string `hcl:"name,label"`
Spa *Spa `hcl:"spa,block"`
AccessControl []string `hcl:"access_control,optional"`
DisableAccessControl []string `hcl:"disable_access_control,optional"`
API *Api `hcl:"api,block"`
Backend string `hcl:"backend,optional"`
BasePath string `hcl:"base_path,optional"`
Endpoints Endpoints `hcl:"endpoint,block"`
ErrorFile string `hcl:"error_file,optional"`
Files *Files `hcl:"files,block"`
Hosts []string `hcl:"hosts,optional"`
Name string `hcl:"name,label"`
Remain hcl.Body `hcl:",remain"`
Spa *Spa `hcl:"spa,block"`
}

func (s Server) Body() hcl.Body {
return s.Remain
}

func (s Server) Reference() string {
return s.Backend
}

func (s Server) Schema(inline bool) *hcl.BodySchema {
if !inline {
schema, _ := gohcl.ImpliedBodySchema(s)
return schema
}

type Inline struct {
Backend *Backend `hcl:"backend,block"`
}
schema, _ := gohcl.ImpliedBodySchema(&Inline{})

// The Server contains a backend reference, backend block is not allowed.
if s.Backend != "" {
schema.Blocks = nil
}

return newBackendSchema(schema, s.Body())
}
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ Endpoints define the entry points of Couper. The mandatory *label* defines the p

| Name | Description |
|:-------------------|:--------------------------------------|
|context|`api` block|
|context|`server` and `api` block|
|*label*|<ul><li>&#9888; mandatory</li><li>defines the path suffix for incoming client requests</li><li>*example:* `endpoint "/dashboard" { `</li><li>incoming client request: `example.com/api/dashboard`</li></ul>|
| `path`|<ul><li>changeable part of upstream URL</li><li>changes the path suffix of the outgoing request</li></ul>|
|[**`access_control`**](#access_control_attribute)|sets predefined `access_control` for `endpoint`|
Expand Down
Loading

0 comments on commit a23cfa7

Please sign in to comment.