From d8824d299aa8dbbd8ef65888fbed8a33db745a02 Mon Sep 17 00:00:00 2001 From: Yar Kravtsov Date: Mon, 16 Oct 2023 07:29:37 +0300 Subject: [PATCH] Add env var support in config --- README.md | 5 + config/config.go | 21 ++- config/config_test.go | 199 ++++++++++++++++++++++++++ example/http/config.json | 4 +- example/http/docker-compose.yml | 5 +- example/websockets/docker-compose.yml | 2 +- 6 files changed, 228 insertions(+), 8 deletions(-) create mode 100644 config/config_test.go diff --git a/README.md b/README.md index bcdc397..c38ed97 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ GateH8 is a flexible and customizable API Gateway designed to proxy requests to - [Configuration Guide](#configuration-guide) - [General Settings](#general-settings) - [Path Variables and Wildcards](#path-variables-and-wildcards) + - [Environment Variables](#environment-variables) - [Virtual Hosts and Routes](#virtual-hosts-and-routes) - [Wildcard Domain Routing](#wildcard-domain-routing) - [CORS Settings](#cors-settings) @@ -99,6 +100,10 @@ Additionally, GateH8 supports wildcards within path configurations. By using an The above configuration will match and route requests like `/products/1`, `/products/soap`, and so on. +### Environment Variables + +GateH8 supports environment variables in the configuration file. This allows you to define dynamic values for your configuration, such as backend URLs, without having to hardcode them. + ### Virtual Hosts and Routes Virtual hosts enable you to route traffic differently based on the domain of the incoming request. diff --git a/config/config.go b/config/config.go index 31fe2ed..a6b6e3a 100644 --- a/config/config.go +++ b/config/config.go @@ -82,17 +82,19 @@ type Config struct { // GetConfig reads the API Gateway's configuration from a JSON file and returns it. // It handles any issues with reading or parsing the configuration file. func GetConfig() (*Config, error) { - content, err := os.ReadFile("config.json") + rawConfig, err := os.ReadFile("config.json") if err != nil { return nil, fmt.Errorf("error reading config.json: %w", err) } + rawConfig = replaceEnvVars(rawConfig) + config := new(Config) // Use new() to get a pointer directly, prevent local-to-heap migration. - if err = json.Unmarshal(content, config); err != nil { + if err = json.Unmarshal(rawConfig, config); err != nil { return nil, fmt.Errorf("error parsing config.json: %w", err) } - anyVhostWithSSL, allVhostsWithSSL := check(config) + anyVhostWithSSL, allVhostsWithSSL := checkVhostsWithTLS(config) // If there's any vhost with TLS configured but not all of them have, then it's a config error. if anyVhostWithSSL && !allVhostsWithSSL { @@ -103,7 +105,18 @@ func GetConfig() (*Config, error) { return config, nil } -func check(config *Config) (bool, bool) { +func replaceEnvVars(rawConfig []byte) []byte { + // replace ${path} with [[path]] to avoid env var replacement + rawConfig = []byte(strings.ReplaceAll(string(rawConfig), "${path}", "[[path]]")) + // replace env vars + rawConfig = []byte(os.ExpandEnv(string(rawConfig))) + // replace [[path]] with ${path} to restore original value + rawConfig = []byte(strings.ReplaceAll(string(rawConfig), "[[path]]", "${path}")) + + return rawConfig +} + +func checkVhostsWithTLS(config *Config) (bool, bool) { // Check if any vhost has TLS configured anyVhostWithSSL := false allVhostsWithSSL := true diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000..6f9f366 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,199 @@ +package config + +import ( + "os" + "reflect" + "testing" +) + +func Test_replaceEnvVars(t *testing.T) { + type args struct { + rawConfig []byte + } + tests := []struct { + name string + args args + envName string + envValue string + want []byte + }{ + { + name: "test", + args: args{ + rawConfig: []byte(`{ +{ + "apiGateway": { + "name": "MyAPIGateway", + "version": "1.0.0" + }, + "vhosts": { + "*": { + "endpoints": [ + { + "path": "/ws", + "backend": { + "url": "ws://127.0.0.1:8080/ws/$TEST" + }, + "websocket": { + "readBufferSize": 1024, + "writeBufferSize": 1024, + "allowedOrigins": ["*"] + } + } + ] + } + } +} +}`), + }, + envName: "TEST", + envValue: "test", + want: []byte(`{ +{ + "apiGateway": { + "name": "MyAPIGateway", + "version": "1.0.0" + }, + "vhosts": { + "*": { + "endpoints": [ + { + "path": "/ws", + "backend": { + "url": "ws://127.0.0.1:8080/ws/test" + }, + "websocket": { + "readBufferSize": 1024, + "writeBufferSize": 1024, + "allowedOrigins": ["*"] + } + } + ] + } + } +} +}`), + }, + { + name: "path", + args: args{ + rawConfig: []byte(`{ +{ + "apiGateway": { + "name": "MyAPIGateway", + "version": "1.0.0" + }, + "vhosts": { + "*": { + "endpoints": [ + { + "path": "/ws", + "backend": { + "url": "ws://127.0.0.1:8080/ws" + }, + "websocket": { + "readBufferSize": 1024, + "writeBufferSize": 1024, + "allowedOrigins": ["*"] + } + } + ] + } + } +} +}`), + }, + want: []byte(`{ +{ + "apiGateway": { + "name": "MyAPIGateway", + "version": "1.0.0" + }, + "vhosts": { + "*": { + "endpoints": [ + { + "path": "/ws", + "backend": { + "url": "ws://127.0.0.1:8080/ws" + }, + "websocket": { + "readBufferSize": 1024, + "writeBufferSize": 1024, + "allowedOrigins": ["*"] + } + } + ] + } + } +} +}`), + envName: "path", + envValue: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = os.Setenv(tt.envName, tt.envValue) + if got := replaceEnvVars(tt.args.rawConfig); !reflect.DeepEqual(got, tt.want) { + t.Errorf("replaceEnvVars() = %v, wantAny %v", got, tt.want) + } + }) + } +} + +func Test_checkVhostsWithTLS(t *testing.T) { + type args struct { + config *Config + } + tests := []struct { + name string + args args + wantAny bool + wantAll bool + }{ + { + name: "test", + args: args{ + config: &Config{Vhosts: map[string]Vhost{ + "www.sample.org": {TLS: &TLSConfig{}}, + }}, + }, + wantAny: true, + wantAll: true, + }, + { + name: "test", + args: args{ + config: &Config{Vhosts: map[string]Vhost{ + "www.sample.org": {TLS: &TLSConfig{}}, + "www1.sample.org": {}, + }}, + }, + wantAny: true, + wantAll: false, + }, + { + name: "test", + args: args{ + config: &Config{Vhosts: map[string]Vhost{ + "www.sample.org": {}, + "www1.sample.org": {}, + }}, + }, + wantAny: false, + wantAll: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := checkVhostsWithTLS(tt.args.config) + if got != tt.wantAny { + t.Errorf("checkVhostsWithTLS() got = %v, wantAny %v", got, tt.wantAny) + } + if got1 != tt.wantAll { + t.Errorf("checkVhostsWithTLS() got1 = %v, wantAny %v", got1, tt.wantAll) + } + }) + } +} diff --git a/example/http/config.json b/example/http/config.json index d0f99e1..bbf9be8 100644 --- a/example/http/config.json +++ b/example/http/config.json @@ -12,7 +12,7 @@ "GET" ], "backend": { - "url": "http://nginx1/", + "url": "http://${HOST1}/", "timeout": 5000 } }, @@ -22,7 +22,7 @@ "GET" ], "backend": { - "url": "http://nginx2/", + "url": "http://${HOST2}/", "timeout": 5000 } } diff --git a/example/http/docker-compose.yml b/example/http/docker-compose.yml index f54a643..0600369 100644 --- a/example/http/docker-compose.yml +++ b/example/http/docker-compose.yml @@ -1,8 +1,11 @@ services: gateh8: - image: yarlson/gateh8:0.2.1 + image: yarlson/gateh8:0.3.0 ports: - "80:80" + environment: + - HOST1=nginx1 + - HOST2=nginx2 volumes: - ./config.json:/config.json command: ["-a", ":80"] diff --git a/example/websockets/docker-compose.yml b/example/websockets/docker-compose.yml index 1fd07fe..9442da5 100644 --- a/example/websockets/docker-compose.yml +++ b/example/websockets/docker-compose.yml @@ -1,6 +1,6 @@ services: gateh8: - image: yarlson/gateh8:0.2.1 + image: yarlson/gateh8:0.3.0 ports: - "80:80" volumes: