Lint BuildManager

This commit is contained in:
Jimmy Zelinskie 2014-11-18 15:45:56 -05:00
parent 043a30ee96
commit 6df6f28edf
9 changed files with 187 additions and 173 deletions

View file

@ -10,7 +10,7 @@ from trollius.coroutines import From
from buildman.basecomponent import BaseComponent
from buildman.buildpack import BuildPackage, BuildPackageException
from buildman.buildstatus import StatusHandler
from buildman.server import BUILD_JOB_RESULT
from buildman.server import BuildJobResult
from buildman.workererror import WorkerError
from data.database import BUILD_PHASE
@ -18,9 +18,10 @@ from data.database import BUILD_PHASE
HEARTBEAT_DELTA = datetime.timedelta(seconds=30)
HEARTBEAT_TIMEOUT = 10
logger = logging.getLogger(__name__)
LOGGER = logging.getLogger(__name__)
class COMPONENT_STATUS(object):
class ComponentStatus(object):
""" ComponentStatus represents the possible states of a component. """
JOINING = 'joining'
WAITING = 'waiting'
RUNNING = 'running'
@ -34,7 +35,7 @@ class BuildComponent(BaseComponent):
expected_token = None
builder_realm = None
_component_status = COMPONENT_STATUS.JOINING
_component_status = ComponentStatus.JOINING
_last_heartbeat = None
_current_job = None
_build_status = None
@ -46,33 +47,35 @@ class BuildComponent(BaseComponent):
BaseComponent.__init__(self, config, **kwargs)
def onConnect(self):
def onConnect(self):
self.join(self.builder_realm)
def onJoin(self, details):
logger.debug('Registering methods and listeners for component %s' % self.builder_realm)
LOGGER.debug('Registering methods and listeners for component %s', self.builder_realm)
yield From(self.register(self._on_ready, u'io.quay.buildworker.ready'))
yield From(self.register(self._ping, u'io.quay.buildworker.ping'))
yield From(self.subscribe(self._on_heartbeat, 'io.quay.builder.heartbeat'))
yield From(self.subscribe(self._on_log_message, 'io.quay.builder.logmessage'))
self._set_status(COMPONENT_STATUS.WAITING)
self._set_status(ComponentStatus.WAITING)
def is_ready(self):
return self._component_status == COMPONENT_STATUS.RUNNING
""" Determines whether a build component is ready to begin a build. """
return self._component_status == ComponentStatus.RUNNING
def start_build(self, build_job):
def start_build(self, build_job):
""" Starts a build. """
self._current_job = build_job
self._build_status = StatusHandler(self.build_logs, build_job.repo_build())
self._image_info = {}
self._set_status(COMPONENT_STATUS.BUILDING)
self._set_status(ComponentStatus.BUILDING)
# Retrieve the job's buildpack.
buildpack_url = self.user_files.get_file_url(build_job.repo_build().resource_key,
requires_cors=False)
logger.debug('Retreiving build package: %s' % buildpack_url)
LOGGER.debug('Retreiving build package: %s', buildpack_url)
buildpack = None
try:
buildpack = BuildPackage.from_url(buildpack_url)
@ -82,7 +85,7 @@ class BuildComponent(BaseComponent):
# Extract the base image information from the Dockerfile.
parsed_dockerfile = None
logger.debug('Parsing dockerfile')
LOGGER.debug('Parsing dockerfile')
build_config = build_job.build_config()
try:
@ -94,11 +97,11 @@ class BuildComponent(BaseComponent):
image_and_tag_tuple = parsed_dockerfile.get_image_and_tag()
if image_and_tag_tuple is None or image_and_tag_tuple[0] is None:
self._build_failure('Missing FROM line in Dockerfile')
return
return
base_image_information = {
'repository': image_and_tag_tuple[0],
'tag': image_and_tag_tuple[1]
'repository': image_and_tag_tuple[0],
'tag': image_and_tag_tuple[1]
}
# Extract the number of steps from the Dockerfile.
@ -115,46 +118,47 @@ class BuildComponent(BaseComponent):
repository_name = repo.namespace_user.username + '/' + repo.name
# Parse the build queue item into build arguments.
# build_package: URL to the build package to download and untar/unzip.
# sub_directory: The location within the build package of the Dockerfile and the build context.
# repository: The repository for which this build is occurring.
# registry: The registry for which this build is occuring. Example: 'quay.io', 'staging.quay.io'
# pull_token: The token to use when pulling the cache for building.
# push_token: The token to use to push the built image.
# tag_names: The name(s) of the tag(s) for the newly built image.
# base_image: The image name and credentials to use to conduct the base image pull.
# repository: The repository to pull.
# tag: The tag to pull.
# username: The username for pulling the base image (if any).
# password: The password for pulling the base image (if any).
# build_package: URL to the build package to download and untar/unzip.
# sub_directory: The location within the build package of the Dockerfile and the build context.
# repository: The repository for which this build is occurring.
# registry: The registry for which this build is occuring (e.g. 'quay.io', 'staging.quay.io').
# pull_token: The token to use when pulling the cache for building.
# push_token: The token to use to push the built image.
# tag_names: The name(s) of the tag(s) for the newly built image.
# base_image: The image name and credentials to use to conduct the base image pull.
# repository: The repository to pull.
# tag: The tag to pull.
# username: The username for pulling the base image (if any).
# password: The password for pulling the base image (if any).
build_arguments = {
'build_package': buildpack_url,
'sub_directory': build_config.get('build_subdir', ''),
'repository': repository_name,
'registry': self.server_hostname,
'pull_token': build_job.repo_build().access_token.code,
'push_token': build_job.repo_build().access_token.code,
'tag_names': build_config.get('docker_tags', ['latest']),
'base_image': base_image_information,
'cached_tag': build_job.determine_cached_tag() or ''
'build_package': buildpack_url,
'sub_directory': build_config.get('build_subdir', ''),
'repository': repository_name,
'registry': self.server_hostname,
'pull_token': build_job.repo_build().access_token.code,
'push_token': build_job.repo_build().access_token.code,
'tag_names': build_config.get('docker_tags', ['latest']),
'base_image': base_image_information,
'cached_tag': build_job.determine_cached_tag() or ''
}
# Invoke the build.
logger.debug('Invoking build: %s', self.builder_realm)
logger.debug('With Arguments: %s', build_arguments)
LOGGER.debug('Invoking build: %s', self.builder_realm)
LOGGER.debug('With Arguments: %s', build_arguments)
return (self.call("io.quay.builder.build", **build_arguments)
.add_done_callback(self._build_complete))
.add_done_callback(self._build_complete))
@staticmethod
def __total_completion(statuses, total_images):
def _total_completion(statuses, total_images):
""" Returns the current amount completion relative to the total completion of a build. """
percentage_with_sizes = float(len(statuses.values())) / total_images
sent_bytes = sum([status['current'] for status in statuses.values()])
total_bytes = sum([status['total'] for status in statuses.values()])
return float(sent_bytes) / total_bytes * percentage_with_sizes
@staticmethod
def __process_pushpull_status(status_dict, current_phase, docker_data, images):
def _process_pushpull_status(status_dict, current_phase, docker_data, images):
if not docker_data:
return
@ -178,7 +182,7 @@ class BuildComponent(BaseComponent):
if 'current' in detail and 'total' in detail:
images[image_id] = detail
status_dict[status_completion_key] = \
BuildComponent.__total_completion(images, max(len(images), num_images))
BuildComponent._total_completion(images, max(len(images), num_images))
def _on_log_message(self, phase, json_data):
# Parse any of the JSON data logged.
@ -209,8 +213,8 @@ class BuildComponent(BaseComponent):
# Parse and update the phase and the status_dict. The status dictionary contains
# the pull/push progress, as well as the current step index.
with self._build_status as status_dict:
self._build_status.set_phase(phase)
BuildComponent.__process_pushpull_status(status_dict, phase, docker_data, self._image_info)
self._build_status.set_phase(phase)
BuildComponent._process_pushpull_status(status_dict, phase, docker_data, self._image_info)
# If the current message represents the beginning of a new step, then update the
# current command index.
@ -228,104 +232,110 @@ class BuildComponent(BaseComponent):
def _build_failure(self, error_message, exception=None):
""" Handles and logs a failed build. """
self._build_status.set_error(error_message, {
'internal_error': exception.message if exception else None
'internal_error': exception.message if exception else None
})
build_id = self._current_job.repo_build().uuid
logger.warning('Build %s failed with message: %s', build_id, self._error_message)
LOGGER.warning('Build %s failed with message: %s', build_id, error_message)
# Mark that the build has finished (in an error state)
self._build_finished(BUILD_JOB_RESULT.ERROR)
self._build_finished(BuildJobResult.ERROR)
def _build_complete(self, result):
""" Wraps up a completed build. Handles any errors and calls self._build_finished. """
try:
# Retrieve the result. This will raise an ApplicationError on any error that occurred.
result.result()
self._build_status.set_phase(BUILD_PHASE.COMPLETE)
self._build_finished(BUILD_JOB_RESULT.COMPLETE)
except ApplicationError as ae:
worker_error = WorkerError(ae.error, ae.kwargs.get('base_error'))
self._build_finished(BuildJobResult.COMPLETE)
except ApplicationError as aex:
worker_error = WorkerError(aex.error, aex.kwargs.get('base_error'))
# Write the error to the log.
self._build_status.set_error(worker_error.public_message(), worker_error.extra_data())
# Mark the build as completed.
if worker_error.is_internal_error():
self._build_finished(BUILD_JOB_RESULT.INCOMPLETE)
self._build_finished(BuildJobResult.INCOMPLETE)
else:
self._build_finished(BUILD_JOB_RESULT.ERROR)
self._build_finished(BuildJobResult.ERROR)
def _build_finished(self, job_status):
""" Alerts the parent that a build has completed and sets the status back to running. """
self.parent_manager.job_completed(self._current_job, job_status, self)
self._current_job = None
# Set the component back to a running state.
self._set_status(COMPONENT_STATUS.RUNNING)
self._set_status(ComponentStatus.RUNNING)
def _ping(self):
def _ping():
""" Ping pong. """
return 'pong'
def _on_ready(self, token):
if self._component_status != 'waiting':
logger.warning('Build component with token %s is already connected', self.expected_token)
LOGGER.warning('Build component with token %s is already connected', self.expected_token)
return
if token != self.expected_token:
logger.warning('Builder token mismatch. Expected: %s. Found: %s', self.expected_token, token)
LOGGER.warning('Builder token mismatch. Expected: %s. Found: %s', self.expected_token, token)
return
self._set_status(COMPONENT_STATUS.RUNNING)
self._set_status(ComponentStatus.RUNNING)
# Start the heartbeat check and updating loop.
loop = trollius.get_event_loop()
loop.create_task(self._heartbeat(loop))
logger.debug('Build worker %s is connected and ready' % self.builder_realm)
loop.create_task(self._heartbeat())
LOGGER.debug('Build worker %s is connected and ready', self.builder_realm)
return True
def _set_status(self, phase):
self._component_status = phase
def _on_heartbeat(self):
""" Updates the last known heartbeat. """
self._last_heartbeat = datetime.datetime.now()
def _start_heartbeat(self, loop):
""" Begins an async loop to keep a heartbeat going with a client. """
trollius.set_event_loop(loop)
loop.run_until_complete(self._heartbeat())
@trollius.coroutine
def _heartbeat(self, loop):
def _heartbeat(self):
""" Coroutine that runs every HEARTBEAT_TIMEOUT seconds, both checking the worker's heartbeat
and updating the heartbeat in the build status dictionary (if applicable). This allows
the build system to catch crashes from either end.
"""
while True:
# If the component is no longer running or actively building, nothing more to do.
if (self._component_status != COMPONENT_STATUS.RUNNING and
self._component_status != COMPONENT_STATUS.BUILDING):
if (self._component_status != ComponentStatus.RUNNING and
self._component_status != ComponentStatus.BUILDING):
return
# If there is an active build, write the heartbeat to its status.
build_status = self._build_status
if build_status is not None:
with build_status as status_dict:
status_dict['heartbeat'] = int(time.time())
status_dict['heartbeat'] = int(datetime.time())
# Check the heartbeat from the worker.
logger.debug('Checking heartbeat on realm %s', self.builder_realm)
LOGGER.debug('Checking heartbeat on realm %s', self.builder_realm)
if not self._last_heartbeat:
self._timeout()
return
if self._last_heartbeat < datetime.datetime.now() - HEARTBEAT_DELTA:
self._timeout()
return
return
yield From(trollius.sleep(HEARTBEAT_TIMEOUT))
def _timeout(self):
self._set_status(COMPONENT_STATUS.TIMED_OUT)
logger.warning('Build component %s timed out', self.expected_token)
self._set_status(ComponentStatus.TIMED_OUT)
LOGGER.warning('Build component %s timed out', self.expected_token)
self._dispose(timed_out=True)
def _dispose(self, timed_out=False):
@ -335,7 +345,7 @@ class BuildComponent(BaseComponent):
if timed_out:
self._build_status.set_error('Build worker timed out. Build has been requeued')
self.parent_manager.job_completed(self._current_job, BUILD_JOB_RESULT.INCOMPLETE, self)
self.parent_manager.job_completed(self._current_job, BuildJobResult.INCOMPLETE, self)
self._build_status = None
self._current_job = None