feat: make reverse proxy operator-managed
Release / release (push) Failing after 10s

This commit is contained in:
Qiufeng
2026-08-17 16:24:32 +08:00
parent aed561c702
commit 7394c9e7e4
20 changed files with 358 additions and 124 deletions
+50 -28
View File
@@ -47,24 +47,32 @@ npm run build
`https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,生产机不执行 `git pull` 或现场编译。 `https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest`,生产机不执行 `git pull` 或现场编译。
安装脚本内置固定的发布公钥指纹,下载的应用包、清单、SBOM 和更新脚本仍须通过 RSA 签名与 SHA-256 校验。 安装脚本内置固定的发布公钥指纹,下载的应用包、清单、SBOM 和更新脚本仍须通过 RSA 签名与 SHA-256 校验。
本版只支持由运维人员预先准备的外部 MySQL 8.4.x;安装器不会安装 MySQL、创建数据库容器或修改现有 本版只支持由运维人员预先准备的外部 MySQL 8.4.x;安装器不会安装 MySQL、创建数据库容器或修改现有
PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.9 发布范围。 PostgreSQL 18。PostgreSQL 兼容开发已冻结,不属于本次 Preview.10 发布范围。
执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq`、Java、 执行命令的机器需预装 `bash`、`sudo`、`curl`、`mktemp` 和 `sha256sum`,并能访问目标 Gitea;`jq`、Java
Nginx 和数据库客户端由安装器补齐。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、 和数据库客户端由安装器补齐。应用固定安装到 `/opt/kaidi`、`/var/lib/kaidi`、
`/var/lib/kaidi-update` 和 `/etc/kaidi`。目标机应为专用主机,或确认现有 Nginx 默认站点可以被替换且 `/var/lib/kaidi-update` 和 `/etc/kaidi`。安装器不安装、不启动也不修改 Nginx、Caddy、宝塔或其他反向代理。
80 端口可用;安装器会接管默认 HTTP 站点。
### 直接 curl 安装 ### 直接 curl 安装
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh | sudo bash curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh | sudo bash
``` ```
这条命令会安装最新签名 Release,并默认进入 `/setup` 安装向导。需要在执行前独立校验安装脚本时使用: 这条命令会提示填写 Java 应用端口,直接回车使用 `18080`;随后安装最新签名 Release,并默认进入 `/setup`
安装向导。Java 默认只监听 `127.0.0.1:所选端口`,前端页面、API 和健康检查均由同一端口提供。无人值守安装可直接指定:
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh -o /tmp/kaidi-install.sh curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh \
printf '%s %s\n' faf52cc902abbc2bd48571caee3f4207de50ce339b82ce88fe23f9c4ce8f89ef /tmp/kaidi-install.sh | sha256sum -c - | sudo env KAIDI_APP_PORT=19090 bash
```
端口必须在 `1024-65535` 范围且未被其他程序监听;安装器会在安装前检查冲突。只有明确需要让其他主机直连 Java
时才设置 `KAIDI_SERVER_ADDRESS=0.0.0.0`,通常应保持默认回环绑定并由本机反向代理访问。需要在执行前独立校验安装脚本时使用:
```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh -o /tmp/kaidi-install.sh
printf '%s %s\n' cc9ae48fc3e36fbeb13b0660b0d91e59bd099a1386d49df699f214285c1afc0a /tmp/kaidi-install.sh | sha256sum -c -
sudo bash /tmp/kaidi-install.sh sudo bash /tmp/kaidi-install.sh
rm -f /tmp/kaidi-install.sh rm -f /tmp/kaidi-install.sh
``` ```
@@ -75,7 +83,7 @@ rm -f /tmp/kaidi-install.sh
包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。 包装器会在 `sudo` 前校验 `deploy/install.sh` 的固定 SHA-256,再按同一公钥信任链安装最新签名 Release。
```bash ```bash
git clone --branch v1.0.0-preview.9 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview git clone --branch v1.0.0-preview.10 --depth 1 https://git.awaioi.com/ERP-Team/kaidi.git kaidi-preview
cd kaidi-preview cd kaidi-preview
./deploy/install-from-git.sh ./deploy/install-from-git.sh
``` ```
@@ -91,19 +99,35 @@ cd kaidi-preview
- 使用安装器内置的 SHA-256 指纹校验 Release 公钥,再用该公钥验证发布清单 RSA 签名。 - 使用安装器内置的 SHA-256 指纹校验 Release 公钥,再用该公钥验证发布清单 RSA 签名。
- 首次安装默认启用 `/setup` 向导,不在命令行保存数据库密码;向导只接受 MySQL 8.4.x,并在提交前验证 DDL/DML 权限。 - 首次安装默认启用 `/setup` 向导,不在命令行保存数据库密码;向导只接受 MySQL 8.4.x,并在提交前验证 DDL/DML 权限。
- 安装签名 Release 到 `/opt/kaidi/releases/<version>`,以 `/opt/kaidi/current` 原子切换当前版本。 - 安装签名 Release 到 `/opt/kaidi/releases/<version>`,以 `/opt/kaidi/current` 原子切换当前版本。
- 安装 Nginx、`kaidi-finance.service`、更新监听服务和健康检查。 - 安装 `kaidi-finance.service`、更新监听服务和健康检查;Java 同时托管 TDesign 前端静态资源及 Vue 路由回退。
- 向导只初始化一个由操作者填写的 `SYSTEM_ADMIN` 管理员,不创建项目、财务、资料或演示账号。 - 向导只初始化一个由操作者填写的 `SYSTEM_ADMIN` 管理员,不创建项目、财务、资料或演示账号。
- 安装器把一次性安装码写入仅 root 可读的 `/root/kaidi-first-login.txt`;完成向导后写入锁定标记并切换正式应用。 - 安装器把一次性安装码写入仅 root 可读的 `/root/kaidi-first-login.txt`;完成向导后写入锁定标记并切换正式应用。
安装完成后先执行 `sudo cat /root/kaidi-first-login.txt`,访问其中的 `/setup` 地址完成数据库和管理员配置;完成后再访问 安装完成后先执行 `sudo cat /root/kaidi-first-login.txt`,其中会显示准确的反向代理目标和一次性安装码。配置反向代理后,
`http://SERVER_IP/` 登录。Preview 使用 HTTP 时安装器默认设置 通过域名的 `/setup` 完成数据库和管理员配置,再访问域名根路径登录。Preview 使用 HTTP 时安装器默认设置
`SESSION_COOKIE_SECURE=false`;配置 HTTPS 反向代理后,应在 `/etc/kaidi/kaidi.env` 改为 `SESSION_COOKIE_SECURE=false`;配置 HTTPS 反向代理后,应在 `/etc/kaidi/kaidi.env` 改为
`SESSION_COOKIE_SECURE=true` 并执行 `sudo systemctl restart kaidi-finance`。 `SESSION_COOKIE_SECURE=true` 并执行 `sudo systemctl restart kaidi-finance`。
安装后执行以下命令确认应用、反向代理和首次登录信息: 以安装端口 `19090` 为例,Nginx 只需代理到 Java,不需要单独托管前端文件:
```nginx
location / {
proxy_pass http://127.0.0.1:19090;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
client_max_body_size 500m;
}
```
宝塔、Caddy 或云网关使用同一个上游地址 `http://127.0.0.1:所选端口`。安装后执行以下命令确认应用和首次登录信息:
```bash ```bash
curl -fsS http://127.0.0.1/actuator/health | jq -e '.status == "UP"' curl -fsS http://127.0.0.1:19090/actuator/health | jq -e '.status == "UP"'
sudo systemctl --no-pager --full status kaidi-finance kaidi-update.path sudo systemctl --no-pager --full status kaidi-finance kaidi-update.path
sudo cat /root/kaidi-first-login.txt sudo cat /root/kaidi-first-login.txt
``` ```
@@ -114,7 +138,7 @@ sudo cat /root/kaidi-first-login.txt
32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令: 32 位服务端镜像,因此 32 位主机需要预先连接一台 MySQL 8.4 数据库,之后仍然只执行一个安装命令:
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh | sudo bash curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh | sudo bash
``` ```
32 位主机不能运行安装器自动创建的 MySQL 容器,因此在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的 32 位主机不能运行安装器自动创建的 MySQL 容器,因此在打开向导前,需要预先创建 `kaidi_finance`,并授予安装账号该库的
@@ -134,17 +158,17 @@ GRANT ALL PRIVILEGES ON kaidi_finance.* TO 'kaidi'@'KAIDI_SERVER_IP';
管理员数据和运维人员新增的环境变量: 管理员数据和运维人员新增的环境变量:
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh \ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh \
| sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash | sudo env KAIDI_REINSTALL=true KAIDI_SETUP_WIZARD=false bash
``` ```
重装失败会恢复原应用链接、Java 运行时、环境文件、systemd 单元和 Nginx 配置;脚本会明确报告回滚不完整, 没有显式设置 `KAIDI_APP_PORT` 时,修复性重装会沿用 `/etc/kaidi/kaidi.env` 中的原端口。重装失败会恢复原应用链接、Java 运行时、环境文件和 systemd 单元;脚本会明确报告回滚不完整,
不会把恢复失败吞掉。 不会把恢复失败吞掉。
如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。 如果安装器已完成但向导尚未提交,可执行下面的命令重新生成一次性安装码;该恢复路径只接受仍处于向导模式且未锁定的安装,正式模式不会被覆盖。
```bash ```bash
curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.9/install.sh \ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-preview.10/install.sh \
| sudo env KAIDI_REINSTALL=true bash | sudo env KAIDI_REINSTALL=true bash
``` ```
@@ -152,14 +176,12 @@ curl -fsSL https://git.awaioi.com/ERP-Team/kaidi/releases/download/v1.0.0-previe
以下命令移除应用程序和服务,但保留 `/var/lib/kaidi`、`/var/lib/kaidi-update`、`/etc/kaidi` 以及数据库, 以下命令移除应用程序和服务,但保留 `/var/lib/kaidi`、`/var/lib/kaidi-update`、`/etc/kaidi` 以及数据库,
便于审计、备份或重新安装。确认数据备份前不要删除这些保留目录或 MySQL 数据卷。 便于审计、备份或重新安装。确认数据备份前不要删除这些保留目录或 MySQL 数据卷。
安装器已经删除原 Nginx 默认站点;停用后需按该主机原有配置恢复或另行创建默认站点。 反向代理由运维人员独立管理,停用程序不会修改其配置。
```bash ```bash
sudo systemctl disable --now kaidi-update.path kaidi-update.service kaidi-finance.service sudo systemctl disable --now kaidi-update.path kaidi-update.service kaidi-finance.service
sudo rm -f /etc/systemd/system/kaidi-finance.service /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path sudo rm -f /etc/systemd/system/kaidi-finance.service /etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.path
sudo rm -f /etc/nginx/conf.d/kaidi-finance.conf
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo nginx -t && sudo systemctl reload nginx
sudo rm -rf /opt/kaidi sudo rm -rf /opt/kaidi
``` ```
@@ -182,8 +204,8 @@ act_runner 提供 `ubuntu-24.04` 标签,并在 tag 发布时执行后端、前
Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布: Preview 属性由 SemVer 版本名表达。之后推送 tag 即会构建、测试、签名并发布:
```bash ```bash
git tag v1.0.0-preview.9 git tag v1.0.0-preview.10
git push origin v1.0.0-preview.9 git push origin v1.0.0-preview.10
``` ```
在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。 在线更新使用独立的 TDesign 页面:隔离的系统管理员进入“系统治理 → 系统更新”。权限与配置页只管理用户、角色、数据范围、表单模板和参数版本,不配置系统名称或域名。
@@ -196,8 +218,8 @@ URL、Token、脚本或命令。更新流程固定为:
的包不能进入安装。 的包不能进入安装。
4. 安装请求持久领取到 `/var/lib/kaidi-update/processing`;进程或主机中断后由 systemd 恢复未完成事务。 4. 安装请求持久领取到 `/var/lib/kaidi-update/processing`;进程或主机中断后由 systemd 恢复未完成事务。
5. root 更新器重新验签和验哈希,确认 `mysqldump` 成功并生成权限为 `0600` 的备份,默认保留最近 5 份。 5. root 更新器重新验签和验哈希,确认 `mysqldump` 成功并生成权限为 `0600` 的备份,默认保留最近 5 份。
6. 校验更新脚本和 systemd 单元后,原子切换 updater、systemd、Nginx 和应用版本。 6. 校验更新脚本和 systemd 单元后,原子切换 updater、systemd 和应用版本,不修改反向代理。
7. 同时检查后端直连、Nginx 健康端点、更新 path unit 和静态首页。全部通过后页面显示 10 秒倒计时并自动 7. 同时检查 Java 健康端点、更新 path unit 和 Java 托管的静态首页。全部通过后页面显示 10 秒倒计时并自动
刷新;刷新或短暂断线发生在安装中时,页面会恢复 3 秒轮询。任一检查失败则恢复并验证上一版本。 刷新;刷新或短暂断线发生在安装中时,页面会恢复 3 秒轮询。任一检查失败则恢复并验证上一版本。
忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;这就是更新执行冷却。10 秒只用于成功后的页面刷新, 忙碌期间检查、下载和安装按钮保持禁用,防止重复请求;这就是更新执行冷却。10 秒只用于成功后的页面刷新,
@@ -217,7 +239,7 @@ cat /var/lib/kaidi-update/status.json
如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,不要删除 如果 `status.json` 显示 `FAILED` 且日志提示回滚未完成,不要删除
`/var/lib/kaidi-update/processing/request.json` 或活动事务目录。systemd 在 300 秒内连续失败 3 次后会停止自动重试, `/var/lib/kaidi-update/processing/request.json` 或活动事务目录。systemd 在 300 秒内连续失败 3 次后会停止自动重试,
修复日志所示的磁盘、权限、Nginx 或旧版本健康问题后执行: 修复日志所示的磁盘、权限或旧版本健康问题后执行:
```bash ```bash
sudo systemctl reset-failed kaidi-update.service kaidi-update.path sudo systemctl reset-failed kaidi-update.service kaidi-update.path
@@ -234,7 +256,7 @@ cat /var/lib/kaidi-update/status.json
```bash ```bash
KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \ KAIDI_RELEASE_SIGNING_KEY=/secure/release-signing-private.pem \
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/package-release.sh 1.0.0-preview.9 ./scripts/package-release.sh 1.0.0-preview.10
KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \ KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256=807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9 \
./scripts/verify-release.sh dist/release ./scripts/verify-release.sh dist/release
``` ```
@@ -3,9 +3,11 @@ package com.kaidi.finance.setup;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
@Configuration @Configuration
@ConditionalOnProperty(name = "finance.setup.enabled", havingValue = "true") @ConditionalOnProperty(name = "finance.setup.enabled", havingValue = "true")
@@ -19,8 +21,20 @@ public class SetupSecurityConfig {
.formLogin(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable) .logout(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize .authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/v1/setup/**", "/actuator/health/**", "/error").permitAll() .requestMatchers("/api/v1/setup/**", "/actuator/health/**", "/error",
"/", "/index.html", "/favicon.ico", "/assets/**").permitAll()
.requestMatchers((RequestMatcher) request -> isSpaRoute(request)).permitAll()
.anyRequest().denyAll()); .anyRequest().denyAll());
return http.build(); return http.build();
} }
private boolean isSpaRoute(jakarta.servlet.http.HttpServletRequest request) {
if (!HttpMethod.GET.matches(request.getMethod()) && !HttpMethod.HEAD.matches(request.getMethod())) {
return false;
}
String path = request.getRequestURI();
return !path.contains(".") && !path.equals("/api") && !path.startsWith("/api/")
&& !path.equals("/actuator") && !path.startsWith("/actuator/")
&& !path.equals("/error");
}
} }
@@ -14,6 +14,7 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.util.matcher.RequestMatcher;
@Configuration @Configuration
@EnableMethodSecurity @EnableMethodSecurity
@@ -37,7 +38,9 @@ public class SecurityConfig {
.authorizeHttpRequests(authorize -> authorize .authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/api/v1/auth/csrf", "/api/v1/auth/login", "/actuator/health/**", .requestMatchers("/api/v1/auth/csrf", "/api/v1/auth/login", "/actuator/health/**",
"/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/error").permitAll() "/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/error",
"/", "/index.html", "/favicon.ico", "/assets/**").permitAll()
.requestMatchers((RequestMatcher) request -> isSpaRoute(request)).permitAll()
.requestMatchers("/api/v1/**").permitAll() .requestMatchers("/api/v1/**").permitAll()
.anyRequest().authenticated()) .anyRequest().authenticated())
.exceptionHandling(exceptions -> exceptions .exceptionHandling(exceptions -> exceptions
@@ -48,6 +51,18 @@ public class SecurityConfig {
return http.build(); return http.build();
} }
private boolean isSpaRoute(jakarta.servlet.http.HttpServletRequest request) {
if (!HttpMethod.GET.matches(request.getMethod()) && !HttpMethod.HEAD.matches(request.getMethod())) {
return false;
}
String path = request.getRequestURI();
return !path.contains(".") && !path.equals("/api") && !path.startsWith("/api/")
&& !path.equals("/actuator") && !path.startsWith("/actuator/")
&& !path.equals("/api-docs") && !path.startsWith("/api-docs/")
&& !path.equals("/swagger-ui.html") && !path.startsWith("/swagger-ui/")
&& !path.equals("/error");
}
@Bean @Bean
PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder() {
return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
@@ -0,0 +1,51 @@
package com.kaidi.finance.shared.web;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/** Forwards extensionless browser routes to the bundled Vue entry point. */
@Component
public class SpaForwardFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (isBrowserRoute(request)) {
request.getRequestDispatcher("/index.html").forward(request, response);
return;
}
filterChain.doFilter(request, response);
}
private boolean isBrowserRoute(HttpServletRequest request) {
if (!"GET".equalsIgnoreCase(request.getMethod()) && !"HEAD".equalsIgnoreCase(request.getMethod())) {
return false;
}
String path = request.getRequestURI();
String contextPath = request.getContextPath();
if (contextPath != null && !contextPath.isEmpty() && path.startsWith(contextPath)) {
path = path.substring(contextPath.length());
}
if (path.isEmpty()) {
path = "/";
}
if (isReserved(path) || path.contains(".")) {
return false;
}
return DispatcherType.REQUEST.equals(request.getDispatcherType());
}
private boolean isReserved(String path) {
return path.equals("/api") || path.startsWith("/api/")
|| path.equals("/actuator") || path.startsWith("/actuator/")
|| path.equals("/api-docs") || path.startsWith("/api-docs/")
|| path.equals("/swagger-ui.html") || path.startsWith("/swagger-ui/")
|| path.equals("/error");
}
}
@@ -1,5 +1,7 @@
package com.kaidi.setup; package com.kaidi.setup;
import com.kaidi.finance.setup.SetupProperties;
import com.kaidi.finance.shared.web.SpaForwardFilter;
import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration; import org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -9,7 +11,7 @@ import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import com.kaidi.finance.setup.SetupProperties; import org.springframework.context.annotation.Import;
/** /**
* Minimal first-run context. It deliberately does not create a business DataSource or run * Minimal first-run context. It deliberately does not create a business DataSource or run
@@ -28,6 +30,7 @@ import com.kaidi.finance.setup.SetupProperties;
} }
) )
@EnableConfigurationProperties(SetupProperties.class) @EnableConfigurationProperties(SetupProperties.class)
@Import(SpaForwardFilter.class)
public class SetupApplication { public class SetupApplication {
public static void main(String[] args) { public static void main(String[] args) {
@@ -1,5 +1,6 @@
server: server:
port: ${SERVER_PORT:18080} port: ${SERVER_PORT:18080}
address: ${SERVER_ADDRESS:127.0.0.1}
shutdown: graceful shutdown: graceful
servlet: servlet:
session: session:
@@ -12,6 +13,9 @@ server:
spring: spring:
application: application:
name: kaidi-finance name: kaidi-finance
web:
resources:
static-locations: ${FINANCE_STATIC_LOCATIONS:file:./public/}
profiles: profiles:
default: local default: local
datasource: datasource:
@@ -31,6 +31,7 @@ class SetupContextSmokeTest {
registry.add("finance.setup.env-file", () -> STATE_ROOT.resolve("application.env").toString()); registry.add("finance.setup.env-file", () -> STATE_ROOT.resolve("application.env").toString());
registry.add("finance.setup.marker-file", () -> STATE_ROOT.resolve("locked").toString()); registry.add("finance.setup.marker-file", () -> STATE_ROOT.resolve("locked").toString());
registry.add("finance.setup.restart-after-complete", () -> false); registry.add("finance.setup.restart-after-complete", () -> false);
registry.add("spring.web.resources.static-locations", () -> "classpath:/spa-fixture/");
} }
@Autowired @Autowired
@@ -49,6 +50,14 @@ class SetupContextSmokeTest {
assertThat(status.getBody().path("data").path("supportedDatabaseTypes").toString()) assertThat(status.getBody().path("data").path("supportedDatabaseTypes").toString())
.isEqualTo("[\"MYSQL\"]"); .isEqualTo("[\"MYSQL\"]");
ResponseEntity<String> setupPage = rest.getForEntity(url("/setup"), String.class);
assertThat(setupPage.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(setupPage.getBody()).contains("kaidi-spa-fixture");
ResponseEntity<String> asset = rest.getForEntity(url("/assets/app.js"), String.class);
assertThat(asset.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(asset.getBody()).contains("kaidi-spa-fixture");
ResponseEntity<JsonNode> business = rest.getForEntity(url("/api/v1/auth/session"), JsonNode.class); ResponseEntity<JsonNode> business = rest.getForEntity(url("/api/v1/auth/session"), JsonNode.class);
assertThat(business.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); assertThat(business.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
} }
@@ -0,0 +1,43 @@
package com.kaidi.finance.shared.web;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
class SpaForwardFilterTest {
private final SpaForwardFilter filter = new SpaForwardFilter();
@Test
void forwardsExtensionlessBrowserRouteToVueEntryPoint() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/finance/workbench");
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicBoolean continued = new AtomicBoolean();
filter.doFilter(request, response, (ignoredRequest, ignoredResponse) -> continued.set(true));
assertThat(response.getForwardedUrl()).isEqualTo("/index.html");
assertThat(continued).isFalse();
}
@Test
void leavesApiAndStaticAssetRequestsToSpringMvc() throws Exception {
assertContinues("/api/v1/unknown-resource");
assertContinues("/actuator/health");
assertContinues("/assets/index-a1b2c3.js");
}
private void assertContinues(String uri) throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", uri);
MockHttpServletResponse response = new MockHttpServletResponse();
AtomicBoolean continued = new AtomicBoolean();
filter.doFilter(request, response, (ignoredRequest, ignoredResponse) -> continued.set(true));
assertThat(continued).isTrue();
assertThat(response.getForwardedUrl()).isNull();
}
}
@@ -0,0 +1 @@
window.__KAIDI_SPA_FIXTURE__ = 'kaidi-spa-fixture';
@@ -0,0 +1,5 @@
<!doctype html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>kaidi-spa-fixture</title></head>
<body><div id="app">kaidi-spa-fixture</div></body>
</html>
+1
View File
@@ -1,5 +1,6 @@
SPRING_PROFILES_ACTIVE=production SPRING_PROFILES_ACTIVE=production
SERVER_PORT=18080 SERVER_PORT=18080
SERVER_ADDRESS=127.0.0.1
SESSION_COOKIE_SECURE=false SESSION_COOKIE_SECURE=false
DB_URL=jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC DB_URL=jdbc:mysql://127.0.0.1:3307/kaidi_finance?useUnicode=true&characterEncoding=utf8&connectionTimeZone=UTC&serverTimezone=UTC
+5 -2
View File
@@ -5,7 +5,7 @@ umask 077
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
INSTALLER="$ROOT/deploy/install.sh" INSTALLER="$ROOT/deploy/install.sh"
INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-faf52cc902abbc2bd48571caee3f4207de50ce339b82ce88fe23f9c4ce8f89ef} INSTALLER_SHA256=${KAIDI_INSTALLER_SHA256:-cc9ae48fc3e36fbeb13b0660b0d91e59bd099a1386d49df699f214285c1afc0a}
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest} RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9} PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-} TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
@@ -44,6 +44,8 @@ fi
KAIDI_REINSTALL=${KAIDI_REINSTALL:-} KAIDI_REINSTALL=${KAIDI_REINSTALL:-}
KAIDI_SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-} KAIDI_SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-}
KAIDI_APP_PORT=${KAIDI_APP_PORT:-}
KAIDI_SERVER_ADDRESS=${KAIDI_SERVER_ADDRESS:-}
KAIDI_DB_URL=${KAIDI_DB_URL:-} KAIDI_DB_URL=${KAIDI_DB_URL:-}
KAIDI_DB_USERNAME=${KAIDI_DB_USERNAME:-} KAIDI_DB_USERNAME=${KAIDI_DB_USERNAME:-}
KAIDI_DB_PASSWORD=${KAIDI_DB_PASSWORD:-} KAIDI_DB_PASSWORD=${KAIDI_DB_PASSWORD:-}
@@ -54,7 +56,8 @@ KAIDI_DB_CONTAINER=${KAIDI_DB_CONTAINER:-}
KAIDI_SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-} KAIDI_SESSION_COOKIE_SECURE=${KAIDI_SESSION_COOKIE_SECURE:-}
KAIDI_FILE_SCANNER_ENABLED=${KAIDI_FILE_SCANNER_ENABLED:-} KAIDI_FILE_SCANNER_ENABLED=${KAIDI_FILE_SCANNER_ENABLED:-}
for name in KAIDI_REINSTALL KAIDI_SETUP_WIZARD KAIDI_DB_URL KAIDI_DB_USERNAME KAIDI_DB_PASSWORD \ for name in KAIDI_REINSTALL KAIDI_SETUP_WIZARD KAIDI_APP_PORT KAIDI_SERVER_ADDRESS \
KAIDI_DB_URL KAIDI_DB_USERNAME KAIDI_DB_PASSWORD \
KAIDI_DB_HOST KAIDI_DB_PORT KAIDI_DB_NAME KAIDI_DB_CONTAINER \ KAIDI_DB_HOST KAIDI_DB_PORT KAIDI_DB_NAME KAIDI_DB_CONTAINER \
KAIDI_SESSION_COOKIE_SECURE KAIDI_FILE_SCANNER_ENABLED; do KAIDI_SESSION_COOKIE_SECURE KAIDI_FILE_SCANNER_ENABLED; do
value=${!name} value=${!name}
+90 -26
View File
@@ -11,7 +11,11 @@ RELEASE_BASE_URL=${KAIDI_RELEASE_BASE_URL:-}
RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest} RELEASE_API_URL=${KAIDI_RELEASE_API_URL:-https://git.awaioi.com/api/v1/repos/ERP-Team/kaidi/releases/latest}
RELEASE_TOKEN=${KAIDI_RELEASE_TOKEN:-} RELEASE_TOKEN=${KAIDI_RELEASE_TOKEN:-}
RELEASE_TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-} RELEASE_TOKEN_FILE=${KAIDI_RELEASE_TOKEN_FILE:-}
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health} HEALTH_URL=${KAIDI_HEALTH_URL:-}
APP_INDEX_URL=${KAIDI_APP_INDEX_URL:-}
APP_PORT=${KAIDI_APP_PORT:-}
SERVER_ADDRESS=${KAIDI_SERVER_ADDRESS:-127.0.0.1}
PROXY_TARGET=
TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9} TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_RELEASE_PUBLIC_KEY_SHA256:-807c6aec1dc3f7ce494db16aa9d763c66f292033c38f328afd0390d2715a8cd9}
REINSTALL=${KAIDI_REINSTALL:-false} REINSTALL=${KAIDI_REINSTALL:-false}
SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true} SETUP_WIZARD=${KAIDI_SETUP_WIZARD:-true}
@@ -36,7 +40,7 @@ preflight_host() {
[ "$(uname -s)" = Linux ] || die "The installer only supports Linux" [ "$(uname -s)" = Linux ] || die "The installer only supports Linux"
[ "$APP_ROOT" = /opt/kaidi ] && [ "$STATE_ROOT" = /var/lib/kaidi ] \ [ "$APP_ROOT" = /opt/kaidi ] && [ "$STATE_ROOT" = /var/lib/kaidi ] \
&& [ "$UPDATE_STATE_ROOT" = /var/lib/kaidi-update ] && [ "$CONFIG_ROOT" = /etc/kaidi ] \ && [ "$UPDATE_STATE_ROOT" = /var/lib/kaidi-update ] && [ "$CONFIG_ROOT" = /etc/kaidi ] \
|| die "Custom installation roots are not supported by the packaged systemd and Nginx configuration" || die "Custom installation roots are not supported by the packaged systemd configuration"
command -v systemctl >/dev/null 2>&1 || die "systemd is required" command -v systemctl >/dev/null 2>&1 || die "systemd is required"
[ -d /run/systemd/system ] || die "systemd is not running as PID 1" [ -d /run/systemd/system ] || die "systemd is not running as PID 1"
[ -d /etc/systemd/system ] || die "/etc/systemd/system is missing" [ -d /etc/systemd/system ] || die "/etc/systemd/system is missing"
@@ -148,11 +152,11 @@ install_packages() {
if command -v apt-get >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update -qq apt-get update -qq
apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar nginx util-linux default-mysql-client apt-get install -y -qq ca-certificates coreutils curl findutils gzip jq openssl tar util-linux default-mysql-client
elif command -v dnf >/dev/null 2>&1; then elif command -v dnf >/dev/null 2>&1; then
dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar nginx util-linux mysql dnf install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux mysql
elif command -v yum >/dev/null 2>&1; then elif command -v yum >/dev/null 2>&1; then
yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar nginx util-linux mysql yum install -y ca-certificates coreutils curl findutils gzip jq openssl tar util-linux mysql
else else
die "Supported package managers are apt, dnf, and yum" die "Supported package managers are apt, dnf, and yum"
fi fi
@@ -264,6 +268,72 @@ read_reinstall_env() {
fi fi
} }
port_is_listening() {
local port_hex files=(/proc/net/tcp)
port_hex=$(printf '%04X' "$APP_PORT")
[ -r /proc/net/tcp ] || return 1
[ ! -r /proc/net/tcp6 ] || files+=(/proc/net/tcp6)
awk -v port="$port_hex" '
toupper($2) ~ (":" port "$") && $4 == "0A" { found=1 }
END { exit(found ? 0 : 1) }
' "${files[@]}" 2>/dev/null
}
valid_app_port() {
[[ "$1" =~ ^[0-9]{1,5}$ ]] && [ "$1" -ge 1024 ] && [ "$1" -le 65535 ]
}
configure_app_port() {
local default_port=18080 existing_port entered probe_host probe_authority
if [ -z "$APP_PORT" ]; then
default_port=$(read_reinstall_env SERVER_PORT || true)
[[ "$default_port" =~ ^[0-9]{1,5}$ ]] || default_port=18080
if [ -t 1 ] && [ -r /dev/tty ]; then
printf '[kaidi-install] Application port [%s]: ' "$default_port" > /dev/tty
if IFS= read -r entered < /dev/tty; then
APP_PORT=${entered:-$default_port}
else
APP_PORT=$default_port
fi
else
APP_PORT=$default_port
log "No interactive terminal detected; using application port $APP_PORT"
fi
fi
valid_app_port "$APP_PORT" \
|| die "KAIDI_APP_PORT must be an integer between 1024 and 65535"
case "$SERVER_ADDRESS" in
''|*[!A-Za-z0-9_.:-]*) die "KAIDI_SERVER_ADDRESS contains unsupported characters" ;;
esac
if port_is_listening; then
existing_port=$(read_existing_env SERVER_PORT || true)
if [ "$REINSTALL" = true ] && [ "$existing_port" = "$APP_PORT" ] \
&& systemctl is-active --quiet kaidi-finance.service; then
log "Application port $APP_PORT is already held by the existing Kaidi service"
else
die "Application port $APP_PORT is already in use; choose another port with KAIDI_APP_PORT"
fi
fi
probe_host=${KAIDI_HEALTH_HOST:-$SERVER_ADDRESS}
case "$probe_host" in
0.0.0.0) probe_host=127.0.0.1 ;;
::) probe_host=::1 ;;
esac
case "$probe_host" in
*:*) probe_authority="[$probe_host]" ;;
*) probe_authority="$probe_host" ;;
esac
if [ -z "$HEALTH_URL" ]; then
HEALTH_URL="http://${probe_authority}:${APP_PORT}/actuator/health"
fi
if [ -z "$APP_INDEX_URL" ]; then
APP_INDEX_URL="http://${probe_authority}:${APP_PORT}/"
fi
PROXY_TARGET="http://${probe_authority}:${APP_PORT}"
log "Application will bind ${SERVER_ADDRESS}:${APP_PORT}"
log "Reverse proxy target: $PROXY_TARGET"
}
is_semver() { is_semver() {
[ "${#1}" -le 128 ] \ [ "${#1}" -le 128 ] \
&& LC_ALL=C grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' \ && LC_ALL=C grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' \
@@ -454,8 +524,6 @@ backup_managed_state() {
/etc/systemd/system/kaidi-finance.service /etc/systemd/system/kaidi-finance.service
/etc/systemd/system/kaidi-update.service /etc/systemd/system/kaidi-update.service
/etc/systemd/system/kaidi-update.path /etc/systemd/system/kaidi-update.path
/etc/nginx/conf.d/kaidi-finance.conf
/etc/nginx/sites-enabled/default
) )
BACKUP_DIR="$WORK_DIR/rollback" BACKUP_DIR="$WORK_DIR/rollback"
mkdir -p "$BACKUP_DIR/files" mkdir -p "$BACKUP_DIR/files"
@@ -470,8 +538,6 @@ backup_managed_state() {
systemctl is-enabled --quiet kaidi-finance.service && PREVIOUS_APP_ENABLED=true || PREVIOUS_APP_ENABLED=false systemctl is-enabled --quiet kaidi-finance.service && PREVIOUS_APP_ENABLED=true || PREVIOUS_APP_ENABLED=false
systemctl is-active --quiet kaidi-update.path && PREVIOUS_UPDATE_ACTIVE=true || PREVIOUS_UPDATE_ACTIVE=false systemctl is-active --quiet kaidi-update.path && PREVIOUS_UPDATE_ACTIVE=true || PREVIOUS_UPDATE_ACTIVE=false
systemctl is-enabled --quiet kaidi-update.path && PREVIOUS_UPDATE_ENABLED=true || PREVIOUS_UPDATE_ENABLED=false systemctl is-enabled --quiet kaidi-update.path && PREVIOUS_UPDATE_ENABLED=true || PREVIOUS_UPDATE_ENABLED=false
systemctl is-active --quiet nginx && PREVIOUS_NGINX_ACTIVE=true || PREVIOUS_NGINX_ACTIVE=false
systemctl is-enabled --quiet nginx && PREVIOUS_NGINX_ENABLED=true || PREVIOUS_NGINX_ENABLED=false
INSTALL_TRANSACTION_ARMED=true INSTALL_TRANSACTION_ARMED=true
} }
@@ -529,10 +595,6 @@ rollback_install() {
|| rollback_failed=true || rollback_failed=true
restore_unit_state kaidi-update.path "$PREVIOUS_UPDATE_ENABLED" "$PREVIOUS_UPDATE_ACTIVE" \ restore_unit_state kaidi-update.path "$PREVIOUS_UPDATE_ENABLED" "$PREVIOUS_UPDATE_ACTIVE" \
|| rollback_failed=true || rollback_failed=true
restore_unit_state nginx "$PREVIOUS_NGINX_ENABLED" "$PREVIOUS_NGINX_ACTIVE" || rollback_failed=true
if [ "$PREVIOUS_NGINX_ACTIVE" = true ]; then
nginx -t >/dev/null 2>&1 && systemctl reload nginx >/dev/null 2>&1 || rollback_failed=true
fi
set -e set -e
[ "$rollback_failed" = false ] [ "$rollback_failed" = false ]
} }
@@ -542,7 +604,7 @@ cleanup() {
trap - EXIT trap - EXIT
if [ "$result" -ne 0 ] && [ "$INSTALL_TRANSACTION_ARMED" = true ] \ if [ "$result" -ne 0 ] && [ "$INSTALL_TRANSACTION_ARMED" = true ] \
&& [ "$INSTALL_COMMITTED" != true ]; then && [ "$INSTALL_COMMITTED" != true ]; then
rollback_install || log "ERROR: rollback was incomplete; inspect systemd and Nginx state" rollback_install || log "ERROR: rollback was incomplete; inspect the managed systemd state"
fi fi
[ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR" [ -z "$WORK_DIR" ] || rm -rf "$WORK_DIR"
exit "$result" exit "$result"
@@ -550,6 +612,7 @@ cleanup() {
trap cleanup EXIT trap cleanup EXIT
configure_app_port
preflight_host preflight_host
install_packages install_packages
@@ -655,7 +718,8 @@ if [ "$SETUP_WIZARD" = true ]; then
fi fi
write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env" \ write_env_file_preserving_unknown "$CONFIG_ROOT/kaidi.env" \
SPRING_PROFILES_ACTIVE production \ SPRING_PROFILES_ACTIVE production \
SERVER_PORT 18080 \ SERVER_PORT "$APP_PORT" \
SERVER_ADDRESS "$SERVER_ADDRESS" \
SESSION_COOKIE_SECURE "$SESSION_COOKIE_SECURE" \ SESSION_COOKIE_SECURE "$SESSION_COOKIE_SECURE" \
DB_URL "$DB_URL" \ DB_URL "$DB_URL" \
DB_USERNAME "$DB_USERNAME" \ DB_USERNAME "$DB_USERNAME" \
@@ -692,6 +756,7 @@ write_env_file "$CONFIG_ROOT/update.env" \
KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \ KAIDI_UPDATE_STATE_ROOT "$UPDATE_STATE_ROOT" \
KAIDI_SERVICE_NAME kaidi-finance.service \ KAIDI_SERVICE_NAME kaidi-finance.service \
KAIDI_HEALTH_URL "$HEALTH_URL" \ KAIDI_HEALTH_URL "$HEALTH_URL" \
KAIDI_APP_INDEX_URL "$APP_INDEX_URL" \
KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-}" \ KAIDI_DB_CONTAINER "${KAIDI_DB_CONTAINER:-}" \
KAIDI_DB_HOST "$(database_host)" \ KAIDI_DB_HOST "$(database_host)" \
KAIDI_DB_PORT "$(database_port)" \ KAIDI_DB_PORT "$(database_port)" \
@@ -703,17 +768,12 @@ chmod 0600 "$CONFIG_ROOT/update.env"
install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.service" /etc/systemd/system/kaidi-finance.service install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.service" /etc/systemd/system/kaidi-finance.service
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service install -m 0644 "$RELEASE_DIR/ops/kaidi-update.service" /etc/systemd/system/kaidi-update.service
install -m 0644 "$RELEASE_DIR/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path install -m 0644 "$RELEASE_DIR/ops/kaidi-update.path" /etc/systemd/system/kaidi-update.path
install -m 0644 "$RELEASE_DIR/ops/kaidi-finance.conf" /etc/nginx/conf.d/kaidi-finance.conf
rm -f /etc/nginx/sites-enabled/default
nginx -t
systemctl daemon-reload systemctl daemon-reload
systemctl enable kaidi-finance.service systemctl enable kaidi-finance.service
systemctl restart kaidi-finance.service systemctl restart kaidi-finance.service
wait_for_health wait_for_health
systemctl enable --now nginx curl -fsS "$APP_INDEX_URL" | grep -Eiq '<!doctype|<html' \
systemctl reload nginx || die "Application frontend entry point is unavailable"
curl -fsS http://127.0.0.1/actuator/health | jq -e '.status == "UP"' >/dev/null \
|| die "Nginx proxy health check failed"
if [ "$SETUP_WIZARD" != true ]; then if [ "$SETUP_WIZARD" != true ]; then
sed -i 's/^FINANCE_BOOTSTRAP_ENABLED="true"$/FINANCE_BOOTSTRAP_ENABLED="false"/' "$CONFIG_ROOT/kaidi.env" sed -i 's/^FINANCE_BOOTSTRAP_ENABLED="true"$/FINANCE_BOOTSTRAP_ENABLED="false"/' "$CONFIG_ROOT/kaidi.env"
@@ -727,14 +787,16 @@ systemctl enable --now kaidi-update.path
if [ "$SETUP_WIZARD" = true ]; then if [ "$SETUP_WIZARD" = true ]; then
cat > /root/kaidi-first-login.txt <<EOF cat > /root/kaidi-first-login.txt <<EOF
URL: http://SERVER_IP/setup URL after reverse proxy: http://SERVER_IP/setup
Reverse proxy target: $PROXY_TARGET
Setup code: $SETUP_CODE Setup code: $SETUP_CODE
Version: $VERSION Version: $VERSION
EOF EOF
chmod 0600 /root/kaidi-first-login.txt chmod 0600 /root/kaidi-first-login.txt
elif [ "$REINSTALL" != true ]; then elif [ "$REINSTALL" != true ]; then
cat > /root/kaidi-first-login.txt <<EOF cat > /root/kaidi-first-login.txt <<EOF
URL: http://SERVER_IP/ URL after reverse proxy: http://SERVER_IP/
Reverse proxy target: $PROXY_TARGET
Username: admin Username: admin
Temporary password: $ADMIN_PASSWORD Temporary password: $ADMIN_PASSWORD
Version: $VERSION Version: $VERSION
@@ -746,10 +808,12 @@ INSTALL_TRANSACTION_ARMED=false
log "Kaidi Finance $VERSION is installed" log "Kaidi Finance $VERSION is installed"
if [ "$SETUP_WIZARD" = true ]; then if [ "$SETUP_WIZARD" = true ]; then
log "Open http://SERVER_IP/setup and complete the first-run wizard" log "Configure your reverse proxy to $PROXY_TARGET"
log "Open /setup through your reverse-proxy domain and complete the first-run wizard"
log "Setup code: /root/kaidi-first-login.txt" log "Setup code: /root/kaidi-first-login.txt"
else else
log "Open http://SERVER_IP/ and sign in as admin" log "Configure your reverse proxy to $PROXY_TARGET"
log "Open the reverse-proxy domain and sign in as admin"
fi fi
if [ "$REINSTALL" != true ] && [ "$SETUP_WIZARD" != true ]; then if [ "$REINSTALL" != true ] && [ "$SETUP_WIZARD" != true ]; then
log "Temporary credentials: /root/kaidi-first-login.txt" log "Temporary credentials: /root/kaidi-first-login.txt"
+3 -24
View File
@@ -2,12 +2,11 @@ server {
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server; listen [::]:80 default_server;
server_name _; server_name _;
root /opt/kaidi/current/public;
index index.html;
client_max_body_size 500m; client_max_body_size 500m;
location /api/v1/ { # Optional example. The installer does not install or modify Nginx.
# Replace 18080 with the KAIDI_APP_PORT selected during installation.
location / {
proxy_pass http://127.0.0.1:18080; proxy_pass http://127.0.0.1:18080;
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
@@ -17,24 +16,4 @@ server {
proxy_connect_timeout 10s; proxy_connect_timeout 10s;
proxy_read_timeout 120s; proxy_read_timeout 120s;
} }
location = /actuator/health {
proxy_pass http://127.0.0.1:18080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
try_files $uri $uri/ /index.html;
add_header X-Content-Type-Options nosniff always;
add_header Referrer-Policy same-origin always;
add_header X-Frame-Options SAMEORIGIN always;
}
location ~* \.(?:js|css|woff2?|png|jpe?g|gif|svg|ico)$ {
try_files $uri =404;
expires 7d;
add_header Cache-Control "public, immutable";
add_header X-Content-Type-Options nosniff always;
}
} }
+1 -1
View File
@@ -20,5 +20,5 @@ IOSchedulingPriority=6
PrivateTmp=true PrivateTmp=true
ProtectHome=true ProtectHome=true
ProtectSystem=full ProtectSystem=full
ReadWritePaths=/opt/kaidi /var/lib/kaidi /var/lib/kaidi-update /var/log/kaidi /etc/systemd/system /etc/nginx/conf.d ReadWritePaths=/opt/kaidi /var/lib/kaidi /var/lib/kaidi-update /var/log/kaidi /etc/systemd/system
UMask=0077 UMask=0077
+9 -23
View File
@@ -19,13 +19,11 @@ RELEASE_TOKEN=${UPDATE_RELEASE_TOKEN:-}
CACHE_ROOT=${KAIDI_UPDATE_CACHE_ROOT:-$STATE_ROOT/cache} CACHE_ROOT=${KAIDI_UPDATE_CACHE_ROOT:-$STATE_ROOT/cache}
SERVICE_NAME=${KAIDI_SERVICE_NAME:-kaidi-finance.service} SERVICE_NAME=${KAIDI_SERVICE_NAME:-kaidi-finance.service}
HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health} HEALTH_URL=${KAIDI_HEALTH_URL:-http://127.0.0.1:18080/actuator/health}
PUBLIC_HEALTH_URL=${KAIDI_PUBLIC_HEALTH_URL:-http://127.0.0.1/actuator/health} APP_INDEX_URL=${KAIDI_APP_INDEX_URL:-http://127.0.0.1:18080/}
PUBLIC_INDEX_URL=${KAIDI_PUBLIC_INDEX_URL:-http://127.0.0.1/}
LOCK_FILE=$STATE_ROOT/update.lock LOCK_FILE=$STATE_ROOT/update.lock
BACKUP_ROOT=${KAIDI_BACKUP_ROOT:-$STATE_ROOT/backups} BACKUP_ROOT=${KAIDI_BACKUP_ROOT:-$STATE_ROOT/backups}
UPDATER_PATH=${KAIDI_UPDATER_PATH:-$APP_ROOT/bin/update.sh} UPDATER_PATH=${KAIDI_UPDATER_PATH:-$APP_ROOT/bin/update.sh}
SYSTEMD_ROOT=${KAIDI_SYSTEMD_ROOT:-/etc/systemd/system} SYSTEMD_ROOT=${KAIDI_SYSTEMD_ROOT:-/etc/systemd/system}
NGINX_CONFIG=${KAIDI_NGINX_CONFIG:-/etc/nginx/conf.d/kaidi-finance.conf}
LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi} LOG_ROOT=${KAIDI_LOG_ROOT:-/var/log/kaidi}
HEALTH_ATTEMPTS=${KAIDI_UPDATE_HEALTH_ATTEMPTS:-60} HEALTH_ATTEMPTS=${KAIDI_UPDATE_HEALTH_ATTEMPTS:-60}
HEALTH_INTERVAL_SECONDS=${KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS:-2} HEALTH_INTERVAL_SECONDS=${KAIDI_UPDATE_HEALTH_INTERVAL_SECONDS:-2}
@@ -161,10 +159,10 @@ wait_for_health() {
return 1 return 1
} }
verify_public_surface() { verify_app_surface() {
wait_for_health "$PUBLIC_HEALTH_URL" || return 1 wait_for_health "$HEALTH_URL" || return 1
index_file=$(mktemp "$STATE_ROOT/work/public-index.XXXXXX") index_file=$(mktemp "$STATE_ROOT/work/public-index.XXXXXX")
if curl -fsS "$PUBLIC_INDEX_URL" -o "$index_file" \ if curl -fsS "$APP_INDEX_URL" -o "$index_file" \
&& grep -Eiq '<!doctype|<html' "$index_file"; then && grep -Eiq '<!doctype|<html' "$index_file"; then
rm -f "$index_file" rm -f "$index_file"
return 0 return 0
@@ -266,8 +264,7 @@ backup_operations() {
backup_managed_file "$UPDATER_PATH" update.sh \ backup_managed_file "$UPDATER_PATH" update.sh \
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service \ && backup_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service \
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service \ && backup_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service \
&& backup_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path \ && backup_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path
&& backup_managed_file "$NGINX_CONFIG" kaidi-finance.conf
} }
apply_operations() { apply_operations() {
@@ -278,9 +275,7 @@ apply_operations() {
"$SYSTEMD_ROOT/kaidi-update.service" 0644 \ "$SYSTEMD_ROOT/kaidi-update.service" 0644 \
&& atomic_install "$RELEASE_DIR/ops/kaidi-update.path" \ && atomic_install "$RELEASE_DIR/ops/kaidi-update.path" \
"$SYSTEMD_ROOT/kaidi-update.path" 0644 \ "$SYSTEMD_ROOT/kaidi-update.path" 0644 \
&& atomic_install "$RELEASE_DIR/ops/kaidi-finance.conf" "$NGINX_CONFIG" 0644 \ && systemctl daemon-reload
&& systemctl daemon-reload \
&& nginx -t
} }
restore_operations() { restore_operations() {
@@ -289,13 +284,7 @@ restore_operations() {
restore_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service || restore_failed=1 restore_managed_file "$SYSTEMD_ROOT/kaidi-finance.service" kaidi-finance.service || restore_failed=1
restore_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service || restore_failed=1 restore_managed_file "$SYSTEMD_ROOT/kaidi-update.service" kaidi-update.service || restore_failed=1
restore_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path || restore_failed=1 restore_managed_file "$SYSTEMD_ROOT/kaidi-update.path" kaidi-update.path || restore_failed=1
restore_managed_file "$NGINX_CONFIG" kaidi-finance.conf || restore_failed=1
systemctl daemon-reload || restore_failed=1 systemctl daemon-reload || restore_failed=1
if nginx -t; then
systemctl reload nginx || restore_failed=1
else
restore_failed=1
fi
[ "$restore_failed" -eq 0 ] [ "$restore_failed" -eq 0 ]
} }
@@ -338,7 +327,7 @@ rollback_active_transaction() {
fi fi
restore_operations || rollback_ok=false restore_operations || rollback_ok=false
systemctl start "$SERVICE_NAME" || rollback_ok=false systemctl start "$SERVICE_NAME" || rollback_ok=false
if [ "$rollback_ok" = true ] && wait_for_health "$HEALTH_URL" && verify_public_surface; then if [ "$rollback_ok" = true ] && verify_app_surface; then
remove_failed_release "$failed_release" remove_failed_release "$failed_release"
rm -rf "$ACTIVE_TRANSACTION" rm -rf "$ACTIVE_TRANSACTION"
fail "$reason; previous release was restored and verified" fail "$reason; previous release was restored and verified"
@@ -499,7 +488,6 @@ fi
[ -s "$WORK_DIR/extracted/ops/kaidi-finance.service" ] || fail "Release application unit is missing" [ -s "$WORK_DIR/extracted/ops/kaidi-finance.service" ] || fail "Release application unit is missing"
[ -s "$WORK_DIR/extracted/ops/kaidi-update.service" ] || fail "Release updater unit is missing" [ -s "$WORK_DIR/extracted/ops/kaidi-update.service" ] || fail "Release updater unit is missing"
[ -s "$WORK_DIR/extracted/ops/kaidi-update.path" ] || fail "Release updater path unit is missing" [ -s "$WORK_DIR/extracted/ops/kaidi-update.path" ] || fail "Release updater path unit is missing"
[ -s "$WORK_DIR/extracted/ops/kaidi-finance.conf" ] || fail "Release Nginx configuration is missing"
validate_release_operations || fail "Release operations validation failed" validate_release_operations || fail "Release operations validation failed"
if [ "$REQUEST_ACTION" = DOWNLOAD ]; then if [ "$REQUEST_ACTION" = DOWNLOAD ]; then
@@ -562,10 +550,8 @@ write_transaction_value phase APP_SWITCHED
status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION" status RUNNING "Starting and verifying release $TARGET_VERSION" "$TARGET_VERSION"
write_transaction_value phase HEALTH_CHECKING write_transaction_value phase HEALTH_CHECKING
if systemctl start "$SERVICE_NAME" \ if systemctl start "$SERVICE_NAME" \
&& wait_for_health "$HEALTH_URL" \
&& systemctl reload nginx \
&& systemctl is-active --quiet kaidi-update.path \ && systemctl is-active --quiet kaidi-update.path \
&& verify_public_surface; then && verify_app_surface; then
write_transaction_value phase COMMITTED write_transaction_value phase COMMITTED
status SUCCEEDED "Release $TARGET_VERSION is running" "$TARGET_VERSION" status SUCCEEDED "Release $TARGET_VERSION is running" "$TARGET_VERSION"
TERMINAL_STATUS_WRITTEN=true TERMINAL_STATUS_WRITTEN=true
@@ -574,4 +560,4 @@ if systemctl start "$SERVICE_NAME" \
exit 0 exit 0
fi fi
rollback_active_transaction "Release health or public-surface verification failed" rollback_active_transaction "Release health or application-surface verification failed"
+1
View File
@@ -91,6 +91,7 @@ cp "$JAR" "$STAGE/app.jar"
cp -R "$ROOT/frontend/dist/." "$STAGE/public/" cp -R "$ROOT/frontend/dist/." "$STAGE/public/"
printf '%s\n' "$VERSION" > "$STAGE/VERSION" printf '%s\n' "$VERSION" > "$STAGE/VERSION"
cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh" cp "$ROOT/deploy/update.sh" "$STAGE/ops/update.sh"
# Kept in the archive so Preview.9's updater can complete the one-time transition.
cp "$ROOT/deploy/nginx/kaidi-finance.conf" "$STAGE/ops/kaidi-finance.conf" cp "$ROOT/deploy/nginx/kaidi-finance.conf" "$STAGE/ops/kaidi-finance.conf"
cp "$ROOT/deploy/systemd/kaidi-finance.service" "$STAGE/ops/kaidi-finance.service" cp "$ROOT/deploy/systemd/kaidi-finance.service" "$STAGE/ops/kaidi-finance.service"
cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service" cp "$ROOT/deploy/systemd/kaidi-update.service" "$STAGE/ops/kaidi-update.service"
+7
View File
@@ -23,6 +23,10 @@ set -Eeuo pipefail
[ "$KAIDI_RELEASE_API_URL" = 'https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest' ] [ "$KAIDI_RELEASE_API_URL" = 'https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest' ]
[ "$KAIDI_RELEASE_PUBLIC_KEY_SHA256" = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ] [ "$KAIDI_RELEASE_PUBLIC_KEY_SHA256" = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' ]
if [ "${EXPECT_PORT:-false}" = true ]; then
[ "$KAIDI_APP_PORT" = '19090' ]
[ "$KAIDI_SERVER_ADDRESS" = '127.0.0.1' ]
fi
if [ "${KAIDI_SETUP_WIZARD:-false}" = true ]; then if [ "${KAIDI_SETUP_WIZARD:-false}" = true ]; then
[ -z "${KAIDI_DB_URL:-}${KAIDI_DB_USERNAME:-}${KAIDI_DB_PASSWORD:-}" ] [ -z "${KAIDI_DB_URL:-}${KAIDI_DB_USERNAME:-}${KAIDI_DB_PASSWORD:-}" ]
else else
@@ -44,9 +48,12 @@ chmod 0600 "$FIXTURE/token"
installer_sha256=$(sha256sum "$FIXTURE/repo/deploy/install.sh" | awk '{print $1}') installer_sha256=$(sha256sum "$FIXTURE/repo/deploy/install.sh" | awk '{print $1}')
PATH="$FIXTURE/bin:$PATH" \ PATH="$FIXTURE/bin:$PATH" \
EXPECT_PORT=true \
KAIDI_INSTALLER_SHA256="$installer_sha256" \ KAIDI_INSTALLER_SHA256="$installer_sha256" \
KAIDI_RELEASE_API_URL=https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest \ KAIDI_RELEASE_API_URL=https://git.example.test/api/v1/repos/TEAM/REPO/releases/latest \
KAIDI_RELEASE_PUBLIC_KEY_SHA256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \ KAIDI_RELEASE_PUBLIC_KEY_SHA256=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \
KAIDI_APP_PORT=19090 \
KAIDI_SERVER_ADDRESS=127.0.0.1 \
KAIDI_DB_URL=jdbc:mysql://DB_HOST:3306/kaidi_finance \ KAIDI_DB_URL=jdbc:mysql://DB_HOST:3306/kaidi_finance \
KAIDI_DB_USERNAME=kaidi \ KAIDI_DB_USERNAME=kaidi \
KAIDI_DB_PASSWORD=fixture-password \ KAIDI_DB_PASSWORD=fixture-password \
+37 -1
View File
@@ -17,6 +17,9 @@ fail() {
sed -n '/^read_existing_env()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^read_existing_env()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^read_setup_env()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^read_setup_env()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^read_reinstall_env()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^read_reinstall_env()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^port_is_listening()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^valid_app_port()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^configure_app_port()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^is_semver()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^write_env_file_preserving_unknown()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^write_env_file_preserving_unknown()/,/^}/p' "$ROOT/deploy/install.sh"
sed -n '/^azul_arch()/,/^}/p' "$ROOT/deploy/install.sh" sed -n '/^azul_arch()/,/^}/p' "$ROOT/deploy/install.sh"
@@ -78,6 +81,31 @@ grep -qx 'jdbc:mysql://runtime/kaidi_finance' <(read_reinstall_env DB_URL) \
grep -qx 'runtime-user' <(read_reinstall_env DB_USERNAME) \ grep -qx 'runtime-user' <(read_reinstall_env DB_USERNAME) \
|| fail 'reinstall did not prefer the completed setup runtime database user' || fail 'reinstall did not prefer the completed setup runtime database user'
for port in 1024 18080 65535; do
valid_app_port "$port" || fail "installer rejected valid application port $port"
done
for port in 0 80 1023 65536 invalid 18080.0; do
! valid_app_port "$port" || fail "installer accepted invalid application port $port"
done
port_is_listening() { return 1; }
log() { printf '%s\n' "$*" >/dev/null; }
# shellcheck disable=SC2329 # Invoked by the extracted installer helper.
die() { printf '%s\n' "$*" >&2; return 1; }
export APP_PORT=19090 SERVER_ADDRESS=127.0.0.1 HEALTH_URL='' APP_INDEX_URL='' REINSTALL=false
configure_app_port
[ "$HEALTH_URL" = 'http://127.0.0.1:19090/actuator/health' ] \
|| fail 'selected application port did not reach the health URL'
[ "$APP_INDEX_URL" = 'http://127.0.0.1:19090/' ] \
|| fail 'selected application port did not reach the frontend URL'
export APP_PORT=19091 SERVER_ADDRESS=::1 HEALTH_URL='' APP_INDEX_URL=''
configure_app_port
[ "$HEALTH_URL" = 'http://[::1]:19091/actuator/health' ] \
|| fail 'IPv6 bind address did not produce a bracketed health URL'
[ "$APP_INDEX_URL" = 'http://[::1]:19091/' ] \
|| fail 'IPv6 bind address did not produce a bracketed frontend URL'
[ "$PROXY_TARGET" = 'http://[::1]:19091' ] \
|| fail 'IPv6 bind address did not produce a bracketed reverse-proxy target'
for version in 0.0.0 1.2.3-alpha- 1.2.3--alpha 1.2.3-alpha+build.07; do for version in 0.0.0 1.2.3-alpha- 1.2.3--alpha 1.2.3-alpha+build.07; do
is_semver "$version" || fail "installer rejected valid SemVer $version" is_semver "$version" || fail "installer rejected valid SemVer $version"
"$ROOT/scripts/check-semver.sh" "$version" || fail "release workflow rejected valid SemVer $version" "$ROOT/scripts/check-semver.sh" "$version" || fail "release workflow rejected valid SemVer $version"
@@ -218,6 +246,14 @@ grep -Fq 'KAIDI_SETUP_WIZARD' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer supports first-run setup mode' || fail 'installer no longer supports first-run setup mode'
grep -Fq 'FINANCE_SETUP_TOKEN_SHA256' "$ROOT/deploy/install.sh" \ grep -Fq 'FINANCE_SETUP_TOKEN_SHA256' "$ROOT/deploy/install.sh" \
|| fail 'installer no longer writes the one-time setup-code hash' || fail 'installer no longer writes the one-time setup-code hash'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'SERVER_PORT "$APP_PORT"' "$ROOT/deploy/install.sh" \
|| fail 'installer does not persist the selected application port'
# shellcheck disable=SC2016 # Match literal installer source.
grep -Fq 'SERVER_ADDRESS "$SERVER_ADDRESS"' "$ROOT/deploy/install.sh" \
|| fail 'installer does not persist the selected bind address'
! grep -Eqi 'nginx|/etc/nginx/' "$ROOT/deploy/install.sh" \
|| fail 'installer must not install, start, or modify Nginx'
grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \ grep -Fq 'EnvironmentFile=-/var/lib/kaidi/setup/application.env' \
"$ROOT/deploy/systemd/kaidi-finance.service" \ "$ROOT/deploy/systemd/kaidi-finance.service" \
|| fail 'application service no longer loads the setup-completion environment' || fail 'application service no longer loads the setup-completion environment'
@@ -230,4 +266,4 @@ grep -Fq 'chown root:kaidi "$UPDATE_STATE_ROOT"' "$ROOT/deploy/install.sh" \
grep -Fq 'install.sh | sudo bash' "$ROOT/README.md" \ grep -Fq 'install.sh | sudo bash' "$ROOT/README.md" \
|| fail 'README does not document the public one-line setup-wizard install path' || fail 'README does not document the public one-line setup-wizard install path'
printf 'Install configuration, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n' printf 'Install configuration, custom port, public Gitea, optional private token, i686, setup wizard, and MySQL 8.4 fixtures passed\n'
+6 -16
View File
@@ -55,7 +55,7 @@ esac
if [ -n "$output" ]; then if [ -n "$output" ]; then
if [ -n "${MOCK_RELEASE_API_URL:-}" ] && [ "$url" = "$MOCK_RELEASE_API_URL" ]; then if [ -n "${MOCK_RELEASE_API_URL:-}" ] && [ "$url" = "$MOCK_RELEASE_API_URL" ]; then
cp "$FIXTURE_RELEASE_ROOT/release-api.json" "$output" cp "$FIXTURE_RELEASE_ROOT/release-api.json" "$output"
elif [ "$url" = "${KAIDI_PUBLIC_INDEX_URL:-http://127.0.0.1/}" ]; then elif [ "$url" = "${KAIDI_APP_INDEX_URL:-http://127.0.0.1:18080/}" ]; then
cp "$MOCK_APP_ROOT/current/public/index.html" "$output" cp "$MOCK_APP_ROOT/current/public/index.html" "$output"
else else
[ "${MOCK_DOWNLOAD_FAILURE:-}" != "${url##*/}" ] || exit 22 [ "${MOCK_DOWNLOAD_FAILURE:-}" != "${url##*/}" ] || exit 22
@@ -75,12 +75,6 @@ SH
#!/bin/sh #!/bin/sh
printf '%s\n' "$*" >> "$MOCK_SYSTEMCTL_LOG" printf '%s\n' "$*" >> "$MOCK_SYSTEMCTL_LOG"
exit 0 exit 0
SH
cat > "$mock_bin/nginx" <<'SH'
#!/bin/sh
printf '%s\n' "$*" >> "$MOCK_NGINX_LOG"
exit 0
SH SH
cat > "$mock_bin/flock" <<'SH' cat > "$mock_bin/flock" <<'SH'
@@ -138,7 +132,7 @@ build_release() {
printf '%s\n' "$version" > "$stage/VERSION" printf '%s\n' "$version" > "$stage/VERSION"
printf '#!/bin/sh\nprintf "new updater\\n"\n' > "$stage/ops/update.sh" printf '#!/bin/sh\nprintf "new updater\\n"\n' > "$stage/ops/update.sh"
chmod 0755 "$stage/ops/update.sh" chmod 0755 "$stage/ops/update.sh"
for name in kaidi-finance.service kaidi-update.service kaidi-update.path kaidi-finance.conf; do for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do
printf 'new %s\n' "$name" > "$stage/ops/$name" printf 'new %s\n' "$name" > "$stage/ops/$name"
done done
@@ -174,7 +168,7 @@ prepare_installation() {
local fixture=$1 local fixture=$1
local version=$2 local version=$2
mkdir -p "$fixture/app/releases/1.0.0-preview.1/public" "$fixture/app/bin" \ mkdir -p "$fixture/app/releases/1.0.0-preview.1/public" "$fixture/app/bin" \
"$fixture/state/inbox" "$fixture/systemd" "$fixture/nginx" "$fixture/log" "$fixture/state/inbox" "$fixture/systemd" "$fixture/log"
printf 'old application\n' > "$fixture/app/releases/1.0.0-preview.1/app.jar" printf 'old application\n' > "$fixture/app/releases/1.0.0-preview.1/app.jar"
printf '<!doctype html><title>old</title>\n' > "$fixture/app/releases/1.0.0-preview.1/public/index.html" printf '<!doctype html><title>old</title>\n' > "$fixture/app/releases/1.0.0-preview.1/public/index.html"
printf '1.0.0-preview.1\n' > "$fixture/app/releases/1.0.0-preview.1/VERSION" printf '1.0.0-preview.1\n' > "$fixture/app/releases/1.0.0-preview.1/VERSION"
@@ -184,7 +178,6 @@ prepare_installation() {
for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do for name in kaidi-finance.service kaidi-update.service kaidi-update.path; do
printf 'old %s\n' "$name" > "$fixture/systemd/$name" printf 'old %s\n' "$name" > "$fixture/systemd/$name"
done done
printf 'old kaidi-finance.conf\n' > "$fixture/nginx/kaidi-finance.conf"
write_request "$fixture" "$version" DOWNLOAD write_request "$fixture" "$version" DOWNLOAD
} }
@@ -227,12 +220,10 @@ run_update() {
MOCK_RELEASE_ORIGIN="${FIXTURE_RELEASE_ORIGIN:-https://release.fixture.invalid}" \ MOCK_RELEASE_ORIGIN="${FIXTURE_RELEASE_ORIGIN:-https://release.fixture.invalid}" \
MOCK_EXPECT_RELEASE_TOKEN="${FIXTURE_RELEASE_TOKEN-}" \ MOCK_EXPECT_RELEASE_TOKEN="${FIXTURE_RELEASE_TOKEN-}" \
MOCK_SYSTEMCTL_LOG="$fixture/systemctl.log" \ MOCK_SYSTEMCTL_LOG="$fixture/systemctl.log" \
MOCK_NGINX_LOG="$fixture/nginx.log" \
KAIDI_APP_ROOT="$fixture/app" \ KAIDI_APP_ROOT="$fixture/app" \
KAIDI_UPDATE_STATE_ROOT="$fixture/state" \ KAIDI_UPDATE_STATE_ROOT="$fixture/state" \
KAIDI_LOG_ROOT="$fixture/log" \ KAIDI_LOG_ROOT="$fixture/log" \
KAIDI_SYSTEMD_ROOT="$fixture/systemd" \ KAIDI_SYSTEMD_ROOT="$fixture/systemd" \
KAIDI_NGINX_CONFIG="$fixture/nginx/kaidi-finance.conf" \
KAIDI_UPDATER_PATH="$fixture/app/bin/update.sh" \ KAIDI_UPDATER_PATH="$fixture/app/bin/update.sh" \
KAIDI_SKIP_DB_BACKUP="$skip_backup" \ KAIDI_SKIP_DB_BACKUP="$skip_backup" \
KAIDI_UPDATE_HEALTH_ATTEMPTS=1 \ KAIDI_UPDATE_HEALTH_ATTEMPTS=1 \
@@ -243,9 +234,8 @@ run_update() {
UPDATE_PUBLIC_KEY="$fixture/release-public.pem" \ UPDATE_PUBLIC_KEY="$fixture/release-public.pem" \
UPDATE_REQUEST_FILE="$fixture/state/inbox/request.json" \ UPDATE_REQUEST_FILE="$fixture/state/inbox/request.json" \
UPDATE_STATUS_FILE="$fixture/state/status.json" \ UPDATE_STATUS_FILE="$fixture/state/status.json" \
KAIDI_HEALTH_URL=http://127.0.0.1:18080/actuator/health \ KAIDI_HEALTH_URL=http://127.0.0.1:19090/actuator/health \
KAIDI_PUBLIC_HEALTH_URL=http://127.0.0.1/actuator/health \ KAIDI_APP_INDEX_URL=http://127.0.0.1:19090/ \
KAIDI_PUBLIC_INDEX_URL=http://127.0.0.1/ \
"$ROOT/deploy/update.sh" "$ROOT/deploy/update.sh"
} }
@@ -319,7 +309,7 @@ assert_success_case() {
[ ! -e "$fixture/state/processing/request.json" ] \ [ ! -e "$fixture/state/processing/request.json" ] \
|| fail 'success case left a claimed request behind' || fail 'success case left a claimed request behind'
grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded' grep -qx 'daemon-reload' "$fixture/systemctl.log" || fail 'systemd units were not reloaded'
grep -qx 'reload nginx' "$fixture/systemctl.log" || fail 'Nginx was not reloaded' ! grep -qi nginx "$fixture/systemctl.log" || fail 'updater unexpectedly managed Nginx'
} }
assert_rollback_case() { assert_rollback_case() {