From 177cd076c2d61afdc8768b82f3ee6a436f80dc17 Mon Sep 17 00:00:00 2001 From: zhaojing1987 Date: Thu, 28 Sep 2023 16:10:04 +0800 Subject: [PATCH] update --- appmanage_new/README.md | 2 +- appmanage_new/src/api/v1/routers/app.py | 177 +++++- appmanage_new/src/config/config.ini | 9 +- .../src/external/nginx_proxy_manager_api.py | 11 +- appmanage_new/src/external/portainer_api.py | 130 +++- appmanage_new/src/main.py | 5 +- appmanage_new/src/schemas/appInstall.py | 2 +- appmanage_new/src/services/app_manager.py | 584 ++++++++++++++++-- appmanage_new/src/services/git_manager.py | 5 +- appmanage_new/src/services/gitea_manager.py | 16 +- .../src/services/portainer_manager.py | 347 +++++++++-- appmanage_new/src/services/proxy_manager.py | 36 +- appmanage_new/src/utils/file_manager.py | 25 +- 13 files changed, 1194 insertions(+), 155 deletions(-) diff --git a/appmanage_new/README.md b/appmanage_new/README.md index 456f9119..9d7f1c58 100644 --- a/appmanage_new/README.md +++ b/appmanage_new/README.md @@ -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 diff --git a/appmanage_new/src/api/v1/routers/app.py b/appmanage_new/src/api/v1/routers/app.py index c261bdc7..d8317331 100644 --- a/appmanage_new/src/api/v1/routers/app.py +++ b/appmanage_new/src/api/v1/routers/app.py @@ -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) \ No newline at end of file + 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) diff --git a/appmanage_new/src/config/config.ini b/appmanage_new/src/config/config.ini index e99af0d4..7c8e0e8d 100644 --- a/appmanage_new/src/config/config.ini +++ b/appmanage_new/src/config/config.ini @@ -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/, diff --git a/appmanage_new/src/external/nginx_proxy_manager_api.py b/appmanage_new/src/external/nginx_proxy_manager_api.py index 3e437bcd..688ce076 100644 --- a/appmanage_new/src/external/nginx_proxy_manager_api.py +++ b/appmanage_new/src/external/nginx_proxy_manager_api.py @@ -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): diff --git a/appmanage_new/src/external/portainer_api.py b/appmanage_new/src/external/portainer_api.py index 712e4130..f3b6efa9 100644 --- a/appmanage_new/src/external/portainer_api.py +++ b/appmanage_new/src/external/portainer_api.py @@ -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", @@ -250,4 +256,98 @@ class PortainerAPI: """ return self.api.delete( path=f"endpoints/{endpointId}/docker/volumes/{volume_name}", - ) \ No newline at end of file + ) + + 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 + } + ) \ No newline at end of file diff --git a/appmanage_new/src/main.py b/appmanage_new/src/main.py index f5e618e2..d4614ad4 100644 --- a/appmanage_new/src/main.py +++ b/appmanage_new/src/main.py @@ -1,6 +1,4 @@ - import logging -import uvicorn from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse @@ -11,12 +9,11 @@ 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: uvicorn_logger.removeHandler(handler) -for handler in logger._error_logger.handlers: +for handler in logger._error_logger.handlers: uvicorn_logger.addHandler(handler) uvicorn_logger.setLevel(logging.INFO) diff --git a/appmanage_new/src/schemas/appInstall.py b/appmanage_new/src/schemas/appInstall.py index c294c5c5..d1ec743c 100644 --- a/appmanage_new/src/schemas/appInstall.py +++ b/appmanage_new/src/schemas/appInstall.py @@ -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 diff --git a/appmanage_new/src/services/app_manager.py b/appmanage_new/src/services/app_manager.py index 777618a2..0ffa4aa6 100644 --- a/appmanage_new/src/services/app_manager.py +++ b/appmanage_new/src/services/app_manager.py @@ -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") - 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: + 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() - else : - # validate the endpointId is exists - is_endpointId_exists = portainerManager.check_endpoint_exists(endpointId) + + # 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() - if not is_endpointId_exists: - raise CustomException( - status_code=404, - message="Invalid Request", - details="EndpointId Not Found" + 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() + + # Check the endpointId is exists. + endpointId = self._check_endpointId(endpointId, portainerManager) + + 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) + 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,49 +277,288 @@ 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() - } - new_env_values["APP_URL"] = domain_names[0] + "POWER_PASSWORD": PasswordGenerator.generate_strong_password(), + "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 - try: - if domain_names: - ProxyManager().create_proxy_for_app(domain_names,app_id,forward_port) - except (CustomException,Exception) as e: + # Install app - Step 4 : create proxy in nginx proxy manager + try: + 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 giteaManager.remove_repo(app_id) # Rollback-2: remove stack in portainer 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): """ Check the app_name and app_version is exists in docker library @@ -142,20 +578,21 @@ 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"] - if not any( - app_version in d["version"] for d in community_editions - ): - logger.error(f"When install app:{app_name}, the app version:{app_version} is not exists in docker library") - raise CustomException( - status_code=400, - message="Invalid Request", - details=f"app_version:{app_version} not supported", - ) + + 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"] + if not any( + app_version in d["version"] for d in community_editions + ): + logger.error(f"When install app:{app_name}, the app version:{app_version} is not exists in docker library") + raise CustomException( + status_code=400, + message="Invalid Request", + 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 \ No newline at end of file diff --git a/appmanage_new/src/services/git_manager.py b/appmanage_new/src/services/git_manager.py index 115ffc4b..c54ae1e2 100644 --- a/appmanage_new/src/services/git_manager.py +++ b/appmanage_new/src/services/git_manager.py @@ -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): @@ -90,5 +88,4 @@ class GitManager: except GitCommandError as e: logger.error(f"Failed to push from 'main' branch in git repository at {self.local_path} to remote '{remote_url}': {str(e)}") raise CustomException() - - \ No newline at end of file + \ No newline at end of file diff --git a/appmanage_new/src/services/gitea_manager.py b/appmanage_new/src/services/gitea_manager.py index db665916..8c1b1036 100644 --- a/appmanage_new/src/services/gitea_manager.py +++ b/appmanage_new/src/services/gitea_manager.py @@ -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() \ No newline at end of file diff --git a/appmanage_new/src/services/portainer_manager.py b/appmanage_new/src/services/portainer_manager.py index 22ea79a0..aaaea79d 100644 --- a/appmanage_new/src/services/portainer_manager.py +++ b/appmanage_new/src/services/portainer_manager.py @@ -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,37 +225,60 @@ 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 try: if stack_name is not None: - volumes = self.get_volumes_by_stack_name(stack_name, endpoint_id,True) + 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"]) - self.remove_volume(volume_names, endpoint_id) + 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: - 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 - } + try: + volumes = response.json().get("Volumes", []) + 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}") - raise CustomException() \ No newline at end of file + 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() \ No newline at end of file diff --git a/appmanage_new/src/services/proxy_manager.py b/appmanage_new/src/services/proxy_manager.py index e0a04353..c8adfd4d 100644 --- a/appmanage_new/src/services/proxy_manager.py +++ b/appmanage_new/src/services/proxy_manager.py @@ -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() \ No newline at end of file diff --git a/appmanage_new/src/utils/file_manager.py b/appmanage_new/src/utils/file_manager.py index 3d6ef75f..c3f20107 100644 --- a/appmanage_new/src/utils/file_manager.py +++ b/appmanage_new/src/utils/file_manager.py @@ -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. """ - - with open(file_path, 'r') as f: - content = f.read() - - return content + 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. """ - - with open(file_path, 'w') as f: - f.write(content) + try: + with open(file_path, 'w') as f: + f.write(content) + except: + logger.error(f"Failed to write file {file_path}") + raise CustomException() \ No newline at end of file