diff --git a/.gitignore b/.gitignore index e5ecc90..07579f2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,8 @@ *.vsix # Ignore local venv environment .venv +__pycache__ +.pytest_cache + # Ignore mac os x .DS_Store files .DS_Store \ No newline at end of file diff --git a/scripts/generators/docker-compose/Dockerfile b/scripts/generators/docker-compose/Dockerfile index c297bfa..f0136f8 100644 --- a/scripts/generators/docker-compose/Dockerfile +++ b/scripts/generators/docker-compose/Dockerfile @@ -15,4 +15,4 @@ RUN chown -R appuser:appgroup /app && chmod +x createComposeFile.py USER appuser # Define the default entrypoint to run the script -ENTRYPOINT ["python", "createComposeFile.py", "--template", "/app/docker-compose.j2"] +ENTRYPOINT ["python", "createComposeFile.py", "--template", "docker-compose.j2"] diff --git a/scripts/generators/docker-compose/createComposeFile.py b/scripts/generators/docker-compose/createComposeFile.py index 1afe0bf..897f3fd 100644 --- a/scripts/generators/docker-compose/createComposeFile.py +++ b/scripts/generators/docker-compose/createComposeFile.py @@ -1,18 +1,59 @@ import argparse +from deepmerge import always_merger +import inflect from jinja2 import Environment, FileSystemLoader import os import yaml +# Initialize inflect engine +p = inflect.engine() + debug_mode = False +defaultValues = { + "imageNamePrefix": "ghcr.io/cisco-open/", + "imageNameSuffix": "latest", + "defaultPorts": { + "services": 80, # this is different from the "default default" on purpose. + "databases": 5432, + "loaders": 6000 # not used + }, + # Keep the default ports for each service type here + # We need them to verify in the docker compose generation if they have been + # changed by the user. + "_defaultDefaultPorts": { + "services": 8080, + "databases": 5432, + "loaders": 6000 + } +} + +def plural_to_singular(word): + return p.singular_noun(word) or word + def read_yaml(file_path): with open(os.path.expanduser(file_path), 'r') as file: # Parse the YAML file data = yaml.safe_load(file) return data +def render_compose_file(template, data, defaultValues): + globalVars = data.get('global', {}) + + config = { + "global": always_merger.merge(defaultValues, globalVars), + "scopes": { + "services": data.get('services', {}), + "databases": data.get('databases', {}), + "loaders": data.get('loaders', {}), + } + } + + return template.render(config) + def main(): global debug_mode + global defaultValues parser = argparse.ArgumentParser(description='Process some configuration file.') parser.add_argument( '--config', @@ -47,9 +88,11 @@ def main(): if isinstance(yaml_data, dict): env = Environment(loader=FileSystemLoader('.')) + env.filters['singularize'] = plural_to_singular + template = env.get_template(template_path) - rendered_content = template.render(yaml_data) + rendered_content = render_compose_file(template, data, defaultValues) with open(output_path, 'w') as output_file: output_file.write(rendered_content) @@ -57,11 +100,13 @@ def main(): else: raise Exception(f"Failed to read the configuration file {config_file}: The top-level structure is not a dictionary.") -try: - main() -except Exception as e: - if not debug_mode: - print(e) - else: - raise e - exit(1) \ No newline at end of file + +if __name__ == "__main__": + try: + main() + except Exception as e: + if not debug_mode: + print(e) + else: + raise e + exit(1) \ No newline at end of file diff --git a/scripts/generators/docker-compose/docker-compose.j2 b/scripts/generators/docker-compose/docker-compose.j2 index f9077b3..edc2c1b 100644 --- a/scripts/generators/docker-compose/docker-compose.j2 +++ b/scripts/generators/docker-compose/docker-compose.j2 @@ -1,72 +1,33 @@ ---- -{%- set global = global | default({}) -%} -{%- set imageNamePrefix = global.imageNamePrefix | default('ghcr.io/cisco-open/') -%} -{%- set imageNameSuffix = global.imageNameSuffix | default('latest') -%} -{%- set serviceDefaultPort = global.serviceDefaultPort | default(80) -%} -{# Ensure the variable always ends with a slash #} -{%- set imageNamePrefix = imageNamePrefix if imageNamePrefix.endswith('/') else imageNamePrefix + '/' %} services: -{%- if services is defined and services is mapping %} - ## services - {%- for name, details in services.items() %} +{%- for scope, scopeDetails in scopes.items() %} + ## {{ scope }} + {%- for name, details in scopeDetails.items() %} {{ name }}: - image: {{ imageNamePrefix }}app-simulator-services-{{ details.type }}:{{ imageNameSuffix }} + image: {{ global.imageNamePrefix }}app-simulator-{{ scope }}-{{ details.type }}:{{global.imageNameSuffix }} {%- if details.port is defined %} ports: - - "{{ details.port }}:8080" + - "{{ details.port }}:{{ global.defaultPorts[scope] }}" {%- endif %} - {%- if serviceDefaultPort != 8080 %} + {%- if global.defaultPorts is defined -%} + {%- if global.defaultPorts[scope] is defined and global.defaultPorts[scope] != global._defaultDefaultPorts[scope] %} environment: - SERVICE_DEFAULT_PORT: "{{ serviceDefaultPort }}" - {%- if serviceDefaultPort <= 1024 %} + - {{ scope | singularize | upper }}_DEFAULT_PORT={{ global.defaultPorts[scope] }} + {%- endif %} + {%- if global.defaultPorts[scope] <= 1024 %} cap_add: - - NET_BIND_SERVICE - {%- endif %} + - NET_BIND_SERVICE + {%- endif %} {%- endif %} configs: - - source: service_{{ name | replace("-", "_") }}_config - target: /config.json - {%- endfor %} -{%- endif -%} -{%- if databases is defined and databases is mapping %} - ## databases - {%- for name, details in databases.items() %} - {{ name }}: - image: {{ imageNamePrefix }}app-simulator-databases-{{ details.type }}:{{ imageNameSuffix }} - configs: - - source: database_{{ name | replace("-", "_") }}_config - target: /config.json - {%- endfor %} -{%- endif -%} -{%- if loaders is defined and loaders is mapping %} - ## loaders - {%- for name, details in loaders.items() %} - {{ name }}: - image: {{ imageNamePrefix }}app-simulator-loaders-{{ details.type }}:{{ imageNameSuffix }} - configs: - - source: loader_{{ name | replace("-", "_") }}_config + - source: {{ scope }}_{{ name | replace("-", "_") | lower }}_config target: /config.json {%- endfor %} -{%- endif %} +{%- endfor %} configs: -{%- if services is defined and services is mapping %} -{%- for name, details in services.items() %} - service_{{ name | replace("-", "_") }}_config: - content: | - {{ details | tojson }} -{%- endfor -%} -{%- endif %} -{%- if databases is defined and databases is mapping %} -{%- for name, details in databases.items() %} - database_{{ name | replace("-", "_") }}_config: - content: | - {{ details | tojson }} -{%- endfor -%} -{%- endif %} -{%- if loaders is defined and loaders is mapping %} -{%- for name, details in loaders.items() %} - loader_{{ name | replace("-", "_") }}_config: +{%- for scope, scopeDetails in scopes.items() %} +{%- for name, details in scopeDetails.items() %} + {{ scope }}_{{ name | replace("-", "_") | lower }}_config: content: | {{ details | tojson }} {%- endfor -%} -{%- endif %} \ No newline at end of file +{%- endfor %} \ No newline at end of file diff --git a/scripts/generators/docker-compose/requirements.txt b/scripts/generators/docker-compose/requirements.txt index 6d43419..1b96a48 100644 --- a/scripts/generators/docker-compose/requirements.txt +++ b/scripts/generators/docker-compose/requirements.txt @@ -1,4 +1,5 @@ +deepmerge==2.0 +inflect==7.5.0 Jinja2==3.1.4 -opentelemetry-api==1.29.0 -opentelemetry-sdk==1.29.0 PyYAML==6.0.2 +pytest==8.3.4 \ No newline at end of file diff --git a/scripts/generators/docker-compose/tests/__init__.py b/scripts/generators/docker-compose/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/generators/docker-compose/tests/test_render_compose_file.py b/scripts/generators/docker-compose/tests/test_render_compose_file.py new file mode 100644 index 0000000..4e49a55 --- /dev/null +++ b/scripts/generators/docker-compose/tests/test_render_compose_file.py @@ -0,0 +1,201 @@ +from createComposeFile import plural_to_singular, render_compose_file +from jinja2 import Environment, FileSystemLoader +import pytest + +@pytest.fixture(scope="session") +def load_template(): + env = Environment(loader=FileSystemLoader('.')) + env.filters['singularize'] = plural_to_singular + return env.get_template('docker-compose.j2') + + +def test_render_compose_file_with_empty_config_and_defaults(load_template): + result = render_compose_file(load_template, {}, {}) + expected = """ +services: + ## services + ## databases + ## loaders +configs: +""".strip() + assert result == expected + +def test_render_compose_file_with_simple_config_and_no_defaults(load_template): + result = render_compose_file(load_template, { + "global": { + "imageNamePrefix": "test/", + "imageNameSuffix": "test" + }, + "services": { + "test": { + "type": "java" + } + }, + "databases": { + "testdb": { + "type": "mysql" + } + }, + "loaders": { + "testloader": { + "type": "curl" + } + } + }, {}) + expected = """ +services: + ## services + test: + image: test/app-simulator-services-java:test + configs: + - source: services_test_config + target: /config.json + ## databases + testdb: + image: test/app-simulator-databases-mysql:test + configs: + - source: databases_testdb_config + target: /config.json + ## loaders + testloader: + image: test/app-simulator-loaders-curl:test + configs: + - source: loaders_testloader_config + target: /config.json +configs: + services_test_config: + content: | + {"type": "java"} + databases_testdb_config: + content: | + {"type": "mysql"} + loaders_testloader_config: + content: | + {"type": "curl"} +""".strip() + assert result == expected + +def test_render_compose_file_with_simple_config_and_defaults(load_template): + result = render_compose_file(load_template, { + "services": { + "test": { + "type": "java" + } + }, + "databases": { + "testdb": { + "type": "mysql" + } + }, + "loaders": { + "testloader": { + "type": "curl" + } + } + }, { + "imageNamePrefix": "test/", + "imageNameSuffix": "test" + }) + expected = """ +services: + ## services + test: + image: test/app-simulator-services-java:test + configs: + - source: services_test_config + target: /config.json + ## databases + testdb: + image: test/app-simulator-databases-mysql:test + configs: + - source: databases_testdb_config + target: /config.json + ## loaders + testloader: + image: test/app-simulator-loaders-curl:test + configs: + - source: loaders_testloader_config + target: /config.json +configs: + services_test_config: + content: | + {"type": "java"} + databases_testdb_config: + content: | + {"type": "mysql"} + loaders_testloader_config: + content: | + {"type": "curl"} +""".strip() + assert result == expected + +def test_render_compose_file_with_port_config(load_template): + result = render_compose_file(load_template, { + "global": { + "imageNamePrefix": "test/", + "imageNameSuffix": "test", + "defaultPorts": { + "services": 1024, + "databases": 5432, + "loaders": 1025 + }, + "_defaultDefaultPorts": { + "services": 8080, + "databases": 5432, + "loaders": 6000 + } + }, + "services": { + "test": { + "type": "java" + } + }, + "databases": { + "testdb": { + "type": "mysql" + } + }, + "loaders": { + "testloader": { + "type": "curl" + } + } + }, {}) + expected = """ +services: + ## services + test: + image: test/app-simulator-services-java:test + environment: + - SERVICE_DEFAULT_PORT=1024 + cap_add: + - NET_BIND_SERVICE + configs: + - source: services_test_config + target: /config.json + ## databases + testdb: + image: test/app-simulator-databases-mysql:test + configs: + - source: databases_testdb_config + target: /config.json + ## loaders + testloader: + image: test/app-simulator-loaders-curl:test + environment: + - LOADER_DEFAULT_PORT=1025 + configs: + - source: loaders_testloader_config + target: /config.json +configs: + services_test_config: + content: | + {"type": "java"} + databases_testdb_config: + content: | + {"type": "mysql"} + loaders_testloader_config: + content: | + {"type": "curl"} +""".strip() + assert result == expected \ No newline at end of file