migrate-compose-file-v1-to-v2.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. #!/usr/bin/env python
  2. """
  3. Migrate a Compose file from the V1 format in Compose 1.5 to the V2 format
  4. supported by Compose 1.6+
  5. """
  6. import argparse
  7. import logging
  8. import sys
  9. import ruamel.yaml
  10. from compose.config.types import VolumeSpec
  11. log = logging.getLogger('migrate')
  12. def migrate(content):
  13. data = ruamel.yaml.load(content, ruamel.yaml.RoundTripLoader)
  14. service_names = data.keys()
  15. for name, service in data.items():
  16. warn_for_links(name, service)
  17. warn_for_external_links(name, service)
  18. rewrite_net(service, service_names)
  19. rewrite_build(service)
  20. rewrite_logging(service)
  21. rewrite_volumes_from(service, service_names)
  22. services = {name: data.pop(name) for name in data.keys()}
  23. data['version'] = "2"
  24. data['services'] = services
  25. create_volumes_section(data)
  26. return data
  27. def warn_for_links(name, service):
  28. links = service.get('links')
  29. if links:
  30. example_service = links[0].partition(':')[0]
  31. log.warning(
  32. "Service {name} has links, which no longer create environment "
  33. "variables such as {example_service_upper}_PORT. "
  34. "If you are using those in your application code, you should "
  35. "instead connect directly to the hostname, e.g. "
  36. "'{example_service}'."
  37. .format(name=name, example_service=example_service,
  38. example_service_upper=example_service.upper()))
  39. def warn_for_external_links(name, service):
  40. external_links = service.get('external_links')
  41. if external_links:
  42. log.warning(
  43. "Service {name} has external_links: {ext}, which now work "
  44. "slightly differently. In particular, two containers must be "
  45. "connected to at least one network in common in order to "
  46. "communicate, even if explicitly linked together.\n\n"
  47. "Either connect the external container to your app's default "
  48. "network, or connect both the external container and your "
  49. "service's containers to a pre-existing network. See "
  50. "https://docs.docker.com/compose/networking/ "
  51. "for more on how to do this."
  52. .format(name=name, ext=external_links))
  53. def rewrite_net(service, service_names):
  54. if 'net' in service:
  55. network_mode = service.pop('net')
  56. # "container:<service name>" is now "service:<service name>"
  57. if network_mode.startswith('container:'):
  58. name = network_mode.partition(':')[2]
  59. if name in service_names:
  60. network_mode = 'service:{}'.format(name)
  61. service['network_mode'] = network_mode
  62. def rewrite_build(service):
  63. if 'dockerfile' in service:
  64. service['build'] = {
  65. 'context': service.pop('build'),
  66. 'dockerfile': service.pop('dockerfile'),
  67. }
  68. def rewrite_logging(service):
  69. if 'log_driver' in service:
  70. service['logging'] = {'driver': service.pop('log_driver')}
  71. if 'log_opt' in service:
  72. service['logging']['options'] = service.pop('log_opt')
  73. def rewrite_volumes_from(service, service_names):
  74. for idx, volume_from in enumerate(service.get('volumes_from', [])):
  75. if volume_from.split(':', 1)[0] not in service_names:
  76. service['volumes_from'][idx] = 'container:%s' % volume_from
  77. def create_volumes_section(data):
  78. named_volumes = get_named_volumes(data['services'])
  79. if named_volumes:
  80. log.warning(
  81. "Named volumes ({names}) must be explicitly declared. Creating a "
  82. "'volumes' section with declarations.\n\n"
  83. "For backwards-compatibility, they've been declared as external. "
  84. "If you don't mind the volume names being prefixed with the "
  85. "project name, you can remove the 'external' option from each one."
  86. .format(names=', '.join(list(named_volumes))))
  87. data['volumes'] = named_volumes
  88. def get_named_volumes(services):
  89. volume_specs = [
  90. VolumeSpec.parse(volume)
  91. for service in services.values()
  92. for volume in service.get('volumes', [])
  93. ]
  94. names = {
  95. spec.external
  96. for spec in volume_specs
  97. if spec.is_named_volume
  98. }
  99. return {name: {'external': True} for name in names}
  100. def write(stream, new_format, indent, width):
  101. ruamel.yaml.dump(
  102. new_format,
  103. stream,
  104. Dumper=ruamel.yaml.RoundTripDumper,
  105. indent=indent,
  106. width=width)
  107. def parse_opts(args):
  108. parser = argparse.ArgumentParser()
  109. parser.add_argument("filename", help="Compose file filename.")
  110. parser.add_argument("-i", "--in-place", action='store_true')
  111. parser.add_argument(
  112. "--indent", type=int, default=2,
  113. help="Number of spaces used to indent the output yaml.")
  114. parser.add_argument(
  115. "--width", type=int, default=80,
  116. help="Number of spaces used as the output width.")
  117. return parser.parse_args()
  118. def main(args):
  119. logging.basicConfig(format='\033[33m%(levelname)s:\033[37m %(message)s\033[0m\n')
  120. opts = parse_opts(args)
  121. with open(opts.filename, 'r') as fh:
  122. new_format = migrate(fh.read())
  123. if opts.in_place:
  124. output = open(opts.filename, 'w')
  125. else:
  126. output = sys.stdout
  127. write(output, new_format, opts.indent, opts.width)
  128. if __name__ == "__main__":
  129. main(sys.argv)