Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reusable proxies #561

Merged
merged 20 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
75 changes: 70 additions & 5 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 @@ -425,7 +434,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 +446,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 +487,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 +618,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
johakoch marked this conversation as resolved.
Show resolved Hide resolved
alex-schneider marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
6 changes: 6 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,11 @@ func validateBody(body hcl.Body, afterMerge bool) error {
if err != nil {
return err
}
case proxy:
if _, set := uniqueProxies[label]; set {
return newDiagErr(&labelRange, "proxy labels must be unique")
}
uniqueProxies[label] = struct{}{}
johakoch marked this conversation as resolved.
Show resolved Hide resolved
}
}
} 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" {
alex-schneider marked this conversation as resolved.
Show resolved Hide resolved
}
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) | ⚠ 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) (⚠ required, if no [Backend Block](backend) reference is defined or no `url` attribute is set.), [Websockets Block](websockets) (⚠ Either websockets attribute or block is allowed.) |
| `proxy` | [Endpoint Block](endpoint) | See `Label` description below. | [Backend Block](backend) (⚠ required, if no [Backend Block](backend) reference is defined or no `url` attribute is set.), [Websockets Block](websockets) (⚠ 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"
johakoch marked this conversation as resolved.
Show resolved Hide resolved
response {
johakoch marked this conversation as resolved.
Show resolved Hide resolved
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"
}
}