mirror of
https://github.com/Red5d/docker-autocompose
synced 2026-03-05 15:38:45 +00:00
Compare commits
22 Commits
1.1
...
85e398193f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e398193f | ||
|
|
dfc73d0bbd | ||
|
|
1768a65fce | ||
|
|
357fef9782 | ||
|
|
0c4ff4fb25 | ||
|
|
e19c4654af | ||
|
|
d6dddedb3d | ||
|
|
0dfdac353f | ||
|
|
a8f00e0deb | ||
|
|
adf98bb062 | ||
|
|
1af6b49233 | ||
|
|
40aaf8e82c | ||
|
|
63810906f9 | ||
|
|
e32c9d4275 | ||
|
|
caa747b605 | ||
|
|
d783902265 | ||
|
|
e6badd31c3 | ||
|
|
3f756235b2 | ||
|
|
b9c096dd94 | ||
|
|
e7dbe41f23 | ||
|
|
d976d520b4 | ||
|
|
ec211717ed |
@@ -43,3 +43,10 @@ Use the new image to generate a docker-compose file from a running container or
|
|||||||
|
|
||||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/red5d/docker-autocompose <container-name-or-id> <additional-names-or-ids>...
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/red5d/docker-autocompose <container-name-or-id> <additional-names-or-ids>...
|
||||||
|
|
||||||
|
To print out all containers in a docker-compose format:
|
||||||
|
|
||||||
|
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/red5d/docker-autocompose $(docker ps -aq)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When making changes, please validate the output from the script by writing it to a file (docker-compose.yml or docker-compose.yaml) and running "docker-compose config" in the same folder with it to ensure that the resulting compose file will be accepted by docker-compose.
|
||||||
|
|||||||
117
autocompose.py
117
autocompose.py
@@ -1,38 +1,84 @@
|
|||||||
#! /usr/bin/env python
|
#! /usr/bin/env python3
|
||||||
|
import datetime
|
||||||
import sys, argparse, pyaml, docker
|
import sys, argparse, pyaml, docker
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
|
||||||
|
def list_container_names():
|
||||||
|
c = docker.from_env()
|
||||||
|
return [container.name for container in c.containers.list(all=True)]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(description='Generate docker-compose yaml definition from running container.')
|
parser = argparse.ArgumentParser(description='Generate docker-compose yaml definition from running container.')
|
||||||
|
parser.add_argument('-a', '--all', action='store_true', help='Include all active containers')
|
||||||
parser.add_argument('-v', '--version', type=int, default=3, help='Compose file version (1 or 3)')
|
parser.add_argument('-v', '--version', type=int, default=3, help='Compose file version (1 or 3)')
|
||||||
parser.add_argument('cnames', nargs='*', type=str, help='The name of the container to process.')
|
parser.add_argument('cnames', nargs='*', type=str, help='The name of the container to process.')
|
||||||
|
parser.add_argument('-c', '--createvolumes', action='store_true', help='Create new volumes instead of reusing existing ones')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
container_names = args.cnames
|
||||||
|
if args.all:
|
||||||
|
container_names.extend(list_container_names())
|
||||||
|
|
||||||
struct = {}
|
struct = {}
|
||||||
networks = []
|
networks = {}
|
||||||
for cname in args.cnames:
|
volumes = {}
|
||||||
cfile, networks = generate(cname)
|
containers = {}
|
||||||
|
for cname in container_names:
|
||||||
|
cfile, c_networks, c_volumes = generate(cname, createvolumes=args.createvolumes)
|
||||||
|
|
||||||
struct.update(cfile)
|
struct.update(cfile)
|
||||||
|
|
||||||
render(struct, args, networks)
|
if not c_networks == None:
|
||||||
|
networks.update(c_networks)
|
||||||
|
if not c_volumes == None:
|
||||||
|
volumes.update(c_volumes)
|
||||||
|
|
||||||
|
# moving the networks = None statemens outside of the for loop. Otherwise any container could reset it.
|
||||||
|
if len(networks) == 0:
|
||||||
|
networks = None
|
||||||
|
if len(volumes) == 0:
|
||||||
|
volumes = None
|
||||||
|
render(struct, args, networks, volumes)
|
||||||
|
|
||||||
def render(struct, args, networks):
|
def render(struct, args, networks, volumes):
|
||||||
# Render yaml file
|
# Render yaml file
|
||||||
if args.version == 1:
|
if args.version == 1:
|
||||||
pyaml.p(OrderedDict(struct))
|
pyaml.p(OrderedDict(struct))
|
||||||
else:
|
else:
|
||||||
pyaml.p(OrderedDict({'version': '"3"', 'services': struct, 'networks': networks}))
|
ans = {'version': '"3.6"', 'services': struct}
|
||||||
|
|
||||||
|
if networks is not None:
|
||||||
|
ans['networks'] = networks
|
||||||
|
|
||||||
|
if volumes is not None:
|
||||||
|
ans['volumes'] = volumes
|
||||||
|
|
||||||
|
pyaml.p(OrderedDict(ans))
|
||||||
|
|
||||||
|
|
||||||
def generate(cname):
|
def is_date_or_time(s: str):
|
||||||
|
for parse_func in [datetime.date.fromisoformat, datetime.datetime.fromisoformat]:
|
||||||
|
try:
|
||||||
|
parse_func(s.rstrip('Z'))
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def fix_label(label: str):
|
||||||
|
return f"'{label}'" if is_date_or_time(label) else label
|
||||||
|
|
||||||
|
|
||||||
|
def generate(cname, createvolumes=False):
|
||||||
c = docker.from_env()
|
c = docker.from_env()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cid = [x.short_id for x in c.containers.list(all=True) if cname == x.name or x.short_id in cname][0]
|
cid = [x.short_id for x in c.containers.list(all=True) if cname == x.name or x.short_id in cname][0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
print("That container is not available.")
|
print("That container is not available.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
cattrs = c.containers.get(cid).attrs
|
cattrs = c.containers.get(cid).attrs
|
||||||
@@ -44,6 +90,8 @@ def generate(cname):
|
|||||||
cfile[cattrs['Name'][1:]] = {}
|
cfile[cattrs['Name'][1:]] = {}
|
||||||
ct = cfile[cattrs['Name'][1:]]
|
ct = cfile[cattrs['Name'][1:]]
|
||||||
|
|
||||||
|
default_networks = ['bridge', 'host', 'none']
|
||||||
|
|
||||||
values = {
|
values = {
|
||||||
'cap_add': cattrs['HostConfig']['CapAdd'],
|
'cap_add': cattrs['HostConfig']['CapAdd'],
|
||||||
'cap_drop': cattrs['HostConfig']['CapDrop'],
|
'cap_drop': cattrs['HostConfig']['CapDrop'],
|
||||||
@@ -55,15 +103,17 @@ def generate(cname):
|
|||||||
'environment': cattrs['Config']['Env'],
|
'environment': cattrs['Config']['Env'],
|
||||||
'extra_hosts': cattrs['HostConfig']['ExtraHosts'],
|
'extra_hosts': cattrs['HostConfig']['ExtraHosts'],
|
||||||
'image': cattrs['Config']['Image'],
|
'image': cattrs['Config']['Image'],
|
||||||
'labels': cattrs['Config']['Labels'],
|
'labels': {label: fix_label(value) for label, value in cattrs['Config']['Labels'].items()},
|
||||||
'links': cattrs['HostConfig']['Links'],
|
'links': cattrs['HostConfig']['Links'],
|
||||||
#'log_driver': cattrs['HostConfig']['LogConfig']['Type'],
|
#'log_driver': cattrs['HostConfig']['LogConfig']['Type'],
|
||||||
#'log_opt': cattrs['HostConfig']['LogConfig']['Config'],
|
#'log_opt': cattrs['HostConfig']['LogConfig']['Config'],
|
||||||
'logging': {'driver': cattrs['HostConfig']['LogConfig']['Type'], 'options': cattrs['HostConfig']['LogConfig']['Config']},
|
'logging': {'driver': cattrs['HostConfig']['LogConfig']['Type'], 'options': cattrs['HostConfig']['LogConfig']['Config']},
|
||||||
'networks': {x for x in cattrs['NetworkSettings']['Networks'].keys() if x != 'bridge'},
|
'networks': {x for x in cattrs['NetworkSettings']['Networks'].keys() if x not in default_networks},
|
||||||
'security_opt': cattrs['HostConfig']['SecurityOpt'],
|
'security_opt': cattrs['HostConfig']['SecurityOpt'],
|
||||||
'ulimits': cattrs['HostConfig']['Ulimits'],
|
'ulimits': cattrs['HostConfig']['Ulimits'],
|
||||||
'volumes': cattrs['HostConfig']['Binds'],
|
# the line below would not handle type bind
|
||||||
|
# 'volumes': [f'{m["Name"]}:{m["Destination"]}' for m in cattrs['Mounts'] if m['Type'] == 'volume'],
|
||||||
|
'mounts': cattrs['Mounts'], #this could be moved outside of the dict. will only use it for generate
|
||||||
'volume_driver': cattrs['HostConfig']['VolumeDriver'],
|
'volume_driver': cattrs['HostConfig']['VolumeDriver'],
|
||||||
'volumes_from': cattrs['HostConfig']['VolumesFrom'],
|
'volumes_from': cattrs['HostConfig']['VolumesFrom'],
|
||||||
'entrypoint': cattrs['Config']['Entrypoint'],
|
'entrypoint': cattrs['Config']['Entrypoint'],
|
||||||
@@ -87,15 +137,47 @@ def generate(cname):
|
|||||||
networks = {}
|
networks = {}
|
||||||
if values['networks'] == set():
|
if values['networks'] == set():
|
||||||
del values['networks']
|
del values['networks']
|
||||||
|
assumed_default_network = list(cattrs['NetworkSettings']['Networks'].keys())[0]
|
||||||
|
values['network_mode'] = assumed_default_network
|
||||||
|
networks = None
|
||||||
else:
|
else:
|
||||||
networklist = c.networks.list()
|
networklist = c.networks.list()
|
||||||
for network in networklist:
|
for network in networklist:
|
||||||
if network.attrs['Name'] in values['networks']:
|
if network.attrs['Name'] in values['networks']:
|
||||||
networks[network.attrs['Name']] = {'external': (not network.attrs['Internal'])}
|
networks[network.attrs['Name']] = {'external': (not network.attrs['Internal']),
|
||||||
|
'name': network.attrs['Name']}
|
||||||
|
# volumes = {}
|
||||||
|
# if values['volumes'] is not None:
|
||||||
|
# for volume in values['volumes']:
|
||||||
|
# volume_name = volume.split(':')[0]
|
||||||
|
# volumes[volume_name] = {'external': True}
|
||||||
|
# else:
|
||||||
|
# volumes = None
|
||||||
|
|
||||||
|
# handles both the returned values['volumes'] (in c_file) and volumes for both, the bind and volume types
|
||||||
|
# also includes the read only option
|
||||||
|
volumes = {}
|
||||||
|
mountpoints = []
|
||||||
|
if values['mounts'] is not None:
|
||||||
|
for mount in values['mounts']:
|
||||||
|
destination = mount['Destination']
|
||||||
|
if not mount['RW']:
|
||||||
|
destination = destination + ':ro'
|
||||||
|
if mount['Type'] == 'volume':
|
||||||
|
mountpoints.append(mount['Name'] + ':' + destination)
|
||||||
|
if not createvolumes:
|
||||||
|
volumes[mount['Name']] = {'external': True} #to reuse an existing volume ... better to make that a choice? (cli argument)
|
||||||
|
elif mount['Type'] == 'bind':
|
||||||
|
mountpoints.append(mount['Source'] + ':' + destination)
|
||||||
|
values['volumes'] = mountpoints
|
||||||
|
if len(volumes) == 0:
|
||||||
|
volumes = None
|
||||||
|
values['mounts'] = None #remove this temporary data from the returned data
|
||||||
|
|
||||||
|
|
||||||
# Check for command and add it if present.
|
# Check for command and add it if present.
|
||||||
if cattrs['Config']['Cmd'] != None:
|
if cattrs['Config']['Cmd'] is not None:
|
||||||
values['command'] = " ".join(cattrs['Config']['Cmd']),
|
values['command'] = cattrs['Config']['Cmd']
|
||||||
|
|
||||||
# Check for exposed/bound ports and add them if needed.
|
# Check for exposed/bound ports and add them if needed.
|
||||||
try:
|
try:
|
||||||
@@ -122,9 +204,8 @@ def generate(cname):
|
|||||||
if (value != None) and (value != "") and (value != []) and (value != 'null') and (value != {}) and (value != "default") and (value != 0) and (value != ",") and (value != "no"):
|
if (value != None) and (value != "") and (value != []) and (value != 'null') and (value != {}) and (value != "default") and (value != 0) and (value != ",") and (value != "no"):
|
||||||
ct[key] = value
|
ct[key] = value
|
||||||
|
|
||||||
return cfile, networks
|
return cfile, networks, volumes
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user