mirror of
https://github.com/Websoft9/websoft9.git
synced 2024-11-22 07:30:24 +00:00
update
This commit is contained in:
parent
f7b3079fc5
commit
177cd076c2
13 changed files with 1194 additions and 155 deletions
|
@ -1,4 +1,4 @@
|
|||
# run app : uvicorn src.main:app --reload --port 9999
|
||||
# run app : uvicorn src.main:app --reload --port 9999 --log-level error
|
||||
# run nginx proxy manager doc:docker run -p 9091:8080 -e SWAGGER_JSON=/foo/api.swagger.json -v /data/websoft9/appmanage_new/docs/:/foo swaggerapi/swagger-ui
|
||||
# supervisorctl
|
||||
## supervisorctl reload
|
||||
|
|
|
@ -1,15 +1,72 @@
|
|||
from fastapi import APIRouter, Query
|
||||
from fastapi import APIRouter, Query,Path
|
||||
from src.schemas.appAvailable import AppAvailableResponse
|
||||
from src.schemas.appCatalog import AppCatalogResponse
|
||||
from src.schemas.appInstall import appInstall
|
||||
from src.schemas.appResponse import AppResponse
|
||||
from src.schemas.errorResponse import ErrorResponse
|
||||
from src.services.app_manager import AppManger
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
@router.get(
|
||||
"/apps/catalog/{locale}",
|
||||
summary="List Catalogs",
|
||||
description="List all app's catalogs",
|
||||
responses={
|
||||
200: {"model": list[AppCatalogResponse]},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
}
|
||||
)
|
||||
def get_catalog_apps(
|
||||
locale: str = Path(..., description="Language to get catalogs from", regex="^(zh|en)$"),
|
||||
):
|
||||
return AppManger().get_catalog_apps(locale)
|
||||
|
||||
@router.get("/apps/")
|
||||
|
||||
@router.get(
|
||||
"/apps/available/{locale}",
|
||||
summary="List Available Apps",
|
||||
description="List all available apps",
|
||||
responses={
|
||||
200: {"model": list[AppAvailableResponse]},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
}
|
||||
)
|
||||
def get_available_apps(
|
||||
locale: str = Path(..., description="Language to get available apps from", regex="^(zh|en)$"),
|
||||
):
|
||||
return AppManger().get_available_apps(locale)
|
||||
|
||||
@router.get(
|
||||
"/apps",
|
||||
summary="List Installed Apps",
|
||||
description="List all installed apps",
|
||||
responses={
|
||||
200: {"model": list[AppResponse]},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
}
|
||||
)
|
||||
def get_apps():
|
||||
return {"apps": []}
|
||||
return AppManger().get_apps()
|
||||
|
||||
@router.get(
|
||||
"/apps/{app_id}",
|
||||
summary="Inspect App",
|
||||
description="Retrieve details about an app",
|
||||
responses={
|
||||
200: {"model": AppResponse},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
}
|
||||
)
|
||||
def get_app_by_id(
|
||||
app_id: str = Path(..., description="App ID to get details from"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to get app details from. If not set, get details from the local endpoint")
|
||||
):
|
||||
return AppManger().get_app_by_id(app_id, endpointId)
|
||||
|
||||
@router.post(
|
||||
"/apps/install",
|
||||
|
@ -26,4 +83,116 @@ def apps_install(
|
|||
appInstall: appInstall,
|
||||
endpointId: int = Query(None, description="Endpoint ID to install app on,if not set, install on the local endpoint"),
|
||||
):
|
||||
AppManger().install_app(appInstall, endpointId)
|
||||
return AppManger().install_app(appInstall, endpointId)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/apps/{app_id}/start",
|
||||
summary="Start App",
|
||||
description="Start an app on an endpoint",
|
||||
status_code=204,
|
||||
responses={
|
||||
204: {"description": "App started successfully"},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
def app_start(
|
||||
app_id: str = Path(..., description="App ID to start"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to start app on. If not set, start on the local endpoint")
|
||||
):
|
||||
AppManger().start_app(app_id, endpointId)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/apps/{app_id}/stop",
|
||||
summary="Stop App",
|
||||
response_model_exclude_defaults=True,
|
||||
description="Stop an app on an endpoint",
|
||||
status_code=204,
|
||||
responses={
|
||||
204: {"description": "App stopped successfully"},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
def app_stop(
|
||||
app_id: str = Path(..., description="App ID to stop"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to stop app on. If not set, stop on the local endpoint"),
|
||||
):
|
||||
AppManger().stop_app(app_id, endpointId)
|
||||
|
||||
@router.post(
|
||||
"/apps/{app_id}/restart",
|
||||
summary="Restart App",
|
||||
response_model_exclude_defaults=True,
|
||||
description="Restart an app on an endpoint",
|
||||
status_code=204,
|
||||
responses={
|
||||
204: {"description": "App restarted successfully"},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
def app_restart(
|
||||
app_id: str = Path(..., description="App ID to restart"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to Restart app on. If not set, Restart on the local endpoint"),
|
||||
):
|
||||
AppManger().restart_app(app_id, endpointId)
|
||||
|
||||
@router.put(
|
||||
"/apps/{app_id}/redeploy",
|
||||
summary="Redeploy App",
|
||||
response_model_exclude_defaults=True,
|
||||
description="Redeploy an app on an endpoint",
|
||||
status_code=204,
|
||||
responses={
|
||||
204: {"description": "App redeploy successfully"},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
def app_redeploy(
|
||||
app_id: str = Path(..., description="App ID to redeploy"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to redeploy app on. If not set, redeploy on the local endpoint"),
|
||||
pullImage: bool = Query(..., description="Whether to pull the image when redeploying the app"),
|
||||
):
|
||||
AppManger().redeploy_app(app_id, pullImage,endpointId)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/apps/{app_id}/uninstall",
|
||||
summary="Uninstall App",
|
||||
description="Uninstall an app on an endpoint",
|
||||
status_code=204,
|
||||
responses={
|
||||
204: {"description": "App uninstalled successfully"},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
}
|
||||
)
|
||||
def apps_uninstall(
|
||||
app_id: str=Path(..., description="App ID to uninstall"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to uninstall app on,if not set, uninstall on the local endpoint"),
|
||||
purge_data: bool = Query(..., description="Whether to purge data when uninstalling the app")
|
||||
):
|
||||
AppManger().uninstall_app(app_id,purge_data, endpointId)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/apps/{app_id}/remove",
|
||||
summary="Remove App",
|
||||
response_model_exclude_defaults=True,
|
||||
description="Remove an app on an endpoint where the app is empty(status is 'inactive')",
|
||||
status_code=204,
|
||||
responses={
|
||||
204: {"description": "App removed successfully"},
|
||||
400: {"model": ErrorResponse},
|
||||
500: {"model": ErrorResponse},
|
||||
},
|
||||
)
|
||||
def app_remove(
|
||||
app_id: str = Path(..., description="App ID to remove"),
|
||||
endpointId: int = Query(None, description="Endpoint ID to remove app on. If not set, remove on the local endpoint"),
|
||||
):
|
||||
AppManger().remove_app(app_id, endpointId)
|
||||
|
|
|
@ -7,26 +7,29 @@ access_token =
|
|||
base_url = http://websoft9-proxy:81/api
|
||||
#base_url = http://47.92.222.186/w9proxy/api
|
||||
user_name = help@websoft9.com
|
||||
user_pwd = websoft9@2023
|
||||
user_pwd = ECTKPRAWhij789yr
|
||||
|
||||
#The config for gitea
|
||||
[gitea]
|
||||
base_url = http://websoft9-git:3000/api/v1
|
||||
# base_url = http://47.92.222.186/w9git/api/v1
|
||||
user_name = websoft9
|
||||
user_pwd = O4rXXHkSoKVY
|
||||
user_pwd = Rk9qOQ68Inf0
|
||||
|
||||
#The config for portainer
|
||||
[portainer]
|
||||
base_url = http://websoft9-deployment:9000/api
|
||||
#base_url = http://47.92.222.186/w9deployment/api
|
||||
user_name = admin
|
||||
user_pwd = &uswVF^wMyi]wpdc
|
||||
user_pwd = ]}fU;XmVH].VI{Hh
|
||||
|
||||
#The path of docker library
|
||||
[docker_library]
|
||||
path = /websoft9/library/apps
|
||||
|
||||
[app_media]
|
||||
path = /websoft9/media/json/
|
||||
|
||||
# public_ip_url_list is a list of public ip url, which is used to get the public ip of the server
|
||||
[public_ip_url_list]
|
||||
url_list = https://api.ipify.org/,
|
||||
|
|
|
@ -13,11 +13,12 @@ class NginxProxyManagerAPI:
|
|||
api (APIHelper): API helper
|
||||
|
||||
Methods:
|
||||
get_token(identity: str, secret: str) -> Response: Request a new access token
|
||||
get_proxy_hosts() -> Response: Get all proxy hosts
|
||||
create_proxy_host(domain_names: List[str], forward_scheme: str, forward_host: str, forward_port: int, advanced_config: str) -> Response: Create a new proxy host
|
||||
update_proxy_host(proxy_id: int, domain_names: List[str], forward_scheme: str, forward_host: str, forward_port: int, advanced_config: str) -> Response: Update an existing proxy host
|
||||
delete_proxy_host(proxy_id: int) -> Response: Delete a proxy host
|
||||
set_token(api_token): Set API token
|
||||
get_token(identity, secret): Request a new access token
|
||||
get_proxy_hosts(): Get all proxy hosts
|
||||
create_proxy_host(domain_names, forward_scheme, forward_host, forward_port, advanced_config): Create a new proxy host
|
||||
update_proxy_host(proxy_id, domain_names, forward_scheme, forward_host, forward_port, advanced_config): Update an existing proxy host
|
||||
delete_proxy_host(proxy_id): Delete a proxy host
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
|
128
appmanage_new/src/external/portainer_api.py
vendored
128
appmanage_new/src/external/portainer_api.py
vendored
|
@ -13,15 +13,20 @@ class PortainerAPI:
|
|||
api (APIHelper): API helper
|
||||
|
||||
Methods:
|
||||
get_jwt_token(username: str, password: str) -> Response): Get JWT token
|
||||
get_endpoints() -> Response: Get endpoints
|
||||
get_stacks(endpointID: int) -> Response: Get stacks
|
||||
get_stack_by_id(stackID: int) -> Response: Get stack by ID
|
||||
remove_stack(stackID: int,endPointID: int) -> Response: Remove a stack
|
||||
create_stack_standlone_repository(app_name: str, endpointId: int,repositoryURL:str) -> Response: Create a stack from a standalone repository
|
||||
start_stack(stackID: int, endpointId: int) -> Response: Start a stack
|
||||
stop_stack(stackID: int, endpointId: int) -> Response: Stop a stack
|
||||
redeploy_stack(stackID: int, endpointId: int) -> Response: Redeploy a stack
|
||||
set_jwt_token(jwt_token): Set JWT token
|
||||
get_jwt_token(username, password): Get JWT token
|
||||
get_endpoints(): Get endpoints
|
||||
get_endpoint_by_id(endpointId): Get endpoint by ID
|
||||
create_endpoint(name, EndpointCreationType): Create an endpoint
|
||||
get_stacks(endpointId): Get stacks
|
||||
get_stack_by_id(stackID): Get stack by ID
|
||||
remove_stack(stackID, endpointId): Remove a stack
|
||||
create_stack_standlone_repository(stack_name, endpointId, repositoryURL): Create a stack from a standalone repository
|
||||
start_stack(stackID, endpointId): Start a stack
|
||||
stop_stack(stackID, endpointId): Stop a stack
|
||||
redeploy_stack(stackID, endpointId): Redeploy a stack
|
||||
get_volumes(endpointId,dangling): Get volumes in endpoint
|
||||
remove_volume_by_name(endpointId,volume_name): Remove volumes by name
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
|
@ -179,9 +184,9 @@ class PortainerAPI:
|
|||
},
|
||||
)
|
||||
|
||||
def start_stack(self, stackID: int, endpointId: int):
|
||||
def up_stack(self, stackID: int, endpointId: int):
|
||||
"""
|
||||
Start a stack
|
||||
Up a stack
|
||||
|
||||
Args:
|
||||
stackID (int): Stack ID
|
||||
|
@ -194,9 +199,9 @@ class PortainerAPI:
|
|||
path=f"stacks/{stackID}/start", params={"endpointId": endpointId}
|
||||
)
|
||||
|
||||
def stop_stack(self, stackID: int, endpointId: int):
|
||||
def down_stack(self, stackID: int, endpointId: int):
|
||||
"""
|
||||
Stop a stack
|
||||
Down a stack
|
||||
|
||||
Args:
|
||||
stackID (int): Stack ID
|
||||
|
@ -224,12 +229,13 @@ class PortainerAPI:
|
|||
path=f"stacks/{stackID}/redeploy", params={"endpointId": endpointId}
|
||||
)
|
||||
|
||||
def get_volumes(self, endpointId: int,dangling: bool = False):
|
||||
def get_volumes(self, endpointId: int,dangling: bool):
|
||||
"""
|
||||
Get volumes in endpoint
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
dangling (bool): the volume is dangling or not
|
||||
"""
|
||||
return self.api.get(
|
||||
path=f"endpoints/{endpointId}/docker/volumes",
|
||||
|
@ -251,3 +257,97 @@ class PortainerAPI:
|
|||
return self.api.delete(
|
||||
path=f"endpoints/{endpointId}/docker/volumes/{volume_name}",
|
||||
)
|
||||
|
||||
def get_containers(self, endpointId: int):
|
||||
"""
|
||||
Get containers in endpoint
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
"""
|
||||
return self.api.get(
|
||||
path=f"endpoints/{endpointId}/docker/containers/json",
|
||||
params={
|
||||
"all": True,
|
||||
}
|
||||
)
|
||||
|
||||
def get_containers_by_stackName(self, endpointId: int,stack_name:str):
|
||||
"""
|
||||
Get containers in endpoint
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
"""
|
||||
return self.api.get(
|
||||
path=f"endpoints/{endpointId}/docker/containers/json",
|
||||
params={
|
||||
"all": True,
|
||||
"filters": json.dumps(
|
||||
{"label": [f"com.docker.compose.project={stack_name}"]}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
def get_container_by_id(self, endpointId: int, container_id: str):
|
||||
"""
|
||||
Get container by ID
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
container_id (str): container ID
|
||||
"""
|
||||
return self.api.get(
|
||||
path=f"endpoints/{endpointId}/docker/containers/{container_id}/json",
|
||||
)
|
||||
|
||||
def stop_container(self, endpointId: int, container_id: str):
|
||||
"""
|
||||
Stop container
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
container_id (str): container ID
|
||||
"""
|
||||
return self.api.post(
|
||||
path=f"endpoints/{endpointId}/docker/containers/{container_id}/stop",
|
||||
)
|
||||
|
||||
def start_container(self, endpointId: int, container_id: str):
|
||||
"""
|
||||
Start container
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
container_id (str): container ID
|
||||
"""
|
||||
return self.api.post(
|
||||
path=f"endpoints/{endpointId}/docker/containers/{container_id}/start",
|
||||
)
|
||||
|
||||
def restart_container(self, endpointId: int, container_id: str):
|
||||
"""
|
||||
Restart container
|
||||
|
||||
Args:
|
||||
endpointId (int): Endpoint ID
|
||||
container_id (str): container ID
|
||||
"""
|
||||
return self.api.post(
|
||||
path=f"endpoints/{endpointId}/docker/containers/{container_id}/restart",
|
||||
)
|
||||
|
||||
def redeploy_stack(self, stackID: int, endpointId: int,pullImage:bool,user_name:str,user_password:str ):
|
||||
return self.api.put(
|
||||
path=f"stacks/{stackID}/git/redeploy",
|
||||
params={"endpointId": endpointId},
|
||||
json={
|
||||
"env":[],
|
||||
"prune":False,
|
||||
"RepositoryReferenceName":"",
|
||||
"RepositoryAuthentication":True,
|
||||
"RepositoryUsername":user_name,
|
||||
"RepositoryPassword":user_password,
|
||||
"PullImage":pullImage
|
||||
}
|
||||
)
|
|
@ -1,6 +1,4 @@
|
|||
|
||||
import logging
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.responses import JSONResponse
|
||||
|
@ -11,7 +9,6 @@ from src.core.exception import CustomException
|
|||
from src.core.logger import logger
|
||||
from src.schemas.errorResponse import ErrorResponse
|
||||
|
||||
|
||||
uvicorn_logger = logging.getLogger("uvicorn")
|
||||
|
||||
for handler in uvicorn_logger.handlers:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import re
|
||||
from typing import Optional, List,Union
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
from src.core.exception import CustomException
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
import ipaddress
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
@ -6,6 +7,7 @@ from src.core.config import ConfigManager
|
|||
from src.core.envHelper import EnvHelper
|
||||
from src.core.exception import CustomException
|
||||
from src.schemas.appInstall import appInstall
|
||||
from src.schemas.appResponse import AppResponse
|
||||
from src.services.git_manager import GitManager
|
||||
from src.services.gitea_manager import GiteaManager
|
||||
from src.services.portainer_manager import PortainerManager
|
||||
|
@ -16,26 +18,223 @@ from src.utils.password_generator import PasswordGenerator
|
|||
|
||||
|
||||
class AppManger:
|
||||
def install_app(self,appInstall: appInstall, endpointId: int = None):
|
||||
library_path = ConfigManager().get_value("docker_library", "path")
|
||||
def get_catalog_apps(self,locale:str):
|
||||
try:
|
||||
# Get the app media path
|
||||
base_path = ConfigManager().get_value("app_media", "path")
|
||||
app_media_path = base_path + 'catalog_' + locale + '.json'
|
||||
# check the app media path is exists
|
||||
if not os.path.exists(app_media_path):
|
||||
logger.error(f"Get catalog apps error: {app_media_path} is not exists")
|
||||
raise CustomException()
|
||||
|
||||
# Get the app catalog list
|
||||
with open(app_media_path, "r") as f:
|
||||
data = json.load(f)
|
||||
return data
|
||||
except (CustomException,Exception) as e:
|
||||
logger.error(f"Get catalog apps error:{e}")
|
||||
raise CustomException()
|
||||
|
||||
def get_available_apps(self,locale:str):
|
||||
try:
|
||||
# Get the app media path
|
||||
base_path = ConfigManager().get_value("app_media", "path")
|
||||
app_media_path = base_path + 'product_' + locale + '.json'
|
||||
# check the app media path is exists
|
||||
if not os.path.exists(app_media_path):
|
||||
logger.error(f"Get available apps error: {app_media_path} is not exists")
|
||||
raise CustomException()
|
||||
|
||||
# Get the app available list
|
||||
with open(app_media_path, "r") as f:
|
||||
data = json.load(f)
|
||||
# appAvailableResponses = [AppAvailableResponse(**item) for item in data]
|
||||
# return appAvailableResponses
|
||||
return data
|
||||
except (CustomException,Exception) as e:
|
||||
logger.error(f"Get available apps error:{e}")
|
||||
raise CustomException()
|
||||
|
||||
def get_apps(self,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# if endpointId is None, get the local endpointId
|
||||
if endpointId is None:
|
||||
try:
|
||||
endpointId = portainerManager.get_local_endpoint_id()
|
||||
except (CustomException,Exception) as e:
|
||||
raise CustomException()
|
||||
else :
|
||||
# validate the endpointId is exists
|
||||
is_endpointId_exists = portainerManager.check_endpoint_exists(endpointId)
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
if not is_endpointId_exists:
|
||||
raise CustomException(
|
||||
status_code=404,
|
||||
message="Invalid Request",
|
||||
details="EndpointId Not Found"
|
||||
try:
|
||||
apps_info = []
|
||||
# Get the stacks
|
||||
stacks = portainerManager.get_stacks(endpointId)
|
||||
for stack in stacks:
|
||||
stack_name = stack.get("Name",None)
|
||||
if stack_name is not None:
|
||||
app_info = self.get_app_by_id(stack_name,endpointId)
|
||||
apps_info.append(app_info)
|
||||
|
||||
# Get the not stacks(not installed apps)
|
||||
all_containers = portainerManager.get_containers(endpointId)
|
||||
# Get the not stacks
|
||||
not_stacks = []
|
||||
for container in all_containers:
|
||||
container_labels = container.get("Labels",None)
|
||||
if container_labels is not None:
|
||||
container_project = container_labels.get("com.docker.compose.project",None)
|
||||
if container_project is not None:
|
||||
if not any(container_project in stack.get("Name",[]) for stack in stacks):
|
||||
not_stacks.append(container_project)
|
||||
# Remove the duplicate elements
|
||||
not_stacks = list(set(not_stacks))
|
||||
# Remove the websoft9
|
||||
if "websoft9" in not_stacks:
|
||||
not_stacks.remove("websoft9")
|
||||
# Get the not stacks info
|
||||
for not_stack in not_stacks:
|
||||
not_stack_response = AppResponse(
|
||||
app_id=not_stack,
|
||||
app_official=False,
|
||||
)
|
||||
apps_info.append(not_stack_response)
|
||||
|
||||
return apps_info
|
||||
except (CustomException,Exception) as e:
|
||||
logger.error(f"Get apps error:{e}")
|
||||
raise CustomException()
|
||||
|
||||
def get_app_by_id(self,app_id:str,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
if not is_stack_exists:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
|
||||
# Get stack_info
|
||||
stack_info = portainerManager.get_stack_by_name(app_id,endpointId)
|
||||
# Get the stack_id
|
||||
stack_id = stack_info.get("Id",None)
|
||||
# Get the stack_status
|
||||
stack_status = stack_info.get("Status",0)
|
||||
# Get the gitConfig
|
||||
gitConfig = stack_info.get("GitConfig",{}) or {}
|
||||
# Get the creationDate
|
||||
creationDate = stack_info.get("CreationDate","")
|
||||
if stack_id is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
|
||||
# Get the domain_names
|
||||
domain_names = ProxyManager().get_proxy_host_by_app(app_id)
|
||||
# Get the proxy_enabled
|
||||
if not domain_names:
|
||||
proxy_enabled = False
|
||||
else :
|
||||
proxy_enabled = True
|
||||
# Get the volumes
|
||||
app_volumes = portainerManager.get_volumes_by_stack_name(app_id,endpointId,False)
|
||||
|
||||
# if stack is empty(status=2-inactive),can not get it
|
||||
if stack_status == 1:
|
||||
# Get the containers
|
||||
app_containers = portainerManager.get_containers_by_stack_name(app_id,endpointId)
|
||||
|
||||
# Get the main container
|
||||
main_container_id = None
|
||||
for container in app_containers:
|
||||
if f"/{app_id}" in container.get("Names", []):
|
||||
main_container_id = container.get("Id", "")
|
||||
break
|
||||
if main_container_id:
|
||||
# Get the main container info
|
||||
main_container_info = portainerManager.get_container_by_id(endpointId, main_container_id)
|
||||
# Get the env
|
||||
app_env = main_container_info.get("Config", {}).get("Env", [])
|
||||
|
||||
# Get http port from env
|
||||
app_http_port = None
|
||||
app_name = None
|
||||
app_dist = None
|
||||
for item in app_env:
|
||||
key, value = item.split("=", 1)
|
||||
if key == "APP_HTTP_PORT":
|
||||
app_http_port = value
|
||||
elif key == "APP_NAME":
|
||||
app_name = value
|
||||
elif key == "APP_DIST":
|
||||
app_dist = value
|
||||
elif key == "APP_VERSION":
|
||||
app_version = value
|
||||
|
||||
# Get the app_port
|
||||
app_port = None
|
||||
if app_http_port:
|
||||
internal_port_str = str(app_http_port) + "/tcp"
|
||||
port_mappings = main_container_info["NetworkSettings"]["Ports"].get(internal_port_str, [])
|
||||
for mapping in port_mappings:
|
||||
try:
|
||||
ipaddress.IPv4Address(mapping["HostIp"])
|
||||
app_port = mapping["HostPort"]
|
||||
except ipaddress.AddressValueError:
|
||||
continue
|
||||
|
||||
appResponse = AppResponse(
|
||||
app_id = app_id,
|
||||
endpointId = endpointId,
|
||||
app_name = app_name,
|
||||
app_port = app_port,
|
||||
app_dist = app_dist,
|
||||
app_version = app_version,
|
||||
app_official = True,
|
||||
proxy_enabled = proxy_enabled,
|
||||
domain_names = domain_names,
|
||||
status = stack_status,
|
||||
creationDate = creationDate,
|
||||
gitConfig = gitConfig,
|
||||
containers = app_containers,
|
||||
volumes = app_volumes,
|
||||
env = app_env
|
||||
)
|
||||
return appResponse
|
||||
else:
|
||||
appResponse = AppResponse(
|
||||
app_id = app_id,
|
||||
endpointId = endpointId,
|
||||
app_name = "",
|
||||
app_port = 0,
|
||||
app_dist = "",
|
||||
app_version = "",
|
||||
app_official = True,
|
||||
proxy_enabled = proxy_enabled,
|
||||
domain_names = domain_names,
|
||||
status = stack_status,
|
||||
creationDate = creationDate,
|
||||
gitConfig = gitConfig,
|
||||
containers = [],
|
||||
volumes = app_volumes,
|
||||
env = []
|
||||
)
|
||||
return appResponse
|
||||
|
||||
def install_app(self,appInstall: appInstall, endpointId: int = None):
|
||||
# Get the library path
|
||||
library_path = ConfigManager().get_value("docker_library", "path")
|
||||
|
||||
# Get the portainer and gitea manager
|
||||
portainerManager = PortainerManager()
|
||||
giteaManager = GiteaManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_name and app_version
|
||||
app_name = appInstall.app_name
|
||||
|
@ -44,29 +243,27 @@ class AppManger:
|
|||
|
||||
# validate the app_id
|
||||
app_id = appInstall.app_id
|
||||
self._check_appId(app_id,endpointId)
|
||||
|
||||
proxy_enabled = appInstall.proxy_enabled
|
||||
domain_names = appInstall.domain_names
|
||||
self._check_appId(app_id,endpointId,giteaManager,portainerManager)
|
||||
|
||||
# validate the domain_names
|
||||
proxy_enabled = appInstall.proxy_enabled
|
||||
domain_names = appInstall.domain_names
|
||||
if proxy_enabled:
|
||||
self._check_domain_names(domain_names)
|
||||
|
||||
# Begin install app
|
||||
# Step 1 : create repo in gitea
|
||||
giteaManager = GiteaManager()
|
||||
# Install app - Step 1 : create repo in gitea
|
||||
repo_url = giteaManager.create_repo(app_id)
|
||||
|
||||
# Step 2 : initialize local git repo and push to gitea
|
||||
# Install app - Step 2 : initialize local git repo and push to gitea
|
||||
try:
|
||||
# The source directory.
|
||||
local_path = f"{library_path}/{app_name}"
|
||||
|
||||
# The destination directory.
|
||||
# Create a temporary directory.
|
||||
app_tmp_dir = "/tmp"
|
||||
app_tmp_dir_path = f"{app_tmp_dir}/{app_name}"
|
||||
|
||||
# Check if the destination directory exists, create it if necessary.
|
||||
# If the temporary directory does not exist, create it.
|
||||
if not os.path.exists(app_tmp_dir):
|
||||
os.makedirs(app_tmp_dir)
|
||||
|
||||
|
@ -80,40 +277,48 @@ class AppManger:
|
|||
# Modify the env file
|
||||
env_file_path = f"{app_tmp_dir_path}/.env"
|
||||
new_env_values = {
|
||||
"APP_NAME": app_id,
|
||||
"APP_ID": app_id,
|
||||
"APP_NAME": app_name,
|
||||
"APP_DIST": "community",
|
||||
"APP_VERSION": app_version,
|
||||
"POWER_PASSWORD": PasswordGenerator.generate_strong_password()
|
||||
"POWER_PASSWORD": PasswordGenerator.generate_strong_password(),
|
||||
"APP_URL": domain_names[0]
|
||||
}
|
||||
new_env_values["APP_URL"] = domain_names[0]
|
||||
EnvHelper(env_file_path).modify_env_values(new_env_values)
|
||||
|
||||
# Get the forward port form env file
|
||||
forward_port = EnvHelper(env_file_path).get_env_value_by_key("APP_HTTP_PORT")
|
||||
|
||||
# Commit and push to remote repo
|
||||
self._init_local_repo_and_push_to_remote(app_tmp_dir_path,repo_url)
|
||||
|
||||
# Remove the tmp dir
|
||||
shutil.rmtree(app_tmp_dir_path)
|
||||
except (CustomException,Exception) as e:
|
||||
# Rollback: remove repo in gitea
|
||||
giteaManager.remove_repo(app_id)
|
||||
raise CustomException()
|
||||
|
||||
# Step 3 : create stack in portainer
|
||||
# Install app - Step 3 : create stack in portainer
|
||||
try:
|
||||
# Get gitea user_name and user_pwd
|
||||
user_name = ConfigManager().get_value("gitea","user_name")
|
||||
user_pwd = ConfigManager().get_value("gitea","user_pwd")
|
||||
portainerManager.create_stack_from_repository(app_id,endpointId,repo_url,user_name,user_pwd)
|
||||
stack_id = portainerManager.get_stack_by_name(app_id,endpointId)["Id"]
|
||||
# Create stack in portainer
|
||||
stack_info = portainerManager.create_stack_from_repository(app_id,endpointId,repo_url,user_name,user_pwd)
|
||||
# Get the stack_id
|
||||
stack_id = stack_info.get("Id")
|
||||
except (CustomException,Exception) as e:
|
||||
# Rollback: remove repo in gitea
|
||||
giteaManager.remove_repo(app_id)
|
||||
raise CustomException()
|
||||
|
||||
# Step 4 : create proxy in proxy
|
||||
# Install app - Step 4 : create proxy in nginx proxy manager
|
||||
try:
|
||||
if domain_names:
|
||||
if proxy_enabled and domain_names:
|
||||
# Get the forward port form env file
|
||||
forward_port = EnvHelper(env_file_path).get_env_value_by_key("APP_HTTP_PORT")
|
||||
# Get the nginx proxy config path
|
||||
nginx_proxy_path = f"{app_tmp_dir_path}/src/nginx-proxy.conf"
|
||||
if os.path.exists(nginx_proxy_path):
|
||||
# Get the advanced config
|
||||
advanced_config = FileHelper.read_file(nginx_proxy_path)
|
||||
ProxyManager().create_proxy_for_app(domain_names,app_id,forward_port,advanced_config)
|
||||
else:
|
||||
ProxyManager().create_proxy_for_app(domain_names,app_id,forward_port)
|
||||
except (CustomException,Exception) as e:
|
||||
# Rollback-1: remove repo in gitea
|
||||
|
@ -122,6 +327,237 @@ class AppManger:
|
|||
portainerManager.remove_stack_and_volumes(stack_id,endpointId)
|
||||
raise CustomException()
|
||||
|
||||
# Remove the tmp dir
|
||||
shutil.rmtree(app_tmp_dir_path)
|
||||
|
||||
return self.get_app_by_id(app_id,endpointId)
|
||||
|
||||
def redeploy_app(self,app_id:str,pull_image:bool,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
if not is_stack_exists:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
# Get stack_id
|
||||
stack_id = portainerManager.get_stack_by_name(app_id,endpointId).get("Id",None)
|
||||
if stack_id is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
else:
|
||||
user_name = ConfigManager().get_value("gitea","user_name")
|
||||
user_pwd = ConfigManager().get_value("gitea","user_pwd")
|
||||
# redeploy stack
|
||||
portainerManager.redeploy_stack(stack_id,endpointId,pull_image,user_name,user_pwd)
|
||||
|
||||
def uninstall_app(self,app_id:str,purge_data:bool,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
if not is_stack_exists:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
# Get stack_id
|
||||
stack_id = portainerManager.get_stack_by_name(app_id,endpointId).get("Id",None)
|
||||
if stack_id is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
|
||||
# get stack status,if stack is empty(status=2-inactive),can not uninstall it
|
||||
stack_status = portainerManager.get_stack_by_name(app_id,endpointId).get("Status")
|
||||
if stack_status == 2:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} is empty, can not uninstall it,you can remove it"
|
||||
)
|
||||
|
||||
if purge_data:
|
||||
# Uninstall app - Step 1 : remove proxy in nginx proxy manager
|
||||
# Check the proxy is exists
|
||||
proxyManager = ProxyManager()
|
||||
proxys_host = proxyManager.get_proxy_host_by_app(app_id)
|
||||
# If the proxy is exists, remove it
|
||||
if proxys_host:
|
||||
proxyManager.remove_proxy_host_for_app(app_id)
|
||||
|
||||
# Uninstall app - Step 2 : remove repo in gitea
|
||||
# Check the repo is exists
|
||||
giteaManager = GiteaManager()
|
||||
is_repo_exists = giteaManager.check_repo_exists(app_id)
|
||||
if is_repo_exists:
|
||||
giteaManager.remove_repo(app_id)
|
||||
|
||||
# Uninstall app - Step 3 : remove stack in portainer
|
||||
# Get stack_id
|
||||
stack_id = portainerManager.get_stack_by_name(app_id,endpointId).get("Id",None)
|
||||
if stack_id is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
# remove stack and volumes
|
||||
portainerManager.remove_stack_and_volumes(stack_id,endpointId)
|
||||
else:
|
||||
# down stack
|
||||
portainerManager.down_stack(stack_id,endpointId)
|
||||
|
||||
def remove_app(self,app_id:str,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
if not is_stack_exists:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
# Get stack_id
|
||||
stack_id = portainerManager.get_stack_by_name(app_id,endpointId).get("Id",None)
|
||||
if stack_id is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
# get stack status,if stack is not empty(status=1-active),can not remove it
|
||||
stack_status = portainerManager.get_stack_by_name(app_id,endpointId).get("Status")
|
||||
if stack_status == 1:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} is not empty, please uninstall it first"
|
||||
)
|
||||
# Check the proxy is exists
|
||||
proxyManager = ProxyManager()
|
||||
proxys_host = proxyManager.get_proxy_host_by_app(app_id)
|
||||
# If the proxy is exists, remove it
|
||||
if proxys_host:
|
||||
proxyManager.remove_proxy_host_for_app(app_id)
|
||||
|
||||
# Check the repo is exists
|
||||
giteaManager = GiteaManager()
|
||||
is_repo_exists = giteaManager.check_repo_exists(app_id)
|
||||
if is_repo_exists:
|
||||
giteaManager.remove_repo(app_id)
|
||||
# remove stack and volumes
|
||||
portainerManager.remove_stack_and_volumes(stack_id,endpointId)
|
||||
|
||||
def start_app(self,app_id:str,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
# is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
# if not is_stack_exists:
|
||||
# raise CustomException(
|
||||
# status_code=400,
|
||||
# message="Invalid Request",
|
||||
# details=f"{app_id} Not Found"
|
||||
# )
|
||||
stack_info = portainerManager.get_stack_by_name(app_id,endpointId)
|
||||
if stack_info is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
stack_status = stack_info.get("Status",None)
|
||||
if stack_status == 2:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} is empty, can not start it,you can redeploy it"
|
||||
)
|
||||
|
||||
portainerManager.start_stack(app_id,endpointId)
|
||||
|
||||
def stop_app(self,app_id:str,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
# is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
# if not is_stack_exists:
|
||||
# raise CustomException(
|
||||
# status_code=400,
|
||||
# message="Invalid Request",
|
||||
# details=f"{app_id} Not Found"
|
||||
# )
|
||||
stack_info = portainerManager.get_stack_by_name(app_id,endpointId)
|
||||
if stack_info is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
stack_status = stack_info.get("Status",None)
|
||||
if stack_status == 2:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} is empty, can not stop it,you can redeploy it"
|
||||
)
|
||||
portainerManager.stop_stack(app_id,endpointId)
|
||||
|
||||
def restart_app(self,app_id:str,endpointId:int = None):
|
||||
portainerManager = PortainerManager()
|
||||
|
||||
# Check the endpointId is exists.
|
||||
endpointId = self._check_endpointId(endpointId, portainerManager)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
# is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
# if not is_stack_exists:
|
||||
# raise CustomException(
|
||||
# status_code=400,
|
||||
# message="Invalid Request",
|
||||
# details=f"{app_id} Not Found"
|
||||
# )
|
||||
stack_info = portainerManager.get_stack_by_name(app_id,endpointId)
|
||||
if stack_info is None:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} Not Found"
|
||||
)
|
||||
stack_status = stack_info.get("Status",None)
|
||||
if stack_status == 2:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=f"{app_id} is empty, can not restart it,you can redeploy it"
|
||||
)
|
||||
portainerManager.restart_stack(app_id,endpointId)
|
||||
|
||||
def _check_appName_and_appVersion(self,app_name:str, app_version:str,library_path:str):
|
||||
"""
|
||||
|
@ -142,6 +578,7 @@ class AppManger:
|
|||
details=f"app_name:{app_name} not supported",
|
||||
)
|
||||
else:
|
||||
|
||||
with open(f"{library_path}/{app_name}/variables.json", "r") as f:
|
||||
variables = json.load(f)
|
||||
community_editions = [d for d in variables["edition"] if d["dist"] == "community"]
|
||||
|
@ -155,7 +592,7 @@ class AppManger:
|
|||
details=f"app_version:{app_version} not supported",
|
||||
)
|
||||
|
||||
def _check_appId(self,app_id:str,endpointId:int):
|
||||
def _check_appId(self,app_id:str,endpointId:int,giteaManager:GiteaManager,portainerManager:PortainerManager):
|
||||
"""
|
||||
Check the app_id is exists in gitea and portainer
|
||||
|
||||
|
@ -167,7 +604,6 @@ class AppManger:
|
|||
CustomException: If the app_id is exists in gitea or portainer
|
||||
"""
|
||||
# validate the app_id is exists in gitea
|
||||
giteaManager = GiteaManager()
|
||||
is_repo_exists = giteaManager.check_repo_exists(app_id)
|
||||
if is_repo_exists:
|
||||
logger.error(f"When install app,the app_id:{{app_id}} is exists in gitea")
|
||||
|
@ -178,7 +614,6 @@ class AppManger:
|
|||
)
|
||||
|
||||
# validate the app_id is exists in portainer
|
||||
portainerManager = PortainerManager()
|
||||
is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
|
||||
if is_stack_exists:
|
||||
logger.error(f"When install app, the app_id:{app_id} is exists in portainer")
|
||||
|
@ -217,3 +652,28 @@ class AppManger:
|
|||
except (CustomException,Exception) as e:
|
||||
logger.error(f"Init local repo and push to remote repo error:{e}")
|
||||
raise CustomException()
|
||||
|
||||
def _check_endpointId(self, endpointId, portainerManager):
|
||||
"""
|
||||
Check the endpointId is exists
|
||||
|
||||
Args:
|
||||
endpointId ([type]): [description]
|
||||
portainerManager ([type]): [description]
|
||||
|
||||
Raises:
|
||||
CustomException: If the endpointId is not exists
|
||||
"""
|
||||
if endpointId is None:
|
||||
# Get the local endpointId
|
||||
endpointId = portainerManager.get_local_endpoint_id()
|
||||
else :
|
||||
# validate the endpointId is exists
|
||||
is_endpointId_exists = portainerManager.check_endpoint_exists(endpointId)
|
||||
if not is_endpointId_exists:
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details="EndpointId Not Found"
|
||||
)
|
||||
return endpointId
|
|
@ -1,5 +1,4 @@
|
|||
import os
|
||||
import shutil
|
||||
from git import Repo, GitCommandError
|
||||
from src.core.exception import CustomException
|
||||
from src.core.logger import logger
|
||||
|
@ -15,7 +14,6 @@ class GitManager:
|
|||
Methods:
|
||||
init_local_repo_from_dir() -> None: Initialize a local git repository from a directory.
|
||||
push_local_repo_to_remote_repo(remote_url:str,user_name:str,user_pwd:str) -> None: Push a local git repository to a remote origin.
|
||||
remove_git_directory() -> None: Remove the .git directory.
|
||||
"""
|
||||
|
||||
def __init__(self,local_path:str):
|
||||
|
@ -91,4 +89,3 @@ class GitManager:
|
|||
logger.error(f"Failed to push from 'main' branch in git repository at {self.local_path} to remote '{remote_url}': {str(e)}")
|
||||
raise CustomException()
|
||||
|
||||
|
|
@ -8,7 +8,13 @@ from src.external.gitea_api import GiteaAPI
|
|||
|
||||
|
||||
class GiteaManager:
|
||||
"""
|
||||
Gitea Manager
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Init GiteaManager
|
||||
"""
|
||||
try:
|
||||
self.gitea = GiteaAPI()
|
||||
self._set_basic_auth_credential()
|
||||
|
@ -43,7 +49,7 @@ class GiteaManager:
|
|||
elif response.status_code == 404:
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Error validate repo is exist from gitea: {response.text}")
|
||||
logger.error(f"Check repo:{repo_name} exists error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def create_repo(self, repo_name: str):
|
||||
|
@ -61,7 +67,7 @@ class GiteaManager:
|
|||
repo_json = response.json()
|
||||
return repo_json["clone_url"]
|
||||
else:
|
||||
logger.error(f"Error create repo from gitea: {response.text}")
|
||||
logger.error(f"Create repo:{repo_name} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_file_content_from_repo(self, repo_name: str, file_path: str):
|
||||
|
@ -78,17 +84,17 @@ class GiteaManager:
|
|||
"content": response_json["content"],
|
||||
}
|
||||
else:
|
||||
logger.error(f"Error get file content from repo from gitea: {response.text}")
|
||||
logger.error(f"Get file:{file_path} content from repo:{repo_name} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def update_file_in_repo(self, repo_name: str, file_path: str, content: str,sha: str):
|
||||
response = self.gitea.update_file_content_in_repo(repo_name, file_path, content, sha)
|
||||
if response.status_code != 201:
|
||||
logger.error(f"Error update file in repo from gitea: {response.text}")
|
||||
logger.error(f"Update file:{file_path} content in repo:{repo_name} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def remove_repo(self, repo_name: str):
|
||||
response = self.gitea.remove_repo(repo_name)
|
||||
if response.status_code != 204:
|
||||
logger.error(f"Error remove repo from gitea: {response.text}")
|
||||
logger.error(f"Remove repo:{repo_name} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
|
@ -9,6 +9,26 @@ from src.core.logger import logger
|
|||
|
||||
|
||||
class PortainerManager:
|
||||
"""
|
||||
Portainer Manager
|
||||
|
||||
Attributes:
|
||||
portainer (PortainerAPI): Portainer API
|
||||
|
||||
Methods:
|
||||
_set_portainer_token(): Set Portainer token
|
||||
get_local_endpoint_id(): Get local endpoint id
|
||||
check_endpoint_exists(endpoint_id): Check endpoint exists
|
||||
check_stack_exists(stack_name, endpoint_id): Check stack exists
|
||||
create_stack_from_repository(stack_name, endpoint_id,repositoryURL,user_name,user_password): Create stack from repository
|
||||
get_stacks(endpoint_id): Get stacks
|
||||
get_stack_by_id(stack_id): Get stack by id
|
||||
get_stack_by_name(stack_name, endpoint_id): Get stack by name
|
||||
remove_stack(stack_id, endpoint_id): Remove stack by id
|
||||
remove_stack_and_volumes(stack_id, endpoint_id): Remove stack and volumes by id
|
||||
get_volumes_by_stack_name(stack_name, endpoint_id): Get volumes by stack name
|
||||
remove_volume(volume_names, endpoint_id): Remove volume by name
|
||||
"""
|
||||
def __init__(self):
|
||||
try:
|
||||
self.portainer = PortainerAPI()
|
||||
|
@ -18,6 +38,9 @@ class PortainerManager:
|
|||
raise CustomException()
|
||||
|
||||
def _set_portainer_token(self):
|
||||
"""
|
||||
Set Portainer token
|
||||
"""
|
||||
service_name = "portainer"
|
||||
token_name = "user_token"
|
||||
|
||||
|
@ -83,10 +106,10 @@ class PortainerManager:
|
|||
if local_endpoint is not None:
|
||||
return local_endpoint["Id"]
|
||||
else:
|
||||
logger.error(f"Error get local endpoint id from portainer: {response.text}")
|
||||
logger.error(f"Can't find local endpoint")
|
||||
raise CustomException()
|
||||
else:
|
||||
logger.error(f"Error get local endpoint id from portainer: {response.text}")
|
||||
logger.error(f"Get local endpoint id error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def check_endpoint_exists(self, endpoint_id: int):
|
||||
|
@ -96,7 +119,7 @@ class PortainerManager:
|
|||
elif response.status_code == 404:
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Error validate endpoint is exist from portainer: {response.text}")
|
||||
logger.error(f"Check endpoint:{endpoint_id} exists error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def check_stack_exists(self, stack_name: str, endpoint_id: int):
|
||||
|
@ -108,29 +131,79 @@ class PortainerManager:
|
|||
return True
|
||||
return False
|
||||
else:
|
||||
logger.error(f"Error validate stack is exist from portainer: {response.text}")
|
||||
logger.error(f"Check stack:{stack_name} exists error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def create_stack_from_repository(self, stack_name: str, endpoint_id: int,repositoryURL : str,user_name:str,user_password:str):
|
||||
response = self.portainer.create_stack_standlone_repository(stack_name, endpoint_id,repositoryURL,user_name,user_password)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Error create stack from portainer: {response.text}")
|
||||
raise CustomException()
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
message = response.text
|
||||
if message:
|
||||
try:
|
||||
response_details = json.loads(message)
|
||||
message = response_details.get('details', 'unknown error')
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.error(f"Create stack:{stack_name} from repository:{repositoryURL} error: {response.status_code}:{response.text}")
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=message
|
||||
)
|
||||
|
||||
def redeploy_stack(self, stack_id: int, endpoint_id: int,pull_image:bool,user_name:str,user_password:str):
|
||||
response = self.portainer.redeploy_stack(stack_id, endpoint_id,pull_image,user_name,user_password)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
message = response.text
|
||||
if message:
|
||||
try:
|
||||
response_details = json.loads(message)
|
||||
message = response_details.get('details', 'unknown error')
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.error(f"Redeploy stack:{stack_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=message
|
||||
)
|
||||
|
||||
def get_stacks(self, endpoint_id: int):
|
||||
"""
|
||||
Get stacks
|
||||
|
||||
Args:
|
||||
endpoint_id (int): endpoint id
|
||||
|
||||
Returns:
|
||||
list: stack list
|
||||
"""
|
||||
response = self.portainer.get_stacks(endpoint_id)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Error get stacks from portainer: {response.text}")
|
||||
logger.error(f"Get stacks from endpoint:{endpoint_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_stack_by_id(self, stack_id: int):
|
||||
"""
|
||||
Get stack by id
|
||||
|
||||
Args:
|
||||
stack_id (int): stack id
|
||||
|
||||
Returns:
|
||||
dict: stack info
|
||||
"""
|
||||
response = self.portainer.get_stack_by_id(stack_id)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Error get stack by id from portainer: {response.text}")
|
||||
logger.error(f"Get stack by id:{stack_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_stack_by_name(self, stack_name: str, endpoint_id: int):
|
||||
|
@ -152,23 +225,45 @@ class PortainerManager:
|
|||
return stack
|
||||
return None
|
||||
else:
|
||||
logger.error(f"Error get stack by name from portainer: {response.text}")
|
||||
logger.error(f"Get stack by name:{stack_name} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def remove_stack(self, stack_id: int, endpoint_id: int):
|
||||
"""
|
||||
Remove stack by id
|
||||
|
||||
Args:
|
||||
stack_id (int): stack id
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
response = self.portainer.remove_stack(stack_id, endpoint_id)
|
||||
if response.status_code != 204:
|
||||
logger.error(f"Error remove stack from portainer: {response.text}")
|
||||
logger.error(f"Remove stack:{stack_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def remove_vloumes(self, stack_name: str, endpoint_id: int):
|
||||
volumes = self.get_volumes_by_stack_name(stack_name, endpoint_id,True)
|
||||
if volumes is not None:
|
||||
volume_names = []
|
||||
for volume in volumes.get("mountpoint", []):
|
||||
volume_names.append(volume["name"])
|
||||
self.remove_volume(volume_names, endpoint_id)
|
||||
|
||||
def remove_stack_and_volumes(self, stack_id: int, endpoint_id: int):
|
||||
"""
|
||||
Remove stack and volumes by id
|
||||
|
||||
Args:
|
||||
stack_id (int): stack id
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
# get stack name
|
||||
stack_name = self.get_stack_by_id(stack_id).get("Name")
|
||||
|
||||
# remove stack
|
||||
response = self.portainer.remove_stack(stack_id, endpoint_id)
|
||||
if response.status_code != 204:
|
||||
logger.error(f"Error remove stack from portainer: {response.text}")
|
||||
logger.error(f"Remove stack:{stack_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
# remove volumes
|
||||
|
@ -176,13 +271,14 @@ class PortainerManager:
|
|||
if stack_name is not None:
|
||||
volumes = self.get_volumes_by_stack_name(stack_name, endpoint_id,True)
|
||||
volume_names = []
|
||||
for volume in volumes.get("mountpoint", []):
|
||||
volume_names.append(volume["name"])
|
||||
for volume in volumes:
|
||||
volume_names.append(volume["Name"])
|
||||
|
||||
if len(volume_names) > 0:
|
||||
self.remove_volume(volume_names, endpoint_id)
|
||||
except (CustomException,Exception) as e:
|
||||
raise CustomException()
|
||||
|
||||
|
||||
def get_volumes_by_stack_name(self, stack_name: str, endpoint_id: int,dangling:bool):
|
||||
"""
|
||||
Get volumes by stack name
|
||||
|
@ -190,32 +286,22 @@ class PortainerManager:
|
|||
Args:
|
||||
stack_name (str): stack name
|
||||
endpoint_id (int): endpoint id
|
||||
dangling (bool): the volume is dangling or not
|
||||
|
||||
Returns:
|
||||
dict: volumes info
|
||||
"""
|
||||
response = self.portainer.get_volumes(endpoint_id,dangling)
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
volumes = response.json().get("Volumes", [])
|
||||
mountpoints = []
|
||||
|
||||
for volume in volumes:
|
||||
labels = volume.get("Labels", {})
|
||||
|
||||
if labels.get("com.docker.compose.project") == stack_name:
|
||||
mountpoint_info = {
|
||||
"name": volume["Name"],
|
||||
"path": volume["Mountpoint"]
|
||||
}
|
||||
|
||||
mountpoints.append(mountpoint_info)
|
||||
|
||||
return {
|
||||
"stack_name": stack_name,
|
||||
"mountpoint": mountpoints
|
||||
}
|
||||
volumes_info = [volume for volume in volumes if volume.get("Labels", {}).get("com.docker.compose.project") == stack_name]
|
||||
except Exception as e:
|
||||
logger.error(f"Get volumes by stack name:{stack_name} error: {e}")
|
||||
raise CustomException()
|
||||
return volumes_info
|
||||
else:
|
||||
logger.error(f"Error remove stack from portainer: {response.text}")
|
||||
logger.error(f"Get volumes by stack name:{stack_name} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def remove_volume(self, volume_names: list, endpoint_id: int):
|
||||
|
@ -229,5 +315,188 @@ class PortainerManager:
|
|||
for volume_name in volume_names:
|
||||
response = self.portainer.remove_volume_by_name(endpoint_id,volume_name)
|
||||
if response.status_code != 204:
|
||||
logger.error(f"Error remove volume from portainer: {response.text}")
|
||||
logger.error(f"Remove volume:{volume_name} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def up_stack(self, stack_id: int, endpoint_id: int):
|
||||
"""
|
||||
Up stack by id
|
||||
|
||||
Args:
|
||||
stack_id (int): stack id
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
response = self.portainer.up_stack(stack_id, endpoint_id)
|
||||
if response.status_code == 409:
|
||||
raise CustomException(400,"Invalid Request","The app is already running")
|
||||
elif response.status_code != 200:
|
||||
logger.error(f"Up stack:{stack_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def down_stack(self, stack_id: int, endpoint_id: int):
|
||||
"""
|
||||
Down stack by id
|
||||
|
||||
Args:
|
||||
stack_id (int): stack id
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
response = self.portainer.down_stack(stack_id, endpoint_id)
|
||||
if response.status_code == 400:
|
||||
raise CustomException(400,"Invalid Request","The app is already uninstalled")
|
||||
elif response.status_code != 200:
|
||||
logger.error(f"Down stack:{stack_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def stop_stack(self, stack_name: int, endpoint_id: int):
|
||||
"""
|
||||
Stop stack by name
|
||||
|
||||
Args:
|
||||
stack_name (int): stack name
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
containers_response = self.portainer.get_containers_by_stackName(endpoint_id,stack_name)
|
||||
if containers_response.status_code == 200:
|
||||
containers = containers_response.json()
|
||||
for container in containers:
|
||||
container_id = container.get("Id")
|
||||
stop_response = self.portainer.stop_container(endpoint_id,container_id)
|
||||
if stop_response.status_code in {304, 404}:
|
||||
continue
|
||||
elif stop_response.status_code != 204:
|
||||
message = stop_response.text
|
||||
if message:
|
||||
try:
|
||||
response_details = json.loads(stop_response.text)
|
||||
message = response_details.get('details', 'unknown error')
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.error(f"Stop container:{container_id} error: {stop_response.status_code}:{message}")
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=message
|
||||
)
|
||||
else:
|
||||
logger.error(f"Get containers by stack name:{stack_name} error: {containers_response.status_code}:{containers_response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def start_stack(self, stack_name: int, endpoint_id: int):
|
||||
"""
|
||||
Start stack by name
|
||||
|
||||
Args:
|
||||
stack_name (int): stack name
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
containers_response = self.portainer.get_containers_by_stackName(endpoint_id,stack_name)
|
||||
if containers_response.status_code == 200:
|
||||
containers = containers_response.json()
|
||||
for container in containers:
|
||||
container_id = container.get("Id")
|
||||
start_response=self.portainer.start_container(endpoint_id,container_id)
|
||||
if start_response.status_code in {304, 404}:
|
||||
continue
|
||||
elif start_response.status_code != 204:
|
||||
message = start_response.text
|
||||
if message:
|
||||
try:
|
||||
response_details = json.loads(start_response.text)
|
||||
message = response_details.get('details', 'unknown error')
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.error(f"Start container:{container_id} error: {start_response.status_code}:{message}")
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=message
|
||||
)
|
||||
else:
|
||||
logger.error(f"Get containers by stack name:{stack_name} error: {containers_response.status_code}:{containers_response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def restart_stack(self, stack_name: int, endpoint_id: int):
|
||||
"""
|
||||
Restart stack by name
|
||||
|
||||
Args:
|
||||
stack_name (int): stack name
|
||||
endpoint_id (int): endpoint id
|
||||
"""
|
||||
containers_response = self.portainer.get_containers_by_stackName(endpoint_id,stack_name)
|
||||
if containers_response.status_code == 200:
|
||||
containers = containers_response.json()
|
||||
for container in containers:
|
||||
container_id = container.get("Id")
|
||||
restart_response=self.portainer.restart_container(endpoint_id,container_id)
|
||||
if restart_response.status_code != 204:
|
||||
message = restart_response.text
|
||||
if message:
|
||||
try:
|
||||
response_details = json.loads(restart_response.text)
|
||||
message = response_details.get('details', 'unknown error')
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
logger.error(f"Restart container:{container_id} error: {restart_response.status_code}:{message}")
|
||||
raise CustomException(
|
||||
status_code=400,
|
||||
message="Invalid Request",
|
||||
details=message
|
||||
)
|
||||
else:
|
||||
logger.error(f"Get containers by stack name:{stack_name} error: {containers_response.status_code}:{containers_response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_containers(self, endpoint_id: int):
|
||||
"""
|
||||
Get containers
|
||||
|
||||
Args:
|
||||
endpoint_id (int): endpoint id
|
||||
|
||||
Returns:
|
||||
list: containers info
|
||||
"""
|
||||
response = self.portainer.get_containers(endpoint_id)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Get containers from endpoint:{endpoint_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_containers_by_stack_name(self, stack_name: str, endpoint_id: int):
|
||||
"""
|
||||
Get containers by stack name
|
||||
|
||||
Args:
|
||||
stack_name (str): stack name
|
||||
endpoint_id (int): endpoint id
|
||||
|
||||
Returns:
|
||||
list: containers info
|
||||
"""
|
||||
response = self.portainer.get_containers_by_stackName(endpoint_id,stack_name)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Get containers by stack name:{stack_name} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_container_by_id(self, endpoint_id: int, container_id: str):
|
||||
"""
|
||||
Get container by id
|
||||
|
||||
Args:
|
||||
endpoint_id (int): endpoint id
|
||||
container_id (str): container id
|
||||
|
||||
Returns:
|
||||
dict: container info
|
||||
"""
|
||||
response = self.portainer.get_container_by_id(endpoint_id, container_id)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"Get container by id:{container_id} error: {response.status_code}:{response.text}")
|
||||
raise CustomException()
|
|
@ -9,6 +9,9 @@ from src.external.nginx_proxy_manager_api import NginxProxyManagerAPI
|
|||
|
||||
|
||||
class ProxyManager:
|
||||
"""
|
||||
Proxy Manager
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the ProxyManager instance.
|
||||
|
@ -88,9 +91,9 @@ class ProxyManager:
|
|||
details=f"matching_domains:{matching_domains} already used"
|
||||
)
|
||||
else:
|
||||
logger.error(f"Check proxy host:{domain_names} exists error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
|
||||
def create_proxy_for_app(self,domain_names: list[str],forward_host: str,forward_port: int,advanced_config: str = "",forward_scheme: str = "http"):
|
||||
response = self.nginx.create_proxy_host(
|
||||
domain_names=domain_names,
|
||||
|
@ -100,10 +103,9 @@ class ProxyManager:
|
|||
advanced_config=advanced_config,
|
||||
)
|
||||
if response.status_code != 201:
|
||||
logger.error(f"Error create proxy for app:{response.text}")
|
||||
logger.error(f"Create proxy for app:{forward_host} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
|
||||
def update_proxy_for_app(self,domain_names: list[str],forward_host: str,forward_port: int,advanced_config: str = "",forward_scheme: str = "http"):
|
||||
response = self.nginx.update_proxy_host(
|
||||
domain_names=domain_names,
|
||||
|
@ -113,5 +115,31 @@ class ProxyManager:
|
|||
advanced_config=advanced_config,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Error update proxy for app:{response.text}")
|
||||
logger.error(f"Update proxy for app:{forward_host} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def get_proxy_host_by_app(self,app_id:str):
|
||||
response = self.nginx.get_proxy_hosts()
|
||||
if response.status_code == 200:
|
||||
proxys_host = response.json()
|
||||
proxy_result = []
|
||||
for proxy_host in proxys_host:
|
||||
if proxy_host.get("forward_host") == app_id:
|
||||
proxy_data = {
|
||||
"proxy_id": proxy_host.get("id"),
|
||||
"domain_names": proxy_host.get("domain_names")
|
||||
}
|
||||
proxy_result.append(proxy_data)
|
||||
return proxy_result
|
||||
else:
|
||||
logger.error(f"Get proxy host by app:{app_id} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
||||
|
||||
def remove_proxy_host_for_app(self,app_id:str):
|
||||
proxy_hosts = self.get_proxy_host_by_app(app_id)
|
||||
if proxy_hosts:
|
||||
for proxy_host in proxy_hosts:
|
||||
response = self.nginx.delete_proxy_host(proxy_host.get("proxy_id"))
|
||||
if response.status_code != 200:
|
||||
logger.error(f"Remove proxy host:{proxy_host.get('proxy_id')} for app:{app_id} error:{response.status_code}:{response.text}")
|
||||
raise CustomException()
|
|
@ -1,3 +1,7 @@
|
|||
from src.core.exception import CustomException
|
||||
from src.core.logger import logger
|
||||
|
||||
|
||||
class FileHelper:
|
||||
"""
|
||||
Helper class for file operations.
|
||||
|
@ -17,11 +21,13 @@ class FileHelper:
|
|||
Returns:
|
||||
str: The contents of the file.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
return content
|
||||
except:
|
||||
logger.error(f"Failed to read file {file_path}")
|
||||
raise CustomException()
|
||||
|
||||
@staticmethod
|
||||
def write_file(file_path, content):
|
||||
|
@ -32,6 +38,9 @@ class FileHelper:
|
|||
file_path (str): The path to the file.
|
||||
content (str): The content to be written.
|
||||
"""
|
||||
|
||||
try:
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
except:
|
||||
logger.error(f"Failed to write file {file_path}")
|
||||
raise CustomException()
|
Loading…
Reference in a new issue