6440e3f679
- 迁移 000044:production_kb_bindings + release_item_executions.target_knowledge_base_id - 系统设置新增「生产知识库绑定(对内/对外)」:从生产 WeKnora 实际列表选库, 保存时真校验(不存在拒收;生产端不可达只提示并如实回报),仅管理员可改 - 执行计划按绑定解析目标库并冻结进 release_item_executions;空目标直接拒收; 已发布过的条目保留原目标(重试/回滚回到同一处) - DocumentBundle.RemapKnowledgeBase:knowledges/chunks/knowledge_tags/wiki_pages/ wiki_folders 的 knowledge_base_id 与 ES 文档字段改写为目标库,重算表/文档摘要并重新封装 - 契约比对改为「声明的映射对」(KnowledgeBaseMapping),其余字段继续 fail-closed - 契约检查器 / 目标身份 / 验证器全部走映射后的目标库;对象通道无需改写 - Compose 拆分:数据面接线 compose.release-sim.yaml + 两道门禁 compose.release-gates.yaml - 测试:RemapKnowledgeBase 全通道/无操作/fail-closed、映射契约、绑定接口、RBAC、真实 PG 集成
253 lines
11 KiB
Go
253 lines
11 KiB
Go
package v071
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
"knowledge-governance/internal/releasebundle"
|
|
releaseworkflow "knowledge-governance/internal/workflows/release"
|
|
)
|
|
|
|
type documentArchive interface {
|
|
Save(bundle *DocumentBundle) error
|
|
Load(releaseRevision string) (*DocumentBundle, error)
|
|
}
|
|
|
|
type targetContractInspector interface {
|
|
Contract(ctx context.Context, item releaseworkflow.PlanItem) (releasebundle.TargetContract, error)
|
|
}
|
|
|
|
type postgresDocumentTarget interface {
|
|
Replace(ctx context.Context, identity PostgresIdentity, snapshot PostgresSnapshot) error
|
|
Disable(ctx context.Context, identity PostgresIdentity) error
|
|
// ReplaceWiki 只应用库级 wiki 快照:撤权/回滚路径上文档行不能重灌,
|
|
// 但 wiki 必须整体替换成目标版本的快照。
|
|
ReplaceWiki(ctx context.Context, identity PostgresIdentity, snapshot PostgresSnapshot) error
|
|
}
|
|
|
|
type searchDocumentTarget interface {
|
|
Replace(ctx context.Context, identity SearchIdentity, snapshot SearchSnapshot) (SearchSnapshot, error)
|
|
Disable(ctx context.Context, identity SearchIdentity) error
|
|
}
|
|
|
|
type objectDocumentTarget interface {
|
|
Stage(ctx context.Context, releaseRevision string, snapshot ObjectSnapshot) (ObjectStage, error)
|
|
Promote(ctx context.Context, stage ObjectStage) error
|
|
}
|
|
|
|
type ProductionVerifier interface {
|
|
Verify(ctx context.Context, item releaseworkflow.PlanItem, bundle *DocumentBundle) error
|
|
VerifyDisabled(ctx context.Context, item releaseworkflow.PlanItem) error
|
|
// VerifyWiki 校验生产端 wiki 与包内快照一致(老包/未启用 wiki 时跳过)。
|
|
VerifyWiki(ctx context.Context, item releaseworkflow.PlanItem, bundle *DocumentBundle) error
|
|
}
|
|
|
|
type DocumentTargetConfig struct {
|
|
Archive documentArchive
|
|
Contract targetContractInspector
|
|
Postgres postgresDocumentTarget
|
|
Search searchDocumentTarget
|
|
Objects objectDocumentTarget
|
|
Verifier ProductionVerifier
|
|
}
|
|
|
|
type DocumentTarget struct {
|
|
config DocumentTargetConfig
|
|
}
|
|
|
|
func NewDocumentTarget(config DocumentTargetConfig) (*DocumentTarget, error) {
|
|
if config.Archive == nil || config.Contract == nil || config.Postgres == nil || config.Search == nil || config.Objects == nil || config.Verifier == nil {
|
|
return nil, errors.New("v0.7.1 document target dependencies are required")
|
|
}
|
|
return &DocumentTarget{config: config}, nil
|
|
}
|
|
|
|
func (target *DocumentTarget) Contract(ctx context.Context, item releaseworkflow.PlanItem) (releasebundle.TargetContract, error) {
|
|
contract, err := target.config.Contract.Contract(ctx, item)
|
|
if err != nil {
|
|
return releasebundle.TargetContract{}, errors.New("inspect target release contract")
|
|
}
|
|
return contract, nil
|
|
}
|
|
|
|
// loadBundle loads an archived bundle and applies the item's 源库 → 目标库
|
|
// mapping, so activate / compensate / restore / verify all write and read the
|
|
// same target base. The archive itself keeps the canonical source-form bundle:
|
|
// the remap is deterministic, so a rollback re-derives the same target form.
|
|
func (target *DocumentTarget) loadBundle(item releaseworkflow.PlanItem, revision string) (*DocumentBundle, error) {
|
|
bundle, err := target.config.Archive.Load(revision)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := bundle.RemapKnowledgeBase(item.TargetBase()); err != nil {
|
|
return nil, err
|
|
}
|
|
return bundle, nil
|
|
}
|
|
|
|
func (target *DocumentTarget) Prepare(ctx context.Context, item releaseworkflow.PlanItem, exported releaseworkflow.ExportedBundle) (releaseworkflow.PreparedChange, error) {
|
|
bundle, ok := exported.(*DocumentBundle)
|
|
if !ok || validateDocumentBundle(bundle) != nil || bundle.Manifest.ReleaseRevision != item.ReleaseRevision {
|
|
return releaseworkflow.PreparedChange{}, errors.New("invalid v0.7.1 target bundle")
|
|
}
|
|
if err := target.config.Archive.Save(bundle); err != nil {
|
|
return releaseworkflow.PreparedChange{}, errors.New("archive v0.7.1 target bundle")
|
|
}
|
|
previousRevision := item.PreviousProductionRevision
|
|
if previousRevision == "" {
|
|
previousRevision = "absent"
|
|
} else if previousRevision != "absent" {
|
|
if _, err := target.config.Archive.Load(previousRevision); err != nil {
|
|
return releaseworkflow.PreparedChange{}, errors.New("prior release bundle is unavailable")
|
|
}
|
|
}
|
|
if _, err := target.config.Objects.Stage(ctx, bundle.Manifest.ReleaseRevision, bundle.Objects); err != nil {
|
|
return releaseworkflow.PreparedChange{}, errors.New("stage v0.7.1 release assets")
|
|
}
|
|
return releaseworkflow.PreparedChange{PreviousRevision: previousRevision, ProductionRevision: bundle.Manifest.ReleaseRevision}, nil
|
|
}
|
|
|
|
func (target *DocumentTarget) Activate(ctx context.Context, item releaseworkflow.PlanItem, change releaseworkflow.PreparedChange) error {
|
|
bundle, err := target.loadBundle(item, change.ProductionRevision)
|
|
if err != nil || bundle.Manifest.ReleaseRevision != item.ReleaseRevision {
|
|
return errors.New("load staged v0.7.1 release bundle")
|
|
}
|
|
stage, err := target.config.Objects.Stage(ctx, bundle.Manifest.ReleaseRevision, bundle.Objects)
|
|
if err != nil {
|
|
return errors.New("restage v0.7.1 release assets")
|
|
}
|
|
if err := target.config.Objects.Promote(ctx, stage); err != nil {
|
|
return errors.New("promote v0.7.1 release assets")
|
|
}
|
|
postgresIdentity, searchIdentity := targetIdentities(item, bundle.Manifest)
|
|
if err := target.config.Postgres.Replace(ctx, postgresIdentity, bundle.Postgres); err != nil {
|
|
return errors.New("activate v0.7.1 PostgreSQL release")
|
|
}
|
|
if _, err := target.config.Search.Replace(ctx, searchIdentity, bundle.Search); err != nil {
|
|
return errors.New("activate v0.7.1 Elasticsearch release")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (target *DocumentTarget) Verify(ctx context.Context, item releaseworkflow.PlanItem, change releaseworkflow.PreparedChange) error {
|
|
bundle, err := target.loadBundle(item, change.ProductionRevision)
|
|
if err != nil {
|
|
return errors.New("load v0.7.1 release for verification")
|
|
}
|
|
if err := target.config.Verifier.Verify(ctx, item, bundle); err != nil {
|
|
return errors.New("verify v0.7.1 production release")
|
|
}
|
|
if err := target.config.Verifier.VerifyWiki(ctx, item, bundle); err != nil {
|
|
return errors.New("verify v0.7.1 production wiki release")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (target *DocumentTarget) Compensate(ctx context.Context, item releaseworkflow.PlanItem, change releaseworkflow.PreparedChange) error {
|
|
if change.PreviousRevision == "" || change.PreviousRevision == "absent" {
|
|
manifestBundle, err := target.loadBundle(item, change.ProductionRevision)
|
|
if err != nil {
|
|
return errors.New("load failed first release bundle")
|
|
}
|
|
postgresIdentity, searchIdentity := targetIdentities(item, manifestBundle.Manifest)
|
|
if err := target.config.Postgres.Disable(ctx, postgresIdentity); err != nil {
|
|
return errors.New("disable failed first PostgreSQL release")
|
|
}
|
|
if err := target.config.Search.Disable(ctx, searchIdentity); err != nil {
|
|
return errors.New("disable failed first Elasticsearch release")
|
|
}
|
|
if err := target.config.Verifier.VerifyDisabled(ctx, item); err != nil {
|
|
return errors.New("verify disabled first release")
|
|
}
|
|
return nil
|
|
}
|
|
return target.restoreBundle(ctx, item, change.PreviousRevision)
|
|
}
|
|
|
|
func (target *DocumentTarget) Restore(ctx context.Context, item releaseworkflow.PlanItem, revision string) (string, error) {
|
|
if revision == "absent" {
|
|
current, err := target.loadBundle(item, item.ReleaseRevision)
|
|
if err != nil {
|
|
return "", errors.New("load current release for disable")
|
|
}
|
|
postgresIdentity, searchIdentity := targetIdentities(item, current.Manifest)
|
|
if err := target.config.Postgres.Disable(ctx, postgresIdentity); err != nil {
|
|
return "", errors.New("disable current PostgreSQL release")
|
|
}
|
|
if err := target.config.Search.Disable(ctx, searchIdentity); err != nil {
|
|
return "", errors.New("disable current Elasticsearch release")
|
|
}
|
|
// 文档行只做 disable,但 wiki 要整体替换成撤权后的快照,
|
|
// 否则生产 wiki 会继续留着这条文档的页面与引用。
|
|
if hasWikiSnapshot(current.Postgres) {
|
|
if err := target.config.Postgres.ReplaceWiki(ctx, postgresIdentity, current.Postgres); err != nil {
|
|
return "", errors.New("apply withdrawn v0.7.1 wiki snapshot")
|
|
}
|
|
}
|
|
return "absent", nil
|
|
}
|
|
if err := target.restoreBundle(ctx, item, revision); err != nil {
|
|
return "", err
|
|
}
|
|
return revision, nil
|
|
}
|
|
|
|
func (target *DocumentTarget) VerifyRestored(ctx context.Context, item releaseworkflow.PlanItem, revision string) error {
|
|
if revision == "absent" {
|
|
if err := target.config.Verifier.VerifyDisabled(ctx, item); err != nil {
|
|
return errors.New("verify disabled production release")
|
|
}
|
|
current, err := target.loadBundle(item, item.ReleaseRevision)
|
|
if err != nil {
|
|
return errors.New("load withdrawn v0.7.1 release for wiki verification")
|
|
}
|
|
if err := target.config.Verifier.VerifyWiki(ctx, item, current); err != nil {
|
|
return errors.New("verify withdrawn v0.7.1 wiki release")
|
|
}
|
|
return nil
|
|
}
|
|
bundle, err := target.loadBundle(item, revision)
|
|
if err != nil {
|
|
return errors.New("load restored v0.7.1 release")
|
|
}
|
|
if err := target.config.Verifier.Verify(ctx, item, bundle); err != nil {
|
|
return errors.New("verify restored v0.7.1 release")
|
|
}
|
|
if err := target.config.Verifier.VerifyWiki(ctx, item, bundle); err != nil {
|
|
return errors.New("verify restored v0.7.1 wiki release")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (target *DocumentTarget) restoreBundle(ctx context.Context, item releaseworkflow.PlanItem, revision string) error {
|
|
bundle, err := target.loadBundle(item, revision)
|
|
if err != nil {
|
|
return errors.New("load prior v0.7.1 release bundle")
|
|
}
|
|
stage, err := target.config.Objects.Stage(ctx, revision, bundle.Objects)
|
|
if err != nil {
|
|
return errors.New("stage prior v0.7.1 release assets")
|
|
}
|
|
if err := target.config.Objects.Promote(ctx, stage); err != nil {
|
|
return errors.New("promote prior v0.7.1 release assets")
|
|
}
|
|
postgresIdentity, searchIdentity := targetIdentities(item, bundle.Manifest)
|
|
if err := target.config.Postgres.Replace(ctx, postgresIdentity, bundle.Postgres); err != nil {
|
|
return errors.New("restore prior v0.7.1 PostgreSQL release")
|
|
}
|
|
if _, err := target.config.Search.Replace(ctx, searchIdentity, bundle.Search); err != nil {
|
|
return errors.New("restore prior v0.7.1 Elasticsearch release")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// targetIdentities is where a release item is written: the 源库 → 目标库 mapped
|
|
// base (item.TargetBase()), the tenant and the knowledge id that travel with the
|
|
// bundle. The bundle's own contract keeps the SOURCE base it was exported from.
|
|
func targetIdentities(item releaseworkflow.PlanItem, manifest releasebundle.Manifest) (PostgresIdentity, SearchIdentity) {
|
|
postgresIdentity := PostgresIdentity{TenantID: manifest.Contract.TenantID, KnowledgeBaseID: item.TargetBase(), KnowledgeID: manifest.Contract.KnowledgeID}
|
|
return postgresIdentity, SearchIdentity(postgresIdentity)
|
|
}
|
|
|
|
var _ releaseworkflow.SidecarTarget = (*DocumentTarget)(nil)
|