From 0e5134ffafdab9346997588c697b5afa275d1a40 Mon Sep 17 00:00:00 2001 From: Alexander Rolek Date: Sat, 17 Aug 2024 13:15:18 -0600 Subject: [PATCH] Implement env.URL config type This type was added for better error handling of the Webserver.HostName config property. --- cmd/tegola/cmd/server.go | 10 +- cmd/tegola_lambda/main.go | 10 +- config/config.go | 14 +- config/config_test.go | 346 +++------------------- config/errors.go | 23 -- config/testdata/empty_proxy_protocol.toml | 68 +++++ config/testdata/happy_path.toml | 62 ++++ config/testdata/missing_env.toml | 62 ++++ config/testdata/test_env.toml | 67 +++++ internal/env/parse.go | 19 ++ internal/env/parse_test.go | 65 ++++ internal/env/types.go | 27 +- 12 files changed, 411 insertions(+), 362 deletions(-) create mode 100644 config/testdata/empty_proxy_protocol.toml create mode 100644 config/testdata/happy_path.toml create mode 100644 config/testdata/missing_env.toml create mode 100644 config/testdata/test_env.toml create mode 100644 internal/env/parse_test.go diff --git a/cmd/tegola/cmd/server.go b/cmd/tegola/cmd/server.go index 9d504872b..13cd5b8f8 100644 --- a/cmd/tegola/cmd/server.go +++ b/cmd/tegola/cmd/server.go @@ -39,13 +39,9 @@ var serverCmd = &cobra.Command{ serverPort = string(conf.Webserver.Port) } - if conf.Webserver.HostName != "" { - hostname, err := url.Parse(string(conf.Webserver.HostName)) - if err != nil { - log.Fatalf("unable to parse webserver.hostname: %s", err) - } - - server.HostName = hostname + if conf.Webserver.HostName.Host != "" { + u := url.URL(conf.Webserver.HostName) + server.HostName = &u } // set our server version diff --git a/cmd/tegola_lambda/main.go b/cmd/tegola_lambda/main.go index f6c9f3af7..aa4654a98 100644 --- a/cmd/tegola_lambda/main.go +++ b/cmd/tegola_lambda/main.go @@ -90,13 +90,9 @@ func init() { // set our server version server.Version = build.Version - if conf.Webserver.HostName != "" { - hostname, err := url.Parse(string(conf.Webserver.HostName)) - if err != nil { - log.Fatalf("unable to parse webserver.hostname: %s", err) - } - - server.HostName = hostname + if conf.Webserver.HostName.Host != "" { + u := url.URL(conf.Webserver.HostName) + server.HostName = &u } // set user defined response headers diff --git a/config/config.go b/config/config.go index 441e172d5..feaccbc63 100644 --- a/config/config.go +++ b/config/config.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "strings" "time" @@ -72,7 +71,7 @@ type Config struct { // Webserver represents the config options for the webserver part of Tegola type Webserver struct { - HostName env.String `toml:"hostname"` + HostName env.URL `toml:"hostname"` Port env.String `toml:"port"` URIPrefix env.String `toml:"uri_prefix"` Headers env.Dict `toml:"headers"` @@ -333,17 +332,6 @@ func (c *Config) Validate() error { } } - // if HostName is set, validate it - if string(c.Webserver.HostName) != "" { - _, err := url.Parse(string(c.Webserver.HostName)) - if err != nil { - return ErrInvalidHostName{ - HostName: string(c.Webserver.HostName), - Err: err, - } - } - } - return nil } diff --git a/config/config_test.go b/config/config_test.go index 1a6b80c71..5af0c4bde 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,11 +2,13 @@ package config_test import ( "errors" + "os" "reflect" "strconv" - "strings" "testing" + "github.com/go-test/deep" + "github.com/go-spatial/tegola/config" "github.com/go-spatial/tegola/internal/env" "github.com/go-spatial/tegola/provider" @@ -50,7 +52,7 @@ func setEnv(t *testing.T) { func TestParse(t *testing.T) { type tcase struct { - config string + configPath string expected config.Config expectedErr error } @@ -60,129 +62,62 @@ func TestParse(t *testing.T) { fn := func(tc tcase) func(*testing.T) { return func(t *testing.T) { - r := strings.NewReader(tc.config) - - conf, err := config.Parse(r, "") + f, err := os.Open(tc.configPath) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + conf, err := config.Parse(f, "") if tc.expectedErr != nil { if err == nil { - t.Errorf("expected err %v, got nil", tc.expectedErr.Error()) - return + t.Fatalf("expected err %s, got nil", tc.expectedErr) } // compare error messages if tc.expectedErr.Error() != err.Error() { - t.Errorf("invalid error. expected %v, got %v", tc.expectedErr, err) - return + t.Fatalf("invalid error. expected %v, got %v", tc.expectedErr, err) } return } - if err != nil { - t.Error(err) - return + t.Fatal(err) } // compare the various parts fo the config - if !reflect.DeepEqual(conf.LocationName, tc.expected.LocationName) { - t.Errorf("expected LocationName \n\n %+v \n\n got \n\n %+v ", tc.expected.LocationName, conf.LocationName) - return + if diff := deep.Equal(conf.LocationName, tc.expected.LocationName); diff != nil { + t.Fatalf("LocationName: got does not match expected: %v", diff) } - if !reflect.DeepEqual(conf.Webserver, tc.expected.Webserver) { - t.Errorf("expected Webserver output \n\n %+v \n\n got \n\n %+v ", tc.expected.Webserver, conf.Webserver) - return + if diff := deep.Equal(conf.Webserver, tc.expected.Webserver); diff != nil { + t.Fatalf("Webserver: got does not match expected: %v", diff) } - if !reflect.DeepEqual(conf.Providers, tc.expected.Providers) { - t.Errorf("expected Providers output \n\n (%+v) \n\n got \n\n (%+v) ", tc.expected.Providers, conf.Providers) - return + if diff := deep.Equal(conf.Providers, tc.expected.Providers); diff != nil { + t.Fatalf("Providers: got does not match expected: %v", diff) } - if !reflect.DeepEqual(conf.Maps, tc.expected.Maps) { - t.Errorf("expected Maps output \n\n (%+v) \n\n got \n\n (%+v) ", tc.expected.Maps, conf.Maps) - return + if diff := deep.Equal(conf.Maps, tc.expected.Maps); diff != nil { + t.Fatalf("Maps: got does not match expected: %v", diff) } - if !reflect.DeepEqual(conf, tc.expected) { - t.Errorf("expected \n\n (%+v) \n\n got \n\n (%+v) ", tc.expected, conf) - return + if diff := deep.Equal(conf, tc.expected); diff != nil { + t.Fatalf("got does not match expected: %v", diff) } } } tests := map[string]tcase{ "happy path": { - config: ` - tile_buffer = 12 - - [webserver] - hostname = "cdn.tegola.io" - port = ":8080" - cors_allowed_origin = "tegola.io" - proxy_protocol = "https" - - [webserver.headers] - Access-Control-Allow-Origin = "*" - Access-Control-Allow-Methods = "GET, OPTIONS" - - [cache] - type = "file" - basepath = "/tmp/tegola-cache" - - [[providers]] - name = "provider1" - type = "postgis" - host = "localhost" - port = 5432 - database = "osm_water" - user = "admin" - password = "" - - [[providers.layers]] - name = "water" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[maps]] - name = "osm" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = [-76.275329586789, 39.153492567373, 8.0] - - [[maps.layers]] - provider_layer = "provider1.water" - min_zoom = 10 - max_zoom = 20 - dont_simplify = true - dont_clip = true - dont_clean = true - - [[maps.params]] - name = "param1" - token = "!param1!" - type = "string" - - [[maps.params]] - name = "param2" - token = "!PARAM2!" - type = "int" - sql = "AND answer = ?" - default_value = "42" - - [[maps.params]] - name = "param3" - token = "!PARAM3!" - type = "float" - default_sql = "AND pi = 3.1415926" - `, + configPath: "testdata/happy_path.toml", expected: config.Config{ TileBuffer: env.IntPtr(env.Int(12)), LocationName: "", Webserver: config.Webserver{ - HostName: "cdn.tegola.io", + HostName: env.URL{ + Scheme: "https", + Host: "cdn.tegola.io", + }, Port: ":8080", ProxyProtocol: "https", Headers: env.Dict{ @@ -257,79 +192,15 @@ func TestParse(t *testing.T) { }, }, "test env": { - config: ` - [webserver] - hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" - port = "${ENV_TEST_WEBSERVER_PORT}" - - [webserver.headers] - Cache-Control = "${ENV_TEST_WEBSERVER_HEADER_STRING}" - Test = "Test" - # impossible but to test ParseDict - Impossible-Header = {"test" = "${ENV_TEST_WEBSERVER_HEADER_STRING}"} - - [[providers]] - name = "provider1" - type = "postgis" - host = "localhost" - port = 5432 - database = "osm_water" - user = "admin" - password = "" - - [[providers.layers]] - name = "water_0_5" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[providers.layers]] - name = "water_6_10" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[maps]] - name = "osm" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = ["${ENV_TEST_CENTER_X}", "${ENV_TEST_CENTER_Y}", "${ENV_TEST_CENTER_Z}"] - - [[maps.layers]] - name = "water" - provider_layer = "${ENV_TEST_PROVIDER_LAYER}" - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_6_10" - min_zoom = 6 - max_zoom = 10 - - [[maps]] - name = "osm_2" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = [-76.275329586789, 39.153492567373, 8.0] - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_0_5" - min_zoom = 0 - max_zoom = 5 - - [maps.layers.default_tags] - provider = "${ENV_TEST_MAP_LAYER_DEFAULT_TAG}" - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_6_10" - min_zoom = 6 - max_zoom = 10`, + configPath: "testdata/test_env.toml", expected: config.Config{ LocationName: "", Webserver: config.Webserver{ - HostName: ENV_TEST_HOST_CONCAT, - Port: ENV_TEST_WEBSERVER_PORT, + HostName: env.URL{ + Scheme: "https", + Host: ENV_TEST_HOST_CONCAT, + }, + Port: ENV_TEST_WEBSERVER_PORT, Headers: env.Dict{ "Cache-Control": ENV_TEST_WEBSERVER_HEADER_STRING, "Test": "Test", @@ -413,147 +284,20 @@ func TestParse(t *testing.T) { }, }, "missing env": { - config: ` - [webserver] - hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" - port = "${ENV_TEST_WEBSERVER_PORT}" - - [webserver.headers] - Cache-Control = "${ENV_TEST_WEBSERVER_HEADER_STRING}" - Test = "${I_AM_MISSING}" - - [[providers]] - name = "provider1" - type = "postgis" - host = "localhost" - port = 5432 - database = "osm_water" - user = "admin" - password = "" - - [[providers.layers]] - name = "water_0_5" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[providers.layers]] - name = "water_6_10" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[maps]] - name = "osm" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = ["${ENV_TEST_CENTER_X}", "${ENV_TEST_CENTER_Y}", "${ENV_TEST_CENTER_Z}"] - - [[maps.layers]] - name = "water" - provider_layer = "${ENV_TEST_PROVIDER_LAYER}" - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_6_10" - min_zoom = 6 - max_zoom = 10 - - [[maps]] - name = "osm_2" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = [-76.275329586789, 39.153492567373, 8.0] - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_0_5" - min_zoom = 0 - max_zoom = 5 - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_6_10" - min_zoom = 6 - max_zoom = 10`, + configPath: "testdata/missing_env.toml", expected: config.Config{}, expectedErr: env.ErrEnvVar("I_AM_MISSING"), }, - "test empty proxy_protocol": { - config: ` - [webserver] - hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" - port = "${ENV_TEST_WEBSERVER_PORT}" - proxy_protocol = "" - - [webserver.headers] - Cache-Control = "${ENV_TEST_WEBSERVER_HEADER_STRING}" - Test = "Test" - # impossible but to test ParseDict - Impossible-Header = {"test" = "${ENV_TEST_WEBSERVER_HEADER_STRING}"} - - [[providers]] - name = "provider1" - type = "postgis" - host = "localhost" - port = 5432 - database = "osm_water" - user = "admin" - password = "" - - [[providers.layers]] - name = "water_0_5" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[providers.layers]] - name = "water_6_10" - geometry_fieldname = "geom" - id_fieldname = "gid" - sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" - - [[maps]] - name = "osm" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = ["${ENV_TEST_CENTER_X}", "${ENV_TEST_CENTER_Y}", "${ENV_TEST_CENTER_Z}"] - - [[maps.layers]] - name = "water" - provider_layer = "${ENV_TEST_PROVIDER_LAYER}" - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_6_10" - min_zoom = 6 - max_zoom = 10 - - [[maps]] - name = "osm_2" - attribution = "Test Attribution" - bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] - center = [-76.275329586789, 39.153492567373, 8.0] - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_0_5" - min_zoom = 0 - max_zoom = 5 - - [maps.layers.default_tags] - provider = "${ENV_TEST_MAP_LAYER_DEFAULT_TAG}" - - [[maps.layers]] - name = "water" - provider_layer = "provider1.water_6_10" - min_zoom = 6 - max_zoom = 10`, + "empty proxy_protocol": { + configPath: "testdata/empty_proxy_protocol.toml", expected: config.Config{ LocationName: "", Webserver: config.Webserver{ - HostName: ENV_TEST_HOST_CONCAT, - Port: ENV_TEST_WEBSERVER_PORT, + HostName: env.URL{ + Scheme: "https", + Host: ENV_TEST_HOST_CONCAT, + }, + Port: ENV_TEST_WEBSERVER_PORT, Headers: env.Dict{ "Cache-Control": ENV_TEST_WEBSERVER_HEADER_STRING, "Test": "Test", @@ -1574,14 +1318,6 @@ func TestValidate(t *testing.T) { }, }, }, - "invalid webserver hostname": { - config: config.Config{ - Webserver: config.Webserver{ - HostName: ":\\malformed.host", - }, - }, - expectedErr: config.ErrInvalidHostName{}, - }, } for name, tc := range tests { diff --git a/config/errors.go b/config/errors.go index 133fae0a9..8475127ed 100644 --- a/config/errors.go +++ b/config/errors.go @@ -1,7 +1,6 @@ package config import ( - "errors" "fmt" "strings" @@ -182,28 +181,6 @@ func (e ErrInvalidURIPrefix) Error() string { return fmt.Sprintf("config: invalid uri_prefix (%s). uri_prefix must start with a forward slash '/' ", string(e)) } -type ErrInvalidHostName struct { - HostName string - Err error -} - -func (e ErrInvalidHostName) Error() string { - return fmt.Sprintf("config: invalid hostname (%s) - %s", string(e.HostName), e.Err) -} - -func (e ErrInvalidHostName) Unwrap() error { - return e.Err -} - -func (e ErrInvalidHostName) Is(target error) bool { - - if _, ok := target.(ErrInvalidHostName); ok { - return true - } - - return errors.Is(e.Err, target) -} - // ErrUnknownProviderType is returned when the config contains a provider type that has not been registered type ErrUnknownProviderType struct { Name string // Name is the name of the entry in the config diff --git a/config/testdata/empty_proxy_protocol.toml b/config/testdata/empty_proxy_protocol.toml new file mode 100644 index 000000000..9bea2f288 --- /dev/null +++ b/config/testdata/empty_proxy_protocol.toml @@ -0,0 +1,68 @@ +[webserver] +hostname = "https://${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" +port = "${ENV_TEST_WEBSERVER_PORT}" +proxy_protocol = "" + +[webserver.headers] + Cache-Control = "${ENV_TEST_WEBSERVER_HEADER_STRING}" + Test = "Test" + # impossible but to test ParseDict + Impossible-Header = {"test" = "${ENV_TEST_WEBSERVER_HEADER_STRING}"} + +[[providers]] +name = "provider1" +type = "postgis" +host = "localhost" +port = 5432 +database = "osm_water" +user = "admin" +password = "" + + [[providers.layers]] + name = "water_0_5" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + + [[providers.layers]] + name = "water_6_10" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + +[[maps]] +name = "osm" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = ["${ENV_TEST_CENTER_X}", "${ENV_TEST_CENTER_Y}", "${ENV_TEST_CENTER_Z}"] + + [[maps.layers]] + name = "water" + provider_layer = "${ENV_TEST_PROVIDER_LAYER}" + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_6_10" + min_zoom = 6 + max_zoom = 10 + +[[maps]] +name = "osm_2" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = [-76.275329586789, 39.153492567373, 8.0] + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_0_5" + min_zoom = 0 + max_zoom = 5 + + [maps.layers.default_tags] + provider = "${ENV_TEST_MAP_LAYER_DEFAULT_TAG}" + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_6_10" + min_zoom = 6 + max_zoom = 10 \ No newline at end of file diff --git a/config/testdata/happy_path.toml b/config/testdata/happy_path.toml new file mode 100644 index 000000000..e50b87971 --- /dev/null +++ b/config/testdata/happy_path.toml @@ -0,0 +1,62 @@ +tile_buffer = 12 + +[webserver] +hostname = "https://cdn.tegola.io" +port = ":8080" +cors_allowed_origin = "tegola.io" +proxy_protocol = "https" + + [webserver.headers] + Access-Control-Allow-Origin = "*" + Access-Control-Allow-Methods = "GET, OPTIONS" + +[cache] +type = "file" +basepath = "/tmp/tegola-cache" + +[[providers]] +name = "provider1" +type = "postgis" +host = "localhost" +port = 5432 +database = "osm_water" +user = "admin" +password = "" + + [[providers.layers]] + name = "water" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + +[[maps]] +name = "osm" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = [-76.275329586789, 39.153492567373, 8.0] + + [[maps.layers]] + provider_layer = "provider1.water" + min_zoom = 10 + max_zoom = 20 + dont_simplify = true + dont_clip = true + dont_clean = true + + [[maps.params]] + name = "param1" + token = "!param1!" + type = "string" + + [[maps.params]] + name = "param2" + token = "!PARAM2!" + type = "int" + sql = "AND answer = ?" + default_value = "42" + + [[maps.params]] + name = "param3" + token = "!PARAM3!" + type = "float" + default_sql = "AND pi = 3.1415926" \ No newline at end of file diff --git a/config/testdata/missing_env.toml b/config/testdata/missing_env.toml new file mode 100644 index 000000000..a0c2fbdf8 --- /dev/null +++ b/config/testdata/missing_env.toml @@ -0,0 +1,62 @@ +[webserver] +hostname = "${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" +port = "${ENV_TEST_WEBSERVER_PORT}" + +[webserver.headers] + Cache-Control = "${ENV_TEST_WEBSERVER_HEADER_STRING}" + Test = "${I_AM_MISSING}" + +[[providers]] +name = "provider1" +type = "postgis" +host = "localhost" +port = 5432 +database = "osm_water" +user = "admin" +password = "" + + [[providers.layers]] + name = "water_0_5" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + + [[providers.layers]] + name = "water_6_10" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + +[[maps]] +name = "osm" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = ["${ENV_TEST_CENTER_X}", "${ENV_TEST_CENTER_Y}", "${ENV_TEST_CENTER_Z}"] + + [[maps.layers]] + name = "water" + provider_layer = "${ENV_TEST_PROVIDER_LAYER}" + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_6_10" + min_zoom = 6 + max_zoom = 10 + +[[maps]] +name = "osm_2" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = [-76.275329586789, 39.153492567373, 8.0] + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_0_5" + min_zoom = 0 + max_zoom = 5 + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_6_10" + min_zoom = 6 + max_zoom = 10 \ No newline at end of file diff --git a/config/testdata/test_env.toml b/config/testdata/test_env.toml new file mode 100644 index 000000000..10e818bbf --- /dev/null +++ b/config/testdata/test_env.toml @@ -0,0 +1,67 @@ +[webserver] +hostname = "https://${ENV_TEST_HOST_1}.${ENV_TEST_HOST_2}.${ENV_TEST_HOST_3}" +port = "${ENV_TEST_WEBSERVER_PORT}" + +[webserver.headers] + Cache-Control = "${ENV_TEST_WEBSERVER_HEADER_STRING}" + Test = "Test" + # impossible but to test ParseDict + Impossible-Header = {"test" = "${ENV_TEST_WEBSERVER_HEADER_STRING}"} + +[[providers]] +name = "provider1" +type = "postgis" +host = "localhost" +port = 5432 +database = "osm_water" +user = "admin" +password = "" + + [[providers.layers]] + name = "water_0_5" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + + [[providers.layers]] + name = "water_6_10" + geometry_fieldname = "geom" + id_fieldname = "gid" + sql = "SELECT gid, ST_AsBinary(geom) AS geom FROM simplified_water_polygons WHERE geom && !BBOX!" + +[[maps]] +name = "osm" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = ["${ENV_TEST_CENTER_X}", "${ENV_TEST_CENTER_Y}", "${ENV_TEST_CENTER_Z}"] + + [[maps.layers]] + name = "water" + provider_layer = "${ENV_TEST_PROVIDER_LAYER}" + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_6_10" + min_zoom = 6 + max_zoom = 10 + +[[maps]] +name = "osm_2" +attribution = "Test Attribution" +bounds = [-180.0, -85.05112877980659, 180.0, 85.0511287798066] +center = [-76.275329586789, 39.153492567373, 8.0] + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_0_5" + min_zoom = 0 + max_zoom = 5 + + [maps.layers.default_tags] + provider = "${ENV_TEST_MAP_LAYER_DEFAULT_TAG}" + + [[maps.layers]] + name = "water" + provider_layer = "provider1.water_6_10" + min_zoom = 6 + max_zoom = 10 \ No newline at end of file diff --git a/internal/env/parse.go b/internal/env/parse.go index 889c85502..7a077fad1 100644 --- a/internal/env/parse.go +++ b/internal/env/parse.go @@ -1,6 +1,7 @@ package env import ( + "net/url" "strconv" "strings" @@ -272,3 +273,21 @@ func ParseDict(v interface{}) (*Dict, error) { return &d, nil } + +func ParseURL(v any) (*url.URL, error) { + if v == nil { + return nil, nil + } + + switch val := v.(type) { + case string: + val, err := replaceEnvVar(val) + if err != nil { + return nil, err + } + + return url.Parse(val) + default: + return nil, ErrType{v} + } +} diff --git a/internal/env/parse_test.go b/internal/env/parse_test.go new file mode 100644 index 000000000..e449d589e --- /dev/null +++ b/internal/env/parse_test.go @@ -0,0 +1,65 @@ +package env_test + +import ( + "errors" + "net/url" + "testing" + + "github.com/go-test/deep" + + "github.com/go-spatial/tegola/internal/env" +) + +func TestParseURL(t *testing.T) { + type tcase struct { + in string + expected *url.URL + expectedErr error + } + + fn := func(tc tcase) func(*testing.T) { + return func(t *testing.T) { + got, err := env.ParseURL(tc.in) + if tc.expectedErr != nil { + if err == nil { + t.Errorf("expected err %v, got nil", tc.expectedErr.Error()) + return + } + + // compare error messages + if errors.Is(tc.expectedErr, err) { + t.Errorf("invalid error. expected %v, got %v", tc.expectedErr, err) + return + } + + return + } + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if diff := deep.Equal(got, tc.expected); diff != nil { + t.Fatalf("expected does not match go: %v", diff) + } + } + } + + tests := map[string]tcase{ + "happy path": { + in: "https://go-spatial.org/tegola", + expected: &url.URL{ + Scheme: "https", + Host: "go-spatial.org", + Path: "/tegola", + }, + }, + "invalid url escape": { + in: "https://go-spatial.org/tegola/_20_%+off_60000_", + expectedErr: url.EscapeError(""), + }, + } + + for name, tc := range tests { + t.Run(name, fn(tc)) + } +} diff --git a/internal/env/types.go b/internal/env/types.go index 1f8f63ac1..d0596a710 100644 --- a/internal/env/types.go +++ b/internal/env/types.go @@ -2,6 +2,7 @@ package env import ( "fmt" + "net/url" "os" "regexp" "strings" @@ -20,7 +21,7 @@ func (e ErrEnvVar) Error() string { // ErrType corresponds with an incorrect type passed to UnmarshalTOML type ErrType struct { - v interface{} + v any } func (te ErrType) Error() string { @@ -53,7 +54,7 @@ func replaceEnvVar(in string) (string, error) { //TODO(@ear7h): implement UnmarshalJSON for types -func (t *Dict) UnmarshalTOML(v interface{}) error { +func (t *Dict) UnmarshalTOML(v any) error { var d *Dict var err error @@ -73,7 +74,7 @@ func BoolPtr(v Bool) *Bool { return &v } -func (t *Bool) UnmarshalTOML(v interface{}) error { +func (t *Bool) UnmarshalTOML(v any) error { var b *bool var err error @@ -92,7 +93,7 @@ func StringPtr(v String) *String { return &v } -func (t *String) UnmarshalTOML(v interface{}) error { +func (t *String) UnmarshalTOML(v any) error { var s *string var err error @@ -111,7 +112,7 @@ func IntPtr(v Int) *Int { return &v } -func (t *Int) UnmarshalTOML(v interface{}) error { +func (t *Int) UnmarshalTOML(v any) error { var i *int var err error @@ -130,7 +131,7 @@ func UintPtr(v Uint) *Uint { return &v } -func (t *Uint) UnmarshalTOML(v interface{}) error { +func (t *Uint) UnmarshalTOML(v any) error { var ui *uint var err error @@ -149,7 +150,7 @@ func FloatPtr(v Float) *Float { return &v } -func (t *Float) UnmarshalTOML(v interface{}) error { +func (t *Float) UnmarshalTOML(v any) error { var f *float64 var err error @@ -161,3 +162,15 @@ func (t *Float) UnmarshalTOML(v interface{}) error { *t = Float(*f) return nil } + +type URL url.URL + +func (t *URL) UnmarshalTOML(v any) error { + u, err := ParseURL(v) + if err != nil { + return err + } + + *t = URL(*u) + return nil +}