import json import logging from cachetools import lru_cache from endpoints.notificationhelper import spawn_notification from data import model from util.imagetree import ImageTree logger = logging.getLogger(__name__) class BuildJobLoadException(Exception): """ Exception raised if a build job could not be instantiated for some reason. """ pass class BuildJob(object): """ Represents a single in-progress build job. """ def __init__(self, job_item): self.job_item = job_item try: self.job_details = json.loads(job_item.body) except ValueError: raise BuildJobLoadException( 'Could not parse build queue item config with ID %s' % self.job_details['build_uuid'] ) def send_notification(self, kind, error_message=None): tags = self.build_config.get('docker_tags', ['latest']) event_data = { 'build_id': self.repo_build.uuid, 'build_name': self.repo_build.display_name, 'docker_tags': tags, 'trigger_id': self.repo_build.trigger.uuid, 'trigger_kind': self.repo_build.trigger.service.name } if error_message is not None: event_data['error_message'] = error_message spawn_notification(self.repo_build.repository, kind, event_data, subpage='build?current=%s' % self.repo_build.uuid, pathargs=['build', self.repo_build.uuid]) @lru_cache(maxsize=1) def _load_repo_build(self): try: return model.get_repository_build(self.job_details['build_uuid']) except model.InvalidRepositoryBuildException: raise BuildJobLoadException( 'Could not load repository build with ID %s' % self.job_details['build_uuid']) @property def repo_build(self): return self._load_repo_build() @property def pull_credentials(self): """ Returns the pull credentials for this job, or None if none. """ return self.job_details.get('pull_credentials') @property def build_config(self): try: return json.loads(self.repo_build.job_config) except ValueError: raise BuildJobLoadException( 'Could not parse repository build job config with ID %s' % self.job_details['build_uuid'] ) def determine_cached_tag(self, base_image_id=None, cache_comments=None): """ Returns the tag to pull to prime the cache or None if none. """ cached_tag = None if base_image_id and cache_comments: cached_tag = self._determine_cached_tag_by_comments(base_image_id, cache_comments) if not cached_tag: cached_tag = self._determine_cached_tag_by_tag() logger.debug('Determined cached tag %s for %s: %s', cached_tag, base_image_id, cache_comments) return cached_tag def _determine_cached_tag_by_comments(self, base_image_id, cache_commands): """ Determines the tag to use for priming the cache for this build job, by matching commands starting at the given base_image_id. This mimics the Docker cache checking, so it should, in theory, provide "perfect" caching. """ # Lookup the base image in the repository. If it doesn't exist, nothing more to do. repo_build = self.repo_build repo_namespace = repo_build.repository.namespace_user.username repo_name = repo_build.repository.name base_image = model.get_image(repo_build.repository, base_image_id) if base_image is None: return None # Build an in-memory tree of the full heirarchy of images in the repository. all_images = model.get_repository_images(repo_namespace, repo_name) all_tags = model.list_repository_tags(repo_namespace, repo_name) tree = ImageTree(all_images, all_tags, base_filter=base_image.id) # Find a path in the tree, starting at the base image, that matches the cache comments # or some subset thereof. def checker(step, image): if step >= len(cache_commands): return False full_command = '["/bin/sh", "-c", "%s"]' % cache_commands[step] return image.storage.comment == full_command path = tree.find_longest_path(base_image.id, checker) if not path: return None # Find any tag associated with the last image in the path. return tree.tag_containing_images(path[-1]) def _determine_cached_tag_by_tag(self): """ Determines the cached tag by looking for one of the tags being built, and seeing if it exists in the repository. This is a fallback for when no comment information is available. """ tags = self.build_config.get('docker_tags', ['latest']) repository = self.repo_build.repository existing_tags = model.list_repository_tags(repository.namespace_user.username, repository.name) cached_tags = set(tags) & set([tag.name for tag in existing_tags]) if cached_tags: return list(cached_tags)[0] return None