bundle.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. from __future__ import absolute_import
  2. from __future__ import unicode_literals
  3. import json
  4. import logging
  5. import six
  6. from docker.utils.ports import split_port
  7. from .cli.errors import UserError
  8. from .config.serialize import denormalize_config
  9. from .network import get_network_defs_for_service
  10. from .service import NoSuchImageError
  11. from .service import parse_repository_tag
  12. log = logging.getLogger(__name__)
  13. SERVICE_KEYS = {
  14. 'command': 'Command',
  15. 'environment': 'Env',
  16. 'working_dir': 'WorkingDir',
  17. }
  18. VERSION = '0.1'
  19. def serialize_bundle(config, image_digests):
  20. if config.networks:
  21. log.warn("Unsupported top level key 'networks' - ignoring")
  22. if config.volumes:
  23. log.warn("Unsupported top level key 'volumes' - ignoring")
  24. return json.dumps(
  25. to_bundle(config, image_digests),
  26. indent=2,
  27. sort_keys=True,
  28. )
  29. def get_image_digests(project):
  30. return {
  31. service.name: get_image_digest(service)
  32. for service in project.services
  33. }
  34. def get_image_digest(service):
  35. if 'image' not in service.options:
  36. raise UserError(
  37. "Service '{s.name}' doesn't define an image tag. An image name is "
  38. "required to generate a proper image digest for the bundle. Specify "
  39. "an image repo and tag with the 'image' option.".format(s=service))
  40. repo, tag, separator = parse_repository_tag(service.options['image'])
  41. # Compose file already uses a digest, no lookup required
  42. if separator == '@':
  43. return service.options['image']
  44. try:
  45. image = service.image()
  46. except NoSuchImageError:
  47. action = 'build' if 'build' in service.options else 'pull'
  48. raise UserError(
  49. "Image not found for service '{service}'. "
  50. "You might need to run `docker-compose {action} {service}`."
  51. .format(service=service.name, action=action))
  52. if image['RepoDigests']:
  53. # TODO: pick a digest based on the image tag if there are multiple
  54. # digests
  55. return image['RepoDigests'][0]
  56. if 'build' not in service.options:
  57. log.warn(
  58. "Compose needs to pull the image for '{s.name}' in order to create "
  59. "a bundle. This may result in a more recent image being used. "
  60. "It is recommended that you use an image tagged with a "
  61. "specific version to minimize the potential "
  62. "differences.".format(s=service))
  63. digest = service.pull()
  64. else:
  65. try:
  66. digest = service.push()
  67. except:
  68. log.error(
  69. "Failed to push image for service '{s.name}'. Please use an "
  70. "image tag that can be pushed to a Docker "
  71. "registry.".format(s=service))
  72. raise
  73. if not digest:
  74. raise ValueError("Failed to get digest for %s" % service.name)
  75. identifier = '{repo}@{digest}'.format(repo=repo, digest=digest)
  76. # Pull by digest so that image['RepoDigests'] is populated for next time
  77. # and we don't have to pull/push again
  78. service.client.pull(identifier)
  79. return identifier
  80. def to_bundle(config, image_digests):
  81. config = denormalize_config(config)
  82. return {
  83. 'version': VERSION,
  84. 'services': {
  85. name: convert_service_to_bundle(
  86. name,
  87. service_dict,
  88. image_digests[name],
  89. )
  90. for name, service_dict in config['services'].items()
  91. },
  92. }
  93. def convert_service_to_bundle(name, service_dict, image_id):
  94. container_config = {'Image': image_id}
  95. for key, value in service_dict.items():
  96. if key in ('build', 'image', 'ports', 'expose', 'networks'):
  97. pass
  98. elif key == 'environment':
  99. container_config['env'] = {
  100. envkey: envvalue for envkey, envvalue in value.items()
  101. if envvalue
  102. }
  103. elif key in SERVICE_KEYS:
  104. container_config[SERVICE_KEYS[key]] = value
  105. else:
  106. log.warn("Unsupported key '{}' in services.{} - ignoring".format(key, name))
  107. container_config['Networks'] = make_service_networks(name, service_dict)
  108. ports = make_port_specs(service_dict)
  109. if ports:
  110. container_config['Ports'] = ports
  111. return container_config
  112. def make_service_networks(name, service_dict):
  113. networks = []
  114. for network_name, network_def in get_network_defs_for_service(service_dict).items():
  115. for key in network_def.keys():
  116. log.warn(
  117. "Unsupported key '{}' in services.{}.networks.{} - ignoring"
  118. .format(key, name, network_name))
  119. networks.append(network_name)
  120. return networks
  121. def make_port_specs(service_dict):
  122. ports = []
  123. internal_ports = [
  124. internal_port
  125. for port_def in service_dict.get('ports', [])
  126. for internal_port in split_port(port_def)[0]
  127. ]
  128. internal_ports += service_dict.get('expose', [])
  129. for internal_port in internal_ports:
  130. spec = make_port_spec(internal_port)
  131. if spec not in ports:
  132. ports.append(spec)
  133. return ports
  134. def make_port_spec(value):
  135. components = six.text_type(value).partition('/')
  136. return {
  137. 'Protocol': components[2] or 'tcp',
  138. 'Port': int(components[0]),
  139. }