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.
- stoobly_agent/__init__.py +1 -1
- stoobly_agent/app/cli/helpers/certificate_authority.py +1 -5
- stoobly_agent/app/cli/scaffold/app.py +14 -32
- stoobly_agent/app/cli/scaffold/app_command.py +4 -7
- stoobly_agent/app/cli/scaffold/app_config.py +15 -2
- stoobly_agent/app/cli/scaffold/app_create_command.py +18 -2
- stoobly_agent/app/cli/scaffold/command.py +1 -1
- stoobly_agent/app/cli/scaffold/constants.py +9 -3
- stoobly_agent/app/cli/scaffold/docker/app_builder.py +3 -7
- stoobly_agent/app/cli/scaffold/docker/constants.py +0 -1
- stoobly_agent/app/cli/scaffold/docker/service/builder.py +12 -11
- stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +14 -31
- stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +6 -2
- stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +6 -2
- stoobly_agent/app/cli/scaffold/service.py +1 -1
- stoobly_agent/app/cli/scaffold/service_command.py +1 -1
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +6 -8
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +2 -4
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile +37 -21
- stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +8 -13
- stoobly_agent/app/cli/scaffold/templates/app/Makefile +1 -1
- stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +8 -4
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/.docker-compose.mock.yml +2 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/.init +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/record/.docker-compose.record.yml +2 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/.init +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/test/.docker-compose.test.yml +2 -6
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/.init +3 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -0
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/.docker-compose.mock.yml +2 -8
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/.docker-compose.record.yml +2 -8
- stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/.docker-compose.test.yml +2 -8
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/.docker-compose.exec.yml +2 -3
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/exec/bin/.logs +1 -0
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +1 -2
- stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +1 -2
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/.init +7 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/.init +7 -1
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.configure +3 -0
- stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/.init +7 -1
- stoobly_agent/app/cli/scaffold/validate_command.py +2 -2
- stoobly_agent/app/cli/scaffold/workflow.py +5 -4
- stoobly_agent/app/cli/scaffold/workflow_command.py +3 -3
- stoobly_agent/app/cli/scaffold/workflow_run_command.py +72 -36
- stoobly_agent/app/cli/scaffold_cli.py +51 -45
- stoobly_agent/app/cli/snapshot_cli.py +6 -2
- stoobly_agent/app/models/adapters/joined_request_adapter.py +6 -0
- stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +3 -1
- stoobly_agent/app/models/helpers/apply.py +34 -17
- stoobly_agent/app/models/helpers/create_request_params_service.py +4 -0
- stoobly_agent/app/proxy/replay/body_parser_service.py +11 -3
- stoobly_agent/config/data_dir.py +2 -1
- stoobly_agent/config/schema.yml +2 -2
- stoobly_agent/test/app/cli/scaffold/cli_invoker.py +1 -2
- stoobly_agent/test/app/cli/snapshot/snapshot_apply_test.py +162 -1
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
- stoobly_agent/test/mock_data/scaffold/docker-compose-assets-service.yml +1 -3
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/METADATA +1 -1
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/RECORD +67 -68
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.2.3.dist-info → stoobly_agent-1.3.0.dist-info}/WHEEL +0 -0
- {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
|
23
|
-
attached: bool
|
23
|
+
class ComposeOptions(TypedDict):
|
24
24
|
namespace: str
|
25
|
-
|
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}
|
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}
|
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
|
-
|
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 '
|
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
|
-
|
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(
|
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.
|
72
|
-
|
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,
|
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
|
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
|
-
|
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,
|
260
|
+
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
|
263
261
|
|
264
|
-
|
265
|
-
|
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
|
301
|
-
command.
|
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
|
374
|
-
@click.option('--
|
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
|
-
|
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
|
-
|
393
|
-
app.certs_dir_path = kwargs['certs_dir_path']
|
395
|
+
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
|
394
396
|
|
395
|
-
|
396
|
-
|
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
|
-
|
422
|
-
|
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
|
-
|
432
|
+
if not kwargs['dry_run']:
|
433
|
+
exec_stream(joined_command)
|
425
434
|
else:
|
426
|
-
print(
|
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'],
|
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
|
|
@@ -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
|
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 >=
|
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
|
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}
|
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"
|
154
|
-
self.__logger(f"{bcolors.
|
155
|
-
return error,
|
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}
|
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"
|
175
|
-
self.__logger(f"{bcolors.
|
176
|
-
return error,
|
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
|
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.
|
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
|
-
**
|
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.
|
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
|
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
|
+
|
stoobly_agent/config/data_dir.py
CHANGED
@@ -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,
|
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)
|