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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

线性表的概念与结构,以及顺序表和链表的简单概念

1.线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直线…

JS hook cookie

JS hook cookie cookie 的值是V&#xff0c;v是动态变化的 可以看到D中生成了cookie的值n 尝试使用RPC定位到cookie。 替换内容&#xff0c;下断点。 将写好的RPC代码直接插入 加入代码&#xff0c;file.virjar.com/sekiro_web_client.js?_123 这个地址是在前端创建客户端…

开源模型应用落地-CodeQwen模型小试-小试牛刀(一)

一、前言 代码专家模型是基于人工智能的先进技术&#xff0c;它能够自动分析和理解大量的代码库&#xff0c;并从中学习常见的编码模式和最佳实践。这种模型可以提供准确而高效的代码建议&#xff0c;帮助开发人员在编写代码时避免常见的错误和陷阱。 通过学习代码专家模型&…

【网络知识】光猫、路由器 和 交换机 的作用和区别?

数字信号&#xff1a;是指自变量是离散的、因变量也是离散的信号&#xff0c;这种信号的自变量用整数表示&#xff0c;因变量用有限数字中的一个数字来表示。在计算机中&#xff0c;数字信号的大小常用有限位的二进制数表示。 模拟信号&#xff1a;模拟信号是指用连续变化的物…

学习c#第26天 面向对象基础之类与对象

1.类 1.什么是类? 俗话说&#xff0c;“物以类聚&#xff0c;人以群分”。意思是同类的东西经常聚在一起&#xff0c;志同道合 的人相聚成群。前者说物&#xff0c;后者说人。这里以物来进行举例说明[见图]&#xff1a; 水果超市&#xff0c;所有同类的水果摆放在一起&#xf…

【机器学习与实现】线性回归分析

目录 一、相关和回归的概念&#xff08;一&#xff09;变量间的关系&#xff08;二&#xff09;Pearson&#xff08;皮尔逊&#xff09;相关系数 二、线性回归的概念和方程&#xff08;一&#xff09;回归分析概述&#xff08;二&#xff09;线性回归方程 三、线性回归模型的损…

vivado刷题笔记46

题目&#xff1a; Design a 1-12 counter with the following inputs and outputs: Reset Synchronous active-high reset that forces the counter to 1 Enable Set high for the counter to run Clk Positive edge-triggered clock input Q[3:0] The output of the counter c…

场外个股期权和场内个股期权的优缺点是什么?

场外个股期权和场内个股期权的优缺点 场外个股期权是指在沪深交易所之外交易的个股期权&#xff0c;其本质是一种金融衍生品&#xff0c;允许投资者在股票交易场所外以特定价格买进或卖出证券。场内个股期权是以单只股票作为标的资产的期权合约&#xff0c;其内在价值是基于标…

金融业开源软件应用 管理指南

金融业开源软件应用 管理指南 1 范围 本文件提供了金融机构在应用开源软件时的全流程管理指南&#xff0c;对开源软件的使用和管理提供了配套 组织架构、配套管理规章制度、生命周期流程管理、风险管理、存量管理、工具化管理等方面的指导。 本文件适用于金融机构规范自身对开…

工业物联网技术在生产流程中的应用及优势与挑战——青创智通

工业物联网解决方案-工业IOT-青创智通 随着科技的不断发展&#xff0c;物联网技术逐渐渗透到各个行业中&#xff0c;尤其是在工业领域&#xff0c;工业物联网的应用正在逐步重塑生产流程。本文将探讨工业物联网如何影响生产流程&#xff0c;并分析其带来的优势和挑战。 一、工…

Amazon Bedrock的进化:更多选择与新特性,助力生成式AI应用更快落地

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

leetcode-没有重复项的全排列-97

题目要求 思路 1.递归&#xff0c;如果num和n的元素个数一样就可以插入res中了&#xff0c;这个作为递归的结束条件 2.因为这个题是属于排列&#xff0c;并非组合&#xff0c;两者的区别是排列需要把之前插入的元素在回退会去&#xff0c;而组合不需要&#xff0c;因此会存在一…

YPay源支付Mini Pro免授权使用版v1.0

YPay源支付Mini Pro免授权使用版v1.0 &#xff0c;修改host屏蔽Pro授权站&#xff0c;可有效防止因用户操作不当导致免授权程序无法执行时 执行授权官方的盗版入库代码&#xff0c;尽可能保证网站安全 1.安装SG14组件 注&#xff1a;仅防止二次开发添加授权 2.”/etc/host”文…

尊享面试100题(314.二叉树的垂直遍历python)

题目关键词&#xff0c;从左到右&#xff0c;从上到下&#xff0c;那么使用bfs宽度优先算法。 使用字典v保存每一列的值。 class Solution:def verticalOrder(self, root: Optional[TreeNode]) -> List[List[int]]:if not root: return []v defaultdict(list)qu deque()…

淘宝扭蛋机小程序开发:开启你的惊喜之旅

一、扭出新世界&#xff0c;惊喜不断 在这个充满无限可能的数字时代&#xff0c;淘宝扭蛋机小程序为你带来了一种全新的购物与娱乐体验。扭蛋机&#xff0c;这个充满童趣和惊喜的玩具&#xff0c;如今在我们的小程序中焕发出新的活力&#xff0c;为你带来一波又一波的惊喜与快…

WES-100B液晶数显式液压万能试验机

一、简介 主机为两立柱、两丝杠、油缸下置式&#xff0c;拉伸空间位于主机的上方&#xff0c;压缩、弯曲试验空间位于主机下横梁和工作台之间。测力仪表采用高清液晶显示屏&#xff0c;实验数据方便直观。 二、 传动系统 下横梁升降采用电机经减速器、链传动机构、丝杠副传动…

Redis开源社区持续壮大,华为云为Valkey项目注入新的活力

背景 今年3月21日&#xff0c;Redis Labs宣布从Redis 7.4版本开始&#xff0c;将原先比较宽松的BSD源码使用协议修改为RSAv2和SSPLv1协议&#xff0c;意味着 Redis在OSI&#xff08;开放源代码促进会&#xff09;定义下不再是严格的开源产品。Redis官方表示&#xff0c;开发者…

QT--1

类型界面 #include "mywidget.h"myWidget::myWidget(QWidget *parent): QWidget(parent) {//窗口相关设置this->resize(680,520);this->setFixedSize(680,520);this->setWindowTitle("Tim");this->setWindowFlag(Qt::FramelessWindowHint);th…

Git -- reset 详解

引言 当我们在项目中有多个人协同开发时候&#xff0c;难免会出现一些错误的提交或者删除了一些重要文件。我们需要回滚到指定的某一个节点。那些乱七八糟的各种提交都要清除掉。 这时候&#xff0c;我们的指令就要用到了。reset 正文 git reset。它的一句话概括 git-reset …

【C++之map的应用】

C学习笔记---021 C之map的应用1、map的简单介绍1.1、基本概念1.2、map基本特性 2、map的基本操作2.1、插入元素2.2、访问元素2.3、删除元素2.4、遍历map2.5、检查元素是否存在2.6、获取map的大小2.7、清空map2.8、基本样例 3、map的基础模拟实现4、测试用例4.1、插入和遍历4.2、…