diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a25ce60..0c4a2946c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/config/configload/load.go b/config/configload/load.go index 654525287..f4a51483c 100644 --- a/config/configload/load.go +++ b/config/configload/load.go @@ -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 } diff --git a/config/configload/merge.go b/config/configload/merge.go index 84027b744..b31a6783c 100644 --- a/config/configload/merge.go +++ b/config/configload/merge.go @@ -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 { @@ -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 @@ -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 { @@ -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 { @@ -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. @@ -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++ @@ -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 } } } @@ -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) { @@ -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) { diff --git a/config/configload/validate.go b/config/configload/validate.go index 44dd88703..0ad87f0fb 100644 --- a/config/configload/validate.go +++ b/config/configload/validate.go @@ -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 { @@ -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 { diff --git a/config/configload/validate_test.go b/config/configload/validate_test.go index 9756fc37f..7f1980a10 100644 --- a/config/configload/validate_test.go +++ b/config/configload/validate_test.go @@ -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 {} diff --git a/docs/website/content/2.configuration/4.block/definitions.md b/docs/website/content/2.configuration/4.block/definitions.md index 6a56f957a..1544a3ef0 100644 --- a/docs/website/content/2.configuration/4.block/definitions.md +++ b/docs/website/content/2.configuration/4.block/definitions.md @@ -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) | diff --git a/docs/website/content/2.configuration/4.block/endpoint.md b/docs/website/content/2.configuration/4.block/endpoint.md index 101927b2c..ca82968e7 100644 --- a/docs/website/content/2.configuration/4.block/endpoint.md +++ b/docs/website/content/2.configuration/4.block/endpoint.md @@ -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", diff --git a/docs/website/content/2.configuration/4.block/proxy.md b/docs/website/content/2.configuration/4.block/proxy.md index b249108fc..67fb52bd6 100644 --- a/docs/website/content/2.configuration/4.block/proxy.md +++ b/docs/website/content/2.configuration/4.block/proxy.md @@ -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 --- @@ -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", diff --git a/main_test.go b/main_test.go index 3d50ee252..30a838d41 100644 --- a/main_test.go +++ b/main_test.go @@ -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) { diff --git a/server/http_endpoints_test.go b/server/http_endpoints_test.go index b3041c998..99c7e8614 100644 --- a/server/http_endpoints_test.go +++ b/server/http_endpoints_test.go @@ -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) + } + } + }) + } +} diff --git a/server/testdata/endpoints/18_couper.hcl b/server/testdata/endpoints/18_couper.hcl new file mode 100644 index 000000000..6f9cde3a9 --- /dev/null +++ b/server/testdata/endpoints/18_couper.hcl @@ -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" + } +} diff --git a/server/testdata/settings/19_couper.hcl b/server/testdata/settings/19_couper.hcl new file mode 100644 index 000000000..fa263b956 --- /dev/null +++ b/server/testdata/settings/19_couper.hcl @@ -0,0 +1,5 @@ +server { + endpoint "/" { + proxy = 1 + } +} diff --git a/server/testdata/settings/20_couper.hcl b/server/testdata/settings/20_couper.hcl new file mode 100644 index 000000000..cdbbc5060 --- /dev/null +++ b/server/testdata/settings/20_couper.hcl @@ -0,0 +1,5 @@ +server { + endpoint "/" { + proxy = "foo" + } +}