Docker应用案例--给企微群推送代码提交消息

前言

继上一篇 跟我一起来学习一下docker 之后,大家对docker的一些重要概念与工作原理,docker的优势,如何创建镜像,运行镜像,重新生成镜像,Docker Desktop的安装使用方法,以及docker常用的一些命令,有了一个初步的了解。今天我们精进一步,通过实现一个应用案例,在windows操作系统下,用docker给企微群推送代码提交消息这个功能,增强一下对docker融会贯通的使用。

原理

要实现给企微群推送Coding代码仓库事件消息,需要了解两个背景知识:

  • 在企微群中创建聊天机器人后,会获得一个webhook地址,如果给这个地址按照企微机器人能够解析的消息格式发送一条消息,就会看到群里的机器人会向所在企微群中推送这条消息。
  • Coding的各种事件都会产生特定的消息, 可以推送给指定的服务器。

例如下面这条是代码合并事件推送的消息,可以看到消息体比较长,信息比较多。

json 复制代码
{
  "event": "", // 事件代码
  "eventName": "", // 事件名
  "mergeRequest": {
    "id": "", // 合并请求编号
    "html_url": "", // 合并请求访问地址
    "patch_url": "", // 合并请求 patch 地址
    "diff_url": "", // 合并请求 diff 地址
    "number": "", // 合并请求资源编号
    "state": "", // 合并请求状态
    "title": "", // 合并请求标题
    "body": "", // 合并请求内容
    "user": {
      "id": "", // 创建者编号
      "avatar_url": "", // 创建者头像
      "html_url": "", // 创建者主页地址
      "name": "", // 创建者名称
      "name_pinyin": "" // 创建者名称(拼音)
    },
    "created_at": "", // 合并请求创建时间
    "updated_at": "", // 合并请求更新时间
    "merge_commit_sha": "", // 合并请求 commit
    "merged": "", // 合并请求是否合并
    "comments": "", // 合并请求评论数
    "commits": "", // 合并请求提交数
    "additions": "", // 合并请求新增数
    "deletions": "", // 合并请求删除数
    "changed_files": "", // 合并请求文件变化数
    "head": {
      "ref": "", // 分支名称
      "sha": "", // 最后一个 commit sha
      "user": {
        "id": "", // 提交者编号
        "avatar_url": "", // 提交者头像
        "html_url": "", // 提交者主页地址
        "name": "", // 提交者名称
        "name_pinyin": "" // 提交者名称(拼音)
      },
      "repo": {
        "id": "", // 代码仓库编号
        "name": "", // 代码仓库标识
        "full_name": "", // 完整路径
        "owner": {
          "id": "", // 所有者编号
          "avatar_url": "", // 所有者头像
          "html_url": "", // 所有者主页地址
          "name": "", // 所有者名称
          "name_pinyin": "" // 所有者名称(拼音)
        },
        "private": "", //是否私有仓库
        "html_url": "", //代码仓库访问地址
        "description": "", // 代码仓库描述
        "fork": "", // 是否可以被 fork
        "created_at": "", // 创建时间
        "updated_at": "", // 更新时间
        "clone_url": "", // HTTP 克隆地址
        "ssh_url": "", // SSH 克隆地址
        "default_branch": "", // 默认分支
        "vcs_type": "" // 代码仓库类型
      }
    },
    "base": {
      "ref": "", // 分支名称
      "sha": "", // 最后一个 commit sha
      "user": {
        "id": "", // 提交者编号
        "avatar_url": "", // 提交者头像
        "html_url": "", // 提交者主页地址
        "name": "", // 提交者名称
        "name_pinyin": "" // 提交者名称(拼音)
      },
      "repo": {
        "id": "", // 代码仓库编号
        "name": "", // 代码仓库标识
        "full_name": "", // 完整路径
        "owner": {
          "id": "", // 所有者编号
          "avatar_url": "", // 所有者头像
          "html_url": "", // 所有者主页地址
          "name": "", // 所有者名称
          "name_pinyin": "" // 所有者名称(拼音)
        },
        "private": "", //是否私有仓库
        "html_url": "", //代码仓库访问地址
        "description": "", // 代码仓库描述
        "fork": "", // 是否可以被 fork
        "created_at": "", // 创建时间
        "updated_at": "", // 更新时间
        "clone_url": "", // HTTP 克隆地址
        "ssh_url": "", // SSH 克隆地址
        "default_branch": "", // 默认分支
        "vcs_type": "" // 代码仓库类型
      }
    }
  },
  "reviewers": [
    {
      "user_id": "", // 评审者编号
      "user_name": "", // 评审者用户名
      "user_email": "", // 评审者邮箱
      "user_global_key": "", // 评审者GK
      "url": "", // 评审者主页地址
      "avatar_url": "" // 评审者头像
    }
  ],
  "watchers": [
    {
      "user_id": "", // 关注者编号
      "user_name": "", // 关注者用户名
      "user_email": "", // 关注者邮箱
      "user_global_key": "", // 关注者GK
      "url": "", // 关注者主页地址
      "avatar_url": "" // 关注者头像
    }
  ],
  "repository": {
    "id": "", // 代码仓库编号
    "name": "", // 代码仓库标识
    "full_name": "", // 完整路径
    "owner": {
      "id": "", // 所有者编号
      "avatar_url": "", // 所有者头像
      "html_url": "", // 所有者主页地址
      "name": "", // 所有者名称
      "name_pinyin": "" // 所有者名称(拼音)
    },
    "private": "", //是否私有仓库
    "html_url": "", //代码仓库访问地址
    "description": "", // 代码仓库描述
    "fork": "", // 是否可以被 fork
    "created_at": "", // 创建时间
    "updated_at": "", // 更新时间
    "clone_url": "", // HTTP 克隆地址
    "ssh_url": "", // SSH 克隆地址
    "default_branch": "", // 默认分支
    "vcs_type": "" // 代码仓库类型
  },
  "sender": {
    "id": "", // 发送者编号
    "avatar_url": "", // 发送者头像
    "url": "", // 发送者主页地址
    "html_url": "", // 发送者主页地址
    "name": "", // 发送者名称
    "name_pinyin": "" // 发送者名称(拼音)
  },
  "project": {
    "id": "", // 项目编号
    "name": "", // 项目标识
    "display_name": "", // 项目名
    "description": "", // 项目描述
    "icon": "", // 项目图标
    "url": "" // 项目访问地址
  },
  "team": {
    "id": "", // 团队编号
    "domain": "", // 团队域名
    "name": "", // 团队名
    "name_pinyin": "", // 团队名(拼音)
    "introduction": "", // 团队简介
    "avatar": "", // 团队图标
    "url": "" // 团队访问地址
  }
}

如上所示:每种Coding代码仓库事件类型的消息体太长太多且无法定制特定场景的消息推送条件,并不是我们想要的。所以需要一个中转服务器处理一下,把接收到的Coding代码仓库事件消息格式化成期望的消息格式,然后再发送给企微群机器人,让其推送到企微群。整个过程数据的流向如下图所示。

实现步骤

分为三步实现:

  1. 在coding中进行配置,使代码仓库事件消息可以推送到中转服务器
  2. 在企微群中加一个机器人,获取机器人的webhook地址
  3. 开发好中转服务器功能:解析coding推送过来的消息,对消息进行格式化,发送给企微群机器人

这样当向coding上的代码仓库推送代码,发起合并请求时,相应的事件会被触发,coding会将代码仓库操作信息推送给中转服务器,中转服务器接收到消息后,对内容进行格式化处理,发送给企微群机器人,然后企微群成员就能看到代码提交合并消息。

Step1 Coding推送消息配置

登陆coding,找到配置菜单入口项目设置,点击之后,会进入到二级导航菜单

找到开发者选项并点击,点击之后,再点击右上角的新建 ServiceHook菜单

新建 ServiceHook第一步中选择Webhook

第二步中推送事件类型选择代码推送合并请求事件

最后一步,填写推送消息的服务器地址,配置好之后,可以点击一下发送测试PING事件测试一下, 配置的服务器地址能否收到测试消息。coding代码仓库事件推送的消息内容可点击这里查看

Step2 企微群机器人配置

需要具有群主权限, 点击右上角的..., 找到添加群机器人菜单

点击新创建一个机器人,输入机器人名称后,机器人就创建好了。

企微群机器人接收消息的Webhook地址会显示在下方

重点看一下消息推送格式,项目中用到了两种格式,一种是text类型,一种是markdown类型。@群里的成员有两种配置方法,一种是在mentioned_list列表中配置用户名,另一种是在mentioned_mobile_list列表中配置手机号,两种方式二选其一。

css 复制代码
{
    "msgtype": "text",
    "text": {
        "content": "广州今日天气:29度,大部分多云,降雨概率:60%",
        "mentioned_list":["wangqing","@all"],
        "mentioned_mobile_list":["13800001111","@all"]
    }
}
swift 复制代码
{
  "msgtype": "markdown",
  "markdown": {
    "content": "实时新增用户反馈<font color=\"warning\">132例</font>,请相关同事注意。\n> 
    类型:<font color=\"comment\">用户反馈</font>\n> 
    普通用户反馈:<font color=\"comment\">117例</font>\n> 
    VIP用户反馈:<font color=\"comment\">15例</font>"
  }
}

除了推送文本消息,也能推送图片,图文, 文件,模板卡片类型的消息,每种消息类型配置的详细参数说明,可在机器人配置说明菜单下查看。

中转服务器功能实现

中转服务复用了项目中一个集大成的后台服务,这个集大成的后台服务业务逻辑采用nest实现, 依赖的mysql数据库服务,rabbitmq消息队列,redis缓存,postgres数据库, nginxweb服务,全部放在docker容器中。这样做的好处是,可以不必在本地机器上安装配置这些后台依赖的软件与服务,把它们制作成docker镜像,让它们运行在docker中,也能提供同样的服务。在分布式架构下,拓展迁移很方便。由于需要创建和启动的容器较多,所以我们采用docker-compose方式管理这些服务。

Docker-compose 是用于定义和运行多容器 Docker 应用程序的编排工具。使用 docker-compose 后不再需要逐一创建和启动容器。使用 YML 文件来配置应用程序需要的所有服务,只需一条命令,就可以从 YML 文件配置中创建并启动所有服务。此项目docker-compose.yml配置如下所示:

yaml 复制代码
version: '3'
services:
  mysql:
    image: mysql:5.7
    restart: always
    ports:
      - '3306:3306'
    environment:
      MYSQL_ROOT_PASSWORD: hello-mysql
  rabbitmq:
    image: rabbitmq:management
    restart: always
    ports:
      - '4369:4369'
      - '5671:5671'
      - '5672:5672'
      - '15671:15671'
      - '15672:15672'
      - '25672:25672'
  redis:
    image: redis:alpine
    restart: always
    ports:
      - '6379:6379'
  postgres:
    image: postgres
    restart: always
    ports:
      - '5432:5432'
    environment:
      POSTGRES_PASSWORD: hello-postgres
  nginx:
    image: nginx:alpine
    restart: always
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/log:/var/log/nginx
      - ./nginx/www:/var/www

上面的docker-compose每个属性的含义如下表所示:

属性名 含义
version 这个定义关乎docker的兼容性。Compose 文件格式有3个版本,分别为1, 2.x 和 3.x 。目前主流的为 3.x 其支持 docker 1.13.0 及其以上的版本
services 定义了服务的配置信息,包含应用于该服务启动的每个容器的配置
image 指定为镜像名称或镜像 ID。如果镜像在本地不存在,docker将会尝试拉取这个镜像。项目中用的的这几个镜像都是从远程docker仓库拉取的
restart 启动策略,有4个取值:no(表示容器退出时,docker不自动重启容器),on-failure[:times](若容器的退出状态非0,则docker自动重启容器,还可以指定重启次数,若超过指定次数未能启动容器则放弃),always(容器退出时总是重启),unless-stopped(容器退出时总是重启,但不考虑Docker守护进程启动时就已经停止的容器)。
ports 暴露端口信息。使用宿主端口:容器端口 (HOST:CONTAINER) 格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。项目中采用的是HOST:CONTAINER格式
environment 服务所需的环境变量
volumes 数据卷所挂载路径设置。可以设置为宿主机路径(HOST:CONTAINER)或者数据卷名称(VOLUME:CONTAINER),如这句./nginx/conf.d:/etc/nginx/conf.d , 将项目下的./nginx/conf.d映挂载到docker的/etc/nginx/conf.d路径下

docker并不包含docker-compose工具,docker-compose需要单独安装,如果你的编码IDE是VSCode的话,可以在应用商店搜索Docker Compose扩展安装使用。

docker-compose常用的命令有:

bash 复制代码
# 默认使用docker-compose.yml构建镜像
docker-compose build
# 不带缓存的构建
docker-compose build --no-cache 
# 指定不同yml文件模板用于构建镜像
docker-compose build -f docker-compose1.yml

# 列出Compose文件构建的镜像
docker-compose images                          

# 启动所有编排容器服务(守护模式)
docker-compose up -d

# 查看正在运行中的容器
docker-compose ps 

# 查看所有编排容器,包括已停止的容器
docker-compose ps -a

# 进入指定容器执行命令
docker-compose exec nginx bash 
docker-compose exec web python manage.py migrate --noinput

# 查看web容器的实时日志
docker-compose logs -f web

# 停止所有up命令启动的容器
docker-compose down 
# 停止所有up命令启动的容器,并移除数据卷
docker-compose down -v

# 重新启动停止服务的容器
docker-compose restart web

# 暂停web容器
docker-compose pause web

# 恢复web容器
docker-compose unpause web

# 删除web容器,删除前必需停止stop web容器服务
docker-compose rm web  

# 查看各个服务容器内运行的进程 
docker-compose top   

集大成的业务服务依赖docker容器服务,所以首先要启动docker容器服务。先启动windows桌面版docker,接着执行docker-compose up命令启动所有编排容器服务,命令执行完成后,可以在桌面版的docker中查看容器服务的运行状态,从下图可以看出,本项目中依赖的5个docker服务均已正常启动。

现在启动集大成的业务服务:执行yarn start,如果控制台没有任何报错的话,说明任务启动正常。

业务服务正常启动后,在这个集大成的后台服务中加上一段给企微群机器人推送消息的功能:

为了防止中转服务被恶意攻击,要加一个签名校验逻辑, 使用Node.js 的 crypto 模块 createHmac() 方法创建一个HMAC-SHA1 实例,传入密钥 process.env.CODING_WEBHOOK_SECRET 和要计算的数据 request.rawBody。然后使用 update() 方法将数据添加到 HMAC 实例中,最后使用 digest() 方法计算出 HMAC-SHA1 的值,并以十六进制字符串的形式输出。与配置在coding上中的签名令牌做比较,进行签名校验。签名校验失败抛出异常,签名校验通过,对接收到的代码仓库事件消息数据进行处理。

js 复制代码
  // 接收 coding webhook 的回调请求并处理
  @Post('coding-webhook')
  async codingWebhook(@Req() request: any, @Body() webhookData: any) {
    const webhookDataHash = crypto
      .createHmac('sha1', process.env.CODING_WEBHOOK_SECRET)
      .update(request.rawBody)
      .digest('hex');
    if (
      !request.headers['x-coding-signature'] ||
      request.headers['x-coding-signature'] !== `sha1=${webhookDataHash}`
    ) {
      this.logger.error(
        `[ToolkitModule][ToolkitController] codingWebhook auth error: sha1=${webhookDataHash}`,
      );
      throw new HttpException('UNAUTHORIZED', HttpStatus.UNAUTHORIZED);
    }

    // 向机器人推送消息
    this.sendBotMsg(webhookData);

    return 'ok';
  }

只有符合条件的分支和事件才触发消息通知,这些信息是从配置文件中读取的。

js 复制代码
  sendBotMsg(webhookData) {
    // 分支名和事件符合条件才推送消息
    if (!this.isCanPostMsg(webhookData)) {
      return;
    }

    // 按照企微消息格式封装推送消息
    const msgArr = this.packageMsg(webhookData);

    if (msgArr.length) {
      // 向机器人推送消息
      this.postMessage(webhookData, msgArr);
    }
  }

上面的packageMsg方法的功能是封装推送消息,解析coding推送过来的代码仓库事件消息,将消息封装成企微群机器人支持的markdowntext格式。markdown格式用于展示仓库合并信息,text格式用于@群成员。

js 复制代码
 
  packageMsg(webhookData): IQywxMsgProps[] {
    // 推送消息类型的格式
    const msgArr: IQywxMsgProps[] = [
      {
        msgtype: 'markdown',
        markdown: {
          content: '',
        },
      },
    ];
    // 拼接推送消息内容的数组
    const contentArr = [];

    let eventShortTitle = '';
    const event = webhookData.event;
    const action = webhookData.action;
    const sourceBranch = webhookData?.mergeRequest?.head?.ref;
    const targetBranch = webhookData?.mergeRequest?.base?.ref;
    const repoName = webhookData?.repository?.name;
    // 当前动作触发人姓名,创建 mr 时是提 mr 的人,合并 mr 是是合并人
    const senderName = webhookData?.sender?.name;
    const mrUserName = webhookData?.mergeRequest?.user?.name;
    const mrReviewerUserName = webhookData?.mergeRequest?.merged_by?.name;
    const title = webhookData?.mergeRequest?.title;
    const state = webhookData?.mergeRequest?.state;
    const labels = webhookData?.mergeRequest?.labels;
    const createdAt = webhookData?.mergeRequest?.created_at;
    const updatedAt = webhookData?.mergeRequest?.updated_at;
    const mergedAt = webhookData?.mergeRequest?.merged_at;
    const viewUrl = webhookData?.mergeRequest?.html_url;

    contentArr.push(
      `${eventShortTitle}标题: <font color="comment">${title}</font>`,
    );
    contentArr.push(`应用名称: <font color="comment">${repoName}</font>`);
    contentArr.push(
      `来源分支: <font color="comment">**${sourceBranch}**</font>`,
    );
    contentArr.push(
      `目标分支: <font color="comment">**${targetBranch}**</font>`,
    );
    contentArr.push(`当前状态: <font color="warning">${state}</font>`);

    // merge事件类型才展示合并人员
    if (event === 'GIT_MR_MERGED') {
      contentArr.push(
        `合并人员: <font color="comment">${mrReviewerUserName}</font>`,
      );
    }

    if (['GIT_MR_UPDATED', 'GIT_MR_MERGED'].includes(event)) {
      // 更新和合并请求的时间取值字段不一样
      const time = {
        GIT_MR_UPDATED: updatedAt,
        GIT_MR_MERGED: mergedAt,
      };

      time[event] &&
        contentArr.push(
          `${eventShortTitle}时间: <font color="comment">${format(
            new Date(time[event]),
            'yyyy-MM-dd HH:mm:ss',
          )}</font>`,
        );
    }

    contentArr.push(`创建人员: <font color="comment">${mrUserName}</font>`);
    contentArr.push(
      `创建时间: <font color="comment">${format(
        new Date(createdAt),
        'yyyy-MM-dd HH:mm:ss',
      )}</font>`,
    );


    contentArr.push(`查看地址: ${viewUrl}`);
    msgArr[0].markdown.content = contentArr.join('\n>');

    // 查找是否配置了@群成员规则
    const mentionedRule = this.findMentionedConfig(webhookData);
    // console.log(mentionedRule);
    // 如果返回的对象值不为空,说明查找到@群成员规则
    if (mentionedRule.mentionedList) {
      // 创建更新代码合并请求|评审未通过,通知消息传入参数为代码合并请求地址
      // 代码评审通过以及代码已合并,通知消息传入参数为要更新代码的分支
      const info =
        ['push', 'bad'].includes(action) || event === 'GIT_MR_CREATED'
          ? viewUrl
          : `${repoName}仓库${targetBranch}`;
      // 配置了@群成员规则,则在推送信息中添加@群成员字段
      msgArr.push({
        msgtype: 'text',
        text: {
          content: mentionedRule.joinContent(info),
          mentioned_list: mentionedRule.mentionedList(mrUserName),
        },
      });
    }

    return msgArr;
  }

上面的postMessage方法用于向微信群推送消息。一般会存在多个群,不同仓库的消息发送到不同的群。

js 复制代码
  // 推送消息
  postMessage(webhookData: any, msgArr: IQywxMsgProps[]) {
    // 获取消息推送地址
    const botApiUrls = this.getBotApiUrls(webhookData);
    // 可向多个地址推送消息
    botApiUrls.forEach((apiUrl) => {
      msgArr.forEach((msg) => {
        this.qywxBotService.postMsg(apiUrl, msg);
      });
    });
  }

获取的相应企微群机器人的推送地址后,调用this.qywxBotService.postMsg方法,将封装好的消息推送给企微机器人。qywxBotService的功能实现如下:

js 复制代码
export class QywxBotService {
  constructor(private httpService: HttpService, private logger: Logger) {}

  // apiUrl--消息推送地址
  // data--要推送的消息
  postMsg(apiUrl: string, data: any): void {
    this.httpService.post(apiUrl, data).subscribe({
      // eslint-disable-next-line @typescript-eslint/no-empty-function
      next: (result) => {},
      error: (error) => {
        this.logger.error(
          '[QywxBotModule][QywxBotService] postMsg error: ',
          error,
          data,
        );
      },
    });
  }
}

推送到企微群的消息效果如下图所示:

最后

本文讲述了如何把coding代码仓库事件消息推送到指定企微群的实现原理与方法,在coding上该做哪么配置,在企微群该做哪些配置,整个过程的数据流动过程是怎样的,至于中转服务器这部分,不是必须的。除非有特殊的消息推送规则。比如什么分支什么事件需要@群里哪些人关注,才需要消息中转服务。如果真要做,你可以采用更轻量化的方式实现,不必参考文中的方法。其它仓库类型的消息推送方式(比如说GitLab),也和本文的差不多,可以触类旁通。如果你能看到这里,恭喜你,又get到了一个新技能。

相关推荐
SameX2 分钟前
初识 HarmonyOS Next 的分布式管理:设备发现与认证
前端·harmonyos
陈小肚12 分钟前
k8s 1.28.2 集群部署 docker registry 接入 MinIO 存储
docker·容器·kubernetes
M_emory_29 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
A陈雷29 分钟前
springboot整合elasticsearch,并使用docker desktop运行elasticsearch镜像容器遇到的问题。
spring boot·elasticsearch·docker
Ciito32 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
小扳34 分钟前
Docker 篇-Docker 详细安装、了解和使用 Docker 核心功能(数据卷、自定义镜像 Dockerfile、网络)
运维·spring boot·后端·mysql·spring cloud·docker·容器
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室3 小时前
ValueError: Circular reference detected
开发语言·前端·javascript