stoobly-agent 1.4.1__py3-none-any.whl → 1.5.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 (65) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/handle_mock_service.py +6 -2
  3. stoobly_agent/app/cli/helpers/request_facade.py +5 -1
  4. stoobly_agent/app/cli/scaffold/constants.py +1 -1
  5. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +3 -2
  6. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +2 -2
  7. stoobly_agent/app/cli/scaffold/service_config.py +16 -2
  8. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +19 -19
  9. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  10. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +12 -5
  11. stoobly_agent/app/cli/scaffold/templates/constants.py +3 -3
  12. stoobly_agent/app/cli/scaffold/templates/factory.py +5 -5
  13. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +1 -8
  14. stoobly_agent/app/cli/scaffold/templates/workflow/mock/fixtures.yml +1 -1
  15. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +1 -8
  16. stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml +1 -1
  17. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  18. stoobly_agent/app/cli/scaffold/workflow_create_command.py +2 -2
  19. stoobly_agent/app/cli/scaffold_cli.py +65 -50
  20. stoobly_agent/app/proxy/context.py +4 -0
  21. stoobly_agent/app/proxy/handle_mock_service.py +81 -54
  22. stoobly_agent/app/proxy/handle_record_service.py +15 -3
  23. stoobly_agent/app/proxy/handle_replay_service.py +44 -18
  24. stoobly_agent/app/proxy/handle_test_service.py +75 -16
  25. stoobly_agent/app/proxy/intercept_handler.py +11 -16
  26. stoobly_agent/app/proxy/intercept_settings.py +17 -4
  27. stoobly_agent/app/proxy/mitmproxy/request_facade.py +5 -2
  28. stoobly_agent/app/proxy/mitmproxy/response_facade.py +5 -4
  29. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +78 -14
  30. stoobly_agent/app/proxy/mock/eval_request_service.py +2 -2
  31. stoobly_agent/app/proxy/record/join_request_service.py +7 -8
  32. stoobly_agent/app/proxy/record/upload_request_service.py +2 -2
  33. stoobly_agent/app/proxy/replay/replay_request_service.py +4 -4
  34. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +2 -2
  35. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -3
  36. stoobly_agent/app/proxy/utils/response_handler.py +0 -2
  37. stoobly_agent/app/proxy/utils/rewrite.py +72 -0
  38. stoobly_agent/app/settings/constants/request_component.py +4 -1
  39. stoobly_agent/cli.py +35 -28
  40. stoobly_agent/config/constants/intercept_policy.py +2 -0
  41. stoobly_agent/config/constants/mock_policy.py +4 -2
  42. stoobly_agent/config/constants/record_policy.py +4 -2
  43. stoobly_agent/config/constants/replay_policy.py +4 -2
  44. stoobly_agent/public/{18-es2015.583f191cc7ad512ee262.js → 18-es2015.503207073756a9c8211a.js} +1 -1
  45. stoobly_agent/public/{18-es5.583f191cc7ad512ee262.js → 18-es5.503207073756a9c8211a.js} +1 -1
  46. stoobly_agent/public/index.html +1 -1
  47. stoobly_agent/public/{main-es2015.2cc16523aa3fcaba51e5.js → main-es2015.d682619f3d6d53d64c6a.js} +1 -1
  48. stoobly_agent/public/{main-es5.2cc16523aa3fcaba51e5.js → main-es5.d682619f3d6d53d64c6a.js} +1 -1
  49. stoobly_agent/public/{runtime-es2015.b914470164e4d6e75d96.js → runtime-es2015.8c1efed946fc02c923fc.js} +1 -1
  50. stoobly_agent/public/{runtime-es5.b914470164e4d6e75d96.js → runtime-es5.8c1efed946fc02c923fc.js} +1 -1
  51. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +2 -1
  52. stoobly_agent/test/app/cli/scaffold/e2e_test.py +2 -2
  53. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  54. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +140 -71
  55. stoobly_agent/test/cli/lifecycle_hooks_test.py +66 -0
  56. stoobly_agent/test/cli/mock_test.py +53 -29
  57. stoobly_agent/test/cli/record_test.py +67 -0
  58. stoobly_agent/test/mock_data/lifecycle_hooks.py +35 -0
  59. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/LICENSE +1 -1
  60. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/METADATA +7 -12
  61. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/RECORD +65 -61
  62. /stoobly_agent/app/cli/scaffold/templates/workflow/mock/{fixtures/.keep → public/.gitignore} +0 -0
  63. /stoobly_agent/app/cli/scaffold/templates/workflow/test/{fixtures/.keep → public/.gitignore} +0 -0
  64. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/WHEEL +0 -0
  65. {stoobly_agent-1.4.1.dist-info → stoobly_agent-1.5.0.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.4.1'
2
+ VERSION = '1.5.0'
@@ -6,9 +6,13 @@ from stoobly_agent.app.proxy.record.response_string import ResponseString
6
6
 
7
7
  RAW_FORMAT = 'raw'
8
8
 
9
- def print_raw_response(response: requests.Response):
9
+ def print_raw_response(response: requests.Response, file_path = None):
10
10
  mitmproxy_response = PythonResponseAdapterFactory(response).mitmproxy_response()
11
11
  facade = MitmproxyResponseFacade(mitmproxy_response)
12
12
  response_string = ResponseString(facade, None)
13
13
 
14
- print(response_string.get().decode(), end="")
14
+ if not file_path:
15
+ print(response_string.get().decode(), end="")
16
+ else:
17
+ with open(file_path, 'w') as fp:
18
+ fp.write(response_string.get().decode())
@@ -53,10 +53,14 @@ class RequestFacade(ReplayFacade):
53
53
  def replay(self, request_key: str, cli_options: ReplayCliOptions):
54
54
  replay_context = self.__build_replay_context(request_key)
55
55
  replay_options = {
56
- 'mode': mode.RECORD if cli_options.get('record') else mode.REPLAY,
56
+ 'mode': mode.REPLAY,
57
57
  **self.__common_replay_options(request_key),
58
58
  **self.common_replay_cli_options(cli_options)
59
59
  }
60
+
61
+ if cli_options.get('record'):
62
+ replay_options['response_mode'] = mode.RECORD
63
+
60
64
  trace_context = replay_options.get('trace_context')
61
65
 
62
66
  return self.__replay(replay_context, trace_context, replay_options)
@@ -14,8 +14,8 @@ CONFIG_FILE = '.config.yml'
14
14
  CONTEXT_DIR_ENV = 'CONTEXT_DIR'
15
15
  DOCKER_NAMESPACE = 'docker'
16
16
  ENV_FILE = '.env'
17
- FIXTURES_FOLDER_NAME = 'fixtures'
18
17
  NAMESERVERS_FILE = '.nameservers'
18
+ PUBLIC_FOLDER_NAME = 'public'
19
19
  SERVICE_DETACHED = '${SERVICE_DETACHED}'
20
20
  SERVICE_DETACHED_ENV = 'SERVICE_DETACHED'
21
21
  SERVICE_HOSTNAME = '${SERVICE_HOSTNAME}'
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import pdb
3
3
 
4
- from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
4
+ from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, SERVICE_PROXY_MODE, STOOBLY_CERTS_DIR
5
5
  from .builder import WorkflowBuilder
6
6
 
7
7
  class MockDecorator():
@@ -24,8 +24,9 @@ class MockDecorator():
24
24
  '--headless',
25
25
  '--intercept',
26
26
  '--lifecycle-hooks-path', 'lifecycle_hooks.py',
27
- '--proxy-mode', config.proxy_mode,
27
+ '--proxy-mode', SERVICE_PROXY_MODE,
28
28
  '--proxy-port', f"{SERVICE_PORT}",
29
+ '--public-directory-path', 'public',
29
30
  '--response-fixtures-path', 'fixtures.yml',
30
31
  '--ssl-insecure'
31
32
  ]
@@ -3,7 +3,7 @@ import pdb
3
3
 
4
4
  from urllib.parse import urlparse
5
5
 
6
- from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, STOOBLY_CERTS_DIR
6
+ from ...constants import SERVICE_HOSTNAME, SERVICE_PORT, SERVICE_PROXY_MODE, STOOBLY_CERTS_DIR
7
7
  from .builder import WorkflowBuilder
8
8
 
9
9
  class ReverseProxyDecorator():
@@ -25,7 +25,7 @@ class ReverseProxyDecorator():
25
25
  command = [
26
26
  '--headless',
27
27
  '--lifecycle-hooks-path', 'lifecycle_hooks.py',
28
- '--proxy-mode', config.proxy_mode,
28
+ '--proxy-mode', SERVICE_PROXY_MODE,
29
29
  '--proxy-port', f"{SERVICE_PORT}",
30
30
  '--ssl-insecure'
31
31
  ]
@@ -92,7 +92,21 @@ class ServiceConfig(Config):
92
92
  if self.__proxy_mode:
93
93
  return (self.__proxy_mode or '').strip()
94
94
 
95
- return f"reverse:{self.scheme}://{self.hostname}"
95
+ if not self.hostname:
96
+ return ''
97
+
98
+ _proxy_mode = f"reverse:{self.scheme}://{self.hostname}"
99
+
100
+ if not self.port:
101
+ return _proxy_mode
102
+
103
+ if self.scheme == 'http' and self.port == '80':
104
+ return _proxy_mode
105
+
106
+ if self.scheme == 'https' and self.port == '443':
107
+ return _proxy_mode
108
+
109
+ return f"{_proxy_mode}:{self.port}"
96
110
 
97
111
  @proxy_mode.setter
98
112
  def proxy_mode(self, v):
@@ -123,7 +137,7 @@ class ServiceConfig(Config):
123
137
  'port': self.port,
124
138
  'priority': self.priority,
125
139
  'proxy_mode': self.proxy_mode,
126
- 'scheme': self.scheme,
140
+ 'scheme': self.scheme if self.hostname else '',
127
141
  }
128
142
 
129
143
  def write(self):
@@ -10,7 +10,7 @@ import yaml
10
10
  from docker.models.containers import Container
11
11
 
12
12
  from stoobly_agent.app.cli.scaffold.constants import (
13
- FIXTURES_FOLDER_NAME,
13
+ PUBLIC_FOLDER_NAME,
14
14
  STOOBLY_DATA_DIR,
15
15
  VIRTUAL_HOST_ENV,
16
16
  VIRTUAL_PORT_ENV,
@@ -40,8 +40,8 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
40
40
  )
41
41
 
42
42
  @property
43
- def fixtures_dir_path(self):
44
- return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
43
+ def public_dir_path(self):
44
+ return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
45
45
 
46
46
  @property
47
47
  def workflow_path(self):
@@ -124,14 +124,14 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
124
124
  if ('200 OK' not in logs) and ('499' not in logs):
125
125
  raise ScaffoldValidateException(f"Error reaching {url} from inside Docker network")
126
126
 
127
- # Check fixtures folder mounted into container
128
- def validate_fixtures_folder(self, container: Container):
127
+ # Check public folder exists in container
128
+ def validate_public_folder(self, container: Container):
129
129
 
130
130
  if self.workflow_name == WORKFLOW_RECORD_TYPE:
131
- print(f"Skipping validating fixtures folder in workflow: {self.workflow_name}, container: {container.name}")
131
+ print(f"Skipping validating public folder in workflow: {self.workflow_name}, container: {container.name}")
132
132
  return
133
133
 
134
- print(f"Validating fixtures folder in container: {container.name}")
134
+ print(f"Validating public folder in container: {container.name}")
135
135
 
136
136
  data_dir_mounted = False
137
137
  volume_mounts = container.attrs['Mounts']
@@ -145,21 +145,21 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
145
145
 
146
146
  # Only the running proxy containers will be checkable
147
147
  if container.status == 'exited':
148
- print(f"Skipping validating fixtures folder contents because container is exited: {container.name}")
148
+ print(f"Skipping validating public folder contents because container is exited: {container.name}")
149
149
  return
150
150
 
151
- # Check contents of fixtures folder to confirm it's shared
152
- fixtures_folder_path = f"{FIXTURES_FOLDER_NAME}"
153
- exec_result = container.exec_run(f"ls -A {fixtures_folder_path}")
151
+ # Check contents of public folder to confirm it's shared
152
+ public_folder_path = f"{PUBLIC_FOLDER_NAME}"
153
+ exec_result = container.exec_run(f"ls -A {public_folder_path}")
154
154
  output = exec_result.output
155
155
 
156
- fixtures_folder_contents_container = output.decode('ascii').split('\n')
157
- if fixtures_folder_contents_container[-1] == '':
158
- fixtures_folder_contents_container.pop()
159
- fixtures_folder_contents_scaffold = os.listdir(self.fixtures_dir_path)
156
+ public_folder_contents_container = output.decode('ascii').split('\n')
157
+ if public_folder_contents_container[-1] == '':
158
+ public_folder_contents_container.pop()
159
+ public_folder_contents_scaffold = os.listdir(self.public_dir_path)
160
160
 
161
- if Counter(fixtures_folder_contents_container) != Counter(fixtures_folder_contents_scaffold):
162
- raise ScaffoldValidateException(f"Fixtures was not mounted properly, expected {self.fixtures_dir_path} to exist in container path {fixtures_folder_path}")
161
+ if Counter(public_folder_contents_container) != Counter(public_folder_contents_scaffold):
162
+ raise ScaffoldValidateException(f"public folder was not mounted properly, expected {self.public_dir_path} to exist in container path {public_folder_path}")
163
163
 
164
164
  # Note: might not need this if the hostname is reachable and working
165
165
  def proxy_environment_variables_exist(self, container: Container) -> None:
@@ -191,7 +191,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
191
191
  raise ScaffoldValidateException(f"Container attributes are missing for: {container.name}")
192
192
 
193
193
  if not self.service_config.detached:
194
- self.validate_fixtures_folder(service_proxy_container)
194
+ self.validate_public_folder(service_proxy_container)
195
195
 
196
196
  self.proxy_environment_variables_exist(service_proxy_container)
197
197
 
@@ -215,7 +215,7 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
215
215
 
216
216
  # Service init containers have a mounted dist folder unlike the core init container
217
217
  init_container = self.docker_client.containers.get(self.service_docker_compose.init_container_name)
218
- self.validate_fixtures_folder(init_container)
218
+ self.validate_public_folder(init_container)
219
219
 
220
220
  if self.service_config.hostname:
221
221
  service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.4
1
+ FROM stoobly/agent:1.5
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -61,8 +61,15 @@ stoobly_exec_run_env=$(source_env) && $(exec_env) && export CONTEXT_DIR="$(app_d
61
61
  workflow_run=$(source_env) && bash "$(workflow_run_script)"
62
62
 
63
63
  ca-cert/install: stoobly/install
64
- @echo "Running stoobly-agent ca-cert install..."; \
65
- stoobly-agent ca-cert install
64
+ @if [ ! -d "$$HOME/.mitmproxy" ]; then \
65
+ read -p "Installing CA certificate is required for $(WORKFLOW)ing requests, continue? (y/N) " confirm && \
66
+ if [ "$$confirm" = "y" ] || [ "$$confirm" = "Y" ]; then \
67
+ echo "Running stoobly-agent ca-cert install..."; \
68
+ stoobly-agent ca-cert install; \
69
+ else \
70
+ echo "You can install the CA certificate later by running: stoobly-agent ca-cert install"; \
71
+ fi \
72
+ fi
66
73
  certs:
67
74
  @export EXEC_COMMAND=bin/.mkcert && \
68
75
  $(stoobly_exec)
@@ -89,7 +96,7 @@ intercept/enable:
89
96
  @export EXEC_COMMAND=bin/.enable && \
90
97
  export EXEC_ARGS=$(scenario_key) && \
91
98
  $(stoobly_exec)
92
- mock: workflow/mock workflow/hostname/install nameservers workflow/up
99
+ mock: workflow/mock ca-cert/install workflow/hostname/install nameservers workflow/up
93
100
  mock/services: workflow/mock workflow/services
94
101
  mock/logs: workflow/mock workflow/logs
95
102
  mock/down: workflow/mock workflow/down workflow/hostname/uninstall
@@ -103,7 +110,7 @@ python/validate:
103
110
  echo "Error: Python 3.10, 3.11, or 3.12 is required."; \
104
111
  exit 1; \
105
112
  fi
106
- record: workflow/record workflow/hostname/install nameservers workflow/up
113
+ record: workflow/record ca-cert/install workflow/hostname/install nameservers workflow/up
107
114
  record/down: workflow/record workflow/down workflow/hostname/uninstall
108
115
  record/services: workflow/record workflow/services
109
116
  record/logs: workflow/record workflow/logs
@@ -139,7 +146,7 @@ scenario/snapshot:
139
146
  stoobly/install: python/validate pipx/install
140
147
  @if ! pipx list 2> /dev/null | grep -q 'stoobly-agent'; then \
141
148
  echo "stoobly-agent not found. Installing..."; \
142
- pipx install stoobly-agent; \
149
+ pipx install stoobly-agent || { echo "Failed to install stoobly-agent"; exit 1; }; \
143
150
  fi
144
151
  test: workflow/test workflow/up
145
152
  test/services: workflow/test workflow/services
@@ -18,12 +18,12 @@ CUSTOM_FIXTURES = 'fixtures.yml'
18
18
  CUSTOM_LIFECYCLE_HOOKS = os.path.join('lifecycle_hooks.py')
19
19
  MAINTAINED_CONFIGURE = os.path.join('bin', '.configure')
20
20
  MAINTAINED_INIT = os.path.join('bin', '.init')
21
- MAINTAINED_FIXTURES = os.path.join('fixtures', '.keep')
21
+ MAINTAINED_PUBLIC = os.path.join('public', '.gitignore')
22
22
 
23
23
  MOCK_WORKFLOW_MAINTAINED_FILES = [
24
24
  MAINTAINED_CONFIGURE,
25
25
  MAINTAINED_INIT,
26
- MAINTAINED_FIXTURES
26
+ MAINTAINED_PUBLIC
27
27
  ]
28
28
 
29
29
  MOCK_WORKFLOW_CUSTOM_FILES = [
@@ -49,7 +49,7 @@ RECORD_WORKFLOW_CUSTOM_FILES = [
49
49
  TEST_WORKFLOW_MAINTAINED_FILES = [
50
50
  MAINTAINED_CONFIGURE,
51
51
  MAINTAINED_INIT,
52
- MAINTAINED_FIXTURES
52
+ MAINTAINED_PUBLIC
53
53
  ]
54
54
 
55
55
  TEST_WORKFLOW_CUSTOM_FILES = [
@@ -1,7 +1,7 @@
1
1
  from ..constants import WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE
2
2
  from ..docker.workflow.builder import WorkflowBuilder
3
3
  from .constants import (
4
- CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE, MAINTAINED_FIXTURES, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
4
+ CUSTOM_CONFIGURE, CUSTOM_INIT, MAINTAINED_CONFIGURE, MAINTAINED_PUBLIC, MOCK_WORKFLOW_CUSTOM_FILES, MOCK_WORKFLOW_MAINTAINED_FILES, RECORD_WORKFLOW_CUSTOM_FILES, RECORD_WORKFLOW_MAINTAINED_FILES, TEST_WORKFLOW_CUSTOM_FILES, TEST_WORKFLOW_MAINTAINED_FILES
5
5
  )
6
6
 
7
7
  def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
@@ -21,8 +21,8 @@ def custom_files(workflow: str, workflow_builder: WorkflowBuilder):
21
21
 
22
22
  # Fixtures are only relevant if the workflow is mock/test and if the service has a hostname
23
23
  if not workflow_builder.config.hostname:
24
- if MAINTAINED_FIXTURES in files:
25
- files.remove(MAINTAINED_FIXTURES)
24
+ if MAINTAINED_PUBLIC in files:
25
+ files.remove(MAINTAINED_PUBLIC)
26
26
 
27
27
  return files
28
28
 
@@ -40,7 +40,7 @@ def maintained_files(workflow: str, workflow_builder: WorkflowBuilder):
40
40
  files.append(MAINTAINED_CONFIGURE)
41
41
 
42
42
  if not workflow_builder.config.hostname:
43
- if MAINTAINED_FIXTURES in files:
44
- files.remove(MAINTAINED_FIXTURES)
43
+ if MAINTAINED_PUBLIC in files:
44
+ files.remove(MAINTAINED_PUBLIC)
45
45
 
46
46
  return files
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
10
10
 
11
11
  if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
12
12
  url="$url:$port"
13
- fi
14
-
15
- # Match Rules
16
- echo "Configuring match rules"
17
- stoobly-agent config match set \
18
- --method GET --method POST --method OPTIONS --method PUT --method DELETE \
19
- --mode mock \
20
- --pattern ".*?"
13
+ fi
@@ -1,5 +1,5 @@
1
1
  # The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
2
- # Assumes that 'user-1.json' is created in the 'fixtures' folder
2
+ # Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
3
3
  #GET:
4
4
  # /users/d+:
5
5
  # path: ./fixtures/user-1.json
@@ -10,11 +10,4 @@ url="$scheme://$hostname"
10
10
 
11
11
  if [ "$scheme" = 'http' -a "$port" != '80' ] || [ "$scheme" = 'https' -a "$port" != '443' ]; then
12
12
  url="$url:$port"
13
- fi
14
-
15
- # Match
16
- echo "Configuring match rules"
17
- stoobly-agent config match set \
18
- --method GET --method POST --method OPTIONS --method PUT --method DELETE \
19
- --mode mock \
20
- --pattern ".*?"
13
+ fi
@@ -1,5 +1,5 @@
1
1
  # The following example matches requests for GET /users/d+ (e.g. /users/1) with the contents of user-1.json
2
- # Assumes that 'user-1.json' is created in the 'fixtures' folder
2
+ # Assumes that 'user-1.json' is created in a 'fixtures' folder in the same directory as this file
3
3
  #GET:
4
4
  # /users/d+:
5
5
  # path: ./fixtures/user-1.json
@@ -6,7 +6,7 @@ from stoobly_agent.lib.logger import Logger
6
6
 
7
7
  from .app import App
8
8
  from .config import Config
9
- from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE, FIXTURES_FOLDER_NAME
9
+ from .constants import BIN_FOLDER_NAME, COMPOSE_TEMPLATE, CONFIG_FILE, ENV_FILE, PUBLIC_FOLDER_NAME
10
10
  from .docker.constants import DOCKER_COMPOSE_CUSTOM
11
11
  from .service_command import ServiceCommand
12
12
 
@@ -88,8 +88,8 @@ class WorkflowCommand(ServiceCommand):
88
88
  return services
89
89
 
90
90
  @property
91
- def fixtures_dir_path(self):
92
- return os.path.join(self.workflow_path, FIXTURES_FOLDER_NAME)
91
+ def public_dir_path(self):
92
+ return os.path.join(self.workflow_path, PUBLIC_FOLDER_NAME)
93
93
 
94
94
  @property
95
95
  def workflow_config(self):
@@ -67,11 +67,11 @@ class WorkflowCreateCommand(WorkflowCommand):
67
67
  return
68
68
 
69
69
  # Maintained files are files that will always be overwritten
70
- maintained_workflow_files = maintained_files(self.workflow_name, workflow_builder)
70
+ maintained_workflow_files = maintained_files(template or self.workflow_name, workflow_builder)
71
71
  self.copy_files(templates_path, maintained_workflow_files, self.workflow_path)
72
72
 
73
73
  # Custom files are files that may be modified by the user
74
- custom_workflow_files = custom_files(self.workflow_name, workflow_builder)
74
+ custom_workflow_files = custom_files(template or self.workflow_name, workflow_builder)
75
75
  self.copy_files_no_replace(templates_path, custom_workflow_files, self.workflow_path)
76
76
 
77
77
  def __write_docker_compose_file(self, **kwargs: BuildOptions):
@@ -108,14 +108,19 @@ def create(**kwargs):
108
108
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
109
109
  @click.option('--context-dir-path', default=data_dir.context_dir_path, help='Path to Stoobly data directory.')
110
110
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
111
+ @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
111
112
  def mkcert(**kwargs):
112
113
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
113
114
  __validate_app(app)
114
115
 
115
- services = __get_services(app.services, service=kwargs['service'])
116
+ services = __get_services(
117
+ app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
118
+ )
116
119
 
117
120
  for service_name in services:
118
121
  service = Service(service_name, app)
122
+ __validate_service_dir(service.dir_path)
123
+
119
124
  service_config = ServiceConfig(service.dir_path)
120
125
 
121
126
  if service_config.scheme != 'https':
@@ -135,19 +140,19 @@ def mkcert(**kwargs):
135
140
  help="Scaffold a service",
136
141
  )
137
142
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
138
- @click.option('--detached', is_flag=True)
143
+ @click.option('--detached', is_flag=True, help='Use isolated and non-persistent context directory.')
139
144
  @click.option('--env', multiple=True, help='Specify an environment variable.')
140
145
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded service files.')
141
- @click.option('--hostname')
142
- @click.option('--port')
143
- @click.option('--priority', default='5.0', help='Determines the service run order.')
146
+ @click.option('--hostname', help='Service hostname.')
147
+ @click.option('--port', help='Service port.')
148
+ @click.option('--priority', default=5, type=click.FloatRange(1.0, 9.0), help='Determines the service run order. Lower values run first.')
144
149
  @click.option('--proxy-mode', help='''
145
150
  Proxy mode can be "regular", "transparent", "socks5",
146
151
  "reverse:SPEC", or "upstream:SPEC". For reverse and
147
152
  upstream proxy modes, SPEC is host specification in
148
153
  the form of "http[s]://host[:port]".
149
154
  ''')
150
- @click.option('--scheme', type=click.Choice(['http', 'https']))
155
+ @click.option('--scheme', type=click.Choice(['http', 'https']), help='Defaults to https if hostname is set.')
151
156
  @click.option('--workflow', multiple=True, type=click.Choice([WORKFLOW_MOCK_TYPE, WORKFLOW_RECORD_TYPE, WORKFLOW_TEST_TYPE]), help='Include pre-defined workflows.')
152
157
  @click.argument('service_name')
153
158
  def create(**kwargs):
@@ -170,19 +175,23 @@ def create(**kwargs):
170
175
  @click.option('--select', multiple=True, help='Select column(s) to display.')
171
176
  @click.option('--service', multiple=True, help='Select specific services.')
172
177
  @click.option('--without-headers', is_flag=True, default=False, help='Disable printing column headers.')
173
- @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by.')
178
+ @click.option('--workflow', multiple=True, help='Specify workflow(s) to filter the services by. Defaults to all.')
174
179
  def _list(**kwargs):
175
- __validate_app_dir(kwargs['app_dir_path'])
176
-
177
180
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
178
181
  __validate_app(app)
179
182
 
183
+ services = __get_services(app, service=kwargs['service'], workflow=kwargs['workflow'])
184
+
180
185
  rows = []
181
- for service_name in __get_workflow_services(app, **kwargs):
186
+ for service_name in services:
182
187
  service = Service(service_name, app)
183
188
  __validate_service_dir(service.dir_path)
189
+
184
190
  service_config = ServiceConfig(service.dir_path)
185
- rows.append(service_config.to_dict())
191
+ rows.append({
192
+ 'name': service_name,
193
+ **service_config.to_dict()
194
+ })
186
195
 
187
196
  print_services(rows, **select_print_options(kwargs))
188
197
 
@@ -192,9 +201,9 @@ def _list(**kwargs):
192
201
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
193
202
  @click.argument('service_name')
194
203
  def delete(**kwargs):
195
- __validate_app_dir(kwargs['app_dir_path'])
196
-
197
204
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
205
+ __validate_app(app)
206
+
198
207
  service = Service(kwargs['service_name'], app)
199
208
 
200
209
  if not os.path.exists(service.dir_path):
@@ -211,9 +220,9 @@ def delete(**kwargs):
211
220
  @click.option('--priority', help='Determines the service run order.')
212
221
  @click.argument('service_name')
213
222
  def update(**kwargs):
214
- __validate_app_dir(kwargs['app_dir_path'])
215
-
216
223
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
224
+ __validate_app(app)
225
+
217
226
  service = Service(kwargs['service_name'], app)
218
227
 
219
228
  __validate_service_dir(service.dir_path)
@@ -304,8 +313,9 @@ def down(**kwargs):
304
313
  if kwargs['namespace'] and not kwargs['network']:
305
314
  kwargs['network'] = kwargs['namespace']
306
315
 
307
- workflow = Workflow(kwargs['workflow_name'], app)
308
- services = __get_services(workflow.services, service=kwargs['service'])
316
+ services = __get_services(
317
+ app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
318
+ )
309
319
 
310
320
  commands: List[WorkflowRunCommand] = []
311
321
  for service in services:
@@ -379,8 +389,9 @@ def logs(**kwargs):
379
389
  if len(kwargs['container']) == 0:
380
390
  kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
381
391
 
382
- workflow = Workflow(kwargs['workflow_name'], app)
383
- services = __get_services(workflow.services, service=kwargs['service'], without_core=True)
392
+ services = __get_services(
393
+ app, service=kwargs['service'], without_core=True, workflow=[kwargs['workflow_name']]
394
+ )
384
395
 
385
396
  commands: List[WorkflowLogCommand] = []
386
397
  for service in services:
@@ -445,10 +456,12 @@ def up(**kwargs):
445
456
  if kwargs['namespace'] and not kwargs['network']:
446
457
  kwargs['network'] = kwargs['namespace']
447
458
 
448
- workflow = Workflow(kwargs['workflow_name'], app)
449
- services = __get_services(workflow.services, service=kwargs['service'])
459
+ services = __get_services(
460
+ app, service=kwargs['service'], workflow=[kwargs['workflow_name']]
461
+ )
450
462
 
451
463
  # Gateway ports are dynamically set depending on the workflow run
464
+ workflow = Workflow(kwargs['workflow_name'], app)
452
465
  set_gateway_ports(workflow.service_paths_from_services(services))
453
466
 
454
467
  commands: List[WorkflowRunCommand] = []
@@ -541,13 +554,15 @@ def validate(**kwargs):
541
554
  help="Update the system hosts file for all scaffold service hostnames"
542
555
  )
543
556
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
544
- @click.option('--service', multiple=True, help='Select specific services.')
545
- @click.option('--workflow', multiple=True, help='Specify services by workflow(s).')
557
+ @click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
558
+ @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
546
559
  def install(**kwargs):
547
560
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
548
561
  __validate_app(app)
549
562
 
550
- services = __get_workflow_services(app, **kwargs)
563
+ services = __get_services(
564
+ app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
565
+ )
551
566
 
552
567
  hostnames = []
553
568
  for service_name in services:
@@ -571,13 +586,15 @@ def install(**kwargs):
571
586
  help="Delete from the system hosts file all scaffold service hostnames"
572
587
  )
573
588
  @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
574
- @click.option('--service', multiple=True, help='Select specific services.')
575
- @click.option('--workflow', multiple=True, help='Specify services by workflow(s).')
589
+ @click.option('--service', multiple=True, help='Select specific services. Defaults to all.')
590
+ @click.option('--workflow', multiple=True, help='Specify services by workflow(s). Defaults to all.')
576
591
  def uninstall(**kwargs):
577
592
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
578
593
  __validate_app(app)
579
594
 
580
- services = __get_workflow_services(app, **kwargs)
595
+ services = __get_services(
596
+ app, service=kwargs['service'], without_core=True, workflow=kwargs['workflow']
597
+ )
581
598
 
582
599
  hostnames = []
583
600
  for service_name in services:
@@ -609,39 +626,37 @@ def __elevate_sudo():
609
626
  subprocess.run(["sudo", sys.executable] + sys.argv)
610
627
  sys.exit(0)
611
628
 
612
- def __get_services(services: List[str], **kwargs) -> List[str]:
629
+ def __get_services(app: App, **kwargs):
613
630
  selected_services = list(kwargs['service'])
614
631
 
615
- # If service is specified, run only those services
616
- if selected_services:
617
- missing_services = [service for service in selected_services if service not in services]
632
+ if not selected_services:
633
+ selected_services = app.services
634
+ else:
635
+ selected_services += CORE_SERVICES
636
+ missing_services = [service for service in selected_services if service not in app.services]
618
637
 
619
- # Remove services that don't exist
620
638
  if missing_services:
639
+ # Warn if an invalid service is provided
621
640
  Logger.instance(LOG_ID).warn(f"Service(s) {','.join(missing_services)} are not found")
622
- selected_services = list(set(selected_services) - set(missing_services))
623
641
 
624
- services = selected_services
642
+ # Remove services that don't exist
643
+ selected_services = list(set(selected_services) - set(missing_services))
625
644
 
626
- services += CORE_SERVICES
645
+ # If without_score is set, filter out CORE_SERVICES
646
+ if kwargs.get('without_core'):
647
+ selected_services = list(set(selected_services) - set(CORE_SERVICES))
627
648
 
628
- services_index = {}
629
- for service in services:
630
- if kwargs.get('without_core') and service in CORE_SERVICES:
631
- continue
632
- services_index[service] = True
633
-
634
- return services_index.keys()
635
-
636
- def __get_workflow_services(app: App, **kwargs):
637
- selected_services = []
638
- if not kwargs['workflow']:
639
- selected_services += __get_services(app.services, service=kwargs['service'], without_core=True)
640
- else:
649
+ # If workflow is set, keep only services in the workflow
650
+ if kwargs.get('workflow'):
651
+ workflow_services = []
641
652
  for workflow_name in kwargs['workflow']:
642
653
  workflow = Workflow(workflow_name, app)
643
- selected_services += __get_services(workflow.services, service=kwargs['service'], without_core=True)
644
- return set(selected_services)
654
+ workflow_services += workflow.services
655
+
656
+ # Intersection
657
+ selected_services = list(filter(lambda x: x in workflow_services, selected_services))
658
+
659
+ return list(set(selected_services))
645
660
 
646
661
  def __print_header(text: str):
647
662
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
@@ -12,6 +12,10 @@ class InterceptContext():
12
12
  def flow(self):
13
13
  return self.__flow
14
14
 
15
+ @flow.setter
16
+ def flow(self, v: MitmproxyHTTPFlow):
17
+ self.__flow = v
18
+
15
19
  @property
16
20
  def intercept_settings(self):
17
21
  return self.__intercept_settings