stoobly-agent 1.9.11__py3-none-any.whl → 1.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/configs_controller.py +3 -3
  4. stoobly_agent/app/cli/decorators/exec.py +1 -1
  5. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  6. stoobly_agent/app/cli/helpers/shell.py +0 -10
  7. stoobly_agent/app/cli/intercept_cli.py +40 -7
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -0
  9. stoobly_agent/app/cli/scaffold/app_config.py +21 -3
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
  11. stoobly_agent/app/cli/scaffold/constants.py +14 -0
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/builder.py +19 -4
  14. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -18
  15. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +24 -0
  16. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  17. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  18. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  20. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  21. stoobly_agent/app/cli/scaffold/service_config.py +144 -21
  22. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  23. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  24. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  25. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  26. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  27. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  28. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  29. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  30. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  31. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  33. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  34. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  35. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  36. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  37. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  38. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  39. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  40. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  41. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  42. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  43. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  44. stoobly_agent/app/cli/scaffold/workflow_namesapce.py +8 -2
  45. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  46. stoobly_agent/app/cli/scaffold_cli.py +77 -83
  47. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  48. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  49. stoobly_agent/app/proxy/intercept_settings.py +11 -7
  50. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +33 -2
  51. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  52. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  53. stoobly_agent/app/proxy/run.py +3 -28
  54. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  55. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  56. stoobly_agent/app/proxy/utils/publish_change_service.py +5 -4
  57. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  58. stoobly_agent/app/settings/__init__.py +9 -3
  59. stoobly_agent/app/settings/data_rules.py +25 -1
  60. stoobly_agent/app/settings/intercept_settings.py +5 -2
  61. stoobly_agent/app/settings/types/__init__.py +0 -1
  62. stoobly_agent/app/settings/ui_settings.py +5 -5
  63. stoobly_agent/cli.py +41 -16
  64. stoobly_agent/config/constants/custom_headers.py +1 -0
  65. stoobly_agent/config/constants/env_vars.py +4 -3
  66. stoobly_agent/config/constants/record_strategy.py +6 -0
  67. stoobly_agent/config/settings.yml.sample +2 -3
  68. stoobly_agent/lib/logger.py +15 -5
  69. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  70. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  71. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  72. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +14 -2
  73. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  74. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/METADATA +2 -1
  75. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/RECORD +78 -62
  76. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/LICENSE +0 -0
  77. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/WHEEL +0 -0
  78. {stoobly_agent-1.9.11.dist-info → stoobly_agent-1.10.0.dist-info}/entry_points.txt +0 -0
@@ -2,4 +2,5 @@ services:
2
2
  stoobly_ui.base:
3
3
  extends:
4
4
  file: ../.docker-compose.base.yml
5
- service: context_base
5
+ service: context_base
6
+ image: stoobly.${USER_ID}
@@ -1,11 +1,14 @@
1
1
  services:
2
2
  stoobly_ui.service:
3
- command: --ui-port 4200
3
+ command: --proxyless --ui-host local.stoobly.com --ui-port 4200
4
4
  extends:
5
5
  file: ../.docker-compose.base.yml
6
6
  service: stoobly_ui.base
7
- image: stoobly.${USER_ID}
7
+ networks:
8
+ app.egress:
9
+ aliases:
10
+ - local.stoobly.com
8
11
  ports:
9
12
  - '${APP_UI_PORT}:4200'
10
13
  profiles:
11
- - mock
14
+ - ${WORKFLOW_NAME}
@@ -1,12 +1,14 @@
1
1
  services:
2
2
  stoobly_ui.service:
3
- command: --ui-port 4200
3
+ command: --proxyless --ui-host local.stoobly.com --ui-port 4200
4
4
  extends:
5
5
  file: ../.docker-compose.base.yml
6
6
  service: stoobly_ui.base
7
- image: stoobly.${USER_ID}
7
+ networks:
8
+ app.egress:
9
+ aliases:
10
+ - local.stoobly.com
8
11
  ports:
9
12
  - '${APP_UI_PORT}:4200'
10
13
  profiles:
11
- - record
12
-
14
+ - ${WORKFLOW_NAME}
@@ -13,6 +13,7 @@ CORE_RECORD_WORKFLOW = 'record'
13
13
 
14
14
  CUSTOM_BUILD = os.path.join('bin', 'build')
15
15
  CUSTOM_CONFIGURE = os.path.join('bin', 'configure')
16
+ CUSTOM_DOCKER_COMPOSE = 'docker-compose.yml'
16
17
  CUSTOM_INIT = os.path.join('bin', 'init')
17
18
  CUSTOM_FIXTURES = 'fixtures.yml'
18
19
  CUSTOM_LIFECYCLE_HOOKS = os.path.join('lifecycle_hooks.py')
@@ -28,6 +29,7 @@ MOCK_WORKFLOW_MAINTAINED_FILES = [
28
29
  MOCK_WORKFLOW_CUSTOM_FILES = [
29
30
  CUSTOM_BUILD,
30
31
  CUSTOM_CONFIGURE,
32
+ CUSTOM_DOCKER_COMPOSE,
31
33
  CUSTOM_FIXTURES,
32
34
  CUSTOM_INIT,
33
35
  CUSTOM_LIFECYCLE_HOOKS,
@@ -42,6 +44,7 @@ RECORD_WORKFLOW_MAINTAINED_FILES = [
42
44
  RECORD_WORKFLOW_CUSTOM_FILES = [
43
45
  CUSTOM_BUILD,
44
46
  CUSTOM_CONFIGURE,
47
+ CUSTOM_DOCKER_COMPOSE,
45
48
  CUSTOM_INIT,
46
49
  CUSTOM_LIFECYCLE_HOOKS,
47
50
  ]
@@ -54,6 +57,7 @@ TEST_WORKFLOW_MAINTAINED_FILES = [
54
57
  TEST_WORKFLOW_CUSTOM_FILES = [
55
58
  CUSTOM_BUILD,
56
59
  CUSTOM_CONFIGURE,
60
+ CUSTOM_DOCKER_COMPOSE,
57
61
  CUSTOM_FIXTURES,
58
62
  CUSTOM_INIT,
59
63
  CUSTOM_LIFECYCLE_HOOKS,
@@ -0,0 +1,22 @@
1
+ ARG CYPRESS_IMAGE
2
+ FROM ${CYPRESS_IMAGE}
3
+
4
+ # Change user id of stoobly user to that of host's user id
5
+ ARG USER_ID
6
+ ARG USER_NAME=stoobly
7
+ RUN set -eux; \
8
+ # Check if a user with this UID exists
9
+ if getent passwd "${USER_ID}" > /dev/null; then \
10
+ EXISTING_USER="$(getent passwd "${USER_ID}" | cut -d: -f1)"; \
11
+ if [ "$EXISTING_USER" != "${USER_NAME}" ]; then \
12
+ usermod -l "${USER_NAME}" "$EXISTING_USER"; \
13
+ usermod -d "/home/${USER_NAME}" -m "${USER_NAME}"; \
14
+ fi; \
15
+ else \
16
+ useradd -u "${USER_ID}" -m "${USER_NAME}"; \
17
+ fi
18
+
19
+ USER stoobly
20
+ WORKDIR /home/stoobly
21
+
22
+ RUN npx cypress install # Install Cypress binary into image
@@ -0,0 +1,19 @@
1
+ services:
2
+ entrypoint.cypress:
3
+ build:
4
+ args:
5
+ CYPRESS_IMAGE: ${CYPRESS_IMAGE:-cypress/browsers:22.11.0}
6
+ USER_ID: ${USER_ID}
7
+ context: ./
8
+ dockerfile: ./.Dockerfile.cypress
9
+ command: npx cypress run --project .
10
+ depends_on:
11
+ entrypoint.configure:
12
+ condition: service_completed_successfully
13
+ networks:
14
+ - "app.ingress"
15
+ profiles:
16
+ - ${WORKFLOW_NAME}
17
+ user: stoobly
18
+ volumes:
19
+ - ${CONTEXT_DIR}:/home/stoobly
@@ -0,0 +1,33 @@
1
+ ARG PLAYWRIGHT_IMAGE
2
+ FROM ${PLAYWRIGHT_IMAGE}
3
+
4
+ RUN set -eux; \
5
+ apt-get update; \
6
+ apt-get install -y --no-install-recommends ca-certificates wget; \
7
+ dpkgArch="$(dpkg --print-architecture | awk -F- '{ print $NF }')"; \
8
+ wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.16/gosu-$dpkgArch"; \
9
+ chmod +x /usr/local/bin/gosu; \
10
+ gosu --version;
11
+ RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
12
+ RUN npx playwright install --with-deps
13
+
14
+ # Change user id of stoobly user to that of host's user id
15
+ ARG USER_ID
16
+ ARG USER_NAME=stoobly
17
+ RUN set -eux; \
18
+ # Check if a user with this UID exists
19
+ if getent passwd "${USER_ID}" > /dev/null; then \
20
+ EXISTING_USER="$(getent passwd "${USER_ID}" | cut -d: -f1)"; \
21
+ if [ "$EXISTING_USER" != "${USER_NAME}" ]; then \
22
+ usermod -l "${USER_NAME}" "$EXISTING_USER"; \
23
+ usermod -d "/home/${USER_NAME}" -m "${USER_NAME}"; \
24
+ fi; \
25
+ else \
26
+ useradd -u "${USER_ID}" -m "${USER_NAME}"; \
27
+ fi
28
+
29
+ WORKDIR /home/stoobly
30
+
31
+ COPY .entrypoint.sh /usr/local/bin/entrypoint.sh
32
+ RUN chmod +x /usr/local/bin/entrypoint.sh
33
+ ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
@@ -0,0 +1,18 @@
1
+ services:
2
+ entrypoint.playwright:
3
+ build:
4
+ args:
5
+ PLAYWRIGHT_IMAGE: ${PLAYWRIGHT_IMAGE:-mcr.microsoft.com/playwright:v1.46.1-jammy}
6
+ USER_ID: ${USER_ID}
7
+ context: ./
8
+ dockerfile: ./.Dockerfile.playwright
9
+ command: npx playwright test --reporter dot
10
+ depends_on:
11
+ entrypoint.configure:
12
+ condition: service_completed_successfully
13
+ networks:
14
+ - "app.ingress"
15
+ profiles:
16
+ - ${WORKFLOW_NAME}
17
+ volumes:
18
+ - ${CONTEXT_DIR}:/home/stoobly
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ # Copy certs and update trust store
5
+ if [ -d "/home/stoobly/.stoobly/certs" ]; then
6
+ cp /home/stoobly/.stoobly/certs/*.crt /usr/local/share/ca-certificates/ || true
7
+ update-ca-certificates
8
+ fi
9
+
10
+ # Execute the CMD from Dockerfile or passed command
11
+ exec gosu stoobly "$@"
@@ -0,0 +1,17 @@
1
+ # Define custom services here
2
+ #
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -0,0 +1,17 @@
1
+ # Define custom services here
2
+ #
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.egress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -0,0 +1,17 @@
1
+ # Define custom services here
2
+ #
3
+ # 1. Uncomment the following
4
+ # 2. Replace <SERVICE-NAME> with the name of the service, this can be found in ../config.yml
5
+ # 3. Extend as needed
6
+ #
7
+ # To learn more, see https://docs.stoobly.com/core-concepts/scaffold
8
+
9
+ #services:
10
+ # <SERVICE-NAME>.<CUSTOM-SERVICE-NAME>:
11
+ # depends_on:
12
+ # <SERVICE-NAME>.configure:
13
+ # condition: service_completed_successfully
14
+ # networks:
15
+ # app.ingress: {}
16
+ # profiles:
17
+ # - ${WORKFLOW_NAME}
@@ -92,6 +92,5 @@ class WorkflowCreateCommand(WorkflowCommand):
92
92
  workflow_decorator(workflow_builder).decorate()
93
93
 
94
94
  workflow_builder.write()
95
- workflow_builder.initialize_custom_file()
96
95
 
97
96
  return workflow_builder
@@ -1,8 +1,8 @@
1
1
  import os
2
-
2
+ import shutil
3
3
 
4
4
  from .app import App
5
- from .constants import DOTENV_FILE, NAMESERVERS_FILE
5
+ from .constants import DOTENV_PATH_ENV, DOTENV_FILE, NAMESERVERS_FILE
6
6
 
7
7
  class WorkflowNamespace():
8
8
 
@@ -35,6 +35,12 @@ class WorkflowNamespace():
35
35
  def traefik_config_path(self):
36
36
  return os.path.join(self.path, 'traefik.yml')
37
37
 
38
+ def copy_dotenv(self):
39
+ dotenv_path = os.environ.get(DOTENV_PATH_ENV) or '.env'
40
+
41
+ if os.path.isfile(dotenv_path):
42
+ shutil.copy(dotenv_path, self.dotenv_path)
43
+
38
44
  def traefik_config_relative_path(self, path: str):
39
45
  if not path:
40
46
  return path
@@ -10,7 +10,7 @@ from stoobly_agent.lib.logger import Logger
10
10
 
11
11
  from .app import App
12
12
  from .constants import (
13
- APP_DIR_ENV, APP_NETWORK_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV,
13
+ APP_DIR_ENV, APP_NETWORK_ENV, APP_PLUGINS_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV,
14
14
  SERVICE_DNS_ENV, SERVICE_NAME_ENV, SERVICE_SCRIPTS_DIR, SERVICE_SCRIPTS_ENV, USER_ID_ENV,
15
15
  WORKFLOW_NAME_ENV, WORKFLOW_NAMESPACE_ENV, WORKFLOW_SCRIPTS_DIR, WORKFLOW_SCRIPTS_ENV, WORKFLOW_TEMPLATE_ENV
16
16
  )
@@ -14,6 +14,7 @@ from stoobly_agent.app.cli.scaffold.app_create_command import AppCreateCommand
14
14
  from stoobly_agent.app.cli.scaffold.constants import (
15
15
  DOCKER_NAMESPACE, WORKFLOW_CONTAINER_PROXY, WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
16
16
  )
17
+ from stoobly_agent.app.cli.scaffold.constants import PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT
17
18
  from stoobly_agent.app.cli.scaffold.containerized_app import ContainerizedApp
18
19
  from stoobly_agent.app.cli.scaffold.docker.service.configure_gateway import configure_gateway
19
20
  from stoobly_agent.app.cli.scaffold.docker.workflow.decorators_factory import get_workflow_decorators
@@ -89,7 +90,8 @@ def hostname(ctx):
89
90
  help="Scaffold application"
90
91
  )
91
92
  @click.option('--app-dir-path', default=current_working_dir, help='Path to create the app scaffold.')
92
- @click.option('--docker-socket-path', default='/var/run/docker.sock', help='Path to Docker socket.')
93
+ @click.option('--docker-socket-path', default='/var/run/docker.sock', type=click.Path(exists=True, file_okay=True, dir_okay=False), help='Path to Docker socket.')
94
+ @click.option('--plugin', multiple=True, type=click.Choice([PLUGIN_CYPRESS, PLUGIN_PLAYWRIGHT]), help='Scaffold integrations.')
93
95
  @click.option('--quiet', is_flag=True, help='Disable log output.')
94
96
  @click.option('--ui-port', default=4200, type=click.IntRange(1, 65535), help='UI service port.')
95
97
  @click.argument('app_name', callback=validate_app_name)
@@ -101,7 +103,10 @@ def create(**kwargs):
101
103
  if not kwargs['quiet'] and os.path.exists(app.scaffold_namespace_path):
102
104
  print(f"{kwargs['app_dir_path']} already exists, updating scaffold maintained files...")
103
105
 
104
- AppCreateCommand(app, **kwargs).build()
106
+ res = AppCreateCommand(app, **kwargs).build()
107
+
108
+ for warning in res['warnings']:
109
+ print(f"{bcolors.WARNING}WARNING{bcolors.ENDC}: {warning}")
105
110
 
106
111
  @app.command(
107
112
  help="Scaffold app service certs"
@@ -129,24 +134,20 @@ def mkcert(**kwargs):
129
134
  @click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
130
135
  @click.option('--env', multiple=True, help='Specify an environment variable.')
131
136
  @click.option('--hostname', callback=validate_hostname, help='Service hostname.')
137
+ @click.option('--local', is_flag=True, help='Specifies upstream service is local. Overrides `--upstream-hostname` option.')
132
138
  @click.option('--port', type=click.IntRange(1, 65535), help='Service port.')
133
139
  @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
134
- @click.option('--proxy-mode', help='''
135
- Proxy mode can be "regular", "transparent", "socks5",
136
- "reverse:SPEC", or "upstream:SPEC". For reverse and
137
- upstream proxy modes, SPEC is host specification in
138
- the form of "http[s]://host[:port]".
139
- ''')
140
+ @click.option('--proxy-mode', type=click.Choice(['regular', 'reverse']), help='Proxy mode can be regular or reverse.')
140
141
  @click.option('--quiet', is_flag=True, help='Disable log output.')
141
142
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
143
+ @click.option('--upstream-hostname', callback=validate_hostname, help='Upstream service hostname.')
144
+ @click.option('--upstream-port', type=click.IntRange(1, 65535), help='Upstream service port.')
145
+ @click.option('--upstream-scheme', type=click.Choice(['http', 'https']), help='Upstream service scheme.')
142
146
  @click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
143
147
  @click.argument('service_name', callback=validate_service_name)
144
148
  def create(**kwargs):
145
149
  __validate_app_dir(kwargs['app_dir_path'])
146
150
 
147
- if kwargs.get("proxy_mode"):
148
- __validate_proxy_mode(kwargs.get("proxy_mode"))
149
-
150
151
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
151
152
  service = Service(kwargs['service_name'], app)
152
153
 
@@ -164,15 +165,17 @@ def create(**kwargs):
164
165
  @click.option('--select', multiple=True, help='Select column(s) to display.')
165
166
  @click.option('--service', multiple=True, help='Select specific services.')
166
167
  @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
168
+ @click.option('--all', is_flag=True, default=False, help='Display all services including core and user defined services')
167
169
  @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by. Defaults to all.')
168
170
  def _list(**kwargs):
169
171
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
170
172
  __validate_app(app)
171
173
 
172
- services = __get_services(app, service=kwargs['service'], workflow=kwargs['workflow'])
174
+ without_core = not kwargs['all']
175
+ services = __get_services(app, service=kwargs['service'], without_core=without_core, workflow=kwargs['workflow'])
173
176
 
174
177
  rows = []
175
- for service_name in services:
178
+ for service_name in services:
176
179
  service = Service(service_name, app)
177
180
  __validate_service_dir(service.dir_path)
178
181
 
@@ -184,6 +187,22 @@ def _list(**kwargs):
184
187
 
185
188
  print_services(rows, **select_print_options(kwargs))
186
189
 
190
+ @service.command(
191
+ help="Show information about a service",
192
+ )
193
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
194
+ @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
195
+ @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
196
+ @click.argument('service_name')
197
+ @click.pass_context
198
+ def show(ctx, **kwargs):
199
+ service_name = kwargs['service_name']
200
+ del kwargs['service_name']
201
+ kwargs['service'] = [service_name]
202
+
203
+ # Invoke list with 1 service
204
+ ctx.invoke(_list, **kwargs)
205
+
187
206
  @service.command(
188
207
  help="Delete a service",
189
208
  )
@@ -207,16 +226,15 @@ def delete(**kwargs):
207
226
  )
208
227
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
209
228
  @click.option('--hostname', callback=validate_hostname, help='Service hostname.')
229
+ @click.option('--local', is_flag=True, help='Specifies upstream service is local. Overrides `--upstream-hostname` option.')
230
+ @click.option('--name', callback=validate_service_name, type=click.STRING, help='New name of the service to update to.')
210
231
  @click.option('--port', type=click.IntRange(1, 65535), help='Service port.')
211
- @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
232
+ @click.option('--priority', type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
233
+ @click.option('--proxy-mode', type=click.Choice(['regular', 'reverse']), help='Proxy mode can be regular or reverse.')
212
234
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
213
- @click.option('--name', callback=validate_service_name, type=click.STRING, help='New name of the service to update to.')
214
- @click.option('--proxy-mode', help='''
215
- Proxy mode can be "regular", "transparent", "socks5",
216
- "reverse:SPEC", or "upstream:SPEC". For reverse and
217
- upstream proxy modes, SPEC is host specification in
218
- the form of "http[s]://host[:port]".
219
- ''')
235
+ @click.option('--upstream-hostname', callback=validate_hostname, help='Upstream service hostname.')
236
+ @click.option('--upstream-port', type=click.IntRange(1, 65535), help='Upstream service port.')
237
+ @click.option('--upstream-scheme', type=click.Choice(['http', 'https']), help='Upstream service scheme.')
220
238
  @click.argument('service_name')
221
239
  def update(**kwargs):
222
240
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
@@ -228,29 +246,32 @@ def update(**kwargs):
228
246
 
229
247
  service_config = ServiceConfig(service.dir_path)
230
248
 
231
- if kwargs['hostname']:
232
- old_hostname = service_config.hostname
249
+ service_config.local = kwargs['local']
233
250
 
234
- if old_hostname != kwargs['hostname']:
235
- service_config.hostname = kwargs['hostname']
236
-
237
- # If this is the default proxy_mode and the origin matches the original hostname, assume it is safe to update with the new hostname
238
- if service_config.proxy_mode.startswith("reverse:"):
239
- old_origin = service_config.proxy_mode.split("reverse:")[1]
240
- parsed_origin_url = urlparse(old_origin)
251
+ if kwargs['hostname']:
252
+ service_config.hostname = kwargs['hostname']
241
253
 
242
- if old_hostname == parsed_origin_url.hostname:
243
- service_config.proxy_mode = service_config.proxy_mode.replace(old_hostname, service_config.hostname)
254
+ if kwargs['port']:
255
+ service_config.port = kwargs['port']
244
256
 
245
257
  if kwargs['priority']:
246
258
  service_config.priority = kwargs['priority']
247
259
 
248
- if kwargs['port']:
249
- service_config.port = kwargs['port']
260
+ if kwargs['proxy_mode']:
261
+ service_config.proxy_mode = kwargs['proxy_mode']
250
262
 
251
263
  if kwargs['scheme']:
252
264
  service_config.scheme = kwargs['scheme']
253
265
 
266
+ if kwargs['upstream_hostname']:
267
+ service_config.upstream_hostname = kwargs['upstream_hostname']
268
+
269
+ if kwargs['upstream_port']:
270
+ service_config.upstream_port = kwargs['upstream_port']
271
+
272
+ if kwargs['upstream_scheme']:
273
+ service_config.upstream_scheme = kwargs['upstream_scheme']
274
+
254
275
  if kwargs['name']:
255
276
  old_service_name = service.service_name
256
277
  new_service_name = kwargs['name']
@@ -264,10 +285,6 @@ def update(**kwargs):
264
285
 
265
286
  print(f"Successfully renamed service to: {new_service_name}")
266
287
 
267
- if kwargs['proxy_mode']:
268
- __validate_proxy_mode(kwargs['proxy_mode'])
269
- service_config.proxy_mode = kwargs['proxy_mode']
270
-
271
288
  service_config.write()
272
289
 
273
290
  @workflow.command(
@@ -314,7 +331,7 @@ def copy(**kwargs):
314
331
  config = { **kwargs }
315
332
  del config['service']
316
333
  config['service_name'] = service_name
317
-
334
+
318
335
  command = WorkflowCopyCommand(app, **config)
319
336
 
320
337
  if not command.app_dir_exists:
@@ -339,7 +356,7 @@ def copy(**kwargs):
339
356
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
340
357
  @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
341
358
  @click.argument('workflow_name')
342
- def down(**kwargs):
359
+ def down(**kwargs):
343
360
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
344
361
 
345
362
  containerized = kwargs['containerized']
@@ -349,6 +366,7 @@ def down(**kwargs):
349
366
  __validate_app(app)
350
367
 
351
368
  __with_namespace_defaults(kwargs)
369
+ __with_workflow_namespace(app, kwargs['namespace'])
352
370
 
353
371
  services = __get_services(
354
372
  app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
@@ -383,7 +401,7 @@ def down(**kwargs):
383
401
  )
384
402
  if not exec_command:
385
403
  continue
386
-
404
+
387
405
  print(exec_command, file=script)
388
406
 
389
407
  # After services are stopped, their network needs to be removed
@@ -447,7 +465,7 @@ def logs(**kwargs):
447
465
  continue
448
466
 
449
467
  config = { **kwargs }
450
- config['service_name'] = service
468
+ config['service_name'] = service
451
469
  command = WorkflowLogCommand(app, **config)
452
470
  commands.append(command)
453
471
 
@@ -503,6 +521,7 @@ def up(**kwargs):
503
521
  __validate_app(app)
504
522
 
505
523
  __with_namespace_defaults(kwargs)
524
+ workflow_namespace = __with_workflow_namespace(app, kwargs['namespace'])
506
525
 
507
526
  services = __get_services(
508
527
  app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
@@ -514,11 +533,7 @@ def up(**kwargs):
514
533
 
515
534
  # Gateway ports are dynamically set depending on the workflow run
516
535
  workflow = Workflow(kwargs['workflow_name'], app)
517
- configure_gateway(
518
- WorkflowNamespace(app, kwargs['namespace'] or workflow.workflow_name),
519
- workflow.service_paths_from_services(services),
520
- kwargs['no_publish']
521
- )
536
+ configure_gateway(workflow_namespace, workflow.service_paths_from_services(services), kwargs['no_publish'])
522
537
 
523
538
  commands: List[WorkflowRunCommand] = []
524
539
  for service in services:
@@ -587,7 +602,7 @@ def validate(**kwargs):
587
602
  __validate_app(app)
588
603
 
589
604
  workflow = Workflow(kwargs['workflow_name'], app)
590
-
605
+
591
606
  config = { **kwargs }
592
607
  config['service_name'] = 'build'
593
608
 
@@ -627,7 +642,7 @@ def install(**kwargs):
627
642
  )
628
643
 
629
644
  hostnames = []
630
- for service_name in services:
645
+ for service_name in services:
631
646
  service = Service(service_name, app)
632
647
  __validate_service_dir(service.dir_path)
633
648
 
@@ -659,7 +674,7 @@ def uninstall(**kwargs):
659
674
  )
660
675
 
661
676
  hostnames = []
662
- for service_name in services:
677
+ for service_name in services:
663
678
  service = Service(service_name, app)
664
679
  __validate_service_dir(service.dir_path)
665
680
 
@@ -736,7 +751,7 @@ def __get_services(app: App, **kwargs):
736
751
 
737
752
  services = list(set(selected_services))
738
753
  services.sort()
739
-
754
+
740
755
  return services
741
756
 
742
757
  def __print_header(text: str):
@@ -773,7 +788,7 @@ def __services_mkcert(app: App, services):
773
788
  continue
774
789
 
775
790
  hostname = service_config.hostname
776
-
791
+
777
792
  if not hostname:
778
793
  continue
779
794
 
@@ -788,8 +803,11 @@ def __validate_app(app: App):
788
803
  sys.exit(1)
789
804
 
790
805
  def __validate_app_dir(app_dir_path):
791
- if not os.path.exists(app_dir_path):
792
- print(f"Error: {app_dir_path} does not exist", file=sys.stderr)
806
+ __validate_dir(app_dir_path)
807
+
808
+ def __validate_dir(dir_path):
809
+ if not os.path.exists(dir_path):
810
+ print(f"Error: {dir_path} does not exist", file=sys.stderr)
793
811
  sys.exit(1)
794
812
 
795
813
  def __validate_service_dir(service_dir_path):
@@ -797,35 +815,6 @@ def __validate_service_dir(service_dir_path):
797
815
  print(f"Error: '{service_dir_path}' does not exist, please scaffold this service", file=sys.stderr)
798
816
  sys.exit(1)
799
817
 
800
- def __validate_proxy_mode(proxy_mode: str) -> None:
801
- valid_exact_matches = {
802
- "regular": None,
803
- "transparent": None,
804
- "socks5": None,
805
- }
806
-
807
- valid_prefixes = {
808
- "reverse": None,
809
- "upstream": None
810
- }
811
-
812
- if proxy_mode in valid_exact_matches:
813
- return
814
-
815
- split_str = proxy_mode.split(":", 1)
816
- if len(split_str) != 2:
817
- print(f"Error: {proxy_mode} is invalid.", file=sys.stderr)
818
- sys.exit(1)
819
-
820
- prefix = split_str[0]
821
- spec = split_str[1]
822
-
823
- if prefix not in valid_prefixes:
824
- print(f"Error: {proxy_mode} is invalid.", file=sys.stderr)
825
- sys.exit(1)
826
-
827
- # TODO: validate SPEC
828
-
829
818
  def __with_namespace_defaults(kwargs):
830
819
  if not kwargs.get('namespace'):
831
820
  kwargs['namespace'] = kwargs.get('workflow_name')
@@ -840,3 +829,8 @@ def __workflow_create(app, **kwargs):
840
829
  template=kwargs['template'],
841
830
  workflow_decorators=workflow_decorators
842
831
  )
832
+
833
+ def __with_workflow_namespace(app: App, namespace: str):
834
+ workflow_namespace = WorkflowNamespace(app, namespace)
835
+ workflow_namespace.copy_dotenv()
836
+ return workflow_namespace