Files
knowledge-governance/cmd/release-worker/config.go
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

156 lines
5.7 KiB
Go

package main
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/url"
"os"
"path/filepath"
"strings"
"syscall"
)
const maxReleaseConfigBytes = 1024 * 1024
type releaseRuntimeConfig struct {
ArchiveRoot string `json:"archive_root"`
Routes []releaseRouteConfig `json:"routes"`
}
type releaseRouteConfig struct {
TenantID uint64 `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
EmbeddingModel string `json:"embedding_model"`
Source releaseEndpointConfig `json:"source"`
Target releaseEndpointConfig `json:"target"`
}
type releaseEndpointConfig struct {
DatabaseURLFile string `json:"database_url_file"`
Elasticsearch releaseElasticsearchConfig `json:"elasticsearch"`
Object releaseObjectConfig `json:"object"`
WeKnora releaseWeKnoraConfig `json:"weknora"`
}
type releaseElasticsearchConfig struct {
BaseURL string `json:"base_url"`
Index string `json:"index"`
TokenFile string `json:"token_file"`
}
type releaseObjectConfig struct {
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
Region string `json:"region"`
Bucket string `json:"bucket"`
StorageBackendID string `json:"storage_backend_id"`
AccessKeyFile string `json:"access_key_file"`
SecretKeyFile string `json:"secret_key_file"`
}
type releaseWeKnoraConfig struct {
BaseURL string `json:"base_url"`
APIKeyFile string `json:"api_key_file"`
AllowDockerHTTP bool `json:"allow_docker_http"`
}
func loadReleaseRuntimeConfig(path string) (releaseRuntimeConfig, error) {
if !filepath.IsAbs(path) {
return releaseRuntimeConfig{}, errors.New("release config path must be absolute")
}
fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
if err != nil {
return releaseRuntimeConfig{}, errors.New("open release config")
}
file := os.NewFile(uintptr(fd), "release-config")
if file == nil {
syscall.Close(fd)
return releaseRuntimeConfig{}, errors.New("open release config")
}
defer file.Close()
info, err := file.Stat()
if err != nil || !info.Mode().IsRegular() || info.Mode().Perm() != 0o600 || info.Size() <= 0 || info.Size() > maxReleaseConfigBytes {
return releaseRuntimeConfig{}, errors.New("release config must be a non-empty regular 0600 file")
}
payload, err := io.ReadAll(io.LimitReader(file, maxReleaseConfigBytes+1))
if err != nil || len(payload) > maxReleaseConfigBytes {
return releaseRuntimeConfig{}, errors.New("read release config")
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
var config releaseRuntimeConfig
if err := decoder.Decode(&config); err != nil {
return releaseRuntimeConfig{}, errors.New("decode release config")
}
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
return releaseRuntimeConfig{}, errors.New("release config contains trailing data")
}
if err := validateReleaseRuntimeConfig(config); err != nil {
return releaseRuntimeConfig{}, err
}
return config, nil
}
func validateReleaseRuntimeConfig(config releaseRuntimeConfig) error {
if !filepath.IsAbs(config.ArchiveRoot) || len(config.Routes) == 0 {
return errors.New("release archive root and routes are required")
}
seen := map[string]bool{}
for _, route := range config.Routes {
if route.TenantID == 0 || !validConfigIdentifier(route.KnowledgeBaseID) || !validConfigIdentifier(route.EmbeddingModel) || seen[route.KnowledgeBaseID] {
return errors.New("invalid or duplicate release route")
}
seen[route.KnowledgeBaseID] = true
if !validReleaseEndpoint(route.Source, false) || !validReleaseEndpoint(route.Target, true) {
return errors.New("invalid release route endpoint")
}
if route.Source.DatabaseURLFile == route.Target.DatabaseURLFile ||
route.Source.Elasticsearch.BaseURL == route.Target.Elasticsearch.BaseURL ||
(route.Source.Object.Endpoint == route.Target.Object.Endpoint && route.Source.Object.Bucket == route.Target.Object.Bucket) {
return errors.New("release source and target must be distinct")
}
}
return nil
}
func validReleaseEndpoint(endpoint releaseEndpointConfig, target bool) bool {
if !absoluteFile(endpoint.DatabaseURLFile) || !validHTTPURL(endpoint.Elasticsearch.BaseURL) || !validConfigIdentifier(endpoint.Elasticsearch.Index) ||
(endpoint.Elasticsearch.TokenFile != "" && !absoluteFile(endpoint.Elasticsearch.TokenFile)) ||
!validObjectEndpoint(endpoint.Object) {
return false
}
if target {
return validConfigIdentifier(endpoint.Object.StorageBackendID) && validHTTPURL(endpoint.WeKnora.BaseURL) && absoluteFile(endpoint.WeKnora.APIKeyFile)
}
return endpoint.WeKnora.BaseURL == "" && endpoint.WeKnora.APIKeyFile == ""
}
func validObjectEndpoint(object releaseObjectConfig) bool {
return (object.Provider == "minio" || object.Provider == "s3" || object.Provider == "oss") && validHTTPURL(object.Endpoint) &&
validConfigIdentifier(object.Bucket) && absoluteFile(object.AccessKeyFile) && absoluteFile(object.SecretKeyFile)
}
func absoluteFile(path string) bool { return filepath.IsAbs(path) && filepath.Clean(path) == path }
func validHTTPURL(value string) bool {
parsed, err := url.Parse(value)
return err == nil && (parsed.Scheme == "http" || parsed.Scheme == "https") && parsed.Host != "" && parsed.User == nil && parsed.RawQuery == "" && parsed.Fragment == ""
}
func validConfigIdentifier(value string) bool {
value = strings.TrimSpace(value)
if value == "" || len(value) > 128 {
return false
}
for _, char := range value {
if (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9') || char == '-' || char == '_' || char == '.' {
continue
}
return false
}
return true
}