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
|
package applicant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
@ -170,7 +171,38 @@ func Get(record *models.Record) (Applicant, error) {
|
|||||||
DisableFollowCNAME: applyConfig.DisableFollowCNAME,
|
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:
|
case configTypeAliyun:
|
||||||
return NewAliyun(option), nil
|
return NewAliyun(option), nil
|
||||||
case configTypeTencent:
|
case configTypeTencent:
|
||||||
|
@ -1,5 +1,16 @@
|
|||||||
package domain
|
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 {
|
type AliyunAccess struct {
|
||||||
AccessKeyId string `json:"accessKeyId"`
|
AccessKeyId string `json:"accessKeyId"`
|
||||||
AccessKeySecret string `json:"accessKeySecret"`
|
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
|
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 {
|
type XError struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
|
@ -1,9 +1,22 @@
|
|||||||
package domain
|
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 {
|
type Workflow struct {
|
||||||
Id string `json:"id"`
|
Meta
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -11,8 +24,6 @@ type Workflow struct {
|
|||||||
Draft *WorkflowNode `json:"draft"`
|
Draft *WorkflowNode `json:"draft"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
HasDraft bool `json:"hasDraft"`
|
HasDraft bool `json:"hasDraft"`
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNode struct {
|
type WorkflowNode struct {
|
||||||
@ -29,11 +40,48 @@ type WorkflowNode struct {
|
|||||||
Branches []WorkflowNode `json:"branches"`
|
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 {
|
type WorkflowNodeIo struct {
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Required bool `json:"required"`
|
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 {
|
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 { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { memo, useState } from "react";
|
import { memo, useEffect, useState } from "react";
|
||||||
|
import { Textarea } from "../ui/textarea";
|
||||||
|
|
||||||
type WorkflowNameEditDialogProps = {
|
type WorkflowNameEditDialogProps = {
|
||||||
trigger: React.ReactNode;
|
trigger: React.ReactNode;
|
||||||
@ -30,6 +31,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({ name: workflow.name, description: workflow.description });
|
||||||
|
}, [workflow]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -71,6 +76,7 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
<Input
|
<Input
|
||||||
placeholder="请输入流程名称"
|
placeholder="请输入流程名称"
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value}
|
||||||
defaultValue={workflow.name}
|
defaultValue={workflow.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
form.setValue("name", e.target.value);
|
form.setValue("name", e.target.value);
|
||||||
@ -90,9 +96,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>说明</FormLabel>
|
<FormLabel>说明</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Textarea
|
||||||
placeholder="请输入流程说明"
|
placeholder="请输入流程说明"
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value}
|
||||||
defaultValue={workflow.description}
|
defaultValue={workflow.description}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
form.setValue("description", e.target.value);
|
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) {
|
if (type === WorkflowNodeType.Branch) {
|
||||||
rs = {
|
rs = {
|
||||||
...rs,
|
...rs,
|
||||||
@ -350,6 +354,20 @@ export const allNodesValidated = (node: WorkflowNode | WorkflowBranchNode): bool
|
|||||||
return true;
|
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 = {
|
export type WorkflowBranchNode = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -428,4 +446,3 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -96,8 +96,8 @@ const WorkflowDetail = () => {
|
|||||||
<WorkflowBaseInfoEditDialog
|
<WorkflowBaseInfoEditDialog
|
||||||
trigger={
|
trigger={
|
||||||
<div className="flex flex-col space-y-1 cursor-pointer items-start">
|
<div className="flex flex-col space-y-1 cursor-pointer items-start">
|
||||||
<div className="">{workflow.name ? workflow.name : "未命名工作流"}</div>
|
<div className="truncate max-w-[200px]">{workflow.name ? workflow.name : "未命名工作流"}</div>
|
||||||
<div className="text-sm text-muted-foreground">{workflow.description ? workflow.description : "添加流程说明"}</div>
|
<div className="text-sm text-muted-foreground truncate max-w-[200px]">{workflow.description ? workflow.description : "添加流程说明"}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -43,7 +43,7 @@ const Workflow = () => {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
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) {
|
if (!description) {
|
||||||
description = "-";
|
description = "-";
|
||||||
}
|
}
|
||||||
return description;
|
return <div className="max-w-[200px] truncate">{description}</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -211,4 +211,3 @@ const Workflow = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default Workflow;
|
export default Workflow;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
addBranch,
|
addBranch,
|
||||||
addNode,
|
addNode,
|
||||||
|
getExecuteMethod,
|
||||||
getWorkflowOutputBeforeId,
|
getWorkflowOutputBeforeId,
|
||||||
initWorkflow,
|
initWorkflow,
|
||||||
removeBranch,
|
removeBranch,
|
||||||
@ -76,11 +77,15 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
switchEnable: async () => {
|
switchEnable: async () => {
|
||||||
|
const root = get().workflow.draft as WorkflowNode;
|
||||||
|
const executeMethod = getExecuteMethod(root);
|
||||||
const resp = await save({
|
const resp = await save({
|
||||||
id: (get().workflow.id as string) ?? "",
|
id: (get().workflow.id as string) ?? "",
|
||||||
content: get().workflow.draft as WorkflowNode,
|
content: root,
|
||||||
enabled: !get().workflow.enabled,
|
enabled: !get().workflow.enabled,
|
||||||
hasDraft: false,
|
hasDraft: false,
|
||||||
|
type: executeMethod.type,
|
||||||
|
crontab: executeMethod.crontab,
|
||||||
});
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
@ -90,15 +95,21 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
content: resp.content,
|
content: resp.content,
|
||||||
enabled: resp.enabled,
|
enabled: resp.enabled,
|
||||||
hasDraft: false,
|
hasDraft: false,
|
||||||
|
type: resp.type,
|
||||||
|
crontab: resp.crontab,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
save: async () => {
|
save: async () => {
|
||||||
|
const root = get().workflow.draft as WorkflowNode;
|
||||||
|
const executeMethod = getExecuteMethod(root);
|
||||||
const resp = await save({
|
const resp = await save({
|
||||||
id: (get().workflow.id as string) ?? "",
|
id: (get().workflow.id as string) ?? "",
|
||||||
content: get().workflow.draft as WorkflowNode,
|
content: root,
|
||||||
hasDraft: false,
|
hasDraft: false,
|
||||||
|
type: executeMethod.type,
|
||||||
|
crontab: executeMethod.crontab,
|
||||||
});
|
});
|
||||||
set((state: WorkflowState) => {
|
set((state: WorkflowState) => {
|
||||||
return {
|
return {
|
||||||
@ -107,6 +118,8 @@ export const useWorkflowStore = create<WorkflowState>((set, get) => ({
|
|||||||
id: resp.id,
|
id: resp.id,
|
||||||
content: resp.content,
|
content: resp.content,
|
||||||
hasDraft: false,
|
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);
|
return getWorkflowOutputBeforeId(get().workflow.draft as WorkflowNode, id, type);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user