runner

package
v1.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 21, 2026 License: MIT Imports: 24 Imported by: 0

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

View Source
const (
	CheckOK    = preflight.LevelOK
	CheckWarn  = preflight.LevelWarn  // 不影响启动,但很可能在跑 Job 时出问题
	CheckError = preflight.LevelError // 当前配置下基本可以确定跑不起来
)
View Source
const (
	RegistrationResultFile = ".registration_result.json"
	GitHubStatusFile       = ".github_status.json"
)

与 handler 写入的文件名一致,供 cron 与 API 读取

View Source
const AgentTokenFile = ".agent_token"

AgentTokenFile Runner 安装目录下保存 Agent 令牌的文件名

View Source
const HostDockerSocket = "/var/run/docker.sock"

HostDockerSocket job_docker_backend=host-socket 时挂载进 Runner 容器的宿主机 Docker socket

View Source
const RunnerDirMode = 0o700

RunnerDirMode 是 Runner 安装目录的权限:只有属主可进入。 目录里有 GitHub 凭据,见 EnsureRunnerDir 的说明。

Variables

This section is empty.

Functions

func CallAgentStart

func CallAgentStart(ctx context.Context, containerName string, port int, token string) error

CallAgentStart 请求 Runner 容器内 Agent 的 POST /start。 token 为空时不带鉴权头,兼容本特性之前创建的容器。

func ConfigScriptName

func ConfigScriptName() string

ConfigScriptName 返回当前系统的配置脚本名

func ContainerName

func ContainerName(name string) string

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

func EnsureAgentToken(installDir string) (string, error)

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

func EnsureRunnerDir(cfg *config.Config, name, subPath string) (string, error)

EnsureRunnerDir 确保 runner 目录存在并返回路径,且必须在 base_path 之下(防路径穿越)

func GitHubSettingsURL

func GitHubSettingsURL(targetType, target string) string

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 ProbeFixCommand

func ProbeFixCommand(t ProbeErrorType) string

ProbeFixCommand 返回可能有副作用的修复命令。

func ProbeSuggestion

func ProbeSuggestion(t ProbeErrorType) string

ProbeSuggestion 返回给调用方的简短排障建议。

func ReadAgentToken

func ReadAgentToken(installDir string) string

ReadAgentToken 读取令牌;文件不存在或内容为空时返回空字符串。 空字符串表示「该 Runner 没有令牌」,此时 Manager 不带鉴权头、Agent 也不校验, 以兼容本特性之前创建的容器。

func RecreateRunnerContainer

func RecreateRunnerContainer(ctx context.Context, cfg *config.Config, runnerName, installDir string) error

RecreateRunnerContainer 无条件按当前配置重建容器,正在运行也照删。 用于「容器在跑、但配置已经变了」这种只能由人来决定何时中断的情况。

func RemoveRunnerContainer

func RemoveRunnerContainer(ctx context.Context, runnerName string) error

RemoveRunnerContainer 停止并删除 Runner 容器(移除 runner 时调用)

func RunScriptName

func RunScriptName() string

RunScriptName 返回当前系统的运行脚本名

func RunnerImages

func RunnerImages(cfg *config.Config) []string

RunnerImages 返回配置中用到的全部 Runner 镜像(去重,保持稳定顺序)。 自 items[].container_image 支持按 Runner 覆盖后,镜像可能不止一个。

func Start

func Start(installDir string) error

Start 在 installDir 下后台启动 runner(执行 run.sh/run.cmd) 将 installDir 转为绝对路径,避免相对路径在 exec 时随进程 CWD 解析导致找不到 run.sh

func StartIfInstalled

func StartIfInstalled(ctx context.Context, cfg *config.Config, name, installDir string) error

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

func Stop(installDir string) error

Stop 向该安装目录下的 Runner 进程发送 SIGTERM。

runnerproc.Find 把监护脚本排在监听器之前,这里按序发信号:先让 run.sh 退出, 它的 while 循环才不会在监听器被终止后又拉起一个新的。 一个都没找到时返回错误——调用方(界面上的「停止」)需要知道没停成。

func StopRunnerContainer

func StopRunnerContainer(ctx context.Context, runnerName string) error

StopRunnerContainer 停止容器(不删除,便于下次 start)

func WarnAgentTokenUnavailable

func WarnAgentTokenUnavailable(installDir string, err error)

WarnAgentTokenUnavailable 令牌拿不到时告警。状态路径每次列表都会调用 EnsureAgentToken, 不能每次都打;但也不能完全不打——拿不到令牌意味着这个 Runner 的 Agent 不鉴权, 而且 agent_token 漂移检查会因为「手头没有令牌」而跳过自己,界面上什么都看不到。

func WriteGitHubStatus

func WriteGitHubStatus(installDir string, registered, busy *bool, checkErr string) error

WriteGitHubStatus 由 cron 调用,写入 GitHub 检查结果到 runner 目录 WriteGitHubStatus 记录一次 GitHub 查询的结论。 registered 为 nil 表示这次没查出答案,checkErr 说明原因; 「查不出来」不可以写成 false——那会在界面上变成一句确定的「未显示」。

Types

type AgentStatus

type AgentStatus struct {
	Status  string `json:"status"`
	Running bool   `json:"running"`
}

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 CheckLevel

type CheckLevel = preflight.Level

CheckLevel 自检结果级别

type CheckResult

type CheckResult = preflight.Result

CheckResult 单项自检结果

func Preflight

func Preflight(ctx context.Context, cfg *config.Config) []CheckResult

Preflight 按当前配置执行一组只读自检,不修改任何状态。 返回结果按检查顺序排列;调用方自行决定是记录日志还是返回给界面。

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) GitHubNo

func (r RunnerInfo) GitHubNo() bool

GitHubNo 表示查到了答案且答案是「没有」

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 目录状态

const (
	StatusUnknown   Status = "unknown"
	StatusInstalled Status = "installed" // 已配置(存在 .runner 等)
	StatusNew       Status = "new"       // 仅目录存在,未注册
	StatusMissing   Status = "missing"   // 目录不存在
)

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)就写不进去了。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL