Repository: geopython/pygeoapi

Issue:
implement env variables inside PYGEOAPI_CONFIG file
**Is your feature request related to a problem? Please describe.**

devop systems (docker, k8) use enviroment variable to set parameters like database connections of  set passwords inside containers. 

For example running pygeopai in openshift and we want to connect to a database that has a secret password. Currentely we would have to set the password on the yaml configuration file and this could  be a security problem, while openshift can set the secret env inside the pod from a secure source  

**Describe the solution you'd like**
Parse the PYGEOAPI_CONFIG yaml file with python module envyaml
(https://github.com/thesimj/envyaml). The current default module yaml should be replaced by envyaml as parser allowing for env variables to be injected into the yaml file

**Describe alternatives you've considered**
The dataprovider would get the env variables by it self, IMHO this is less elegant and we may need the described solutions for other configurations

**Additional context**
Add any other context or screenshots about the feature request here.
![image](https://user-images.githubusercontent.com/1275400/65626708-2cd62700-dfce-11e9-9926-ae7e1a193b31.png)



Discussion:

Patch:
diff --git a/pygeoapi/api.py b/pygeoapi/api.py
index 250e21e..1915472 100644
--- a/pygeoapi/api.py
+++ b/pygeoapi/api.py
@@ -997,7 +997,7 @@ def to_json(dict_):
     :returns: JSON string representation
     """
 
-    return json.dumps(dict_, default=json_serial)
+    return json.dumps(dict_)
 
 
 def _render_j2_template(config, template, data):
diff --git a/pygeoapi/util.py b/pygeoapi/util.py
index fe37953..7d8b331 100644
--- a/pygeoapi/util.py
+++ b/pygeoapi/util.py
@@ -2,7 +2,7 @@
 #
 # Authors: Tom Kralidis <tomkralidis@gmail.com>
 #
-# Copyright (c) 2018 Tom Kralidis
+# Copyright (c) 2019 Tom Kralidis
 #
 # Permission is hereby granted, free of charge, to any person
 # obtaining a copy of this software and associated documentation
@@ -32,27 +32,34 @@
 from datetime import date, datetime, time
 from decimal import Decimal
 import logging
+import os
+import re
 
 import yaml
 
 LOGGER = logging.getLogger(__name__)
 
 
-def get_url(scheme, host, port, basepath):
+def get_typed_value(value):
     """
-    Provides URL of instance
+    Derive true type from data value
 
-    :returns: string of complete baseurl
-    """
-
-    url = '{}://{}'.format(scheme, host)
+    :param value: value
 
-    if port not in [80, 443]:
-        url = '{}:{}'.format(url, port)
+    :returns: value as a native Python data type
+    """
 
-    url = '{}{}'.format(url, basepath)
+    try:
+        if '.' in value:  # float?
+            value2 = float(value)
+        elif len(value) > 1 and value.startswith('0'):
+            value2 = value
+        else:  # int?
+            value2 = int(value)
+    except ValueError:  # string (default)?
+        value2 = value
 
-    return url
+    return value2
 
 
 def yaml_load(fh):
@@ -64,11 +71,23 @@ def yaml_load(fh):
     :returns: `dict` representation of YAML
     """
 
-    try:
-        return yaml.load(fh, Loader=yaml.FullLoader)
-    except AttributeError as err:
-        LOGGER.warning('YAML loading error: {}'.format(err))
-        return yaml.load(fh)
+    # support environment variables in config
+    # https://stackoverflow.com/a/55301129
+    path_matcher = re.compile(r'.*\$\{([^}^{]+)\}.*')
+
+    def path_constructor(loader, node):
+        env_var = path_matcher.match(node.value).group(1)
+        if env_var not in os.environ:
+            raise EnvironmentError('Undefined environment variable in config')
+        return get_typed_value(os.path.expandvars(node.value))
+
+    class EnvVarLoader(yaml.SafeLoader):
+        pass
+
+    EnvVarLoader.add_implicit_resolver('!path', path_matcher, None)
+    EnvVarLoader.add_constructor('!path', path_constructor)
+
+    return yaml.load(fh, Loader=EnvVarLoader)
 
 
 def str2bool(value):
@@ -100,8 +119,7 @@ def json_serial(obj):
     """
 
     if isinstance(obj, (datetime, date, time)):
-        serial = obj.isoformat()
-        return serial
+        return obj.isoformat()
     elif isinstance(obj, Decimal):
         return float(obj)
 


Test patch:
diff --git a/tests/pygeoapi-test-config-envvars.yml b/tests/pygeoapi-test-config-envvars.yml
new file mode 100644
index 0000000..717f94f
--- /dev/null
+++ b/tests/pygeoapi-test-config-envvars.yml
@@ -0,0 +1,120 @@
+# =================================================================
+#
+# Authors: Tom Kralidis <tomkralidis@gmail.com>
+#
+# Copyright (c) 2019 Tom Kralidis
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+server:
+    bind:
+        host: 0.0.0.0
+        port: ${PYGEOAPI_PORT}
+    url: http://localhost:5000/
+    mimetype: application/json; charset=UTF-8
+    encoding: utf-8
+    language: en-US
+    cors: true
+    pretty_print: true
+    limit: 10
+    # templates: /path/to/templates
+    map:
+        url: https://maps.wikimedia.org/osm-intl/{z}/{x}/{y}.png
+        attribution: '<a href="https://wikimediafoundation.org/wiki/Maps_Terms_of_Use">Wikimedia maps</a> | Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
+
+logging:
+    level: ERROR
+    #logfile: /tmp/pygeoapi.log
+
+metadata:
+    identification:
+        title: pygeoapi default instance ${PYGEOAPI_TITLE}
+        description: pygeoapi provides an API to geospatial data
+        keywords:
+            - geospatial
+            - data
+            - api
+        keywords_type: theme
+        terms_of_service: None
+        url: http://example.org
+    license:
+        name: CC-BY 4.0 license
+        url: https://creativecommons.org/licenses/by/4.0/
+    provider:
+        name: Organization Name
+        url: https://pygeoapi.io
+    contact:
+        name: Lastname, Firstname
+        position: Position Title
+        address: Mailing Address
+        city: City
+        stateorprovince: Administrative Area
+        postalcode: Zip or Postal Code
+        country: Country
+        phone: +xx-xxx-xxx-xxxx
+        fax: +xx-xxx-xxx-xxxx
+        email: you@example.org
+        url: Contact URL
+        hours: Hours of Service
+        instructions: During hours of service.  Off on weekends.
+        role: pointOfContact
+
+datasets:
+    obs:
+        title: Observations
+        description: My cool observations
+        keywords:
+            - observations
+            - monitoring
+        links:
+            - type: text/csv
+              rel: canonical
+              title: data
+              href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv
+              hreflang: en-US
+            - type: text/csv
+              rel: alternate
+              title: data
+              href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv
+              hreflang: en-US
+        extents:
+            spatial:
+                bbox: [-180,-90,180,90]
+                crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
+            temporal:
+                begin: 2000-10-30T18:24:39Z
+                end: 2007-10-30T08:57:29Z
+                trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian
+        provider:
+            name: CSV
+            data: tests/data/obs.csv
+            id_field: id
+            geometry:
+                x_field: long
+                y_field: lat
+
+processes:
+    hello-world:
+        processor:
+            name: HelloWorld
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..7d2b455
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,62 @@
+# =================================================================
+#
+# Authors: Tom Kralidis <tomkralidis@gmail.com>
+#
+# Copyright (c) 2019 Tom Kralidis
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+import os
+
+import pytest
+
+from pygeoapi.util import yaml_load
+
+
+def get_test_file_path(filename):
+    """helper function to open test file safely"""
+
+    if os.path.isfile(filename):
+        return filename
+    else:
+        return 'tests/{}'.format(filename)
+
+
+def test_config_envvars():
+    os.environ['PYGEOAPI_PORT'] = '5001'
+    os.environ['PYGEOAPI_TITLE'] = 'my title'
+
+    with open(get_test_file_path('pygeoapi-test-config-envvars.yml')) as fh:
+        config = yaml_load(fh)
+
+    assert isinstance(config, dict)
+    assert config['server']['bind']['port'] == 5001
+    assert config['metadata']['identification']['title'] == \
+        'pygeoapi default instance my title'
+
+    os.environ.pop('PYGEOAPI_PORT')
+
+    with pytest.raises(EnvironmentError):
+        with open(get_test_file_path('pygeoapi-test-config-envvars.yml')) as fh:  # noqa
+            config = yaml_load(fh)
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..f4b8aca
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,98 @@
+# =================================================================
+#
+# Authors: Tom Kralidis <tomkralidis@gmail.com>
+#
+# Copyright (c) 2019 Tom Kralidis
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation
+# files (the "Software"), to deal in the Software without
+# restriction, including without limitation the rights to use,
+# copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following
+# conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+# OTHER DEALINGS IN THE SOFTWARE.
+#
+# =================================================================
+
+from datetime import datetime, date, time
+from decimal import Decimal
+import os
+
+import pytest
+
+from pygeoapi import util
+
+
+def get_test_file_path(filename):
+    """helper function to open test file safely"""
+
+    if os.path.isfile(filename):
+        return filename
+    else:
+        return 'tests/{}'.format(filename)
+
+
+def test_get_typed_value():
+    value = util.get_typed_value('2')
+    assert isinstance(value, int)
+
+    value = util.get_typed_value('1.2')
+    assert isinstance(value, float)
+
+    value = util.get_typed_value('1.c2')
+    assert isinstance(value, str)
+
+
+def test_yaml_load():
+    with open(get_test_file_path('pygeoapi-test-config.yml')) as fh:
+        d = util.yaml_load(fh)
+        assert isinstance(d, dict)
+    with pytest.raises(FileNotFoundError):
+        with open(get_test_file_path('404.yml')) as fh:
+            d = util.yaml_load(fh)
+
+
+def test_str2bool():
+    assert util.str2bool(False) is False
+    assert util.str2bool('0') is False
+    assert util.str2bool('no') is False
+    assert util.str2bool('yes') is True
+    assert util.str2bool('1') is True
+    assert util.str2bool(True) is True
+    assert util.str2bool('true') is True
+    assert util.str2bool('True') is True
+    assert util.str2bool('TRUE') is True
+    assert util.str2bool('tRuE') is True
+    assert util.str2bool('on') is True
+    assert util.str2bool('On') is True
+    assert util.str2bool('off') is False
+
+
+def test_json_serial():
+    d = datetime(1972, 10, 30)
+    assert util.json_serial(d) == '1972-10-30T00:00:00'
+
+    d = date(2010, 7, 31)
+    assert util.json_serial(d) == '2010-07-31'
+
+    d = time(11)
+    assert util.json_serial(d) == '11:00:00'
+
+    d = Decimal(1.0)
+    assert util.json_serial(d) == 1.0
+
+    with pytest.raises(TypeError):
+        util.json_serial('foo')