This commit is contained in:
zhaojing1987 2023-10-11 17:09:54 +08:00
parent d3fc72cb0a
commit d13d58f297
73 changed files with 461 additions and 70 deletions

20
appmanage_new/README.md → apphub/README.md Normal file → Executable file
View file

@ -1,11 +1,17 @@
## 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
## supervisorctl update
## supervisorctl status
# gitea_token: da7b9891a0bc71b5026b389c11ed13238c9a3866
# cli
## apphub
'''
Usage: apphub [OPTIONS] COMMAND [ARGS]...
Options:
--help Show this message and exit.
Commands:
genkey Generate a new API key
getconfig Get a config value
setconfig Set a config value
'''
# 项目结构
## src

View file

@ -0,0 +1,36 @@
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
import click
from src.services.apikey_manager import APIKeyManager
from src.services.settings_manager import SettingsManager
@click.group()
def cli():
pass
@cli.command()
def genkey():
"""Generate a new API key"""
key = APIKeyManager().generate_key()
click.echo(f"{key}")
@cli.command()
@click.option('--section',required=True, help='The section name')
@click.option('--key', required=True, help='The key name')
@click.option('--value', required=True,help='The value of the key')
def setconfig(section, key, value):
"""Set a config value"""
SettingsManager().write_section(section, key, value)
@cli.command()
@click.option('--section',required=True, help='The section name')
@click.option('--key', required=True, help='The key name')
def getconfig(section, key):
"""Get a config value"""
value = SettingsManager().read_section(section, key)
click.echo(f"{value}")
if __name__ == "__main__":
cli()

View file

@ -0,0 +1,28 @@
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
import click
from src.services.apikey_manager import APIKeyManager
from src.services.settings_manager import SettingsManager
@click.group()
def cli():
pass
@cli.command()
def genkey():
"""Generate a new API key"""
key = APIKeyManager().generate_key()
click.echo(f"{key}")
@cli.command()
@click.option('--section',required=True, help='The section name')
@click.option('--key', required=True, help='The key name')
@click.option('--value', required=True,help='The value of the key')
def setconfig(section, key, value):
"""Set a config value"""
SettingsManager().write_section(section, key, value)
if __name__ == "__main__":
cli()

View file

View file

View file

View file

View file

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View file

View file

View file

0
appmanage_new/docs/user.md → apphub/docs/user.md Normal file → Executable file
View file

View file

@ -4,4 +4,5 @@ keyring
keyrings.alt
requests
GitPython
PyJWT
PyJWT
click

16
apphub/setup.py Normal file
View file

@ -0,0 +1,16 @@
from setuptools import find_packages, setup
setup(
name='apphub',
version='0.2',
packages=find_packages(where='src'),
package_dir={'': 'src'},
install_requires=[
'click',
],
entry_points={
'console_scripts': [
'apphub=cli.apphub_cli:cli',
],
},
)

View file

View file

View file

View file

@ -0,0 +1,52 @@
from fastapi import APIRouter, Query,Path
from src.schemas.appSettings import AppSettings
from src.schemas.errorResponse import ErrorResponse
from typing import List
from src.services.settings_manager import SettingsManager
router = APIRouter()
@router.get("/settings",
summary="Get settings",
description="Get settings",
responses={
200: {"model": AppSettings},
400: {"model": ErrorResponse},
500: {"model": ErrorResponse},
}
)
def get_settings():
return SettingsManager().read_all()
@router.get(
"/settings/{section}",
summary="Get settings",
description="Get settings by section",
responses={
200: {"model": AppSettings},
400: {"model": ErrorResponse},
500: {"model": ErrorResponse},
}
)
def get_setting_by_section(
section: str = Path(..., description="Section name to update settings from"),
):
return SettingsManager().read_section(section)
@router.put(
"/settings/{section}",
summary="Update Settings",
description="Update settings",
responses={
200: {"model": AppSettings},
400: {"model": ErrorResponse},
500: {"model": ErrorResponse},
}
)
def update_settings(
section: str = Path(..., description="Section name to update settings from"),
key: str = Query(..., description="Key name to update settings from"),
value: str = Query(..., description="Key value to update settings from"),
):
return SettingsManager().write_section(section,key,value)

View file

@ -0,0 +1,3 @@
Metadata-Version: 2.1
Name: apphub
Version: 0.2

View file

@ -0,0 +1,16 @@
README.md
setup.py
/websoft9/apphub/src/apphub.egg-info/PKG-INFO
/websoft9/apphub/src/apphub.egg-info/SOURCES.txt
/websoft9/apphub/src/apphub.egg-info/dependency_links.txt
/websoft9/apphub/src/apphub.egg-info/entry_points.txt
/websoft9/apphub/src/apphub.egg-info/requires.txt
/websoft9/apphub/src/apphub.egg-info/top_level.txt
src/apphub.egg-info/PKG-INFO
src/apphub.egg-info/SOURCES.txt
src/apphub.egg-info/dependency_links.txt
src/apphub.egg-info/entry_points.txt
src/apphub.egg-info/requires.txt
src/apphub.egg-info/top_level.txt
src/cli/__init__.py
src/cli/apphub_cli.py

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,2 @@
[console_scripts]
apphub = cli.apphub_cli:cli

View file

@ -0,0 +1 @@
click

View file

@ -0,0 +1 @@
cli

0
apphub/src/cli/__init__.py Executable file
View file

View file

@ -0,0 +1,3 @@
Metadata-Version: 2.1
Name: apphub
Version: 0.1

View file

@ -0,0 +1,7 @@
setup.py
apphub.egg-info/PKG-INFO
apphub.egg-info/SOURCES.txt
apphub.egg-info/dependency_links.txt
apphub.egg-info/entry_points.txt
apphub.egg-info/requires.txt
apphub.egg-info/top_level.txt

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,2 @@
[console_scripts]
apphub = cli.apphub_cli:main

View file

@ -0,0 +1 @@
click

View file

@ -0,0 +1 @@
cli

36
apphub/src/cli/apphub_cli.py Executable file
View file

@ -0,0 +1,36 @@
import sys
import os
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
import click
from src.services.apikey_manager import APIKeyManager
from src.services.settings_manager import SettingsManager
@click.group()
def cli():
pass
@cli.command()
def genkey():
"""Generate a new API key"""
key = APIKeyManager().generate_key()
click.echo(f"{key}")
@cli.command()
@click.option('--section',required=True, help='The section name')
@click.option('--key', required=True, help='The key name')
@click.option('--value', required=True,help='The value of the key')
def setconfig(section, key, value):
"""Set a config value"""
SettingsManager().write_section(section, key, value)
@cli.command()
@click.option('--section',required=True, help='The section name')
@click.option('--key', required=True, help='The key name')
def getconfig(section, key):
"""Get a config value"""
value = SettingsManager().read_key(section, key)
click.echo(f"{value}")
if __name__ == "__main__":
cli()

View file

@ -2,23 +2,33 @@
[nginx_proxy_manager]
base_url = http://websoft9-proxy:81/api
user_name = help@websoft9.com
user_pwd = ECTKPRAWhij789yr
user_pwd = L2l6nuFu2jL0Rm8L
nike_name = admin
#The config for gitea
[gitea]
base_url = http://websoft9-git:3000/api/v1
user_name = websoft9
user_pwd = Rk9qOQ68Inf0
user_pwd = XATY3rEgQ1bA
#The config for portainer
[portainer]
base_url = http://websoft9-deployment:9000/api
user_name = admin
user_pwd = ]}fU;XmVH].VI{Hh
user_pwd = fn9BrJhd)An8T8Ez
#The path of docker library
[docker_library]
path = /websoft9/library/apps
#The path of app media
[app_media]
path = /websoft9/media/json/
path = /websoft9/media/json/
#The value of api_key
[api_key]
key =
#The config for cockpit
[cockpit]
port = 9090

0
apphub/src/config/system.ini Executable file
View file

View file

View file

View file

View file

View file

View file

View file

113
apphub/src/main.py Executable file
View file

@ -0,0 +1,113 @@
import logging
from fastapi import FastAPI, Request,Security,Depends
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from src.api.v1.routers import app as api_app
from src.api.v1.routers import settings as api_settings
from src.api.v1.routers import proxy as api_proxy
from src.core.config import ConfigManager
from src.core.exception import CustomException
from src.core.logger import logger
from src.schemas.errorResponse import ErrorResponse
from fastapi.responses import HTMLResponse
from fastapi.security.api_key import APIKeyHeader
uvicorn_logger = logging.getLogger("uvicorn")
for handler in uvicorn_logger.handlers:
uvicorn_logger.removeHandler(handler)
for handler in logger._error_logger.handlers:
uvicorn_logger.addHandler(handler)
uvicorn_logger.setLevel(logging.INFO)
API_KEY = ConfigManager().get_value("api_key","key")
API_KEY_NAME = "api_key"
api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=False)
async def verify_key(request: Request, api_key_header: str = Security(api_key_header)):
if request.url.path == "/docs":
return None
if api_key_header is None:
raise CustomException(
status_code=400,
message="Invalid Request",
details="No API Key provided"
)
if api_key_header != API_KEY:
raise CustomException(
status_code=400,
message="Invalid Request",
details="Invalid API Key"
)
return api_key_header
app = FastAPI(
title="AppManae API",
description="This documentation describes the AppManage API.",
version="0.0.1",
docs_url=None,
dependencies=[Depends(verify_key)]
)
app.mount("/static", StaticFiles(directory="swagger-ui"), name="static")
@app.get("/docs", response_class=HTMLResponse,include_in_schema=False,)
async def custom_swagger_ui_html():
return """
<!DOCTYPE html>
<html>
<head>
<title>Websoft9 API</title>
<link rel="stylesheet" type="text/css" href="/static/swagger-ui.css">
<script src="/static/swagger-ui-bundle.js"></script>
</head>
<body>
<div id="swagger-ui"></div>
<script>
const ui = SwaggerUIBundle({
url: "/openapi.json",
dom_id: '#swagger-ui',
presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.SwaggerUIStandalonePreset],
layout: "BaseLayout"
})
</script>
</body>
</html>
"""
# remove 422 responses
@app.on_event("startup")
async def remove_422_responses():
openapi_schema = app.openapi()
for path, path_item in openapi_schema["paths"].items():
for method, operation in path_item.items():
operation["responses"].pop("422", None)
app.openapi_schema = openapi_schema
#custom error handler
@app.exception_handler(CustomException)
async def custom_exception_handler(request, exc: CustomException):
return JSONResponse(
status_code=exc.status_code,
content=ErrorResponse(message=exc.message, details=exc.details).model_dump(),
)
# 422 error handler:set 422 response to 400
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
#errors = {err['loc'][1]: err['msg'] for err in exc.errors()}
errors = ", ".join(f"{err['loc'][1]}: {err['msg']}" for err in exc.errors())
return JSONResponse(
status_code=400,
content=ErrorResponse(message="Request Validation Error", details=errors).model_dump(),
)
app.include_router(api_app.router,tags=["apps"])
app.include_router(api_settings.router,tags=["settings"])
app.include_router(api_proxy.router,tags=["proxys"])

View file

@ -0,0 +1,23 @@
import secrets
import hashlib
from src.core.config import ConfigManager
from src.core.exception import CustomException
from src.core.logger import logger
class APIKeyManager:
def generate_key(self):
try:
base = secrets.token_urlsafe(32)
key = hashlib.sha256(base.encode()).hexdigest()
ConfigManager().set_value('api_key', 'key', key)
return key
except Exception as e:
logger.error("Error generating API key"+str(e))
raise CustomException()
def delete_key(self):
try:
ConfigManager().remove_value("api_key", "key")
except Exception as e:
logger.error("Error deleting API key"+e)
raise CustomException()

View file

@ -719,7 +719,6 @@ class AppManger:
else:
raise CustomException()
def _init_local_repo_and_push_to_remote(self,local_path:str,repo_url:str):
"""
Initialize a local git repository from a directory and push to remote repo

View file

@ -0,0 +1,89 @@
import os
import configparser
from typing import Dict
from src.core.exception import CustomException
from src.core.logger import logger
from src.schemas.appSettings import AppSettings
class SettingsManager:
def __init__(self):
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, "../config")
self.config_file_path = os.path.join(config_dir, "config.ini")
self.config_file_path = os.path.abspath(self.config_file_path)
self.config = configparser.ConfigParser()
def read_all(self) -> Dict[str, Dict[str, str]]:
try:
self.config.read(self.config_file_path)
data = {s:dict(self.config.items(s)) for s in self.config.sections()}
return AppSettings(**data)
except Exception as e:
logger.error(e)
raise CustomException()
def write_all(self, data: AppSettings):
for section, kv in data.model_dump().items():
if section not in self.config.sections():
self.config.add_section(section)
for key, value in kv.items():
self.config.set(section, key, value)
with open(self.filename, 'w') as configfile:
self.config.write(configfile)
def read_section(self, section_name: str) -> Dict[str, str]:
try:
self.config.read(self.config_file_path)
return dict(self.config.items(section_name))
except Exception as e:
logger.error(e)
raise CustomException()
def read_key(self, section: str, key:str) -> str:
try:
self.config.read(self.config_file_path)
if section not in self.config.sections():
raise CustomException(
status_code=400,
message="Invalid Request",
details=f"Section:{section} does not exist"
)
if key not in self.config[section]:
raise CustomException(
status_code=400,
message="Invalid Request",
details=f"Key:{key} does not exist"
)
return self.config[section][key]
except CustomException as e:
raise e
except Exception as e:
logger.error("Error in read_key:"+str(e))
raise CustomException()
def write_section(self, section: str, key:str,value:str):
try:
self.config.read(self.config_file_path)
if section not in self.config.sections():
raise CustomException(
status_code=400,
message="Invalid Request",
details=f"Section:{section} does not exist"
)
if key not in self.config[section]:
raise CustomException(
status_code=400,
message="Invalid Request",
details=f"Key:{key} does not exist"
)
self.config.set(section, key, value)
with open(self.config_file_path, 'w') as configfile:
self.config.write(configfile)
return self.read_section(section)
except CustomException as e:
raise e
except Exception as e:
logger.error("Error in write_section:"+str(e))
raise CustomException()

0
apphub/tests/README.md Executable file
View file

View file

@ -1,23 +0,0 @@
from fastapi import APIRouter, Query
from src.schemas.appSettings import AppSettings
from src.schemas.errorResponse import ErrorResponse
from src.services.settings_manager import SettingsManager
router = APIRouter()
@router.get("/settings",
summary="Get settings",
description="Get settings",
responses={
200: {"model": AppSettings},
400: {"model": ErrorResponse},
500: {"model": ErrorResponse},
}
)
def get_settings():
return SettingsManager().read_all()
@router.put("/settings")
def update_settings():
return {"settings": "settings"}

View file

@ -1,34 +0,0 @@
import os
import configparser
from typing import Dict
from src.core.exception import CustomException
from src.core.logger import logger
from src.schemas.appSettings import AppSettings
class SettingsManager:
def __init__(self):
script_dir = os.path.dirname(os.path.realpath(__file__))
config_dir = os.path.join(script_dir, "../config")
self.config_file_path = os.path.join(config_dir, "config.ini")
self.config_file_path = os.path.abspath(self.config_file_path)
self.config = configparser.ConfigParser()
def read_all(self) -> Dict[str, Dict[str, str]]:
try:
self.config.read(self.config_file_path)
data = {s:dict(self.config.items(s)) for s in self.config.sections()}
return AppSettings(**data)
except Exception as e:
logger.error(e)
raise CustomException()
def write_all(self, data: AppSettings):
for section, kv in data.model_dump().items():
if section not in self.config.sections():
self.config.add_section(section)
for key, value in kv.items():
self.config.set(section, key, value)
with open(self.filename, 'w') as configfile:
self.config.write(configfile)