feat: publish immutable workflow definitions with typed SDK

This commit is contained in:
2026-09-18 00:28:00 +08:00
parent a664ec9370
commit 520e98d9d1
33 changed files with 2603 additions and 11 deletions

View File

@@ -0,0 +1,258 @@
package mysql
import (
"context"
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"git.yuntuo.hk/k2safe/orbit/internal/fault"
"git.yuntuo.hk/k2safe/orbit/internal/jsonvalue"
"git.yuntuo.hk/k2safe/orbit/internal/jsonwire"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/auth"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
driver "github.com/go-sql-driver/mysql"
)
// WorkflowDefinitionStore 只管理定义与不可变版本,关联校验、幂等和审计同事务,不创建业务任务。
type WorkflowDefinitionStore struct{ *TaskStore }
func NewWorkflowDefinitionStore(db *sql.DB) *WorkflowDefinitionStore {
return &WorkflowDefinitionStore{NewTaskStore(db)}
}
var _ definition.Store = (*WorkflowDefinitionStore)(nil)
func readWorkflowDefinition(ctx context.Context, tx *sql.Tx, ns, id core.ID, lock bool) (definition.Summary, error) {
var out definition.Summary
q := `SELECT id,namespace_id,name,description,revision,created_at,updated_at FROM weave_definitions WHERE namespace_id=? AND id=?`
if lock {
q += ` FOR UPDATE`
}
err := tx.QueryRowContext(ctx, q, ns, id).Scan(&out.ID, &out.NamespaceID, &out.Name, &out.Description, &out.Revision, &out.CreatedAt, &out.UpdatedAt)
if errors.Is(err, sql.ErrNoRows) {
err = fault.NotFound
}
return out, err
}
func workflowAnalysis(c definition.Checked) definition.Analysis {
queues := append([]core.ID{}, c.Queues...)
return definition.Analysis{Steps: c.Steps, MaxConcurrency: c.MaxConcurrency, MaxTaskAttempts: c.MaxTaskAttempts, Queues: queues}
}
func readWorkflowVersion(ctx context.Context, tx *sql.Tx, ns, id core.ID, version string) (definition.VersionView, error) {
var out definition.VersionView
var raw []byte
var schema uint32
var actor string
var digest core.Digest
err := tx.QueryRowContext(ctx, `SELECT id,namespace_id,definition_id,version,schema_version,specification,content_digest,published_by,created_at FROM weave_definition_versions WHERE namespace_id=? AND definition_id=? AND version=?`, ns, id, version).Scan(&out.ID, &out.NamespaceID, &out.DefinitionID, &out.Version, &schema, &raw, &digest, &actor, &out.PublishedAt)
if errors.Is(err, sql.ErrNoRows) {
return out, fault.NotFound
}
if err != nil {
return out, err
}
out.PublishedBy = core.ID(actor)
if out.PublishedBy.Validate() != nil {
return out, errors.New("invalid stored workflow publisher")
}
if len(raw) > definition.MaxSpecificationBytes || json.Unmarshal(raw, &out.Specification) != nil || out.Specification.SchemaVersion != schema {
return out, errors.New("invalid stored workflow specification")
}
checked, err := definition.Check(out.Specification)
if err != nil {
return out, err
}
// 同时检查原文,不能让JSON解码忽略的未知字段或大小写别名绕过完整性验证。
canonical, err := jsonvalue.Canonical(raw)
if err != nil {
return out, err
}
rawDigest := core.Digest(sha256.Sum256(append([]byte("orbit.weave.spec.v1\x00"), canonical...)))
if digest != checked.Digest || digest != rawDigest {
return out, errors.New("workflow version digest mismatch")
}
out.ContentDigest = hex.EncodeToString(digest[:])
out.Analysis = workflowAnalysis(checked)
return out, nil
}
func authorizeWorkflowQueues(ctx context.Context, tx *sql.Tx, ns core.ID, p auth.Permissions, queues []core.ID) error {
for _, id := range queues {
if !p.AllowsQueue(id) {
return fault.NotFound
}
var found core.ID
err := tx.QueryRowContext(ctx, `SELECT id FROM pulse_queues WHERE namespace_id=? AND id=? FOR SHARE`, ns, id).Scan(&found)
if errors.Is(err, sql.ErrNoRows) {
return fault.NotFound
}
if err != nil {
return err
}
}
return nil
}
func (s *WorkflowDefinitionStore) Create(ctx context.Context, t auth.Token, key string, in definition.CreateInput) (definition.CreateResult, error) {
var out definition.CreateResult
err := s.transaction(ctx, t, definition.PublishAction, func(tx *sql.Tx, p auth.Permissions) error {
if _, err := lockApplicationResources(ctx, tx, t.NamespaceID, false); err != nil {
return err
}
id, err := core.NewID()
if err != nil {
return err
}
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "workflow.definition.create", "", key, "workflow_definition", id, in)
if err != nil {
return err
}
if cached != nil {
if err := json.Unmarshal(cached, &out); err != nil {
return err
}
out.Deduplicated = true
return nil
}
var count int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_definitions WHERE namespace_id=?`, t.NamespaceID).Scan(&count); err != nil {
return err
}
if count >= definition.MaxDefinitions {
return fault.New("WORKFLOW_DEFINITION_LIMIT", "namespace workflow definition limit reached")
}
now, err := dbNow(ctx, tx)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO weave_definitions(namespace_id,id,name,description,created_at,updated_at) VALUES (?,?,?,?,?,?)`, t.NamespaceID, id, in.Name, in.Description, now, now)
var duplicate *driver.MySQLError
if errors.As(err, &duplicate) && duplicate.Number == 1062 {
return fault.Conflict
}
if err != nil {
return err
}
out.Definition, err = readWorkflowDefinition(ctx, tx, t.NamespaceID, id, false)
if err != nil {
return err
}
if err := appendAudit(ctx, tx, t, identity, id, "workflow_definition", "workflow.definition.create", "create workflow definition", nil, out.Definition); err != nil {
return err
}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
})
return out, err
}
func (s *WorkflowDefinitionStore) Get(ctx context.Context, t auth.Token, id core.ID) (definition.Summary, error) {
var out definition.Summary
err := s.transaction(ctx, t, definition.ReadAction, func(tx *sql.Tx, _ auth.Permissions) error {
var err error
out, err = readWorkflowDefinition(ctx, tx, t.NamespaceID, id, false)
return err
})
return out, err
}
func (s *WorkflowDefinitionStore) Publish(ctx context.Context, t auth.Token, id core.ID, key string, in definition.PublishInput) (definition.PublishResult, error) {
var out definition.PublishResult
checked, err := definition.Check(in.Specification)
if err != nil {
return out, fault.Invalid(err.Error())
}
err = s.transaction(ctx, t, definition.PublishAction, func(tx *sql.Tx, p auth.Permissions) error {
if _, err := lockApplicationResources(ctx, tx, t.NamespaceID, false); err != nil {
return err
}
// 摘要身份冻结整个命令(含原因),同版本去重只比较specification内容摘要。
identity, cached, err := reserveIdentity(ctx, tx, t.NamespaceID, "workflow.version.publish", string(id), key, "workflow_definition", id, in)
if err != nil {
return err
}
before, err := readWorkflowDefinition(ctx, tx, t.NamespaceID, id, true)
if err != nil {
return err
}
if err := authorizeWorkflowQueues(ctx, tx, t.NamespaceID, p, checked.Queues); err != nil {
return err
}
stored, err := readWorkflowVersion(ctx, tx, t.NamespaceID, id, in.Version)
if err != nil && !errors.Is(err, fault.NotFound) {
return err
}
if cached != nil {
if err != nil {
return errors.New("published workflow version missing")
}
if err := json.Unmarshal(cached, &out); err != nil {
return err
}
if out.Version.ID != stored.ID || stored.ContentDigest != hex.EncodeToString(checked.Digest[:]) {
return errors.New("workflow publication receipt mismatch")
}
out.Version = stored
out.Deduplicated = true
return nil
}
if err == nil {
if stored.ContentDigest != hex.EncodeToString(checked.Digest[:]) {
return fault.Conflict
}
out = definition.PublishResult{Version: stored, Deduplicated: true}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
}
var count int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM weave_definition_versions WHERE namespace_id=? AND definition_id=?`, t.NamespaceID, id).Scan(&count); err != nil {
return err
}
if count >= definition.MaxVersions {
return fault.New("WORKFLOW_VERSION_LIMIT", "workflow definition version limit reached")
}
versionID, err := core.NewID()
if err != nil {
return err
}
raw, err := jsonwire.Marshal(in.Specification)
if err != nil {
return err
}
now, err := dbNow(ctx, tx)
if err != nil {
return err
}
_, err = tx.ExecContext(ctx, `INSERT INTO weave_definition_versions(namespace_id,id,definition_id,version,schema_version,specification,content_digest,published_by,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)`, t.NamespaceID, versionID, id, in.Version, in.Specification.SchemaVersion, string(raw), checked.Digest, string(t.CredentialID), now, now)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `UPDATE weave_definitions SET revision=revision+1,updated_at=? WHERE namespace_id=? AND id=?`, now, t.NamespaceID, id); err != nil {
return err
}
out.Version, err = readWorkflowVersion(ctx, tx, t.NamespaceID, id, in.Version)
if err != nil {
return err
}
// 审计记录固定版本、摘要和预算,不复制潜在敏感的业务常量。
audit := map[string]any{"version_id": versionID, "version": in.Version, "content_digest": out.Version.ContentDigest, "analysis": out.Version.Analysis, "revision": before.Revision + 1}
if err := appendAudit(ctx, tx, t, identity, id, "workflow_definition", "workflow.version.publish", in.Reason, map[string]any{"revision": before.Revision}, audit); err != nil {
return err
}
return saveIdentity(ctx, tx, t.NamespaceID, identity, out)
})
return out, err
}
func (s *WorkflowDefinitionStore) Version(ctx context.Context, t auth.Token, id core.ID, version string) (definition.VersionView, error) {
var out definition.VersionView
err := s.transaction(ctx, t, definition.ReadAction, func(tx *sql.Tx, p auth.Permissions) error {
if _, err := readWorkflowDefinition(ctx, tx, t.NamespaceID, id, false); err != nil {
return err
}
var err error
out, err = readWorkflowVersion(ctx, tx, t.NamespaceID, id, version)
if err != nil {
return err
}
return authorizeWorkflowQueues(ctx, tx, t.NamespaceID, p, out.Analysis.Queues)
})
return out, err
}