stoobly-agent 1.2.2__py3-none-any.whl → 1.3.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 (69) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/certificate_authority.py +1 -5
  3. stoobly_agent/app/cli/scaffold/app.py +14 -32
  4. stoobly_agent/app/cli/scaffold/app_command.py +4 -7
  5. stoobly_agent/app/cli/scaffold/app_config.py +15 -2
  6. stoobly_agent/app/cli/scaffold/app_create_command.py +18 -2
  7. stoobly_agent/app/cli/scaffold/command.py +1 -1
  8. stoobly_agent/app/cli/scaffold/constants.py +9 -3
  9. stoobly_agent/app/cli/scaffold/docker/app_builder.py +3 -7
  10. stoobly_agent/app/cli/scaffold/docker/builder.py +1 -1
  11. stoobly_agent/app/cli/scaffold/docker/constants.py +0 -1
  12. stoobly_agent/app/cli/scaffold/docker/service/builder.py +12 -11
  13. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +14 -31
  14. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +6 -2
  15. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +6 -2
  16. stoobly_agent/app/cli/scaffold/service.py +1 -1
  17. stoobly_agent/app/cli/scaffold/service_command.py +1 -1
  18. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +6 -8
  19. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +2 -4
  20. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +37 -21
  21. stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +8 -13
  22. stoobly_agent/app/cli/scaffold/templates/app/Makefile +1 -1
  23. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +8 -4
  24. stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +2 -6
  25. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +3 -0
  26. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +3 -0
  27. stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +2 -6
  28. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +3 -0
  29. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +3 -0
  30. stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +2 -6
  31. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +3 -0
  32. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +3 -0
  33. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -0
  34. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +2 -8
  35. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +2 -8
  36. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +2 -8
  37. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +2 -3
  38. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +1 -0
  39. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +1 -2
  40. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +1 -2
  41. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
  42. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +7 -1
  43. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
  44. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +7 -1
  45. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
  46. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +7 -1
  47. stoobly_agent/app/cli/scaffold/validate_command.py +2 -2
  48. stoobly_agent/app/cli/scaffold/workflow.py +5 -4
  49. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  50. stoobly_agent/app/cli/scaffold/workflow_run_command.py +74 -35
  51. stoobly_agent/app/cli/scaffold_cli.py +61 -44
  52. stoobly_agent/app/cli/snapshot_cli.py +6 -2
  53. stoobly_agent/app/models/adapters/joined_request_adapter.py +6 -0
  54. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +3 -1
  55. stoobly_agent/app/models/helpers/apply.py +34 -17
  56. stoobly_agent/app/models/helpers/create_request_params_service.py +4 -0
  57. stoobly_agent/app/proxy/replay/body_parser_service.py +11 -3
  58. stoobly_agent/config/data_dir.py +2 -1
  59. stoobly_agent/config/schema.yml +2 -2
  60. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +1 -2
  61. stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +162 -1
  62. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  63. stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +1 -3
  64. {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/METADATA +1 -1
  65. {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/RECORD +68 -69
  66. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
  67. {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/LICENSE +0 -0
  68. {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/WHEEL +0 -0
  69. {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -12,42 +12,46 @@ from stoobly_agent.lib.logger import Logger
12
12
  from .app import App
13
13
  from .constants import (
14
14
  APP_NETWORK_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV, NAMESERVERS_FILE,
15
- SERVICE_DNS_ENV, SERVICE_NAME_ENV, USER_ID_ENV, WORKFLOW_NAME_ENV
15
+ SERVICE_DNS_ENV, SERVICE_NAME_ENV, USER_ID_ENV, WORKFLOW_NAME_ENV, WORKFLOW_NAMESPACE_ENV
16
16
  )
17
+ from .docker.constants import DOCKERFILE_CONTEXT
17
18
  from .workflow_command import WorkflowCommand
18
19
  from .workflow_env import WorkflowEnv
19
20
 
20
21
  LOG_ID = 'WorkflowRunCommand'
21
22
 
22
- class UpOptions(TypedDict):
23
- attached: bool
23
+ class ComposeOptions(TypedDict):
24
24
  namespace: str
25
- no_cache: bool
25
+ user_id: str
26
+
27
+ class BuildOptions(ComposeOptions):
28
+ user_id: str
26
29
  verbose: bool
27
30
 
31
+ class DownOptions(ComposeOptions):
32
+ rmi: bool
33
+
34
+ class UpOptions(ComposeOptions):
35
+ attached: bool
36
+ pull: bool
37
+
28
38
  class WorkflowRunCommand(WorkflowCommand):
29
39
  def __init__(self, app: App, **kwargs):
30
40
  super().__init__(app, **kwargs)
31
41
 
32
42
  self.__current_working_dir = os.getcwd()
33
- self.__ca_certs_dir_path = app.ca_certs_dir_path
34
- self.__certs_dir_path = app.certs_dir_path
35
- self.__context_dir_path = app.context_dir_path
43
+ self.__ca_certs_dir_path = kwargs.get('ca_certs_dir_path') or app.ca_certs_dir_path
44
+ self.__certs_dir_path = kwargs.get('certs_dir_path') or app.certs_dir_path
45
+ self.__context_dir_path = kwargs.get('context_dir_path') or app.context_dir_path
36
46
  self.__extra_compose_path = kwargs.get('extra_compose_path')
37
47
  self.__network = kwargs.get('network') or self.app_config.network
38
48
 
39
49
  @property
40
50
  def ca_certs_dir_path(self):
41
- if not os.path.exists(self.__ca_certs_dir_path):
42
- os.makedirs(self.__ca_certs_dir_path)
43
-
44
51
  return self.__ca_certs_dir_path
45
52
 
46
53
  @property
47
54
  def certs_dir_path(self):
48
- if not os.path.exists(self.__certs_dir_path):
49
- os.makedirs(self.__certs_dir_path)
50
-
51
55
  return self.__certs_dir_path
52
56
 
53
57
  @property
@@ -88,14 +92,43 @@ class WorkflowRunCommand(WorkflowCommand):
88
92
  def network(self):
89
93
  return self.__network
90
94
 
95
+ def create_image(self, **options: BuildOptions):
96
+ relative_namespace_path = os.path.relpath(self.scaffold_namespace_path, self.__current_working_dir)
97
+ dockerfile_path = os.path.join(relative_namespace_path, DOCKERFILE_CONTEXT)
98
+ user_id = options['user_id'] or os.getuid()
99
+
100
+ command = ['docker', 'build']
101
+ command.append(f"-f {dockerfile_path}")
102
+ command.append(f"-t stoobly.{user_id}")
103
+ command.append(f"--build-arg USER_ID={user_id}")
104
+
105
+ if not os.environ.get('STOOBLY_IMAGE_USE_LOCAL'):
106
+ command.append('--pull')
107
+
108
+ if not options.get('verbose'):
109
+ command.append('--quiet')
110
+
111
+ # To avoid large context transfer times, should be a folder with relatively low number of files
112
+ command.append(relative_namespace_path)
113
+
114
+ return ' '.join(command)
115
+
116
+ def remove_image(self, user_id: str = None):
117
+ user_id = user_id or os.getuid()
118
+ command = ['docker', 'rmi', f"stoobly.{user_id}", '&>', '/dev/null']
119
+ command.append('|| true')
120
+ return ' '.join(command)
121
+
91
122
  def create_network(self):
92
- return f"docker network create {self.network} 2> /dev/null"
123
+ return f"docker network create {self.network} &> /dev/null"
124
+
125
+ def remove_network(self):
126
+ return f"docker network rm {self.network} &> /dev/null || true"
93
127
 
94
128
  def up(self, **options: UpOptions):
95
129
  if not os.path.exists(self.compose_path):
96
130
  return ''
97
131
 
98
- build_command = ['docker', 'compose']
99
132
  command = ['COMPOSE_IGNORE_ORPHANS=true', 'docker', 'compose']
100
133
  command_options = []
101
134
 
@@ -125,22 +158,19 @@ class WorkflowRunCommand(WorkflowCommand):
125
158
 
126
159
  command_options.append(f"--profile {self.workflow_name}")
127
160
 
128
- if options.get('namespace'):
129
- command_options.append(f"-p {options['namespace']}")
130
-
131
- build_command += command_options
132
- build_command.append('build')
133
- build_command.append('--pull')
134
-
135
- if not options.get('verbose'):
136
- build_command.append('--quiet')
137
-
138
- if options.get('no_cache'):
139
- build_command.append('--no-cache')
161
+ if not options.get('namespace'):
162
+ options['namespace'] = self.workflow_name
163
+ command_options.append(f"-p {options['namespace']}")
140
164
 
141
165
  command += command_options
142
166
  command.append('up')
143
167
 
168
+ if options.get('build'):
169
+ command.append('--build')
170
+
171
+ if options.get('pull'):
172
+ command.append('--pull missing')
173
+
144
174
  if not options.get('attached'):
145
175
  command.append('-d')
146
176
  else:
@@ -156,11 +186,11 @@ class WorkflowRunCommand(WorkflowCommand):
156
186
  # Otherwise, even if a service exits with a non-zero exit code, exit code 0 is returned
157
187
  command.append(option)
158
188
 
159
- self.write_env()
189
+ self.write_env(**options)
160
190
 
161
- return ' && '.join([' '.join(build_command), ' '.join(command)])
191
+ return ' '.join(command)
162
192
 
163
- def down(self, **options):
193
+ def down(self, **options: DownOptions):
164
194
  if not os.path.exists(self.compose_path):
165
195
  return ''
166
196
 
@@ -178,13 +208,16 @@ class WorkflowRunCommand(WorkflowCommand):
178
208
 
179
209
  command.append(f"--profile {self.workflow_name}")
180
210
 
181
- if options.get('namespace'):
182
- command.append(f"-p {options['namespace']}")
211
+ if not options.get('namespace'):
212
+ options['namespace'] = self.workflow_name
213
+ command.append(f"-p {options['namespace']}")
183
214
 
184
215
  command.append('down')
185
216
 
186
217
  if options.get('rmi'):
187
- command.append(f"--rmi local")
218
+ command.append('--rmi local')
219
+
220
+ self.write_env(**options)
188
221
 
189
222
  return ' '.join(command)
190
223
 
@@ -209,14 +242,20 @@ class WorkflowRunCommand(WorkflowCommand):
209
242
  if nameservers:
210
243
  fp.write("\n".join(nameservers))
211
244
 
212
- def write_env(self):
245
+ def write_env(self, **options: ComposeOptions):
246
+ namespace = options.get('namespace')
247
+ user_id = options.get('user_id')
248
+
213
249
  _config = {}
214
250
  _config[CA_CERTS_DIR_ENV] = self.ca_certs_dir_path
215
251
  _config[CERTS_DIR_ENV] = self.certs_dir_path
216
252
  _config[CONTEXT_DIR_ENV] = self.context_dir_path
217
253
  _config[SERVICE_NAME_ENV] = self.service_name
218
- _config[USER_ID_ENV] = os.getuid()
254
+ _config[USER_ID_ENV] = user_id or os.getuid()
219
255
  _config[WORKFLOW_NAME_ENV] = self.workflow_name
256
+
257
+ if namespace:
258
+ _config[WORKFLOW_NAMESPACE_ENV] = namespace
220
259
 
221
260
  if self.network:
222
261
  _config[APP_NETWORK_ENV] = self.network
@@ -62,14 +62,18 @@ def service(ctx):
62
62
  )
63
63
  @click.option('--app-dir-path', default=os.getcwd(), help='Path to create the app scaffold.')
64
64
  @click.option('--force', is_flag=True, help='Overwrite maintained scaffolded app files.')
65
+ @click.option('--network', help='App default network name. Defaults to app name.')
65
66
  @click.argument('app_name')
66
67
  def create(**kwargs):
67
68
  __validate_app_dir(kwargs['app_dir_path'])
68
69
 
69
70
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE)
70
71
 
71
- if kwargs['force'] or not os.path.exists(app.namespace_path):
72
- __app_build(app, **kwargs)
72
+ if kwargs['force'] or not os.path.exists(app.scaffold_namespace_path):
73
+ if not kwargs['network']:
74
+ kwargs['network'] = kwargs['app_name']
75
+
76
+ AppCreateCommand(app, **kwargs).build()
73
77
  else:
74
78
  print(f"{kwargs['app_dir_path']} already exists, use option '--force' to continue ")
75
79
 
@@ -82,13 +86,7 @@ def create(**kwargs):
82
86
  @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
83
87
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
84
88
  def mkcert(**kwargs):
85
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, ca_certs_dir_path=kwargs['ca_certs_dir_path'])
86
-
87
- if kwargs['certs_dir_path']:
88
- app.certs_dir_path = kwargs['certs_dir_path']
89
-
90
- if kwargs['context_dir_path']:
91
- app.context_dir_path = kwargs['context_dir_path']
89
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
92
90
 
93
91
  if not app.exists:
94
92
  print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
@@ -249,19 +247,21 @@ def copy(**kwargs):
249
247
  Log levels can be "debug", "info", "warning", or "error"
250
248
  ''')
251
249
  @click.option('--namespace', help='Workflow namespace.')
250
+ @click.option('--network', help='Workflow network name.')
252
251
  @click.option('--rmi', is_flag=True, help='Remove images used by containers.')
253
252
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
253
+ @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
254
254
  @click.argument('workflow_name')
255
255
  def down(**kwargs):
256
256
  cwd = os.getcwd()
257
257
 
258
- if not os.getenv(env_vars.LOG_LEVEL):
259
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
258
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
260
259
 
261
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, skip_validate_path=True)
260
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
262
261
 
263
- if kwargs['context_dir_path']:
264
- app.context_dir_path = kwargs['context_dir_path']
262
+ # If namespace is set, default network to namespace
263
+ if kwargs['namespace'] and not kwargs['network']:
264
+ kwargs['network'] = kwargs['namespace']
265
265
 
266
266
  if not app.exists:
267
267
  print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
@@ -279,11 +279,10 @@ def down(**kwargs):
279
279
  commands.append(command)
280
280
 
281
281
  commands = sorted(commands, key=lambda command: command.service_config.priority)
282
-
283
282
  for command in commands:
284
283
  __print_header(f"SERVICE {command.service_name}")
285
284
 
286
- exec_command = command.down(namespace=kwargs['namespace'], rmi=kwargs['rmi'])
285
+ exec_command = command.down(namespace=kwargs['namespace'], rmi=kwargs['rmi'], user_id=kwargs['user_id'])
287
286
  if not exec_command:
288
287
  continue
289
288
 
@@ -292,6 +291,23 @@ def down(**kwargs):
292
291
  else:
293
292
  print(exec_command)
294
293
 
294
+ # After services are stopped, their network needs to be removed
295
+ if len(commands) > 0:
296
+ command: WorkflowRunCommand = commands[0]
297
+
298
+ if kwargs['rmi']:
299
+ remove_image_command = command.remove_image(kwargs['user_id'])
300
+ if not kwargs['dry_run']:
301
+ exec_stream(remove_image_command)
302
+ else:
303
+ print(remove_image_command)
304
+
305
+ remove_network_command = command.remove_network()
306
+ if not kwargs['dry_run']:
307
+ exec_stream(remove_network_command)
308
+ else:
309
+ print(remove_network_command)
310
+
295
311
  @workflow.command()
296
312
  @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
297
313
  @click.option(
@@ -299,10 +315,15 @@ def down(**kwargs):
299
315
  )
300
316
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
301
317
  @click.option('--follow', is_flag=True, help='Follow last container log output.')
318
+ @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
319
+ Log levels can be "debug", "info", "warning", or "error"
320
+ ''')
302
321
  @click.option('--namespace', help='Workflow namespace.')
303
322
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
304
323
  @click.argument('workflow_name')
305
324
  def logs(**kwargs):
325
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
326
+
306
327
  if len(kwargs['container']) == 0:
307
328
  kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
308
329
 
@@ -332,7 +353,6 @@ def logs(**kwargs):
332
353
  commands.append(command)
333
354
 
334
355
  commands = sorted(commands, key=lambda command: command.service_config.priority)
335
-
336
356
  for index, command in enumerate(commands):
337
357
  __print_header(f"SERVICE {command.service_name}")
338
358
 
@@ -349,40 +369,34 @@ def logs(**kwargs):
349
369
 
350
370
  @workflow.command()
351
371
  @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
372
+ @click.option('--build', is_flag=True, help='Build images before starting containers.')
352
373
  @click.option('--ca-certs-dir-path', default=DataDir.instance().mitmproxy_conf_dir_path, help='Path to ca certs directory used to sign SSL certs. Defaults to ~/.mitmproxy')
353
374
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
354
375
  @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
355
376
  @click.option('--detached', is_flag=True, help='If set, will not run the highest priority service in the foreground.')
356
377
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
357
378
  @click.option('--extra-compose-path', help='Path to extra compose configuration files.')
379
+ @click.option('--from-make', is_flag=True, help='Set if run from scaffolded Makefile.')
358
380
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
359
381
  Log levels can be "debug", "info", "warning", or "error"
360
382
  ''')
361
383
  @click.option('--namespace', help='Workflow namespace.')
362
- @click.option('--network', help='Workflow network namespace.')
363
- @click.option('--no-cache', is_flag=True, help='Do not use cache when building images.')
384
+ @click.option('--network', help='Workflow network name.')
385
+ @click.option('--pull', is_flag=True, help='Pull image before running.')
364
386
  @click.option('--service', multiple=True, help='Select which services to run. Defaults to all.')
387
+ @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
365
388
  @click.option('--verbose', is_flag=True)
366
389
  @click.argument('workflow_name')
367
390
  def up(**kwargs):
368
391
  cwd = os.getcwd()
369
392
 
370
- # Create the certs_dir_path if it doesn't exist
371
- DataDir.instance().certs_dir_path
372
-
373
- if not os.getenv(env_vars.LOG_LEVEL):
374
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
393
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
375
394
 
376
- app = App(
377
- kwargs['app_dir_path'], DOCKER_NAMESPACE,
378
- ca_certs_dir_path=kwargs['ca_certs_dir_path'], skip_validate_path=kwargs['dry_run']
379
- )
395
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
380
396
 
381
- if kwargs['certs_dir_path']:
382
- app.certs_dir_path = kwargs['certs_dir_path']
383
-
384
- if kwargs['context_dir_path']:
385
- app.context_dir_path = kwargs['context_dir_path']
397
+ # If namespace is set, default network to namespace
398
+ if kwargs['namespace'] and not kwargs['network']:
399
+ kwargs['network'] = kwargs['namespace']
386
400
 
387
401
  if not app.exists:
388
402
  print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
@@ -402,17 +416,23 @@ def up(**kwargs):
402
416
  command.current_working_dir = cwd
403
417
  commands.append(command)
404
418
 
405
- # Before services can be started, their network needs to be created
419
+ # Before services can be started, their image and network needs to be created
406
420
  if len(commands) > 0:
407
- command = commands[0]
408
- create_network_command = command.create_network()
421
+ command: WorkflowRunCommand = commands[0]
409
422
 
410
- if not kwargs['dry_run']:
411
- command.write_nameservers()
423
+ init_commands = []
424
+ if not kwargs['from_make']:
425
+ create_image_command = command.create_image(user_id=kwargs['user_id'], verbose=kwargs['verbose'])
426
+ init_commands.append(create_image_command)
427
+
428
+ init_commands.append(command.create_network())
429
+ joined_command = ' && '.join(init_commands)
430
+ command.write_nameservers()
412
431
 
413
- exec_stream(create_network_command)
432
+ if not kwargs['dry_run']:
433
+ exec_stream(joined_command)
414
434
  else:
415
- print(create_network_command)
435
+ print(joined_command)
416
436
 
417
437
  commands = sorted(commands, key=lambda command: command.service_config.priority)
418
438
  for index, command in enumerate(commands):
@@ -422,7 +442,7 @@ def up(**kwargs):
422
442
  # However, this can change if the user has configured a service's priority to be higher
423
443
  attached = not kwargs['detached'] and index == len(commands) - 1
424
444
  exec_command = command.up(
425
- attached=attached, namespace=kwargs['namespace'], no_cache=kwargs['no_cache'], verbose=kwargs['verbose']
445
+ attached=attached, build=kwargs['build'], namespace=kwargs['namespace'], pull=kwargs['pull'], user_id=kwargs['user_id']
426
446
  )
427
447
  if not exec_command:
428
448
  continue
@@ -487,9 +507,6 @@ def __get_services(services: List[str], **kwargs):
487
507
  def __print_header(text: str):
488
508
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
489
509
 
490
- def __app_build(app, **kwargs):
491
- AppCreateCommand(app, **kwargs).build()
492
-
493
510
  def __scaffold_build(app, **kwargs):
494
511
  command = ServiceCreateCommand(app, **kwargs)
495
512
 
@@ -36,11 +36,15 @@ def snapshot(ctx):
36
36
  @click.argument('uuid', required=False)
37
37
  def apply(**kwargs):
38
38
  apply = Apply(force=kwargs['force']).with_logger(print)
39
+ completed = True
39
40
 
40
41
  if kwargs.get('uuid'):
41
- apply.single(kwargs['uuid'])
42
+ completed = apply.single(kwargs['uuid'])
42
43
  else:
43
- apply.all()
44
+ completed = apply.all()
45
+
46
+ if not completed:
47
+ sys.exit(1)
44
48
 
45
49
  @snapshot.command(
46
50
  help="Copy snapshots to a different data directory."
@@ -18,6 +18,12 @@ class JoinedRequestAdapter():
18
18
  payloads_delimitter = payloads_delimitter.encode()
19
19
 
20
20
  self.__split_joined_request_string = joined_request_string.split(payloads_delimitter)
21
+ if len(self.__split_joined_request_string) != 2:
22
+ self.__split_joined_request_string = joined_request_string.split(payloads_delimitter.replace(b"\n", b"\r\n"))
23
+
24
+ if len(self.__split_joined_request_string) != 2:
25
+ raise ValueError(f"Could not split by {payloads_delimitter}")
26
+
21
27
  self.__raw_request_string = None
22
28
  self.__raw_response_string = None
23
29
 
@@ -34,7 +34,9 @@ class ScenarioSnapshot(Snapshot):
34
34
  try:
35
35
  return json.loads(fp.read())
36
36
  except Exception:
37
- return {}
37
+ return {
38
+ 'name': self.uuid
39
+ }
38
40
 
39
41
  @property
40
42
  def metadata_backup(self):
@@ -61,15 +61,17 @@ class Apply():
61
61
  return
62
62
 
63
63
  last_processed_event = None
64
+ completed = True
64
65
 
65
66
  for event in unprocessed_events:
66
67
  if self.__logger:
67
- self.__logger(f"Processing event {event.uuid}")
68
+ self.__logger(f"{bcolors.OKBLUE}Processing Event{bcolors.ENDC} {event.uuid}")
68
69
 
69
70
  results = event.apply(**self.__handlers())
70
71
  if results:
71
72
  status = results[1]
72
- if status == 0 or status >= 500:
73
+ if status == 0 or status >= 400:
74
+ completed = False
73
75
  break
74
76
 
75
77
  last_processed_event = event
@@ -86,6 +88,8 @@ class Apply():
86
88
 
87
89
  log.lock()
88
90
 
91
+ return completed
92
+
89
93
  def request(self, uuid: str):
90
94
  result = self.__apply_put_request(uuid)
91
95
  if not result:
@@ -121,9 +125,13 @@ class Apply():
121
125
  return False
122
126
 
123
127
  if self.__logger:
124
- self.__logger(f"Processing event {event.uuid}")
128
+ self.__logger(f"{bcolors.OKBLUE}Processing Event{bcolors.ENDC} {event.uuid}")
125
129
 
126
- event.apply(**self.__handlers())
130
+ results = event.apply(**self.__handlers())
131
+ if results:
132
+ status = results[1]
133
+ if status == 0 or status >= 400:
134
+ return False
127
135
 
128
136
  return True
129
137
 
@@ -139,7 +147,7 @@ class Apply():
139
147
  res, status = self.request_model.destroy(uuid, force=self.__force)
140
148
 
141
149
  if status == 200:
142
- self.__logger(f"{bcolors.WARNING}Deleted{bcolors.ENDC} request {uuid}")
150
+ self.__logger(f"{bcolors.WARNING}Deleted Request{bcolors.ENDC} {uuid}")
143
151
  else:
144
152
  self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
145
153
 
@@ -150,9 +158,9 @@ class Apply():
150
158
 
151
159
  raw_request = snapshot.request
152
160
  if not raw_request:
153
- error = f"Snapshot for request {uuid} not found"
154
- self.__logger(f"{bcolors.FAIL}400{bcolors.ENDC} {error}")
155
- return error, 400
161
+ error = f"snapshot for request {uuid} not found"
162
+ self.__logger(f"{bcolors.WARNING}Skipping Request{bcolors.ENDC} {error}")
163
+ return error, 301
156
164
 
157
165
  return self.__put_request(uuid, raw_request)
158
166
 
@@ -160,7 +168,7 @@ class Apply():
160
168
  res, status = self.scenario_model.destroy(uuid, force=self.__force)
161
169
 
162
170
  if self.__logger and status == 200:
163
- self.__logger(f"{bcolors.WARNING}Deleted{bcolors.ENDC} scenario {uuid}")
171
+ self.__logger(f"{bcolors.WARNING}Deleted Scenario{bcolors.ENDC} {uuid}")
164
172
  else:
165
173
  self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
166
174
 
@@ -171,9 +179,9 @@ class Apply():
171
179
  metadata = snapshot.metadata
172
180
 
173
181
  if not metadata:
174
- error = f"Snapshot for scenario {uuid} not found"
175
- self.__logger(f"{bcolors.FAIL}400{bcolors.ENDC} {error}")
176
- return error, 400
182
+ error = f"snapshot for scenario {uuid} not found"
183
+ self.__logger(f"{bcolors.WARNING}Skipping Scenario{bcolors.ENDC} {error}")
184
+ return error, 301
177
185
 
178
186
  res, status = self.scenario_model.show(uuid)
179
187
  if status == 404:
@@ -184,7 +192,7 @@ class Apply():
184
192
 
185
193
  if self.__logger:
186
194
  if status == 200:
187
- self.__logger(f"{bcolors.OKGREEN}Created scenario{bcolors.ENDC} {res['name']}")
195
+ self.__logger(f"{bcolors.OKGREEN}Created Scenario{bcolors.ENDC} {res['name']}")
188
196
  else:
189
197
  self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
190
198
  else:
@@ -195,7 +203,7 @@ class Apply():
195
203
 
196
204
  if self.__logger:
197
205
  if status == 200:
198
- self.__logger(f"{bcolors.OKBLUE}Updated{bcolors.ENDC} scenario {res['name']}")
206
+ self.__logger(f"{bcolors.OKCYAN}Updated Scenario{bcolors.ENDC} {res['name']}")
199
207
  else:
200
208
  self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
201
209
 
@@ -221,6 +229,9 @@ class Apply():
221
229
  uuid = control.id
222
230
  res, status = self.__put_request(uuid, raw_request, scenario_id=scenario['id'])
223
231
 
232
+ if status != 200:
233
+ return res, status
234
+
224
235
  snapshot_requests[uuid] = res
225
236
 
226
237
  # Remove requests in scenario that don't exist in the snapshot
@@ -242,15 +253,21 @@ class Apply():
242
253
  res, status = self.request_model.show(uuid)
243
254
 
244
255
  if status == 404:
256
+ request_params = build_params(raw_request)
257
+
258
+ if not request_params:
259
+ self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} failed to join request {uuid}")
260
+ return res, status
261
+
245
262
  params = {
246
- **build_params(raw_request),
263
+ **request_params,
247
264
  **base_params,
248
265
  }
249
266
 
250
267
  res, status = self.request_model.create(**params)
251
268
 
252
269
  if self.__logger and status == 200:
253
- self.__logger(f"{bcolors.OKGREEN}Created{bcolors.ENDC} {res['list'][0]['url']}")
270
+ self.__logger(f"{bcolors.OKGREEN}Created Request{bcolors.ENDC} {res['list'][0]['url']}")
254
271
  else:
255
272
  self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
256
273
  elif status == 200:
@@ -263,7 +280,7 @@ class Apply():
263
280
 
264
281
  if self.__logger:
265
282
  if status == 200:
266
- self.__logger(f"{bcolors.OKBLUE}Updated{bcolors.ENDC} {res['url']}")
283
+ self.__logger(f"{bcolors.OKCYAN}Updated Request{bcolors.ENDC} {res['url']}")
267
284
  else:
268
285
  self.__logger(f"{bcolors.FAIL}{status}{bcolors.ENDC} {res}")
269
286
 
@@ -6,6 +6,9 @@ from stoobly_agent.app.models.adapters.python import PythonRequestAdapterFactory
6
6
 
7
7
  from stoobly_agent.app.proxy.record.join_request_service import InterceptSettings, join_request, MitmproxyRequestFacade, MitmproxyResponseFacade
8
8
  from stoobly_agent.app.settings import Settings
9
+ from stoobly_agent.lib.logger import Logger
10
+
11
+ LOG_ID = 'CreateRequestParamsService'
9
12
 
10
13
  class MitmproxyFlowMock():
11
14
  def __init__(self, request, response):
@@ -16,6 +19,7 @@ def build_params(raw_requests: str, payloads_delimitter = None):
16
19
  try:
17
20
  joined_request = JoinedRequestAdapter(raw_requests, payloads_delimitter).adapt()
18
21
  except Exception as e:
22
+ Logger.instance(LOG_ID).error(e)
19
23
  return
20
24
 
21
25
  request_adapter = RawHttpRequestAdapter(joined_request.request_string.get())
@@ -1,8 +1,8 @@
1
- import cgi
2
1
  import json
3
2
  import pdb
4
3
  import urllib.parse
5
4
 
5
+ from email.message import Message
6
6
  from mitmproxy.coretypes.multidict import MultiDict
7
7
  from mitmproxy.net import encoding
8
8
  from typing import Dict, Union
@@ -108,11 +108,19 @@ def serialize_www_form_urlencoded(o):
108
108
  def normalize_header(header):
109
109
  if isinstance(header, bytes):
110
110
  header = header.decode('utf-8')
111
- return cgi.parse_header(header)[0].lower()
111
+ return __parse_separated_header(header).lower()
112
112
 
113
113
  def is_traversable(content):
114
114
  return isinstance(content, list) or isinstance(content, dict) or isinstance(content, MultiDict)
115
115
 
116
116
  def is_json(content_type):
117
117
  _content_type = content_type.lower()
118
- return _content_type == JSON or _content_type.startswith('application/x-amz-json')
118
+ return _content_type == JSON or _content_type.startswith('application/x-amz-json')
119
+
120
+
121
+ def __parse_separated_header(header: str):
122
+ # Adapted from https://peps.python.org/pep-0594/#cgi
123
+ message = Message()
124
+ message['content-type'] = header
125
+ return message.get_content_type()
126
+
@@ -4,6 +4,7 @@ import shutil
4
4
 
5
5
  from stoobly_agent.config.constants.env_vars import ENV
6
6
 
7
+ CERTS_DIR_NAME = 'certs'
7
8
  DATA_DIR_NAME = '.stoobly'
8
9
  DB_FILE_NAME = 'stoobly_agent.sqlite3'
9
10
  DB_VERSION_NAME = 'VERSION'
@@ -85,7 +86,7 @@ class DataDir:
85
86
 
86
87
  @property
87
88
  def certs_dir_path(self):
88
- certs_dir_path = os.path.join(self.path, 'certs')
89
+ certs_dir_path = os.path.join(self.path, CERTS_DIR_NAME)
89
90
 
90
91
  if not os.path.exists(certs_dir_path):
91
92
  os.mkdir(certs_dir_path)