04-25 周四 FastBuild重构实践
时间 | 版本 | 修改人 | 描述 |
---|---|---|---|
04-25 | V0.1 | 宋全恒 | 新建文档 |
2024年5月6日14:33:16 | V1.0 | 宋全恒 | 完成文档撰写 |
简介
由于 04-22 周日 阿里云-瑶光上部署FastBuild过程(配置TLS、自定义辅助命令)描述了重新部署一个FastBuild实例的过程,通过阅读这个,可以看到部署一个FastBuild的实例是非常复杂的,之前的两次部署,直接让我花费了将近10个小时,太痛苦了。因此优化就成了必须要进行的,因为我也是一个有完美主义倾向的程序猿。
问题分析-依赖
因为它有如下的依赖:
- Docker 服务器配置(启用TLS的话,还需要对TLS服务进行启动,在某些部署时,不需要启动TLS,但由于代码写死,不具有灵活性,在不需要启动TLS的环境下,也需要启用了TLS的Docker服务)
- FastBuild运行时,需要tools和source以及tls信息。而且这些信息无法再运行后配置,必须运行之前准备。但一旦外部环境变化,重新部署,需要重新更新配置文件。因此提前准备配置文件无法一劳永逸。因为镜像启动以及挂载这些路径由上层应用决定,因此在运行时未添加判断需要的目录是否存在的问题,导致问题定位非常不便。
- 瑶光镜像判断问题,在代码中写死
- 还有一个很痛苦的点,就是,每次部署一个新的环境,竟然就需要开辟一个新的分支,然后将配置写入配置文件,提交commit,这几乎是无法忍受的,更灵活的配置,应该就是一个分支足够了,在不同的环境下运行,在运行后配置一下即可。不然就有太多机械的,无意义的工作存在。
- 还有一个痛苦的点,就是在镜像构建完成之后,回调外部提供的接口,由于之前的代码写死了header,导致在第二次部署时扯皮比较多,而且日志打印的也少。
注:所有的写死,都是自己坑自己。切记这个教训。
将依赖可配置
这是本次重构的核心思想,
将所有的外部依赖进行可配置
即先保证FastBuild不需要任何的外部依赖,而可以运行,然后在运行时通过接口一次诸如全部依赖配置。沿着这个思想,我们就需要把所有的配置从配置文件移动到库中了。
主要问题
- 容器服务器ip问题
- 挂载目录存在问题
- Docker服务配置问题
- TLS的问题[]自动切换
- 回调接口问题
- 日志打印问题
- 瑶光镜像判断
构建镜像测试请求
json
{"webSSHSecret": "qkrhxQmWOe5kvpJpplvTuQ==", "jupyterLabSecret": "qkrhxQmWOe5kvpJpplvTuQ==", "task_data": {"task_name": "10.101.12.128-songquanheng@zhejianglab.com-1714116639", "target_image_name": "10.101.12.128/songquanheng-zhejianglab.com/ubuntu:sqh-18.04", "callback_url": "http://alkaidos.cn/api/app/dros-ic-platform/harbor/image/callback"}, "dockerfile_json": {"base_image": "harbor.alkaidos.cn/base/ubuntu:18.04", "maintainer": "1597398607723978754", "image_installer_config": {"python_env": {"present": "", "update": false, "target": "", "install_loc": "/usr/local/dros/python"}, "pip_installer_config": {"installer_name": "pip", "install": {"present": "", "update": false, "target": "", "install_loc": ""}, "source": {"installer_name": "", "type": "", "file_name": ""}, "software_list": [], "delimiter": "", "python_version": []}, "package_manager_installer_config": {"installer_name": "apt", "install": {"present": "apt 1.6.14 (amd64)", "update": false, "target": "", "install_loc": ""}, "source": {"installer_name": "", "type": "ali", "file_name": "ubuntu-18.04.list"}, "software_list": [], "delimiter": "", "python_version": []}, "conda_installer_config": {"installer_name": "conda", "install": {"present": "", "update": false, "target": "", "install_loc": ""}, "source": {"installer_name": "", "type": "", "file_name": ""}, "software_list": [], "delimiter": "", "python_version": []}, "webSSHSecret": "", "jupyterLabSecret": ""}}}
解决
TLS、Docker、Harbor、Host依赖
问题分析
容器IP问题
之前是放在配置文件中的,现在通过接口传入
分析关于IP问题
可以看到系统可以只依赖端口,可以将Host放置在数据库中
新增数据库表fb_host_table,保存ip,端口等信息。
增加python的数据库服务DBHostService,同时增加host_controller.py,host_controller.py,同时在main.py中引入这个router。
其中host_controller中包含主机信息的查询和新建
tls信息使用
python
fb_tls_config = Configuration.fb_tls_config()
remote_docker = Configuration.remote_docker()
而在ImageUtils类中,这是普通成员变量
python
class ImageUtils:
"""
镜像工具包,用于对镜像进行检测,处理启动容器,执行语句,构建镜像
"""
# docker sdk中上层的api,需要使用远端的docker server执行镜像构建
tls_config = TLSConfig(
client_cert=(fb_tls_config.client_cert_path, fb_tls_config.client_key_path),
ca_cert=fb_tls_config.ca_path,
verify=True
)
docker_client = docker.DockerClient(base_url=remote_docker.get_base_url(), tls=tls_config)
# api_client docker sdk进行原始的接口调用,主要用来进行inspect_image进行调用获取镜像元数据
api_client = docker.APIClient(base_url=remote_docker.get_base_url(), tls=tls_config)
从上述的代码看,docker_client和api_client这两个变量时关键,登录的方式
bash
docker_client = docker.DockerClient(base_url=remote_docker.get_base_url(), tls=tls_config)
# api_client docker sdk进行原始的接口调用,主要用来进行inspect_image进行调用获取镜像元数据
api_client = docker.APIClient(base_url=remote_docker.get_base_url(), tls=tls_config)
docker_client.login(username=harbor_config.username, password=harbor_config.password,
registry=harbor_config.registry)
api_client.login(username=harbor_config.username, password=harbor_config.password, registry=harbor_config.registry)
api_client执行路如下的工作:
而docker_client用于启动容器和获取镜像信息
而Harbor仓库主要有三个配置,其实有4个,就是harbor的域名。
ini
[harbor]
username = robot$algorithm
password = 1rhkIx3ufBI37tvkQ6kwDx7mRYcZpFCB
registry = 10.101.12.128
remote_docker一共六个配置项,tls是否启用也是一个配置项
ini
[tls]
client_cert_path = /mnt/self-define/meizhewei/fastbuild/tls/cert-jenkins.pem
client_key_path = /mnt/self-define/meizhewei/fastbuild/tls/key-jenkins.pem
ca_path = /mnt/self-define/meizhewei/fastbuild/tls/ca-jenkins.pem
[remote-docker]
# 记录远端docker server的host:port
host = 10.101.12.122
port = 2375
将Harbor信息拷贝到数据库中
Docker Server 信息拷贝到数据库中
注,主要是UploadFile,Form花费了较多的时间。
python
@router.post("/update-docker-server")
async def update_docker_server_config(host: str = Form(), port: int = Form(), tls_tar_file: UploadFile = File(None)):
if not validate_host(host):
return Response.error(f"请输入有效的ip或者域名,参数host: {host}")
new_docker_server = DBDockerServer(
host=host,
port=port,
tls_verify=False
)
if tls_tar_file:
tls_folder_name = get_ip_address_folder(host)
target_dir = "/mnt/nas_self-define/fastbuild/tls"
tls_dir = save_and_extract_tar(tls_tar_file, target_dir, tls_folder_name)
tls_files = ["ca-jenkins.pem", "cert-jenkins.pem", "key-jenkins.pem"]
if not all(file in get_files_in_directory(tls_dir) for file in tls_files):
return Response.error(f"请上传正确的tls文件,当前上传的文件为{tls_tar_file.filename},解压后不包含{' '.join(tls_files)}")
new_docker_server.tls_verify = True
new_docker_server.client_cert_path = os.path.join(tls_dir, "cert-jenkins.pem")
new_docker_server.ca_path = os.path.join(tls_dir, "ca-jenkins.pem")
new_docker_server.client_key_path = os.path.join(tls_dir, "key-jenkins.pem")
print(f"插入一条新的Docker server配置, {new_docker_server}")
DBDockerServerService.save(new_docker_server)
return Response.success(msg=f"插入docker_server信息{new_docker_server}")
采用的方式是一直插入,取最新的,这样比较简单。
防御代码-重构ImageUtils
怎么防止在未进行配置时调用ImageUtils对象呢?
这也是一个不太优雅的地方,就是在FastBuild的关键几处采用了相同的防御代码。
防御代码的目的是确保在使用FastBuild进行镜像构建的时候,已经完成了Docker、Harbor、host的配置
镜像构建
python
@router.post("/build-image")
async def build_image(image_request: ImageRequest):
"""
镜像构建接口,用于根据用户选择的需求配置,生成dockerfile,构建镜像,推送到harbor仓库。
:param image_request 任务构建请求
:return:
"""
host = DBHostService.query_latest_host()
if host.not_set():
return Response.error("要使用FastBuild服务构建镜像,请先配置FastBuild容器服务所在的宿主机")
harbor = DBHarborService.query_latest_harbor()
if not harbor:
return Response.error("在使用FastBuild时,请先配置Harbor仓库")
docker_server = DBDockerServerService.query_latest_docker_server()
if not docker_server:
return Response.error("在使用FastBuild时,请先配置Docker Server")
...
拉取镜像
python
@router.post("/pull-image")
async def pull_image(image_name: str):
harbor = DBHarborService.query_latest_harbor()
if not harbor:
return Response.error("在使用FastBuild时,请先配置Harbor仓库")
docker_server = DBDockerServerService.query_latest_docker_server()
if not docker_server:
return Response.error("在使用FastBuild时,请先配置Docker Server")
...
检查对象
python
@router.post("/check-image")
async def check_image(image_name: str):
harbor = DBHarborService.query_latest_harbor()
if not harbor:
return Response.error("在使用FastBuild时,请先配置Harbor仓库")
docker_server = DBDockerServerService.query_latest_docker_server()
if not docker_server:
return Response.error("在使用FastBuild时,请先配置Docker Server")
...
开始的情况
起初配置文件如下所示: 我们的目标是删除其中的[tls]、[remote-docker]、[harbor]
ini
[fb]
# 系纾_湾P彉~@作¨潛®弾U, 佅¶中1级潛®弾U表示湾P潚~D类佞~KL奾B轘¿轇~LaliL 缾Q彘~S(163), 淾E位~N(qinghua)
source_dir = /mnt/nas_self-define/meizhewei/fastbuild/source
# 轕~\佃~O彞~D建任佊¡庠¹潛®弾UL佅¶中任佊¡潛®弾U侾]嬾X乾FDockerfile以住~J轜~@襾A潚~D轕~\佃~O彞~D建彝~P彖~Y
task_dir = /mnt/nas_self-define/meizhewei/fastbuild/task
# 孾I袾E余¨彉~@作¨潛®弾UL pip⽀~Aconda⽀~Apython佝~G伾M乾N佅¶中⽀~B佅¶中pip中住~H佈~F为pip2佒~Lpip3潛®弾U
tools_dir = /mnt/nas_self-define/meizhewei/fastbuild/tools
# FB彉~@作¨潚~D主彜º
host = 10.101.12.88
# FB彉~@位| 潔¨潚~D端住£
port = 48001
[db]
file = sqlite:mnt/nas_self-define/meizhewei/fastbuild/database/fb-prod.db
[tls]
client_cert_path = /mnt/nas_self-define/meizhewei/fastbuild/tls/cert-jenkins.pem
client_key_path = /mnt/nas_self-define/meizhewei/fastbuild/tls/key-jenkins.pem
ca_path = /mnt/nas_self-define/meizhewei/fastbuild/tls/ca-jenkins.pem
[callback]
# 记弾U乾F轕~\佃~O彞~D建襾A䷾J彊¥潚~D主彜º端住£信彁¯L轇~G潔¨HTTP位~O议
host = 10.101.12.120
port = 40096
[remote-docker]
# 记弾U达\端docker server潚~Dhost:port
host = 10.101.12.122
port = 2375
[aes]
# 潔¨乾NAES佊| 宾F潚~Dkey
key = c7e71f37dda040fd
# 潔¨乾NAES佊| 宾F潚~D佁~O移轇~O设置
iv = 0000000000000000
[harbor]
username = robot$algorithm
password = 1rhkIx3ufBI37tvkQ6kwDx7mRYcZpFCB
registry = 10.101.12.128
实践-配置FastBuild并重构ImageUtils
建表
基本的过程是设置了三个表:
如fb_harbor表定义如下:
python
#!/usr/bin/env python
# -*- coding:UTF-8 -*-
"""
@author: songquanheng
@email: wannachan@outlook.com
@time: 2024年4月25日11:21:38
@desc: 用于存储Harbor相关的信息
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime
from db.db_element import Base
class DBHarbor(Base):
"""表示运行作业表,其对应slurm中正在运行的作业"""
__tablename__ = 'fb_harbor_table'
primary_id = Column(Integer, primary_key=True)
"""记录创建时间"""
create_time = Column(DateTime, nullable=False, default=datetime.now)
"""更新时间"""
update_time = Column(DateTime(timezone=True), default=datetime.now, onupdate=datetime.now, comment="修改时间")
"""用户名"""
username = Column(String)
"""harbor密码"""
password = Column(String)
"""harbor的仓库地址"""
registry = Column(String)
registry_dns = Column(String)
def __repr__(self):
return f"<DBHarbor(primary_id={self.primary_id}, harbor username={self.username}, " \
f"password={self.password}, registry={self.registry}, registry_dns={self.registry_dns})>"
def get_harbor_config_dict(self):
return {"username": self.username, "password": self.password, "registry": self.registry}
可以看出,在上述的代码中,引入了registry_dins,这主要是为了瑶光镜像判断,harbor的域名也可以作为镜像名称的准备。
表操纵的接口
依然以DBHarborService为例:
Python
#!/usr/bin/env python
# -*- coding:UTF-8 -*-
"""
@author: songquanheng
@email: wannachan@outlook.com
@time: 2024年4月25日11:22:09
@desc: 用于对Harbor仓库的信息进行更改
"""
from typing import List
from db.db_element import Session
from db.db_harbor import DBHarbor
class DBHarborService:
"""镜像构建任务存储服务"""
@staticmethod
def query_all() -> List[DBHarbor]:
with Session() as session:
return session.query(DBHarbor) \
.order_by(DBHarbor.update_time.desc()) \
.all()
@staticmethod
def save(harbor: DBHarbor) -> DBHarbor:
"""新增或者更新一组集群信息"""
with Session() as session:
session.add(harbor)
session.commit()
return harbor
@staticmethod
def query_latest_harbor() -> DBHarbor:
with Session() as session:
return session.query(DBHarbor) \
.order_by(DBHarbor.create_time.desc()) \
.first()
可以看到上述,最主要的事query_latest_harbor,按照数据字段进行降序配列,并取第一个为当前有效的。并且,没有更新某个条目,而是不断地插入。
重构ImageUtils
这部分的重构是很关键的,因为之前是通过读取文件生成了单例的配置对象,而现在要读取数据库中的配置。在实现时,是将Docker的配置以及Harbor的配置传入了ImageUtils类
这样修改之后,构造器方法变得复杂了
python
def __init__(self, docker_server: DBDockerServer, harbor: DBHarbor) -> None:
"""
:rtype: object
"""
self.docker_server = docker_server
self.harbor = harbor
# docker sdk中上层的api,需要使用远端的docker server执行镜像构建
if docker_server.tls_verify:
tls_config = TLSConfig(
client_cert=(docker_server.client_cert_path, docker_server.client_key_path),
ca_cert=docker_server.ca_path,
verify=True
)
self.docker_client = docker.DockerClient(base_url=docker_server.get_base_url(), tls=tls_config)
self.api_client = docker.APIClient(base_url=docker_server.get_base_url(), tls=tls_config)
else:
self.docker_client = docker.DockerClient(base_url=docker_server.get_base_url())
self.api_client = docker.APIClient(base_url=docker_server.get_base_url())
self.docker_client.login(username=harbor.username, password=harbor.password, registry=harbor.registry)
self.api_client.login(username=harbor.username, password=harbor.password, registry=harbor.registry)
super().__init__()
这样的重构,逻辑上是没有问题的, 但是楼主在运行时发现了,在Task类重构,即将一个实例注入到了Task类中,进行构建镜像的时候,如果仍然使用同一个image_utils对象的时候,会报错,具体原因还不知道
如上图所示,在构建镜像的时候,楼主重新实例化了一个局部的image_utils对象。这样没有了报错
[FIX]不清楚为什么要在构建的时候,使用新建的image_utils对象,而不能是self.image_utils,新建之后没有报AttributeError: 'APIclient' object has no attribute '_proxy_configs'错
一键配置config-fastbuild接口
通过接口一键完成FastBuild的更新,并且在配置更新时,验证了提供的Docker和Harbor信息的有效性。
关于一键配置接口,可以参见 04-28 周日 FastAPI Post请求同时传递文件和普通参数
python
@router.post("/config-fastbuild")
async def update_docker_server_config_in_object(
fastbuild_host: str = Form(), fastbuild_port: int = Form(default=48001),
harbor_username: str = Form(), harbor_password: str = Form(), harbor_registry: str = Form(),
harbor_registry_dns: str = Form(default=''),
docker_host: str = Form(), docker_port: int = Form(default=2375), docker_tls_tar_file: UploadFile = File(None)):
print("fastbuild: ", fastbuild_host, fastbuild_port)
print("harbor: ", harbor_username, harbor_password, harbor_registry, harbor_registry_dns)
if not all(map(validate_host, [fastbuild_host, harbor_registry, harbor_registry_dns, docker_host])):
return Response.error(data="fastbuild_host, harbor_registry_host, harbor_registry_dns, docker_host均应为有效的ip或者域名")
db_host = DBHost(host_ip=fastbuild_host, host_port=fastbuild_port)
db_harbor = DBHarbor(username=harbor_username, password=harbor_password, registry=harbor_registry,
registry_dns=harbor_registry_dns)
db_docker = DBDockerServer(host=docker_host, port=docker_port, tls_verify=False)
if docker_tls_tar_file:
tls_folder_name = get_ip_address_folder(db_docker.host)
tls_dir = save_and_extract_tar(docker_tls_tar_file, system_config.get_tls_dir(), tls_folder_name)
tls_files = ["ca-jenkins.pem", "cert-jenkins.pem", "key-jenkins.pem"]
if not all(file in get_files_in_directory(tls_dir) for file in tls_files):
return Response.error(f"请上传正确的tls文件,当前上传的文件为{docker_tls_tar_file.filename},解压后不包含{' '.join(tls_files)}")
db_docker.tls_verify = True
db_docker.client_cert_path = os.path.join(tls_dir, "cert-jenkins.pem")
db_docker.ca_path = os.path.join(tls_dir, "ca-jenkins.pem")
db_docker.client_key_path = os.path.join(tls_dir, "key-jenkins.pem")
try:
image_utils = ImageUtils(db_docker, db_harbor)
except DockerException as exe:
print(f"发生异常: {exe}")
raise FBException(code=123, message=f"使用提供的docker和harbor信息,进行登录测试,测试失败,请检查,错误信息为{str(exe)}")
DBHostService.save(db_host)
DBHarborService.save(db_harbor)
DBDockerServerService.save(db_docker)
return Response.success(data="成功完成为FastBuild配置需要的宿主机信息,Docker信息以及Harbor信息")
使用Controller接口config-fastbuild完成这些信息的更新
目录挂载问题
在main.py中,直接判断必要的目录是否存在,不存在直接报错
python
if __name__ == '__main__':
print("FastBuild 启动ing")
necessary_paths = [system_config.get_source_dir(), system_config.get_tools_dir(), system_config.get_task_dir()
]
if not all(os.path.exists(path) for path in necessary_paths):
print(f"在FastBuild运行前,请首先保证目录存在: source_dir: {system_config.get_source_dir()}用于保存安装器源文件, "
f"tools_dir: {system_config.get_tools_dir()}用于保存安装器工具文件")
print("由于目录验证失败,本次运行失败,FastBuild即将推出")
sys.exit(1)
db_host = DBHostService.query_latest_host()
db_harbor = DBHarborService.query_latest_harbor()
db_docker = DBDockerServerService.query_latest_docker_server()
if all([db_host, db_harbor, db_docker]):
print("FastBuild系统已经正确配置")
print(f"FastBuild主机环境为: {db_host.host_ip} 端口: {db_host.host_port}")
print(f"FastBuild镜像仓库环境为: {db_harbor}")
print(f"FastBuild 镜像构建Docker环境为: {db_docker}")
else:
print("FastBuild系统尚未配置,请先调用/api/fast-build/config/config-fastbuild 配置FastBuild系统")
add_default_host()
print(f"查看激活配置文件: {get_config_file()}")
uvicorn.run(app='main:app', host=host_config.host,
port=int(host_config.port), reload=True, debug=True)
进一步使用搬移函数,将代码重构成如下的内容:
python
if __name__ == '__main__':
print("FastBuild 启动ing")
if not system_config.necessary_dirs_exist():
print(f"在FastBuild运行前,请首先保证目录存在: source_dir: {system_config.get_source_dir()}用于保存安装器源文件, "
f"tools_dir: {system_config.get_tools_dir()}用于保存安装器工具文件")
print("由于目录验证失败,本次运行失败,FastBuild即将推出")
sys.exit(1)
add_default_host()
add_default_header()
print_env_configuration()
print(f"查看激活配置文件: {get_config_file()}")
uvicorn.run(app='main:app', host=host_config.host,
port=int(host_config.port), reload=True, debug=True)
通过代码函数名提升可读性,也更加合理。至此完成了TLS的重构,而配置文件变成了如下的样子
ini
[fb]
# 系统源所在目录, 其中1级目录表示源的类型,如阿里ali, 网易(163), 清华(qinghua)
source_dir = /mnt/nas_self-define/meizhewei/fastbuild/source
# 镜像构建任务根目录,其中任务目录保存了Dockerfile以及需要的镜像构建材料
task_dir = /mnt/nas_self-define/meizhewei/fastbuild/task
# 安装器所在目录, pip、conda、python均位于其中。其中pip中又分为pip2和pip3目录
tools_dir = /mnt/nas_self-define/meizhewei/fastbuild/tools
tls_dir = /mnt/nas_self-define/meizhewei/fastbuild/tls
# FB所在的主机
host = 0.0.0.0
# FB所占用的端口
port = 48001
[callback]
headers = {"X-BizType": "DROS", "X-Login-UserId": "1", "Content-Type": "application/json"}
[db]
file = sqlite:mnt/nas_self-define/meizhewei/fastbuild/database/fb-prod.db
[aes]
# 用于AES加密的key
key = c7e71f37dda040fd
# 用于AES加密的偏移量设置
iv = 0000000000000000
太多的重复的路径,抽取basic_dir
回调测试信息
sqlite json数据存储
这个是和回调测试这个需求一样的,就是在数据库中需要保存字典,因此同样的就是建表,
建表
python
#!/usr/bin/env python
# -*- coding:UTF-8 -*-
"""
@author: songquanheng
@email: wannachan@outlook.com
@time: 2024年4月25日11:21:38
@desc: 保存FastBuild与上层服务的所有回调的信息,包括url,header,结果
"""
from datetime import datetime
from sqlalchemy import Column, Integer, String, DateTime, JSON
from db.db_element import Base
class DBCallback(Base):
"""表示每次回调的信息,包括回调的url,回调的header以及回调的结果"""
__tablename__ = 'fb_callback_table'
primary_id = Column(Integer, primary_key=True)
"""记录创建时间"""
create_time = Column(DateTime, nullable=False, default=datetime.now)
"""更新时间"""
update_time = Column(DateTime(timezone=True), default=datetime.now, onupdate=datetime.now, comment="修改时间")
url = Column(String)
headers = Column(JSON)
state = Column(JSON)
result = Column(String)
def __repr__(self):
return f"<DBCallback(url='{self.url}', headers='{self.headers}', result={self.result}"
可以看到headers和state,均指定了JSON格式的数据库字段类型。
表操作API
此处需要注意的query_all在获取所有的回调结果时,按照update_time排序降序。如果要更新headers,则可以调用update_latest_callback_headers,但这其实破坏了数据的完整性,暂时也没什么重要的,先这样实现的。未来则可以进一步的新建一个数据,主要是为了快速实现所以这样做的。
python
#!/usr/bin/env python
# -*- coding:UTF-8 -*-
"""
@author: songquanheng
@email: wannachan@outlook.com
@time: 2024年4月29日14:12:03
@desc: 回调记录管理
"""
from typing import List
from db.db_callback import DBCallback
from db.db_element import Session
class DBCallbackService:
@staticmethod
def query_all() -> List[DBCallback]:
with Session() as session:
return session.query(DBCallback) \
.order_by(DBCallback.update_time.desc()) \
.all()
@staticmethod
def save(callback: DBCallback) -> DBCallback:
"""或添加一条DockerServer数据"""
with Session() as session:
session.add(callback)
session.commit()
return callback
@staticmethod
def query_latest_callback() -> DBCallback:
with Session() as session:
return session.query(DBCallback) \
.order_by(DBCallback.update_time.desc()) \
.first()
@staticmethod
def update_latest_callback_headers(headers: dict):
with Session() as session:
callback: DBCallback = session.query(DBCallback) \
.order_by(DBCallback.update_time.desc()) \
.first()
callback.headers = headers
session.commit()
保存和更新headers
保存指的是在main.py运行时,将默认的headers插入到数据库中,或者在FastBuild运行后,更新headers
python
def add_default_header():
"""添加默认请求头"""
callback = DBCallbackService.query_latest_callback()
if callback is not None:
return
DBCallbackService.save(DBCallback(headers=callback_config.header))
python
@router.post("/update-headers")
async def update_harbor_config(headers: dict):
DBCallbackService.update_latest_callback_headers(headers=headers)
return Response.success(msg="完成回调请求时需要的的headers信息的更新")
同时,提供了测试回调的接口,方便直接测试回调的结果进行打印。这主要是回调时和外部系统交互,防止扯皮而开发的接口,比较直观的定位问题。
python
@router.post("/test-callback")
async def test_callback():
callback = DBCallbackService.query_latest_callback()
if not callback.url:
return Response.error(msg="当前还没有回调记录,因此无法获取真实有效的回调地址")
header = callback.headers
url = callback.url
state = callback.state
result = CallBackService.state_upload(url=url, state=state, headers=header)
return Response.success(msg="使用系统保留的最后一次回调记录测试回调请求", data=result)
Python全局捕获异常
捕获FBException
在main.py中,配置了该代码,这样系统发生FBException会转移到这个地方
python
# 全局异常处理中间件
@app.exception_handler(FBException)
async def http_exception_handler(request: Request, exc: FBException):
print(f"request_url: {request.url}")
return JSONResponse(
status_code=500,
content={"message": f"{exc.message}", "code": f"{exc.code}"}
)
在config-fastbuid时可以触发该异常:
python
try:
image_utils = ImageUtils(db_docker, db_harbor)
except DockerException as exe:
print(f"发生异常: {exe}")
raise FBException(code=123, message=f"使用提供的docker和harbor信息,进行登录测试,测试失败,请检查,错误信息为{str(exe)}")
注:当前不确定是只有在Controller层中出现的异常可以被全局捕获异常捕获,还是所有的异常都可以被FastBuild捕获。
注:已经验证了,全局捕获异常好像只有在Controler中的异常可以被有效捕获并返回给客户端,系统中触发的异常,无法有效的捕获。
动态导入
动态导入,是这次重构的一个意外的行为,这是因为自己重新添加了三个controller,结果呢,需要在main.py中将这三个controller依次写入,就非常尴尬,就想着是否可以自动注入。因此搜索之后,通过代码进行解决
注,这要求将所有的controller放在同一个包中。
在main.py中添加如下的代码:
python
# 定义一个动态导入控制器的函数
def include_all_routers():
controllers_package = 'controller'
# 获取 controller 包的路径
package = importlib.import_module(controllers_package)
path = package.__path__
# 迭代 controller 包下的所有模块
for _, module_name, _ in pkgutil.iter_modules(path):
module = importlib.import_module(f"{controllers_package}.{module_name}")
if hasattr(module, 'router'):
app.include_router(getattr(module, 'router'))
# 调用函数动态包含所有路由器
include_all_routers()
这样之后的效果就是,我们在controller中新增加了controller接口,系统可以自动识别。
commit一览
bash
[Fix]由于代码重构修改了返回值,而页面在调用构建列表时需要使用task_id,因此重新再构建任务的返回信息中添加了该任务id的信息 songquanheng 2024/4/29 17:17
[MOD]使用importlib自动导入controller下的各个router songquanheng 2024/4/29 16:28
[MOD]将上述数据库查询服务获取全部结果query_all均按照更新时间进行排序 songquanheng 2024/4/29 15:53
[Fix]由于之前的重构调整了state_upload两个参数的顺序,将外部调用的顺序也进行调整。 songquanheng 2024/4/29 15:47
[ADD]完成回调逻辑的添加 songquanheng 2024/4/29 15:40
[MOD]完成默认的headers存入数据库,并且在main.py运行时,从配置文件中读取默认的headers songquanheng 2024/4/29 15:29
[MOD]调整main.py中包含顺序router的顺序,在swagger上以字母顺序展示router songquanheng 2024/4/29 15:11
[MOD]重构函数,将必要的目录存在necessary_dirs_exist()移动到SystemConfig类中 songquanheng 2024/4/29 10:15
[MOD]暂时维持配置文件不变,不引入新的问题 songquanheng 2024/4/28 18:15
[MOD]完成对于FastBuild的配置,发现Bug,将基于harbor和docker的登录验证放入了if之后了。测试登录应该在外层代码中 songquanheng 2024/4/28 18:08
[ADD]在配置文件中添加tls_dir配置,这样在程序使用tls目录时,可以读取 songquanheng 2024/4/28 17:42
[ADD]完成接口config-fastbuild的添加,通过一个接口完成docker、harbor、fastbuild信息的配置 songquanheng 2024/4/28 17:08
[ADD]添加全局捕获异常,并且在反馈给页面的时候获取异常信息提示 songquanheng 2024/4/28 17:03
[FIX]由于引用了PyDantic的类,因此在响应构建任务时,直接返回,重复序列化会报错 songquanheng 2024/4/26 20:53
[FIX]不清楚为什么要在构建的时候,使用新建的image_utils对象,而不能是self.image_utils,新建之后没有报AttributeError: 'APIclient' object has no attribute '_proxy_configs'错 songquanheng 2024/4/26 20:37
[MOD]由于遇到问题,暂时将代码中tls_config的配置移动出来 songquanheng 2024/4/26 19:30
[MOD]修改,因为在DBHost类中的字段名为host_port songquanheng 2024/4/26 18:54
[MOD]更新,优化更新DockerServer时的配置接口响应信息 songquanheng 2024/4/26 17:20
[MOD]由于后增加了registry_dns,添加对于该字段的支持 songquanheng 2024/4/26 17:03
[Fix]将删除多了的db_config重新添加到文件中 songquanheng 2024/4/26 16:44
[DEL]移除不在需要的harbor_config对象,以及HostConfig中的get_static_resource_prefix函数,HarborConfig类中的get_harbor_config_dict songquanheng 2024/4/26 16:27
[MOD]重构代码,将静态资源修改为从数据库中获取,同时在开始创建惊险构建任务时,必须保证已经正确配置了FastBuild服务所在的宿主机IP信息 songquanheng 2024/4/26 16:23
[ADD]添加在FastBuild中想要使用服务,必须首先配置容器所在的宿主机IP地址 songquanheng 2024/4/26 16:06
[DEL]移除配置文件中的[harbor]、[remote-docker]、[tls]项 songquanheng 2024/4/26 15:23
[Refactor]由于Harbor的配置位于数据库中,因此读取Harbor配置,并且对于is_alkaid_image的判断逻辑进行修改,同时将docker_server和harbor作为ImageUtils的普通成员,并移除从普通文件中获取remote_docker和harbor的代码 songquanheng 2024/4/26 15:03
[ADD]为DBHarbor添加字段registry_dns,用来存储harbor的域名 songquanheng 2024/4/26 14:53
[ADD]添加DockerServer和Harbor配置进入数据库的功能,其他位置采用防御代码,系统必须首先正确配置DockerServer和Harbor,FastBuild才能正常的工作 songquanheng 2024/4/26 14:41
[MOD]修改代码格式,移除无用的注释代码 songquanheng 2024/4/26 13:47
[ADD]添加DockerServer相关的数据结构和控制接口,在传递文件的时候,表明启用TLS songquanheng 2024/4/26 11:22
[ADD]添加辅助函数,获取目录下的文件列表,另外判断IP是否有效,以及根据IP获取相应的目录名,以保存tls文件 songquanheng 2024/4/26 11:21
[ADD]将Harbor的配置信息移入数据库,不再配置文件中进行。 songquanheng 2024/4/25 16:59
[MOD]当初始运行时,如果发现host信息不为空,则避免重新插入默认的主机信息0.0.0.0 songquanheng 2024/4/25 16:49
[MOD]由于函数名为update_host,因此,不再每次都添加一条主机信息的记录 songquanheng 2024/4/25 15:19
[ADD]添加config_controller,用来统一对系统进行配置和查看 songquanheng 2024/4/25 14:58
[MOD]将配置文件中的[fb]下的host从ip修改为0.0.0.0,在程序运行之后修改主机配置信息,当前仅允许主机IP进行修改,并且更新会重新插入一条主机信息 songquanheng 2024/4/25 14:08
[DEL]由于回调地址由上层确定,因此配置文件中的callback项,不再需要,因此移除 songquanheng 2024/4/24 15:08
[MOD]在回调服务的时候,打印入参,出参,url方便问题的定位 songquanheng 2024/4/24 14:53
[MOD]添加日志打印,方便问题排查 songquanheng 2024/4/22 17:18
[MOD]修改FastBuild容器所在的宿主机主机ip为43 songquanheng 2024/4/22 15:33
[MOD]为阿里云-瑶光添加相应的配置 songquanheng 2024/4/22 15:06
[Fix]在镜像拉取和推送过程中,支持镜像名称和目标镜像名称两个英文: 即172.27.213.154:30003/base/ubuntu:v3的支持。同时在is_alkaid_image中添加云栖的ip前缀支持 songquanheng 2024/3/14 15:30
[MOD]为云栖工程院阿里云部署配置远程docker,本服务所在主机IP,Harbor仓库用户名密码 songquanheng 2024/3/8 14:23
总结
这次代码重构,其实还是挺不错的体验,一共花费了四天的时间,这样就将FastBuild推到了一个新的状态,依赖较少,这样我觉得下次需要重新部署的时候,可以很方便的,因为FastBuild均可以通过接口进行配置了。
经过重构之后的FastBuild取得了如下的效果:
- Docker、Habor、Host的信息从配置文件中移动到数据库中,可以在运行后灵活的更新,并且支持TLS是否开启的灵活配置
- 使用Jenkins打包时,仅仅需要同一个master分支即可,不需要重新新建分支,并更新各种配置。
- 回调的测试,可以在接口中实现查询回调结果。并且将header移动到数据库中,在实践的时候使用了sqlite的JSON数据结构,比较方便进行更新。
- 新增了controller了,可以自动导入,而不需要修改main.py
- 增加了全局捕获异常,因此对于Controller中的异常,可以比较灵活的返回给用户错误的提示信息。
- 瑶光镜像判断,也不用写死了,基于config-fastbuild的传入的Harbor的ip和域名进行判断,更加灵活。
- 目录挂载有效性判断,防止在没有有效挂载目录的情况下,没有日志打印的问题。
注: 言而总之,还是希望每个程序猿可以更多的关注重构,关于重构可以参考如下的文章: