1
+ # -*- coding: utf-8 -*-
2
+
1
3
"""Documentation Builder Environments."""
2
4
3
5
from __future__ import absolute_import
14
16
15
17
from readthedocs .core .utils import slugify
16
18
from django .conf import settings
17
- from django .utils .translation import ugettext_lazy as _ , ugettext_noop
19
+ from django .utils .translation import ugettext_lazy as _
18
20
from docker import Client
19
21
from docker .utils import create_host_config
20
22
from docker .errors import APIError as DockerAPIError , DockerException
40
42
__all__ = (
41
43
'api_v2' ,
42
44
'BuildCommand' , 'DockerBuildCommand' ,
43
- 'BuildEnvironment' , 'LocalEnvironment' , 'DockerEnvironment' ,
45
+ 'LocalEnvironment' ,
46
+ 'LocalBuildEnvironment' , 'DockerBuildEnvironment' ,
44
47
)
45
48
46
49
@@ -70,7 +73,7 @@ class BuildCommand(BuildCommandResultMixin):
70
73
71
74
def __init__ (self , command , cwd = None , shell = False , environment = None ,
72
75
combine_output = True , input_data = None , build_env = None ,
73
- bin_path = None , description = None ):
76
+ bin_path = None , description = None , record_as_success = False ):
74
77
self .command = command
75
78
self .shell = shell
76
79
if cwd is None :
@@ -93,6 +96,7 @@ def __init__(self, command, cwd=None, shell=False, environment=None,
93
96
self .description = ''
94
97
if description is not None :
95
98
self .description = description
99
+ self .record_as_success = record_as_success
96
100
self .exit_code = None
97
101
98
102
def __str__ (self ):
@@ -180,12 +184,21 @@ def get_command(self):
180
184
181
185
def save (self ):
182
186
"""Save this command and result via the API."""
187
+ exit_code = self .exit_code
188
+
189
+ # Force record this command as success to avoid Build reporting errors
190
+ # on commands that are just for checking purposes and do not interferes
191
+ # in the Build
192
+ if self .record_as_success :
193
+ log .warning ('Recording command exit_code as success' )
194
+ exit_code = 0
195
+
183
196
data = {
184
197
'build' : self .build_env .build .get ('id' ),
185
198
'command' : self .get_command (),
186
199
'description' : self .description ,
187
200
'output' : self .output ,
188
- 'exit_code' : self . exit_code ,
201
+ 'exit_code' : exit_code ,
189
202
'start_time' : self .start_time ,
190
203
'end_time' : self .end_time ,
191
204
}
@@ -268,7 +281,100 @@ def get_wrapped_command(self):
268
281
for part in self .command ]))))
269
282
270
283
271
- class BuildEnvironment (object ):
284
+ class BaseEnvironment (object ):
285
+
286
+ """
287
+ Base environment class.
288
+
289
+ Used to run arbitrary commands outside a build.
290
+ """
291
+
292
+ def __init__ (self , project , environment = None ):
293
+ # TODO: maybe we can remove this Project dependency also
294
+ self .project = project
295
+ self .environment = environment or {}
296
+ self .commands = []
297
+
298
+ def record_command (self , command ):
299
+ pass
300
+
301
+ def _log_warning (self , msg ):
302
+ log .warning (LOG_TEMPLATE .format (
303
+ project = self .project .slug ,
304
+ version = 'latest' ,
305
+ msg = msg ,
306
+ ))
307
+
308
+ def run (self , * cmd , ** kwargs ):
309
+ """Shortcut to run command from environment."""
310
+ return self .run_command_class (cls = self .command_class , cmd = cmd , ** kwargs )
311
+
312
+ def run_command_class (
313
+ self , cls , cmd , record = None , warn_only = False ,
314
+ record_as_success = False , ** kwargs ):
315
+ """
316
+ Run command from this environment.
317
+
318
+ :param cls: command class to instantiate a command
319
+ :param cmd: command (as a list) to execute in this environment
320
+ :param record: whether or not to record this particular command
321
+ (``False`` implies ``warn_only=True``)
322
+ :param warn_only: don't raise an exception on command failure
323
+ :param record_as_success: force command ``exit_code`` to be saved as
324
+ ``0`` (``True`` implies ``warn_only=True`` and ``record=True``)
325
+ """
326
+ if record is None :
327
+ # ``self.record`` only exists when called from ``*BuildEnvironment``
328
+ record = getattr (self , 'record' , False )
329
+
330
+ if not record :
331
+ warn_only = True
332
+
333
+ if record_as_success :
334
+ record = True
335
+ warn_only = True
336
+ # ``record_as_success`` is needed to instantiate the BuildCommand
337
+ kwargs .update ({'record_as_success' : record_as_success })
338
+
339
+ # Remove PATH from env, and set it to bin_path if it isn't passed in
340
+ env_path = self .environment .pop ('BIN_PATH' , None )
341
+ if 'bin_path' not in kwargs and env_path :
342
+ kwargs ['bin_path' ] = env_path
343
+ assert 'environment' not in kwargs , "environment can't be passed in via commands."
344
+ kwargs ['environment' ] = self .environment
345
+
346
+ # ``build_env`` is passed as ``kwargs`` when it's called from a
347
+ # ``*BuildEnvironment``
348
+ build_cmd = cls (cmd , ** kwargs )
349
+ self .commands .append (build_cmd )
350
+ build_cmd .run ()
351
+
352
+ if record :
353
+ # TODO: I don't like how it's handled this entry point here since
354
+ # this class should know nothing about a BuildCommand (which are the
355
+ # only ones that can be saved/recorded)
356
+ self .record_command (build_cmd )
357
+
358
+ if build_cmd .failed :
359
+ msg = u'Command {cmd} failed' .format (cmd = build_cmd .get_command ())
360
+
361
+ if build_cmd .output :
362
+ msg += u':\n {out}' .format (out = build_cmd .output )
363
+
364
+ if warn_only :
365
+ self ._log_warning (msg )
366
+ else :
367
+ raise BuildEnvironmentWarning (msg )
368
+ return build_cmd
369
+
370
+
371
+ class LocalEnvironment (BaseEnvironment ):
372
+
373
+ # TODO: BuildCommand name doesn't make sense here, should be just Command
374
+ command_class = BuildCommand
375
+
376
+
377
+ class BuildEnvironment (BaseEnvironment ):
272
378
273
379
"""
274
380
Base build environment.
@@ -303,15 +409,13 @@ class BuildEnvironment(object):
303
409
304
410
def __init__ (self , project = None , version = None , build = None , config = None ,
305
411
record = True , environment = None , update_on_success = True ):
306
- self . project = project
412
+ super ( BuildEnvironment , self ). __init__ ( project , environment )
307
413
self .version = version
308
414
self .build = build
309
415
self .config = config
310
416
self .record = record
311
- self .environment = environment or {}
312
417
self .update_on_success = update_on_success
313
418
314
- self .commands = []
315
419
self .failure = None
316
420
self .start_time = datetime .utcnow ()
317
421
@@ -348,48 +452,28 @@ def handle_exception(self, exc_type, exc_value, _):
348
452
self .failure = exc_value
349
453
return True
350
454
351
- def run (self , * cmd , ** kwargs ):
352
- """Shortcut to run command from environment."""
353
- return self .run_command_class (cls = self .command_class , cmd = cmd , ** kwargs )
455
+ def record_command (self , command ):
456
+ command .save ()
354
457
355
- def run_command_class (self , cls , cmd , ** kwargs ):
356
- """
357
- Run command from this environment.
358
-
359
- Use ``cls`` to instantiate a command
360
-
361
- :param warn_only: Don't raise an exception on command failure
362
- """
363
- warn_only = kwargs .pop ('warn_only' , False )
364
- # Remove PATH from env, and set it to bin_path if it isn't passed in
365
- env_path = self .environment .pop ('BIN_PATH' , None )
366
- if 'bin_path' not in kwargs and env_path :
367
- kwargs ['bin_path' ] = env_path
368
- assert 'environment' not in kwargs , "environment can't be passed in via commands."
369
- kwargs ['environment' ] = self .environment
370
- kwargs ['build_env' ] = self
371
- build_cmd = cls (cmd , ** kwargs )
372
- self .commands .append (build_cmd )
373
- build_cmd .run ()
374
-
375
- # Save to database
376
- if self .record :
377
- build_cmd .save ()
378
-
379
- if build_cmd .failed :
380
- msg = u'Command {cmd} failed' .format (cmd = build_cmd .get_command ())
458
+ def _log_warning (self , msg ):
459
+ # :'(
460
+ log .warning (LOG_TEMPLATE .format (
461
+ project = self .project .slug ,
462
+ version = self .version .slug ,
463
+ msg = msg ,
464
+ ))
381
465
382
- if build_cmd .output :
383
- msg += u':\n {out}' .format (out = build_cmd .output )
466
+ def run (self , * cmd , ** kwargs ):
467
+ kwargs .update ({
468
+ 'build_env' : self ,
469
+ })
470
+ return super (BuildEnvironment , self ).run (* cmd , ** kwargs )
384
471
385
- if warn_only :
386
- log .warning (LOG_TEMPLATE
387
- .format (project = self .project .slug ,
388
- version = self .version .slug ,
389
- msg = msg ))
390
- else :
391
- raise BuildEnvironmentWarning (msg )
392
- return build_cmd
472
+ def run_command_class (self , * cmd , ** kwargs ): # pylint: disable=arguments-differ
473
+ kwargs .update ({
474
+ 'build_env' : self ,
475
+ })
476
+ return super (BuildEnvironment , self ).run_command_class (* cmd , ** kwargs )
393
477
394
478
@property
395
479
def successful (self ):
@@ -497,14 +581,14 @@ def update_build(self, state=None):
497
581
log .exception ("Unknown build exception" )
498
582
499
583
500
- class LocalEnvironment (BuildEnvironment ):
584
+ class LocalBuildEnvironment (BuildEnvironment ):
501
585
502
- """Local execution environment."""
586
+ """Local execution build environment."""
503
587
504
588
command_class = BuildCommand
505
589
506
590
507
- class DockerEnvironment (BuildEnvironment ):
591
+ class DockerBuildEnvironment (BuildEnvironment ):
508
592
509
593
"""
510
594
Docker build environment, uses docker to contain builds.
@@ -527,7 +611,7 @@ class DockerEnvironment(BuildEnvironment):
527
611
528
612
def __init__ (self , * args , ** kwargs ):
529
613
self .docker_socket = kwargs .pop ('docker_socket' , DOCKER_SOCKET )
530
- super (DockerEnvironment , self ).__init__ (* args , ** kwargs )
614
+ super (DockerBuildEnvironment , self ).__init__ (* args , ** kwargs )
531
615
self .client = None
532
616
self .container = None
533
617
self .container_name = slugify (
0 commit comments