This commit is contained in:
zhaojing1987 2023-09-28 16:10:04 +08:00
parent f7b3079fc5
commit 177cd076c2
13 changed files with 1194 additions and 155 deletions

View file

@ -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 # 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
## supervisorctl reload ## supervisorctl reload

View file

@ -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.appInstall import appInstall
from src.schemas.appResponse import AppResponse
from src.schemas.errorResponse import ErrorResponse from src.schemas.errorResponse import ErrorResponse
from src.services.app_manager import AppManger from src.services.app_manager import AppManger
router = APIRouter(prefix="/api/v1") 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(): 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( @router.post(
"/apps/install", "/apps/install",
@ -26,4 +83,116 @@ def apps_install(
appInstall: appInstall, appInstall: appInstall,
endpointId: int = Query(None, description="Endpoint ID to install app on,if not set, install on the local endpoint"), 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)

View file

@ -7,26 +7,29 @@ access_token =
base_url = http://websoft9-proxy:81/api base_url = http://websoft9-proxy:81/api
#base_url = http://47.92.222.186/w9proxy/api #base_url = http://47.92.222.186/w9proxy/api
user_name = help@websoft9.com user_name = help@websoft9.com
user_pwd = websoft9@2023 user_pwd = ECTKPRAWhij789yr
#The config for gitea #The config for gitea
[gitea] [gitea]
base_url = http://websoft9-git:3000/api/v1 base_url = http://websoft9-git:3000/api/v1
# base_url = http://47.92.222.186/w9git/api/v1 # base_url = http://47.92.222.186/w9git/api/v1
user_name = websoft9 user_name = websoft9
user_pwd = O4rXXHkSoKVY user_pwd = Rk9qOQ68Inf0
#The config for portainer #The config for portainer
[portainer] [portainer]
base_url = http://websoft9-deployment:9000/api base_url = http://websoft9-deployment:9000/api
#base_url = http://47.92.222.186/w9deployment/api #base_url = http://47.92.222.186/w9deployment/api
user_name = admin user_name = admin
user_pwd = &uswVF^wMyi]wpdc user_pwd = ]}fU;XmVH].VI{Hh
#The path of docker library #The path of docker library
[docker_library] [docker_library]
path = /websoft9/library/apps 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 is a list of public ip url, which is used to get the public ip of the server
[public_ip_url_list] [public_ip_url_list]
url_list = https://api.ipify.org/, url_list = https://api.ipify.org/,

View file

@ -13,11 +13,12 @@ class NginxProxyManagerAPI:
api (APIHelper): API helper api (APIHelper): API helper
Methods: Methods:
get_token(identity: str, secret: str) -> Response: Request a new access token set_token(api_token): Set API token
get_proxy_hosts() -> Response: Get all proxy hosts get_token(identity, secret): Request a new access token
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 get_proxy_hosts(): Get all proxy hosts
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 create_proxy_host(domain_names, forward_scheme, forward_host, forward_port, advanced_config): Create a new proxy host
delete_proxy_host(proxy_id: int) -> Response: Delete a 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): def __init__(self):

View file

@ -13,15 +13,20 @@ class PortainerAPI:
api (APIHelper): API helper api (APIHelper): API helper
Methods: Methods:
get_jwt_token(username: str, password: str) -> Response): Get JWT token set_jwt_token(jwt_token): Set JWT token
get_endpoints() -> Response: Get endpoints get_jwt_token(username, password): Get JWT token
get_stacks(endpointID: int) -> Response: Get stacks get_endpoints(): Get endpoints
get_stack_by_id(stackID: int) -> Response: Get stack by ID get_endpoint_by_id(endpointId): Get endpoint by ID
remove_stack(stackID: int,endPointID: int) -> Response: Remove a stack create_endpoint(name, EndpointCreationType): Create an endpoint
create_stack_standlone_repository(app_name: str, endpointId: int,repositoryURL:str) -> Response: Create a stack from a standalone repository get_stacks(endpointId): Get stacks
start_stack(stackID: int, endpointId: int) -> Response: Start a stack get_stack_by_id(stackID): Get stack by ID
stop_stack(stackID: int, endpointId: int) -> Response: Stop a stack remove_stack(stackID, endpointId): Remove a stack
redeploy_stack(stackID: int, endpointId: int) -> Response: Redeploy 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): 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: Args:
stackID (int): Stack ID stackID (int): Stack ID
@ -194,9 +199,9 @@ class PortainerAPI:
path=f"stacks/{stackID}/start", params={"endpointId": endpointId} 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: Args:
stackID (int): Stack ID stackID (int): Stack ID
@ -224,12 +229,13 @@ class PortainerAPI:
path=f"stacks/{stackID}/redeploy", params={"endpointId": endpointId} 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 Get volumes in endpoint
Args: Args:
endpointId (int): Endpoint ID endpointId (int): Endpoint ID
dangling (bool): the volume is dangling or not
""" """
return self.api.get( return self.api.get(
path=f"endpoints/{endpointId}/docker/volumes", path=f"endpoints/{endpointId}/docker/volumes",
@ -251,3 +257,97 @@ class PortainerAPI:
return self.api.delete( return self.api.delete(
path=f"endpoints/{endpointId}/docker/volumes/{volume_name}", 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
}
)

View file

@ -1,6 +1,4 @@
import logging import logging
import uvicorn
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
@ -11,7 +9,6 @@ from src.core.exception import CustomException
from src.core.logger import logger from src.core.logger import logger
from src.schemas.errorResponse import ErrorResponse from src.schemas.errorResponse import ErrorResponse
uvicorn_logger = logging.getLogger("uvicorn") uvicorn_logger = logging.getLogger("uvicorn")
for handler in uvicorn_logger.handlers: for handler in uvicorn_logger.handlers:

View file

@ -1,5 +1,5 @@
import re import re
from typing import Optional, List,Union from typing import Optional, List
from pydantic import BaseModel, Field, validator from pydantic import BaseModel, Field, validator
from src.core.exception import CustomException from src.core.exception import CustomException

View file

@ -1,4 +1,5 @@
import ipaddress
import json import json
import os import os
import shutil import shutil
@ -6,6 +7,7 @@ from src.core.config import ConfigManager
from src.core.envHelper import EnvHelper from src.core.envHelper import EnvHelper
from src.core.exception import CustomException from src.core.exception import CustomException
from src.schemas.appInstall import appInstall from src.schemas.appInstall import appInstall
from src.schemas.appResponse import AppResponse
from src.services.git_manager import GitManager from src.services.git_manager import GitManager
from src.services.gitea_manager import GiteaManager from src.services.gitea_manager import GiteaManager
from src.services.portainer_manager import PortainerManager from src.services.portainer_manager import PortainerManager
@ -16,26 +18,223 @@ from src.utils.password_generator import PasswordGenerator
class AppManger: class AppManger:
def install_app(self,appInstall: appInstall, endpointId: int = None): def get_catalog_apps(self,locale:str):
library_path = ConfigManager().get_value("docker_library", "path") 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() portainerManager = PortainerManager()
# if endpointId is None, get the local endpointId # Check the endpointId is exists.
if endpointId is None: endpointId = self._check_endpointId(endpointId, portainerManager)
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)
if not is_endpointId_exists: try:
raise CustomException( apps_info = []
status_code=404, # Get the stacks
message="Invalid Request", stacks = portainerManager.get_stacks(endpointId)
details="EndpointId Not Found" 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 # validate the app_name and app_version
app_name = appInstall.app_name app_name = appInstall.app_name
@ -44,29 +243,27 @@ class AppManger:
# validate the app_id # validate the app_id
app_id = appInstall.app_id app_id = appInstall.app_id
self._check_appId(app_id,endpointId) self._check_appId(app_id,endpointId,giteaManager,portainerManager)
proxy_enabled = appInstall.proxy_enabled
domain_names = appInstall.domain_names
# validate the domain_names # validate the domain_names
proxy_enabled = appInstall.proxy_enabled
domain_names = appInstall.domain_names
if proxy_enabled: if proxy_enabled:
self._check_domain_names(domain_names) self._check_domain_names(domain_names)
# Begin install app # Install app - Step 1 : create repo in gitea
# Step 1 : create repo in gitea
giteaManager = GiteaManager()
repo_url = giteaManager.create_repo(app_id) 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: try:
# The source directory.
local_path = f"{library_path}/{app_name}" local_path = f"{library_path}/{app_name}"
# The destination directory. # Create a temporary directory.
app_tmp_dir = "/tmp" app_tmp_dir = "/tmp"
app_tmp_dir_path = f"{app_tmp_dir}/{app_name}" 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): if not os.path.exists(app_tmp_dir):
os.makedirs(app_tmp_dir) os.makedirs(app_tmp_dir)
@ -80,41 +277,49 @@ class AppManger:
# Modify the env file # Modify the env file
env_file_path = f"{app_tmp_dir_path}/.env" env_file_path = f"{app_tmp_dir_path}/.env"
new_env_values = { new_env_values = {
"APP_NAME": app_id, "APP_ID": app_id,
"APP_NAME": app_name,
"APP_DIST": "community",
"APP_VERSION": app_version, "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) 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 # Commit and push to remote repo
self._init_local_repo_and_push_to_remote(app_tmp_dir_path,repo_url) 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: except (CustomException,Exception) as e:
# Rollback: remove repo in gitea # Rollback: remove repo in gitea
giteaManager.remove_repo(app_id) giteaManager.remove_repo(app_id)
raise CustomException() raise CustomException()
# Step 3 : create stack in portainer # Install app - Step 3 : create stack in portainer
try: try:
# Get gitea user_name and user_pwd
user_name = ConfigManager().get_value("gitea","user_name") user_name = ConfigManager().get_value("gitea","user_name")
user_pwd = ConfigManager().get_value("gitea","user_pwd") user_pwd = ConfigManager().get_value("gitea","user_pwd")
portainerManager.create_stack_from_repository(app_id,endpointId,repo_url,user_name,user_pwd) # Create stack in portainer
stack_id = portainerManager.get_stack_by_name(app_id,endpointId)["Id"] 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: except (CustomException,Exception) as e:
# Rollback: remove repo in gitea # Rollback: remove repo in gitea
giteaManager.remove_repo(app_id) giteaManager.remove_repo(app_id)
raise CustomException() raise CustomException()
# Step 4 : create proxy in proxy # Install app - Step 4 : create proxy in nginx proxy manager
try: try:
if domain_names: if proxy_enabled and domain_names:
ProxyManager().create_proxy_for_app(domain_names,app_id,forward_port) # 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: except (CustomException,Exception) as e:
# Rollback-1: remove repo in gitea # Rollback-1: remove repo in gitea
giteaManager.remove_repo(app_id) giteaManager.remove_repo(app_id)
@ -122,6 +327,237 @@ class AppManger:
portainerManager.remove_stack_and_volumes(stack_id,endpointId) portainerManager.remove_stack_and_volumes(stack_id,endpointId)
raise CustomException() 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): def _check_appName_and_appVersion(self,app_name:str, app_version:str,library_path:str):
""" """
@ -142,20 +578,21 @@ class AppManger:
details=f"app_name:{app_name} not supported", details=f"app_name:{app_name} not supported",
) )
else: 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",
)
def _check_appId(self,app_id:str,endpointId:int): 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,giteaManager:GiteaManager,portainerManager:PortainerManager):
""" """
Check the app_id is exists in gitea and portainer 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 CustomException: If the app_id is exists in gitea or portainer
""" """
# validate the app_id is exists in gitea # validate the app_id is exists in gitea
giteaManager = GiteaManager()
is_repo_exists = giteaManager.check_repo_exists(app_id) is_repo_exists = giteaManager.check_repo_exists(app_id)
if is_repo_exists: if is_repo_exists:
logger.error(f"When install app,the app_id:{{app_id}} is exists in gitea") 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 # validate the app_id is exists in portainer
portainerManager = PortainerManager()
is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId) is_stack_exists = portainerManager.check_stack_exists(app_id,endpointId)
if is_stack_exists: if is_stack_exists:
logger.error(f"When install app, the app_id:{app_id} is exists in portainer") 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: except (CustomException,Exception) as e:
logger.error(f"Init local repo and push to remote repo error:{e}") logger.error(f"Init local repo and push to remote repo error:{e}")
raise CustomException() 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

View file

@ -1,5 +1,4 @@
import os import os
import shutil
from git import Repo, GitCommandError from git import Repo, GitCommandError
from src.core.exception import CustomException from src.core.exception import CustomException
from src.core.logger import logger from src.core.logger import logger
@ -15,7 +14,6 @@ class GitManager:
Methods: Methods:
init_local_repo_from_dir() -> None: Initialize a local git repository from a directory. 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. 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): 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)}") logger.error(f"Failed to push from 'main' branch in git repository at {self.local_path} to remote '{remote_url}': {str(e)}")
raise CustomException() raise CustomException()

View file

@ -8,7 +8,13 @@ from src.external.gitea_api import GiteaAPI
class GiteaManager: class GiteaManager:
"""
Gitea Manager
"""
def __init__(self): def __init__(self):
"""
Init GiteaManager
"""
try: try:
self.gitea = GiteaAPI() self.gitea = GiteaAPI()
self._set_basic_auth_credential() self._set_basic_auth_credential()
@ -43,7 +49,7 @@ class GiteaManager:
elif response.status_code == 404: elif response.status_code == 404:
return False return False
else: 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() raise CustomException()
def create_repo(self, repo_name: str): def create_repo(self, repo_name: str):
@ -61,7 +67,7 @@ class GiteaManager:
repo_json = response.json() repo_json = response.json()
return repo_json["clone_url"] return repo_json["clone_url"]
else: 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() raise CustomException()
def get_file_content_from_repo(self, repo_name: str, file_path: str): def get_file_content_from_repo(self, repo_name: str, file_path: str):
@ -78,17 +84,17 @@ class GiteaManager:
"content": response_json["content"], "content": response_json["content"],
} }
else: 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() raise CustomException()
def update_file_in_repo(self, repo_name: str, file_path: str, content: str,sha: str): 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) response = self.gitea.update_file_content_in_repo(repo_name, file_path, content, sha)
if response.status_code != 201: 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() raise CustomException()
def remove_repo(self, repo_name: str): def remove_repo(self, repo_name: str):
response = self.gitea.remove_repo(repo_name) response = self.gitea.remove_repo(repo_name)
if response.status_code != 204: 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() raise CustomException()

View file

@ -9,6 +9,26 @@ from src.core.logger import logger
class PortainerManager: 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): def __init__(self):
try: try:
self.portainer = PortainerAPI() self.portainer = PortainerAPI()
@ -18,6 +38,9 @@ class PortainerManager:
raise CustomException() raise CustomException()
def _set_portainer_token(self): def _set_portainer_token(self):
"""
Set Portainer token
"""
service_name = "portainer" service_name = "portainer"
token_name = "user_token" token_name = "user_token"
@ -83,10 +106,10 @@ class PortainerManager:
if local_endpoint is not None: if local_endpoint is not None:
return local_endpoint["Id"] return local_endpoint["Id"]
else: else:
logger.error(f"Error get local endpoint id from portainer: {response.text}") logger.error(f"Can't find local endpoint")
raise CustomException() raise CustomException()
else: 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() raise CustomException()
def check_endpoint_exists(self, endpoint_id: int): def check_endpoint_exists(self, endpoint_id: int):
@ -96,7 +119,7 @@ class PortainerManager:
elif response.status_code == 404: elif response.status_code == 404:
return False return False
else: 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() raise CustomException()
def check_stack_exists(self, stack_name: str, endpoint_id: int): def check_stack_exists(self, stack_name: str, endpoint_id: int):
@ -108,29 +131,79 @@ class PortainerManager:
return True return True
return False return False
else: 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() raise CustomException()
def create_stack_from_repository(self, stack_name: str, endpoint_id: int,repositoryURL : str,user_name:str,user_password:str): 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) response = self.portainer.create_stack_standlone_repository(stack_name, endpoint_id,repositoryURL,user_name,user_password)
if response.status_code != 200: if response.status_code == 200:
logger.error(f"Error create stack from portainer: {response.text}") return response.json()
raise CustomException() 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): 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) response = self.portainer.get_stacks(endpoint_id)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: 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() raise CustomException()
def get_stack_by_id(self, stack_id: int): 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) response = self.portainer.get_stack_by_id(stack_id)
if response.status_code == 200: if response.status_code == 200:
return response.json() return response.json()
else: 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() raise CustomException()
def get_stack_by_name(self, stack_name: str, endpoint_id: int): def get_stack_by_name(self, stack_name: str, endpoint_id: int):
@ -152,23 +225,45 @@ class PortainerManager:
return stack return stack
return None return None
else: 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() raise CustomException()
def remove_stack(self, stack_id: int, endpoint_id: int): 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) response = self.portainer.remove_stack(stack_id, endpoint_id)
if response.status_code != 204: 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() 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): 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 # get stack name
stack_name = self.get_stack_by_id(stack_id).get("Name") stack_name = self.get_stack_by_id(stack_id).get("Name")
# remove stack # remove stack
response = self.portainer.remove_stack(stack_id, endpoint_id) response = self.portainer.remove_stack(stack_id, endpoint_id)
if response.status_code != 204: 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() raise CustomException()
# remove volumes # remove volumes
@ -176,13 +271,14 @@ class PortainerManager:
if stack_name is not None: 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 = [] volume_names = []
for volume in volumes.get("mountpoint", []): for volume in volumes:
volume_names.append(volume["name"]) volume_names.append(volume["Name"])
self.remove_volume(volume_names, endpoint_id)
if len(volume_names) > 0:
self.remove_volume(volume_names, endpoint_id)
except (CustomException,Exception) as e: except (CustomException,Exception) as e:
raise CustomException() raise CustomException()
def get_volumes_by_stack_name(self, stack_name: str, endpoint_id: int,dangling:bool): def get_volumes_by_stack_name(self, stack_name: str, endpoint_id: int,dangling:bool):
""" """
Get volumes by stack name Get volumes by stack name
@ -190,32 +286,22 @@ class PortainerManager:
Args: Args:
stack_name (str): stack name stack_name (str): stack name
endpoint_id (int): endpoint id endpoint_id (int): endpoint id
dangling (bool): the volume is dangling or not
Returns: Returns:
dict: volumes info dict: volumes info
""" """
response = self.portainer.get_volumes(endpoint_id,dangling) response = self.portainer.get_volumes(endpoint_id,dangling)
if response.status_code == 200: if response.status_code == 200:
volumes = response.json().get("Volumes", []) try:
mountpoints = [] volumes = response.json().get("Volumes", [])
volumes_info = [volume for volume in volumes if volume.get("Labels", {}).get("com.docker.compose.project") == stack_name]
for volume in volumes: except Exception as e:
labels = volume.get("Labels", {}) logger.error(f"Get volumes by stack name:{stack_name} error: {e}")
raise CustomException()
if labels.get("com.docker.compose.project") == stack_name: return volumes_info
mountpoint_info = {
"name": volume["Name"],
"path": volume["Mountpoint"]
}
mountpoints.append(mountpoint_info)
return {
"stack_name": stack_name,
"mountpoint": mountpoints
}
else: 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() raise CustomException()
def remove_volume(self, volume_names: list, endpoint_id: int): def remove_volume(self, volume_names: list, endpoint_id: int):
@ -229,5 +315,188 @@ class PortainerManager:
for volume_name in volume_names: for volume_name in volume_names:
response = self.portainer.remove_volume_by_name(endpoint_id,volume_name) response = self.portainer.remove_volume_by_name(endpoint_id,volume_name)
if response.status_code != 204: 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() 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()

View file

@ -9,6 +9,9 @@ from src.external.nginx_proxy_manager_api import NginxProxyManagerAPI
class ProxyManager: class ProxyManager:
"""
Proxy Manager
"""
def __init__(self): def __init__(self):
""" """
Initialize the ProxyManager instance. Initialize the ProxyManager instance.
@ -88,9 +91,9 @@ class ProxyManager:
details=f"matching_domains:{matching_domains} already used" details=f"matching_domains:{matching_domains} already used"
) )
else: else:
logger.error(f"Check proxy host:{domain_names} exists error:{response.status_code}:{response.text}")
raise CustomException() 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"): 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( response = self.nginx.create_proxy_host(
domain_names=domain_names, domain_names=domain_names,
@ -100,10 +103,9 @@ class ProxyManager:
advanced_config=advanced_config, advanced_config=advanced_config,
) )
if response.status_code != 201: 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() 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"): 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( response = self.nginx.update_proxy_host(
domain_names=domain_names, domain_names=domain_names,
@ -113,5 +115,31 @@ class ProxyManager:
advanced_config=advanced_config, advanced_config=advanced_config,
) )
if response.status_code != 200: 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() 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()

View file

@ -1,3 +1,7 @@
from src.core.exception import CustomException
from src.core.logger import logger
class FileHelper: class FileHelper:
""" """
Helper class for file operations. Helper class for file operations.
@ -17,11 +21,13 @@ class FileHelper:
Returns: Returns:
str: The contents of the file. str: The contents of the file.
""" """
try:
with open(file_path, 'r') as f: with open(file_path, 'r') as f:
content = f.read() content = f.read()
return content
return content except:
logger.error(f"Failed to read file {file_path}")
raise CustomException()
@staticmethod @staticmethod
def write_file(file_path, content): def write_file(file_path, content):
@ -32,6 +38,9 @@ class FileHelper:
file_path (str): The path to the file. file_path (str): The path to the file.
content (str): The content to be written. content (str): The content to be written.
""" """
try:
with open(file_path, 'w') as f: with open(file_path, 'w') as f:
f.write(content) f.write(content)
except:
logger.error(f"Failed to write file {file_path}")
raise CustomException()