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

Env concept #521

Merged
merged 11 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all 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 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. |
alex-schneider marked this conversation as resolved.
Show resolved Hide resolved

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
}

alex-schneider marked this conversation as resolved.
Show resolved Hide resolved
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