diff --git a/CHANGELOG.md b/CHANGELOG.md index e597e2bd4..33f6f1322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Unreleased changes are available as `avenga/couper:edge` container. +* **Added** + * [`environment` block](https://docs.couper.io/configuration/block/environment), [setting](https://docs.couper.io/configuration/block/settings) and [`couper.environment` variable](https://docs.couper.io/configuration/variables#couper) ([#521](https://github.com/avenga/couper/pull/521), ([#534](https://github.com/avenga/couper/pull/534), [#545](https://github.com/avenga/couper/pull/545)) + * **Fixed** * Disallow empty path parameters ([#526](https://github.com/avenga/couper/pull/526)) * Basic Auth client authentication with OAuth2 (client ID and secret must be URL encoded) ([#537](https://github.com/avenga/couper/pull/537)) diff --git a/config/body/merged_test.go b/config/body/merged_test.go index 0b6133c75..e2d77d4db 100644 --- a/config/body/merged_test.go +++ b/config/body/merged_test.go @@ -149,7 +149,7 @@ block { } } - hclcontext := eval.NewContext(nil, nil).HCLContext() + hclcontext := eval.NewDefaultContext().HCLContext() for k, attr := range expectedAttributes { a, exist := resultAttributes[k] diff --git a/config/configload/helper.go b/config/configload/helper.go index d687c3357..59d461cee 100644 --- a/config/configload/helper.go +++ b/config/configload/helper.go @@ -22,7 +22,7 @@ type helper struct { } // newHelper creates a container with some methods to keep things simple here and there. -func newHelper(body hcl.Body, src [][]byte) (*helper, error) { +func newHelper(body hcl.Body, src [][]byte, environment string) (*helper, error) { defaultsBlock := &config.DefaultsBlock{} if diags := gohcl.DecodeBody(body, nil, defaultsBlock); diags.HasErrors() { return nil, diags @@ -31,7 +31,7 @@ func newHelper(body hcl.Body, src [][]byte) (*helper, error) { defSettings := config.DefaultSettings couperConfig := &config.Couper{ - Context: eval.NewContext(src, defaultsBlock.Defaults), + Context: eval.NewContext(src, defaultsBlock.Defaults, environment), Definitions: &config.Definitions{}, Defaults: defaultsBlock.Defaults, Settings: &defSettings, @@ -151,7 +151,7 @@ func (h *helper) resolveBackendDeps() (uniqueItems []string, err error) { // do not forget the other ones var standalone []string - for def, _ := range h.defsBackends { + for def := range h.defsBackends { standalone = append(standalone, def) } items = append(items, standalone) diff --git a/config/configload/load.go b/config/configload/load.go index 634473856..5ac09ed63 100644 --- a/config/configload/load.go +++ b/config/configload/load.go @@ -69,7 +69,7 @@ func init() { } } -func updateContext(body hcl.Body, srcBytes [][]byte) hcl.Diagnostics { +func updateContext(body hcl.Body, srcBytes [][]byte, environment string) hcl.Diagnostics { defaultsBlock := &config.DefaultsBlock{} if diags := gohcl.DecodeBody(body, nil, defaultsBlock); diags.HasErrors() { return diags @@ -77,7 +77,7 @@ func updateContext(body hcl.Body, srcBytes [][]byte) hcl.Diagnostics { // We need the "envContext" to be able to resolve absolute paths in the config. defaultsConfig = defaultsBlock.Defaults - evalContext = eval.NewContext(srcBytes, defaultsConfig) + evalContext = eval.NewContext(srcBytes, defaultsConfig, environment) envContext = evalContext.HCLContext() return nil @@ -165,7 +165,7 @@ func LoadFiles(filesList []string, env string) (*config.Couper, error) { Blocks: hclsyntax.Blocks{defaultsBlock}, } - if diags := updateContext(defs, srcBytes); diags.HasErrors() { + if diags := updateContext(defs, srcBytes, env); diags.HasErrors() { return nil, diags } @@ -196,7 +196,7 @@ func LoadFiles(filesList []string, env string) (*config.Couper, error) { Blocks: configBlocks, } - conf, err := LoadConfig(configBody, srcBytes) + conf, err := LoadConfig(configBody, srcBytes, env) if err != nil { return nil, err } @@ -216,17 +216,17 @@ func LoadBytes(src []byte, filename string) (*config.Couper, error) { return nil, diags } - return LoadConfig(hclBody, [][]byte{src}) + return LoadConfig(hclBody, [][]byte{src}, "") } -func LoadConfig(body hcl.Body, src [][]byte) (*config.Couper, error) { +func LoadConfig(body hcl.Body, src [][]byte, environment string) (*config.Couper, error) { var err error if diags := ValidateConfigSchema(body, &config.Couper{}); diags.HasErrors() { return nil, diags } - helper, err := newHelper(body, src) + helper, err := newHelper(body, src, environment) if err != nil { return nil, err } diff --git a/config/runtime/server_internal_test.go b/config/runtime/server_internal_test.go index 6d3d2d24a..2ac27c036 100644 --- a/config/runtime/server_internal_test.go +++ b/config/runtime/server_internal_test.go @@ -208,7 +208,7 @@ func TestServer_validatePortHosts(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(subT *testing.T) { - tt.args.conf.Context = eval.NewContext(nil, nil) + tt.args.conf.Context = eval.NewDefaultContext() tt.args.conf.Settings = &config.DefaultSettings logger, _ := logrustest.NewNullLogger() diff --git a/docs/website/content/2.configuration/4.block/environment.md b/docs/website/content/2.configuration/4.block/environment.md index 1a3254e77..9bc49643c 100644 --- a/docs/website/content/2.configuration/4.block/environment.md +++ b/docs/website/content/2.configuration/4.block/environment.md @@ -1,52 +1,54 @@ -# Environment Block +# Environment The `environment` block lets you refine the Couper configuration based on the set -[environment](/configuration/command-linemd#global-options). +[environment](../command-line#global-options). | Block name | Context | Label | Nested block(s) | | :------------ | :------- | :----------------------------------------------- | :---------------------------------- | | `environment` | Overall. | ⚠ required, multiple labels are supported. | All configuration blocks of Couper. | The `environment` block works like a preprocessor. If the label of an `environment` -block do not match the set [environment](./CLI.md#global-options) value, the preprocessor -removes this block and their content. Otherwise, the content of the block is applied +block does not match the set [`COUPER_ENVIRONMENT`](../command-line#global-options) value, the preprocessor +removes this block and its content. Otherwise, the content of the block is added to the configuration. -If the [environment](/configuration/command-linemd#global-options) value set to `prod`, the following configuration +## Example + +Considering the following configuration with the `COUPER_ENVIRONMENT` value set to `prod` ```hcl server { - api "protected" { - endpoint "/secure" { - environment "prod" { - access_control = ["jwt"] - } - - proxy { - environment "prod" { - url = "https://protected-resource.org" - } - environment "stage" { - url = "https://test-resource.org" - } - } + api "protected" { + endpoint "/secure" { + environment "prod" { + access_control = ["jwt"] + } + + proxy { + environment "prod" { + url = "https://protected-resource.org" + } + environment "stage" { + url = "https://test-resource.org" } + } } + } } ``` -produces after the preprocessing the following configuration: +the result will be: ```hcl server { - api "protected" { - endpoint "/secure" { - access_control = ["jwt"] + api "protected" { + endpoint "/secure" { + access_control = ["jwt"] - proxy { - url = "https://protected-resource.org" - } - } + proxy { + url = "https://protected-resource.org" + } } + } } ``` diff --git a/docs/website/content/2.configuration/4.block/settings.md b/docs/website/content/2.configuration/4.block/settings.md index 937c61cc5..c72b8d2f8 100644 --- a/docs/website/content/2.configuration/4.block/settings.md +++ b/docs/website/content/2.configuration/4.block/settings.md @@ -7,6 +7,7 @@ gateway instance. |:--------------------------------|:---------------|:--------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------| | `accept_forwarded_url` | tuple (string) | `[]` | Which `X-Forwarded-*` request headers should be accepted to change the [request variables](../modifiers#request) `url`, `origin`, `protocol`, `host`, `port`. Valid values are `"proto"`, `"host"` and `"port"`. The port in `X-Forwarded-Port` takes precedence over a port in `X-Forwarded-Host`. | Affects relative url values for [`sp_acs_url`](saml) attribute and `redirect_uri` attribute within [beta_oauth2](oauth2) & [oidc](oidc). | `["proto","host","port"]` | | `default_port` | number | `8080` | Port which will be used if not explicitly specified per host within the [`hosts`](server) list. | - | - | +| `environment` | string | `""` | [Environment](../command-line#global-options) Couper is to run in. | - | `"prod"` | | `health_path` | string | `"/healthz"` | Health path which is available for all configured server and ports. | - | - | | `https_dev_proxy` | tuple (string) | `[]` | List of tls port mappings to define the tls listen port and the target one. A self-signed certificate will be generated on the fly based on given hostname. | Certificates will be hold in memory and are generated once. | `["443:8080", "8443:8080"]` | | `log_format` | string | `"common"` | Switch for tab/field based colored view or JSON log lines. Valid values are `"common"` and `"json"`. | - | - | @@ -16,10 +17,10 @@ gateway instance. | `request_id_accept_from_header` | string | `""` | Name of a client request HTTP header field that transports the `request.id` which Couper takes for logging and transport to the backend (if configured). | - | `X-UID` | | `request_id_backend_header` | string | `Couper-Request-ID` | Name of a HTTP header field which Couper uses to transport the `request.id` to the backend. | - | - | | `request_id_client_header` | string | `Couper-Request-ID` | Name of a HTTP header field which Couper uses to transport the `request.id` to the client. | - | - | -| `request_id_format` | string | `"common"` | Valid values are `"common"` and `"uuid4"`. If set to `"uuid4"` a rfc4122 uuid is used for `request.id` and related log fields. | - | - | +| `request_id_format` | string | `"common"` | Valid values are `"common"` and `"uuid4"`. If set to `"uuid4"` a RFC 4122 uuid is used for `request.id` and related log fields. | - | - | | `secure_cookies` | string | `""` | Valid values are `""` and `"strip"`. If set to `"strip"`, the `Secure` flag is removed from all `Set-Cookie` HTTP header fields. | - | - | | `xfh` | bool | `false` | Option to use the `X-Forwarded-Host` header as the request host. | - | - | -| `beta_metrics` | bool | `false` | Option to enable the Prometheus [metrics](METRICS.md) exporter. | - | - | +| `beta_metrics` | bool | `false` | Option to enable the Prometheus [metrics](/observation/metrics) exporter. | - | - | | `beta_metrics_port` | number | `9090` | Prometheus exporter listen port. | - | - | | `beta_service_name` | string | `"couper"` | The service name which applies to the `service_name` metric labels. | - | - | -| `ca_file` | string | `""` | Option for adding the given PEM encoded ca-certificate to the existing system certificate pool for all outgoing connections. | - | - | +| `ca_file` | string | `""` | Option for adding the given PEM encoded CA certificate to the existing system certificate pool for all outgoing connections. | - | - | diff --git a/docs/website/content/2.configuration/5.variables.md b/docs/website/content/2.configuration/5.variables.md index dad922c23..b167efb03 100644 --- a/docs/website/content/2.configuration/5.variables.md +++ b/docs/website/content/2.configuration/5.variables.md @@ -11,9 +11,10 @@ The second evaluation will happen during the request/response handling. ## `couper` -| Variable | Type | Description | Example | -| :------------------------------- | :----- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------- | -| `version` | string | Couper's version number | `"1.3.1"` | +| Variable | Type | Description | Example | +| :------------------------------- | :----- | :------------------------------------------------------------------------- | :-------- | +| `version` | string | Couper's version number | `"1.9.2"` | +| `environment` | string | The [environment](../command-line#global-options) Couper currently runs in | `"prod"` | ## `env` diff --git a/eval/context.go b/eval/context.go index bd3e08bb8..d95df2822 100644 --- a/eval/context.go +++ b/eval/context.go @@ -54,7 +54,7 @@ type Context struct { syncedVariables *SyncedVariables } -func NewContext(srcBytes [][]byte, defaults *config.Defaults) *Context { +func NewContext(srcBytes [][]byte, defaults *config.Defaults, environment string) *Context { var defaultEnvVariables config.DefaultEnvVars if defaults != nil { defaultEnvVariables = defaults.EnvironmentVariables @@ -62,7 +62,7 @@ func NewContext(srcBytes [][]byte, defaults *config.Defaults) *Context { variables := make(map[string]cty.Value) variables[Environment] = newCtyEnvMap(srcBytes, defaultEnvVariables) - variables[Couper] = newCtyCouperVariablesMap() + variables[Couper] = newCtyCouperVariablesMap(environment) return &Context{ eval: &hcl.EvalContext{ @@ -73,13 +73,17 @@ func NewContext(srcBytes [][]byte, defaults *config.Defaults) *Context { } } +func NewDefaultContext() *Context { + return NewContext(nil, nil, "") +} + // ContextFromRequest extracts the eval.Context implementation value from given request and // returns a noop one as fallback. func ContextFromRequest(req *http.Request) *Context { if evalCtx, ok := req.Context().Value(request.ContextType).(*Context); ok { return evalCtx } - return NewContext(nil, nil) + return NewDefaultContext() } func (c *Context) WithContext(ctx context.Context) context.Context { @@ -603,9 +607,10 @@ func newCtyEnvMap(srcBytes [][]byte, defaultValues map[string]string) cty.Value return cty.MapVal(ctyMap) } -func newCtyCouperVariablesMap() cty.Value { +func newCtyCouperVariablesMap(environment string) cty.Value { ctyMap := map[string]cty.Value{ - "version": cty.StringVal(utils.VersionName), + "environment": cty.StringVal(environment), + "version": cty.StringVal(utils.VersionName), } return cty.MapVal(ctyMap) } diff --git a/eval/context_test.go b/eval/context_test.go index c5c52436e..8f5dba9de 100644 --- a/eval/context_test.go +++ b/eval/context_test.go @@ -13,6 +13,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/avenga/couper/config/configload" + "github.com/avenga/couper/config/parser" "github.com/avenga/couper/config/request" "github.com/avenga/couper/eval" "github.com/avenga/couper/internal/seetie" @@ -32,7 +33,7 @@ func TestNewHTTPContext(t *testing.T) { } } - baseCtx := eval.NewContext(nil, nil) + baseCtx := eval.NewDefaultContext() tests := []struct { name string @@ -212,24 +213,40 @@ func TestCouperVariables(t *testing.T) { tests := []struct { name string hcl string + env string want map[string]string }{ { "test", ` - server "test" { - api {} - } + server "test" {} + `, + "", + map[string]string{"version": utils.VersionName, "environment": ""}, + }, + { + "environment", + ` + server {} `, - map[string]string{"version": utils.VersionName}, + "bar", + map[string]string{"version": utils.VersionName, "environment": "bar"}, }, } for _, tt := range tests { t.Run(tt.name, func(subT *testing.T) { - cf, err := configload.LoadBytes([]byte(tt.hcl), "couper.hcl") + bytes := []byte(tt.hcl) + hclBody, diags := parser.Load(bytes, "couper.hcl") + if diags.HasErrors() { + subT.Error(diags) + return + } + + cf, err := configload.LoadConfig(hclBody, [][]byte{bytes}, tt.env) if err != nil { - subT.Fatal(err) + subT.Error(err) + return } hclContext := cf.Context.Value(request.ContextType).(*eval.Context).HCLContext() diff --git a/eval/value_test.go b/eval/value_test.go index e7b05594a..47c117989 100644 --- a/eval/value_test.go +++ b/eval/value_test.go @@ -14,7 +14,7 @@ import ( ) func TestValue(t *testing.T) { - evalCtx := NewContext(nil, &config.Defaults{}).HCLContext() + evalCtx := NewContext(nil, &config.Defaults{}, "").HCLContext() rootObj := cty.ObjectVal(map[string]cty.Value{ "exist": cty.StringVal("here"), "slice": seetie.GoToValue([]string{"1", "2"}), diff --git a/handler/endpoint_test.go b/handler/endpoint_test.go index 35fc42129..6745d2ab6 100644 --- a/handler/endpoint_test.go +++ b/handler/endpoint_test.go @@ -71,7 +71,7 @@ func TestEndpoint_RoundTrip_Eval(t *testing.T) { }}, } - evalCtx := eval.NewContext(nil, nil) + evalCtx := eval.NewDefaultContext() for _, tt := range tests { t.Run(tt.name, func(subT *testing.T) { @@ -229,7 +229,7 @@ func TestEndpoint_RoundTripContext_Variables_json_body(t *testing.T) { // normally injected by server/http helper.Must(eval.SetGetBody(req, eval.BufferRequest, 1024)) - *req = *req.WithContext(eval.NewContext(nil, nil).WithClientRequest(req)) + *req = *req.WithContext(eval.NewDefaultContext().WithClientRequest(req)) rec := httptest.NewRecorder() rw := writer.NewResponseWriter(rec, "") // crucial for working ep due to res.Write() @@ -350,7 +350,7 @@ func TestEndpoint_RoundTripContext_Null_Eval(t *testing.T) { } else { req.Header.Set("Content-Type", "application/json") } - req = req.WithContext(eval.NewContext(nil, nil).WithClientRequest(req)) + req = req.WithContext(eval.NewDefaultContext().WithClientRequest(req)) rec := httptest.NewRecorder() rw := writer.NewResponseWriter(rec, "") // crucial for working ep due to res.Write() @@ -432,7 +432,7 @@ func TestEndpoint_ServeHTTP_FaultyDefaultResponse(t *testing.T) { ctx := context.Background() req := httptest.NewRequest(http.MethodGet, "http://", nil).WithContext(ctx) - ctx = eval.NewContext(nil, nil).WithClientRequest(req) + ctx = eval.NewDefaultContext().WithClientRequest(req) ctx = context.WithValue(ctx, request.UID, "test123") rec := httptest.NewRecorder() @@ -483,7 +483,7 @@ func TestEndpoint_ServeHTTP_Cancel(t *testing.T) { }, log.WithContext(ctx), nil) req := httptest.NewRequest(http.MethodGet, "https://couper.io/", nil) - ctx = eval.NewContext(nil, nil).WithClientRequest(req.WithContext(ctx)) + ctx = eval.NewDefaultContext().WithClientRequest(req.WithContext(ctx)) start := time.Now() go func() { diff --git a/handler/producer/request_test.go b/handler/producer/request_test.go index e76e24d40..b5498a42e 100644 --- a/handler/producer/request_test.go +++ b/handler/producer/request_test.go @@ -102,7 +102,7 @@ func Test_ProduceExpectedStatus(t *testing.T) { for i, rt := range []producer.Roundtrip{requests, proxies} { t.Run(testNames[i]+"_"+tt.name, func(t *testing.T) { - ctx := eval.NewContext(nil, nil).WithClientRequest(clientRequest) + ctx := eval.NewDefaultContext().WithClientRequest(clientRequest) outreq := clientRequest.WithContext(ctx) outreq.Header.Set("X-Status", strconv.Itoa(tt.reflectStatus)) diff --git a/handler/proxy_test.go b/handler/proxy_test.go index 233ed26ad..5927c3fac 100644 --- a/handler/proxy_test.go +++ b/handler/proxy_test.go @@ -34,7 +34,7 @@ func TestProxy_BlacklistHeaderRemoval(t *testing.T) { outreq := httptest.NewRequest("GET", "https://1.2.3.4/", nil) outreq.Header.Set("Authorization", "Basic 123") outreq.Header.Set("Cookie", "123") - outreq = outreq.WithContext(eval.NewContext(nil, &config.Defaults{}).WithClientRequest(outreq)) + outreq = outreq.WithContext(eval.NewContext(nil, &config.Defaults{}, "").WithClientRequest(outreq)) ctx, cancel := context.WithDeadline(outreq.Context(), time.Now().Add(time.Millisecond*50)) outreq = outreq.WithContext(ctx) defer cancel() diff --git a/handler/transport/backend_test.go b/handler/transport/backend_test.go index 3bc606ed0..95497c444 100644 --- a/handler/transport/backend_test.go +++ b/handler/transport/backend_test.go @@ -383,7 +383,7 @@ func TestBackend_director(t *testing.T) { if !ok && tt.expReq.Host != outreq.Host { subT.Errorf("expected same host value, want: %q, got: %q", outreq.Host, tt.expReq.Host) } else if ok { - hostVal, _ := hostnameExp.Expr.Value(eval.NewContext(nil, nil).HCLContext()) + hostVal, _ := hostnameExp.Expr.Value(eval.NewDefaultContext().HCLContext()) hostname := seetie.ValueToString(hostVal) if hostname != tt.expReq.Host { subT.Errorf("expected a configured request host: %q, got: %q", hostname, tt.expReq.Host) diff --git a/internal/test/helper_proxy.go b/internal/test/helper_proxy.go index 8a2c7efc0..18bc00fdc 100644 --- a/internal/test/helper_proxy.go +++ b/internal/test/helper_proxy.go @@ -41,6 +41,6 @@ func (h *Helper) NewInlineContext(inlineHCL string) hcl.Body { } var remain hclBody - h.Must(hclsimple.Decode(h.tb.Name()+".hcl", []byte(inlineHCL), eval.NewContext(nil, nil).HCLContext(), &remain)) + h.Must(hclsimple.Decode(h.tb.Name()+".hcl", []byte(inlineHCL), eval.NewDefaultContext().HCLContext(), &remain)) return body.MergeBodies(remain.Inline) } diff --git a/server/testdata/integration/environment/01_couper.hcl b/server/testdata/integration/environment/01_couper.hcl index 16f5ea2e0..101d3178e 100644 --- a/server/testdata/integration/environment/01_couper.hcl +++ b/server/testdata/integration/environment/01_couper.hcl @@ -3,7 +3,7 @@ environment "test" { endpoint "/test" { environment "test" "foo" "bar" { set_response_headers = { - X-Test-Env = "test" + X-Test-Env = couper.environment } } proxy {