Prow Manual-Trigger 深度解析:架构、工作流程与实战 📚 前言 本文深入剖析 Kubernetes test-infra 仓库中的 prow/cmd/manual-trigger 项目,详细讲解其技术架构、工作流程和使用方法。Manual-Trigger 是一个 HTTP 服务,允许用户手动触发 Prow CI/CD 任务,而无需依赖 GitHub webhook 事件。
一、Test-Infra 仓库整体概述 1.1 Test-Infra 是什么? test-infra 是 Kubernetes 官方的测试基础设施仓库 ,包含了 Kubernetes 项目的所有 CI/CD 工具和配置文件。它是一个大型的测试自动化框架,主要用于:
CI 任务管理 : 自动化测试、构建、部署流程
代码审查 : PR 自动化测试和合并管理
测试结果展示 : 历史测试数据和失败分析
资源管理 : GCP 项目池、GitHub API 代理等
1.2 核心组件 - Prow Prow 是这个仓库的核心,它是一个基于 Kubernetes 的 CI/CD 系统,专为 GitHub 设计。Prow 的主要组件包括:
组件
功能
Hook
监听 GitHub webhook 事件
Plank
调度和管理 ProwJob
Deck
Web UI,展示任务状态
Tide
自动合并 PR
Crier
上报任务结果到 GitHub
Manual-Trigger
手动触发任务(本文重点)
二、Manual-Trigger 项目深度剖析 2.1 项目定位 Manual-Trigger 是一个 HTTP 服务 ,允许用户手动触发 Prow 任务 ,而无需通过 GitHub 事件(如 push、PR)。这在以下场景非常有用:
✅ 手动测试 CI 任务 ✅ 对特定代码版本运行测试 ✅ 运维和维护操作 ✅ 使用自定义参数触发任务
2.2 技术架构 2.2.1 技术栈 1 2 3 4 5 6 7 编程语言: Go 1.21 框架: 标准库 net/http Kubernetes: 通过 client-go 与 K8s API 交互 依赖: - Prow API (prowjobs/v1) - Kubernetes Core API (corev1) - Prow 配置管理 (config.Agent)
2.2.2 架构图 graph TB
A[用户/客户端] -->|HTTP POST| B[Manual-Trigger 服务]
B -->|读取配置| C[Config Agent]
B -->|创建 ProwJob| D[ProwJob Client]
D -->|提交 CRD| E[Kubernetes API Server]
E -->|存储| F[ProwJob Custom Resource]
F -->|监听| G[Prow Plank]
G -->|创建 Pod| H[Kubernetes Pod]
H -->|执行任务| I[CI/CD 任务]
B -->|返回结果| A
核心组件说明 :
HTTP Handler : 接收和解析用户请求
Config Agent : 读取 Prow 配置文件,查找任务定义
ProwJob Client : Kubernetes 客户端,用于创建 ProwJob CRD
Prow Plank : 监听 ProwJob 创建事件,调度 Pod 执行
三、工作流程详解 3.1 核心工作流 (Step by Step) 步骤 1: 接收 HTTP 请求 用户发送 POST 请求到 /manual-trigger 端点:
1 2 3 4 5 6 7 8 9 10 curl -X POST "http://manual-trigger.tessprow/manual-trigger" \ -H "Content-Type: application/json" \ -d '{ "org": "tess", "repo": "tessops", "base_ref": "master", "prowtype": "postsubmit", "prowjob": "sddz-e2e-k8s-1.32", "user": "fesu" }'
参数说明 :
参数
说明
必需
示例
org
GitHub 组织名
✅
tess
repo
仓库名
✅
tessops
base_ref
基础分支
✅
master
prowtype
任务类型
✅
postsubmit
prowjob
任务名称
✅
sddz-e2e-k8s-1.32
pullrequest
PR 号(presubmit 必需)
条件
123
user
自定义用户名
❌
fesu
步骤 2: 请求解析与验证 handleManualTrigger 函数处理请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 if req.Org == "" || req.Repo == "" || req.BaseRef == "" || req.ProwType == "" || req.ProwJob == "" { } if req.ProwType == "presubmit" && req.PullRequest <= 0 { }
步骤 3: 查找任务配置 从 Prow 配置中查找对应的任务定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cfg := s.configAgent.Config() if presubmits, ok := cfg.PresubmitsStatic[req.Org+"/" +req.Repo]; ok { for _, p := range presubmits { if p.Name == req.ProwJob { prowJob, err = s.createProwJobFromPresubmit(p, req) break } } } if prowJob == nil { if postsubmits, ok := cfg.PostsubmitsStatic[req.Org+"/" +req.Repo]; ok { } }
步骤 4: 创建 ProwJob 规范 根据任务类型创建 ProwJob 对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 func (s *server) createProwJobFromPresubmit(presubmit config.Presubmit, req triggerRequest) (*prowapi.ProwJob, error ) { refs := prowapi.Refs{ Org: req.Org, Repo: req.Repo, BaseRef: req.BaseRef, RepoLink: fmt.Sprintf("https://github.corp.ebay.com/%s/%s" , req.Org, req.Repo), } if req.ProwType == "presubmit" && req.PullRequest > 0 { refs.Pulls = []prowapi.Pull{ { Number: req.PullRequest, Link: fmt.Sprintf("https://github.corp.ebay.com/%s/%s/pull/%d" , req.Org, req.Repo, req.PullRequest), }, } } spec := pjutil.PresubmitSpec(presubmit, refs) if req.ProwType == "postsubmit" { spec.Type = prowapi.PostsubmitJob } pj := pjutil.NewProwJob(spec, labels, annotations) if req.User != "" { s.addAuthorEnvToProwJob(&pj, req.User) } return &pj, nil }
步骤 5: 提交到 Kubernetes 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 if !s.dryRun { ctx := context.Background() _, err = s.prowJobClient.Create(ctx, prowJob, metav1.CreateOptions{}) if err != nil { } ctx, cancel := context.WithTimeout(context.Background(), 15 *time.Second) defer cancel() ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): goto buildResponse case <-ticker.C: latestProwJob, _ := s.prowJobClient.Get(context.Background(), prowJob.Name, metav1.GetOptions{}) if latestProwJob.Status.BuildID != "" { prowJob = latestProwJob goto buildResponse } } } }
步骤 6: 返回响应 1 2 3 4 5 6 7 8 9 10 11 12 statusLink := fmt.Sprintf("https://%s/prowjob?prowjob=%s" , baseURL, prowJob.Name) logLink := fmt.Sprintf("https://%s/log?job=%s&id=%s" , baseURL, req.ProwJob, prowJob.Status.BuildID) { "success" : true , "message" : "ProwJob created successfully" , "job_name" : "715c1a56-f815-11f0-b860-8acb6d93d6ad" , "status_link" : "https://prow.example.com/prowjob?prowjob=..." , "log_link" : "https://prow.example.com/log?job=...&id=..." }
步骤 7: Prow 执行任务 一旦 ProwJob CRD 被创建:
Plank 组件监听到新的 ProwJob
Plank 根据 spec.PodSpec 创建 Kubernetes Pod
Pod 执行测试/构建任务
Plank 更新 ProwJob 的 status 字段
Deck 在 Web UI 上展示任务状态
Crier 可以将结果上报到外部系统
3.2 核心数据结构 ProwJob CRD 结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type ProwJob struct { metav1.TypeMeta metav1.ObjectMeta Spec ProwJobSpec Status ProwJobStatus } type ProwJobSpec struct { Type ProwJobType Agent ProwJobAgent Cluster string Job string Refs *Refs PodSpec *corev1.PodSpec } type Refs struct { Org string Repo string BaseRef string BaseSHA string Pulls []Pull } type ProwJobStatus struct { State ProwJobState BuildID string StartTime metav1.Time CompletionTime *metav1.Time URL string }
ProwJob 生命周期状态 :
stateDiagram-v2
[*] --> Triggered: ProwJob 创建
Triggered --> Pending: Pod 开始运行
Pending --> Success: 任务成功 (exit 0)
Pending --> Failure: 任务失败 (exit non-zero)
Pending --> Aborted: 被提前终止
Pending --> Error: 调度失败
Success --> [*]
Failure --> [*]
Aborted --> [*]
Error --> [*]
四、关键特性详解 4.1 灵活的类型转换 Manual-Trigger 支持将 presubmit 任务作为 postsubmit 运行 :
1 2 3 4 5 6 if req.ProwType == "postsubmit" { spec := pjutil.PresubmitSpec(presubmit, refs) spec.Type = prowapi.PostsubmitJob }
使用场景 : 想在某个分支上运行测试,但不想创建 PR
4.2 自定义环境变量注入 通过 user 参数可以注入 AUTHOR 环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func (s *server) addAuthorEnvToProwJob(pj *prowapi.ProwJob, user string ) { if pj.Spec.PodSpec != nil && pj.Spec.PodSpec.Containers != nil { for i := range pj.Spec.PodSpec.Containers { authorEnv := corev1.EnvVar{ Name: "AUTHOR" , Value: user, } found := false for j := range pj.Spec.PodSpec.Containers[i].Env { if pj.Spec.PodSpec.Containers[i].Env[j].Name == "AUTHOR" { pj.Spec.PodSpec.Containers[i].Env[j].Value = user found = true break } } if !found { pj.Spec.PodSpec.Containers[i].Env = append (pj.Spec.PodSpec.Containers[i].Env, authorEnv) } } } }
使用场景 : 任务脚本可以根据 AUTHOR 环境变量执行不同逻辑
4.3 BuildID 等待机制 为了提供完整的日志链接,服务会等待 BuildID 生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ctx, cancel := context.WithTimeout(context.Background(), 15 *time.Second) defer cancel()ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop()for { select { case <-ctx.Done(): goto buildResponse case <-ticker.C: latestProwJob, err := s.prowJobClient.Get(context.Background(), prowJob.Name, metav1.GetOptions{}) if latestProwJob.Status.BuildID != "" { goto buildResponse } } }
五、部署架构 5.1 Kubernetes 部署清单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 apiVersion: v1 kind: ServiceAccount metadata: name: manual-trigger namespace: tessprow --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: manual-trigger namespace: tessprow rules: - apiGroups: - prow.k8s.io resources: - prowjobs verbs: - create - get - list --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: manual-trigger namespace: tessprow roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: manual-trigger subjects: - kind: ServiceAccount name: manual-trigger namespace: tessprow --- apiVersion: apps/v1 kind: Deployment metadata: name: manual-trigger namespace: tessprow labels: app: manual-trigger spec: replicas: 1 selector: matchLabels: app: manual-trigger template: metadata: labels: app: manual-trigger spec: serviceAccountName: manual-trigger containers: - name: manual-trigger image: hub.tess.io/prowimages/manual-trigger:latest imagePullPolicy: Always args: - --config-path=/etc/config/config.yaml - --dry-run=false - --port=8080 - --namespace=tessprow ports: - name: http containerPort: 8080 - name: metrics containerPort: 9090 livenessProbe: httpGet: path: / port: 8080 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: / port: 8080 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi volumeMounts: - name: config mountPath: /etc/config readOnly: true volumes: - name: config configMap: name: config --- apiVersion: v1 kind: Service metadata: name: manual-trigger namespace: tessprow labels: app: manual-trigger spec: type: ClusterIP ports: - name: http port: 80 targetPort: 8080 protocol: TCP - name: metrics port: 9090 targetPort: 9090 protocol: TCP selector: app: manual-trigger
5.2 容器镜像构建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FROM golang:1.21 AS builderWORKDIR /workspace COPY . . WORKDIR /workspace/prow/cmd/manual-trigger RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manual-trigger . FROM hub.tess.io/tess/ubuntu-22.04 :hardenedCOPY --from=builder /workspace/prow/cmd/manual-trigger/manual-trigger /usr/local/bin/manual-trigger RUN useradd -r -u 1000 -g root prow && \ chmod +x /usr/local/bin/manual-trigger USER 1000 :0 ENTRYPOINT ["/usr/local/bin/manual-trigger" ]
构建命令 :
1 2 docker build -t hub.tess.io/fesu/manual-trigger:latest \ -f prow/cmd/manual-trigger/Dockerfile .
六、使用示例 6.1 基础使用 1 2 3 4 5 6 7 8 9 10 11 curl -X POST "http://manual-trigger.tessprow/manual-trigger" \ -H "Content-Type: application/json" \ -d '{ "org": "tess", "repo": "tessops", "base_ref": "master", "prowtype": "postsubmit", "prowjob": "sddz-e2e-k8s-1.32", "user": "admin" }'
响应示例 :
1 2 3 4 5 6 7 { "success" : true , "message" : "ProwJob created successfully" , "job_name" : "715c1a56-f815-11f0-b860-8acb6d93d6ad" , "status_link" : "https://prow.tessprow/prowjob?prowjob=715c1a56-f815-11f0-b860-8acb6d93d6ad" , "log_link" : "https://prow.tessprow/log?job=sddz-e2e-k8s-1.32&id=20260328-100000" }
6.2 触发 presubmit 任务 1 2 3 4 5 6 7 8 9 10 11 curl -X POST "http://manual-trigger.tessprow/manual-trigger" \ -H "Content-Type: application/json" \ -d '{ "org": "tess", "repo": "tessops", "base_ref": "master", "prowtype": "presubmit", "prowjob": "sddz-e2e-k8s-1.32", "pullrequest": 123 }'
6.3 将 presubmit 任务作为 postsubmit 运行 1 2 3 4 5 6 7 8 9 curl -X POST "http://manual-trigger.tessprow/manual-trigger" \ -d '{ "org": "tess", "repo": "tessops", "base_ref": "feature-branch", "prowtype": "postsubmit", "prowjob": "sddz-e2e-k8s-1.32" }'
6.4 Shell 脚本封装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #!/bin/bash MANUAL_TRIGGER_URL="http://manual-trigger.tessprow/manual-trigger" trigger_job () { local org=$1 local repo=$2 local branch=$3 local job=$4 local type =${5:-postsubmit} local pr =$6 local user=${7:-""} local data="{\"org\":\"$org \",\"repo\":\"$repo \",\"base_ref\":\"$branch \",\"prowtype\":\"$type \",\"prowjob\":\"$job \"}" if [ ! -z "$pr " ]; then data=$(echo $data | sed "s/}$/,\"pullrequest\":$pr }/" ) fi if [ ! -z "$user " ]; then data=$(echo $data | sed "s/}$/,\"user\":\"$user \"}/" ) fi echo "📤 触发任务: $data " response=$(curl -s -X POST $MANUAL_TRIGGER_URL \ -H "Content-Type: application/json" \ -d "$data " ) echo "$response " | jq . log_link=$(echo "$response " | jq -r '.log_link' ) if [ "$log_link " != "null" ] && [ ! -z "$log_link " ]; then echo "📊 日志链接: $log_link " fi } trigger_job "tess" "tessops" "master" "sddz-e2e-k8s-1.32" "postsubmit" "" "admin"
七、与 Prow 生态的集成 7.1 Prow 配置文件 Manual-Trigger 依赖 Prow 配置文件来查找任务定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 presubmits: tess/tessops: - name: sddz-e2e-k8s-1.32 always_run: true decorate: true spec: containers: - image: gcr.io/k8s-testimages/kubekins-e2e:latest command: - runner.sh args: - make - test env: - name: AUTHOR value: ""
7.2 与其他 Prow 组件交互 graph LR
A[Manual-Trigger] -->|创建 ProwJob| B[Kubernetes API]
B --> C[Plank 调度器]
B --> D[Deck Web UI]
B --> E[Crier 通知器]
C -->|创建 Pod| F[执行任务]
D -->|展示状态| G[用户界面]
E -->|上报结果| H[GitHub/Slack]
八、安全与监控 8.1 RBAC 权限 最小权限原则 :
权限
说明
create
只能创建 ProwJob
get
只能查询 ProwJob 状态
list
只能列出 ProwJob
❌ delete
不能删除 ProwJob
❌ update
不能更新 ProwJob
8.2 监控指标 服务暴露 Prometheus metrics (端口 9090):
1 2 metrics.ExposeMetrics("manual-trigger" , configAgent.Config().PushGateway, o.instrumentationOptions.MetricsPort)
可监控的指标 :
HTTP 请求计数
请求延迟
错误率
ProwJob 创建成功/失败次数
Prometheus 查询示例 :
1 2 3 4 5 6 7 8 9 # 每秒请求数 rate(http_requests_total{job="manual-trigger"}[5m]) # 错误率 rate(http_requests_total{job="manual-trigger",code=~"4.."}[5m]) / rate(http_requests_total{job="manual-trigger"}[5m]) # P95 延迟 histogram_quantile(0.95, http_request_duration_seconds_bucket{job="manual-trigger"})
8.3 运行安全
✅ 非 root 用户 : UID 1000
✅ 只读配置 : ConfigMap 挂载为 readOnly
✅ 健康检查 : liveness 和 readiness probe
✅ 资源限制 : CPU 和内存限制
九、故障排查 9.1 常见错误 错误 1: Job not found in config 1 2 3 4 { "success" : false , "message" : "Job sddz-e2e-k8s-1.32 not found in config for tess/tessops" }
原因 :
任务名称拼写错误
任务未在 Prow 配置中定义
org/repo 组合错误
解决方法 :
1 2 3 4 5 kubectl get configmap config -n tessprow -o yaml | grep "sddz-e2e-k8s-1.32" cat config/jobs/tess/tessops/tessops-presubmits.yaml | grep "name:"
错误 2: Pull request number is required 1 2 3 4 { "success" : false , "message" : "Pull request number is required for presubmit jobs. Please provide 'pullrequest' parameter." }
原因 : presubmit 任务必须提供 PR 号
解决方法 : 添加 pullrequest 参数或改用 prowtype=postsubmit
9.2 调试技巧 查看 ProwJob 状态 1 2 3 4 5 6 7 8 kubectl get prowjobs -n tessprow --sort-by=.metadata.creationTimestamp | tail -10 kubectl get prowjob <job_name> -n tessprow -o yaml kubectl logs -n tessprow <pod_name>
查看服务日志 1 2 kubectl logs -n tessprow -l app=manual-trigger --tail =100 -f
十、总结 10.1 Manual-Trigger 的核心价值
价值
说明
灵活性
绕过 GitHub webhook,手动控制 CI 任务
可测试性
在不创建 PR 的情况下测试任务
可运维性
用于维护、热修复、紧急部署
可扩展性
支持自定义参数和环境变量
10.2 技术亮点 ✅ 简洁的架构 : 单一 HTTP 服务,职责清晰 ✅ Kubernetes 原生 : 使用 CRD,与 Prow 生态无缝集成 ✅ 类型安全 : Go 强类型,减少运行时错误 ✅ 优雅的错误处理 : 详细的错误信息,易于调试 ✅ 等待机制 : BuildID 轮询,提供完整的用户体验
10.3 适用场景
场景
说明
手动测试
开发新 CI 任务时,手动触发测试
紧急修复
需要立即在特定分支运行测试
批量触发
脚本批量触发多个任务
定制化执行
使用自定义环境变量运行任务
无 PR 测试
在分支上运行 presubmit 类型的任务
10.4 扩展阅读
参考资料
Kubernetes test-infra 仓库
Prow 官方文档
Manual-Trigger 源码
ProwJob API 定义
作者 : Tashen日期 : 2026-03-28标签 : #Kubernetes #Prow #CI/CD #Go #DevOps
💡 提示 : 如果你在使用 Manual-Trigger 过程中遇到问题,欢迎在评论区讨论!