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 }