mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
Add workflow execution process
This commit is contained in:
parent
bde2147dd3
commit
775b12aec1
@ -1,6 +1,7 @@
|
||||
package applicant
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
@ -170,7 +171,38 @@ func Get(record *models.Record) (Applicant, error) {
|
||||
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
|
||||
}
|
||||
|
||||
switch access.GetString("configType") {
|
||||
return GetWithTypeOption(access.GetString("configType"), option)
|
||||
}
|
||||
|
||||
func GetWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
|
||||
// 获取授权配置
|
||||
accessRepo := repository.NewAccessRepository()
|
||||
|
||||
access, err := accessRepo.GetById(context.Background(), node.GetConfigString("access"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("access record not found: %w", err)
|
||||
}
|
||||
|
||||
timeout := node.GetConfigInt64("timeout")
|
||||
if timeout == 0 {
|
||||
timeout = defaultTimeout
|
||||
}
|
||||
|
||||
applyConfig := &ApplyOption{
|
||||
Email: node.GetConfigString("email"),
|
||||
Domain: node.GetConfigString("domain"),
|
||||
Access: access.Config,
|
||||
KeyAlgorithm: node.GetConfigString("keyAlgorithm"),
|
||||
Nameservers: node.GetConfigString("nameservers"),
|
||||
Timeout: timeout,
|
||||
DisableFollowCNAME: node.GetConfigBool("disableFollowCNAME"),
|
||||
}
|
||||
|
||||
return GetWithTypeOption(access.ConfigType, applyConfig)
|
||||
}
|
||||
|
||||
func GetWithTypeOption(t string, option *ApplyOption) (Applicant, error) {
|
||||
switch t {
|
||||
case configTypeAliyun:
|
||||
return NewAliyun(option), nil
|
||||
case configTypeTencent:
|
||||
|
@ -1,5 +1,16 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Access struct {
|
||||
Meta
|
||||
Name string `json:"name"`
|
||||
Config string `json:"config"`
|
||||
ConfigType string `json:"configType"`
|
||||
Deleted time.Time `json:"deleted"`
|
||||
Usage string `json:"usage"`
|
||||
}
|
||||
|
||||
type AliyunAccess struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
|
39
internal/domain/certificate.go
Normal file
39
internal/domain/certificate.go
Normal file
@ -0,0 +1,39 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Certificate struct {
|
||||
Meta
|
||||
SAN string `json:"san"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
IssuerCertificate string `json:"issuerCertificate"`
|
||||
CertUrl string `json:"certUrl"`
|
||||
CertStableUrl string `json:"certStableUrl"`
|
||||
Output string `json:"output"`
|
||||
ExpireAt time.Time `json:"ExpireAt"`
|
||||
}
|
||||
|
||||
type MetaData struct {
|
||||
Version string `json:"version"`
|
||||
SerialNumber string `json:"serialNumber"`
|
||||
Validity CertificateValidity `json:"validity"`
|
||||
SignatureAlgorithm string `json:"signatureAlgorithm"`
|
||||
Issuer CertificateIssuer `json:"issuer"`
|
||||
Subject CertificateSubject `json:"subject"`
|
||||
}
|
||||
|
||||
type CertificateIssuer struct {
|
||||
Country string `json:"country"`
|
||||
Organization string `json:"organization"`
|
||||
CommonName string `json:"commonName"`
|
||||
}
|
||||
|
||||
type CertificateSubject struct {
|
||||
CN string `json:"CN"`
|
||||
}
|
||||
|
||||
type CertificateValidity struct {
|
||||
NotBefore string `json:"notBefore"`
|
||||
NotAfter string `json:"notAfter"`
|
||||
}
|
9
internal/domain/common.go
Normal file
9
internal/domain/common.go
Normal file
@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type Meta struct {
|
||||
Id string `json:"id"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
@ -1,6 +1,16 @@
|
||||
package domain
|
||||
|
||||
var ErrAuthFailed = NewXError(4999, "auth failed")
|
||||
var (
|
||||
ErrInvalidParams = NewXError(400, "invalid params")
|
||||
ErrRecordNotFound = NewXError(404, "record not found")
|
||||
)
|
||||
|
||||
func IsRecordNotFound(err error) bool {
|
||||
if e, ok := err.(*XError); ok {
|
||||
return e.GetCode() == ErrRecordNotFound.GetCode()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type XError struct {
|
||||
Code int `json:"code"`
|
||||
|
@ -1,9 +1,22 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
WorkflowNodeTypeStart = "start"
|
||||
WorkflowNodeTypeEnd = "end"
|
||||
WorkflowNodeTypeApply = "apply"
|
||||
WorkflowNodeTypeDeply = "deploy"
|
||||
WorkflowNodeTypeNotify = "notify"
|
||||
WorkflowNodeTypeBranch = "branch"
|
||||
WorkflowNodeTypeCondition = "condition"
|
||||
)
|
||||
|
||||
type Workflow struct {
|
||||
Id string `json:"id"`
|
||||
Meta
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type"`
|
||||
@ -11,8 +24,6 @@ type Workflow struct {
|
||||
Draft *WorkflowNode `json:"draft"`
|
||||
Enabled bool `json:"enabled"`
|
||||
HasDraft bool `json:"hasDraft"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type WorkflowNode struct {
|
||||
@ -29,11 +40,48 @@ type WorkflowNode struct {
|
||||
Branches []WorkflowNode `json:"branches"`
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigString(key string) string {
|
||||
if v, ok := n.Config[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigBool(key string) bool {
|
||||
if v, ok := n.Config[key]; ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *WorkflowNode) GetConfigInt64(key string) int64 {
|
||||
// 先转成字符串,再转成 int64
|
||||
if v, ok := n.Config[key]; ok {
|
||||
temp := fmt.Sprintf("%v", v)
|
||||
if i, err := strconv.ParseInt(temp, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
type WorkflowNodeIo struct {
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Label string `json:"label"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Required bool `json:"required"`
|
||||
Value any `json:"value"`
|
||||
ValueSelector WorkflowNodeIoValueSelector `json:"valueSelector"`
|
||||
}
|
||||
|
||||
type WorkflowNodeIoValueSelector struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type WorkflowRunReq struct {
|
||||
|
12
internal/domain/workflow_output.go
Normal file
12
internal/domain/workflow_output.go
Normal file
@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
const WorkflowOutputCertificate = "certificate"
|
||||
|
||||
type WorkflowOutput struct {
|
||||
Meta
|
||||
Workflow string `json:"workflow"`
|
||||
NodeId string `json:"nodeId"`
|
||||
Node *WorkflowNode `json:"node"`
|
||||
Output []WorkflowNodeIo `json:"output"`
|
||||
Succeed bool `json:"succeed"`
|
||||
}
|
17
internal/repository/access.go
Normal file
17
internal/repository/access.go
Normal file
@ -0,0 +1,17 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type AccessRepository struct{}
|
||||
|
||||
func NewAccessRepository() *AccessRepository {
|
||||
return &AccessRepository{}
|
||||
}
|
||||
|
||||
func (a *AccessRepository) GetById(ctx context.Context, id string) (*domain.Access, error) {
|
||||
return nil, nil
|
||||
}
|
54
internal/repository/workflow.go
Normal file
54
internal/repository/workflow.go
Normal file
@ -0,0 +1,54 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
type WorkflowRepository struct{}
|
||||
|
||||
func NewWorkflowRepository() *WorkflowRepository {
|
||||
return &WorkflowRepository{}
|
||||
}
|
||||
|
||||
func (w *WorkflowRepository) Get(ctx context.Context, id string) (*domain.Workflow, error) {
|
||||
record, err := app.GetApp().Dao().FindRecordById("workflow", id)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
content := &domain.WorkflowNode{}
|
||||
if err := record.UnmarshalJSONField("content", content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
draft := &domain.WorkflowNode{}
|
||||
if err := record.UnmarshalJSONField("draft", draft); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
workflow := &domain.Workflow{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
Created: record.GetTime("created"),
|
||||
Updated: record.GetTime("updated"),
|
||||
},
|
||||
Name: record.GetString("name"),
|
||||
Description: record.GetString("description"),
|
||||
Type: record.GetString("type"),
|
||||
Enabled: record.GetBool("enabled"),
|
||||
HasDraft: record.GetBool("hasDraft"),
|
||||
|
||||
Content: content,
|
||||
Draft: draft,
|
||||
}
|
||||
|
||||
return workflow, nil
|
||||
}
|
105
internal/repository/workflow_output.go
Normal file
105
internal/repository/workflow_output.go
Normal file
@ -0,0 +1,105 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
type WorkflowOutputRepository struct{}
|
||||
|
||||
func NewWorkflowOutputRepository() *WorkflowOutputRepository {
|
||||
return &WorkflowOutputRepository{}
|
||||
}
|
||||
|
||||
func (w *WorkflowOutputRepository) Get(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error) {
|
||||
record, err := app.GetApp().Dao().FindFirstRecordByFilter("workflow_output", "nodeId={:nodeId}", dbx.Params{"nodeId": nodeId})
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, domain.ErrRecordNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
node := &domain.WorkflowNode{}
|
||||
if err := record.UnmarshalJSONField("node", node); err != nil {
|
||||
return nil, errors.New("failed to unmarshal node")
|
||||
}
|
||||
|
||||
output := make([]domain.WorkflowNodeIo, 0)
|
||||
if err := record.UnmarshalJSONField("output", &output); err != nil {
|
||||
return nil, errors.New("failed to unmarshal output")
|
||||
}
|
||||
|
||||
rs := &domain.WorkflowOutput{
|
||||
Meta: domain.Meta{
|
||||
Id: record.GetId(),
|
||||
Created: record.GetTime("created"),
|
||||
Updated: record.GetTime("updated"),
|
||||
},
|
||||
Workflow: record.GetString("workflow"),
|
||||
NodeId: record.GetString("nodeId"),
|
||||
Node: node,
|
||||
Output: output,
|
||||
}
|
||||
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
// 保存节点输出
|
||||
func (w *WorkflowOutputRepository) Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error {
|
||||
var record *models.Record
|
||||
var err error
|
||||
|
||||
if output.Id == "" {
|
||||
collection, err := app.GetApp().Dao().FindCollectionByNameOrId("workflow_output")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
record = models.NewRecord(collection)
|
||||
} else {
|
||||
record, err = app.GetApp().Dao().FindRecordById("workflow_output", output.Id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
record.Set("workflow", output.Workflow)
|
||||
record.Set("nodeId", output.NodeId)
|
||||
record.Set("node", output.Node)
|
||||
record.Set("output", output.Output)
|
||||
|
||||
if err := app.GetApp().Dao().SaveRecord(record); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cb != nil && certificate != nil {
|
||||
if err := cb(record.GetId()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certCollection, err := app.GetApp().Dao().FindCollectionByNameOrId("certificate")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certRecord := models.NewRecord(certCollection)
|
||||
certRecord.Set("certificate", certificate.Certificate)
|
||||
certRecord.Set("privateKey", certificate.PrivateKey)
|
||||
certRecord.Set("issuerCertificate", certificate.IssuerCertificate)
|
||||
certRecord.Set("san", certificate.SAN)
|
||||
certRecord.Set("workflowOutput", certificate.Output)
|
||||
certRecord.Set("expireAt", certificate.ExpireAt)
|
||||
certRecord.Set("certUrl", certificate.CertUrl)
|
||||
certRecord.Set("certStableUrl", certificate.CertStableUrl)
|
||||
|
||||
if err := app.GetApp().Dao().SaveRecord(certRecord); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
103
internal/workflow/node-processor/apply_node.go
Normal file
103
internal/workflow/node-processor/apply_node.go
Normal file
@ -0,0 +1,103 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
"github.com/usual2970/certimate/internal/repository"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type applyNode struct {
|
||||
node *domain.WorkflowNode
|
||||
outputRepo WorkflowOutputRepository
|
||||
*Logger
|
||||
}
|
||||
|
||||
func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
||||
return &applyNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
outputRepo: repository.NewWorkflowOutputRepository(),
|
||||
}
|
||||
}
|
||||
|
||||
type WorkflowOutputRepository interface {
|
||||
// 查询节点输出
|
||||
Get(ctx context.Context, nodeId string) (*domain.WorkflowOutput, error)
|
||||
|
||||
// 保存节点输出
|
||||
Save(ctx context.Context, output *domain.WorkflowOutput, certificate *domain.Certificate, cb func(id string) error) error
|
||||
}
|
||||
|
||||
// 申请节点根据申请类型执行不同的操作
|
||||
func (a *applyNode) Run(ctx context.Context) error {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "开始执行")
|
||||
// 查询是否申请过,已申请过则直接返回(先保持和 v0.2 一致)
|
||||
output, err := a.outputRepo.Get(ctx, a.node.Id)
|
||||
if err != nil && !domain.IsRecordNotFound(err) {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "查询申请记录失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
if output != nil && output.Succeed {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "已申请过")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 获取Applicant
|
||||
apply, err := applicant.GetWithApplyNode(a.node)
|
||||
if err != nil {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "获取申请对象失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// 申请
|
||||
certificate, err := apply.Apply()
|
||||
if err != nil {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "申请失败", err.Error())
|
||||
return err
|
||||
}
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "申请成功")
|
||||
|
||||
// 记录申请结果
|
||||
output = &domain.WorkflowOutput{
|
||||
Workflow: GetWorkflowId(ctx),
|
||||
NodeId: a.node.Id,
|
||||
Node: a.node,
|
||||
Succeed: true,
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificateFromPEM(certificate.Certificate)
|
||||
if err != nil {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "解析证书失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
certificateRecord := &domain.Certificate{
|
||||
SAN: cert.Subject.CommonName,
|
||||
Certificate: certificate.Certificate,
|
||||
PrivateKey: certificate.PrivateKey,
|
||||
IssuerCertificate: certificate.IssuerCertificate,
|
||||
CertUrl: certificate.CertUrl,
|
||||
CertStableUrl: certificate.CertStableUrl,
|
||||
ExpireAt: cert.NotAfter,
|
||||
}
|
||||
|
||||
if err := a.outputRepo.Save(ctx, output, certificateRecord, func(id string) error {
|
||||
if certificateRecord != nil {
|
||||
certificateRecord.Id = id
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "保存申请记录失败", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
a.AddOutput(ctx, xtime.BeijingTimeStr(), a.node.Name, "保存申请记录成功")
|
||||
|
||||
return nil
|
||||
}
|
29
internal/workflow/node-processor/condition_node.go
Normal file
29
internal/workflow/node-processor/condition_node.go
Normal file
@ -0,0 +1,29 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type conditionNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*Logger
|
||||
}
|
||||
|
||||
func NewConditionNode(node *domain.WorkflowNode) *conditionNode {
|
||||
return &conditionNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
}
|
||||
}
|
||||
|
||||
// 条件节点没有任何操作
|
||||
func (c *conditionNode) Run(ctx context.Context) error {
|
||||
c.AddOutput(ctx, xtime.BeijingTimeStr(),
|
||||
c.node.Name,
|
||||
"完成",
|
||||
)
|
||||
return nil
|
||||
}
|
1
internal/workflow/node-processor/deploy_node.go
Normal file
1
internal/workflow/node-processor/deploy_node.go
Normal file
@ -0,0 +1 @@
|
||||
package nodeprocessor
|
1
internal/workflow/node-processor/notify_node.go
Normal file
1
internal/workflow/node-processor/notify_node.go
Normal file
@ -0,0 +1 @@
|
||||
package nodeprocessor
|
69
internal/workflow/node-processor/processor.go
Normal file
69
internal/workflow/node-processor/processor.go
Normal file
@ -0,0 +1,69 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type RunLog struct {
|
||||
NodeName string `json:"node_name"`
|
||||
Err string `json:"err"`
|
||||
Outputs []RunLogOutput `json:"outputs"`
|
||||
}
|
||||
|
||||
type RunLogOutput struct {
|
||||
Time string `json:"time"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type NodeProcessor interface {
|
||||
Run(ctx context.Context) error
|
||||
Log(ctx context.Context) *RunLog
|
||||
AddOutput(ctx context.Context, time, title, content string, err ...string)
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
log *RunLog
|
||||
}
|
||||
|
||||
func NewLogger(node *domain.WorkflowNode) *Logger {
|
||||
return &Logger{
|
||||
log: &RunLog{
|
||||
NodeName: node.Name,
|
||||
Outputs: make([]RunLogOutput, 0),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Log(ctx context.Context) *RunLog {
|
||||
return l.log
|
||||
}
|
||||
|
||||
func (l *Logger) AddOutput(ctx context.Context, time, title, content string, err ...string) {
|
||||
output := RunLogOutput{
|
||||
Time: time,
|
||||
Title: title,
|
||||
Content: content,
|
||||
}
|
||||
if len(err) > 0 {
|
||||
output.Error = err[0]
|
||||
l.log.Err = err[0]
|
||||
}
|
||||
l.log.Outputs = append(l.log.Outputs, output)
|
||||
}
|
||||
|
||||
func GetProcessor(node *domain.WorkflowNode) (NodeProcessor, error) {
|
||||
switch node.Type {
|
||||
case domain.WorkflowNodeTypeStart:
|
||||
return NewStartNode(node), nil
|
||||
case domain.WorkflowNodeTypeCondition:
|
||||
return NewConditionNode(node), nil
|
||||
case domain.WorkflowNodeTypeApply:
|
||||
return NewApplyNode(node), nil
|
||||
}
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
29
internal/workflow/node-processor/start_node.go
Normal file
29
internal/workflow/node-processor/start_node.go
Normal file
@ -0,0 +1,29 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/xtime"
|
||||
)
|
||||
|
||||
type startNode struct {
|
||||
node *domain.WorkflowNode
|
||||
*Logger
|
||||
}
|
||||
|
||||
func NewStartNode(node *domain.WorkflowNode) *startNode {
|
||||
return &startNode{
|
||||
node: node,
|
||||
Logger: NewLogger(node),
|
||||
}
|
||||
}
|
||||
|
||||
// 开始节点没有任何操作
|
||||
func (s *startNode) Run(ctx context.Context) error {
|
||||
s.AddOutput(ctx, xtime.BeijingTimeStr(),
|
||||
s.node.Name,
|
||||
"完成",
|
||||
)
|
||||
return nil
|
||||
}
|
64
internal/workflow/node-processor/workflow_processor.go
Normal file
64
internal/workflow/node-processor/workflow_processor.go
Normal file
@ -0,0 +1,64 @@
|
||||
package nodeprocessor
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
)
|
||||
|
||||
type workflowProcessor struct {
|
||||
workflow *domain.Workflow
|
||||
logs []RunLog
|
||||
}
|
||||
|
||||
func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor {
|
||||
return &workflowProcessor{
|
||||
workflow: workflow,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *workflowProcessor) Run(ctx context.Context) error {
|
||||
ctx = WithWorkflowId(ctx, w.workflow.Id)
|
||||
return w.runNode(ctx, w.workflow.Content)
|
||||
}
|
||||
|
||||
func (w *workflowProcessor) runNode(ctx context.Context, node *domain.WorkflowNode) error {
|
||||
current := node
|
||||
for current != nil {
|
||||
if current.Type == domain.WorkflowNodeTypeBranch {
|
||||
for _, branch := range current.Branches {
|
||||
if err := w.runNode(ctx, &branch); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current.Type != domain.WorkflowNodeTypeBranch {
|
||||
processor, err := GetProcessor(current)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = processor.Run(ctx)
|
||||
|
||||
log := processor.Log(ctx)
|
||||
if log != nil {
|
||||
w.logs = append(w.logs, *log)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func WithWorkflowId(ctx context.Context, id string) context.Context {
|
||||
return context.WithValue(ctx, "workflow_id", id)
|
||||
}
|
||||
|
||||
func GetWorkflowId(ctx context.Context) string {
|
||||
return ctx.Value("workflow_id").(string)
|
||||
}
|
52
internal/workflow/service.go
Normal file
52
internal/workflow/service.go
Normal file
@ -0,0 +1,52 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
nodeprocessor "github.com/usual2970/certimate/internal/workflow/node-processor"
|
||||
)
|
||||
|
||||
type WorkflowRepository interface {
|
||||
Get(ctx context.Context, id string) (*domain.Workflow, error)
|
||||
}
|
||||
|
||||
type WorkflowService struct {
|
||||
repo WorkflowRepository
|
||||
}
|
||||
|
||||
func NewWorkflowService(repo WorkflowRepository) *WorkflowService {
|
||||
return &WorkflowService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WorkflowService) Run(ctx context.Context, req *domain.WorkflowRunReq) error {
|
||||
// 查询
|
||||
if req.Id == "" {
|
||||
return domain.ErrInvalidParams
|
||||
}
|
||||
|
||||
workflow, err := s.repo.Get(ctx, req.Id)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("failed to get workflow", "id", req.Id, "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 执行
|
||||
if !workflow.Enabled {
|
||||
app.GetApp().Logger().Error("workflow is disabled", "id", req.Id)
|
||||
return fmt.Errorf("workflow is disabled")
|
||||
}
|
||||
|
||||
processor := nodeprocessor.NewWorkflowProcessor(workflow)
|
||||
if err := processor.Run(ctx); err != nil {
|
||||
return fmt.Errorf("failed to run workflow: %w", err)
|
||||
}
|
||||
|
||||
// 保存执行日志
|
||||
|
||||
return nil
|
||||
}
|
@ -8,7 +8,8 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
||||
import { Input } from "../ui/input";
|
||||
import { Button } from "../ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { memo, useState } from "react";
|
||||
import { memo, useEffect, useState } from "react";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
||||
type WorkflowNameEditDialogProps = {
|
||||
trigger: React.ReactNode;
|
||||
@ -30,6 +31,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.reset({ name: workflow.name, description: workflow.description });
|
||||
}, [workflow]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
@ -71,6 +76,7 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
||||
<Input
|
||||
placeholder="请输入流程名称"
|
||||
{...field}
|
||||
value={field.value}
|
||||
defaultValue={workflow.name}
|
||||
onChange={(e) => {
|
||||
form.setValue("name", e.target.value);
|
||||
@ -90,9 +96,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
||||
<FormItem>
|
||||
<FormLabel>说明</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
<Textarea
|
||||
placeholder="请输入流程说明"
|
||||
{...field}
|
||||
value={field.value}
|
||||
defaultValue={workflow.description}
|
||||
onChange={(e) => {
|
||||
form.setValue("description", e.target.value);
|
||||
|
@ -155,6 +155,10 @@ export const newWorkflowNode = (type: WorkflowNodeType, options: NewWorkflowNode
|
||||
};
|
||||
}
|
||||
|
||||
if (type == WorkflowNodeType.Condition) {
|
||||
rs.validated = true;
|
||||
}
|
||||
|
||||
if (type === WorkflowNodeType.Branch) {
|
||||
rs = {
|
||||
...rs,
|
||||
@ -350,6 +354,20 @@ export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): bool
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getExecuteMethod = (node: WorkflowNode): { type: string; crontab: string } => {
|
||||
if (node.type === WorkflowNodeType.Start) {
|
||||
return {
|
||||
type: (node.config?.executionMethod as string) ?? "",
|
||||
crontab: (node.config?.crontab as string) ?? "",
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: "",
|
||||
crontab: "",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export type WorkflowBranchNode = {
|
||||
id: string;
|
||||
name: string;
|
||||
@ -428,4 +446,3 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -96,8 +96,8 @@ const WorkflowDetail = () => {
|
||||
<WorkflowBaseInfoEditDialog
|
||||
trigger={
|
||||
<div className="flex flex-col space-y-1 cursor-pointer items-start">
|
||||
<div className="">{workflow.name ? workflow.name : "未命名工作流"}</div>
|
||||
<div className="text-sm text-muted-foreground">{workflow.description ? workflow.description : "添加流程说明"}</div>
|
||||
<div className="truncate max-w-[200px]">{workflow.name ? workflow.name : "未命名工作流"}</div>
|
||||
<div className="text-sm text-muted-foreground truncate max-w-[200px]">{workflow.description ? workflow.description : "添加流程说明"}</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
@ -43,7 +43,7 @@ const Workflow = () => {
|
||||
if (!name) {
|
||||
name = "未命名工作流";
|
||||
}
|
||||
return <div className="flex items-center">{name}</div>;
|
||||
return <div className="max-w-[150px] truncate">{name}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -54,7 +54,7 @@ const Workflow = () => {
|
||||
if (!description) {
|
||||
description = "-";
|
||||
}
|
||||
return description;
|
||||
return <div className="max-w-[200px] truncate">{description}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -211,4 +211,3 @@ const Workflow = () => {
|
||||
};
|
||||
|
||||
export default Workflow;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
addBranch,
|
||||
addNode,
|
||||
getExecuteMethod,
|
||||
getWorkflowOutputBeforeId,
|
||||
initWorkflow,
|
||||
removeBranch,
|
||||
@ -76,11 +77,15 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
});
|
||||
},
|
||||
switchEnable: async () => {
|
||||
const root = get().workflow.draft as WorkflowNode;
|
||||
const executeMethod = getExecuteMethod(root);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
content: get().workflow.draft as WorkflowNode,
|
||||
content: root,
|
||||
enabled: !get().workflow.enabled,
|
||||
hasDraft: false,
|
||||
type: executeMethod.type,
|
||||
crontab: executeMethod.crontab,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
@ -90,15 +95,21 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
content: resp.content,
|
||||
enabled: resp.enabled,
|
||||
hasDraft: false,
|
||||
type: resp.type,
|
||||
crontab: resp.crontab,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
save: async () => {
|
||||
const root = get().workflow.draft as WorkflowNode;
|
||||
const executeMethod = getExecuteMethod(root);
|
||||
const resp = await save({
|
||||
id: (get().workflow.id as string) ?? "",
|
||||
content: get().workflow.draft as WorkflowNode,
|
||||
content: root,
|
||||
hasDraft: false,
|
||||
type: executeMethod.type,
|
||||
crontab: executeMethod.crontab,
|
||||
});
|
||||
set((state: WorkflowState) => {
|
||||
return {
|
||||
@ -107,6 +118,8 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
id: resp.id,
|
||||
content: resp.content,
|
||||
hasDraft: false,
|
||||
type: resp.type,
|
||||
crontab: resp.crontab,
|
||||
},
|
||||
};
|
||||
});
|
||||
@ -205,4 +218,3 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
||||
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||
},
|
||||
}));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user