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.
- 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/builder.py +1 -1
- 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 +74 -35
- stoobly_agent/app/cli/scaffold_cli.py +61 -44
- 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.2.dist-info → stoobly_agent-1.3.0.dist-info}/METADATA +1 -1
- {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/RECORD +68 -69
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.proxy +0 -34
- {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/LICENSE +0 -0
- {stoobly_agent-1.2.2.dist-info → stoobly_agent-1.3.0.dist-info}/WHEEL +0 -0
- {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
|
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,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}
|
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
|
-
|
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 '
|
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
|
-
|
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(
|
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.
|
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,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
|
-
|
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,
|
260
|
+
app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
|
262
261
|
|
263
|
-
|
264
|
-
|
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
|
363
|
-
@click.option('--
|
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
|
-
|
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
|
-
|
382
|
-
|
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
|
-
|
411
|
-
|
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
|
-
|
432
|
+
if not kwargs['dry_run']:
|
433
|
+
exec_stream(joined_command)
|
414
434
|
else:
|
415
|
-
print(
|
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'],
|
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
|
|
@@ -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)
|