thestage 0.6.2__py3-none-any.whl → 0.6.4__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 +4 -5
- thestage/__init__.py +3 -3
- thestage/__main__.py +9 -9
- thestage/cli_command.py +56 -56
- thestage/cli_command_helper.py +51 -51
- thestage/color_scheme/color_scheme.py +7 -7
- thestage/config/__init__.py +18 -18
- thestage/config/config_storage.py +5 -5
- thestage/config/env_base.py +7 -7
- thestage/controllers/__init__.py +0 -0
- thestage/controllers/base_controller.py +67 -67
- thestage/controllers/config_controller.py +137 -137
- thestage/controllers/container_controller.py +389 -389
- thestage/controllers/instance_controller.py +183 -183
- thestage/controllers/project_controller.py +810 -810
- thestage/controllers/utils_controller.py +32 -32
- thestage/debug_main.dist.py +28 -28
- thestage/entities/__init__.py +0 -0
- thestage/entities/container.py +17 -17
- thestage/entities/enums/__init__.py +0 -0
- thestage/entities/enums/order_direction_type.py +6 -6
- thestage/entities/enums/shell_type.py +7 -7
- thestage/entities/enums/tail_output_type.py +6 -6
- thestage/entities/enums/yes_no_response.py +7 -7
- thestage/entities/file_item.py +27 -27
- thestage/entities/project_inference_simulator.py +18 -18
- thestage/entities/project_inference_simulator_model.py +17 -17
- thestage/entities/project_task.py +19 -19
- thestage/entities/rented_instance.py +19 -19
- thestage/entities/self_hosted_instance.py +18 -18
- thestage/exceptions/__init__.py +0 -0
- thestage/exceptions/auth_exception.py +6 -6
- thestage/exceptions/base_exception.py +13 -13
- thestage/exceptions/business_logic_exception.py +6 -6
- thestage/exceptions/config_exception.py +6 -6
- thestage/exceptions/file_system_exception.py +6 -6
- thestage/exceptions/git_access_exception.py +17 -17
- thestage/exceptions/remote_server_exception.py +24 -24
- thestage/git/ProgressPrinter.py +22 -22
- thestage/helpers/__init__.py +0 -0
- thestage/helpers/error_handler.py +115 -115
- thestage/helpers/exception_hook.py +14 -14
- thestage/helpers/logger/__init__.py +0 -0
- thestage/helpers/logger/app_logger.py +50 -50
- thestage/helpers/ssh_util.py +38 -38
- thestage/i18n/en_GB/messages.po +947 -947
- thestage/i18n/translation.py +9 -9
- thestage/main.py +36 -36
- thestage/services/.env +6 -6
- thestage/services/__init__.py +0 -0
- thestage/services/abstract_mapper.py +9 -9
- thestage/services/abstract_service.py +87 -87
- thestage/services/app_config_service.py +52 -52
- thestage/services/clients/__init__.py +0 -0
- thestage/services/clients/git/__init__.py +0 -0
- thestage/services/clients/git/git_client.py +436 -436
- thestage/services/clients/thestage_api/__init__.py +0 -0
- thestage/services/clients/thestage_api/api_client.py +718 -718
- thestage/services/clients/thestage_api/core/api_client_core.py +108 -108
- thestage/services/clients/thestage_api/core/http_client_exception.py +12 -12
- thestage/services/clients/thestage_api/dtos/__init__.py +0 -0
- thestage/services/clients/thestage_api/dtos/base_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +19 -19
- thestage/services/clients/thestage_api/dtos/container_param_request.py +11 -11
- thestage/services/clients/thestage_api/dtos/container_response.py +67 -67
- thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +10 -10
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/docker_container_mapping.py +10 -10
- thestage/services/clients/thestage_api/dtos/entity_filter_request.py +14 -14
- thestage/services/clients/thestage_api/dtos/enums/__init__.py +0 -0
- thestage/services/clients/thestage_api/dtos/enums/container_pending_action.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/container_status.py +17 -17
- thestage/services/clients/thestage_api/dtos/enums/cpu_type.py +8 -8
- thestage/services/clients/thestage_api/dtos/enums/currency_type.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +9 -9
- thestage/services/clients/thestage_api/dtos/enums/disk_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/drive_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/gpu_name.py +8 -8
- thestage/services/clients/thestage_api/dtos/enums/inference_model_status.py +9 -9
- thestage/services/clients/thestage_api/dtos/enums/inference_simulator_status.py +15 -15
- thestage/services/clients/thestage_api/dtos/enums/instance_rented_status.py +17 -17
- thestage/services/clients/thestage_api/dtos/enums/instance_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/location_region.py +11 -11
- thestage/services/clients/thestage_api/dtos/enums/power_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/provider_name.py +11 -11
- thestage/services/clients/thestage_api/dtos/enums/selfhosted_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/task_execution_status.py +12 -12
- thestage/services/clients/thestage_api/dtos/enums/task_status.py +12 -12
- thestage/services/clients/thestage_api/dtos/frontend_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +10 -10
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_request.py +14 -14
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_simulator_model_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/inference_simulator_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/installed_service.py +17 -17
- thestage/services/clients/thestage_api/dtos/instance_detected_gpus.py +20 -20
- thestage/services/clients/thestage_api/dtos/instance_rented_response.py +71 -71
- thestage/services/clients/thestage_api/dtos/logging_controller/docker_container_log_stream_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_response.py +14 -14
- thestage/services/clients/thestage_api/dtos/logging_controller/task_log_stream_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +21 -21
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_response.py +14 -14
- thestage/services/clients/thestage_api/dtos/paginated_entity_list.py +11 -11
- thestage/services/clients/thestage_api/dtos/pagination_data.py +10 -10
- thestage/services/clients/thestage_api/dtos/price_definition.py +14 -14
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_response.py +6 -6
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +15 -15
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_response.py +32 -32
- thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +56 -56
- thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +13 -13
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +10 -10
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py +9 -9
- thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/user_controller/user_profile.py +12 -12
- thestage/services/clients/thestage_api/dtos/validate_token_response.py +11 -11
- thestage/services/config_provider/__init__.py +0 -0
- thestage/services/config_provider/config_provider.py +237 -237
- thestage/services/connect/connect_service.py +193 -196
- thestage/services/connect/dto/remote_server_config.py +9 -9
- thestage/services/container/__init__.py +0 -0
- thestage/services/container/container_service.py +374 -374
- thestage/services/container/mapper/__init__.py +0 -0
- thestage/services/container/mapper/container_mapper.py +30 -30
- thestage/services/core_files/config_entity.py +26 -26
- thestage/services/filesystem_service.py +133 -133
- thestage/services/instance/__init__.py +0 -0
- thestage/services/instance/instance_service.py +303 -303
- thestage/services/instance/mapper/__init__.py +0 -0
- thestage/services/instance/mapper/instance_mapper.py +24 -24
- thestage/services/instance/mapper/selfhosted_mapper.py +33 -33
- thestage/services/logging/byte_print_style.py +5 -5
- thestage/services/logging/dto/log_message.py +15 -15
- thestage/services/logging/dto/log_type.py +6 -6
- thestage/services/logging/exception/log_polling_exception.py +6 -6
- thestage/services/logging/logging_constants.py +3 -3
- thestage/services/logging/logging_service.py +367 -367
- thestage/services/project/__init__.py +0 -0
- thestage/services/project/dto/inference_simulator_dto.py +22 -22
- thestage/services/project/dto/inference_simulator_model_dto.py +20 -20
- thestage/services/project/dto/project_config.py +14 -14
- thestage/services/project/mapper/__init__.py +0 -0
- thestage/services/project/mapper/project_inference_simulator_mapper.py +21 -21
- thestage/services/project/mapper/project_inference_simulator_model_mapper.py +21 -21
- thestage/services/project/mapper/project_task_mapper.py +22 -22
- thestage/services/project/project_service.py +1253 -1253
- thestage/services/remote_server_service.py +609 -609
- thestage/services/service_factory.py +97 -97
- thestage/services/task/dto/task_dto.py +40 -40
- thestage/services/validation_service.py +61 -61
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/LICENSE.txt +12 -12
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/METADATA +3 -2
- thestage-0.6.4.dist-info/RECORD +176 -0
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/WHEEL +1 -1
- thestage-0.6.2.dist-info/RECORD +0 -176
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,237 +1,237 @@
|
|
|
1
|
-
import typer
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional, Dict, Any
|
|
7
|
-
|
|
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
|
-
from thestage.services.core_files.config_entity import ConfigEntity
|
|
11
|
-
from thestage.helpers.ssh_util import parse_private_key
|
|
12
|
-
from thestage.services.connect.dto.remote_server_config import RemoteServerConfig
|
|
13
|
-
from thestage.services.filesystem_service import FileSystemService
|
|
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
|
-
config_storage
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class ConfigProvider:
|
|
20
|
-
_file_system_service = FileSystemService
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def __init__(
|
|
24
|
-
self,
|
|
25
|
-
file_system_service: FileSystemService,
|
|
26
|
-
):
|
|
27
|
-
self._file_system_service = file_system_service
|
|
28
|
-
|
|
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)
|
|
32
|
-
|
|
33
|
-
global_config_file = self.get_global_config_file()
|
|
34
|
-
self._file_system_service.create_if_not_exists_file(global_config_file)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# must be called right after the app is started
|
|
38
|
-
def build_config(self) -> ConfigEntity:
|
|
39
|
-
config_values = {}
|
|
40
|
-
|
|
41
|
-
config_from_env = {}
|
|
42
|
-
config_from_env['main'] = {}
|
|
43
|
-
if THESTAGE_AUTH_TOKEN:
|
|
44
|
-
config_from_env['main']['thestage_auth_token'] = THESTAGE_AUTH_TOKEN
|
|
45
|
-
if THESTAGE_API_URL:
|
|
46
|
-
config_from_env['main']['thestage_api_url'] = THESTAGE_API_URL
|
|
47
|
-
|
|
48
|
-
if config_from_env:
|
|
49
|
-
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_env)
|
|
50
|
-
|
|
51
|
-
config_from_file = self._file_system_service.read_config_file(self.get_global_config_file())
|
|
52
|
-
if config_from_file:
|
|
53
|
-
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_file)
|
|
54
|
-
|
|
55
|
-
config = ConfigEntity.model_validate(config_values)
|
|
56
|
-
config.runtime.config_global_path = str(self.get_global_config_path())
|
|
57
|
-
|
|
58
|
-
config_storage.APP_CONFIG = config
|
|
59
|
-
|
|
60
|
-
return config
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
def save_project_config(self, project_config: ProjectConfig):
|
|
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)
|
|
72
|
-
|
|
73
|
-
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
74
|
-
self._file_system_service.create_if_not_exists_file(project_data_filepath)
|
|
75
|
-
|
|
76
|
-
self.__save_config_file(data=project_config.model_dump(), file_path=project_data_filepath)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def save_project_deploy_ssh_key(self, deploy_ssh_key: str, project_slug: str, project_id: int) -> str:
|
|
80
|
-
deploy_key_dirpath = self.get_global_config_path().joinpath('project_deploy_keys')
|
|
81
|
-
self._file_system_service.create_if_not_exists_dir(deploy_key_dirpath)
|
|
82
|
-
|
|
83
|
-
deploy_key_filepath = deploy_key_dirpath.joinpath(f'project_deploy_key_{project_id}_{project_slug}')
|
|
84
|
-
self._file_system_service.create_if_not_exists_file(deploy_key_filepath)
|
|
85
|
-
|
|
86
|
-
text_file = open(deploy_key_filepath, "w")
|
|
87
|
-
text_file.write(deploy_ssh_key)
|
|
88
|
-
text_file.close()
|
|
89
|
-
os.chmod(deploy_key_filepath, 0o600)
|
|
90
|
-
|
|
91
|
-
return str(deploy_key_filepath)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def read_project_config(self) -> Optional[ProjectConfig]:
|
|
95
|
-
project_data_dirpath = self.__get_project_config_path()
|
|
96
|
-
if not project_data_dirpath.exists():
|
|
97
|
-
return None
|
|
98
|
-
|
|
99
|
-
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
100
|
-
if not project_data_filepath.exists():
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
config_data = self._file_system_service.read_config_file(project_data_filepath) if project_data_filepath and project_data_filepath.exists() else {}
|
|
104
|
-
return ProjectConfig.model_validate(config_data)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def __get_project_config_path(self, with_file: bool = False) -> Path:
|
|
108
|
-
if with_file:
|
|
109
|
-
return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR).joinpath('project.json')
|
|
110
|
-
else:
|
|
111
|
-
return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def __get_remote_server_config_path(self) -> Path:
|
|
115
|
-
return self.get_global_config_path().joinpath('remote_server_config.json')
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def update_remote_server_config_entry(self, ip_address: str, ssh_key_path: Optional[Path]):
|
|
119
|
-
# double checking if key is parseable (fails if key is encrypted)
|
|
120
|
-
# see remote_server_service for similar check
|
|
121
|
-
if ssh_key_path:
|
|
122
|
-
pkey = parse_private_key(str(ssh_key_path))
|
|
123
|
-
|
|
124
|
-
if pkey is None:
|
|
125
|
-
typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
|
|
126
|
-
raise typer.Exit(1)
|
|
127
|
-
|
|
128
|
-
config = self.__read_remote_server_config()
|
|
129
|
-
remote_server_config_filepath = self.__get_remote_server_config_path()
|
|
130
|
-
if config:
|
|
131
|
-
if not config.ip_address_to_ssh_key_map:
|
|
132
|
-
config.ip_address_to_ssh_key_map = {}
|
|
133
|
-
|
|
134
|
-
existing_path = config.ip_address_to_ssh_key_map.get(ip_address)
|
|
135
|
-
if ssh_key_path and existing_path != str(ssh_key_path):
|
|
136
|
-
config.ip_address_to_ssh_key_map.update({ip_address: str(ssh_key_path)})
|
|
137
|
-
typer.echo(f'Updated ssh key for {ip_address}: {ssh_key_path}')
|
|
138
|
-
|
|
139
|
-
if not ssh_key_path and existing_path:
|
|
140
|
-
typer.echo(f"Private key at path {existing_path} was not found")
|
|
141
|
-
config.ip_address_to_ssh_key_map.pop(ip_address, None)
|
|
142
|
-
else:
|
|
143
|
-
self._file_system_service.create_if_not_exists_file(remote_server_config_filepath)
|
|
144
|
-
config = RemoteServerConfig(ip_address_to_ssh_key_map={ip_address: str(ssh_key_path)})
|
|
145
|
-
|
|
146
|
-
self.__save_config_file(data=config.model_dump(), file_path=remote_server_config_filepath)
|
|
147
|
-
|
|
148
|
-
return str(remote_server_config_filepath)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def __read_remote_server_config(self) -> Optional[RemoteServerConfig]:
|
|
152
|
-
config_filepath = self.__get_remote_server_config_path()
|
|
153
|
-
if not config_filepath.is_file():
|
|
154
|
-
return None
|
|
155
|
-
|
|
156
|
-
config_data = self._file_system_service.read_config_file(config_filepath) if config_filepath and config_filepath.exists() else {}
|
|
157
|
-
return RemoteServerConfig.model_validate(config_data)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def get_valid_private_key_path_by_ip_address(self, ip_address: str) -> Optional[str]:
|
|
161
|
-
remote_server_config = self.__read_remote_server_config()
|
|
162
|
-
if remote_server_config and remote_server_config.ip_address_to_ssh_key_map:
|
|
163
|
-
private_key_path = remote_server_config.ip_address_to_ssh_key_map.get(ip_address)
|
|
164
|
-
if private_key_path and Path(private_key_path).is_file():
|
|
165
|
-
return private_key_path
|
|
166
|
-
elif private_key_path:
|
|
167
|
-
self.update_remote_server_config_entry(ip_address, None)
|
|
168
|
-
return None
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
@staticmethod
|
|
172
|
-
def __update_config_values_dict(values_to_update: Dict, new_values: Dict):
|
|
173
|
-
if 'main' in new_values:
|
|
174
|
-
if 'main' in values_to_update:
|
|
175
|
-
values_to_update['main'].update(new_values['main'])
|
|
176
|
-
else:
|
|
177
|
-
values_to_update['main'] = new_values['main']
|
|
178
|
-
|
|
179
|
-
@staticmethod
|
|
180
|
-
def __save_config_file(data: Dict, file_path: Path):
|
|
181
|
-
with open(file_path, 'w') as configfile:
|
|
182
|
-
json.dump(data, configfile, indent=1)
|
|
183
|
-
|
|
184
|
-
|
|
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)
|
|
189
|
-
data.update(config.model_dump(exclude_none=True, by_alias=True, exclude={'runtime', 'RUNTIME', 'daemon', 'DAEMON'}))
|
|
190
|
-
self.__save_config_file(data=data, file_path=global_config_file)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def clear_config(self, ):
|
|
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))
|
|
197
|
-
|
|
198
|
-
os.unsetenv('THESTAGE_CONFIG_DIR')
|
|
199
|
-
os.unsetenv('THESTAGE_CONFIG_FILE')
|
|
200
|
-
os.unsetenv('THESTAGE_CLI_ENV')
|
|
201
|
-
os.unsetenv('THESTAGE_API_URL')
|
|
202
|
-
os.unsetenv('THESTAGE_LOG_FILE')
|
|
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
|
+
import typer
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, Dict, Any
|
|
7
|
+
|
|
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
|
+
from thestage.services.core_files.config_entity import ConfigEntity
|
|
11
|
+
from thestage.helpers.ssh_util import parse_private_key
|
|
12
|
+
from thestage.services.connect.dto.remote_server_config import RemoteServerConfig
|
|
13
|
+
from thestage.services.filesystem_service import FileSystemService
|
|
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
|
+
config_storage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConfigProvider:
|
|
20
|
+
_file_system_service = FileSystemService
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
file_system_service: FileSystemService,
|
|
26
|
+
):
|
|
27
|
+
self._file_system_service = file_system_service
|
|
28
|
+
|
|
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)
|
|
32
|
+
|
|
33
|
+
global_config_file = self.get_global_config_file()
|
|
34
|
+
self._file_system_service.create_if_not_exists_file(global_config_file)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# must be called right after the app is started
|
|
38
|
+
def build_config(self) -> ConfigEntity:
|
|
39
|
+
config_values = {}
|
|
40
|
+
|
|
41
|
+
config_from_env = {}
|
|
42
|
+
config_from_env['main'] = {}
|
|
43
|
+
if THESTAGE_AUTH_TOKEN:
|
|
44
|
+
config_from_env['main']['thestage_auth_token'] = THESTAGE_AUTH_TOKEN
|
|
45
|
+
if THESTAGE_API_URL:
|
|
46
|
+
config_from_env['main']['thestage_api_url'] = THESTAGE_API_URL
|
|
47
|
+
|
|
48
|
+
if config_from_env:
|
|
49
|
+
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_env)
|
|
50
|
+
|
|
51
|
+
config_from_file = self._file_system_service.read_config_file(self.get_global_config_file())
|
|
52
|
+
if config_from_file:
|
|
53
|
+
self.__update_config_values_dict(values_to_update=config_values, new_values=config_from_file)
|
|
54
|
+
|
|
55
|
+
config = ConfigEntity.model_validate(config_values)
|
|
56
|
+
config.runtime.config_global_path = str(self.get_global_config_path())
|
|
57
|
+
|
|
58
|
+
config_storage.APP_CONFIG = config
|
|
59
|
+
|
|
60
|
+
return config
|
|
61
|
+
|
|
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
|
+
|
|
69
|
+
def save_project_config(self, project_config: ProjectConfig):
|
|
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)
|
|
72
|
+
|
|
73
|
+
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
74
|
+
self._file_system_service.create_if_not_exists_file(project_data_filepath)
|
|
75
|
+
|
|
76
|
+
self.__save_config_file(data=project_config.model_dump(), file_path=project_data_filepath)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def save_project_deploy_ssh_key(self, deploy_ssh_key: str, project_slug: str, project_id: int) -> str:
|
|
80
|
+
deploy_key_dirpath = self.get_global_config_path().joinpath('project_deploy_keys')
|
|
81
|
+
self._file_system_service.create_if_not_exists_dir(deploy_key_dirpath)
|
|
82
|
+
|
|
83
|
+
deploy_key_filepath = deploy_key_dirpath.joinpath(f'project_deploy_key_{project_id}_{project_slug}')
|
|
84
|
+
self._file_system_service.create_if_not_exists_file(deploy_key_filepath)
|
|
85
|
+
|
|
86
|
+
text_file = open(deploy_key_filepath, "w")
|
|
87
|
+
text_file.write(deploy_ssh_key)
|
|
88
|
+
text_file.close()
|
|
89
|
+
os.chmod(deploy_key_filepath, 0o600)
|
|
90
|
+
|
|
91
|
+
return str(deploy_key_filepath)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def read_project_config(self) -> Optional[ProjectConfig]:
|
|
95
|
+
project_data_dirpath = self.__get_project_config_path()
|
|
96
|
+
if not project_data_dirpath.exists():
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
project_data_filepath = self.__get_project_config_path(with_file=True)
|
|
100
|
+
if not project_data_filepath.exists():
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
config_data = self._file_system_service.read_config_file(project_data_filepath) if project_data_filepath and project_data_filepath.exists() else {}
|
|
104
|
+
return ProjectConfig.model_validate(config_data)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def __get_project_config_path(self, with_file: bool = False) -> Path:
|
|
108
|
+
if with_file:
|
|
109
|
+
return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR).joinpath('project.json')
|
|
110
|
+
else:
|
|
111
|
+
return Path(self.get_config().runtime.working_directory).joinpath(THESTAGE_CONFIG_DIR)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def __get_remote_server_config_path(self) -> Path:
|
|
115
|
+
return self.get_global_config_path().joinpath('remote_server_config.json')
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def update_remote_server_config_entry(self, ip_address: str, ssh_key_path: Optional[Path]):
|
|
119
|
+
# double checking if key is parseable (fails if key is encrypted)
|
|
120
|
+
# see remote_server_service for similar check
|
|
121
|
+
if ssh_key_path:
|
|
122
|
+
pkey = parse_private_key(str(ssh_key_path))
|
|
123
|
+
|
|
124
|
+
if pkey is None:
|
|
125
|
+
typer.echo("Could not identify provided private key (expected RSA, ECDSA, ED25519)")
|
|
126
|
+
raise typer.Exit(1)
|
|
127
|
+
|
|
128
|
+
config = self.__read_remote_server_config()
|
|
129
|
+
remote_server_config_filepath = self.__get_remote_server_config_path()
|
|
130
|
+
if config:
|
|
131
|
+
if not config.ip_address_to_ssh_key_map:
|
|
132
|
+
config.ip_address_to_ssh_key_map = {}
|
|
133
|
+
|
|
134
|
+
existing_path = config.ip_address_to_ssh_key_map.get(ip_address)
|
|
135
|
+
if ssh_key_path and existing_path != str(ssh_key_path):
|
|
136
|
+
config.ip_address_to_ssh_key_map.update({ip_address: str(ssh_key_path)})
|
|
137
|
+
typer.echo(f'Updated ssh key for {ip_address}: {ssh_key_path}')
|
|
138
|
+
|
|
139
|
+
if not ssh_key_path and existing_path:
|
|
140
|
+
typer.echo(f"Private key at path {existing_path} was not found")
|
|
141
|
+
config.ip_address_to_ssh_key_map.pop(ip_address, None)
|
|
142
|
+
else:
|
|
143
|
+
self._file_system_service.create_if_not_exists_file(remote_server_config_filepath)
|
|
144
|
+
config = RemoteServerConfig(ip_address_to_ssh_key_map={ip_address: str(ssh_key_path)})
|
|
145
|
+
|
|
146
|
+
self.__save_config_file(data=config.model_dump(), file_path=remote_server_config_filepath)
|
|
147
|
+
|
|
148
|
+
return str(remote_server_config_filepath)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def __read_remote_server_config(self) -> Optional[RemoteServerConfig]:
|
|
152
|
+
config_filepath = self.__get_remote_server_config_path()
|
|
153
|
+
if not config_filepath.is_file():
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
config_data = self._file_system_service.read_config_file(config_filepath) if config_filepath and config_filepath.exists() else {}
|
|
157
|
+
return RemoteServerConfig.model_validate(config_data)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def get_valid_private_key_path_by_ip_address(self, ip_address: str) -> Optional[str]:
|
|
161
|
+
remote_server_config = self.__read_remote_server_config()
|
|
162
|
+
if remote_server_config and remote_server_config.ip_address_to_ssh_key_map:
|
|
163
|
+
private_key_path = remote_server_config.ip_address_to_ssh_key_map.get(ip_address)
|
|
164
|
+
if private_key_path and Path(private_key_path).is_file():
|
|
165
|
+
return private_key_path
|
|
166
|
+
elif private_key_path:
|
|
167
|
+
self.update_remote_server_config_entry(ip_address, None)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def __update_config_values_dict(values_to_update: Dict, new_values: Dict):
|
|
173
|
+
if 'main' in new_values:
|
|
174
|
+
if 'main' in values_to_update:
|
|
175
|
+
values_to_update['main'].update(new_values['main'])
|
|
176
|
+
else:
|
|
177
|
+
values_to_update['main'] = new_values['main']
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def __save_config_file(data: Dict, file_path: Path):
|
|
181
|
+
with open(file_path, 'w') as configfile:
|
|
182
|
+
json.dump(data, configfile, indent=1)
|
|
183
|
+
|
|
184
|
+
|
|
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)
|
|
189
|
+
data.update(config.model_dump(exclude_none=True, by_alias=True, exclude={'runtime', 'RUNTIME', 'daemon', 'DAEMON'}))
|
|
190
|
+
self.__save_config_file(data=data, file_path=global_config_file)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def clear_config(self, ):
|
|
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))
|
|
197
|
+
|
|
198
|
+
os.unsetenv('THESTAGE_CONFIG_DIR')
|
|
199
|
+
os.unsetenv('THESTAGE_CONFIG_FILE')
|
|
200
|
+
os.unsetenv('THESTAGE_CLI_ENV')
|
|
201
|
+
os.unsetenv('THESTAGE_API_URL')
|
|
202
|
+
os.unsetenv('THESTAGE_LOG_FILE')
|
|
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
|