Skip to content

Commit

Permalink
Multiple SPA blocks (#510)
Browse files Browse the repository at this point in the history
* Change multiple spa blocks

* (docs) Add changelog, update docs

* Add multi-file spa test

* fmt

* (docs) update merge behaviour

* Update config/configload/merge.go

Co-authored-by: Alex Schneider <alex.schneider@avenga.com>

* Change docu

* Revert changes

Co-authored-by: Alex Schneider <alex.schneider@avenga.com>
  • Loading branch information
Marcel Ludwig and Alex Schneider authored May 25, 2022
1 parent b95ad1f commit c81cbab
Show file tree
Hide file tree
Showing 21 changed files with 267 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
* renamed `beta_insufficient_scope` [error type](./docs/ERRORS.md#api-and-endpoint-error-types) to `beta_insufficient_permissions`
* added `request.context.beta_required_permission` and `request.context.beta_granted_permissions` [request variables](./docs/REFERENCE.md#request)
* Clarified the type of various [attributes/variables](./docs/REFERENCE.md) ([#485](https://github.com/avenga/couper/pull/485))
* [`spa` block](./docs/REFERENCE.md#spa-block) can be defined multiple times now ([#510](https://github.com/avenga/couper/pull/510))

* **Fixed**
* Keys in object type attribute values are only handled case-insensitively if reasonable (e.g. they represent HTTP methods or header field values) ([#461](https://github.com/avenga/couper/pull/461))
Expand Down
13 changes: 13 additions & 0 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const (
request = "request"
server = "server"
settings = "settings"
spa = "spa"
// defaultNameLabel maps the hcl label attr 'name'.
defaultNameLabel = "default"
)
Expand Down Expand Up @@ -396,6 +397,18 @@ func LoadConfig(body hcl.Body, src []byte, filename, dirPath string) (*config.Co
}
}

for _, spaBlock := range bodyToContent(serverConfig.Remain).Blocks.OfType(spa) {
spaConfig := &config.Spa{}
if diags := gohcl.DecodeBody(spaBlock.Body, helper.context, spaConfig); diags.HasErrors() {
return nil, diags
}

if len(spaBlock.Labels) > 0 {
spaConfig.Name = spaBlock.Labels[0]
}
serverConfig.SPAs = append(serverConfig.SPAs, spaConfig)
}

// standalone endpoints
err = refineEndpoints(helper, serverConfig.Endpoints, true)
if err != nil {
Expand Down
85 changes: 78 additions & 7 deletions config/configload/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,17 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
endpoints namedBlocks
errorHandler namedBlocks
}
spaDefinition struct {
labels []string
typeRange hcl.Range
labelRanges []hcl.Range
openBraceRange hcl.Range
closeBraceRange hcl.Range
attributes hclsyntax.Attributes
blocks namedBlocks
}
namedAPIs map[string]*apiDefinition
namedSPAs map[string]*spaDefinition
serverDefinition struct {
labels []string
typeRange hcl.Range
Expand All @@ -43,7 +53,9 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
blocks namedBlocks
endpoints namedBlocks
apis namedAPIs
spas namedSPAs
}

servers map[string]*serverDefinition
)

Expand Down Expand Up @@ -99,6 +111,7 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
blocks: make(namedBlocks),
endpoints: make(namedBlocks),
apis: make(namedAPIs),
spas: make(namedSPAs),
}
}

Expand All @@ -111,14 +124,11 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
}

for _, block := range outerBlock.Body.Blocks {
uniqueAPILabels := make(map[string]struct{})
uniqueAPILabels, uniqueSPALabels := make(map[string]struct{}), make(map[string]struct{})

// TODO: Do we need this IF around the FOR?
if block.Type == "files" || block.Type == "spa" || block.Type == api || block.Type == endpoint {
for _, name := range []string{"error_file", "document_root"} {
if attr, ok := block.Body.Attributes[name]; ok {
block.Body.Attributes[name].Expr = absPath(attr)
}
for _, name := range []string{"error_file", "document_root"} {
if attr, ok := block.Body.Attributes[name]; ok {
block.Body.Attributes[name].Expr = absPath(attr)
}
}

Expand Down Expand Up @@ -207,6 +217,44 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
results[serverKey].apis[apiKey].blocks[subBlock.Type] = subBlock
}
}
} else if block.Type == spa {
var spaKey string

if len(block.Labels) > 0 {
spaKey = block.Labels[0]
}

if len(bodies) > 1 {
if _, ok := uniqueSPALabels[spaKey]; ok {
return nil, newMergeError(errUniqueLabels, block)
}

uniqueSPALabels[spaKey] = struct{}{}
} else {
// Create unique key for multiple spa blocks inside a single config file.
spaKey += fmt.Sprintf("|%p", &spaKey)
}

if results[serverKey].spas[spaKey] == nil {
results[serverKey].spas[spaKey] = &spaDefinition{
labels: block.Labels,
typeRange: block.TypeRange,
labelRanges: block.LabelRanges,
openBraceRange: block.OpenBraceRange,
closeBraceRange: block.CloseBraceRange,
attributes: make(hclsyntax.Attributes),
blocks: make(namedBlocks),
}
}

for name, attr := range block.Body.Attributes {
results[serverKey].spas[spaKey].attributes[name] = attr
}

for _, subBlock := range block.Body.Blocks {
results[serverKey].spas[spaKey].blocks[subBlock.Type] = subBlock
}

} else {
results[serverKey].blocks[block.Type] = block
}
Expand Down Expand Up @@ -258,6 +306,29 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
serverBlocks = append(serverBlocks, mergedAPI)
}

for _, spaBlock := range serverBlock.spas {
var spaBlocks hclsyntax.Blocks

for _, b := range spaBlock.blocks {
spaBlocks = append(spaBlocks, b)
}

mergedSPA := &hclsyntax.Block{
Type: spa,
Labels: spaBlock.labels,
Body: &hclsyntax.Body{
Attributes: spaBlock.attributes,
Blocks: spaBlocks,
},
TypeRange: spaBlock.typeRange,
LabelRanges: spaBlock.labelRanges,
OpenBraceRange: spaBlock.openBraceRange,
CloseBraceRange: spaBlock.closeBraceRange,
}

serverBlocks = append(serverBlocks, mergedSPA)
}

mergedServer := &hclsyntax.Block{
Type: server,
Labels: serverBlock.labels,
Expand Down
2 changes: 1 addition & 1 deletion config/configload/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func completeSchemaComponents(body hcl.Body, schema *hcl.BodySchema, attrs hcl.A
added := false
for _, block := range bodyContent.Blocks {
switch block.Type {
case "api", "backend", "error_handler", "proxy", "request", "server":
case api, backend, errorHandler, proxy, request, server, spa:
blocks = append(blocks, block)

added = true
Expand Down
17 changes: 12 additions & 5 deletions config/runtime/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,22 @@ func newEndpointMap(srvConf *config.Server, serverOptions *server.Options) (endp
continue
}

var filesBasePath, spaBasePath string
var filesBasePath string
if serverOptions.FilesBasePath != "" {
filesBasePath = serverOptions.FilesBasePath
}
if serverOptions.SPABasePath != "" {
spaBasePath = serverOptions.SPABasePath
}

isAPIBasePathUniqueToFilesAndSPA := basePath != filesBasePath && basePath != spaBasePath
var isAPIBasePathUniqueToFilesAndSPA bool
if len(serverOptions.SPABasePaths) > 0 {
for _, s := range serverOptions.SPABasePaths {
isAPIBasePathUniqueToFilesAndSPA = basePath != filesBasePath && basePath != s
if !isAPIBasePathUniqueToFilesAndSPA {
break
}
}
} else {
isAPIBasePathUniqueToFilesAndSPA = basePath != filesBasePath
}

if isAPIBasePathUniqueToFilesAndSPA {
endpoints[apiConf.CatchAllEndpoint] = apiConf
Expand Down
32 changes: 17 additions & 15 deletions config/runtime/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/docker/go-units"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/sirupsen/logrus"

ac "github.com/avenga/couper/accesscontrol"
Expand Down Expand Up @@ -144,8 +145,8 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
serverBodies = append(serverBodies, srvConf.Remain)

var spaHandler http.Handler
if srvConf.Spa != nil {
spaHandler, err = handler.NewSpa(srvConf.Spa.BootstrapFile, serverOptions, []hcl.Body{srvConf.Spa.Remain, srvConf.Remain})
for _, spaConf := range srvConf.SPAs {
spaHandler, err = handler.NewSpa(spaConf.BootstrapFile, serverOptions, []hcl.Body{spaConf.Remain, srvConf.Remain})
if err != nil {
return nil, err
}
Expand All @@ -157,7 +158,7 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca

spaHandler, err = configureProtectedHandler(accessControls, confCtx,
config.NewAccessControl(srvConf.AccessControl, srvConf.DisableAccessControl),
config.NewAccessControl(srvConf.Spa.AccessControl, srvConf.Spa.DisableAccessControl),
config.NewAccessControl(spaConf.AccessControl, spaConf.DisableAccessControl),
&protectedOptions{
epOpts: epOpts,
handler: spaHandler,
Expand All @@ -169,22 +170,27 @@ func NewServerConfiguration(conf *config.Couper, log *logrus.Entry, memStore *ca
return nil, err
}

corsOptions, cerr := middleware.NewCORSOptions(whichCORS(srvConf, srvConf.Spa), allowedMethodsHandler.MethodAllowed)
corsOptions, cerr := middleware.NewCORSOptions(whichCORS(srvConf, spaConf), allowedMethodsHandler.MethodAllowed)
if cerr != nil {
return nil, cerr
}

spaHandler = middleware.NewCORSHandler(corsOptions, spaHandler)

spaBodies := bodiesWithACBodies(conf.Definitions, srvConf.Spa.AccessControl, srvConf.Spa.DisableAccessControl)
spaBodies := bodiesWithACBodies(conf.Definitions, spaConf.AccessControl, spaConf.DisableAccessControl)
spaHandler = middleware.NewCustomLogsHandler(
append(serverBodies, append(spaBodies, srvConf.Spa.Remain)...), spaHandler, "",
append(serverBodies, append(spaBodies, spaConf.Remain)...), spaHandler, "",
)

for _, spaPath := range srvConf.Spa.Paths {
err = setRoutesFromHosts(serverConfiguration, portsHosts, path.Join(serverOptions.SPABasePath, spaPath), spaHandler, spa)
for _, spaPath := range spaConf.Paths {
err = setRoutesFromHosts(serverConfiguration, portsHosts,
path.Join(serverOptions.SrvBasePath, spaConf.BasePath, spaPath), spaHandler, spa)
if err != nil {
return nil, err
sbody := spaConf.HCLBody().(*hclsyntax.Body)
return nil, hcl.Diagnostics{&hcl.Diagnostic{
Subject: &sbody.Attributes["paths"].SrcRange,
Summary: err.Error(),
}}
}
}
}
Expand Down Expand Up @@ -645,8 +651,6 @@ func setRoutesFromHosts(
path = utils.JoinPath("/", path)

for port, hosts := range portsHosts {
check := make(map[string]struct{})

for host := range hosts {
var routes map[string]http.Handler

Expand All @@ -663,13 +667,11 @@ func setRoutesFromHosts(
return fmt.Errorf("unknown route kind")
}

key := fmt.Sprintf("%d:%s:%s\n", port, host, path)
if _, exist := check[key]; exist {
return fmt.Errorf("duplicate route found on port %q: %q", port, path)
if _, exist := routes[path]; exist {
return fmt.Errorf("duplicate route found on port %d: %s", port, path)
}

routes[path] = handler
check[key] = struct{}{}
}
}

Expand Down
6 changes: 3 additions & 3 deletions config/runtime/server/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Options struct {
ServerErrTpl *errors.Template
APIBasePaths map[*config.API]string
FilesBasePath string
SPABasePath string
SPABasePaths []string
SrvBasePath string
ServerName string
}
Expand Down Expand Up @@ -77,8 +77,8 @@ func NewServerOptions(conf *config.Server, logger *logrus.Entry) (*Options, erro
options.FilesBasePath = utils.JoinPath(options.SrvBasePath, conf.Files.BasePath)
}

if conf.Spa != nil {
options.SPABasePath = utils.JoinPath(options.SrvBasePath, conf.Spa.BasePath)
for _, s := range conf.SPAs {
options.SPABasePaths = append(options.SPABasePaths, utils.JoinPath(options.SrvBasePath, s.BasePath))
}

return options, nil
Expand Down
8 changes: 3 additions & 5 deletions config/runtime/server/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ func TestServer_NewServerOptions_NoConfig(t *testing.T) {
ServerErrTpl: errors.DefaultHTML,
APIBasePaths: map[*config.API]string(nil),
FilesBasePath: "",
SPABasePath: "",
SrvBasePath: "",
ServerName: "",
}
Expand All @@ -45,7 +44,6 @@ func TestServer_NewServerOptions_EmptyConfig(t *testing.T) {
ServerErrTpl: errors.DefaultHTML,
APIBasePaths: map[*config.API]string(nil),
FilesBasePath: "",
SPABasePath: "",
SrvBasePath: "/",
ServerName: "",
}
Expand Down Expand Up @@ -77,9 +75,9 @@ func TestServer_NewServerOptions_ConfigWithPaths(t *testing.T) {
Files: &config.Files{
BasePath: "/files",
},
Spa: &config.Spa{
SPAs: []*config.Spa{{
BasePath: "/spa",
},
}},
APIs: config.APIs{
api1, api2,
},
Expand All @@ -96,7 +94,7 @@ func TestServer_NewServerOptions_ConfigWithPaths(t *testing.T) {
ServerErrTpl: errors.DefaultHTML,
APIBasePaths: abps,
FilesBasePath: "/server/files",
SPABasePath: "/server/spa",
SPABasePaths: []string{"/server/spa"},
SrvBasePath: "/server",
ServerName: "ServerName",
}
Expand Down
6 changes: 4 additions & 2 deletions config/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ type Server struct {
Hosts []string `hcl:"hosts,optional"`
Name string `hcl:"name,label"`
Remain hcl.Body `hcl:",remain"`
Spa *Spa `hcl:"spa,block"`
APIs APIs

APIs APIs
SPAs SPAs
}

// Servers represents a list of <Server> objects.
Expand All @@ -35,6 +36,7 @@ func (s Server) HCLBody() hcl.Body {
func (s Server) Inline() interface{} {
type Inline struct {
APIs APIs `hcl:"api,block"`
SPAs SPAs `hcl:"spa,block"`
AddResponseHeaders map[string]string `hcl:"add_response_headers,optional"`
DelResponseHeaders []string `hcl:"remove_response_headers,optional"`
SetResponseHeaders map[string]string `hcl:"set_response_headers,optional"`
Expand Down
3 changes: 3 additions & 0 deletions config/spa.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import (

var _ Inline = &Spa{}

type SPAs []*Spa

// Spa represents the <Spa> object.
type Spa struct {
AccessControl []string `hcl:"access_control,optional"`
BasePath string `hcl:"base_path,optional"`
BootstrapFile string `hcl:"bootstrap_file"`
CORS *CORS `hcl:"cors,block"`
DisableAccessControl []string `hcl:"disable_access_control,optional"`
Name string `hcl:"name,label"`
Paths []string `hcl:"paths"`
Remain hcl.Body `hcl:",remain"`
}
Expand Down
Loading

0 comments on commit c81cbab

Please sign in to comment.