-
Notifications
You must be signed in to change notification settings - Fork 13
Closes #386: Add ephemeral port support to generated apps #399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
c8696ea
4cb4310
3bec447
1b7e09c
47f235c
e019d14
4728368
a01813b
b00f792
8cc463d
1231f74
1b0e7c4
7a944f0
ea3bf83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Added opt-in ephemeral Docker host ports for generated app development environments. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Set host ports to ephemeral values to avoid conflicts with other local services. | ||
| --- | ||
| services: | ||
| nautobot: | ||
| ports: !override | ||
| - "8080" | ||
| docs: | ||
| ports: !override | ||
| - "8001" | ||
| # To expose postgres (5432), mysql (3306) on db service or redis (6379) to the host using | ||
| # ephemeral ports, uncomment the following. Ensure to match the 2 indented spaces which | ||
| # have the service nested under services. | ||
| # db: | ||
| # ports: !override | ||
| # - "5432" | ||
| # - "3306" | ||
| # redis: | ||
| # ports: !override | ||
| # - "6379" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| # Set Mattermost host ports to ephemeral values to avoid conflicts with other local services. | ||
| --- | ||
| services: | ||
| mattermost: | ||
| ports: !override | ||
| - "8065" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,13 @@ | |
| nautobot_ver: "{{ min_nautobot_version }}" | ||
| local: false | ||
| python_ver: "3.12" | ||
| # ephemeral_ports: false | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume this option will not work if the "default" example is to have a list of compose files that is different than the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. I also tweaked the dev_environment.md file to clarify the behavior there. |
||
| compose_dir: "development" | ||
| compose_files: | ||
| - "docker-compose.base.yml" | ||
| - "docker-compose.redis.yml" | ||
| - "docker-compose.postgres.yml" | ||
| - "docker-compose.dev.yml" | ||
| # - "docker-compose.ephemeral-ports.yml" | ||
| - "mattermost/docker-compose.yml" | ||
| # - "mattermost/docker-compose.ephemeral-ports.yml" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../../nautobot-app/{{ cookiecutter.project_slug }}/development/docker-compose.ephemeral-ports.yml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../../../nautobot-app/{{ cookiecutter.project_slug }}/development/docker-compose.ephemeral-ports.yml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Set host ports to ephemeral values to avoid conflicts with other local services. | ||
| --- | ||
| services: | ||
| nautobot: | ||
| ports: !override | ||
| - "8080" | ||
| docs: | ||
| ports: !override | ||
| - "8001" | ||
| # To expose postgres (5432), mysql (3306) on db service or redis (6379) to the host using | ||
| # ephemeral ports, uncomment the following. Ensure to match the 2 indented spaces which | ||
| # have the service nested under services. | ||
| # db: | ||
| # ports: !override | ||
| # - "5432" | ||
| # - "3306" | ||
| # redis: | ||
| # ports: !override | ||
| # - "6379" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,8 @@ | |
| limitations under the License. | ||
| """ | ||
|
|
||
| import concurrent.futures | ||
| import json | ||
| import os | ||
| import re | ||
| import shutil | ||
|
|
@@ -24,6 +26,14 @@ | |
| from invoke.tasks import task as invoke_task | ||
|
|
||
|
|
||
| ORIGINAL_COMPOSE_FILES = [ | ||
| "docker-compose.base.yml", | ||
| "docker-compose.redis.yml", | ||
| "docker-compose.postgres.yml", | ||
| "docker-compose.dev.yml", | ||
| ] | ||
|
|
||
|
|
||
| def is_truthy(arg): | ||
| """Convert "truthy" strings into Booleans. | ||
|
|
||
|
|
@@ -56,13 +66,9 @@ def is_truthy(arg): | |
| "project_name": "{{ cookiecutter.app_slug }}", | ||
| "python_ver": "3.12", | ||
| "local": False, | ||
| "ephemeral_ports": False, | ||
| "compose_dir": os.path.join(os.path.dirname(__file__), "development"), | ||
| "compose_files": [ | ||
| "docker-compose.base.yml", | ||
| "docker-compose.redis.yml", | ||
| "docker-compose.postgres.yml", | ||
| "docker-compose.dev.yml", | ||
| ], | ||
| "compose_files": ORIGINAL_COMPOSE_FILES.copy(), | ||
| "compose_http_timeout": "86400", | ||
| } | ||
| } | ||
|
|
@@ -138,6 +144,17 @@ def docker_compose(context, command, **kwargs): | |
| compose_file_path = os.path.join(context.{{ cookiecutter.app_name }}.compose_dir, compose_file) | ||
| compose_command_tokens.append(f' -f "{compose_file_path}"') | ||
|
|
||
| if ( | ||
| context.{{ cookiecutter.app_name }}.ephemeral_ports | ||
| and context.{{ cookiecutter.app_name }}.compose_files == ORIGINAL_COMPOSE_FILES | ||
| ): | ||
| compose_file_path = os.path.join( | ||
| context.{{ cookiecutter.app_name }}.compose_dir, "docker-compose.ephemeral-ports.yml" | ||
| ) | ||
| compose_command_tokens.append( | ||
| f' -f "{compose_file_path}"' | ||
| ) | ||
|
|
||
| compose_command_tokens.append(command) | ||
|
|
||
| # If `service` was passed as a kwarg, add it to the end. | ||
|
|
@@ -151,6 +168,44 @@ def docker_compose(context, command, **kwargs): | |
| return context.run(compose_command, env=build_env, **kwargs) | ||
|
|
||
|
|
||
| @task | ||
| def dump_service_ports_to_disk(context): | ||
| """Useful for downstream utilities without direct docker access to determine ports. | ||
|
|
||
| This function will sometimes be called asynchronously while containers are still | ||
| firing up, hence the `attempt` loop. | ||
| """ | ||
| service_ports = {} | ||
|
|
||
| for _ in range(4): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of trying multiple times? In case the services haven't come up yet?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah; it's functionally identical to the corresponding function in in the nautobot core. I've updated the docstring to also match the one used by the core, which is more explicit. |
||
| result = docker_compose(context, "ps --format json", hide=True) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on my previous comment, it looks like we need to actually make
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Easy enough. Would you rather I rolled that into this PR or submitted as its own?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like just that one print statement, so I'd say roll it into this one.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added. Command output: BeforeAfter |
||
|
|
||
| for line in result.stdout.splitlines(): | ||
| try: | ||
| service_def = json.loads(line) | ||
| service_name = re.search( | ||
| r"com\.docker\.compose\.service=(?P<service>\w+)", service_def["Labels"] | ||
| ).group("service") | ||
|
|
||
| ports_found = {} | ||
| for port in service_def["Publishers"]: | ||
| if port.get("PublishedPort", 0): | ||
| ports_found[port["TargetPort"]] = port["PublishedPort"] | ||
|
|
||
| if ports_found: | ||
| service_ports[service_name] = ports_found | ||
| except (json.decoder.JSONDecodeError, AttributeError, IndexError, KeyError): | ||
| continue | ||
|
|
||
| if set(["nautobot", "worker"]).issubset(service_ports.keys()): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this the correct list of expected services to be exposed by default? I added these changes to one of my dev environments and I saw this output:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @nrnvgh this still needs changed. We don't expose the worker port by default, so this causes this loop to run all 4 times regardless. The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dag nabbit. Good catch; fixed. BeforeAfter |
||
| break | ||
|
|
||
| sleep(5) | ||
|
|
||
| with open(".service_ports.json", "w", encoding="utf-8") as file: | ||
| json.dump(service_ports, file, indent=4) | ||
|
|
||
|
|
||
| def run_command(context, command, service="nautobot", **kwargs): | ||
| """Wrapper to run a command locally or inside the nautobot container.""" | ||
| if is_truthy(context.{{ cookiecutter.app_name }}.local): | ||
|
|
@@ -298,7 +353,9 @@ def debug(context, service=None): | |
| """Start specified or all services and its dependencies in debug mode.""" | ||
| service = " ".join(service) if service else "" | ||
| print(f"Starting {service or 'all services'} in debug mode...") | ||
| docker_compose(context, "up", service=service) | ||
| with concurrent.futures.ThreadPoolExecutor() as executor: | ||
| executor.submit(dump_service_ports_to_disk, context) | ||
| docker_compose(context, "up", service=service) | ||
|
|
||
|
|
||
| @task( | ||
|
|
@@ -312,6 +369,7 @@ def start(context, service=None): | |
| service = " ".join(service) if service else "" | ||
| print(f"Starting {service or 'all services'} in detached mode...") | ||
| docker_compose(context, "up --detach", service=service) | ||
| dump_service_ports_to_disk(context) | ||
|
|
||
|
|
||
| @task( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect. cookiecutter-based apps used 8080 as the internal port, so the for ephemeral ports, 8001 should be changed to 8080...if we want to keep the docs container listening on 8080. Alternatively, we can change it to 8001, in line with the nautobot core, and leave this file as-is.