fix(libraryctl): 同镜像不等于同一个写入者
build / build (push) Has been cancelled

共享业务栈与专用验收实例共用同一个 WeKnora 镜像,原来只按镜像判"未声明写入者",
任何一台开着共享栈的机器都永远开不了窗。判据改为:同项目或无项目标签即冲突;
另一个项目只有数据库/队列目标与源相同才算冲突。单测覆盖六种组合。
This commit is contained in:
姚章浩
2026-10-29 05:01:22 +08:00
parent 906c282986
commit fd71ea349e
2 changed files with 100 additions and 4 deletions
+53 -4
View File
@@ -732,7 +732,11 @@ func runCapture(ctx context.Context, opts options) error {
if err != nil {
return err
}
return checkNoUndeclaredWriter(ctx, images, declared)
app, err := containerEnv(ctx, opts.sourceApp)
if err != nil {
return err
}
return checkNoUndeclaredWriter(ctx, images, declared, opts.project, sourceTargetOf(app))
}
result, err := v071.CaptureLibrary(ctx, coordinator, archive, pool, search, objects, sourceReady,
v071.LibraryCaptureRequest{JobID: job, Scope: opts.scope,
@@ -809,21 +813,66 @@ func writerImageIDs(ctx context.Context, items []libraryfreeze.Service) ([]strin
// checkNoUndeclaredWriter refuses to run when a running container uses a declared
// writer image but is not part of the declared group: such a container could keep
// writing during the maintenance window.
func checkNoUndeclaredWriter(ctx context.Context, images []string, declared map[string]bool) error {
func checkNoUndeclaredWriter(ctx context.Context, images []string, declared map[string]bool,
sourceProject string, source map[string]string) error {
for _, image := range images {
out, err := dockerOutput(ctx, "ps", "-q", "--filter", "ancestor="+image)
if err != nil {
return err
}
for _, id := range strings.Fields(out) {
if !declared[id] {
return fmt.Errorf("container %s uses a declared writer image but is not in the declared writer group", id[:12])
if declared[id] {
continue
}
project, err := dockerOutput(ctx, "inspect", "--format", `{{index .Config.Labels "com.docker.compose.project"}}`, id)
if err != nil {
return err
}
env, err := containerEnv(ctx, id)
if err != nil {
return err
}
if conflict, reason := undeclaredWriterConflict(false, project, sourceProject, env, source); conflict {
return fmt.Errorf("container %s uses a declared writer image but is not in the declared writer group: %s", id[:12], reason)
}
}
}
return nil
}
// sourceTargetOf 只取"写到哪儿"的身份:同一个数据库或同一个队列才是真冲突。
func sourceTargetOf(env map[string]string) map[string]string {
return map[string]string{
"DB_HOST": env["DB_HOST"], "DB_NAME": env["DB_NAME"], "DB_PORT": env["DB_PORT"],
"REDIS_ADDR": env["REDIS_ADDR"], "REDIS_NAMESPACE": env["REDIS_NAMESPACE"],
}
}
// undeclaredWriterConflict 判断一个同镜像容器是否构成"未声明的并发写入者"。
//
// 同一 compose 项目或没有项目标签 ⇒ 无法证明隔离,直接冲突。
// 另一个项目 ⇒ 只有数据目标(数据库/队列)与源相同才算冲突:
// 镜像相同不等于写同一个库,本机共享业务栈与专用验收实例就是这种关系。
func undeclaredWriterConflict(declared bool, project, sourceProject string, env, source map[string]string) (bool, string) {
if declared {
return false, ""
}
if project == "" {
return true, "没有 compose 项目标签,无法证明它不写同一个目标"
}
if project == sourceProject {
return true, "属于同一 compose 项目,却不是声明的写入服务"
}
if source["DB_HOST"] != "" && env["DB_HOST"] == source["DB_HOST"] && env["DB_NAME"] == source["DB_NAME"] {
return true, "另一个项目的同镜像容器指向同一个源数据库"
}
if source["REDIS_ADDR"] != "" && env["REDIS_ADDR"] == source["REDIS_ADDR"] &&
env["REDIS_NAMESPACE"] == source["REDIS_NAMESPACE"] {
return true, "另一个项目的同镜像容器共用同一个源队列"
}
return false, "另一套隔离栈(数据目标不同)"
}
func containerImageOf(ctx context.Context, id string) (string, error) {
image, err := dockerOutput(ctx, "inspect", "--format", "{{.Image}}", id)
if err != nil {
+47
View File
@@ -0,0 +1,47 @@
package main
import (
"strings"
"testing"
)
// 同一台机器上可以有两套完全隔离的 WeKnora 栈共用同一个镜像(本机就是这样:
// 共享业务栈 + 专用验收实例)。只按镜像判断"未声明的写入者",会把另一套栈
// 当成并发写入者,验收链永远开不了窗;但只看项目标签又会放过多起一个
// 指向同一个库/队列的副本。判据要同时看归属与真实数据目标。
func TestUndeclaredWriterConflict(t *testing.T) {
source := map[string]string{
"DB_HOST": "kgmaint-postgres", "DB_NAME": "kgmaint_target", "DB_PORT": "5432",
"REDIS_ADDR": "kgmaint-redis:6379", "REDIS_NAMESPACE": "kgmaint",
}
cases := []struct {
name string
declared bool
project string
env map[string]string
want bool
reasonHint string
}{
{"已声明容器本身", true, "kgmaint-weknora", source, false, ""},
{"同项目里另起一份(不同名字)", false, "kgmaint-weknora", source, true, "同一 compose 项目"},
{"没有 compose 项目标签(docker run 的副本)", false, "", source, true, "没有 compose 项目标签"},
{"另一套隔离栈,数据目标不同", false, "weknora071full", map[string]string{
"DB_HOST": "WKNFull-postgres", "DB_NAME": "weknora_full", "REDIS_ADDR": "WKNFull-redis:6379", "REDIS_NAMESPACE": "weknora"},
false, ""},
{"另一套栈但指向同一个库", false, "other-project", map[string]string{
"DB_HOST": "kgmaint-postgres", "DB_NAME": "kgmaint_target", "REDIS_ADDR": "WKNFull-redis:6379"},
true, "同一个源数据库"},
{"另一套栈但共用同一队列", false, "other-project", map[string]string{
"DB_HOST": "other-pg", "DB_NAME": "other", "REDIS_ADDR": "kgmaint-redis:6379", "REDIS_NAMESPACE": "kgmaint"},
true, "同一个源队列"},
}
for _, tc := range cases {
conflict, reason := undeclaredWriterConflict(tc.declared, tc.project, "kgmaint-weknora", tc.env, source)
if conflict != tc.want {
t.Fatalf("%s: conflict=%v want %v (%s)", tc.name, conflict, tc.want, reason)
}
if tc.want && tc.reasonHint != "" && !strings.Contains(reason, tc.reasonHint) {
t.Fatalf("%s: reason %q should mention %q", tc.name, reason, tc.reasonHint)
}
}
}