stoobly-agent 1.2.3__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 (68) 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/constants.py +0 -1
  11. stoobly_agent/app/cli/scaffold/docker/service/builder.py +12 -11
  12. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +14 -31
  13. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +6 -2
  14. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +6 -2
  15. stoobly_agent/app/cli/scaffold/service.py +1 -1
  16. stoobly_agent/app/cli/scaffold/service_command.py +1 -1
  17. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +6 -8
  18. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +2 -4
  19. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +37 -21
  20. stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +8 -13
  21. stoobly_agent/app/cli/scaffold/templates/app/Makefile +1 -1
  22. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +8 -4
  23. stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +2 -6
  24. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +3 -0
  25. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +3 -0
  26. stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +2 -6
  27. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +3 -0
  28. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +3 -0
  29. stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +2 -6
  30. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +3 -0
  31. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +3 -0
  32. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -0
  33. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +2 -8
  34. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +2 -8
  35. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +2 -8
  36. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +2 -3
  37. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +1 -0
  38. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +1 -2
  39. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +1 -2
  40. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
  41. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +7 -1
  42. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
  43. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +7 -1
  44. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
  45. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +7 -1
  46. stoobly_agent/app/cli/scaffold/validate_command.py +2 -2
  47. stoobly_agent/app/cli/scaffold/workflow.py +5 -4
  48. stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
  49. stoobly_agent/app/cli/scaffold/workflow_run_command.py +72 -36
  50. stoobly_agent/app/cli/scaffold_cli.py +51 -45
  51. stoobly_agent/app/cli/snapshot_cli.py +6 -2
  52. stoobly_agent/app/models/adapters/joined_request_adapter.py +6 -0
  53. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +3 -1
  54. stoobly_agent/app/models/helpers/apply.py +34 -17
  55. stoobly_agent/app/models/helpers/create_request_params_service.py +4 -0
  56. stoobly_agent/app/proxy/replay/body_parser_service.py +11 -3
  57. stoobly_agent/config/data_dir.py +2 -1
  58. stoobly_agent/config/schema.yml +2 -2
  59. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +1 -2
  60. stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +162 -1
  61. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  62. stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +1 -3
  63. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/METADATA +1 -1
  64. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/RECORD +67 -68
  65. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
  66. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/LICENSE +0 -0
  67. {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/WHEEL +0 -0
  68. {stoobly_agent-1.2.3.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,17 +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"
93
124
 
94
125
  def remove_network(self):
95
- return f"docker network rm {self.network} > /dev/null"
126
+ return f"docker network rm {self.network} &> /dev/null || true"
96
127
 
97
128
  def up(self, **options: UpOptions):
98
129
  if not os.path.exists(self.compose_path):
99
130
  return ''
100
131
 
101
- build_command = ['docker', 'compose']
102
132
  command = ['COMPOSE_IGNORE_ORPHANS=true', 'docker', 'compose']
103
133
  command_options = []
104
134
 
@@ -128,22 +158,19 @@ class WorkflowRunCommand(WorkflowCommand):
128
158
 
129
159
  command_options.append(f"--profile {self.workflow_name}")
130
160
 
131
- if options.get('namespace'):
132
- command_options.append(f"-p {options['namespace']}")
133
-
134
- build_command += command_options
135
- build_command.append('build')
136
- build_command.append('--pull')
137
-
138
- if not options.get('verbose'):
139
- build_command.append('--quiet')
140
-
141
- if options.get('no_cache'):
142
- 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']}")
143
164
 
144
165
  command += command_options
145
166
  command.append('up')
146
167
 
168
+ if options.get('build'):
169
+ command.append('--build')
170
+
171
+ if options.get('pull'):
172
+ command.append('--pull missing')
173
+
147
174
  if not options.get('attached'):
148
175
  command.append('-d')
149
176
  else:
@@ -159,11 +186,11 @@ class WorkflowRunCommand(WorkflowCommand):
159
186
  # Otherwise, even if a service exits with a non-zero exit code, exit code 0 is returned
160
187
  command.append(option)
161
188
 
162
- self.write_env()
189
+ self.write_env(**options)
163
190
 
164
- return ' && '.join([' '.join(build_command), ' '.join(command)])
191
+ return ' '.join(command)
165
192
 
166
- def down(self, **options):
193
+ def down(self, **options: DownOptions):
167
194
  if not os.path.exists(self.compose_path):
168
195
  return ''
169
196
 
@@ -181,13 +208,16 @@ class WorkflowRunCommand(WorkflowCommand):
181
208
 
182
209
  command.append(f"--profile {self.workflow_name}")
183
210
 
184
- if options.get('namespace'):
185
- 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']}")
186
214
 
187
215
  command.append('down')
188
216
 
189
217
  if options.get('rmi'):
190
- command.append(f"--rmi local")
218
+ command.append('--rmi local')
219
+
220
+ self.write_env(**options)
191
221
 
192
222
  return ' '.join(command)
193
223
 
@@ -212,14 +242,20 @@ class WorkflowRunCommand(WorkflowCommand):
212
242
  if nameservers:
213
243
  fp.write("\n".join(nameservers))
214
244
 
215
- def write_env(self):
245
+ def write_env(self, **options: ComposeOptions):
246
+ namespace = options.get('namespace')
247
+ user_id = options.get('user_id')
248
+
216
249
  _config = {}
217
250
  _config[CA_CERTS_DIR_ENV] = self.ca_certs_dir_path
218
251
  _config[CERTS_DIR_ENV] = self.certs_dir_path
219
252
  _config[CONTEXT_DIR_ENV] = self.context_dir_path
220
253
  _config[SERVICE_NAME_ENV] = self.service_name
221
- _config[USER_ID_ENV] = os.getuid()
254
+ _config[USER_ID_ENV] = user_id or os.getuid()
222
255
  _config[WORKFLOW_NAME_ENV] = self.workflow_name
256
+
257
+ if namespace:
258
+ _config[WORKFLOW_NAMESPACE_ENV] = namespace
223
259
 
224
260
  if self.network:
225
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,20 +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.')
252
- @click.option('--network', help='Workflow network namespace.')
250
+ @click.option('--network', help='Workflow network name.')
253
251
  @click.option('--rmi', is_flag=True, help='Remove images used by containers.')
254
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.')
255
254
  @click.argument('workflow_name')
256
255
  def down(**kwargs):
257
256
  cwd = os.getcwd()
258
257
 
259
- if not os.getenv(env_vars.LOG_LEVEL):
260
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
258
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
261
259
 
262
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, skip_validate_path=True)
260
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
263
261
 
264
- if kwargs['context_dir_path']:
265
- 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']
266
265
 
267
266
  if not app.exists:
268
267
  print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
@@ -283,7 +282,7 @@ def down(**kwargs):
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
 
@@ -295,11 +294,16 @@ def down(**kwargs):
295
294
  # After services are stopped, their network needs to be removed
296
295
  if len(commands) > 0:
297
296
  command: WorkflowRunCommand = commands[0]
298
- remove_network_command = command.remove_network()
299
297
 
300
- if not kwargs['dry_run']:
301
- command.write_nameservers()
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)
302
304
 
305
+ remove_network_command = command.remove_network()
306
+ if not kwargs['dry_run']:
303
307
  exec_stream(remove_network_command)
304
308
  else:
305
309
  print(remove_network_command)
@@ -311,10 +315,15 @@ def down(**kwargs):
311
315
  )
312
316
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
313
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
+ ''')
314
321
  @click.option('--namespace', help='Workflow namespace.')
315
322
  @click.option('--service', multiple=True, help='Select which services to log. Defaults to all.')
316
323
  @click.argument('workflow_name')
317
324
  def logs(**kwargs):
325
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
326
+
318
327
  if len(kwargs['container']) == 0:
319
328
  kwargs['container'] = [WORKFLOW_CONTAINER_PROXY]
320
329
 
@@ -360,40 +369,34 @@ def logs(**kwargs):
360
369
 
361
370
  @workflow.command()
362
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.')
363
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')
364
374
  @click.option('--certs-dir-path', help='Path to certs directory. Defaults to the certs dir of the context.')
365
375
  @click.option('--context-dir-path', default=DataDir.instance().context_dir_path, help='Path to Stoobly data directory.')
366
376
  @click.option('--detached', is_flag=True, help='If set, will not run the highest priority service in the foreground.')
367
377
  @click.option('--dry-run', default=False, is_flag=True, help='If set, prints commands.')
368
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.')
369
380
  @click.option('--log-level', default=INFO, type=click.Choice([DEBUG, INFO, WARNING, ERROR]), help='''
370
381
  Log levels can be "debug", "info", "warning", or "error"
371
382
  ''')
372
383
  @click.option('--namespace', help='Workflow namespace.')
373
- @click.option('--network', help='Workflow network namespace.')
374
- @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.')
375
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.')
376
388
  @click.option('--verbose', is_flag=True)
377
389
  @click.argument('workflow_name')
378
390
  def up(**kwargs):
379
391
  cwd = os.getcwd()
380
392
 
381
- # Create the certs_dir_path if it doesn't exist
382
- DataDir.instance().certs_dir_path
383
-
384
- if not os.getenv(env_vars.LOG_LEVEL):
385
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
386
-
387
- app = App(
388
- kwargs['app_dir_path'], DOCKER_NAMESPACE,
389
- ca_certs_dir_path=kwargs['ca_certs_dir_path'], skip_validate_path=kwargs['dry_run']
390
- )
393
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
391
394
 
392
- if kwargs['certs_dir_path']:
393
- app.certs_dir_path = kwargs['certs_dir_path']
395
+ app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
394
396
 
395
- if kwargs['context_dir_path']:
396
- 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']
397
400
 
398
401
  if not app.exists:
399
402
  print(f"Error: {app.dir_path} does not exist", file=sys.stderr)
@@ -413,17 +416,23 @@ def up(**kwargs):
413
416
  command.current_working_dir = cwd
414
417
  commands.append(command)
415
418
 
416
- # 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
417
420
  if len(commands) > 0:
418
421
  command: WorkflowRunCommand = commands[0]
419
- create_network_command = command.create_network()
420
422
 
421
- if not kwargs['dry_run']:
422
- 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()
423
431
 
424
- exec_stream(create_network_command)
432
+ if not kwargs['dry_run']:
433
+ exec_stream(joined_command)
425
434
  else:
426
- print(create_network_command)
435
+ print(joined_command)
427
436
 
428
437
  commands = sorted(commands, key=lambda command: command.service_config.priority)
429
438
  for index, command in enumerate(commands):
@@ -433,7 +442,7 @@ def up(**kwargs):
433
442
  # However, this can change if the user has configured a service's priority to be higher
434
443
  attached = not kwargs['detached'] and index == len(commands) - 1
435
444
  exec_command = command.up(
436
- 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']
437
446
  )
438
447
  if not exec_command:
439
448
  continue
@@ -498,9 +507,6 @@ def __get_services(services: List[str], **kwargs):
498
507
  def __print_header(text: str):
499
508
  Logger.instance(LOG_ID).info(f"{bcolors.OKBLUE}{text}{bcolors.ENDC}")
500
509
 
501
- def __app_build(app, **kwargs):
502
- AppCreateCommand(app, **kwargs).build()
503
-
504
510
  def __scaffold_build(app, **kwargs):
505
511
  command = ServiceCreateCommand(app, **kwargs)
506
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)