|
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 | from collections import ChainMap |
| 3 | +import os.path |
3 | 4 | from string import Template |
4 | | -from typing import Any, Dict, List, Mapping, Union |
| 5 | +from typing import Any, Dict, List, Mapping, Tuple, Union |
5 | 6 |
|
6 | 7 | HAS_CHARDET = True |
7 | 8 | try: |
|
12 | 13 | except ImportError: |
13 | 14 | HAS_CHARDET = False |
14 | 15 | from logzero import logger |
| 16 | +try: |
| 17 | + import simplejson as json |
| 18 | + from simplejson.errors import JSONDecodeError |
| 19 | +except ImportError: |
| 20 | + import json |
| 21 | + from json.decoder import JSONDecodeError |
| 22 | +import yaml |
15 | 23 |
|
16 | 24 | from chaoslib.exceptions import ActivityFailed |
17 | | -from chaoslib.types import Configuration, Secrets |
| 25 | +from chaoslib.types import Configuration, ConfigVars, Secrets, SecretVars |
18 | 26 |
|
19 | | -__all__ = ["__version__", "decode_bytes", "substitute"] |
| 27 | +__all__ = ["__version__", "decode_bytes", "substitute", "merge_vars", |
| 28 | + "convert_vars"] |
20 | 29 | __version__ = '1.11.1' |
21 | 30 |
|
22 | 31 |
|
@@ -119,3 +128,126 @@ def decode_bytes(data: bytes, default_encoding: str = 'utf-8') -> str: |
119 | 128 | except UnicodeDecodeError: |
120 | 129 | raise ActivityFailed( |
121 | 130 | "Failed to decode bytes using encoding '{}'".format(encoding)) |
| 131 | + |
| 132 | + |
| 133 | +def merge_vars(var: Dict[str, Union[str, float, int, bytes]] = None, # noqa: C901 |
| 134 | + var_files: List[str] = None) -> Tuple[ConfigVars, SecretVars]: |
| 135 | + """ |
| 136 | + Load configuration and secret values from the given set of variables. |
| 137 | + These values are applicable for substitution when the experiment runs. |
| 138 | + If `var` is set, it must be a dictionary which will be used as |
| 139 | + configuration values only. |
| 140 | + If `var_files` is set, it can be a list of any of these two items: |
| 141 | + * a Json or Yaml payload which must be also mappings with two top-level |
| 142 | + keys: `"configuration"` and `"secrets"`. If any is present, it must |
| 143 | + respect the format of the confiuration and secrets of the experiment |
| 144 | + format. |
| 145 | + * a .env file which is used to load environment variable on the fly. |
| 146 | + In that case, the values are injected in the process environment so that |
| 147 | + they get picked up during experiment's execution as if they had been |
| 148 | + from the terminal itself. |
| 149 | + Note that, when multiple var files are provided, they can override each |
| 150 | + other. |
| 151 | + The output of this function is a tuple made of configuration and secrets |
| 152 | + that will be used during the experiment's execution for lookup. |
| 153 | + """ |
| 154 | + config_vars = {} |
| 155 | + secret_vars = {} |
| 156 | + |
| 157 | + if var_files: |
| 158 | + for var_file in var_files: |
| 159 | + logger.debug("Loading var from file '{}'".format(var_file)) |
| 160 | + |
| 161 | + if not os.path.isfile(var_file): |
| 162 | + logger.error("Cannot read var file '{}'".format(var_file)) |
| 163 | + continue |
| 164 | + |
| 165 | + content = None |
| 166 | + with open(var_file) as f: |
| 167 | + content = f.read() |
| 168 | + |
| 169 | + if not content: |
| 170 | + logger.debug("Var file '{}' is empty".format(var_file)) |
| 171 | + continue |
| 172 | + |
| 173 | + data = None |
| 174 | + _, ext = os.path.splitext(var_file) |
| 175 | + if ext in (".yaml", ".yml"): |
| 176 | + try: |
| 177 | + data = yaml.safe_load(content) |
| 178 | + except yaml.YAMLError as y: |
| 179 | + logger.error( |
| 180 | + "Failed to parse variable file '{}': {}".format( |
| 181 | + var_file, str(y))) |
| 182 | + continue |
| 183 | + elif ext in (".json"): |
| 184 | + try: |
| 185 | + data = json.loads(content) |
| 186 | + except JSONDecodeError as x: |
| 187 | + logger.error( |
| 188 | + "Failed to parse variable file '{}': {}".format( |
| 189 | + var_file, str(x))) |
| 190 | + continue |
| 191 | + |
| 192 | + # process .env files |
| 193 | + if not data: |
| 194 | + for line in content.split(os.linesep): |
| 195 | + line = line.strip() |
| 196 | + if not line or line.startswith("#"): |
| 197 | + continue |
| 198 | + |
| 199 | + k, v = line.split('=', 1) |
| 200 | + os.environ[k] = v |
| 201 | + logger.debug( |
| 202 | + "Inject environment variable '{}' from " |
| 203 | + "file '{}'".format(k, var_file)) |
| 204 | + else: |
| 205 | + logger.debug( |
| 206 | + "Reading configuration/secrets from {}".format(f.name)) |
| 207 | + config_vars.update(data.get("configuration", {})) |
| 208 | + secret_vars.update(data.get("secrets", {})) |
| 209 | + |
| 210 | + if var: |
| 211 | + for k in var: |
| 212 | + logger.debug("Using configuration variable '{}'".format(k)) |
| 213 | + config_vars[k] = var[k] |
| 214 | + |
| 215 | + return (config_vars, secret_vars) |
| 216 | + |
| 217 | + |
| 218 | +def convert_vars(value: List[str]) -> Dict[str, Any]: # noqa: C901 |
| 219 | + """ |
| 220 | + Process all variables and return a dictionnary of them with the |
| 221 | + value converted to the appropriate type. |
| 222 | +
|
| 223 | + The list of values is as follows: `key[:type]=value` with `type` being one |
| 224 | + of: str, int, float and bytes. `str` is the default and can be omitted. |
| 225 | + """ |
| 226 | + var = {} |
| 227 | + for v in value: |
| 228 | + try: |
| 229 | + k, v = v.split('=', 1) |
| 230 | + if ':' in k: |
| 231 | + k, typ = k.rsplit(':', 1) |
| 232 | + try: |
| 233 | + if typ == 'str': |
| 234 | + pass |
| 235 | + elif typ == 'int': |
| 236 | + v = int(v) |
| 237 | + elif typ == 'float': |
| 238 | + v = float(v) |
| 239 | + elif typ == 'bytes': |
| 240 | + v = v.encode('utf-8') |
| 241 | + else: |
| 242 | + raise ValueError( |
| 243 | + 'var supports only: str, int, float and bytes') |
| 244 | + except (TypeError, UnicodeEncodeError): |
| 245 | + raise ValueError( |
| 246 | + 'var cannot convert value to required type') |
| 247 | + var[k] = v |
| 248 | + except ValueError: |
| 249 | + raise |
| 250 | + except Exception: |
| 251 | + raise ValueError('var needs to be in the format name[:type]=value') |
| 252 | + |
| 253 | + return var |
0 commit comments