| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  | from io import StringIO | 
					
						
							|  |  |  | from typing import List | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  | import sys | 
					
						
							|  |  |  | import pathlib | 
					
						
							| 
									
										
										
										
											2022-12-03 23:00:50 -08:00
										 |  |  | from collections.abc import Mapping | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | import click | 
					
						
							|  |  |  | import yaml | 
					
						
							|  |  |  | from dotenv import dotenv_values | 
					
						
							|  |  |  | from yaml import Loader | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  | COMPOSE_SPECS = { | 
					
						
							|  |  |  |     "docker-compose.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose.yml", | 
					
						
							|  |  |  |         "../docker-compose.override.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose-m1.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose.yml", | 
					
						
							|  |  |  |         "../docker-compose.override.yml", | 
					
						
							|  |  |  |         "../docker-compose.m1.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose-without-neo4j.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose-without-neo4j.yml", | 
					
						
							|  |  |  |         "../docker-compose-without-neo4j.override.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose-without-neo4j-m1.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose-without-neo4j.yml", | 
					
						
							|  |  |  |         "../docker-compose-without-neo4j.override.yml", | 
					
						
							|  |  |  |         "../docker-compose-without-neo4j.m1.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose.monitoring.quickstart.yml": [ | 
					
						
							|  |  |  |         "../monitoring/docker-compose.monitoring.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose.consumers.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose.consumers.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose.consumers-without-neo4j.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose.consumers-without-neo4j.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  |     "docker-compose.kafka-setup.quickstart.yml": [ | 
					
						
							|  |  |  |         "../docker-compose.kafka-setup.yml", | 
					
						
							|  |  |  |     ], | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | omitted_services = [ | 
					
						
							|  |  |  |     "kafka-rest-proxy", | 
					
						
							|  |  |  |     "kafka-topics-ui", | 
					
						
							|  |  |  |     "schema-registry-ui", | 
					
						
							|  |  |  |     "kibana", | 
					
						
							|  |  |  | ] | 
					
						
							| 
									
										
										
										
											2021-06-23 12:59:49 -07:00
										 |  |  | # Note that these are upper bounds on memory usage. Once exceeded, the container is killed. | 
					
						
							|  |  |  | # Each service will be configured to use much less Java heap space than allocated here. | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | mem_limits = { | 
					
						
							| 
									
										
										
										
											2023-05-08 23:42:15 +02:00
										 |  |  |     "elasticsearch": "1G", | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def dict_merge(dct, merge_dct): | 
					
						
							|  |  |  |     for k, v in merge_dct.items(): | 
					
						
							|  |  |  |         if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], Mapping): | 
					
						
							|  |  |  |             dict_merge(dct[k], merge_dct[k]) | 
					
						
							| 
									
										
										
										
											2023-01-24 16:12:57 +00:00
										 |  |  |         elif k in dct and isinstance(dct[k], list): | 
					
						
							|  |  |  |             a = set(dct[k]) | 
					
						
							|  |  |  |             b = set(merge_dct[k]) | 
					
						
							|  |  |  |             if a != b: | 
					
						
							| 
									
										
										
										
											2023-02-07 00:56:08 -08:00
										 |  |  |                 dct[k] = sorted(list(a.union(b))) | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |         else: | 
					
						
							|  |  |  |             dct[k] = merge_dct[k] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | def modify_docker_config(base_path, docker_yaml_config): | 
					
						
							| 
									
										
										
										
											2023-01-31 00:34:36 +01:00
										 |  |  |     if not docker_yaml_config["services"]: | 
					
						
							|  |  |  |         docker_yaml_config["services"] = {} | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |     # 0. Filter out services to be omitted. | 
					
						
							| 
									
										
										
										
											2023-01-31 00:34:36 +01:00
										 |  |  |     for key in docker_yaml_config["services"]: | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |         if key in omitted_services: | 
					
						
							|  |  |  |             del docker_yaml_config["services"][key] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for name, service in docker_yaml_config["services"].items(): | 
					
						
							|  |  |  |         # 1. Extract the env file pointer | 
					
						
							|  |  |  |         env_file = service.get("env_file") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if env_file is not None: | 
					
						
							|  |  |  |             # 2. Construct full .env path | 
					
						
							|  |  |  |             env_file_path = os.path.join(base_path, env_file) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # 3. Resolve the .env values | 
					
						
							|  |  |  |             env_vars = dotenv_values(env_file_path) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-15 19:05:52 +00:00
										 |  |  |             # 4. Create an "environment" block if it does not exist | 
					
						
							|  |  |  |             if "environment" not in service: | 
					
						
							|  |  |  |                 service["environment"] = list() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             # 5. Append to an "environment" block to YAML | 
					
						
							|  |  |  |             for key, value in env_vars.items(): | 
					
						
							| 
									
										
										
										
											2022-12-03 23:00:50 -08:00
										 |  |  |                 if value is not None: | 
					
						
							|  |  |  |                     service["environment"].append(f"{key}={value}") | 
					
						
							|  |  |  |                 else: | 
					
						
							|  |  |  |                     service["environment"].append(f"{key}") | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-15 19:05:52 +00:00
										 |  |  |             # 6. Delete the "env_file" value | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |             del service["env_file"] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-15 19:05:52 +00:00
										 |  |  |         # 7. Delete build instructions | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |         if "build" in service: | 
					
						
							|  |  |  |             del service["build"] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-15 19:05:52 +00:00
										 |  |  |         # 8. Set memory limits | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |         if name in mem_limits: | 
					
						
							| 
									
										
										
										
											2023-05-08 23:42:15 +02:00
										 |  |  |             service["deploy"] = {"resources":{"limits":{"memory":mem_limits[name]}}} | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-03-15 19:05:52 +00:00
										 |  |  |         # 9. Correct relative paths for volume mounts | 
					
						
							| 
									
										
										
										
											2021-10-08 15:46:18 -07:00
										 |  |  |         if "volumes" in service: | 
					
						
							|  |  |  |             volumes = service["volumes"] | 
					
						
							|  |  |  |             for i in range(len(volumes)): | 
					
						
							|  |  |  |                 ## Quickstart yaml files are located under quickstart. To get correct paths, need to refer to parent directory | 
					
						
							|  |  |  |                 if volumes[i].startswith("../"): | 
					
						
							|  |  |  |                     volumes[i] = "../" + volumes[i] | 
					
						
							|  |  |  |                 elif volumes[i].startswith("./"): | 
					
						
							|  |  |  |                     volumes[i] = "." + volumes[i] | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-08 23:42:15 +02:00
										 |  |  |     # 10. Set docker compose version to 3. | 
					
						
							| 
									
										
										
										
											2021-07-13 12:02:05 -07:00
										 |  |  |     # We need at least this version, since we use features like start_period for | 
					
						
							| 
									
										
										
										
											2023-05-08 23:42:15 +02:00
										 |  |  |     # healthchecks (with services dependencies based on them) and shell-like variable interpolation. | 
					
						
							|  |  |  |     docker_yaml_config["version"] = "3.9" | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  | def dedup_env_vars(merged_docker_config): | 
					
						
							|  |  |  |     for service in merged_docker_config["services"]: | 
					
						
							|  |  |  |         if "environment" in merged_docker_config["services"][service]: | 
					
						
							|  |  |  |             lst = merged_docker_config["services"][service]["environment"] | 
					
						
							|  |  |  |             if lst is not None: | 
					
						
							|  |  |  |                 # use a set to cache duplicates | 
					
						
							|  |  |  |                 caches = set() | 
					
						
							|  |  |  |                 results = {} | 
					
						
							|  |  |  |                 for item in lst: | 
					
						
							|  |  |  |                     partitions = item.rpartition("=") | 
					
						
							|  |  |  |                     prefix = partitions[0] | 
					
						
							|  |  |  |                     suffix = partitions[1] | 
					
						
							|  |  |  |                     # check whether prefix already exists | 
					
						
							|  |  |  |                     if prefix not in caches and suffix != "": | 
					
						
							|  |  |  |                         results[prefix] = item | 
					
						
							|  |  |  |                         caches.add(prefix) | 
					
						
							|  |  |  |                 if set(lst) != set([v for k, v in results.items()]): | 
					
						
							|  |  |  |                     sorted_vars = sorted([k for k in results]) | 
					
						
							|  |  |  |                     merged_docker_config["services"][service]["environment"] = [ | 
					
						
							|  |  |  |                         results[var] for var in sorted_vars | 
					
						
							|  |  |  |                     ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def merge_files(compose_files: List[str]) -> str: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Generates a merged docker-compose file with env variables inlined. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Example Usage: python3 generate_docker_quickstart.py generate-one ../docker-compose.yml ../docker-compose.override.yml ../docker-compose-gen.yml | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Resolve .env files to inlined vars | 
					
						
							|  |  |  |     modified_files = [] | 
					
						
							|  |  |  |     for compose_file in compose_files: | 
					
						
							|  |  |  |         with open(compose_file, "r") as orig_conf: | 
					
						
							|  |  |  |             docker_config = yaml.load(orig_conf, Loader=Loader) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         base_path = os.path.dirname(compose_file) | 
					
						
							|  |  |  |         modify_docker_config(base_path, docker_config) | 
					
						
							|  |  |  |         modified_files.append(docker_config) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Merge services, networks, and volumes maps | 
					
						
							|  |  |  |     merged_docker_config = modified_files[0] | 
					
						
							|  |  |  |     for modified_file in modified_files: | 
					
						
							|  |  |  |         dict_merge(merged_docker_config, modified_file) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-01-24 16:12:57 +00:00
										 |  |  |     # Dedup env vars, last wins | 
					
						
							|  |  |  |     dedup_env_vars(merged_docker_config) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  |     # Generate yaml to string. | 
					
						
							|  |  |  |     out = StringIO() | 
					
						
							|  |  |  |     yaml.dump( | 
					
						
							|  |  |  |         merged_docker_config, | 
					
						
							|  |  |  |         out, | 
					
						
							|  |  |  |         default_flow_style=False, | 
					
						
							|  |  |  |         width=1000, | 
					
						
							|  |  |  |     ) | 
					
						
							|  |  |  |     return out.getvalue() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @click.group() | 
					
						
							|  |  |  | def main_cmd() -> None: | 
					
						
							|  |  |  |     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @main_cmd.command() | 
					
						
							|  |  |  | @click.argument( | 
					
						
							|  |  |  |     "compose-files", | 
					
						
							|  |  |  |     nargs=-1, | 
					
						
							|  |  |  |     type=click.Path( | 
					
						
							|  |  |  |         exists=True, | 
					
						
							|  |  |  |         dir_okay=False, | 
					
						
							|  |  |  |     ), | 
					
						
							|  |  |  | ) | 
					
						
							|  |  |  | @click.argument("output-file", type=click.Path()) | 
					
						
							|  |  |  | def generate_one(compose_files, output_file) -> None: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Generates a merged docker-compose file with env variables inlined. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Example Usage: python3 generate_docker_quickstart.py generate-one ../docker-compose.yml ../docker-compose.override.yml ../docker-compose-gen.yml | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     merged_contents = merge_files(compose_files) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  |     # Write output file | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  |     pathlib.Path(output_file).parent.mkdir(parents=True, exist_ok=True) | 
					
						
							|  |  |  |     pathlib.Path(output_file).write_text(merged_contents) | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |     print(f"Successfully generated {output_file}.") | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  | @main_cmd.command() | 
					
						
							|  |  |  | @click.pass_context | 
					
						
							|  |  |  | def generate_all(ctx: click.Context) -> None: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Generates all merged docker-compose files with env variables inlined. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for output_compose_file, inputs in COMPOSE_SPECS.items(): | 
					
						
							|  |  |  |         ctx.invoke(generate_one, compose_files=inputs, output_file=output_compose_file) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @main_cmd.command() | 
					
						
							|  |  |  | def check_all() -> None: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Checks that the generated docker-compose files are up to date. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     for output_compose_file, inputs in COMPOSE_SPECS.items(): | 
					
						
							|  |  |  |         expected = merge_files(inputs) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         # Check that the files match. | 
					
						
							|  |  |  |         current = pathlib.Path(output_compose_file).read_text() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if expected != current: | 
					
						
							|  |  |  |             print( | 
					
						
							|  |  |  |                 f"File {output_compose_file} is out of date. Please run `python3 generate_docker_quickstart.py generate-all`." | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |             sys.exit(1) | 
					
						
							| 
									
										
										
										
											2023-01-24 16:12:57 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-06-14 17:15:24 -07:00
										 |  |  | if __name__ == "__main__": | 
					
						
							| 
									
										
										
										
											2023-04-26 03:05:34 +05:30
										 |  |  |     main_cmd() |