安装向导:把部署的第一道坎搬进浏览器

在此之前,没有数据库连接信息程序就直接退出——部署的第一道坎。现在它会转入安装模式,打开浏览器填完四步就能装上,而且不需要手动重启。

在此之前,Lumo 的部署流程是这样的:把二进制拷上去,设好 LUMO_DATABASE_DSN,启动。听起来不难。

但如果没有设那个变量呢?程序直接退出。它不会说「你还没配数据库,要不要我帮你建一个」,只会打印一条用法说明然后消失。对一个已经会写 systemd unit 的人来说这不算什么,但这种人不需要安装向导;需要安装向导的人在这一步就走了。

这道坎值得单独搬开,因为它是整个部署链上唯一一处「你必须先知道答案才能开始」的地方。

向导必须活在没有任何东西存在的时候

第一版的直觉是把向导做成后台的一个页面。这条路走不通,原因很具体:

后台的三个接口平面默认强制认证。而认证要查库——会话表、用户表。在向导该出现的那个时刻,库可能压根不存在,或者存在但还没有任何用户。这时候认证中间件会先于业务逻辑返回 401,而且返回得很合理:它没有做错任何事。

所以安装端点被注册在一个没有中间件、没有前缀的根平面上,并且无条件注册

GET  /api/v1/install/status
POST /api/v1/install/database/test
POST /api/v1/install/apply

无条件这点很重要。这些端点在站点装好之后是永远锁死的,很容易顺手写成「装好了就不注册」。但那样一来它们就不会出现在导出的 OpenAPI 规范里,Console 的 TypeScript 类型也就生成不出来——而那套类型是自动生成的,手写的第二份类型是生态项目的慢性病。

把它们始终注册进规范、锁在处理器里,是更省事的做法。

安装模式下只挂三样东西:Console 的静态资源、上面这三个端点、健康检查。根路径和 /console 一律重定向到 /console/install,所以站长拿到地址打开就是向导,不用知道那个长路径。

连接串写在哪

写进 data/install.json,权限 0600,而不是写进 config.yaml

这条不是偏好,是项目的一条硬线:数据库 DSN 只走环境变量,不进配置文件。 配置文件是要被拷来拷去、贴进工单、塞进版本库的东西,口令不该跟它一起走。

但向导的产物又必须能持久化,否则重启就回到没装的状态。于是给它一个单独的文件,权限收紧,并且明确它是「安装产物」而不是配置。

配置链因此变成四段:

默认值 < config.yaml < 环境变量 < 安装产物兜底

环境变量排在安装产物前面,这一条是刻意留的出口:运维随时可以用 LUMO_DATABASE_DSN 盖掉向导写下的值,不需要去改那个文件。反过来,接管一个已有站点时,install.json 里的值只在没有任何环境变量时说上话。

装完不要让人手动重启

向导的最后一步是跑迁移、建管理员、写站点设置。做完之后,那个正在运行的进程手里还拿着一份「数据库连接串为空」的配置。

最省事的做法是提示「请重启服务」。这个做法被否掉了,因为它把一个纯技术的步骤变成了站长的问题——而站长刚刚才从「手动配环境变量」那一步被解救出来。

实际做法是让 serve 里跑一个循环:

读到配置 → 没有 DSN? → 起一个只见安装端点的服务,等它完成
                              ↓ 完成
                        优雅关闭 → 回到循环开头,重新读配置
                              ↓ 这次有 DSN 了
                        起正常站点

同一个进程,一轮循环,不需要重启。 在途请求会先写完再退出,模块按逆序关闭(邮件队列在这里做限时排空)。

循环里踩过一个坑,值得记下来:配置里的安装产物必须在环境变量应用之后才读。第一版是在之前读的,结果是安装完成、进程重启、配置重新加载——然后它又回到了向导模式。原因是读 install.json 的路径来自 dataDir,而 dataDir 可能正是由 LUMO_DATA_DIR 指定的:在环境变量生效前去算那个路径,算出来的是另一个位置。

修法就是把那一步挪到环境变量之后,并且加一句提前返回:连接串已经有值时,根本不看安装产物。 这样「环境变量优先」这条规则在代码里只写一次,读的人不会看漏。

「测试连接」要返回真的东西

向导第二步是填数据库信息。这一步的「测试连接」如果只回一句「连接成功」,那它只是个安慰剂——它没有回答站长真正在担心的问题。

所以它返回目标库上真实查出来的三件事

  • version() 的结果,也就是 PostgreSQL 的真实版本号
  • 数据库的字符编码
  • 已经有多少个用户

第三项决定了向导接下来的表述。库里已经有数据时,界面不会假装在「新建」,而是明说这是在接管一个已有站点——因为把接管说成新建,是在鼓励人在生产库上按下一步。

超时是算出来的,不是拍的

安装的第四步要跑完十三个来源的迁移、建超级管理员、写站点设置。这一整套被套了一个 45 秒的期限。

45 这个数字不是感觉来的:服务端的 WriteTimeout 默认是 60 秒。超过 60 秒的等待毫无意义——连接那时候已经被服务端自己掐了,站长看到的是一个没有响应,而不是一个失败理由。 所以安装的期限必须短于它,留出写响应的余量。

同一条推演也适用于连接测试:探活单独给 20 秒,因为「库连不上」和「库上跑迁移跑了一半」是两种完全不同的失败,不该共用一个期限。

一个只在真实数据库上才会出现的 bug

安装流程复用了正常启动的模块装配,Provision 钩子拿到的是服务自己的配置对象 s.cfg。而在安装模式下,那份配置里的 Database.DSN 是空的——这正是进入安装模式的原因。

于是迁移锁开了一个独立的连接池,去连「本地默认的那个库」。现象很怪:表单里填的是 A 库,报错说的是另一个不存在的库。

修法只有一行:

provisionCfg := s.cfg
provisionCfg.Database.DSN = dsn

记下来是因为这属于「配置对象兼作状态」的典型故障:一个字段同时表示「系统当前配置」和「用户刚刚填的东西」,两者在安装模式下必然不一致。

界面上两处只有走查才会发现的问题

向导做完之后,用真实浏览器把整个过程走了一遍(桌面与窄屏两种宽度、故意填错的输入、测试连接、执行安装、装完后登录后台)。两处缺陷是这么找出来的,jsdom 冒烟用例看不见:

  • 步骤条里「执行安装」用了勾号图标,和同一屏「已完成」状态的勾撞了语义。第一眼读过去像是「已经装完了」。
  • 管理员步骤的口令说明和校验错误都写「至少 8 位」,等于把同一句话在同一屏上说了两遍。说明被改成了正常该说的话(建议更长、混用字符类),把「至少 8 位」留给校验错误。

这类问题没有一行代码是错的,单测也永远是绿的。

最后

安装接口在站点装好之后永久锁定,再调用返回 409。

如果你更愿意用编排系统管凭据——systemd、Kubernetes、Ansible——那么预先设好 LUMO_DATABASE_DSN 再启动就能完全跳过向导,用 lumo admin create-user 建第一个管理员。向导不是必经之路,它只是那条路的替代品。

收藏

评论

还没有评论,来说两句。