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

Allow evaluation for the defaults block #630

Merged
merged 14 commits into from
Nov 25, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Unreleased changes are available as `avenga/couper:edge` container.
* **Changed**
* Replaced the JWT library because the former library was no longer maintained ([#612](https://github.com/avenga/couper/pull/612))
* Routing and [OpenAPI validation](https://docs.couper.io/configuration/block/openapi) now use gorilla/mux ([#614](https://github.com/avenga/couper/pull/614))
* Usage of `env` variables and functions is now possible for the `defaults` block ([#630](https://github.com/avenga/couper/pull/630))

* **Fixed**
* Aligned the evaluation of [`beta_oauth2`](https://docs.couper.io/configuration/block/oauth2_ac)/[`oidc`](https://docs.couper.io/configuration/block/oidc) `redirect_uri` to `saml` `sp_acs_url` ([#589](https://github.com/avenga/couper/pull/589))
Expand Down
14 changes: 4 additions & 10 deletions config/configload/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
hclbody "github.com/avenga/couper/config/body"
"github.com/avenga/couper/config/sequence"
"github.com/avenga/couper/errors"
"github.com/avenga/couper/eval"
)

type helper struct {
Expand All @@ -23,16 +22,11 @@ type helper struct {
}

// newHelper creates a container with some methods to keep things simple here and there.
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
}

func newHelper(body hcl.Body) (*helper, error) {
couperConfig := &config.Couper{
Context: eval.NewContext(src, defaultsBlock.Defaults, environment),
Context: evalContext,
Definitions: &config.Definitions{},
Defaults: defaultsBlock.Defaults,
Defaults: defaultsConfig,
Settings: config.NewDefaultSettings(),
}

Expand All @@ -45,7 +39,7 @@ func newHelper(body hcl.Body, src [][]byte, environment string) (*helper, error)
return &helper{
config: couperConfig,
content: content,
context: couperConfig.Context.(*eval.Context).HCLContext(),
context: evalContext.HCLContext(),
defsBackends: make(map[string]*hclsyntax.Body),
}, nil
}
Expand Down
28 changes: 17 additions & 11 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,16 @@ func init() {

func updateContext(body hcl.Body, srcBytes [][]byte, environment string) hcl.Diagnostics {
defaultsBlock := &config.DefaultsBlock{}
if diags := gohcl.DecodeBody(body, nil, defaultsBlock); diags.HasErrors() {
// defaultsCtx is a temporary one to allow env variables and functions for defaults {}
defaultsCtx := eval.NewContext(srcBytes, nil, environment).HCLContext()
if diags := gohcl.DecodeBody(body, defaultsCtx, defaultsBlock); diags.HasErrors() {
return diags
}
defaultsConfig = defaultsBlock.Defaults // global assign

// We need the "envContext" to be able to resolve absolute paths in the config.
defaultsConfig = defaultsBlock.Defaults
evalContext = eval.NewContext(srcBytes, defaultsConfig, environment)
envContext = evalContext.HCLContext()
envContext = evalContext.HCLContext() // global assign

return nil
}
Expand Down Expand Up @@ -179,7 +181,7 @@ func bodiesToConfig(parsedBodies []*hclsyntax.Body, srcBytes [][]byte, env strin
return nil, err
}

conf, err := LoadConfig(configBody, srcBytes, env)
conf, err := LoadConfig(configBody)
if err != nil {
return nil, err
}
Expand All @@ -206,12 +208,12 @@ func LoadFiles(filesList []string, env string) (*config.Couper, error) {

if env == "" {
settingsBlock := mergeSettings(parsedBodies)
settings := &config.Settings{}
if diags := gohcl.DecodeBody(settingsBlock.Body, nil, settings); diags.HasErrors() {
confSettings := &config.Settings{}
if diags := gohcl.DecodeBody(settingsBlock.Body, nil, confSettings); diags.HasErrors() {
return nil, diags
}
if settings.Environment != "" {
return LoadFiles(filesList, settings.Environment)
if confSettings.Environment != "" {
return LoadFiles(filesList, confSettings.Environment)
}
}

Expand Down Expand Up @@ -257,6 +259,10 @@ func loadTestContents(tcs []testContent) (*config.Couper, error) {
}

func LoadBytes(src []byte, filename string) (*config.Couper, error) {
return LoadBytesEnv(src, filename, "")
}

func LoadBytesEnv(src []byte, filename, env string) (*config.Couper, error) {
hclBody, err := parser.Load(src, filename)
if err != nil {
return nil, err
Expand All @@ -266,17 +272,17 @@ func LoadBytes(src []byte, filename string) (*config.Couper, error) {
return nil, err
}

return LoadConfig(hclBody, [][]byte{src}, "")
return bodiesToConfig([]*hclsyntax.Body{hclBody}, [][]byte{src}, env)
}

func LoadConfig(body *hclsyntax.Body, src [][]byte, environment string) (*config.Couper, error) {
func LoadConfig(body *hclsyntax.Body) (*config.Couper, error) {
var err error

if diags := ValidateConfigSchema(body, &config.Couper{}); diags.HasErrors() {
return nil, diags
}

helper, err := newHelper(body, src, environment)
helper, err := newHelper(body)
if err != nil {
return nil, err
}
Expand Down
38 changes: 38 additions & 0 deletions config/configload/load_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,44 @@ func TestConfigErrors(t *testing.T) {
}`,
`configuration error: referenced backend "as" is not defined`,
},
{
"wrong environment_variables type",
`server {}
defaults {
environment_variables = "val"
}`,
"couper.hcl:3,30-35: environment_variables must be object type; ",
},
{
"unsupported key scope traversal expression",
`server {}
defaults {
environment_variables = {
env.FOO = "val"
}
}`,
"couper.hcl:4,8-15: unsupported key scope traversal expression; ",
},
{
"unsupported key template expression",
`server {}
defaults {
environment_variables = {
"key${1 + 0}" = "val"
}
}`,
"couper.hcl:4,8-21: unsupported key template expression; ",
},
{
"unsupported key expression",
`server {}
defaults {
environment_variables = {
to_upper("key") = "val"
}
}`,
"couper.hcl:4,8-23: unsupported key expression; ",
},
}

for _, tt := range tests {
Expand Down
72 changes: 53 additions & 19 deletions config/configload/merge.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,48 +563,82 @@ func mergeDefinitions(bodies []*hclsyntax.Body) (*hclsyntax.Block, map[string]*h

func mergeDefaults(bodies []*hclsyntax.Body) (*hclsyntax.Block, error) {
attrs := make(hclsyntax.Attributes)
envVars := make(map[string]cty.Value)
envVars := make(map[string]hclsyntax.Expression)

for _, body := range bodies {
for _, block := range body.Blocks {
if block.Type == defaults {
for name, attr := range block.Body.Attributes {
if name == environmentVars {
v, err := eval.Value(nil, attr.Expr)
if err != nil {
return nil, err
}
if block.Type != defaults {
continue
}

for k, value := range v.AsValueMap() {
if value.Type() != cty.String {
return nil, fmt.Errorf("value in 'environment_variables' is not a string")
}
for name, attr := range block.Body.Attributes {
if name != environmentVars {
attrs[name] = attr // Currently not used
continue
}

expObj, ok := attr.Expr.(*hclsyntax.ObjectConsExpr)
if !ok {
r := attr.Expr.Range()
return nil, newDiagErr(&r, fmt.Sprintf("%s must be object type", environmentVars))
}

envVars[k] = value
for _, item := range expObj.Items {
k := item.KeyExpr.(*hclsyntax.ObjectConsKeyExpr)
r := item.KeyExpr.Range()
var keyName string
switch exp := k.Wrapped.(type) {
case *hclsyntax.ScopeTraversalExpr:
if len(exp.Traversal) > 1 {
return nil, newDiagErr(&r, "unsupported key scope traversal expression")
}
} else {
attrs[name] = attr // Currently not used
keyName = exp.Traversal.RootName()
case *hclsyntax.TemplateExpr:
if !exp.IsStringLiteral() {
return nil, newDiagErr(&r, "unsupported key template expression")
}
v, _ := exp.Value(nil)
keyName = v.AsString()
default:
r := item.KeyExpr.Range()
return nil, newDiagErr(&r, "unsupported key expression")
}

envVars[keyName] = item.ValueExpr
}
}
}
}

if len(envVars) > 0 {
items := make([]hclsyntax.ObjectConsItem, 0)
for k, v := range envVars {
items = append(items, hclsyntax.ObjectConsItem{
KeyExpr: &hclsyntax.ObjectConsKeyExpr{
Wrapped: &hclsyntax.ScopeTraversalExpr{
Traversal: hcl.Traversal{hcl.TraverseRoot{Name: k}},
},
ForceNonLiteral: false,
},
ValueExpr: v,
})
}

attrs[environmentVars] = &hclsyntax.Attribute{
Name: environmentVars,
Expr: &hclsyntax.LiteralValueExpr{
Val: cty.MapVal(envVars),
Expr: &hclsyntax.ObjectConsExpr{
Items: items,
},
}
}

return &hclsyntax.Block{
defaultsBlock := &hclsyntax.Block{
Type: defaults,
Body: &hclsyntax.Body{
Attributes: attrs,
},
}, nil
}
return defaultsBlock, nil
}

func mergeSettings(bodies []*hclsyntax.Body) *hclsyntax.Block {
Expand Down
2 changes: 1 addition & 1 deletion config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "github.com/hashicorp/hcl/v2"
type DefaultEnvVars map[string]string

type Defaults struct {
EnvironmentVariables DefaultEnvVars `hcl:"environment_variables,optional" docs:"One or more environment variable assignments"`
EnvironmentVariables DefaultEnvVars `hcl:"environment_variables,optional" docs:"One or more environment variable assignments. Keys must be either identifiers or simple string expressions."`
}

type DefaultsBlock struct {
Expand Down
2 changes: 1 addition & 1 deletion docs/website/content/2.configuration/4.block/defaults.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The `defaults` block lets you define default values.
values: [
{
"default": "",
"description": "One or more environment variable assignments",
"description": "One or more environment variable assignments. Keys must be either identifiers or simple string expressions.",
"name": "environment_variables",
"type": "object"
}
Expand Down
13 changes: 3 additions & 10 deletions eval/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ 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"
Expand Down Expand Up @@ -191,7 +190,8 @@ func TestDefaultEnvVariables(t *testing.T) {
t.Run(tt.name, func(subT *testing.T) {
cf, err := configload.LoadBytes([]byte(tt.hcl), "couper.hcl")
if err != nil {
subT.Fatal(err)
subT.Error(err)
return
}

hclContext := cf.Context.(*eval.Context).HCLContext()
Expand Down Expand Up @@ -236,14 +236,7 @@ func TestCouperVariables(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(subT *testing.T) {
bytes := []byte(tt.hcl)
hclBody, err := parser.Load(bytes, "couper.hcl")
if err != nil {
subT.Error(err)
return
}

cf, err := configload.LoadConfig(hclBody, [][]byte{bytes}, tt.env)
cf, err := configload.LoadBytesEnv([]byte(tt.hcl), "couper.hcl", tt.env)
if err != nil {
subT.Error(err)
return
Expand Down
35 changes: 35 additions & 0 deletions server/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,41 @@ func TestHTTPServer_EnvVars(t *testing.T) {
}
}

func TestHTTPServer_DefaultEnvVars(t *testing.T) {
helper := test.New(t)
client := newClient()

env.SetTestOsEnviron(func() []string {
return []string{"VALUE_4=value4"}
})
defer env.SetTestOsEnviron(os.Environ)

shutdown, hook := newCouper("testdata/integration/env/02_couper.hcl", test.New(t))
defer shutdown()

hook.Reset()

req, err := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)
helper.Must(err)

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

if res.StatusCode != http.StatusOK {
t.Errorf("expected 200, got %d", res.StatusCode)
}

b, err := io.ReadAll(res.Body)
helper.Must(err)

var result []string
helper.Must(json.Unmarshal(b, &result))

if diff := cmp.Diff(result, []string{"value1", "", "default_value_3", "value4", "value5"}); diff != "" {
t.Error(diff)
}
}

func TestHTTPServer_XFHHeader(t *testing.T) {
client := newClient()

Expand Down
3 changes: 1 addition & 2 deletions server/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,7 @@ func TestHTTPServer_ServeHTTP_Files2(t *testing.T) {
helper.Must(err)

error404Content := []byte("<html><body><h1>route not found error: My custom error template</h1></body></html>")
spaContent, err := os.ReadFile(conf.Servers[0].SPAs[0].BootstrapFile)
helper.Must(err)
spaContent := []byte("<html><body><h1>vue.js</h1></body></html>")

tmpStoreCh := make(chan struct{})
defer close(tmpStoreCh)
Expand Down
17 changes: 17 additions & 0 deletions server/testdata/integration/env/02_couper.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
server {
endpoint "/" {
response {
json_body = [env.KEY1, env.KEY2, env.KEY3, env.KEY4, env.KEY5]
}
}
}

defaults {
environment_variables = {
KEY1 = "value1"
KEY2 = env.VALUE_2
KEY3 = default(env.VALUE_3, "default_value_3")
KEY4 = env.VALUE_4
"KEY5" = "value5"
}
}