From 9057c1cdc559706187d87e840b5e1aaceda5ab41 Mon Sep 17 00:00:00 2001 From: Jeff Kingyens Date: Fri, 29 Aug 2014 18:04:09 -0700 Subject: [PATCH] add a basic fig push command --- fig/cli/command.py | 4 +++- fig/cli/main.py | 25 ++++++++++++++++++++----- fig/project.py | 21 ++++++++++++++------- fig/service.py | 41 +++++++++++++++++++++++++++++++++++++++-- 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/fig/cli/command.py b/fig/cli/command.py index fd266d03bd4..fc27faaa9e7 100644 --- a/fig/cli/command.py +++ b/fig/cli/command.py @@ -44,6 +44,7 @@ def perform_command(self, options, handler, command_options): project = self.get_project( self.get_config_path(explicit_config_path), project_name=options.get('--project-name'), + repo_name=options.get('--repository-name'), verbose=options.get('--verbose')) handler(project, command_options) @@ -68,10 +69,11 @@ def get_config(self, config_path): raise errors.FigFileNotFound(os.path.basename(e.filename)) raise errors.UserError(six.text_type(e)) - def get_project(self, config_path, project_name=None, verbose=False): + def get_project(self, config_path, project_name=None, repo_name='', verbose=False): try: return Project.from_config( self.get_project_name(config_path, project_name), + repo_name, self.get_config(config_path), self.get_client(verbose=verbose)) except ConfigError as e: diff --git a/fig/cli/main.py b/fig/cli/main.py index eb19d46d8a4..c8a349ed023 100644 --- a/fig/cli/main.py +++ b/fig/cli/main.py @@ -74,13 +74,15 @@ class TopLevelCommand(Command): fig -h|--help Options: - --verbose Show more output - --version Print version and exit - -f, --file FILE Specify an alternate fig file (default: fig.yml) - -p, --project-name NAME Specify an alternate project name (default: directory name) + --verbose Show more output + --version Print version and exit + -f, --file FILE Specify an alternate fig file (default: fig.yml) + -p, --project-name NAME Specify an alternate project name (default: directory name) + -r, --repository-name NAME Specify an alternate project name (default: none) Commands: build Build or rebuild services + push Push built services help Get help on a command kill Kill containers logs View output from containers @@ -102,7 +104,7 @@ def build(self, project, options): """ Build or rebuild services. - Services are built once and then tagged as `project_service`, + Services are built once and then tagged as `repository/project_service`, e.g. `figtest_db`. If you change a service's `Dockerfile` or the contents of its build directory, you can run `fig build` to rebuild it. @@ -114,6 +116,19 @@ def build(self, project, options): no_cache = bool(options.get('--no-cache', False)) project.build(service_names=options['SERVICE'], no_cache=no_cache) + def push(self, project, options): + """ + Push built services. + + Services which have been built & tagged are pushed to repo `repository/project_service`, + e.g. `myrepo/figtest_db`. If you change a service's `Dockerfile` or the + contents of its build directory, run `fig build` to rebuild and then `fig push` to publish + + Usage: push [SERVICE...] + + """ + project.push(service_names=options['SERVICE']) + def help(self, project, options): """ Get help on a command. diff --git a/fig/project.py b/fig/project.py index d0c556c487c..19744bf6480 100644 --- a/fig/project.py +++ b/fig/project.py @@ -44,33 +44,33 @@ class Project(object): """ A collection of services. """ - def __init__(self, name, services, client): + def __init__(self, name, repo_name, services, client): self.name = name + self.repo_name = repo_name self.services = services self.client = client @classmethod - def from_dicts(cls, name, service_dicts, client): + def from_dicts(cls, name, repo_name, service_dicts, client): """ Construct a ServiceCollection from a list of dicts representing services. """ - project = cls(name, [], client) + project = cls(name, repo_name, [], client) for service_dict in sort_service_dicts(service_dicts): links = project.get_links(service_dict) volumes_from = project.get_volumes_from(service_dict) - - project.services.append(Service(client=client, project=name, links=links, volumes_from=volumes_from, **service_dict)) + project.services.append(Service(client=client, project=name, repository=repo_name, links=links, volumes_from=volumes_from, **service_dict)) return project @classmethod - def from_config(cls, name, config, client): + def from_config(cls, name, repo_name, config, client): dicts = [] for service_name, service in list(config.items()): if not isinstance(service, dict): raise ConfigurationError('Service "%s" doesn\'t have any configuration options. All top level keys in your fig.yml must map to a dictionary of configuration options.') service['name'] = service_name dicts.append(service) - return cls.from_dicts(name, dicts, client) + return cls.from_dicts(name, repo_name, dicts, client) def get_service(self, name): """ @@ -163,6 +163,13 @@ def build(self, service_names=None, no_cache=False): else: log.info('%s uses an image, skipping' % service.name) + def push(self, service_names=None): + for service in self.get_services(service_names): + if service.can_be_built(): + service.push() + else: + log.info('%s uses an image, skipping' % service.name) + def up(self, service_names=None, start_links=True, recreate=True): running_containers = [] diff --git a/fig/service.py b/fig/service.py index 48f63e6a5b2..07bc9ec24db 100644 --- a/fig/service.py +++ b/fig/service.py @@ -30,6 +30,10 @@ def __init__(self, service, reason): self.service = service self.reason = reason +class PublishError(Exception): + def __init__(self, service, reason): + self.service = service + self.reason = reason class CannotBeScaledError(Exception): pass @@ -40,11 +44,14 @@ class ConfigError(ValueError): class Service(object): - def __init__(self, name, client=None, project='default', links=None, volumes_from=None, **options): + def __init__(self, name, client=None, project='default', repository='', links=None, volumes_from=None, **options): if not re.match('^%s+$' % VALID_NAME_CHARS, name): raise ConfigError('Invalid service name "%s" - only %s are allowed' % (name, VALID_NAME_CHARS)) if not re.match('^%s+$' % VALID_NAME_CHARS, project): raise ConfigError('Invalid project name "%s" - only %s are allowed' % (project, VALID_NAME_CHARS)) + if repository: + if not re.match('^%s+$' % VALID_NAME_CHARS, repository): + raise ConfigError('Invalid repository name "%s" - only %s are allowed' % (repository, VALID_NAME_CHARS)) if 'image' in options and 'build' in options: raise ConfigError('Service %s has both an image and build path specified. A service can either be built to image or use an existing image, not both.' % name) @@ -58,6 +65,7 @@ def __init__(self, name, client=None, project='default', links=None, volumes_fro raise ConfigError(msg) self.name = name + self.repository = repository self.client = client self.project = project self.links = links or [] @@ -386,6 +394,32 @@ def build(self, no_cache=False): return image_id + def push(self): + log.info('Pushing %s...' % self.name) + + push_output = self.client.push( + self._build_tag_name(), + stream=True + ) + + try: + all_events = stream_output(push_output, sys.stdout) + except StreamOutputError, e: + raise PublishError(self, unicode(e)) + + success = False + + for event in all_events: + if 'status' in event: + match = re.search(r'Pushing tag for rev', event.get('status', '')) + if match: + success = True + + if success is False: + raise PublishError(self) + + return True + def can_be_built(self): return 'build' in self.options @@ -393,7 +427,10 @@ def _build_tag_name(self): """ The tag to give to images built for this service. """ - return '%s_%s' % (self.project, self.name) + if self.repository: + return '%s/%s_%s' % (self.repository, self.project, self.name) + else: + return '%s_%s' % (self.project, self.name) def can_be_scaled(self): for port in self.options.get('ports', []):