共享业务栈与专用验收实例共用同一个 WeKnora 镜像,原来只按镜像判"未声明写入者", 任何一台开着共享栈的机器都永远开不了窗。判据改为:同项目或无项目标签即冲突; 另一个项目只有数据库/队列目标与源相同才算冲突。单测覆盖六种组合。
This commit is contained in:
+53
-4
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user