Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2017 The LUCI Authors. All rights reserved. | 1 # Copyright 2017 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 """The context module provides APIs for manipulating a few pieces of 'ambient' | 5 """The context module provides APIs for manipulating a few pieces of 'ambient' |
| 6 data that affect how steps are run. | 6 data that affect how steps are run. |
| 7 | 7 |
| 8 The pieces of information which can be modified are: | 8 The pieces of information which can be modified are: |
| 9 * cwd - The current working directory. | 9 * cwd - The current working directory. |
| 10 * env - The environment variables. | 10 * env - The environment variables. |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 21 program. | 21 program. |
| 22 | 22 |
| 23 Example: | 23 Example: |
| 24 ```python | 24 ```python |
| 25 with api.context(cwd=api.path['start_dir'].join('subdir')): | 25 with api.context(cwd=api.path['start_dir'].join('subdir')): |
| 26 # this step is run inside of the subdir directory. | 26 # this step is run inside of the subdir directory. |
| 27 api.step("cat subdir/foo", ['cat', './foo']) | 27 api.step("cat subdir/foo", ['cat', './foo']) |
| 28 ``` | 28 ``` |
| 29 """ | 29 """ |
| 30 | 30 |
| 31 | |
| 32 import collections | 31 import collections |
| 33 import os | 32 import copy |
| 34 import types | |
| 35 | 33 |
| 36 from contextlib import contextmanager | 34 from contextlib import contextmanager |
| 37 | 35 |
| 38 from recipe_engine import recipe_api | |
| 39 from recipe_engine.config_types import Path | 36 from recipe_engine.config_types import Path |
| 40 from recipe_engine.recipe_api import RecipeApi | 37 from recipe_engine.recipe_api import RecipeApi |
| 41 | 38 |
| 39 from libs import luci_context as luci_ctx | |
| 40 | |
| 42 | 41 |
| 43 def check_type(name, var, expect): | 42 def check_type(name, var, expect): |
| 44 if not isinstance(var, expect): # pragma: no cover | 43 if not isinstance(var, expect): # pragma: no cover |
| 45 raise TypeError('%s is not %s: %r (%s)' % ( | 44 raise TypeError('%s is not %s: %r (%s)' % ( |
| 46 name, expect.__name__, var, type(var).__name__)) | 45 name, expect.__name__, var, type(var).__name__)) |
| 47 | 46 |
| 48 | 47 |
| 49 class ContextApi(RecipeApi): | 48 class ContextApi(RecipeApi): |
| 50 | 49 |
| 51 # TODO(iannucci): move implementation of these data directly into this class. | 50 # TODO(iannucci): move implementation of these data directly into this class. |
| 52 def __init__(self, **kwargs): | 51 def __init__(self, **kwargs): |
| 53 super(RecipeApi, self).__init__(**kwargs) | 52 super(RecipeApi, self).__init__(**kwargs) |
| 54 | 53 |
| 55 self._cwd = [None] | 54 self._cwd = [None] |
| 56 self._env_prefixes = [{}] | 55 self._env_prefixes = [{}] |
| 57 self._env = [{}] | 56 self._env = [{}] |
| 57 if self._test_data.enabled: | |
| 58 self._luci_context = [self._test_data.get('luci_context', {})] | |
| 59 else: # pragma: no cover | |
| 60 self._luci_context = [luci_ctx.read_full()] | |
| 58 self._infra_step = [False] | 61 self._infra_step = [False] |
| 59 self._name_prefix = [''] | 62 self._name_prefix = [''] |
| 60 # this could be a number, but it makes the logic easier to use a stack. | 63 # this could be a number, but it makes the logic easier to use a stack. |
| 61 self._nest_level = [0] | 64 self._nest_level = [0] |
| 62 | 65 |
| 63 @contextmanager | 66 @contextmanager |
| 64 def __call__(self, cwd=None, env_prefixes=None, env=None, | 67 def __call__(self, cwd=None, env_prefixes=None, env=None, luci_context=None, |
| 65 increment_nest_level=None, infra_steps=None, name_prefix=None): | 68 increment_nest_level=None, infra_steps=None, name_prefix=None): |
| 66 """Allows adjustment of multiple context values in a single call. | 69 """Allows adjustment of multiple context values in a single call. |
| 67 | 70 |
| 68 Args: | 71 Args: |
| 69 * cwd (Path) - the current working directory to use for all steps. | 72 * cwd (Path) - the current working directory to use for all steps. |
| 70 To 'reset' to the original cwd at the time recipes started, pass | 73 To 'reset' to the original cwd at the time recipes started, pass |
| 71 `api.path['start_dir']`. | 74 `api.path['start_dir']`. |
| 72 * env_prefixes (dict) - Environmental variable prefix augmentations. See | 75 * env_prefixes (dict) - Environmental variable prefix augmentations. See |
| 73 below for more info. | 76 below for more info. |
| 74 * env (dict) - Environmental variable overrides. See below for more info. | 77 * env (dict) - Environmental variable overrides. See below for more info. |
| 78 * luci_context (dict) - LUCI_CONTEXT overrides. | |
|
iannucci
2017/08/04 18:55:50
"see below for more info"
| |
| 75 * increment_nest_level (True) - increment the nest level by 1 in this | 79 * increment_nest_level (True) - increment the nest level by 1 in this |
| 76 context. Typically you won't directly interact with this, but should | 80 context. Typically you won't directly interact with this, but should |
| 77 use api.step.nest instead. | 81 use api.step.nest instead. |
| 78 * infra_steps (bool) - if steps in this context should be considered | 82 * infra_steps (bool) - if steps in this context should be considered |
| 79 infrastructure steps. On failure, these will raise InfraFailure | 83 infrastructure steps. On failure, these will raise InfraFailure |
| 80 exceptions instead of StepFailure exceptions. | 84 exceptions instead of StepFailure exceptions. |
| 81 * name_prefix (str) - A string to prepend to the names of all steps in | 85 * name_prefix (str) - A string to prepend to the names of all steps in |
| 82 this context. These compose with '.' characters if multiple name prefix | 86 this context. These compose with '.' characters if multiple name prefix |
| 83 contexts occur. See below for more info. | 87 contexts occur. See below for more info. |
| 84 | 88 |
| (...skipping 25 matching lines...) Expand all Loading... | |
| 110 Which, at the time the step executes, will inject the current value of | 114 Which, at the time the step executes, will inject the current value of |
| 111 $PATH. | 115 $PATH. |
| 112 | 116 |
| 113 "env_prefix" is a list of Path or strings that get prefixed to their | 117 "env_prefix" is a list of Path or strings that get prefixed to their |
| 114 respective environment variables, delimited with the system's path | 118 respective environment variables, delimited with the system's path |
| 115 separator. This can be used to add entries to environment variables such | 119 separator. This can be used to add entries to environment variables such |
| 116 as "PATH" and "PYTHONPATH". If prefixes are specified and a value is also | 120 as "PATH" and "PYTHONPATH". If prefixes are specified and a value is also |
| 117 defined in "env", it will be installed as the last path component if it is | 121 defined in "env", it will be installed as the last path component if it is |
| 118 not empty. | 122 not empty. |
| 119 | 123 |
| 124 LUCI_CONTEXT overrides: | |
| 125 | |
| 126 This is advanced stuff. LUCI_CONTEXT is used to pass ambient information | |
| 127 between layers of LUCI stack. 'luci_context' can be used to replace, add or | |
| 128 pop items in the current LUCI_CONTEXT. Unlike 'env', LUCI_CONTEXT contains | |
| 129 structured values, so values of 'luci_context' dict can be anything (not | |
| 130 only strings), and we are not attempting to merge them in any way, e.g | |
| 131 there's no equivalent of %(PATH)s trick. | |
|
iannucci
2017/08/04 18:55:50
we're going to delete the %(PATH) trick anyway, so
| |
| 132 | |
| 133 See https://github.com/luci/client-py/blob/master/LUCI_CONTEXT.md. | |
| 134 | |
| 120 **TODO(iannucci): combine nest_level and name_prefix** | 135 **TODO(iannucci): combine nest_level and name_prefix** |
| 121 | 136 |
| 122 Look at the examples in "examples/" for examples of context module usage. | 137 Look at the examples in "examples/" for examples of context module usage. |
| 123 """ | 138 """ |
| 124 to_pop = [] | 139 to_pop = [] |
| 125 def _push(st, val): | 140 def _push(st, val): |
| 126 st.append(val) | 141 st.append(val) |
| 127 to_pop.append(st) | 142 to_pop.append(st) |
| 128 | 143 |
| 129 if cwd is not None: | 144 if cwd is not None: |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 175 # If the string contains any accidental sequential lookups, this | 190 # If the string contains any accidental sequential lookups, this |
| 176 # will raise an exception. If not, then this is a pluasible format | 191 # will raise an exception. If not, then this is a pluasible format |
| 177 # string. | 192 # string. |
| 178 ('%(foo)s'+v) % collections.defaultdict(str) | 193 ('%(foo)s'+v) % collections.defaultdict(str) |
| 179 except Exception: | 194 except Exception: |
| 180 raise ValueError(('Invalid %%-formatting parameter in envvar, ' | 195 raise ValueError(('Invalid %%-formatting parameter in envvar, ' |
| 181 'only %%(ENVVAR)s allowed: %r') % (v,)) | 196 'only %%(ENVVAR)s allowed: %r') % (v,)) |
| 182 new[k] = v | 197 new[k] = v |
| 183 _push(self._env, new) | 198 _push(self._env, new) |
| 184 | 199 |
| 200 if luci_context is not None and len(luci_context) > 0: | |
| 201 check_type('luci_context', luci_context, dict) | |
| 202 new = dict(self._luci_context[-1]) | |
| 203 for k, v in luci_context.iteritems(): | |
| 204 k = str(k) | |
| 205 if v is None: | |
| 206 new.pop(k, None) | |
| 207 elif isinstance(v, dict): | |
| 208 new[k] = copy.deepcopy(v) | |
|
iannucci
2017/08/04 18:55:51
can we assert that v is serializable to json? Othe
| |
| 209 else: | |
| 210 raise TypeError( | |
| 211 'Bad type for LUCI_CONTEXT[%r]: %s', k, type(v).__name__) | |
| 212 _push(self._luci_context, new) | |
| 213 | |
| 185 try: | 214 try: |
| 186 yield | 215 yield |
| 187 finally: | 216 finally: |
| 188 for p in to_pop: | 217 for p in to_pop: |
| 189 p.pop() | 218 p.pop() |
| 190 | 219 |
| 191 @property | 220 @property |
| 192 def cwd(self): | 221 def cwd(self): |
| 193 """Returns the current working directory that steps will run in. | 222 """Returns the current working directory that steps will run in. |
| 194 | 223 |
| (...skipping 26 matching lines...) Expand all Loading... | |
| 221 prefixes registered with the environment. | 250 prefixes registered with the environment. |
| 222 | 251 |
| 223 **Returns (dict)** - The env-key -> value(Path) mapping of current | 252 **Returns (dict)** - The env-key -> value(Path) mapping of current |
| 224 environment prefix modifications. | 253 environment prefix modifications. |
| 225 """ | 254 """ |
| 226 # TODO(iannucci): store env in an immutable way to avoid excessive copies. | 255 # TODO(iannucci): store env in an immutable way to avoid excessive copies. |
| 227 # TODO(iannucci): handle case-insensitive keys on windows | 256 # TODO(iannucci): handle case-insensitive keys on windows |
| 228 return dict(self._env_prefixes[-1]) | 257 return dict(self._env_prefixes[-1]) |
| 229 | 258 |
| 230 @property | 259 @property |
| 260 def luci_context(self): | |
| 261 """Returns a dict with current state of LUCI_CONTEXT. | |
| 262 | |
| 263 This is advanced stuff. LUCI_CONTEXT is used to pass ambient information | |
| 264 between layers of LUCI stack. Unlike 'env', it MAY be populated with | |
| 265 information about LUCI execution environment when recipe engine starts. | |
| 266 | |
| 267 Do not dump the entirety of LUCI_CONTEXT into logs, it may contain secrets. | |
|
iannucci
2017/08/04 18:55:50
s/may contain/contains
make them scared :)
| |
| 268 | |
| 269 See https://github.com/luci/client-py/blob/master/LUCI_CONTEXT.md. | |
| 270 """ | |
| 271 return dict(self._luci_context[-1]) | |
| 272 | |
| 273 @property | |
| 231 def infra_step(self): | 274 def infra_step(self): |
| 232 """Returns the current value of the infra_step setting. | 275 """Returns the current value of the infra_step setting. |
| 233 | 276 |
| 234 **Returns (bool)** - True iff steps are currently considered infra steps. | 277 **Returns (bool)** - True iff steps are currently considered infra steps. |
| 235 """ | 278 """ |
| 236 return self._infra_step[-1] | 279 return self._infra_step[-1] |
| 237 | 280 |
| 238 @property | 281 @property |
| 239 def name_prefix(self): | 282 def name_prefix(self): |
| 240 """Gets the current step name prefix. | 283 """Gets the current step name prefix. |
| 241 | 284 |
| 242 **Returns (str)** - The string prefix that every step will have prepended to | 285 **Returns (str)** - The string prefix that every step will have prepended to |
| 243 it. | 286 it. |
| 244 """ | 287 """ |
| 245 return self._name_prefix[-1] | 288 return self._name_prefix[-1] |
| 246 | 289 |
| 247 @property | 290 @property |
| 248 def nest_level(self): | 291 def nest_level(self): |
| 249 """Returns the current 'nesting' level. | 292 """Returns the current 'nesting' level. |
| 250 | 293 |
| 251 Note: This api is low-level, and you should always prefer to use | 294 Note: This api is low-level, and you should always prefer to use |
| 252 `api.step.nest`. This api is included for completeness and documentation | 295 `api.step.nest`. This api is included for completeness and documentation |
| 253 purposes. | 296 purposes. |
| 254 | 297 |
| 255 **Returns (int)** - The current nesting level. | 298 **Returns (int)** - The current nesting level. |
| 256 """ | 299 """ |
| 257 return self._nest_level[-1] | 300 return self._nest_level[-1] |
| OLD | NEW |