diff --git a/atsf_agent/internal/nginx/manager_test.go b/atsf_agent/internal/nginx/manager_test.go index 1a7f2241..636da6cc 100644 --- a/atsf_agent/internal/nginx/manager_test.go +++ b/atsf_agent/internal/nginx/manager_test.go @@ -203,6 +203,42 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) { } } +func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { + runner := &fakeRunner{} + executor := &DockerExecutor{ + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/managed/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/managed/conf.d"), + CertDir: filepath.Clean("/tmp/managed/certs"), + NginxCertDir: "/etc/nginx/atsflare-certs", + Runner: runner, + } + + if err := executor.runContainer(context.Background()); err != nil { + t.Fatalf("runContainer failed: %v", err) + } + + if len(runner.calls) != 1 { + t.Fatalf("expected one docker run call, got %d", len(runner.calls)) + } + + expectedArgs := []string{ + "run", "-d", + "--name", "atsflare-openresty", + "-p", "80:80", + "-p", "443:443", + "-v", "/tmp/managed/nginx.conf:" + DockerMainConfigPath, + "-v", "/tmp/managed/conf.d:/etc/nginx/conf.d", + "-v", "/tmp/managed/certs:/etc/nginx/atsflare-certs", + "openresty/openresty:alpine", + } + if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) { + t.Fatalf("unexpected docker run args: %#v", runner.calls[0].args) + } +} + func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) { runner := &fakeRunner{ runFn: func(name string, args ...string) ([]byte, error) { diff --git a/atsf_agent/internal/sync/service_test.go b/atsf_agent/internal/sync/service_test.go index f9150b59..67538f06 100644 --- a/atsf_agent/internal/sync/service_test.go +++ b/atsf_agent/internal/sync/service_test.go @@ -137,6 +137,15 @@ func TestSyncOnceSuccess(t *testing.T) { if len(client.reports) != 1 || client.reports[0].Result != ApplyResultSuccess { t.Fatal("expected successful apply report to be sent") } + if client.reports[0].Checksum != "checksum-1" { + t.Fatalf("expected config checksum to be reported, got %q", client.reports[0].Checksum) + } + if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" { + t.Fatal("expected main and route config checksums to be reported") + } + if client.reports[0].SupportFileCount != 1 { + t.Fatalf("expected support file count to be reported, got %d", client.reports[0].SupportFileCount) + } } func TestSyncOnceRollbackOnNginxFailure(t *testing.T) { @@ -212,6 +221,15 @@ func TestSyncOnceRollbackOnNginxFailure(t *testing.T) { if len(client.reports) != 1 || client.reports[0].Result != ApplyResultFailed { t.Fatal("expected failed apply report to be sent") } + if client.reports[0].Checksum != "checksum-2" { + t.Fatalf("expected failed report to retain target checksum, got %q", client.reports[0].Checksum) + } + if client.reports[0].MainConfigChecksum == "" || client.reports[0].RouteConfigChecksum == "" { + t.Fatal("expected failed report to include main and route config checksums") + } + if client.reports[0].SupportFileCount != 1 { + t.Fatalf("expected failed report to include support file count, got %d", client.reports[0].SupportFileCount) + } } func TestSyncOnStartupRecreatesRuntimeWhenChecksumMatches(t *testing.T) { diff --git a/docs/deployment.md b/docs/deployment.md index 2f47d3fe..735f889f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -181,9 +181,9 @@ swag init -g main.go -o docs * `agent_token` 与 `discovery_token` 至少填写一个 * 若 `agent_token` 为空且 `discovery_token` 存在,Agent 会自动注册并写回新的专属 `agent_token` * `node_name` 与 `node_ip` 可省略,未填写时自动探测 -* 未配置 `openresty_path` 时,默认使用 Docker OpenResty 容器 - -z### 3.3 第五版新增部署约束 +* 未配置 `openresty_path` 时,默认使用 Docker OpenResty 容器 + +### 3.3 第五版新增部署约束 第五版开发完成后,OpenResty 主配置将进入 Agent 受管范围。部署与联调时应满足: @@ -236,9 +236,65 @@ go build -o atsflare-agent ./cmd/agent 2. 自动注册模式下完成 Token 置换 3. 拉取激活版本 4. 写入主配置、路由配置与必要证书文件 -5. 执行 `openresty -t` -6. 执行 `openresty -s reload` -7. 上报应用结果 +5. 执行 `openresty -t` +6. 执行 `openresty -s reload` +7. 上报应用结果 + +### 5.3.1 Docker OpenResty 模式最小验证 + +建议使用最小 `agent.json`: + +```json +{ + "server_url": "http://127.0.0.1:3000", + "discovery_token": "replace-with-global-discovery-token", + "data_dir": "./data", + "openresty_container_name": "atsflare-openresty", + "openresty_docker_image": "openresty/openresty:alpine" +} +``` + +验证点: + +1. 首次启动后确认 `data/etc/nginx/nginx.conf`、`data/etc/nginx/conf.d/atsflare_routes.conf` 与 `data/etc/nginx/certs` 已由 Agent 创建 +2. 确认容器实际挂载了主配置、路由目录和证书目录 +3. 在管理端发布一次新版本后,确认节点 `current_version` 追平激活版本 +4. 在节点详情查看“当前目标版本”与“最近应用”,确认主配置/路由配置快照和 checksum 已可见 + +推荐检查命令: + +```bash +docker inspect atsflare-openresty +docker exec atsflare-openresty openresty -t +``` + +说明: + +* `docker inspect` 重点确认主配置文件、`conf.d` 目录和证书目录都来自 Agent 受管路径 +* 若容器名使用默认值,请将上述命令中的名称替换为 `atsflare-openresty` + +### 5.3.2 本机 OpenResty 模式最小验证 + +建议显式提供以下路径: + +```json +{ + "server_url": "http://127.0.0.1:3000", + "agent_token": "replace-with-node-auth-token", + "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", + "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", + "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", + "cert_dir": "/usr/local/openresty/nginx/conf/certs", + "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs" +} +``` + +验证点: + +1. 发布前先备份 `main_config_path` 与 `route_config_path` +2. 首次发布后执行 `openresty -t`,确认主配置已由 Server 模板接管且 include 指向 Agent 写入的路由文件 +3. 再次发布修改后的规则或 OpenResty 参数,确认 `openresty -s reload` 成功且节点版本更新 +4. 在节点详情与应用记录页确认主配置 checksum、路由配置 checksum 和支持文件数已上报 ### 5.4 验证管理端状态 @@ -251,12 +307,35 @@ go build -o atsflare-agent ./cmd/agent ### 5.5 验证失败回滚 -人为制造 `openresty -t` 失败后再次发布,预期: - -* Agent 回滚旧配置 -* 主配置与路由配置一起回滚 -* 节点 `last_error` 更新 -* 应用记录中出现失败记录 +人为制造 `openresty -t` 失败后再次发布,预期: + +* Agent 回滚旧配置 +* 主配置与路由配置一起回滚 +* 节点 `last_error` 更新 +* 应用记录中出现失败记录 + +### 5.5.1 建议的失败演练方式 + +建议只在测试节点进行,避免直接污染生产节点。 + +本机 OpenResty 模式: + +1. 先备份当前 `agent.json` +2. 将 `openresty_path` 临时改为一个包装脚本,在收到 `-t` 时固定返回非零,其余参数转发到真实 `openresty` +3. 触发一次新版本发布,确认应用失败、主配置与路由配置回滚、节点 `last_error` 更新 +4. 恢复真实 `openresty_path` 后再次发布,确认节点重新追平版本 + +Docker OpenResty 模式: + +1. 在测试节点上保留默认受管路径 +2. 临时将 `openresty_docker_image` 指向一个不包含 `openresty` 运行时的错误镜像标签,或在测试环境用包装镜像让 `openresty -t` 固定失败 +3. 再次发布,确认节点应用失败但 `data/etc/nginx/nginx.conf` 与 `data/etc/nginx/conf.d/atsflare_routes.conf` 已回滚为旧版本 +4. 恢复正确镜像后重新发布,确认节点恢复健康 + +说明: + +* 第五版的失败演练重点不在“如何制造错误”,而在确认失败后旧主配置、旧路由配置和支持文件都会被一起恢复 +* 若不方便做真实环境演练,至少应运行 Agent 回归测试,覆盖主配置写入、Docker 挂载与失败回滚 ---