04-25 周四 FastBuild重构实践-TLS、全局捕获异常、一键配置

news2024/10/5 13:01:00
04-25 周四 FastBuild重构实践
时间版本修改人描述
04-25V0.1宋全恒新建文档
2024年5月6日14:33:16V1.0宋全恒完成文档撰写

简介

 由于 04-22 周日 阿里云-瑶光上部署FastBuild过程(配置TLS、自定义辅助命令)描述了重新部署一个FastBuild实例的过程,通过阅读这个,可以看到部署一个FastBuild的实例是非常复杂的,之前的两次部署,直接让我花费了将近10个小时,太痛苦了。因此优化就成了必须要进行的,因为我也是一个有完美主义倾向的程序猿。

问题分析-依赖

因为它有如下的依赖:

  • Docker 服务器配置(启用TLS的话,还需要对TLS服务进行启动,在某些部署时,不需要启动TLS,但由于代码写死,不具有灵活性,在不需要启动TLS的环境下,也需要启用了TLS的Docker服务)
  • FastBuild运行时,需要tools和source以及tls信息。而且这些信息无法再运行后配置,必须运行之前准备。但一旦外部环境变化,重新部署,需要重新更新配置文件。因此提前准备配置文件无法一劳永逸。因为镜像启动以及挂载这些路径由上层应用决定,因此在运行时未添加判断需要的目录是否存在的问题,导致问题定位非常不便。
  • 瑶光镜像判断问题,在代码中写死
  • 还有一个很痛苦的点,就是,每次部署一个新的环境,竟然就需要开辟一个新的分支,然后将配置写入配置文件,提交commit,这几乎是无法忍受的,更灵活的配置,应该就是一个分支足够了,在不同的环境下运行,在运行后配置一下即可。不然就有太多机械的,无意义的工作存在。
  • 还有一个痛苦的点,就是在镜像构建完成之后,回调外部提供的接口,由于之前的代码写死了header,导致在第二次部署时扯皮比较多,而且日志打印的也少。

注:所有的写死,都是自己坑自己。切记这个教训。

image-20240506145629006

将依赖可配置

 这是本次重构的核心思想,

将所有的外部依赖进行可配置

 即先保证FastBuild不需要任何的外部依赖,而可以运行,然后在运行时通过接口一次诸如全部依赖配置。沿着这个思想,我们就需要把所有的配置从配置文件移动到库中了。

 主要问题

  • 容器服务器ip问题
  • 挂载目录存在问题
  • Docker服务配置问题
  • TLS的问题[]自动切换
  • 回调接口问题
  • 日志打印问题
  • 瑶光镜像判断

构建镜像测试请求

{"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问题

image-20240425110708890

 可以看到系统可以只依赖端口,可以将Host放置在数据库中

 新增数据库表fb_host_table,保存ip,端口等信息。

 增加python的数据库服务DBHostService,同时增加host_controller.py,host_controller.py,同时在main.py中引入这个router。

 其中host_controller中包含主机信息的查询和新建

tls信息使用
image-20240425152845738
fb_tls_config = Configuration.fb_tls_config()
remote_docker = Configuration.remote_docker()

 而在ImageUtils类中,这是普通成员变量

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这两个变量时关键,登录的方式

    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执行路如下的工作:

image-20240425153642855

 而docker_client用于启动容器和获取镜像信息

image-20240425153531758

 而Harbor仓库主要有三个配置,其实有4个,就是harbor的域名。

[harbor]
username = robot$algorithm
password = 1rhkIx3ufBI37tvkQ6kwDx7mRYcZpFCB
registry = 10.101.12.128

 remote_docker一共六个配置项,tls是否启用也是一个配置项

[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花费了较多的时间。

@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的配置

镜像构建
@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")
    ...
拉取镜像
@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")
    ...
检查对象
@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]

[fb]
# 系纾_湾P彉~@作¨潛®弾U, 佅¶中1级潛®弾U表示湾P潚~D类佞~K﻾L奾B轘¿轇~Lali﻾L 缾Q彘~S(163), 淾E位~N(qinghua)
source_dir = /mnt/nas_self-define/meizhewei/fastbuild/source
# 轕~\佃~O彞~D建任佊¡庠¹潛®弾U﻾L佅¶中任佊¡潛®弾U侾]嬾X乾FDockerfile以住~J轜~@襾A潚~D轕~\佃~O彞~D建彝~P彖~Y
task_dir = /mnt/nas_self-define/meizhewei/fastbuild/task
# 孾I袾E余¨彉~@作¨潛®弾U﻾L 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

建表

 基本的过程是设置了三个表:

image-20240506153627715

 如fb_harbor表定义如下:

#!/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为例:

#!/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类

image-20240506154529109

 这样修改之后,构造器方法变得复杂了

    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-20240506154901200

 如上图所示,在构建镜像的时候,楼主重新实例化了一个局部的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请求同时传递文件和普通参数

@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中,直接判断必要的目录是否存在,不存在直接报错

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)

 进一步使用搬移函数,将代码重构成如下的内容:

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的重构,而配置文件变成了如下的样子

[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数据存储

 这个是和回调测试这个需求一样的,就是在数据库中需要保存字典,因此同样的就是建表,

建表

#!/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,但这其实破坏了数据的完整性,暂时也没什么重要的,先这样实现的。未来则可以进一步的新建一个数据,主要是为了快速实现所以这样做的。

#!/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

def add_default_header():
    """添加默认请求头"""

    callback = DBCallbackService.query_latest_callback()
    if callback is not None:
        return
    DBCallbackService.save(DBCallback(headers=callback_config.header))
@router.post("/update-headers")
async def update_harbor_config(headers: dict):
    DBCallbackService.update_latest_callback_headers(headers=headers)
    return Response.success(msg="完成回调请求时需要的的headers信息的更新")

 同时,提供了测试回调的接口,方便直接测试回调的结果进行打印。这主要是回调时和外部系统交互,防止扯皮而开发的接口,比较直观的定位问题。

@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会转移到这个地方

# 全局异常处理中间件
@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时可以触发该异常:

    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依次写入,就非常尴尬,就想着是否可以自动注入。因此搜索之后,通过代码进行解决

image-20240506163151984

注,这要求将所有的controller放在同一个包中。

 在main.py中添加如下的代码:

# 定义一个动态导入控制器的函数
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一览

[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和域名进行判断,更加灵活。
  • 目录挂载有效性判断,防止在没有有效挂载目录的情况下,没有日志打印的问题。

注: 言而总之,还是希望每个程序猿可以更多的关注重构,关于重构可以参考如下的文章:

《重构 改善既有代码的设计》之重构,第一个案例详解

《重构 改善既有代码的设计》之重构原则

《重构 改善既有代码的设计》之代码的坏味道

《重构 改善既有代码的设计》之重构列表

Extract Method

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1647388.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ADS过孔---过孔建模自动化

当前快速建模的方法有两类&#xff1a;一是脚本自动化&#xff0c;也就是今天要分享的方法&#xff0c;但该方法需要工程师有基本的脚本编辑能力&#xff0c;然后根据自己的需要去修改&#xff0c;难度较大一点点&#xff1b;二是参数化建模&#xff0c;也就是在GUI界面输入相应…

基于 Spring Boot 博客系统开发(七)

基于 Spring Boot 博客系统开发&#xff08;七&#xff09; 本系统是简易的个人博客系统开发&#xff0c;为了更加熟练地掌握 SprIng Boot 框架及相关技术的使用。&#x1f33f;&#x1f33f;&#x1f33f; 基于 Spring Boot 博客系统开发&#xff08;六&#xff09;&#x1f…

远程连接是什么?

远程连接是指通过网络连接两个或多个设备&#xff0c;实现远程访问、控制或传输数据的技术。它在现代科技发展中起到了重要作用&#xff0c;使得我们可以随时随地与远程设备进行交互、管理和操作。 天联组网是一种高效的远程连接解决方案&#xff0c;它因为操作简单、跨平台应用…

算法(C++

题目&#xff1a;螺旋矩阵&#xff08;59. 螺旋矩阵 II - 力扣&#xff08;LeetCode&#xff09;&#xff09; 给你一个正整数 n &#xff0c;生成一个包含 1 到 n2 所有元素&#xff0c;且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。 示例 1&#xff1a; 输入&am…

IT养生知识之:子午流注

《子午流注口诀》 肺寅大卯胃辰宫&#xff0c; 脾巳心午小未中&#xff0c; 申膀酉肾心包戌&#xff0c; 亥焦子胆丑肝通。 何为子午流注&#xff1f; 子午流注是中医圣贤发现的一种规律&#xff0c;中医认为人体中十二条经脉对应着每日的十二个时辰&#xff0c;由于时辰在…

计算机网络4——网络层8 软件定义网络 SDN

文章目录 一、介绍1、简介2、原理3、案例1&#xff09;普通2&#xff09;负载均衡的例子3&#xff09;防火墙的例子 二、控制层面1、特征2、层次 一、介绍 1、简介 SDN的概念最初由斯坦福大学N.McKeown于2009年首先提出。当时还只是在学术界进行探讨的一种新的网络体系结构。…

springboot+mp自动生成没有实体类

mybatisX版本冲突问题 一开始我的MyBatisX版本是1.6.1-3,使用mybatis-plus一直不能正常生成实体类 将MyBatisX的版本换成了1.5.7就可以了 MyBatisX版本更换 1.将原有的MyBatisX卸载后重新安装一个新的版本 2.选择一个合适的版本,这里我选的是1.5.7 下载完成后自己选择一个…

css实现上下左右对勾选中状态角标

&#x1f365;左上角 &#x1f365;右上角 &#x1f365;左下角 &#x1f365;右下角: &#x1f365;左上角: .blueBackground {position: relative;border: 1px solid #91c7f3;background: #F0F8FF !important;&:after {content: "";position: absolute;top:…

【Java从入门到精通】Java 重写(Override)与重载(Overload)

重写(Override) 重写&#xff08;Override&#xff09;是指子类定义了一个与其父类中具有相同名称、参数列表和返回类型的方法&#xff0c;并且子类方法的实现覆盖了父类方法的实现。 即外壳不变&#xff0c;核心重写&#xff01; 重写的好处在于子类可以根据需要&#xff0c…

jmeter后置处理器提取到的参数因为换行符导致json解析错误

现象&#xff1a; {"message":"JSON parse error: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Ill…

FastAdmin实现后台菜单自定义方法

FastAdmin实现后台菜单自定义显示&#xff0c;默认显示的是编辑和删除操作&#xff0c;我们有的时候是需要增加一些功能性的按钮&#xff0c;例如审核&#xff0c;或者说更多的关联性的信息。那么我们就可以按如下的操作去做 上面是默认展示的功能图片&#xff0c;下面我来简单…

上海亚商投顾:沪指创年内新高 化工板块掀涨停潮

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 三大指数昨日高开震荡&#xff0c;沪指涨超1%续创年内新高&#xff0c;深成指、创业板指均涨约2%。化工股集体…

PPT中如何在原有的表格中新增/删除一行

在编辑PPT中插入的表格时&#xff0c;如果发现原有表格不够用/原有表格需过多&#xff0c;需要新插入/新删除一行&#xff0c;应该怎么做&#xff1f;今天就为大家演示一下这个小常识。 1.PPT中在原有的表格中新增一行 -->任意选中表格中的一行(默认一般选择最后一行) --&…

深入C语言:文件操作实现局外影响程序

一、什么是文件 文件其实是指一组相关数据的有序集合。这个数据集有一个名称&#xff0c;叫做文件名。文件通常是驻留在外部介质(如磁盘等)上的&#xff0c;在使用时才调入内存中来。 文件一般讲两种&#xff1a;程序文件和数据文件&#xff1a; 程序文件&#xff1a;包括源程…

模拟IC设计实践教程(入门)

一、教学大纲 二、芯片设计及量产总体流程 芯片需求分析&#xff1a;即产品定义&#xff0c;功能要求&#xff0c;性能要求工艺选择及评估&#xff1a;不同工艺特性相差较大&#xff0c;不同工艺的成本也是不同的。主要用于评估工艺的性价比&#xff0c;工艺提供的元器件能不能…

pip install 过程中报错:Microsoft Visual C++ 14.0 is required.

这是因为电脑中缺少这个组件导致的,我们将这个组件安装上即可解决问题。 安装报错关键信息:Microsoft Visual C++ 14.0 is required. 目录 一、下载组件 二、 安装步骤 一、下载组件 阿里网盘:VisualStudioSetup.exe:

绝地求生:季后赛名额确定!NH战队总积分榜排名第一!

2024年5月5日&#xff0c;PCL春季赛常规赛第五阶段第三天比赛结束&#xff0c;今天打完春季赛常规赛结束&#xff0c;16个战队进入季后赛的名额已确定。NH战队总积分506分&#xff0c;总积分榜排名第一&#xff01;&#xff01;NH战队也是唯一一支总积分超过500分的队伍。今天最…

LabVIEW自动机械变速器(AMT)开发

LabVIEW自动机械变速器&#xff08;AMT&#xff09;开发 在现代汽车工业中&#xff0c;提升车辆的自动化水平和驾驶体验是一个不断追求的目标。随着技术的发展&#xff0c;自动机械变速器&#xff08;AutomatedMechanical Transmission, AMT&#xff09;凭借其较高的能效和较低…

详解嵌入式MCU运行时分配的stack和heap

目录 概述 1 认识stack和heap 1.1 栈区&#xff08;stack&#xff09; 1.2 堆区&#xff08;heap&#xff09; 2 stack和heap的区别 2.1 管理方式的不同 2.2 空间大小不同 2.3 产生碎片不同 2.4 增长方式不同 2.5 分配方式不同 2.6 分配效率不同 3 确定stack和heap…

细数:智能物流装备界的并购案~

导语 大家好&#xff0c;我是智能仓储物流技术研习社的社长&#xff0c;老K。专注分享智能仓储物流技术、智能制造等内容。 新书《智能物流系统构成与技术实践》 近年来&#xff0c;随着智能仓储物流行业的快速发展&#xff0c;全球范围内的并购活动日益频繁&#xff0c;各大企…