Repository: 2gis/k8s-handle

Issue:
It is not possible to concatenate several environment variables into one value
Привет.
Столкнулся с невозможностью одновременного использования нескольких переменных окружения при объявлении переменной в config.yaml. Небольшой пример:
Мы при развертывании сервиса иногда создаем более одного деплоя, registry используем как приватные так и публичные, да еще и registry приватных у нас несколько. Так же при каждом деплое у нас выбирается образ по тэгу в git. Для того, что бы не создавать множество шаблонов деплоя, мы сделали один универсальный, в цикле которого добавляем все необходимые образы. По этому все образы объявляются в config.yaml
```yaml
common:
  deployments:
    service_name:
      containers:
        app:
          image: "{{ env='CI_REGISTRY' }}/service-image:{{ env='TAG' }}"
        nginx:
          image: "{{ env='CI_REGISTRY' }}/custom-nginx:configmap"
```
Так вот при такой записи в случае с образом nginx все ОК, в случае с контейнером app после создания манифеста имеем "{{ env='CI_REGISTRY' }}/service-image:1.0.0". Заглянул в код и понял, что в текущей реализации не получится из нескольких переменных окружения получить нужное нам имя образа.

Discussion:
codecov-io: # [Codecov](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=h1) Report
> Merging [#73](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=desc) into [master](https://codecov.io/gh/2gis/k8s-handle/commit/e727854bd0493af8f836abf496582f53c2d9c711?src=pr&el=desc) will **decrease** coverage by `0.06%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/2gis/k8s-handle/pull/73/graphs/tree.svg?width=650&token=tv8Ij8dA6k&height=150&src=pr)](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #73      +/-   ##
==========================================
- Coverage    73.9%   73.84%   -0.07%     
==========================================
  Files           8        8              
  Lines         801      799       -2     
==========================================
- Hits          592      590       -2     
  Misses        209      209
```


| [Impacted Files](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [config.py](https://codecov.io/gh/2gis/k8s-handle/pull/73/diff?src=pr&el=tree#diff-Y29uZmlnLnB5) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=footer). Last update [e727854...cdb67cc](https://codecov.io/gh/2gis/k8s-handle/pull/73?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

otherNoscript: ууупс, не в ту ветку
dekhtyarev: @otherNoscript , привет! 

Обнови PR с мастера, пжлста. У нас произошли некоторые изменения в рамках #70 

Patch:
diff --git a/.dockerignore b/.dockerignore
index d3f33a2..fea1e67 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -6,5 +6,10 @@ Dockerfile*
 .gitignore
 .idea/
 .tox/
+.travis.yml
+tox.ini
 __pychache__
 htmlcov/
+tests/
+*.png
+
diff --git a/k8s_handle/config.py b/k8s_handle/config.py
index acbf34f..6c2564f 100644
--- a/k8s_handle/config.py
+++ b/k8s_handle/config.py
@@ -13,7 +13,7 @@ from k8s_handle.templating import b64decode
 log = logging.getLogger(__name__)
 
 INCLUDE_RE = re.compile(r'{{\s?file\s?=\s?\'(?P<file>[^\']*)\'\s?}}')
-CUSTOM_ENV_RE = re.compile(r'^(?P<prefix>.*){{\s*env\s*=\s*\'(?P<env>[^\']*)\'\s*}}(?P<postfix>.*)$')  # noqa
+CUSTOM_ENV_RE = r'{{\s*env\s*=\s*\'([^\']*)\'\s*}}'
 
 KEY_USE_KUBECONFIG = 'use_kubeconfig'
 KEY_K8S_MASTER_URI = 'k8s_master_uri'
@@ -113,19 +113,15 @@ def _process_variable(variable):
     if matches:
         return load_yaml(matches.groupdict().get('file'))
 
-    matches = CUSTOM_ENV_RE.match(variable)
+    try:
+        return re.sub(CUSTOM_ENV_RE, lambda m: os.environ[m.group(1)], variable)
 
-    if matches:
-        prefix = matches.groupdict().get('prefix')
-        env_var_name = matches.groupdict().get('env')
-        postfix = matches.groupdict().get('postfix')
-
-        if os.environ.get(env_var_name) is None and settings.GET_ENVIRON_STRICT:
-            raise RuntimeError('Environment variable "{}" is not set'.format(env_var_name))
-
-        return prefix + os.environ.get(env_var_name, '') + postfix
+    except KeyError as err:
+        log.debug('Environment variable "{}" is not set'.format(err.args[0]))
+        if settings.GET_ENVIRON_STRICT:
+            raise RuntimeError('Environment variable "{}" is not set'.format(err.args[0]))
 
-    return variable
+    return re.sub(CUSTOM_ENV_RE, lambda m: os.environ.get(m.group(1), ''), variable)
 
 
 def _update_single_variable(value, include_history):
diff --git a/k8s_handle/filesystem.py b/k8s_handle/filesystem.py
index 5d200eb..7993290 100644
--- a/k8s_handle/filesystem.py
+++ b/k8s_handle/filesystem.py
@@ -17,7 +17,7 @@ class InvalidYamlError(Exception):
 def load_yaml(path):
     try:
         with open(path) as f:
-            return yaml.load(f.read())
+            return yaml.safe_load(f.read())
     except Exception as e:
         raise InvalidYamlError("file '{}' doesn't contain valid yaml: {}".format(
             path, e))
diff --git a/k8s_handle/templating.py b/k8s_handle/templating.py
index 2a5c441..7476f07 100644
--- a/k8s_handle/templating.py
+++ b/k8s_handle/templating.py
@@ -19,7 +19,7 @@ log = logging.getLogger(__name__)
 def get_template_contexts(file_path):
     with open(file_path) as f:
         try:
-            contexts = yaml.load_all(f.read())
+            contexts = yaml.safe_load_all(f.read())
         except Exception as e:
             raise RuntimeError('Unable to load yaml file: {}, {}'.format(file_path, e))
 


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 2f2e2b3..ce3e228 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -118,13 +118,17 @@ class TestContextGeneration(unittest.TestCase):
                     'section1-key4': [0, 1, 2, 3],
                     'section1-key5': "{{ env='CUSTOM_ENV' }}",
                     'section1-key6': "{{ file='tests/fixtures/include.yaml' }}",
+                    'section1-key7': "{{ env='CUSTOM_ENV'}} = {{ env='CUSTOM_ENV' }}",
+                    'section1-key8': "{{ env='NULL_VAR' }}-{{ env='CUSTOM_ENV' }}"
                 }
             },
             'section2': [
                 {},
                 'var2',
                 'var3',
-                '{{ env=\'CUSTOM_ENV\' }}'
+                '{{ env=\'CUSTOM_ENV\' }}',
+                '{{ env=\'CUSTOM_ENV\' }} = {{ env=\'CUSTOM_ENV\' }}',
+                '{{ env=\'NULL_VAR\' }}-{{ env=\'CUSTOM_ENV\' }}'
             ],
             'section3': [0, 1, 2, 3, 4]
         }
@@ -137,13 +141,17 @@ class TestContextGeneration(unittest.TestCase):
                     'section1-key4': [0, 1, 2, 3],
                     'section1-key5': 'My value',
                     'section1-key6': {'ha_ha': 'included_var'},
+                    'section1-key7': 'My value = My value',
+                    'section1-key8': "-My value"
                 }
             },
             'section2': [
                 {},
                 'var2',
                 'var3',
-                'My value'
+                'My value',
+                'My value = My value',
+                '-My value'
             ],
             'section3': [0, 1, 2, 3, 4]
         }