Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion fig/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
25 changes: 20 additions & 5 deletions fig/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -105,7 +107,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.

Expand All @@ -117,6 +119,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.
Expand Down
21 changes: 14 additions & 7 deletions fig/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -167,6 +167,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 = []

Expand Down
41 changes: 39 additions & 2 deletions fig/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,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
Expand All @@ -44,11 +48,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)

Expand All @@ -62,6 +69,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 []
Expand Down Expand Up @@ -395,14 +403,43 @@ 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

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)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a bunch of name parsing code that expects fig images to always be in the form , does this pass the integration tests? Does it cleanup all the images afterward?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can revert the naming scheme change to just use the existing fig style. Will this be enough to avoid conflicts with your PR? Instead, push could filter for services that define both a "build" AND "image" property. This would allow a more descriptive image since the image name here would be decoupled from the fig service name. The repo could also be specified here. It would also make it possible to limit which build you actually care about pushing (since it would only push builds that also have an image property defined). Does this sound better? other suggestions?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think using the existing naming scheme will be enough to avoid merge conflicts, yes.

I think the decoupled image name you're talking about is the same as the tags from #457?

I think specifying both build and image maybe be confusing, because you'd be re-using image to mean two things (either source image or destination image) based on the presence of another field. Right now specifying both raises an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to avoid confusion it sounds like it would be best to just have a separate field to track images want to be pushed. Maybe this command is not even needed. It basically reduces to a shortcut around "docker push" for a group of images that each play a role in the current fig file.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was thinking that the tags field from #457 would be that extra field. That way docker push could push all of the tags.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense to me. I'll wait for #457 to land and then I'll update.


def can_be_scaled(self):
for port in self.options.get('ports', []):
Expand Down