Compare commits

...

27 Commits
0.5.0 ... 0.5.1

Author SHA1 Message Date
Ben Firshman
0021a06468 Merge pull request #321 from orchardup/update-install-docs-for-0.5.1
Update install docs for 0.5.1
2014-07-15 00:05:13 +01:00
Aanand Prasad
6ead40e14c Update install docs for 0.5.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 16:01:41 -07:00
Aanand Prasad
5e2308d14a CHANGES.md credits for 0.5.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 15:58:26 -07:00
Ben Firshman
9ab8f358ca Merge pull request #315 from orchardup/ship-0.5.1
Ship 0.5.1
2014-07-14 23:22:48 +01:00
Aanand Prasad
d9c9b5e1f0 Ship 0.5.1
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 13:36:32 -07:00
Aanand Prasad
730de9187a Merge pull request #320 from orchardup/add-egg-info-to-clean
Add fig.egg-info to clean script
2014-07-14 13:35:22 -07:00
Ben Firshman
bad0f45816 Merge pull request #319 from orchardup/fix-project-name-regression
Fix regression of default behaviour in Command.project_name
2014-07-14 20:31:20 +01:00
Ben Firshman
190ea2bbd6 Add fig.egg-info to clean script
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-14 11:34:17 -07:00
Aanand Prasad
91fe414522 Fix regression of default behaviour in Command.project_name
Needed an `os.abspath` in there. Added more tests, too.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-14 11:32:10 -07:00
Ben Firshman
7fb43cc85f Merge pull request #283 from d11wtq/feature/optional-command
Make fig run COMMAND parameter optional.
2014-07-14 18:33:18 +01:00
Ben Firshman
d6657ed16c Merge pull request #317 from ryanbrainard/project-name-dirname
Set default project name to dir name of fig.yml
2014-07-14 18:26:43 +01:00
Ben Firshman
48e7c86d66 Merge pull request #316 from orchardup/restructure-yaml-docs
Improve fig.yml docs
2014-07-14 18:24:50 +01:00
Ryan Brainard
e9c2f2c5fb Default project_name to dirname of fig.yml
Signed-off-by: Ryan Brainard <brainard@heroku.com>
2014-07-12 23:38:20 -07:00
Aanand Prasad
197fd77b99 Improve some of the .yml options docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-12 09:55:13 -07:00
Aanand Prasad
36bef254ff Restructure .yml docs
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-12 09:47:43 -07:00
Aanand Prasad
8e265905d3 Add host-in-port-mapping feature to docs and changelog
Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-12 09:38:18 -07:00
Chris Corbyn
ef2fb77c1d Make fig run COMMAND parameter optional.
This behaves more like the native docker client, where the absence of a
command means docker runs the CMD in the Dockerfile. If a command is
defined in fig.yml this is used instead.

Signed-off-by: Chris Corbyn <chris@w3style.co.uk>
2014-07-12 02:30:34 +00:00
Ben Firshman
dd5c2e8767 Merge pull request #313 from orchardup/fix-volumes-from-container
Fix volumes_from container
2014-07-12 01:10:45 +01:00
Aanand Prasad
c255999fce Merge pull request #312 from orchardup/fix-default-values-for-service-args
Remove empty lists from default Service args
2014-07-11 17:06:55 -07:00
Aanand Prasad
89341013a0 Fix volumes_from container
Closes #311.

Signed-off-by: Aanand Prasad <aanand.prasad@gmail.com>
2014-07-11 16:39:23 -07:00
Ben Firshman
f983110492 Remove empty lists from default Service args
This will cause terrifying bugs.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 16:07:31 -07:00
Aanand Prasad
9fd296f416 Merge pull request #310 from orchardup/fix-race-condition-in-recreate-containers
Fix race condition in recreate containers
2014-07-11 15:14:17 -07:00
Ben Firshman
bb89f85984 Remove stop timeout in recreate containers
This doesn't seem like a good idea – 1 second isn't enough time
to stop gracefully.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:53:56 -07:00
Ben Firshman
b573b87a92 Fix race condition in recreate containers
Container might have stopped between checking `is_running` and
calling `stop()`, which then threw an exception.

Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:53:46 -07:00
Aanand Prasad
036adb2de9 Merge pull request #309 from orchardup/update-docs-to-be-0.5.0
Update docs to be 0.5.0
2014-07-11 14:29:38 -07:00
Ben Firshman
36f4e30dba Add 0.5.0 to docs
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:16:39 -07:00
Ben Firshman
9f0cfbdfd2 Document release process
Signed-off-by: Ben Firshman <ben@firshman.co.uk>
2014-07-11 14:16:29 -07:00
15 changed files with 265 additions and 64 deletions

View File

@@ -1,6 +1,17 @@
Change log
==========
0.5.1 (2014-07-11)
------------------
- If a service has a command defined, `fig run [service]` with no further arguments will run it.
- The project name now defaults to the directory containing fig.yml, not the current working directory (if they're different)
- `volumes_from` now works properly with containers as well as services
- Fixed a race condition when recreating containers in `fig up`
Thanks @ryanbrainard and @d11wtq!
0.5.0 (2014-07-11)
------------------
@@ -23,12 +34,20 @@ Change log
- container_name
```
- A host address can now be specified in `ports`:
```
ports:
- "0.0.0.0:8000:8000"
- "127.0.0.1:8001:8001"
```
- The `net` and `workdir` options are now supported in `fig.yml`.
- The `hostname` option now works in the same way as the Docker CLI, splitting out into a `domainname` option.
- TTY behaviour is far more robust, and resizes are supported correctly.
- Load YAML files safely.
Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca and @mozz100 for their help with this release!
Thanks to @d11wtq, @ryanbrainard, @rail44, @j0hnsmith, @binarin, @Elemecca, @mozz100 and @marksteve for their help with this release!
0.4.2 (2014-06-18)

View File

@@ -73,3 +73,21 @@ The easiest way to do this is to use the `--signoff` flag when committing. E.g.:
$ git commit --signoff
## Release process
1. Open pull request that:
- Updates version in `fig/__init__.py`
- Updates version in `docs/install.md`
- Adds release notes to `CHANGES.md`
2. Create unpublished GitHub release with release notes
3. Build Linux version on any Docker host with `script/build-linux` and attach to release
4. Build OS X version on Mountain Lion with `script/build-osx` and attach to release
5. Publish GitHub release, creating tag

View File

@@ -16,12 +16,12 @@ Docker has guides for [Ubuntu](http://docs.docker.io/en/latest/installation/ubun
Next, install Fig. On OS X:
$ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/darwin > /usr/local/bin/fig
$ curl -L https://github.com/orchardup/fig/releases/download/0.5.1/darwin > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
On 64-bit Linux:
$ curl -L https://github.com/orchardup/fig/releases/download/0.4.2/linux > /usr/local/bin/fig
$ curl -L https://github.com/orchardup/fig/releases/download/0.5.1/linux > /usr/local/bin/fig
$ chmod +x /usr/local/bin/fig
Fig is also available as a Python package if you're on another platform (or if you prefer that sort of thing):

View File

@@ -10,62 +10,111 @@ Each service defined in `fig.yml` must specify exactly one of `image` or `build`
As with `docker run`, options specified in the Dockerfile (e.g. `CMD`, `EXPOSE`, `VOLUME`, `ENV`) are respected by default - you don't need to specify them again in `fig.yml`.
```yaml
-- Tag or partial image ID. Can be local or remote - Fig will attempt to pull
-- if it doesn't exist locally.
###image
Tag or partial image ID. Can be local or remote - Fig will attempt to pull if it doesn't exist locally.
```
image: ubuntu
image: orchardup/postgresql
image: a4bc65fd
```
-- Path to a directory containing a Dockerfile. Fig will build and tag it with
-- a generated name, and use that image thereafter.
### build
Path to a directory containing a Dockerfile. Fig will build and tag it with a generated name, and use that image thereafter.
```
build: /path/to/build/dir
```
-- Override the default command.
### command
Override the default command.
```
command: bundle exec thin -p 3000
```
-- Link to containers in another service. Optionally specify an alternate name
-- for the link, which will determine how environment variables are prefixed,
-- e.g. "db" -> DB_1_PORT, "db:database" -> DATABASE_1_PORT
### links
Link to containers in another service. Optionally specify an alternate name for the link, which will determine how environment variables are prefixed, e.g. `db` -> `DB_1_PORT`, `db:database` -> `DATABASE_1_PORT`
```
links:
- db
- db:database
- redis
```
-- Expose ports. Either specify both ports (HOST:CONTAINER), or just the
-- container port (a random host port will be chosen).
-- Note: When mapping ports in the HOST:CONTAINER format, you may experience
-- erroneous results when using a container port lower than 60, because YAML
-- will parse numbers in the format "xx:yy" as sexagesimal (base 60). For
-- this reason, we recommend always explicitly specifying your port mappings
-- as strings.
### ports
Expose ports. Either specify both ports (`HOST:CONTAINER`), or just the container port (a random host port will be chosen).
**Note:** When mapping ports in the `HOST:CONTAINER` format, you may experience erroneous results when using a container port lower than 60, because YAML will parse numbers in the format `xx:yy` as sexagesimal (base 60). For this reason, we recommend always explicitly specifying your port mappings as strings.
```
ports:
- "3000"
- "8000:8000"
- "49100:22"
- "127.0.0.1:8001:8001"
```
-- Expose ports without publishing them to the host machine - they'll only be
-- accessible to linked services. Only the internal port can be specified.
### expose
Expose ports without publishing them to the host machine - they'll only be accessible to linked services. Only the internal port can be specified.
```
expose:
- "3000"
- "8000"
```
-- Map volumes from the host machine (HOST:CONTAINER).
### volumes
Mount paths as volumes, optionally specifying a path on the host machine (`HOST:CONTAINER`).
```
volumes:
- /var/lib/mysql
- cache/:/tmp/cache
```
-- Mount all of the volumes from another service or container
### volumes_from
Mount all of the volumes from another service or container.
```
volumes_from:
- service_name
- container_name
```
-- Add environment variables.
-- Environment variables with only a key are resolved to values on the host
-- machine, which can be helpful for secret or host-specific values.
### environment
Add environment variables. You can use either an array or a dictionary.
Environment variables with only a key are resolved to their values on the machine Fig is running on, which can be helpful for secret or host-specific values.
```
environment:
RACK_ENV: development
SESSION_SECRET:
environment:
- RACK_ENV=development
- SESSION_SECRET
```
-- Networking mode. Use the same values as the docker client --net parameter
### net
Networking mode. Use the same values as the docker client `--net` parameter.
```
net: "bridge"
net: "none"
net: "container:[name or id]"
net: "host"
```

View File

@@ -1,4 +1,4 @@
from __future__ import unicode_literals
from .service import Service
__version__ = '0.5.0'
__version__ = '0.5.1'

View File

@@ -23,7 +23,7 @@ class Command(DocoptCommand):
base_dir = '.'
def __init__(self):
self.yaml_path = os.environ.get('FIG_FILE', None)
self._yaml_path = os.environ.get('FIG_FILE', None)
self.explicit_project_name = None
def dispatch(self, *args, **kwargs):
@@ -56,10 +56,7 @@ class Command(DocoptCommand):
@cached_property
def project(self):
try:
yaml_path = self.yaml_path
if yaml_path is None:
yaml_path = self.check_yaml_filename()
config = yaml.safe_load(open(yaml_path))
config = yaml.safe_load(open(self.yaml_path))
except IOError as e:
if e.errno == errno.ENOENT:
raise errors.FigFileNotFound(os.path.basename(e.filename))
@@ -72,7 +69,7 @@ class Command(DocoptCommand):
@cached_property
def project_name(self):
project = os.path.basename(os.getcwd())
project = os.path.basename(os.path.dirname(os.path.abspath(self.yaml_path)))
if self.explicit_project_name is not None:
project = self.explicit_project_name
project = re.sub(r'[^a-zA-Z0-9]', '', project)
@@ -84,8 +81,11 @@ class Command(DocoptCommand):
def formatter(self):
return Formatter()
def check_yaml_filename(self):
if os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
@cached_property
def yaml_path(self):
if self._yaml_path is not None:
return self._yaml_path
elif os.path.exists(os.path.join(self.base_dir, 'fig.yaml')):
log.warning("Fig just read the file 'fig.yaml' on startup, rather than 'fig.yml'")
log.warning("Please be aware that fig.yml the expected extension in most cases, and using .yaml can cause compatibility issues in future")
@@ -93,3 +93,7 @@ class Command(DocoptCommand):
return os.path.join(self.base_dir, 'fig.yaml')
else:
return os.path.join(self.base_dir, 'fig.yml')
@yaml_path.setter
def yaml_path(self, value):
self._yaml_path = value

View File

@@ -206,7 +206,7 @@ class TopLevelCommand(Command):
running. If you do not want to start linked services, use
`fig run --no-deps SERVICE COMMAND [ARGS...]`.
Usage: run [options] SERVICE COMMAND [ARGS...]
Usage: run [options] SERVICE [COMMAND] [ARGS...]
Options:
-d Detached mode: Run container in the background, print
@@ -233,8 +233,13 @@ class TopLevelCommand(Command):
if options['-d'] or options['-T'] or not sys.stdin.isatty():
tty = False
if options['COMMAND']:
command = [options['COMMAND']] + options['ARGS']
else:
command = service.options.get('command')
container_options = {
'command': [options['COMMAND']] + options['ARGS'],
'command': command,
'tty': tty,
'stdin_open': not options['-d'],
}

View File

@@ -135,8 +135,8 @@ class Project(object):
volumes_from.append(service)
except NoSuchService:
try:
container = Container.from_id(client, volume_name)
volumes_from.append(Container.from_id(client, volume_name))
container = Container.from_id(self.client, volume_name)
volumes_from.append(container)
except APIError:
raise ConfigurationError('Service "%s" mounts volumes from "%s", which is not the name of a service or container.' % (service_dict['name'], volume_name))
del service_dict['volumes_from']

View File

@@ -40,7 +40,7 @@ class ConfigError(ValueError):
class Service(object):
def __init__(self, name, client=None, project='default', links=[], volumes_from=[], **options):
def __init__(self, name, client=None, project='default', 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):
@@ -177,8 +177,15 @@ class Service(object):
return tuples
def recreate_container(self, container, **override_options):
if container.is_running:
container.stop(timeout=1)
try:
container.stop()
except APIError as e:
if (e.response.status_code == 500
and e.explanation
and 'no such process' in str(e.explanation)):
pass
else:
raise
intermediate_container = Container.create(
self.client,

View File

@@ -1,3 +1,3 @@
#!/bin/sh
find . -type f -name '*.pyc' -delete
rm -rf docs/_site build dist
rm -rf docs/_site build dist fig.egg-info

View File

@@ -0,0 +1,5 @@
implicit:
image: figtest_test
explicit:
image: figtest_test
command: [ "/bin/true" ]

View File

@@ -22,7 +22,7 @@ class CLITestCase(DockerClientTestCase):
def test_ps(self, mock_stdout):
self.command.project.get_service('simple').create_container()
self.command.dispatch(['ps'], None)
self.assertIn('fig_simple_1', mock_stdout.getvalue())
self.assertIn('simplefigfile_simple_1', mock_stdout.getvalue())
@patch('sys.stdout', new_callable=StringIO)
def test_ps_default_figfile(self, mock_stdout):
@@ -31,9 +31,9 @@ class CLITestCase(DockerClientTestCase):
self.command.dispatch(['ps'], None)
output = mock_stdout.getvalue()
self.assertIn('fig_simple_1', output)
self.assertIn('fig_another_1', output)
self.assertNotIn('fig_yetanother_1', output)
self.assertIn('multiplefigfiles_simple_1', output)
self.assertIn('multiplefigfiles_another_1', output)
self.assertNotIn('multiplefigfiles_yetanother_1', output)
@patch('sys.stdout', new_callable=StringIO)
def test_ps_alternate_figfile(self, mock_stdout):
@@ -42,9 +42,9 @@ class CLITestCase(DockerClientTestCase):
self.command.dispatch(['-f', 'fig2.yml', 'ps'], None)
output = mock_stdout.getvalue()
self.assertNotIn('fig_simple_1', output)
self.assertNotIn('fig_another_1', output)
self.assertIn('fig_yetanother_1', output)
self.assertNotIn('multiplefigfiles_simple_1', output)
self.assertNotIn('multiplefigfiles_another_1', output)
self.assertIn('multiplefigfiles_yetanother_1', output)
def test_up(self):
self.command.dispatch(['up', '-d'], None)
@@ -109,7 +109,7 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(self.command.project.containers()), 0)
@patch('dockerpty.start')
def test_run_service_with_links(self, mock_stdout):
def test_run_service_with_links(self, __):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['run', 'web', '/bin/true'], None)
db = self.command.project.get_service('db')
@@ -118,18 +118,14 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(len(console.containers()), 0)
@patch('dockerpty.start')
def test_run_with_no_deps(self, mock_stdout):
mock_stdout.fileno = lambda: 1
def test_run_with_no_deps(self, __):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['run', '--no-deps', 'web', '/bin/true'], None)
db = self.command.project.get_service('db')
self.assertEqual(len(db.containers()), 0)
@patch('dockerpty.start')
def test_run_does_not_recreate_linked_containers(self, mock_stdout):
mock_stdout.fileno = lambda: 1
def test_run_does_not_recreate_linked_containers(self, __):
self.command.base_dir = 'tests/fixtures/links-figfile'
self.command.dispatch(['up', '-d', 'db'], None)
db = self.command.project.get_service('db')
@@ -144,6 +140,30 @@ class CLITestCase(DockerClientTestCase):
self.assertEqual(old_ids, new_ids)
@patch('dockerpty.start')
def test_run_without_command(self, __):
self.command.base_dir = 'tests/fixtures/commands-figfile'
self.client.build('tests/fixtures/simple-dockerfile', tag='figtest_test')
for c in self.command.project.containers(stopped=True, one_off=True):
c.remove()
self.command.dispatch(['run', 'implicit'], None)
service = self.command.project.get_service('implicit')
containers = service.containers(stopped=True, one_off=True)
self.assertEqual(
[c.human_readable_command for c in containers],
[u'/bin/sh -c echo "success"'],
)
self.command.dispatch(['run', 'explicit'], None)
service = self.command.project.get_service('explicit')
containers = service.containers(stopped=True, one_off=True)
self.assertEqual(
[c.human_readable_command for c in containers],
[u'/bin/true'],
)
def test_rm(self):
service = self.command.project.get_service('simple')
service.create_container()

View File

@@ -1,9 +1,49 @@
from __future__ import unicode_literals
from fig.project import Project, ConfigurationError
from fig.container import Container
from .testcases import DockerClientTestCase
class ProjectTest(DockerClientTestCase):
def test_volumes_from_service(self):
project = Project.from_config(
name='figtest',
config={
'data': {
'image': 'busybox:latest',
'volumes': ['/var/data'],
},
'db': {
'image': 'busybox:latest',
'volumes_from': ['data'],
},
},
client=self.client,
)
db = project.get_service('db')
data = project.get_service('data')
self.assertEqual(db.volumes_from, [data])
def test_volumes_from_container(self):
data_container = Container.create(
self.client,
image='busybox:latest',
volumes=['/var/data'],
name='figtest_data_container',
)
project = Project.from_config(
name='figtest',
config={
'db': {
'image': 'busybox:latest',
'volumes_from': ['figtest_data_container'],
},
},
client=self.client,
)
db = project.get_service('db')
self.assertEqual(db.volumes_from, [data_container])
def test_start_stop_kill_remove(self):
web = self.create_service('web')
db = self.create_service('db')

View File

@@ -113,12 +113,12 @@ class ServiceTest(DockerClientTestCase):
'db',
environment={'FOO': '1'},
volumes=['/var/db'],
entrypoint=['ps'],
command=['ax']
entrypoint=['sleep'],
command=['300']
)
old_container = service.create_container()
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['ax'])
self.assertEqual(old_container.dictionary['Config']['Entrypoint'], ['sleep'])
self.assertEqual(old_container.dictionary['Config']['Cmd'], ['300'])
self.assertIn('FOO=1', old_container.dictionary['Config']['Env'])
self.assertEqual(old_container.name, 'figtest_db_1')
service.start_container(old_container)
@@ -134,8 +134,8 @@ class ServiceTest(DockerClientTestCase):
new_container = tuples[0][1]
self.assertEqual(intermediate_container.dictionary['Config']['Entrypoint'], ['echo'])
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['ps'])
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['ax'])
self.assertEqual(new_container.dictionary['Config']['Entrypoint'], ['sleep'])
self.assertEqual(new_container.dictionary['Config']['Cmd'], ['300'])
self.assertIn('FOO=2', new_container.dictionary['Config']['Env'])
self.assertEqual(new_container.name, 'figtest_db_1')
self.assertEqual(new_container.inspect()['Volumes']['/var/db'], volume_path)
@@ -145,6 +145,19 @@ class ServiceTest(DockerClientTestCase):
self.assertNotEqual(old_container.id, new_container.id)
self.assertRaises(APIError, lambda: self.client.inspect_container(intermediate_container.id))
def test_recreate_containers_when_containers_are_stopped(self):
service = self.create_service(
'db',
environment={'FOO': '1'},
volumes=['/var/db'],
entrypoint=['sleep'],
command=['300']
)
old_container = service.create_container()
self.assertEqual(len(service.containers(stopped=True)), 1)
service.recreate_containers()
self.assertEqual(len(service.containers(stopped=True)), 1)
def test_start_container_passes_through_options(self):
db = self.create_service('db')
db.start_container(environment={'FOO': 'BAR'})

View File

@@ -3,8 +3,29 @@ from __future__ import absolute_import
from .. import unittest
from fig.cli.main import TopLevelCommand
from fig.packages.six import StringIO
import os
class CLITestCase(unittest.TestCase):
def test_default_project_name(self):
cwd = os.getcwd()
try:
os.chdir('tests/fixtures/simple-figfile')
command = TopLevelCommand()
self.assertEquals('simplefigfile', command.project_name)
finally:
os.chdir(cwd)
def test_project_name_with_explicit_base_dir(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/simple-figfile'
self.assertEquals('simplefigfile', command.project_name)
def test_project_name_with_explicit_project_name(self):
command = TopLevelCommand()
command.explicit_project_name = 'explicit-project-name'
self.assertEquals('explicitprojectname', command.project_name)
def test_yaml_filename_check(self):
command = TopLevelCommand()
command.base_dir = 'tests/fixtures/longer-filename-figfile'