bootstrap

package
v0.33.5 Latest Latest
Warning

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

Go to latest
Published: Nov 4, 2026 License: Apache-2.0 Imports: 101 Imported by: 0

README

bootstrap/ - 服务初始化(库入口)

此目录存放服务启动时的初始化代码。

文件说明

  • bootstrap.go - 初始化入口,协调各组件的初始化顺序

初始化顺序

  1. 配置 - 加载配置文件
  2. 日志 - 初始化日志系统
  3. 数据库 - 建立数据库连接
  4. 缓存 - 建立 Redis 连接
  5. 服务器 - 创建 HTTP 服务器并注册路由

使用方式

// main.go
func main() {
    h, err := bootstrap.Bootstrap("/path/to/config.yaml")
    if err != nil {
        log.Fatal(err)
    }
    defer bootstrap.Cleanup()

    h.Spin()
}

添加新组件初始化

// bootstrap.go
func Bootstrap(configPath string) (*server.Hertz, error) {
    // ... 现有初始化

    // 添加新组件
    if err := initNewComponent(); err != nil {
        return nil, err
    }

    // ...
}

清理资源

func Cleanup() {
    logger.Sync()
    _ = db.Close()
    _ = redis.Close()
    // 添加新组件的清理
}

Documentation

Overview

API Key 临期站内通知(波次3):定期扫描 7 天内到期的有效 Key, 给属主发站内信(带 Webhook 外推),并打去重标记。 通知失败不阻塞扫描(下轮重试);标记成功才视为已通知,绝不重复打扰。

datamigrate.go — 启动期数据迁移(2026-10-26 兼容层清理)。

为什么要 Go 侧迁移而不是 migrations/*.sql: 版本化迁移只在 database.migrate=true 时执行;默认路径走 GORM AutoMigrate, 而 AutoMigrate **不会**做数据回填(也不会删列)。本轮退役的兼容层里有 「存量数据必须先补齐,代码才能删掉兜底」的部分(子表回填),必须两条路径 都生效 ⇒ 实现为幂等的 Go 侧迁移。

全部步骤都幂等:可重复执行,失败只记日志不阻断启动(数据类迁移失败不应 让站点起不来;但读取侧会因缺数据而报错,故日志必须响亮)。

setup_gate.go —— 首启向导(/setup)的暴露面加固(2026-10-22)。

问题

/setup 是**无鉴权**的建号端点,唯一的护栏是「库里还没有 admin」。 ⇒ 一个尚未初始化的实例,**任何能访问到它的人都可以抢先完成首启**并拿到 管理员(setup-race)。此前四个打包面把「没配密码 → 走 /setup」设为默认 (hub/ugreen/terramaster 的 compose entrypoint、qnap 的 prepare_env), 这条默认把暴露面从「弱口令 admin/admin123」换成了「先到先得的管理员」—— 在**内网**是体验升级,在**公网**是新的暴露面。

三道加固(都不改变「不配密码也能用」的默认体验)

  1. **可选令牌**:设 PB_SETUP_TOKEN 后,建号端点需带匹配的 X-Setup-Token 头 (或 ?token= 查询参数 —— 便于把「服务启动日志里的那条链接」直接粘进 浏览器完成首启)。未设 = 不要求(保持默认体验不变)。
  2. **按 IP 失败锁定**:复用 middleware.Lockout(Redis 共享计数,多副本 生效;无 Redis 退进程内)。**只把「令牌不匹配」计为失败**——校验 参数不合法(弱口令等)是用户自己的输入问题,不该把他锁在门外。
  3. **启动告警**:未初始化且未设令牌时,响亮提示暴露面与两种处置办法。

为什么不用「必须令牌」

强制令牌会把「打开浏览器就走完首启」这条最顺的路变成「先翻日志抄令牌」, 与「多部署方式下的第一天体验」这个目标相反。故做成**可选加固**: 内网自托管(NAS)默认零摩擦;公网部署按需一行 env 打开。

Index

Constants

View Source
const EnvAdminPasswordResetFile = "PB_ADMIN_PASSWORD_RESET_FILE"

EnvAdminPasswordResetFile 一次性「管理员口令原地重置」文件路径的环境变量名。

平台层(fnOS/QNAP 向导)投递:把新口令写进该文件,core 启动时消费。 平台层**不碰数据库**(此前是删 admin 行 ⇒ user id 变化 ⇒ 名下数据成孤儿)。

Variables

View Source
var ErrInsecureDefaultAdmin = errors.New("refusing to create default admin with known default password in production mode")

ErrInsecureDefaultAdmin 生产模式下拒绝以已知默认口令创建默认管理员时返回 (errors.Is 可判)。逃生门:PB_ADMIN_PASSWORD 注入强密码,或 PB_DISABLE_DEFAULT_ADMIN=true 走 /setup 首启向导。

Functions

func ApplyPendingAdminPasswordReset added in v0.33.5

func ApplyPendingAdminPasswordReset(database *gorm.DB) error

ApplyPendingAdminPasswordReset 原地重置管理员口令(2026-11-04,fnos#182)。

背景:平台层此前的「重置管理员口令」= 删 users 表 admin 行 + core 启动时按 PB_ADMIN_PASSWORD 重建 ⇒ admin 的 **user id 变化** ⇒ 名下 file_codes / user_api_keys 全部成孤儿(真机 88 份分享 + 3 把 Key)。

⇒ 口令哈希与「改密」语义归 core(bcrypt 唯一真相源):平台层只投递新口令 (一次性文件),core **原地**改哈希 ⇒

① admin 的 user id 不变 ⇒ 名下分享/Key 归属不变(不再孤儿化);
② 旧 API Key 继续有效(API Key 鉴权不校验会话纪元,见 pkg/middleware/apikey.go);
③ 旧 JWT 失效(会话纪元 +1,与 app/user.ChangePassword 同口径)。

一次性语义:消费后即删除该文件 ⇒ 不会把用户后来在管理台改的口令反复冲掉。 文件内容 = 新口令明文(末尾换行会被裁掉;空口令拒绝执行——否则管理员被锁在门外)。

无待处理文件 ⇒ 直接返回(正常启动路径)。

func Bootstrap

func Bootstrap(configPath string) (*server.Hertz, error)

Bootstrap 应用程序启动入口(兼容入口,等价于无选项的 BootstrapWithOptions)。 configPath 为空时依次回退到 CONFIG_PATH 环境变量、默认 configs/config.yaml。

func BootstrapWithOptions

func BootstrapWithOptions(configPath string, opts ...Option) (*server.Hertz, error)

BootstrapWithOptions 带函数选项的启动入口。 可用选项见 options.go(如 WithStaticDir 覆盖前端静态资源目录)。

func CORS

func CORS() app.HandlerFunc

CORS 跨域中间件(配置化)。

安全策略:

  • 配置了 allow_origins 白名单时,仅放行白名单内的 Origin(生产推荐)
  • 未配置白名单时,退化为反射 Origin(便于本地开发,等同于宽松模式)
  • allow_credentials=true 时,绝不返回 "*",而是精确匹配的 Origin

同时允许 X-Trace-Id / X-API-Key 等自定义请求头跨域。 allow_origins 来源:yaml 的 security.cors.allow_origins(数组)或 环境变量 PB_CORS_ALLOW_ORIGINS(逗号分隔,如 "https://a.com,https://b.com")。

func Cleanup

func Cleanup()

Cleanup 清理资源(优雅停机编排,oekit core/lifecycle——Manager 原语自 logx 迁来,logx 依赖随之移除):teardown 先取消后台任务根 ctx(拦下一轮),随后 各资源按注册序、各自独立超时预算排水——panic 隔离,单个资源卡死不拖垮整体 停机;在飞轮次由 bg-tasks 预算内等其跑完。停机失败经 Errors() 落回 zap (Manager 内部日志走自身门面,宿主观测以 Errors 为准)。

func CreateDefaultAdmin

func CreateDefaultAdmin(database *gorm.DB, cfg *conf.AppConfiguration) error

CreateDefaultAdmin 创建默认管理员。

密码来源(优先级):PB_ADMIN_PASSWORD 环境变量 > 默认 admin123。 密码用 bcrypt 现场哈希(此前硬编码的哈希与 admin123 不匹配,导致管理员无法登录)。 生产模式(app.production 或 server.mode=release)下若未注入 PB_ADMIN_PASSWORD 且库中无管理员,返回 ErrInsecureDefaultAdmin 由调用方终止启动——已知弱口令 不允许上线(2026-10-08 审计:此前仅 Warn,与 JWT secret 的全环境 fail-fast 不对称)。 仅在"即将创建"时校验:库中已有 admin 的存量部署升级不受影响(每次启动都查会 打断未配 env 的老部署)。cfg 允许 nil(视为非生产,保持旧行为)。

func InitDatabase

func InitDatabase(config *conf.DatabaseConfig) (*gorm.DB, error)

InitDatabase 初始化数据库。

迁移策略(按配置):

  • database.migrate=true:先执行版本化迁移(migrations/*.sql),适合生产/需要版本控制
  • database.auto_migrate(默认 true):GORM AutoMigrate,开发友好、自动补表/列
  • 两者可共存:版本化迁移建表后,AutoMigrate 兜底补充新字段

func NewOfflineBackupService added in v0.32.38

func NewOfflineBackupService(cfg *conf.AppConfiguration, database *gorm.DB) (*backup.Service, error)

NewOfflineBackupService 构造**离线**备份服务(导出 / 恢复用;不启动 HTTP 服务)。

为什么是「离线」而不是管理面端点

导出与恢复天然是**同机**操作(要读本地 DB 与本地存储):

· 做成 HTTP 端点 ⇒ 多一层「服务端正在运行 ⇒ 恢复会砸自己的脚」的风险;
· `pb` 是**纯 HTTP 客户端**(刻意不链接 core)⇒ 让 `pb backup` 落地就得把
  core 拖进这个轻量 CLI(体积 + 数十 MB)。

⇒ 形态定为**同机离线子命令**(server 二进制:`pigeonbox backup export` / `pigeonbox restore`),与「恢复必须停服」的事实一致。

前置

调用方必须先完成:InitConfig → conf.SetGlobalConfig → InitDatabase (否则 DB 与存储后端都没就绪)。本函数只做**装配**,不负责初始化。

func ReapplyEnvOverrides added in v0.22.0

func ReapplyEnvOverrides()

ReapplyEnvOverrides 把非空 env 绑定值重新施加到全局 conf(幂等)。

由 config service 的 OnOverlayApplied 钩子在每次 DB overlay 后调用; 启动期因钩子注册晚于首次 RestoreAdminSettings,wiring 在注册后显式补调一次。

⚠️ **copy-on-write**(2026-10-31 更正)

此前本函数**原地改写**调用方传入的 conf 指针(bootstrap 包级 config)。 自全局配置改为 copy-on-write(原子换指针)后:

  • 传入的 bootstrap.config 是**被换掉的旧对象** ⇒ env 覆盖**根本没作用到 当前快照** ⇒ 「env 恒覆盖」这条配置优先级规则**静默失效**;
  • 更糟:旧对象被改后再被 COW 的浅拷贝带走 ⇒ env 覆盖值会**污染后续所有 快照**(看起来像生效了,实际是脏值扩散)。

⇒ 现在走 conf.UpdateGlobalConfig:在**当前快照的副本**上反射改字段, 再原子换指针 ⇒ env 覆盖既生效、又不会造成撕裂读。

func RunDataMigrations added in v0.32.14

func RunDataMigrations(gormDB *gorm.DB)

RunDataMigrations 启动期数据迁移入口(幂等)。

调用时机:InitDatabase 之后(AutoMigrate 已补齐表结构)、对外服务之前 (读取侧必须先于迁移完成——子表是文件清单的唯一真相源)。

Types

type CapabilitySnapshot added in v0.32.30

type CapabilitySnapshot struct {
	// DeploymentMode 部署模式:standalone | public | admin。
	// 多副本拓扑下前端据此知道「本副本是公开面/管理面」,不再各面各猜。
	DeploymentMode string `json:"deployment_mode"`
	// PublicURLSet 站点公网地址是否已配置(base.public_url 或 server.base_url)。
	// 未配置时分享链接按**请求来源**拼 ⇒ 内网部署会把内网 IP 发给外部用户。
	PublicURLSet bool `json:"public_url_set"`
	// SetupTokenRequired 首启建号是否需要令牌(自动生成的一次性令牌或 PB_SETUP_TOKEN)。
	// true = 未初始化实例的 /setup 会被门禁拦下(前端据此提示「令牌在启动日志里」)。
	SetupTokenRequired bool `json:"setup_token_required"`
	// Federation 联邦是否启用(**生效值**:多副本下自动降级关闭)。
	Federation bool `json:"federation"`
	// FederationDelivery 跨站投递是否可用(联邦前台化,2026-11-01)。
	//
	// 语义 = **联邦启用** 且 **登记了至少一个可投递对端** ⇒ 两者缺一即不可用。
	//
	// ⚠️ 公开面下发这一位**不泄露对端身份**(访客只知道「本站支持跨站投递」,
	// 不知道对端是谁、在哪)⇒ 无害;反之 storage_backend / redis_mode /
	// federation 这几项会泄露部署形态,留在管理面。
	FederationDelivery bool `json:"federation_delivery"`
	// RedisMode redis | memory(redis.host 空 = 单机内存模式)。
	RedisMode string `json:"redis_mode"`
	// StorageBackend 当前存储后端(**生效值**,含远端构造失败后的降级结果)。
	StorageBackend string `json:"storage_backend"`
	// StorageDegraded 是否发生降级(配置的后端 ≠ 生效后端 ⇒ 远端构造失败回落 local)。
	// ⚠️ 这是**真实故障的信号**:用户以为在用 S3,实际全落在本地盘上。
	StorageDegraded bool `json:"storage_degraded"`
}

CapabilitySnapshot 描述「本实例实际支持什么」。

⚠️ 字段语义一律是**生效值**(runtime 真实状态),不是配置回显—— 后者会与实际漂移(如后端启动后才起 Redis、远端存储构造失败降级 local)。

type Config

type Config = conf.AppConfiguration

Config 与 internal/conf 包中的统一配置类型互为别名。

func GetConfig

func GetConfig() *Config

GetConfig 获取全局配置

func InitConfig

func InitConfig(configPath string) (*Config, error)

InitConfig 初始化配置。

配置来源优先级(高 → 低):

  1. 环境变量(PB_ 前缀完整名 / 文档化的短名,见 bindEnvironment)
  2. 配置文件(yaml,路径由 configPath 或 CONFIG_PATH 决定)
  3. 代码内默认值(SetDefault)

这样容器化部署(K8s/Docker)可通过 env 注入敏感配置(jwt_secret、db 密码等), 而无需修改镜像内的配置文件,符合 12-factor。

type DiagnosticsDatabase added in v0.32.30

type DiagnosticsDatabase struct {
	// Driver 驱动名(sqlite / mysql / postgres)。
	Driver string `json:"driver"`
	// Reachable 是否可连通(Ping 成功)。
	Reachable bool `json:"reachable"`
	// Error 连通失败原因(reachable=true 时为空)。
	Error string `json:"error,omitempty"`
}

DiagnosticsDatabase 数据库连通性。

type DiagnosticsRedis added in v0.32.30

type DiagnosticsRedis struct {
	// Mode redis | memory。
	Mode string `json:"mode"`
	// Address Redis 地址(mode=memory 时为空)。
	Address string `json:"address,omitempty"`
}

DiagnosticsRedis 缓存模式。

type DiagnosticsReport added in v0.32.30

type DiagnosticsReport struct {
	// Version **构建版本**(oekit/core/version.Version,由 ldflags 注入)。
	// ⚠️ 不是 config.App.Version —— 那是 yaml 里的 app.version(缺省 1.0.0,
	// 常年不更新)⇒ 真机上 doctor 会把 4.2.0 的实例报成「1.0.0」。
	// 版本披露用于 CVE 匹配,必须与 /version 端点**同源**。
	Version string `json:"version"`
	// BuildCommit 构建 commit(版本对齐用)。
	BuildCommit string `json:"build_commit,omitempty"`
	// BuildTime 构建时间。
	BuildTime string `json:"build_time,omitempty"`
	// DeploymentMode 部署模式:standalone | public | admin。
	DeploymentMode string `json:"deployment_mode"`
	// ServerTime 服务端当前时间(CLI 用它与本机时钟比对 ⇒ 时钟偏差)。
	ServerTime time.Time `json:"server_time"`
	// Database 数据库连通性。
	Database DiagnosticsDatabase `json:"database"`
	// Storage 存储后端状态。
	Storage DiagnosticsStorage `json:"storage"`
	// Redis 缓存模式。
	Redis DiagnosticsRedis `json:"redis"`
	// Site 站点对外地址状态。
	Site DiagnosticsSite `json:"site"`
	// Setup 首启状态。
	Setup DiagnosticsSetup `json:"setup"`
	// Capabilities 能力快照(生效值)。
	Capabilities CapabilitySnapshot `json:"capabilities"`
}

DiagnosticsReport 部署自检报告(一份快照)。

type DiagnosticsSetup added in v0.32.30

type DiagnosticsSetup struct {
	// Initialized 是否已初始化(已有 admin)。
	Initialized bool `json:"initialized"`
	// TokenRequired 建号是否需要令牌。
	TokenRequired bool `json:"token_required"`
	// TokenSource 令牌来源:env | auto | none。
	TokenSource string `json:"token_source"`
}

DiagnosticsSetup 首启状态。

type DiagnosticsSite added in v0.32.30

type DiagnosticsSite struct {
	// PublicURL 生效的站点公网地址(未配置时为空)。
	PublicURL string `json:"public_url,omitempty"`
	// PublicURLSet 是否已配置。
	PublicURLSet bool `json:"public_url_set"`
}

DiagnosticsSite 站点对外地址状态。

type DiagnosticsStorage added in v0.32.30

type DiagnosticsStorage struct {
	// Backend **生效**后端(远端构造失败会降级 local ⇒ 这里与 Configured 可能不同)。
	Backend string `json:"backend"`
	// Configured 配置里指定的后端。
	Configured string `json:"configured"`
	// Degraded 是否降级(Backend ≠ Configured ⇒ 远端构造失败回落 local)。
	Degraded bool `json:"degraded"`
	// DataPath 本地存储根目录(远端模式下仍承载分片合并副本)。
	DataPath string `json:"data_path,omitempty"`
	// InitError 降级原因(无降级时为空)。
	InitError string `json:"init_error,omitempty"`
}

DiagnosticsStorage 存储后端状态。

type Option

type Option func(*Options)

Option 是 Bootstrap 的可选配置项(函数选项模式)。

用法:

h, err := bootstrap.BootstrapWithOptions(configPath,
    bootstrap.WithStaticDir("/opt/pb/ui"),
)

兼容性:Bootstrap(configPath) 等价于无选项的 BootstrapWithOptions, 既有调用方(server、fnos)不受影响。

func WithStaticDir

func WithStaticDir(dir string) Option

WithStaticDir 覆盖前端静态资源目录(空串忽略,保持默认)。

type Options

type Options struct {
	// StaticDir 是前端构建产物的根目录。
	// 该目录需包含 index.html 与 assets/(Vite 构建产物布局)。
	// 默认 "./static"(相对进程工作目录)。
	// 典型消费者:fnos 单容器部署时把前端内嵌到自定义路径。
	StaticDir string
}

Options 控制 Bootstrap 的行为。零值字段取默认。

type PublicCapabilities added in v0.32.31

type PublicCapabilities struct {
	// DeploymentMode 部署模式:standalone | public | admin。
	DeploymentMode string `json:"deployment_mode"`
	// PublicURLSet 站点公网地址是否已配置。
	PublicURLSet bool `json:"public_url_set"`
	// SetupTokenRequired 首启建号是否需要令牌。
	SetupTokenRequired bool `json:"setup_token_required"`
	// FederationDelivery 跨站投递是否可用(联邦前台化,2026-11-01)。
	//
	// ⚠️ 只暴露「能不能用」,**不暴露对端身份**(谁、在哪)⇒ 访客看得见
	// 功能入口,但看不到管理员配了对端。
	FederationDelivery bool `json:"federation_delivery"`
}

PublicCapabilities 公开面(/api/config)下发的能力子集。

⚠️ **独立结构体**,不复用 CapabilitySnapshot:复用会把管理面字段 (redis_mode / storage_backend / …)以**空串**形态泄到公开 JSON 里 ⇒ 客户端读到的 `redis_mode: ""` 既不是事实、也不是"未知"的可辨识形态。

Jump to

Keyboard shortcuts

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