Skip to content

Commit

Permalink
Env concept (#521)
Browse files Browse the repository at this point in the history
* Add new cli param

* Docu

* Implement env-concept

* Fix tests

* Simplify code

* Add test

* Use newDiagErr()

* Log used env

* Test multiple labels

Co-authored-by: Marcel Ludwig <marcel.ludwig@avenga.com>
  • Loading branch information
Alex Schneider and Marcel Ludwig authored Jun 22, 2022
1 parent 0682420 commit 02b7652
Show file tree
Hide file tree
Showing 15 changed files with 201 additions and 25 deletions.
1 change: 1 addition & 0 deletions DOCKER.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ docker run avenga/couper run -watch -p 8081
|:-------------------------------------| :------ | :---------- |
| COUPER_FILE | `couper.hcl` | Path to the configuration file. |
| COUPER_FILE_DIRECTORY | `""` | Path to the configuration files directory. |
| COUPER_ENVIRONMENT | `""` | Name of environment in which Couper is currently running. |
| COUPER_ACCEPT_FORWARDED_URL | `""` | Which `X-Forwarded-*` request headers should be accepted to change the [request variables](https://github.com/avenga/couper/blob/master/docs/REFERENCE.md#request) `url`, `origin`, `protocol`, `host`, `port`. Comma-separated list of values. Valid values: `proto`, `host`, `port`. |
| COUPER_DEFAULT_PORT | `8080` | Sets the default port to the given value and does not override explicit `[host:port]` configurations from file. |
| COUPER_HEALTH_PATH | `/healthz` | Path for health-check requests for all servers and ports. |
Expand Down
4 changes: 2 additions & 2 deletions command/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestNewRun(t *testing.T) {
return
}

couperFile, err := configload.LoadFile(filepath.Join(wd, "testdata/settings", tt.file))
couperFile, err := configload.LoadFile(filepath.Join(wd, "testdata/settings", tt.file), "")
if err != nil {
subT.Error(err)
}
Expand Down Expand Up @@ -192,7 +192,7 @@ func TestAcceptForwarded(t *testing.T) {
return
}

couperFile, err := configload.LoadFile(filepath.Join(wd, "testdata/settings", tt.file))
couperFile, err := configload.LoadFile(filepath.Join(wd, "testdata/settings", tt.file), "")
if err != nil {
subT.Error(err)
}
Expand Down
4 changes: 2 additions & 2 deletions command/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ func NewVerify() *Verify {
return &Verify{}
}

func (v Verify) Execute(args Args, _ *config.Couper, logger *logrus.Entry) error {
cf, err := configload.LoadFiles(args)
func (v Verify) Execute(args Args, conf *config.Couper, logger *logrus.Entry) error {
cf, err := configload.LoadFiles(args, conf.Environment)
if diags, ok := err.(hcl.Diagnostics); ok {
for _, diag := range diags {
logger.WithError(diag).Error()
Expand Down
57 changes: 57 additions & 0 deletions config/configload/environment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package configload

import (
"github.com/hashicorp/hcl/v2/hclsyntax"
)

func preprocessEnvironmentBlocks(bodies []*hclsyntax.Body, env string) error {
for _, body := range bodies {
if err := preprocessBody(body, env); err != nil {
return err
}
}

return nil
}

func preprocessBody(parent *hclsyntax.Body, env string) error {
var blocks []*hclsyntax.Block

for _, block := range parent.Blocks {
if block.Type != environment {
blocks = append(blocks, block)

continue
}

if len(block.Labels) == 0 {
defRange := block.DefRange()

return newDiagErr(&defRange, "Missing label(s) for 'environment' block")
}

for _, label := range block.Labels {
if err := validLabel(label, getRange(block.Body)); err != nil {
return err
}

if label == env {
blocks = append(blocks, block.Body.Blocks...)

for name, attr := range block.Body.Attributes {
parent.Attributes[name] = attr
}
}
}
}

for _, block := range blocks {
if err := preprocessBody(block.Body, env); err != nil {
return err
}
}

parent.Blocks = blocks

return nil
}
16 changes: 10 additions & 6 deletions config/configload/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
defaults = "defaults"
definitions = "definitions"
endpoint = "endpoint"
environment = "environment"
errorHandler = "error_handler"
files = "files"
nameLabel = "name"
Expand Down Expand Up @@ -125,7 +126,7 @@ func parseFiles(files configfile.Files) ([]*hclsyntax.Body, [][]byte, error) {
return parsedBodies, srcBytes, nil
}

func LoadFiles(filesList []string) (*config.Couper, error) {
func LoadFiles(filesList []string, env string) (*config.Couper, error) {
configFiles, err := configfile.NewFiles(filesList)
if err != nil {
return nil, err
Expand All @@ -140,6 +141,10 @@ func LoadFiles(filesList []string) (*config.Couper, error) {
return nil, fmt.Errorf("missing configuration files")
}

if err := preprocessEnvironmentBlocks(parsedBodies, env); err != nil {
return nil, err
}

defaultsBlock, err := mergeDefaults(parsedBodies)
if err != nil {
return nil, err
Expand Down Expand Up @@ -190,8 +195,8 @@ func LoadFiles(filesList []string) (*config.Couper, error) {
return conf, nil
}

func LoadFile(file string) (*config.Couper, error) {
return LoadFiles([]string{file})
func LoadFile(file, env string) (*config.Couper, error) {
return LoadFiles([]string{file}, env)
}

func LoadBytes(src []byte, filename string) (*config.Couper, error) {
Expand Down Expand Up @@ -449,9 +454,8 @@ func absolutizePaths(fileBody *hclsyntax.Body) error {
if strings.HasPrefix(filePath, "http://") || strings.HasPrefix(filePath, "https://") {
return nil
}
if strings.HasPrefix(filePath, "file:") {
filePath = filePath[5:]
}

filePath = strings.TrimPrefix(filePath, "file:")
if path.IsAbs(filePath) {
return nil
}
Expand Down
2 changes: 2 additions & 0 deletions config/couper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"context"

"github.com/avenga/couper/config/configload/file"
)

Expand All @@ -11,6 +12,7 @@ const DefaultFilename = "couper.hcl"
// Couper represents the <Couper> config object.
type Couper struct {
Context context.Context
Environment string
Files file.Files
Definitions *Definitions `hcl:"definitions,block"`
Servers Servers `hcl:"server,block"`
Expand Down
3 changes: 2 additions & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Couper is build as binary called `couper` with the following commands:
| Argument | Default | Environment | Description |
|:---------------------|:-------------|:---------------------------|:-----------------------------------------------------------------------------------------------------------------------------|
| `-f` | `couper.hcl` | `COUPER_FILE` | Path to a Couper configuration file. |
| `-d` | - | `COUPER_FILE_DIRECTORY` | Path to a directory containing Couper configuration files. |
| `-d` | `""` | `COUPER_FILE_DIRECTORY` | Path to a directory containing Couper configuration files. |
| `-e` | `""` | `COUPER_ENVIRONMENT` | Name of environment in which Couper is currently running. |
| `-watch` | `false` | `COUPER_WATCH` | Watch for configuration file changes and reload on modifications. |
| `-watch-retries` | `5` | `COUPER_WATCH_RETRIES` | Maximum retry count for configuration reloads which could not bind the configured port. |
| `-watch-retry-delay` | `500ms` | `COUPER_WATCH_RETRY_DELAY` | Delay duration before next attempt if an error occurs. |
Expand Down
59 changes: 59 additions & 0 deletions docs/REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [Defaults Block](#defaults-block)
- [Health Block (Beta)](#health-block)
- [Error Handler Block](#error-handler-block)
- [Environment Block](#environment-block)
- [Access Control](#access-control)
- [Health-Check](#health-check)
- [Variables](#variables)
Expand Down Expand Up @@ -624,6 +625,64 @@ Examples:

- [Error Handling for Access Controls](https://github.com/avenga/couper-examples/blob/master/error-handling-ba/README.md).

### Environment Block

The `environment` block lets you refine the Couper configuration based on the set
[environment](./CLI.md#global-options).

| Block name | Context | Label | Nested block(s) |
| :------------ | :------- | :----------------------------------------------- | :---------------------------------- |
| `environment` | Overall. | &#9888; 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
to the configuration.

<!-- TODO: Add link to (still missing) example. Remove the following example. -->

If the [environment](./CLI.md#global-options) value set to `prod`, the following
configuration:

```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"
}
}
}
}
}
```

produces after the preprocessing the following configuration:

```hcl
server {
api "protected" {
endpoint "/secure" {
access_control = ["jwt"]
proxy {
url = "https://protected-resource.org"
}
}
}
}
```

**Note:** The value of the environment set via [Defaults Block](#defaults-block) is ignored.

## Access Control

The configuration of access control is twofold in Couper: You define the particular
Expand Down
18 changes: 12 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func realmain(ctx context.Context, arguments []string) int {
DebugEndpoint bool `env:"debug"`
FilePath string `env:"file"`
DirPath string `env:"file_directory"`
Environment string `env:"environment"`
FileWatch bool `env:"watch"`
FileWatchRetryDelay time.Duration `env:"watch_retry_delay"`
FileWatchRetries int `env:"watch_retries"`
Expand All @@ -69,6 +70,7 @@ func realmain(ctx context.Context, arguments []string) int {
set.BoolVar(&flags.DebugEndpoint, "debug", false, "-debug")
set.Var(&filesList, "f", "-f /path/to/couper.hcl ...")
set.Var(&filesList, "d", "-d /path/to/couper.d/ ...")
set.StringVar(&flags.Environment, "e", "", "-e stage")
set.BoolVar(&flags.FileWatch, "watch", false, "-watch")
set.DurationVar(&flags.FileWatchRetryDelay, "watch-retry-delay", time.Millisecond*500, "-watch-retry-delay 1s")
set.IntVar(&flags.FileWatchRetries, "watch-retries", 5, "-watch-retries 10")
Expand Down Expand Up @@ -117,19 +119,23 @@ func realmain(ctx context.Context, arguments []string) int {
}
}

if cmd == "verify" {
log := newLogger(flags.LogFormat, flags.LogLevel, flags.LogPretty)
log := newLogger(flags.LogFormat, flags.LogLevel, flags.LogPretty)

if flags.Environment != "" {
log.Info(`couper uses "` + flags.Environment + `" environment`)
}

err = command.NewCommand(ctx, cmd).Execute(filesList.paths, nil, log)
if cmd == "verify" {
err = command.NewCommand(ctx, cmd).Execute(filesList.paths, &config.Couper{Environment: flags.Environment}, log)
if err != nil {
return 1
}
return 0
}

confFile, err := configload.LoadFiles(filesList.paths)
confFile, err := configload.LoadFiles(filesList.paths, flags.Environment)
if err != nil {
newLogger(flags.LogFormat, flags.LogLevel, flags.LogPretty).WithError(err).Error()
log.WithError(err).Error()
return 1
}

Expand Down Expand Up @@ -210,7 +216,7 @@ func realmain(ctx context.Context, arguments []string) int {
errRetries = 0 // reset
logger.Info("reloading couper configuration")

cf, reloadErr := configload.LoadFiles(filesList.paths)
cf, reloadErr := configload.LoadFiles(filesList.paths, flags.Environment)
if reloadErr != nil {
logger.WithError(reloadErr).Error("reload failed")
time.Sleep(flags.FileWatchRetryDelay)
Expand Down
2 changes: 1 addition & 1 deletion server/http_endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -806,7 +806,7 @@ func TestEndpointCyclicSequence(t *testing.T) {
defer cleanup(func() {}, test.New(t))

path := filepath.Join(testdataPath, testcase.file)
_, err := configload.LoadFile(path)
_, err := configload.LoadFile(path, "")

diags, ok := err.(*hcl.Diagnostic)
if !ok {
Expand Down
4 changes: 2 additions & 2 deletions server/http_error_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ func TestAccessControl_ErrorHandler_BasicAuth_Wildcard(t *testing.T) {
}

func TestAccessControl_ErrorHandler_Configuration_Error(t *testing.T) {
_, err := configload.LoadFile("testdata/integration/error_handler/03_couper.hcl")
_, err := configload.LoadFile("testdata/integration/error_handler/03_couper.hcl", "")

expectedMsg := "03_couper.hcl:24,12-12: Missing required argument; The argument \"grant_type\" is required, but no definition was found."

Expand Down Expand Up @@ -334,7 +334,7 @@ func TestAccessControl_ErrorHandler_Permissions(t *testing.T) {
}

func Test_Panic_Multi_EH(t *testing.T) {
_, err := configload.LoadFile("testdata/settings/16_couper.hcl")
_, err := configload.LoadFile("testdata/settings/16_couper.hcl", "")

expectedMsg := `: duplicate error type registration: "*"; `

Expand Down
6 changes: 3 additions & 3 deletions server/http_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ func teardown() {
}

func newCouper(file string, helper *test.Helper) (func(), *logrustest.Hook) {
couperConfig, err := configload.LoadFile(filepath.Join(testWorkingDir, file))
couperConfig, err := configload.LoadFile(filepath.Join(testWorkingDir, file), "test")
helper.Must(err)

return newCouperWithConfig(couperConfig, helper)
}

func newCouperMultiFiles(file, dir string, helper *test.Helper) (func(), *logrustest.Hook) {
couperConfig, err := configload.LoadFiles([]string{file, dir})
couperConfig, err := configload.LoadFiles([]string{file, dir}, "test")
helper.Must(err)

return newCouperWithConfig(couperConfig, helper)
Expand Down Expand Up @@ -3506,7 +3506,7 @@ func TestJWKsMaxStale(t *testing.T) {

func TestJWTAccessControlSourceConfig(t *testing.T) {
helper := test.New(t)
couperConfig, err := configload.LoadFile("testdata/integration/config/05_couper.hcl")
couperConfig, err := configload.LoadFile("testdata/integration/config/05_couper.hcl", "")
helper.Must(err)

log, _ := logrustest.NewNullLogger()
Expand Down
22 changes: 22 additions & 0 deletions server/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,3 +710,25 @@ func TestHTTPServer_parseDuration(t *testing.T) {
t.Errorf("%#v", logs[0].Message)
}
}

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

shutdown, _ := newCouper("testdata/integration/environment/01_couper.hcl", test.New(t))
defer shutdown()

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

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

if h := res.Header.Get("X-Test-Env"); h != "test" {
t.Errorf("Unexpected header given: %q", h)
}

if res.StatusCode != http.StatusOK {
t.Errorf("Unexpected status code: %d", res.StatusCode)
}
}
4 changes: 2 additions & 2 deletions server/multi_files_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func TestMultiFiles_MultipleBackends(t *testing.T) {
{"testdata/multi/backends/errors/api_ep.hcl"},
} {
t.Run(tc.config, func(st *testing.T) {
_, err := configload.LoadFile(filepath.Join(testWorkingDir, tc.config))
_, err := configload.LoadFile(filepath.Join(testWorkingDir, tc.config), "")

if !strings.Contains(err.Error(), "Multiple definitions of backend are not allowed.") {
st.Errorf("Unexpected error: %s", err.Error())
Expand Down Expand Up @@ -234,7 +234,7 @@ func Test_MultipleLabels(t *testing.T) {
},
} {
t.Run(tc.name, func(st *testing.T) {
_, err := configload.LoadFile(filepath.Join(testWorkingDir, tc.configPath))
_, err := configload.LoadFile(filepath.Join(testWorkingDir, tc.configPath), "")

if (err != nil && tc.expError == "") ||
(tc.expError != "" && (err == nil || !strings.Contains(err.Error(), tc.expError))) {
Expand Down
Loading

0 comments on commit 02b7652

Please sign in to comment.