Files
orbit/internal/storage/mysql/workflow_definitions.go

267 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
scope, err := json.Marshal(append([]core.ID{}, checked.Queues...))
if err != nil {
return err
}
if _, err = tx.ExecContext(ctx, `INSERT INTO weave_version_scopes(namespace_id,definition_version_id,definition_id,content_digest,queue_ids,created_at) VALUES (?,?,?,?,?,?)`, t.NamespaceID, versionID, id, checked.Digest, string(scope), now); 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
}