Last active
February 2, 2021 03:51
-
-
Save elijahr/86f615c030fa5087d42934b4d8dab3eb to your computer and use it in GitHub Desktop.
Python script to merge multipe docker-compose configs into a single config
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import argparse | |
import sys | |
from collections import OrderedDict | |
from ruamel.yaml import YAML | |
yaml = YAML() | |
def combine(*configs): | |
aggregate = {} | |
# First pass, copy over version, etc | |
for config in configs: | |
if float(config["version"]) < 3: | |
raise ValueError( | |
"Only docker-compose files version 3 or greater are supported." | |
) | |
for section, value in config.items(): | |
if section not in ("networks", "services", "volumes"): | |
if section not in aggregate or aggregate[section] != value: | |
# Version or some other key | |
aggregate[section] = value | |
# Second pass, merge services/networks/volumes; preserves order. | |
aggregate.update( | |
{ | |
"services": {}, | |
"networks": {}, | |
"volumes": {}, | |
} | |
) | |
for config in configs: | |
for section, value in config.items(): | |
if section in ("networks", "services", "volumes"): | |
# A proper section | |
for name, definition in value.items(): | |
if name in aggregate[section]: | |
# merge service/network/volume definitions | |
merge(definition, aggregate[section][name]) | |
else: | |
aggregate[section][name] = definition | |
# Remove network/volumes if unused | |
for section in ("networks", "volumes"): | |
if not aggregate[section]: | |
del aggregate[section] | |
return aggregate | |
def merge(source, destination): | |
""" | |
Deep merge dictionaries, join lists. | |
""" | |
if isinstance(destination, dict): | |
for key, value in source.items(): | |
if isinstance(value, dict): | |
# get node or create one | |
node = destination.setdefault(key, OrderedDict()) | |
merge(value, node) | |
elif isinstance(destination.get(key), list) and isinstance(value, list): | |
node = destination.setdefault(key, []) | |
node += value | |
else: | |
destination[key] = value | |
elif isinstance(destination, list): | |
destination += source | |
return destination | |
if __name__ == "__main__": | |
parser = argparse.ArgumentParser( | |
description="Combine multiple docker-compose config files into a single config file." | |
) | |
parser.add_argument( | |
"config_paths", | |
metavar="PATH", | |
type=str, | |
nargs="+", | |
help="Paths to docker-compose YAML config files", | |
) | |
args = parser.parse_args() | |
configs = [] | |
for path in args.config_paths: | |
with open(path, "r") as f: | |
configs.append(yaml.load(f)) | |
config = combine(*configs) | |
yaml.dump(config, sys.stdout) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment