Documentation
¶
Overview ¶
Agent 调用令牌:Runner 容器内的 Agent 暴露 /start、/stop 等控制接口, 同一 docker 网络内的任何容器都能访问。令牌用于确认调用方确实是 Manager。
令牌存放在 Runner 安装目录下,该目录既被 Manager 访问(宿主机侧路径), 也挂载进 Runner 容器(/runner),因此无需通过环境变量传递, Manager 重启后也能重新读到同一个令牌。
容器模式:通过 Docker CLI 与 Runner 容器内 Agent 实现 C/S 控制与状态查询。 本包负责 Runner 容器的创建/启停/删除及与 Agent 的 HTTP 通信;Manager 仅编排,不承载 Runner 进程。
容器配置漂移检测。
Runner 容器的镜像、网络、挂载目录、Job Docker 后端全都只在 docker create 那一刻定下来, 之后改配置对已经存在的容器没有任何影响——docker start 只是把原样的容器再拉起来。 于是「改了配置却不生效」成了最容易踩的坑:以 host-socket 建出来的 Runner,在切到 dind 之后依旧挂着宿主机 socket,本想收紧的隔离根本没发生,而界面上一切正常。
这里把「按当前配置该建成什么样」抽成 containerSpec:创建参数与漂移比对都由它产出, 两边不会各写一套。比对的是 docker inspect 回来的实际值,所以旧版本建的容器同样适用。
启动自检:把「配置错了但要等 Job 跑挂才发现」的问题提前到 Manager 启动那一刻暴露。 典型如 runner-net 未创建、挂载目录不属于 UID 1001、docker.sock 不可达、Runner 镜像不存在。
Index ¶
- Constants
- func CallAgentStart(ctx context.Context, containerName string, port int, token string) error
- func ConfigScriptName() string
- func ContainerName(name string) string
- func ContainerState(ctx context.Context, containerName string) (exists bool, status string, err error)
- func EnsureAgentToken(installDir string) (string, error)
- func EnsureRunnerDir(cfg *config.Config, name, subPath string) (string, error)
- func GitHubSettingsURL(targetType, target string) string
- func LogPreflight(results []CheckResult)
- func ManagerDockerHostIsDind() bool
- func ProbeCheckCommand(t ProbeErrorType) string
- func ProbeFixCommand(t ProbeErrorType) string
- func ProbeSuggestion(t ProbeErrorType) string
- func ReadAgentToken(installDir string) string
- func RecreateRunnerContainer(ctx context.Context, cfg *config.Config, runnerName, installDir string) error
- func RemoveRunnerContainer(ctx context.Context, runnerName string) error
- func RunScriptName() string
- func RunnerImages(cfg *config.Config) []string
- func Start(installDir string) error
- func StartIfInstalled(ctx context.Context, cfg *config.Config, name, installDir string) error
- func StartRunnerContainer(ctx context.Context, cfg *config.Config, runnerName, installDir string) error
- func Stop(installDir string) error
- func StopRunnerContainer(ctx context.Context, runnerName string) error
- func WarnAgentTokenUnavailable(installDir string, err error)
- func WriteGitHubStatus(installDir string, registered, busy *bool, checkErr string) error
- type AgentStatus
- type CheckLevel
- type CheckResult
- type ProbeError
- type ProbeErrorType
- type ProbeInfo
- type RunnerInfo
- type Status
Constants ¶
const ( CheckOK = preflight.LevelOK CheckWarn = preflight.LevelWarn // 不影响启动,但很可能在跑 Job 时出问题 CheckError = preflight.LevelError // 当前配置下基本可以确定跑不起来 )
const ( RegistrationResultFile = ".registration_result.json" GitHubStatusFile = ".github_status.json" )
与 handler 写入的文件名一致,供 cron 与 API 读取
const AgentTokenFile = ".agent_token"
AgentTokenFile Runner 安装目录下保存 Agent 令牌的文件名
const HostDockerSocket = "/var/run/docker.sock"
HostDockerSocket job_docker_backend=host-socket 时挂载进 Runner 容器的宿主机 Docker socket
const PreflightLogMarker = "[preflight"
PreflightLogMarker 是每条自检日志的行首标记。
六种语言的文档都把「先看启动自检」写成排障第一步,给出的命令是 `docker compose logs runner-manager | grep '\[preflight'`。这个标记原本是 「[自检 …]」——于是英/法/德/日/韩五份文档里躺着一个非中文用户既打不出、 也看不懂的 grep 关键词。日志正文目前仍是中文(那是更大的一件事), 但至少让「怎么把这些行捞出来」不依赖读者认识汉字。
提成常量是为了让 preflight_test.go 能守住它:文档里的命令依赖这个字面量, 改了它而不改六份文档,排障第一步就会静默地什么都 grep 不到。
const RunnerDirMode = 0o700
RunnerDirMode 是 Runner 安装目录的权限:只有属主可进入。 目录里有 GitHub 凭据,见 EnsureRunnerDir 的说明。
Variables ¶
This section is empty.
Functions ¶
func CallAgentStart ¶
CallAgentStart 请求 Runner 容器内 Agent 的 POST /start。 token 为空时不带鉴权头,兼容本特性之前创建的容器。
func ContainerName ¶
ContainerName 将 runner 名称转为合法容器名,与 config 包规则一致
func ContainerState ¶
func ContainerState(ctx context.Context, containerName string) (exists bool, status string, err error)
ContainerState 返回容器是否存在及其状态(running / exited / created 等)。 容器不存在时返回 exists=false 且不报错——调用方多数只关心「名字是否被占用」。
func EnsureAgentToken ¶
EnsureAgentToken 返回该 Runner 的 Agent 令牌:已存在则读出,否则生成并以 0600 写入。 生成或写入失败时返回错误,调用方可选择降级为不带令牌(保持旧行为)。
启停路径(持 runnerOps 锁)与状态路径(不持锁,因为它每次列表都要跑,不能被一次几秒的 docker create 挡住)会同时走到这里,所以创建必须是「单一胜者」:各生成一个的话, 后落盘的会覆盖先落盘的,而注入进容器的是先落盘的那个——Agent 认自己 env 里的 A、 Manager 之后从盘上读到 B,从此永远 401,且容器 env 非空,agent_token 漂移检查也修不回来。
做法是「先写临时文件,内容齐全后再原子地挂上正式名字」:os.Link 不会覆盖已有的名字, 所以既是单一胜者,又保证 .agent_token 这个名字要么不存在、要么内容完整—— 不存在「已创建、还没写完」的中间态。
刻意不去回收空文件。回收必然要 unlink,而「等若干毫秒还是空就判定写入者已死」 是不成立的:写入者可能只是被调度走了,或者文件系统写入卡了一下(宿主机负载、 NFS、cgroup IO 限流都会)。此时 unlink 就会把一个还活着的写入者的 inode 摘掉, 它照样把 A 写进那个已经没有名字的 inode 并返回 A,而盘上留下的是回收者发布的 B—— 正是上面那种分裂。本实现既然不产生空文件,也就没有需要回收的东西。
func EnsureRunnerDir ¶
EnsureRunnerDir 确保 runner 目录存在并返回路径,且必须在 base_path 之下(防路径穿越)
func GitHubSettingsURL ¶
GitHubSettingsURL 拼出该目标在 GitHub 上的 Actions Runners 设置页地址。
指向列表页而不是 /runners/<id> 那样的单个 Runner 页:Runner 的数字 id 只在 查询 GitHub 时才拿得到,没配 PAT 的部署根本没有它,做成「有时能跳、有时跳不了」 不如始终跳到那一页——要找的 Runner 就在上面。
目标非法时返回空串,由调用方决定不渲染链接。这里不能返回一个半成品 URL: 界面上一个点了 404 的链接比没有链接更难判断问题出在哪。
逐段转义,理由与 githubcheck.runnersEndpoint 相同:ValidateTarget 只管 / 与空值, 不限制字符集。前缀是写死的 https://github.com,所以协议不可能被 target 改写—— 这也是模板敢把它放进 href 的前提。
func LogPreflight ¶
func LogPreflight(results []CheckResult)
LogPreflight 将自检结果按级别打到日志,error 项额外给出修复建议
func ManagerDockerHostIsDind ¶
func ManagerDockerHostIsDind() bool
ManagerDockerHostIsDind 供启动时检查:若为 true 且开启容器模式,Manager 无法创建 Runner 容器
func ProbeCheckCommand ¶
func ProbeCheckCommand(t ProbeErrorType) string
ProbeCheckCommand 返回只读检查命令(无副作用)。
func ReadAgentToken ¶
ReadAgentToken 读取令牌;文件不存在或内容为空时返回空字符串。 空字符串表示「该 Runner 没有令牌」,此时 Manager 不带鉴权头、Agent 也不校验, 以兼容本特性之前创建的容器。
func RecreateRunnerContainer ¶
func RecreateRunnerContainer(ctx context.Context, cfg *config.Config, runnerName, installDir string) error
RecreateRunnerContainer 无条件按当前配置重建容器,正在运行也照删。 用于「容器在跑、但配置已经变了」这种只能由人来决定何时中断的情况。
func RemoveRunnerContainer ¶
RemoveRunnerContainer 停止并删除 Runner 容器(移除 runner 时调用)
func RunnerImages ¶
RunnerImages 返回配置中用到的全部 Runner 镜像(去重,保持稳定顺序)。 自 items[].container_image 支持按 Runner 覆盖后,镜像可能不止一个。
func Start ¶
Start 在 installDir 下后台启动 runner(执行 run.sh/run.cmd) 将 installDir 转为绝对路径,避免相对路径在 exec 时随进程 CWD 解析导致找不到 run.sh
func StartIfInstalled ¶
StartIfInstalled 若已注册则启动:容器模式调 StartRunnerContainer,否则调 Start。供 main 与 handler 统一“已注册未运行则启动”逻辑
func StartRunnerContainer ¶
func StartRunnerContainer(ctx context.Context, cfg *config.Config, runnerName, installDir string) error
StartRunnerContainer 若容器不存在则创建并启动,若存在则 start;创建时挂载 installDir 到 /runner。 已存在的容器若创建参数与当前配置不一致(换了镜像、网络、挂载目录或 Job Docker 后端), 会先删除再按新配置重建——但仅限已停止的容器,正在运行的不动,见 startRunnerContainer。
func Stop ¶
Stop 向该安装目录下的 Runner 进程发送 SIGTERM。
runnerproc.Find 把监护脚本排在监听器之前,这里按序发信号:先让 run.sh 退出, 它的 while 循环才不会在监听器被终止后又拉起一个新的。 一个都没找到时返回错误——调用方(界面上的「停止」)需要知道没停成。
func StopRunnerContainer ¶
StopRunnerContainer 停止容器(不删除,便于下次 start)
func WarnAgentTokenUnavailable ¶
WarnAgentTokenUnavailable 令牌拿不到时告警。状态路径每次列表都会调用 EnsureAgentToken, 不能每次都打;但也不能完全不打——拿不到令牌意味着这个 Runner 的 Agent 不鉴权, 而且 agent_token 漂移检查会因为「手头没有令牌」而跳过自己,界面上什么都看不到。
Types ¶
type AgentStatus ¶
AgentStatus 容器内 Agent /status 返回结构
func GetAgentStatus ¶
func GetAgentStatus(ctx context.Context, containerName string, port int, token string) (*AgentStatus, error)
GetAgentStatus 请求 Runner 容器内 Agent 的 /status,超时 5 秒。 token 为空时不带鉴权头,兼容本特性之前创建的容器。
type CheckResult ¶
CheckResult 单项自检结果
type ProbeError ¶
type ProbeError struct {
Type ProbeErrorType
Err error
}
ProbeError 包装底层错误并携带可机器识别的失败类型。
func (*ProbeError) Error ¶
func (e *ProbeError) Error() string
func (*ProbeError) Unwrap ¶
func (e *ProbeError) Unwrap() error
type ProbeErrorType ¶
type ProbeErrorType string
ProbeErrorType 用于标识容器状态探测失败类型,便于 API/UI 分类展示。
const ( ProbeErrorTypeUnknown ProbeErrorType = "unknown" ProbeErrorTypeDockerAccess ProbeErrorType = "docker-access" ProbeErrorTypeAgentHTTP ProbeErrorType = "agent-http" ProbeErrorTypeAgentConnect ProbeErrorType = "agent-connect" )
func DetectProbeErrorType ¶
func DetectProbeErrorType(err error) ProbeErrorType
DetectProbeErrorType 提取探测错误类型,供 handler/API/UI 统一使用。
type ProbeInfo ¶
type ProbeInfo struct {
Error string `json:"error"`
Type string `json:"type"`
Suggestion string `json:"suggestion"`
CheckCommand string `json:"check_command"`
FixCommand string `json:"fix_command"`
}
ProbeInfo 为容器探测失败的结构化信息。
type RunnerInfo ¶
type RunnerInfo struct {
Name string `json:"name"`
Path string `json:"path"`
TargetType string `json:"target_type"`
Target string `json:"target"`
Labels []string `json:"labels"`
Status Status `json:"status"`
InstallDir string `json:"install_dir"`
Running bool `json:"running"` // 进程是否在跑
Probe *ProbeInfo `json:"probe,omitempty"` // 结构化探测信息(error/type/suggestion/check_command/fix_command)
JobDockerBackend string `json:"job_docker_backend"` // 容器模式下 Job 内 Docker 后端:dind / host-socket / none
ContainerDrift string `json:"container_drift,omitempty"` // 容器创建参数与当前配置的差异,非空表示容器是按旧配置建的
RegistrationMessage string `json:"registration_message"` // 最近一次注册结果信息(成功或失败原因)
RegistrationCheckedAt string `json:"registration_checked_at"` // 注册结果时间
RegisteredOnGitHub *bool `json:"registered_on_github"` // cron 通过 GitHub API 检查是否在 GitHub 显示,nil 表示未检查或查不到答案
GitHubCheckAt string `json:"github_check_at"` // 最近一次 GitHub 检查时间
GitHubCheckError string `json:"github_check_error,omitempty"` // 查不到答案时的原因(令牌过期、限流、网络不通等)
GitHubBusy *bool `json:"github_busy,omitempty"` // GitHub 侧该 Runner 是否正在跑 Job,nil 表示不知道(没查、没查到、或没配 PAT)
GitHubURL string `json:"github_url,omitempty"` // 该目标在 GitHub 上的 Actions Runners 设置页,target 非法时为空
}
RunnerInfo 供前端展示的 runner 信息
func GetByName ¶
func GetByName(cfg *config.Config, name string) *RunnerInfo
GetByName 根据名称获取单个 runner 信息,不存在返回 nil;cfg 为 nil 时安全返回 nil
func List ¶
func List(cfg *config.Config) []RunnerInfo
List 根据配置与磁盘状态列出所有 runner。
注意:容器模式下每项的 Running 不可信——Runner 进程在各自的容器里, 本进程扫 /proc 看不到它们。需要真实运行状态的调用方请用 ListWithLiveStatus。
func ListWithLiveStatus ¶
func ListWithLiveStatus(ctx context.Context, cfg *config.Config) []RunnerInfo
ListWithLiveStatus 在 List 之上补齐真实运行状态:容器模式下逐个问容器内的 Agent, 默认模式下 List 已经是本机进程的真实状态,直接返回。
后台的「已注册未运行则拉起」两个循环必须用这个,不能用 List:容器模式下 List 的 Running 恒为 false(Manager 与 Runner 不在同一个 PID namespace), 于是每一轮巡检都会把每个 runner 再拉起一遍。
探测失败时把该项置为 StatusUnknown 而不是保留 installed:拉起的前提是 「确知它没在跑」,Docker 不可达时并不确知,此时什么都不做比反复重启稳妥。
func (RunnerInfo) GitHubBusyYes ¶
func (r RunnerInfo) GitHubBusyYes() bool
GitHubBusyYes 表示上次查询时 GitHub 说它正在跑 Job。 和上面三个同理:GitHubBusy 是 *bool,模板里写 {{if .GitHubBusy}} 会把 指向 false 的指针也当成真,于是「空闲」显示成「忙碌中」。
func (RunnerInfo) GitHubUnknown ¶
func (r RunnerInfo) GitHubUnknown() bool
GitHubUnknown 表示这次检查没能得出答案(与「从未检查」由 GitHubCheckAt 区分)
func (RunnerInfo) GitHubYes ¶
func (r RunnerInfo) GitHubYes() bool
GitHubYes / GitHubNo / GitHubUnknown 供模板判断三态。
模板里不能直接写 {{if .RegisteredOnGitHub}}:html/template 对指针只看是否为 nil, 指向 false 的指针同样为真,于是「GitHub 上没有这个 Runner」会被渲染成「GitHub ✓」。
type Status ¶
type Status string
Status 表示 runner 目录状态
func ContainerRunnerStatus ¶
func ContainerRunnerStatus(ctx context.Context, cfg *config.Config, runnerName, installDir string) (running bool, status Status, drift string, err error)
ContainerRunnerStatus 在容器模式下获取某 runner 的状态:先看容器是否运行,再问 Agent。 同一次 inspect 顺带比对创建参数,drift 非空表示容器是按旧配置建的。
容器不存在或未运行时没有人可问,此时如实回落到磁盘状态(已注册的仍为 StatusInstalled, 界面据此显示「已注册未运行」)。这里曾经不看磁盘、一律返回 StatusInstalled,于是配置里 一个从没注册过的 runner(磁盘上是 new/missing)会被说成「已注册未运行」——界面显示错误, 而后台的「已注册未运行则拉起」会照着给它建容器、发 /start;安装目录压根不存在时, bind mount 的源路径还会被 Docker 以 root 属主创建出来,之后 Manager(UID 1001)就写不进去了。