thestage 0.5.46__py3-none-any.whl → 0.5.47__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.
- thestage/.env +5 -0
- thestage/__init__.py +2 -1
- thestage/cli_command.py +56 -0
- thestage/cli_command_helper.py +51 -0
- thestage/config/config_storage.py +3 -0
- thestage/controllers/base_controller.py +19 -15
- thestage/controllers/config_controller.py +30 -38
- thestage/controllers/container_controller.py +43 -79
- thestage/controllers/instance_controller.py +23 -40
- thestage/controllers/project_controller.py +95 -184
- thestage/controllers/utils_controller.py +12 -8
- thestage/debug_tests.py +12 -0
- thestage/entities/container.py +0 -5
- thestage/entities/rented_instance.py +0 -5
- thestage/entities/self_hosted_instance.py +0 -2
- thestage/helpers/error_handler.py +14 -14
- thestage/helpers/exception_hook.py +14 -0
- thestage/helpers/logger/app_logger.py +3 -4
- thestage/main.py +25 -13
- thestage/services/abstract_service.py +0 -40
- thestage/services/app_config_service.py +7 -6
- thestage/services/clients/.DS_Store +0 -0
- thestage/services/clients/thestage_api/api_client.py +54 -68
- thestage/services/clients/thestage_api/core/api_client_core.py +92 -9
- thestage/services/clients/thestage_api/dtos/container_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/inference_simulator_model_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/inference_simulator_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/instance_rented_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py +1 -1
- thestage/services/clients/thestage_api/dtos/validate_token_response.py +11 -0
- thestage/services/config_provider/config_provider.py +75 -47
- thestage/services/connect/connect_service.py +11 -22
- thestage/services/container/container_service.py +6 -23
- thestage/services/core_files/config_entity.py +7 -1
- thestage/services/filesystem_service.py +2 -2
- thestage/services/instance/instance_service.py +12 -26
- thestage/services/logging/logging_service.py +17 -45
- thestage/services/project/project_service.py +33 -72
- thestage/services/remote_server_service.py +3 -4
- thestage/services/service_factory.py +21 -27
- thestage/services/validation_service.py +14 -9
- {thestage-0.5.46.dist-info → thestage-0.5.47.dist-info}/METADATA +3 -4
- {thestage-0.5.46.dist-info → thestage-0.5.47.dist-info}/RECORD +47 -41
- {thestage-0.5.46.dist-info → thestage-0.5.47.dist-info}/WHEEL +1 -1
- thestage/exceptions/http_error_exception.py +0 -12
- thestage/services/clients/thestage_api/core/api_client_abstract.py +0 -91
- {thestage-0.5.46.dist-info → thestage-0.5.47.dist-info}/LICENSE.txt +0 -0
- {thestage-0.5.46.dist-info → thestage-0.5.47.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from typing import Optional, List, Dict
|
|
2
|
+
|
|
3
|
+
from pydantic import Field, BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from thestage.services.clients.thestage_api.dtos.base_response import TheStageBaseResponse, TheStageBasePaginatedResponse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ValidateTokenResponse(TheStageBaseResponse):
|
|
9
|
+
model_config = ConfigDict(use_enum_values=True)
|
|
10
|
+
|
|
11
|
+
allowedCliCommands: Optional[Dict[str, bool]] = Field(default={}, alias='allowedCliCommands')
|
|
@@ -2,50 +2,40 @@ import typer
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
-
from json import JSONDecodeError
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Optional, Dict, Any
|
|
8
7
|
|
|
9
|
-
from thestage.
|
|
8
|
+
from thestage.cli_command import CliCommand, CliCommandAvailability, ALWAYS_AVAILABLE_COMMANDS
|
|
9
|
+
from thestage.services.clients.thestage_api.dtos.validate_token_response import ValidateTokenResponse
|
|
10
10
|
from thestage.services.core_files.config_entity import ConfigEntity
|
|
11
11
|
from thestage.helpers.ssh_util import parse_private_key
|
|
12
12
|
from thestage.services.connect.dto.remote_server_config import RemoteServerConfig
|
|
13
13
|
from thestage.services.filesystem_service import FileSystemService
|
|
14
14
|
from thestage.services.project.dto.project_config import ProjectConfig
|
|
15
|
-
from thestage.config import THESTAGE_CONFIG_DIR, THESTAGE_CONFIG_FILE, THESTAGE_AUTH_TOKEN, THESTAGE_API_URL
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
_local_config_path: Optional[Path] = None
|
|
21
|
-
_global_config_path: Optional[Path] = None
|
|
22
|
-
_global_config_file: Optional[Path] = None
|
|
15
|
+
from thestage.config import THESTAGE_CONFIG_DIR, THESTAGE_CONFIG_FILE, THESTAGE_AUTH_TOKEN, THESTAGE_API_URL, \
|
|
16
|
+
config_storage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigProvider:
|
|
23
20
|
_file_system_service = FileSystemService
|
|
24
21
|
|
|
25
22
|
|
|
26
23
|
def __init__(
|
|
27
24
|
self,
|
|
28
|
-
|
|
25
|
+
file_system_service: FileSystemService,
|
|
29
26
|
):
|
|
30
|
-
self._file_system_service =
|
|
31
|
-
if local_path:
|
|
32
|
-
self._local_path = self._file_system_service.get_path(directory=local_path, auto_create=False)
|
|
33
|
-
|
|
34
|
-
self._local_config_path = self._local_path.joinpath(THESTAGE_CONFIG_DIR)
|
|
35
|
-
|
|
36
|
-
home_dir = self._file_system_service.get_home_path()
|
|
37
|
-
self._global_config_path = home_dir.joinpath(THESTAGE_CONFIG_DIR)
|
|
27
|
+
self._file_system_service = file_system_service
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
global_config_path = self.get_global_config_path()
|
|
30
|
+
if not global_config_path.exists():
|
|
31
|
+
self._file_system_service.create_if_not_exists_dir(global_config_path)
|
|
42
32
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
self._file_system_service.create_if_not_exists_file(self._global_config_file)
|
|
33
|
+
global_config_file = self.get_global_config_file()
|
|
34
|
+
self._file_system_service.create_if_not_exists_file(global_config_file)
|
|
46
35
|
|
|
47
36
|
|
|
48
|
-
|
|
37
|
+
# must be called right after the app is started
|
|
38
|
+
def build_config(self) -> ConfigEntity:
|
|
49
39
|
config_values = {}
|
|
50
40
|
|
|
51
41
|
config_from_env = {}
|
|
@@ -58,35 +48,36 @@ class ConfigProvider():
|
|
|
58
48
|
if config_from_env:
|
|
59
49
|
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_env)
|
|
60
50
|
|
|
61
|
-
config_from_file = self._file_system_service.read_config_file(self.
|
|
51
|
+
config_from_file = self._file_system_service.read_config_file(self.get_global_config_file())
|
|
62
52
|
if config_from_file:
|
|
63
53
|
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_file)
|
|
64
54
|
|
|
65
55
|
config = ConfigEntity.model_validate(config_values)
|
|
56
|
+
config.runtime.config_global_path = str(self.get_global_config_path())
|
|
66
57
|
|
|
67
|
-
|
|
68
|
-
config.runtime.working_directory = str(self._local_path)
|
|
69
|
-
if self._global_config_path and not config.runtime.config_global_path:
|
|
70
|
-
config.runtime.config_global_path = str(self._global_config_path)
|
|
58
|
+
config_storage.APP_CONFIG = config
|
|
71
59
|
|
|
72
60
|
return config
|
|
73
61
|
|
|
74
62
|
|
|
63
|
+
def get_config(self) -> ConfigEntity:
|
|
64
|
+
if not config_storage.APP_CONFIG:
|
|
65
|
+
raise Exception('Config storage was not initialized')
|
|
66
|
+
return config_storage.APP_CONFIG
|
|
67
|
+
|
|
68
|
+
|
|
75
69
|
def save_project_config(self, project_config: ProjectConfig):
|
|
76
|
-
project_data_dirpath = self.__get_project_config_path()
|
|
77
|
-
|
|
78
|
-
self._file_system_service.create_if_not_exists_dir(project_data_dirpath)
|
|
70
|
+
project_data_dirpath = self.__get_project_config_path(with_file=False)
|
|
71
|
+
self._file_system_service.create_if_not_exists_dir(project_data_dirpath)
|
|
79
72
|
|
|
80
73
|
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
81
|
-
|
|
82
|
-
self._file_system_service.create_if_not_exists_file(project_data_filepath)
|
|
74
|
+
self._file_system_service.create_if_not_exists_file(project_data_filepath)
|
|
83
75
|
|
|
84
|
-
|
|
85
|
-
self.__save_config_file(data=project_config.model_dump(), file_path=project_config_path)
|
|
76
|
+
self.__save_config_file(data=project_config.model_dump(), file_path=project_data_filepath)
|
|
86
77
|
|
|
87
78
|
|
|
88
79
|
def save_project_deploy_ssh_key(self, deploy_ssh_key: str, project_slug: str, project_id: int) -> str:
|
|
89
|
-
deploy_key_dirpath = self.
|
|
80
|
+
deploy_key_dirpath = self.get_global_config_path().joinpath('project_deploy_keys')
|
|
90
81
|
self._file_system_service.create_if_not_exists_dir(deploy_key_dirpath)
|
|
91
82
|
|
|
92
83
|
deploy_key_filepath = deploy_key_dirpath.joinpath(f'project_deploy_key_{project_id}_{project_slug}')
|
|
@@ -115,13 +106,13 @@ class ConfigProvider():
|
|
|
115
106
|
|
|
116
107
|
def __get_project_config_path(self, with_file: bool = False) -> Path:
|
|
117
108
|
if with_file:
|
|
118
|
-
return self.
|
|
109
|
+
return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR).joinpath('project.json')
|
|
119
110
|
else:
|
|
120
|
-
return self.
|
|
111
|
+
return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR)
|
|
121
112
|
|
|
122
113
|
|
|
123
114
|
def __get_remote_server_config_path(self) -> Path:
|
|
124
|
-
return self.
|
|
115
|
+
return self.get_global_config_path().joinpath('remote_server_config.json')
|
|
125
116
|
|
|
126
117
|
|
|
127
118
|
def update_remote_server_config_entry(self, ip_address: str, ssh_key_path: Optional[Path]):
|
|
@@ -191,15 +182,18 @@ class ConfigProvider():
|
|
|
191
182
|
json.dump(data, configfile, indent=1)
|
|
192
183
|
|
|
193
184
|
|
|
194
|
-
def save_config(self
|
|
195
|
-
|
|
185
|
+
def save_config(self):
|
|
186
|
+
config = self.get_config()
|
|
187
|
+
global_config_file = self.get_global_config_file()
|
|
188
|
+
data: Dict[str, Any] = self._file_system_service.read_config_file(global_config_file)
|
|
196
189
|
data.update(config.model_dump(exclude_none=True, by_alias=True, exclude={'runtime', 'RUNTIME', 'daemon', 'DAEMON'}))
|
|
197
|
-
self.__save_config_file(data=data, file_path=
|
|
190
|
+
self.__save_config_file(data=data, file_path=global_config_file)
|
|
198
191
|
|
|
199
192
|
|
|
200
193
|
def clear_config(self, ):
|
|
201
|
-
|
|
202
|
-
|
|
194
|
+
global_config_path = self.get_global_config_path()
|
|
195
|
+
if global_config_path.exists():
|
|
196
|
+
self._file_system_service.remove_folder(str(global_config_path))
|
|
203
197
|
|
|
204
198
|
os.unsetenv('THESTAGE_CONFIG_DIR')
|
|
205
199
|
os.unsetenv('THESTAGE_CONFIG_FILE')
|
|
@@ -207,3 +201,37 @@ class ConfigProvider():
|
|
|
207
201
|
os.unsetenv('THESTAGE_API_URL')
|
|
208
202
|
os.unsetenv('THESTAGE_LOG_FILE')
|
|
209
203
|
os.unsetenv('THESTAGE_AUTH_TOKEN')
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def update_config(self, updated_config: ConfigEntity):
|
|
207
|
+
config_storage.APP_CONFIG = updated_config
|
|
208
|
+
|
|
209
|
+
def get_global_config_path(self) -> Path:
|
|
210
|
+
home_dir = self._file_system_service.get_home_path()
|
|
211
|
+
return home_dir.joinpath(THESTAGE_CONFIG_DIR)
|
|
212
|
+
|
|
213
|
+
def get_global_config_file(self) -> Path:
|
|
214
|
+
return self.get_global_config_path().joinpath(THESTAGE_CONFIG_FILE)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def update_allowed_commands_and_is_token_valid(self, validate_token_response: ValidateTokenResponse):
|
|
218
|
+
config = self.get_config()
|
|
219
|
+
for cli_command_item in CliCommand:
|
|
220
|
+
is_command_available = validate_token_response.allowedCliCommands.get(cli_command_item)
|
|
221
|
+
command_availability = CliCommandAvailability.DEPRECATED
|
|
222
|
+
|
|
223
|
+
if cli_command_item in ALWAYS_AVAILABLE_COMMANDS:
|
|
224
|
+
command_availability = CliCommandAvailability.ALLOWED
|
|
225
|
+
else:
|
|
226
|
+
if is_command_available is True:
|
|
227
|
+
command_availability = CliCommandAvailability.ALLOWED
|
|
228
|
+
if is_command_available is False:
|
|
229
|
+
command_availability = CliCommandAvailability.RESTRICTED
|
|
230
|
+
config.runtime.allowed_commands.update({cli_command_item: command_availability})
|
|
231
|
+
config.runtime.is_token_valid = validate_token_response.is_success
|
|
232
|
+
self.update_config(config)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def is_cli_command_allowed(self, cli_command: CliCommand) -> bool:
|
|
236
|
+
config = self.get_config()
|
|
237
|
+
return True if config.runtime.allowed_commands.get(cli_command) == CliCommandAvailability.ALLOWED else False
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
import typer
|
|
3
3
|
|
|
4
|
+
from thestage.cli_command import CliCommand
|
|
5
|
+
from thestage.cli_command_helper import check_command_permission
|
|
4
6
|
from thestage.i18n.translation import __
|
|
5
7
|
from thestage.services.clients.thestage_api.core.http_client_exception import HttpClientException
|
|
6
8
|
from thestage.services.clients.thestage_api.dtos.enums.container_status import DockerContainerStatus
|
|
@@ -11,7 +13,6 @@ from thestage.helpers.error_handler import error_handler
|
|
|
11
13
|
from thestage.services.clients.thestage_api.api_client import TheStageApiClient
|
|
12
14
|
from thestage.services.clients.thestage_api.dtos.enums.task_status import TaskStatus
|
|
13
15
|
from thestage.services.clients.thestage_api.dtos.instance_rented_response import InstanceRentedDto
|
|
14
|
-
from thestage.services.config_provider.config_provider import ConfigProvider
|
|
15
16
|
from thestage.services.container.container_service import ContainerService
|
|
16
17
|
from thestage.services.instance.instance_service import InstanceService
|
|
17
18
|
from thestage.services.logging.logging_service import LoggingService
|
|
@@ -26,14 +27,12 @@ class ConnectService(AbstractService):
|
|
|
26
27
|
|
|
27
28
|
def __init__(
|
|
28
29
|
self,
|
|
29
|
-
config_provider: ConfigProvider,
|
|
30
30
|
thestage_api_client: TheStageApiClient,
|
|
31
31
|
instance_service: InstanceService,
|
|
32
32
|
container_service: ContainerService,
|
|
33
33
|
logging_service: LoggingService,
|
|
34
34
|
):
|
|
35
35
|
super(ConnectService, self).__init__(
|
|
36
|
-
config_provider=config_provider,
|
|
37
36
|
)
|
|
38
37
|
self.__thestage_api_client = thestage_api_client
|
|
39
38
|
self.__instance_service = instance_service
|
|
@@ -48,24 +47,22 @@ class ConnectService(AbstractService):
|
|
|
48
47
|
username: Optional[str],
|
|
49
48
|
private_key_path: Optional[str],
|
|
50
49
|
):
|
|
51
|
-
config = self._config_provider.get_full_config()
|
|
52
|
-
|
|
53
50
|
try:
|
|
54
|
-
instance_selfhosted = self.__thestage_api_client.get_selfhosted_instance(
|
|
51
|
+
instance_selfhosted = self.__thestage_api_client.get_selfhosted_instance(instance_slug=uid)
|
|
55
52
|
except HttpClientException as e:
|
|
56
53
|
if e.get_status_code() == 403:
|
|
57
54
|
typer.echo("Missing permission to view self-hosted instances")
|
|
58
55
|
instance_selfhosted = None
|
|
59
56
|
|
|
60
57
|
try:
|
|
61
|
-
instance_rented = self.__thestage_api_client.get_rented_instance(
|
|
58
|
+
instance_rented = self.__thestage_api_client.get_rented_instance(instance_slug=uid)
|
|
62
59
|
except HttpClientException as e:
|
|
63
60
|
if e.get_status_code() == 403:
|
|
64
61
|
typer.echo("Missing permission to view rented instances")
|
|
65
62
|
instance_rented = None
|
|
66
63
|
|
|
67
64
|
try:
|
|
68
|
-
container = self.__thestage_api_client.get_container(
|
|
65
|
+
container = self.__thestage_api_client.get_container(container_slug=uid,)
|
|
69
66
|
except HttpClientException as e:
|
|
70
67
|
if e.get_status_code() == 403:
|
|
71
68
|
typer.echo("Missing permission to view containers")
|
|
@@ -74,7 +71,7 @@ class ConnectService(AbstractService):
|
|
|
74
71
|
task: Optional[TaskDto] = None
|
|
75
72
|
if uid.isdigit():
|
|
76
73
|
try:
|
|
77
|
-
task_view_response = self.__thestage_api_client.get_task(
|
|
74
|
+
task_view_response = self.__thestage_api_client.get_task(task_id=int(uid))
|
|
78
75
|
except HttpClientException as e:
|
|
79
76
|
if e.get_status_code() == 403:
|
|
80
77
|
typer.echo("Missing permission to view tasks")
|
|
@@ -114,27 +111,27 @@ class ConnectService(AbstractService):
|
|
|
114
111
|
raise typer.Exit(code=1)
|
|
115
112
|
|
|
116
113
|
if rented_presence:
|
|
114
|
+
check_command_permission(CliCommand.INSTANCE_RENTED_CONNECT)
|
|
117
115
|
typer.echo("Connecting to rented instance...")
|
|
118
116
|
self.__instance_service.connect_to_rented_instance(
|
|
119
117
|
instance_rented_slug=uid,
|
|
120
|
-
config=config,
|
|
121
118
|
input_ssh_key_path=private_key_path
|
|
122
119
|
)
|
|
123
120
|
|
|
124
121
|
if container_presence:
|
|
122
|
+
check_command_permission(CliCommand.CONTAINER_CONNECT)
|
|
125
123
|
typer.echo("Connecting to docker container...")
|
|
126
124
|
self.__container_service.connect_to_container(
|
|
127
|
-
config=config,
|
|
128
125
|
container_uid=uid,
|
|
129
126
|
username=username,
|
|
130
127
|
input_ssh_key_path=private_key_path
|
|
131
128
|
)
|
|
132
129
|
|
|
133
130
|
if selfhosted_presence:
|
|
131
|
+
check_command_permission(CliCommand.INSTANCE_SELF_HOSTED_CONNECT)
|
|
134
132
|
typer.echo("Connecting to self-hosted instance...")
|
|
135
133
|
|
|
136
134
|
self.__instance_service.connect_to_selfhosted_instance(
|
|
137
|
-
config=config,
|
|
138
135
|
selfhosted_instance_slug=uid,
|
|
139
136
|
username=username,
|
|
140
137
|
input_ssh_key_path=private_key_path
|
|
@@ -142,20 +139,15 @@ class ConnectService(AbstractService):
|
|
|
142
139
|
|
|
143
140
|
if task_presence:
|
|
144
141
|
typer.echo(__("Connecting to task..."))
|
|
145
|
-
self.__logging_service.stream_task_logs_with_controls(
|
|
146
|
-
config=config,
|
|
147
|
-
task_id=int(uid),
|
|
148
|
-
)
|
|
142
|
+
self.__logging_service.stream_task_logs_with_controls(task_id=int(uid))
|
|
149
143
|
|
|
150
144
|
|
|
151
145
|
@error_handler()
|
|
152
146
|
def upload_ssh_key(self, public_key_contents: str, instance_slug: Optional[str]):
|
|
153
|
-
config = self._config_provider.get_full_config()
|
|
154
|
-
|
|
155
147
|
instance_rented: Optional[InstanceRentedDto] = None
|
|
156
148
|
if instance_slug:
|
|
157
149
|
try:
|
|
158
|
-
instance_rented = self.__thestage_api_client.get_rented_instance(
|
|
150
|
+
instance_rented = self.__thestage_api_client.get_rented_instance(instance_slug=instance_slug)
|
|
159
151
|
except HttpClientException as e:
|
|
160
152
|
instance_rented = None
|
|
161
153
|
|
|
@@ -167,7 +159,6 @@ class ConnectService(AbstractService):
|
|
|
167
159
|
note_to_send: Optional[str] = None
|
|
168
160
|
|
|
169
161
|
is_user_already_has_key_response = self.__thestage_api_client.is_user_has_ssh_public_key(
|
|
170
|
-
token=config.main.thestage_auth_token,
|
|
171
162
|
public_key=public_key_contents
|
|
172
163
|
)
|
|
173
164
|
|
|
@@ -187,7 +178,6 @@ class ConnectService(AbstractService):
|
|
|
187
178
|
|
|
188
179
|
if is_adding_key_to_user:
|
|
189
180
|
add_ssh_key_to_user_response = self.__thestage_api_client.add_public_ssh_key_to_user(
|
|
190
|
-
token=config.main.thestage_auth_token,
|
|
191
181
|
public_key=public_key_contents,
|
|
192
182
|
note=note_to_send
|
|
193
183
|
)
|
|
@@ -196,7 +186,6 @@ class ConnectService(AbstractService):
|
|
|
196
186
|
|
|
197
187
|
if instance_rented:
|
|
198
188
|
self.__thestage_api_client.add_public_ssh_key_to_instance_rented(
|
|
199
|
-
token=config.main.thestage_auth_token,
|
|
200
189
|
instance_rented_id=instance_rented.id,
|
|
201
190
|
ssh_key_pair_id=ssh_key_pair_id
|
|
202
191
|
)
|
|
@@ -2,7 +2,6 @@ from pathlib import Path
|
|
|
2
2
|
from typing import List, Tuple, Optional, Dict
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
|
-
from thestage.services.core_files.config_entity import ConfigEntity
|
|
6
5
|
from thestage.entities.container import DockerContainerEntity
|
|
7
6
|
from thestage.services.clients.thestage_api.dtos.container_param_request import DockerContainerActionRequestDto
|
|
8
7
|
from thestage.services.clients.thestage_api.dtos.enums.container_pending_action import DockerContainerAction
|
|
@@ -23,6 +22,7 @@ from thestage.services.config_provider.config_provider import ConfigProvider
|
|
|
23
22
|
class ContainerService(AbstractService):
|
|
24
23
|
|
|
25
24
|
__thestage_api_client: TheStageApiClient = None
|
|
25
|
+
__config_provider: ConfigProvider = None
|
|
26
26
|
|
|
27
27
|
def __init__(
|
|
28
28
|
self,
|
|
@@ -31,9 +31,7 @@ class ContainerService(AbstractService):
|
|
|
31
31
|
remote_server_service: RemoteServerService,
|
|
32
32
|
file_system_service: FileSystemService,
|
|
33
33
|
):
|
|
34
|
-
|
|
35
|
-
config_provider=config_provider
|
|
36
|
-
)
|
|
34
|
+
self.__config_provider = config_provider
|
|
37
35
|
self.__thestage_api_client = thestage_api_client
|
|
38
36
|
self.__remote_server_service = remote_server_service
|
|
39
37
|
self.__file_system_service = file_system_service
|
|
@@ -42,13 +40,12 @@ class ContainerService(AbstractService):
|
|
|
42
40
|
@error_handler()
|
|
43
41
|
def print_container_list(
|
|
44
42
|
self,
|
|
45
|
-
config: ConfigEntity,
|
|
46
43
|
row: int,
|
|
47
44
|
page: int,
|
|
48
45
|
project_uid: Optional[str],
|
|
49
46
|
statuses: List[str],
|
|
50
47
|
):
|
|
51
|
-
container_status_map = self.__thestage_api_client.get_container_business_status_map(
|
|
48
|
+
container_status_map = self.__thestage_api_client.get_container_business_status_map()
|
|
52
49
|
|
|
53
50
|
if not statuses:
|
|
54
51
|
statuses = ({key: container_status_map[key] for key in [
|
|
@@ -77,7 +74,7 @@ class ContainerService(AbstractService):
|
|
|
77
74
|
|
|
78
75
|
project_id: Optional[int] = None
|
|
79
76
|
if project_uid:
|
|
80
|
-
project = self.__thestage_api_client.get_project_by_slug(slug=project_uid
|
|
77
|
+
project = self.__thestage_api_client.get_project_by_slug(slug=project_uid)
|
|
81
78
|
project_id = project.id
|
|
82
79
|
|
|
83
80
|
self.print(
|
|
@@ -87,7 +84,6 @@ class ContainerService(AbstractService):
|
|
|
87
84
|
'project_id': project_id,
|
|
88
85
|
},
|
|
89
86
|
mapper=ContainerMapper(),
|
|
90
|
-
config=config,
|
|
91
87
|
headers=list(map(lambda x: x.alias, DockerContainerEntity.model_fields.values())),
|
|
92
88
|
row=row,
|
|
93
89
|
page=page,
|
|
@@ -99,7 +95,6 @@ class ContainerService(AbstractService):
|
|
|
99
95
|
@error_handler()
|
|
100
96
|
def get_list(
|
|
101
97
|
self,
|
|
102
|
-
config: ConfigEntity,
|
|
103
98
|
statuses: List[str],
|
|
104
99
|
row: int = 5,
|
|
105
100
|
page: int = 1,
|
|
@@ -107,7 +102,6 @@ class ContainerService(AbstractService):
|
|
|
107
102
|
) -> PaginatedEntityList[DockerContainerDto]:
|
|
108
103
|
|
|
109
104
|
list = self.__thestage_api_client.get_container_list(
|
|
110
|
-
token=config.main.thestage_auth_token,
|
|
111
105
|
statuses=statuses,
|
|
112
106
|
page=page,
|
|
113
107
|
limit=row,
|
|
@@ -119,12 +113,10 @@ class ContainerService(AbstractService):
|
|
|
119
113
|
@error_handler()
|
|
120
114
|
def get_container(
|
|
121
115
|
self,
|
|
122
|
-
config: ConfigEntity,
|
|
123
116
|
container_id: Optional[int] = None,
|
|
124
117
|
container_slug: Optional[str] = None,
|
|
125
118
|
) -> Optional[DockerContainerDto]:
|
|
126
119
|
return self.__thestage_api_client.get_container(
|
|
127
|
-
token=config.main.thestage_auth_token,
|
|
128
120
|
container_id=container_id,
|
|
129
121
|
container_slug=container_slug,
|
|
130
122
|
)
|
|
@@ -154,13 +146,13 @@ class ContainerService(AbstractService):
|
|
|
154
146
|
|
|
155
147
|
private_key_path = private_key_path_override
|
|
156
148
|
if not private_key_path:
|
|
157
|
-
private_key_path = self.
|
|
149
|
+
private_key_path = self.__config_provider.get_valid_private_key_path_by_ip_address(ip_address)
|
|
158
150
|
if private_key_path:
|
|
159
151
|
typer.echo(f'Using configured private key for {ip_address}: {private_key_path}')
|
|
160
152
|
else:
|
|
161
153
|
typer.echo(f'Using SSH agent to connect to {ip_address}')
|
|
162
154
|
else:
|
|
163
|
-
self.
|
|
155
|
+
self.__config_provider.update_remote_server_config_entry(ip_address, Path(private_key_path))
|
|
164
156
|
typer.echo(f'Updated private key path for {ip_address}: {private_key_path}')
|
|
165
157
|
|
|
166
158
|
return username, ip_address, private_key_path
|
|
@@ -168,13 +160,11 @@ class ContainerService(AbstractService):
|
|
|
168
160
|
@error_handler()
|
|
169
161
|
def connect_to_container(
|
|
170
162
|
self,
|
|
171
|
-
config: ConfigEntity,
|
|
172
163
|
container_uid: str,
|
|
173
164
|
username: Optional[str],
|
|
174
165
|
input_ssh_key_path: Optional[str],
|
|
175
166
|
):
|
|
176
167
|
container: Optional[DockerContainerDto] = self.get_container(
|
|
177
|
-
config=config,
|
|
178
168
|
container_slug=container_uid,
|
|
179
169
|
)
|
|
180
170
|
|
|
@@ -321,7 +311,6 @@ class ContainerService(AbstractService):
|
|
|
321
311
|
container: DockerContainerDto,
|
|
322
312
|
src_path: str,
|
|
323
313
|
copy_only_folder_contents: bool,
|
|
324
|
-
config: ConfigEntity,
|
|
325
314
|
destination_path: Optional[str] = None,
|
|
326
315
|
username_param: Optional[str] = None,
|
|
327
316
|
):
|
|
@@ -344,8 +333,6 @@ class ContainerService(AbstractService):
|
|
|
344
333
|
typer.echo(__("Cannot find matching container volume mapping for specified file path"))
|
|
345
334
|
raise typer.Exit(1)
|
|
346
335
|
|
|
347
|
-
|
|
348
|
-
|
|
349
336
|
self.__remote_server_service.download_data_from_container(
|
|
350
337
|
ip_address=ip_address,
|
|
351
338
|
username=username,
|
|
@@ -353,19 +340,16 @@ class ContainerService(AbstractService):
|
|
|
353
340
|
instance_path=instance_path,
|
|
354
341
|
copy_only_folder_contents=copy_only_folder_contents,
|
|
355
342
|
private_key_path=private_key_path,
|
|
356
|
-
config=config,
|
|
357
343
|
)
|
|
358
344
|
|
|
359
345
|
|
|
360
346
|
@error_handler()
|
|
361
347
|
def request_docker_container_action(
|
|
362
348
|
self,
|
|
363
|
-
config: ConfigEntity,
|
|
364
349
|
container_uid: str,
|
|
365
350
|
action: DockerContainerAction,
|
|
366
351
|
):
|
|
367
352
|
container: Optional[DockerContainerDto] = self.get_container(
|
|
368
|
-
config=config,
|
|
369
353
|
container_slug=container_uid,
|
|
370
354
|
)
|
|
371
355
|
if not container:
|
|
@@ -383,7 +367,6 @@ class ContainerService(AbstractService):
|
|
|
383
367
|
action=action,
|
|
384
368
|
)
|
|
385
369
|
result = self.__thestage_api_client.container_action(
|
|
386
|
-
token=config.main.thestage_auth_token,
|
|
387
370
|
request_param=request_params,
|
|
388
371
|
)
|
|
389
372
|
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional, Dict
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, Field
|
|
4
4
|
|
|
5
|
+
from thestage.cli_command import CliCommand, CliCommandAvailability
|
|
6
|
+
|
|
7
|
+
|
|
5
8
|
# saved to file
|
|
6
9
|
class MainConfigEntity(BaseModel):
|
|
7
10
|
thestage_auth_token: Optional[str] = Field(None, alias='thestage_auth_token')
|
|
8
11
|
thestage_api_url: Optional[str] = Field(None, alias='thestage_api_url')
|
|
9
12
|
|
|
13
|
+
|
|
10
14
|
# not saved to file
|
|
11
15
|
class RuntimeConfigEntity(BaseModel):
|
|
12
16
|
working_directory: Optional[str] = Field(None, alias='working_directory')
|
|
13
17
|
config_global_path: Optional[str] = Field(None, alias='config_global_path')
|
|
18
|
+
allowed_commands: Dict[CliCommand, CliCommandAvailability] = {}
|
|
19
|
+
is_token_valid: bool = Field(None, alias='is_token_valid')
|
|
14
20
|
|
|
15
21
|
|
|
16
22
|
class ConfigEntity(BaseModel):
|
|
@@ -126,8 +126,8 @@ class FileSystemService:
|
|
|
126
126
|
try:
|
|
127
127
|
if os.stat(path).st_size != 0:
|
|
128
128
|
result = json.load(file)
|
|
129
|
-
except JSONDecodeError:
|
|
130
|
-
|
|
129
|
+
except JSONDecodeError as e:
|
|
130
|
+
raise Exception(f"Config file is malformed: {path}") from e
|
|
131
131
|
except OSError:
|
|
132
132
|
raise FileSystemException(f"Could not open config file: {path}")
|
|
133
133
|
return result
|