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 |