bundle.py 6.1 KB

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