Skip to content

Commit

Permalink
Reusable proxies (#561)
Browse files Browse the repository at this point in the history
* Docu

* Tests

* More tests

* Implementation of reusable proxy blocks

* Changelog

* Reusable proxies in api-endpoints, too

* DRY

* Simplify tests

* Reference the attribute in the error message

* Check for string type

Co-authored-by: Johannes Koch <53434855+johakoch@users.noreply.github.com>

* No need to delete before throwing error.

* Test non-string proxy reference

* Test if a proxy reference does not exist

* Test unique proxy labels

* Fix merge error

* Check for unique proxy labels only before merge

* WS

Co-authored-by: Johannes Koch <53434855+johakoch@users.noreply.github.com>

Co-authored-by: Johannes Koch <53434855+johakoch@users.noreply.github.com>
  • Loading branch information
Alex Schneider and johakoch authored Aug 31, 2022
1 parent 7bc88d7 commit 0adf7d3
Show file tree
Hide file tree
Showing 13 changed files with 216 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
* used go version in `version` command ([#552](https://github.com/avenga/couper/pull/552))
* new `grant_type`s `"password"` and `"urn:ietf:params:oauth:grant-type:jwt-bearer"` with related attributes for [`oauth2` block](https://docs.couper.io/configuration/block/oauth2) ([#555](https://github.com/avenga/couper/pull/555))
* [`beta_token_request` block](https://docs.couper.io/configuration/block/token_request), [`backend`](https://docs.couper.io/configuration/variables#backend) and [`beta_token_response`](https://docs.couper.io/configuration/variables#beta_token_response) variables and `beta_token(s)` properties of [`backends` variable](https://docs.couper.io/configuration/variables#backends) ([#517](https://github.com/avenga/couper/pull/517))
* reusable [`proxy` block](https://docs.couper.io/configuration/block/proxy) ([#561](https://github.com/avenga/couper/pull/561))

* **Changed**
* Starting will now fail if `environment` blocks are used without `COUPER_ENVIRONMENT` being set ([#546](https://github.com/avenga/couper/pull/546))
Expand Down
4 changes: 2 additions & 2 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ func bodiesToConfig(parsedBodies []*hclsyntax.Body, srcBytes [][]byte, env strin

settingsBlock := mergeSettings(parsedBodies)

definitionsBlock, err := mergeDefinitions(parsedBodies)
definitionsBlock, proxies, err := mergeDefinitions(parsedBodies)
if err != nil {
return nil, err
}

serverBlocks, err := mergeServers(parsedBodies)
serverBlocks, err := mergeServers(parsedBodies, proxies)
if err != nil {
return nil, err
}
Expand Down
77 changes: 70 additions & 7 deletions config/configload/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const (
errUniqueLabels = "All %s blocks must have unique labels."
)

func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
func mergeServers(bodies []*hclsyntax.Body, proxies map[string]*hclsyntax.Block) (hclsyntax.Blocks, error) {
type (
namedBlocks map[string]*hclsyntax.Block
apiDefinition struct {
Expand Down Expand Up @@ -151,6 +151,10 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
return nil, newMergeError(errUniqueLabels, block)
}

if err := addProxy(block, proxies); err != nil {
return nil, err
}

results[serverKey].endpoints[block.Labels[0]] = block
} else if block.Type == api {
var apiKey string
Expand Down Expand Up @@ -198,6 +202,10 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
return nil, newMergeError(errUniqueLabels, subBlock)
}

if err := addProxy(subBlock, proxies); err != nil {
return nil, err
}

results[serverKey].apis[apiKey].endpoints[subBlock.Labels[0]] = subBlock
} else if subBlock.Type == errorHandler {
if err := absInBackends(subBlock); err != nil {
Expand Down Expand Up @@ -401,11 +409,12 @@ func mergeServers(bodies []*hclsyntax.Body) (hclsyntax.Blocks, error) {
return mergedServers, nil
}

func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, map[string]*hclsyntax.Block, error) {
type data map[string]*hclsyntax.Block
type list map[string]data

definitionsBlock := make(list)
proxiesList := make(data)

for _, body := range bodies {
for _, outerBlock := range body.Blocks {
Expand All @@ -415,8 +424,6 @@ func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
definitionsBlock[innerBlock.Type] = make(data)
}

definitionsBlock[innerBlock.Type][innerBlock.Labels[0]] = innerBlock

// Count the "backend" blocks and "backend" attributes to
// forbid multiple backend definitions.

Expand All @@ -425,7 +432,7 @@ func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
for _, block := range innerBlock.Body.Blocks {
if block.Type == errorHandler {
if err := absInBackends(block); err != nil {
return nil, err
return nil, nil, err
}
} else if block.Type == backend {
backends++
Expand All @@ -437,7 +444,28 @@ func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
}

if backends > 1 {
return nil, newMergeError(errMultipleBackends, innerBlock)
return nil, nil, newMergeError(errMultipleBackends, innerBlock)
}

if innerBlock.Type != proxy {
definitionsBlock[innerBlock.Type][innerBlock.Labels[0]] = innerBlock
} else {
label := innerBlock.Labels[0]

if attr, ok := innerBlock.Body.Attributes["name"]; ok {
name, err := attrStringValue(attr)
if err != nil {
return nil, nil, err
}

innerBlock.Labels[0] = name

delete(innerBlock.Body.Attributes, "name")
} else {
innerBlock.Labels[0] = defaultNameLabel
}

proxiesList[label] = innerBlock
}
}
}
Expand All @@ -457,7 +485,7 @@ func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
Body: &hclsyntax.Body{
Blocks: blocks,
},
}, nil
}, proxiesList, nil
}

func mergeDefaults(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
Expand Down Expand Up @@ -588,6 +616,41 @@ func absInBackends(block *hclsyntax.Block) error {
return nil
}

func addProxy(block *hclsyntax.Block, proxies map[string]*hclsyntax.Block) error {
if attr, ok := block.Body.Attributes[proxy]; ok {
reference, err := attrStringValue(attr)
if err != nil {
return err
}

if proxyBlock, ok := proxies[reference]; !ok {
sr := attr.Expr.StartRange()

return newDiagErr(&sr, "proxy reference is not defined")
} else {
delete(block.Body.Attributes, proxy)

block.Body.Blocks = append(block.Body.Blocks, proxyBlock)
}
}

return nil
}

func attrStringValue(attr *hclsyntax.Attribute) (string, error) {
v, err := eval.Value(nil, attr.Expr)
if err != nil {
return "", err
}

if v.Type() != cty.String {
sr := attr.Expr.StartRange()
return "", newDiagErr(&sr, fmt.Sprintf("%s must evaluate to string", attr.Name))
}

return v.AsString(), nil
}

// newErrorHandlerKey returns a merge key based on a possible mixed error-kind format.
// "label1" and/or "label2 label3" results in "label1 label2 label3".
func newErrorHandlerKey(block *hclsyntax.Block) (key string) {
Expand Down
8 changes: 8 additions & 0 deletions config/configload/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func validateBody(body hcl.Body, afterMerge bool) error {
uniqueBackends := make(map[string]struct{})
uniqueACs := make(map[string]struct{})
uniqueJWTSigningProfiles := make(map[string]struct{})
uniqueProxies := make(map[string]struct{})
for _, innerBlock := range outerBlock.Body.Blocks {
if !afterMerge {
if len(innerBlock.Labels) == 0 {
Expand Down Expand Up @@ -92,6 +93,13 @@ func validateBody(body hcl.Body, afterMerge bool) error {
if err != nil {
return err
}
case proxy:
if !afterMerge {
if _, set := uniqueProxies[label]; set {
return newDiagErr(&labelRange, "proxy labels must be unique")
}
uniqueProxies[label] = struct{}{}
}
}
}
} else if outerBlock.Type == server {
Expand Down
11 changes: 11 additions & 0 deletions config/configload/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,17 @@ func Test_validateBody(t *testing.T) {
}`,
"couper.hcl:5,15-20: backend labels must be unique; ",
},
{
"duplicate proxy labels",
`server {}
definitions {
proxy "foo" {
}
proxy "foo" {
}
}`,
"couper.hcl:5,13-18: proxy labels must be unique; ",
},
{
"missing basic_auth label",
`server {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ Use the `definitions` block to define configurations you want to reuse.

| Block name | Context | Label | Nested block(s) |
|:--------------|:--------|:---------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `definitions` | - | no label | [Backend Block(s)](backend), [Basic Auth Block(s)](basic_auth), [JWT Block(s)](jwt), [JWT Signing Profile Block(s)](jwt_signing_profile), [SAML Block(s)](saml), [OAuth2 AC Block(s)](oauth2), [OIDC Block(s)](oidc) |
| `definitions` | - | no label | [Backend Block(s)](backend), [Basic Auth Block(s)](basic_auth), [JWT Block(s)](jwt), [JWT Signing Profile Block(s)](jwt_signing_profile), [SAML Block(s)](saml), [OAuth2 AC Block(s)](oauth2), [OIDC Block(s)](oidc), [Proxy Block(s)](proxy) |
6 changes: 6 additions & 0 deletions docs/website/content/2.configuration/4.block/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ values: [
"default": "",
"description": "Location of the error file template."
},
{
"name": "proxy",
"type": "string",
"default": "",
"description": "proxy block reference"
},
{
"name": "remove_form_params",
"type": "object",
Expand Down
9 changes: 8 additions & 1 deletion docs/website/content/2.configuration/4.block/proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ The `proxy` block creates and executes a proxy request to a backend service.
| Block name | Context | Label | Nested block(s) |
|:-----------|:----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `proxy` | [Endpoint Block](endpoint) | &#9888; A `proxy` block or [Request Block](request) w/o a label has an implicit label `"default"`. Only **one** `proxy` block or [Request Block](request) w/ label `"default"` per [Endpoint Block](endpoint) is allowed. | [Backend Block](backend) (&#9888; required, if no [Backend Block](backend) reference is defined or no `url` attribute is set.), [Websockets Block](websockets) (&#9888; Either websockets attribute or block is allowed.) |
| `proxy` | [Endpoint Block](endpoint) | See `Label` description below. | [Backend Block](backend) (&#9888; required, if no [Backend Block](backend) reference is defined or no `url` attribute is set.), [Websockets Block](websockets) (&#9888; Either websockets attribute or block is allowed.) |

**Label:** If defined in an [Endpoint Block](endpoint), a `proxy` block or [Request Block](request) w/o a label has an implicit name `"default"`. If defined in the [Definitions Block](definitions), the label of `proxy` is used as reference in [Endpoint Blocks](endpoint) and the name can be defined via `name` attribute. Only **one** `proxy` block or [Request Block](request) w/ label `"default"` per [Endpoint Block](endpoint) is allowed.

::attributes
---
Expand Down Expand Up @@ -48,6 +49,12 @@ values: [
"default": "[]",
"description": "If defined, the response status code will be verified against this list of codes. If the status code not included in this list an `unexpected_status` error will be thrown which can be handled with an [`error_handler`](error_handler)."
},
{
"name": "name",
"type": "string",
"default": "default",
"description": "Defines the proxy request name. Allowed only in the [Definitions Block](definitions)."
},
{
"name": "remove_form_params",
"type": "object",
Expand Down
2 changes: 2 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ func Test_realmain(t *testing.T) {
{"invalid method in allowed_methods in endpoint", []string{"couper", "run", "-f", base + "/14_couper.hcl"}, nil, `level=error msg="%s/14_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
{"invalid method in allowed_methods in api", []string{"couper", "run", "-f", base + "/15_couper.hcl"}, nil, `level=error msg="%s/15_couper.hcl:3,5-35: method contains invalid character(s); " build=dev`, 1},
{"rate_limit block in anonymous backend", []string{"couper", "run", "-f", base + "/17_couper.hcl"}, nil, `level=error msg="configuration error: anonymous_3_11: anonymous backend (\"anonymous_3_11\") cannot define 'beta_rate_limit' block(s)" build=dev`, 1},
{"non-string proxy reference", []string{"couper", "run", "-f", base + "/19_couper.hcl"}, nil, `level=error msg="%s/19_couper.hcl:3,13-14: proxy must evaluate to string; " build=dev`, 1},
{"proxy reference does not exist", []string{"couper", "run", "-f", base + "/20_couper.hcl"}, nil, `level=error msg="%s/20_couper.hcl:3,14-17: proxy reference is not defined; " build=dev`, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(subT *testing.T) {
Expand Down
49 changes: 49 additions & 0 deletions server/http_endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -961,3 +961,52 @@ func TestEndpointACBufferOptions(t *testing.T) {
})
}
}

func TestEndpoint_ReusableProxies(t *testing.T) {
client := test.NewHTTPClient()
helper := test.New(t)

shutdown, hook := newCouper(filepath.Join(testdataPath, "18_couper.hcl"), helper)
defer shutdown()

type testCase struct {
path string
name string
expStatus int
}

for _, tc := range []testCase{
{"/abcdef", "abcdef", 204},
{"/reuse", "abcdef", 204},
{"/default", "default", 200},
{"/api-abcdef", "abcdef", 204},
{"/api-reuse", "abcdef", 204},
{"/api-default", "default", 200},
} {
t.Run(tc.path, func(st *testing.T) {
h := test.New(st)

req, err := http.NewRequest(http.MethodGet, "http://example.com:8080"+tc.path, nil)
h.Must(err)

hook.Reset()

res, err := client.Do(req)
h.Must(err)

if res.StatusCode != tc.expStatus {
st.Errorf("want: %d, got: %d", tc.expStatus, res.StatusCode)
}

for _, e := range hook.AllEntries() {
if e.Data["type"] != "couper_backend" {
continue
}

if name := e.Data["request"].(logging.Fields)["name"]; name != tc.name {
st.Errorf("want: %s, got: %s", tc.name, name)
}
}
})
}
}
48 changes: 48 additions & 0 deletions server/testdata/endpoints/18_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
server "couper" {
endpoint "/abcdef" {
proxy = "test"
response {
status = 204
}
}
endpoint "/reuse" {
proxy = "test"
response {
status = 204
}
}

endpoint "/default" {
proxy = "defaultName"
}

api {
endpoint "/api-abcdef" {
proxy = "test"
response {
status = 204
}
}
endpoint "/api-reuse" {
proxy = "test"
response {
status = 204
}
}

endpoint "/api-default" {
proxy = "defaultName"
}
}
}

definitions {
proxy "defaultName" {
url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
}

proxy "test" {
name = "abcdef"
url = "${env.COUPER_TEST_BACKEND_ADDR}/anything"
}
}
5 changes: 5 additions & 0 deletions server/testdata/settings/19_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
server {
endpoint "/" {
proxy = 1
}
}
5 changes: 5 additions & 0 deletions server/testdata/settings/20_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
server {
endpoint "/" {
proxy = "foo"
}
}

0 comments on commit 0adf7d3

Please sign in to comment.