mirror of https://github.com/docker/compose.git
174 lines
5.4 KiB
Python
Executable File
174 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python
|
|
"""
|
|
Migrate a Compose file from the V1 format in Compose 1.5 to the V2 format
|
|
supported by Compose 1.6+
|
|
"""
|
|
from __future__ import absolute_import
|
|
from __future__ import unicode_literals
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
|
|
import ruamel.yaml
|
|
|
|
from compose.config.types import VolumeSpec
|
|
|
|
|
|
log = logging.getLogger('migrate')
|
|
|
|
|
|
def migrate(content):
|
|
data = ruamel.yaml.load(content, ruamel.yaml.RoundTripLoader)
|
|
|
|
service_names = data.keys()
|
|
|
|
for name, service in data.items():
|
|
warn_for_links(name, service)
|
|
warn_for_external_links(name, service)
|
|
rewrite_net(service, service_names)
|
|
rewrite_build(service)
|
|
rewrite_logging(service)
|
|
rewrite_volumes_from(service, service_names)
|
|
|
|
services = {name: data.pop(name) for name in data.keys()}
|
|
|
|
data['version'] = "2"
|
|
data['services'] = services
|
|
create_volumes_section(data)
|
|
|
|
return data
|
|
|
|
|
|
def warn_for_links(name, service):
|
|
links = service.get('links')
|
|
if links:
|
|
example_service = links[0].partition(':')[0]
|
|
log.warning(
|
|
"Service {name} has links, which no longer create environment "
|
|
"variables such as {example_service_upper}_PORT. "
|
|
"If you are using those in your application code, you should "
|
|
"instead connect directly to the hostname, e.g. "
|
|
"'{example_service}'."
|
|
.format(name=name, example_service=example_service,
|
|
example_service_upper=example_service.upper()))
|
|
|
|
|
|
def warn_for_external_links(name, service):
|
|
external_links = service.get('external_links')
|
|
if external_links:
|
|
log.warning(
|
|
"Service {name} has external_links: {ext}, which now work "
|
|
"slightly differently. In particular, two containers must be "
|
|
"connected to at least one network in common in order to "
|
|
"communicate, even if explicitly linked together.\n\n"
|
|
"Either connect the external container to your app's default "
|
|
"network, or connect both the external container and your "
|
|
"service's containers to a pre-existing network. See "
|
|
"https://docs.docker.com/compose/networking/ "
|
|
"for more on how to do this."
|
|
.format(name=name, ext=external_links))
|
|
|
|
|
|
def rewrite_net(service, service_names):
|
|
if 'net' in service:
|
|
network_mode = service.pop('net')
|
|
|
|
# "container:<service name>" is now "service:<service name>"
|
|
if network_mode.startswith('container:'):
|
|
name = network_mode.partition(':')[2]
|
|
if name in service_names:
|
|
network_mode = 'service:{}'.format(name)
|
|
|
|
service['network_mode'] = network_mode
|
|
|
|
|
|
def rewrite_build(service):
|
|
if 'dockerfile' in service:
|
|
service['build'] = {
|
|
'context': service.pop('build'),
|
|
'dockerfile': service.pop('dockerfile'),
|
|
}
|
|
|
|
|
|
def rewrite_logging(service):
|
|
if 'log_driver' in service:
|
|
service['logging'] = {'driver': service.pop('log_driver')}
|
|
if 'log_opt' in service:
|
|
service['logging']['options'] = service.pop('log_opt')
|
|
|
|
|
|
def rewrite_volumes_from(service, service_names):
|
|
for idx, volume_from in enumerate(service.get('volumes_from', [])):
|
|
if volume_from.split(':', 1)[0] not in service_names:
|
|
service['volumes_from'][idx] = 'container:%s' % volume_from
|
|
|
|
|
|
def create_volumes_section(data):
|
|
named_volumes = get_named_volumes(data['services'])
|
|
if named_volumes:
|
|
log.warning(
|
|
"Named volumes ({names}) must be explicitly declared. Creating a "
|
|
"'volumes' section with declarations.\n\n"
|
|
"For backwards-compatibility, they've been declared as external. "
|
|
"If you don't mind the volume names being prefixed with the "
|
|
"project name, you can remove the 'external' option from each one."
|
|
.format(names=', '.join(list(named_volumes))))
|
|
|
|
data['volumes'] = named_volumes
|
|
|
|
|
|
def get_named_volumes(services):
|
|
volume_specs = [
|
|
VolumeSpec.parse(volume)
|
|
for service in services.values()
|
|
for volume in service.get('volumes', [])
|
|
]
|
|
names = {
|
|
spec.external
|
|
for spec in volume_specs
|
|
if spec.is_named_volume
|
|
}
|
|
return {name: {'external': True} for name in names}
|
|
|
|
|
|
def write(stream, new_format, indent, width):
|
|
ruamel.yaml.dump(
|
|
new_format,
|
|
stream,
|
|
Dumper=ruamel.yaml.RoundTripDumper,
|
|
indent=indent,
|
|
width=width)
|
|
|
|
|
|
def parse_opts(args):
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("filename", help="Compose file filename.")
|
|
parser.add_argument("-i", "--in-place", action='store_true')
|
|
parser.add_argument(
|
|
"--indent", type=int, default=2,
|
|
help="Number of spaces used to indent the output yaml.")
|
|
parser.add_argument(
|
|
"--width", type=int, default=80,
|
|
help="Number of spaces used as the output width.")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main(args):
|
|
logging.basicConfig(format='\033[33m%(levelname)s:\033[37m %(message)s\033[0m\n')
|
|
|
|
opts = parse_opts(args)
|
|
|
|
with open(opts.filename, 'r') as fh:
|
|
new_format = migrate(fh.read())
|
|
|
|
if opts.in_place:
|
|
output = open(opts.filename, 'w')
|
|
else:
|
|
output = sys.stdout
|
|
write(output, new_format, opts.indent, opts.width)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv)
|