mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
commit
62e2ed2fb8
@ -112,6 +112,7 @@ type WorkflowNodeConfigForNotify struct {
|
|||||||
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
|
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
|
||||||
Subject string `json:"subject"` // 通知主题
|
Subject string `json:"subject"` // 通知主题
|
||||||
Message string `json:"message"` // 通知内容
|
Message string `json:"message"` // 通知内容
|
||||||
|
SkipOnAllPrevSkipped bool `json:"skipOnAllPrevSkipped"` // 前序节点均已跳过时是否跳过
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowNodeConfigForCondition struct {
|
type WorkflowNodeConfigForCondition struct {
|
||||||
@ -128,7 +129,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
|
|||||||
CAProvider: maputil.GetString(n.Config, "caProvider"),
|
CAProvider: maputil.GetString(n.Config, "caProvider"),
|
||||||
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
|
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
|
||||||
CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"),
|
CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"),
|
||||||
KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"),
|
KeyAlgorithm: maputil.GetOrDefaultString(n.Config, "keyAlgorithm", string(CertificateKeyAlgorithmTypeRSA2048)),
|
||||||
Nameservers: maputil.GetString(n.Config, "nameservers"),
|
Nameservers: maputil.GetString(n.Config, "nameservers"),
|
||||||
DnsPropagationWait: maputil.GetInt32(n.Config, "dnsPropagationWait"),
|
DnsPropagationWait: maputil.GetInt32(n.Config, "dnsPropagationWait"),
|
||||||
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
|
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
|
||||||
@ -175,6 +176,7 @@ func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
|
|||||||
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
|
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
|
||||||
Subject: maputil.GetString(n.Config, "subject"),
|
Subject: maputil.GetString(n.Config, "subject"),
|
||||||
Message: maputil.GetString(n.Config, "message"),
|
Message: maputil.GetString(n.Config, "message"),
|
||||||
|
SkipOnAllPrevSkipped: maputil.GetBool(n.Config, "skipOnAllPrevSkipped"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +285,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c
|
|||||||
ClientToken: generateClientToken(),
|
ClientToken: generateClientToken(),
|
||||||
ListenerPort: uint16(cloudHttpsListenerPort),
|
ListenerPort: uint16(cloudHttpsListenerPort),
|
||||||
Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler,
|
Scheduler: describeAppHTTPSListenersResp.ListenerList[0].Scheduler,
|
||||||
|
CertIds: describeAppHTTPSListenersResp.ListenerList[0].CertIds,
|
||||||
AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel {
|
AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel {
|
||||||
if domain.Host == d.config.Domain {
|
if domain.Host == d.config.Domain {
|
||||||
return bceappblb.AdditionalCertDomainsModel{
|
return bceappblb.AdditionalCertDomainsModel{
|
||||||
|
@ -283,6 +283,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c
|
|||||||
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
|
updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{
|
||||||
ClientToken: generateClientToken(),
|
ClientToken: generateClientToken(),
|
||||||
ListenerPort: uint16(cloudHttpsListenerPort),
|
ListenerPort: uint16(cloudHttpsListenerPort),
|
||||||
|
CertIds: describeHTTPSListenersResp.ListenerList[0].CertIds,
|
||||||
AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel {
|
AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel {
|
||||||
if domain.Host == d.config.Domain {
|
if domain.Host == d.config.Domain {
|
||||||
return bceblb.AdditionalCertDomainsModel{
|
return bceblb.AdditionalCertDomainsModel{
|
||||||
|
@ -6,11 +6,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||||
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate"
|
uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate"
|
||||||
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn"
|
wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn"
|
||||||
|
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeployerConfig struct {
|
type DeployerConfig struct {
|
||||||
@ -18,7 +20,7 @@ type DeployerConfig struct {
|
|||||||
AccessKeyId string `json:"accessKeyId"`
|
AccessKeyId string `json:"accessKeyId"`
|
||||||
// 网宿云 AccessKeySecret。
|
// 网宿云 AccessKeySecret。
|
||||||
AccessKeySecret string `json:"accessKeySecret"`
|
AccessKeySecret string `json:"accessKeySecret"`
|
||||||
// 加速域名数组。
|
// 加速域名数组(支持泛域名)。
|
||||||
Domains []string `json:"domains"`
|
Domains []string `json:"domains"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +82,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
|
|||||||
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
|
certId, _ := strconv.ParseInt(upres.CertId, 10, 64)
|
||||||
batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{
|
batchUpdateCertificateConfigReq := &wangsusdk.BatchUpdateCertificateConfigRequest{
|
||||||
CertificateId: certId,
|
CertificateId: certId,
|
||||||
DomainNames: d.config.Domains,
|
DomainNames: sliceutil.Map(d.config.Domains, func(domain string) string {
|
||||||
|
// "*.example.com" → ".example.com",适配网宿云 CDN 要求的泛域名格式
|
||||||
|
return strings.TrimPrefix(domain, "*")
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfig(batchUpdateCertificateConfigReq)
|
batchUpdateCertificateConfigResp, err := d.sdkClient.BatchUpdateCertificateConfig(batchUpdateCertificateConfigReq)
|
||||||
d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp))
|
d.logger.Debug("sdk request 'cdn.BatchUpdateCertificateConfig'", slog.Any("request", batchUpdateCertificateConfigReq), slog.Any("response", batchUpdateCertificateConfigResp))
|
||||||
|
@ -77,25 +77,6 @@ func (r *CertificateRepository) GetByWorkflowNodeId(ctx context.Context, workflo
|
|||||||
return r.castRecordToModel(records[0])
|
return r.castRecordToModel(records[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CertificateRepository) GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error) {
|
|
||||||
records, err := app.GetApp().FindRecordsByFilter(
|
|
||||||
domain.CollectionNameCertificate,
|
|
||||||
"workflowRunId={:workflowRunId} && deleted=null",
|
|
||||||
"-created",
|
|
||||||
1, 0,
|
|
||||||
dbx.Params{"workflowRunId": workflowRunId},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(records) == 0 {
|
|
||||||
return nil, domain.ErrRecordNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.castRecordToModel(records[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) {
|
func (r *CertificateRepository) Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error) {
|
||||||
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate)
|
collection, err := app.GetApp().FindCollectionByNameOrId(domain.CollectionNameCertificate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -112,6 +112,7 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow
|
|||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 优化可读性
|
// TODO: 优化可读性
|
||||||
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
|
if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition {
|
||||||
current = nil
|
current = nil
|
||||||
|
@ -3,6 +3,7 @@ package nodeprocessor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -35,7 +36,8 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *applyNode) Process(ctx context.Context) error {
|
func (n *applyNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to obtain certificiate ...")
|
nodeCfg := n.node.GetConfigForApply()
|
||||||
|
n.logger.Info("ready to obtain certificiate ...", slog.Any("config", nodeCfg))
|
||||||
|
|
||||||
// 查询上次执行结果
|
// 查询上次执行结果
|
||||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||||
@ -45,6 +47,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
|
|
||||||
// 检测是否可以跳过本次执行
|
// 检测是否可以跳过本次执行
|
||||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||||
|
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
|
||||||
n.logger.Info(fmt.Sprintf("skip this application, because %s", reason))
|
n.logger.Info(fmt.Sprintf("skip this application, because %s", reason))
|
||||||
return nil
|
return nil
|
||||||
} else if reason != "" {
|
} else if reason != "" {
|
||||||
@ -101,8 +104,8 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 保存 ARI 记录
|
// 保存 ARI 记录
|
||||||
if applyResult.ARIReplaced {
|
if applyResult.ARIReplaced && lastOutput != nil {
|
||||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, lastOutput.NodeId)
|
||||||
if lastCertificate != nil {
|
if lastCertificate != nil {
|
||||||
lastCertificate.ACMERenewed = true
|
lastCertificate.ACMERenewed = true
|
||||||
n.certRepo.Save(ctx, lastCertificate)
|
n.certRepo.Save(ctx, lastCertificate)
|
||||||
@ -110,6 +113,7 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 记录中间结果
|
// 记录中间结果
|
||||||
|
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
|
||||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
||||||
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
|
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
|
||||||
|
|
||||||
@ -120,39 +124,40 @@ func (n *applyNode) Process(ctx context.Context) error {
|
|||||||
func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
||||||
if lastOutput != nil && lastOutput.Succeeded {
|
if lastOutput != nil && lastOutput.Succeeded {
|
||||||
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
|
// 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致
|
||||||
currentNodeConfig := n.node.GetConfigForApply()
|
thisNodeCfg := n.node.GetConfigForApply()
|
||||||
lastNodeConfig := lastOutput.Node.GetConfigForApply()
|
lastNodeCfg := lastOutput.Node.GetConfigForApply()
|
||||||
if currentNodeConfig.Domains != lastNodeConfig.Domains {
|
|
||||||
|
if thisNodeCfg.Domains != lastNodeCfg.Domains {
|
||||||
return false, "the configuration item 'Domains' changed"
|
return false, "the configuration item 'Domains' changed"
|
||||||
}
|
}
|
||||||
if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail {
|
if thisNodeCfg.ContactEmail != lastNodeCfg.ContactEmail {
|
||||||
return false, "the configuration item 'ContactEmail' changed"
|
return false, "the configuration item 'ContactEmail' changed"
|
||||||
}
|
}
|
||||||
if currentNodeConfig.Provider != lastNodeConfig.Provider {
|
if thisNodeCfg.Provider != lastNodeCfg.Provider {
|
||||||
return false, "the configuration item 'Provider' changed"
|
return false, "the configuration item 'Provider' changed"
|
||||||
}
|
}
|
||||||
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
|
if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
|
||||||
return false, "the configuration item 'ProviderAccessId' changed"
|
return false, "the configuration item 'ProviderAccessId' changed"
|
||||||
}
|
}
|
||||||
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) {
|
if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
|
||||||
return false, "the configuration item 'ProviderConfig' changed"
|
return false, "the configuration item 'ProviderConfig' changed"
|
||||||
}
|
}
|
||||||
if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider {
|
if thisNodeCfg.CAProvider != lastNodeCfg.CAProvider {
|
||||||
return false, "the configuration item 'CAProvider' changed"
|
return false, "the configuration item 'CAProvider' changed"
|
||||||
}
|
}
|
||||||
if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId {
|
if thisNodeCfg.CAProviderAccessId != lastNodeCfg.CAProviderAccessId {
|
||||||
return false, "the configuration item 'CAProviderAccessId' changed"
|
return false, "the configuration item 'CAProviderAccessId' changed"
|
||||||
}
|
}
|
||||||
if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) {
|
if !maps.Equal(thisNodeCfg.CAProviderConfig, lastNodeCfg.CAProviderConfig) {
|
||||||
return false, "the configuration item 'CAProviderConfig' changed"
|
return false, "the configuration item 'CAProviderConfig' changed"
|
||||||
}
|
}
|
||||||
if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm {
|
if thisNodeCfg.KeyAlgorithm != lastNodeCfg.KeyAlgorithm {
|
||||||
return false, "the configuration item 'KeyAlgorithm' changed"
|
return false, "the configuration item 'KeyAlgorithm' changed"
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, lastOutput.NodeId)
|
||||||
if lastCertificate != nil {
|
if lastCertificate != nil {
|
||||||
renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24
|
renewalInterval := time.Duration(thisNodeCfg.SkipBeforeExpiryDays) * time.Hour * 24
|
||||||
expirationTime := time.Until(lastCertificate.ExpireAt)
|
expirationTime := time.Until(lastCertificate.ExpireAt)
|
||||||
if expirationTime > renewalInterval {
|
if expirationTime > renewalInterval {
|
||||||
daysLeft := int(expirationTime.Hours() / 24)
|
daysLeft := int(expirationTime.Hours() / 24)
|
||||||
@ -160,7 +165,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
|
|||||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
||||||
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10)
|
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10)
|
||||||
|
|
||||||
return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, currentNodeConfig.SkipBeforeExpiryDays)
|
return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", daysLeft, thisNodeCfg.SkipBeforeExpiryDays)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,6 @@ func (n *conditionNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) {
|
func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) {
|
||||||
variables := GetNodeOutputs(ctx)
|
variables := GetAllNodeOutputs(ctx)
|
||||||
return expression.Eval(variables)
|
return expression.Eval(variables)
|
||||||
}
|
}
|
||||||
|
@ -3,4 +3,5 @@ package nodeprocessor
|
|||||||
const (
|
const (
|
||||||
outputKeyForCertificateValidity = "certificate.validity"
|
outputKeyForCertificateValidity = "certificate.validity"
|
||||||
outputKeyForCertificateDaysLeft = "certificate.daysLeft"
|
outputKeyForCertificateDaysLeft = "certificate.daysLeft"
|
||||||
|
outputKeyForNodeSkipped = "node.skipped"
|
||||||
)
|
)
|
||||||
|
@ -25,6 +25,15 @@ func newNodeOutputsContainer() *nodeOutputsContainer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取节点输出容器
|
||||||
|
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
|
||||||
|
value := ctx.Value(nodeOutputsKey)
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value.(*nodeOutputsContainer)
|
||||||
|
}
|
||||||
|
|
||||||
// 添加节点输出到上下文
|
// 添加节点输出到上下文
|
||||||
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
|
func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context {
|
||||||
container := getNodeOutputsContainer(ctx)
|
container := getNodeOutputsContainer(ctx)
|
||||||
@ -50,7 +59,7 @@ func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) co
|
|||||||
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
||||||
container := getNodeOutputsContainer(ctx)
|
container := getNodeOutputsContainer(ctx)
|
||||||
if container == nil {
|
if container == nil {
|
||||||
return nil
|
container = newNodeOutputsContainer()
|
||||||
}
|
}
|
||||||
|
|
||||||
container.RLock()
|
container.RLock()
|
||||||
@ -69,22 +78,11 @@ func GetNodeOutput(ctx context.Context, nodeId string) map[string]any {
|
|||||||
return outputCopy
|
return outputCopy
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取特定节点的特定输出项
|
|
||||||
func GetNodeOutputValue(ctx context.Context, nodeId string, key string) (any, bool) {
|
|
||||||
output := GetNodeOutput(ctx, nodeId)
|
|
||||||
if output == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
value, exists := output[key]
|
|
||||||
return value, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有节点输出
|
// 获取所有节点输出
|
||||||
func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
|
func GetAllNodeOutputs(ctx context.Context) map[string]map[string]any {
|
||||||
container := getNodeOutputsContainer(ctx)
|
container := getNodeOutputsContainer(ctx)
|
||||||
if container == nil {
|
if container == nil {
|
||||||
return nil
|
container = newNodeOutputsContainer()
|
||||||
}
|
}
|
||||||
|
|
||||||
container.RLock()
|
container.RLock()
|
||||||
@ -103,26 +101,3 @@ func GetNodeOutputs(ctx context.Context) map[string]map[string]any {
|
|||||||
|
|
||||||
return allOutputs
|
return allOutputs
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取节点输出容器
|
|
||||||
func getNodeOutputsContainer(ctx context.Context) *nodeOutputsContainer {
|
|
||||||
value := ctx.Value(nodeOutputsKey)
|
|
||||||
if value == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return value.(*nodeOutputsContainer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查节点是否有输出
|
|
||||||
func HasNodeOutput(ctx context.Context, nodeId string) bool {
|
|
||||||
container := getNodeOutputsContainer(ctx)
|
|
||||||
if container == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
container.RLock()
|
|
||||||
defer container.RUnlock()
|
|
||||||
|
|
||||||
_, exists := container.outputs[nodeId]
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/deployer"
|
"github.com/usual2970/certimate/internal/deployer"
|
||||||
@ -33,7 +34,8 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *deployNode) Process(ctx context.Context) error {
|
func (n *deployNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to deploy certificate ...")
|
nodeCfg := n.node.GetConfigForDeploy()
|
||||||
|
n.logger.Info("ready to deploy certificate ...", slog.Any("config", nodeCfg))
|
||||||
|
|
||||||
// 查询上次执行结果
|
// 查询上次执行结果
|
||||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||||
@ -58,6 +60,7 @@ func (n *deployNode) Process(ctx context.Context) error {
|
|||||||
// 检测是否可以跳过本次执行
|
// 检测是否可以跳过本次执行
|
||||||
if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
|
if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
|
||||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||||
|
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
|
||||||
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason))
|
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason))
|
||||||
return nil
|
return nil
|
||||||
} else if reason != "" {
|
} else if reason != "" {
|
||||||
@ -96,6 +99,9 @@ func (n *deployNode) Process(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 记录中间结果
|
||||||
|
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
|
||||||
|
|
||||||
n.logger.Info("deployment completed")
|
n.logger.Info("deployment completed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -103,16 +109,17 @@ func (n *deployNode) Process(ctx context.Context) error {
|
|||||||
func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
func (n *deployNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
||||||
if lastOutput != nil && lastOutput.Succeeded {
|
if lastOutput != nil && lastOutput.Succeeded {
|
||||||
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
|
// 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致
|
||||||
currentNodeConfig := n.node.GetConfigForDeploy()
|
thisNodeCfg := n.node.GetConfigForDeploy()
|
||||||
lastNodeConfig := lastOutput.Node.GetConfigForDeploy()
|
lastNodeCfg := lastOutput.Node.GetConfigForDeploy()
|
||||||
if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId {
|
|
||||||
|
if thisNodeCfg.ProviderAccessId != lastNodeCfg.ProviderAccessId {
|
||||||
return false, "the configuration item 'ProviderAccessId' changed"
|
return false, "the configuration item 'ProviderAccessId' changed"
|
||||||
}
|
}
|
||||||
if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) {
|
if !maps.Equal(thisNodeCfg.ProviderConfig, lastNodeCfg.ProviderConfig) {
|
||||||
return false, "the configuration item 'ProviderConfig' changed"
|
return false, "the configuration item 'ProviderConfig' changed"
|
||||||
}
|
}
|
||||||
|
|
||||||
if currentNodeConfig.SkipOnLastSucceeded {
|
if thisNodeCfg.SkipOnLastSucceeded {
|
||||||
return true, "the certificate has already been deployed"
|
return true, "the certificate has already been deployed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,9 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"math"
|
"math"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -30,13 +32,12 @@ func NewMonitorNode(node *domain.WorkflowNode) *monitorNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *monitorNode) Process(ctx context.Context) error {
|
func (n *monitorNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to monitor certificate ...")
|
|
||||||
|
|
||||||
nodeCfg := n.node.GetConfigForMonitor()
|
nodeCfg := n.node.GetConfigForMonitor()
|
||||||
|
n.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg))
|
||||||
|
|
||||||
targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port)
|
targetAddr := net.JoinHostPort(nodeCfg.Host, fmt.Sprintf("%d", nodeCfg.Port))
|
||||||
if nodeCfg.Port == 0 {
|
if nodeCfg.Port == 0 {
|
||||||
targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host)
|
targetAddr = net.JoinHostPort(nodeCfg.Host, "443")
|
||||||
}
|
}
|
||||||
|
|
||||||
targetDomain := nodeCfg.Domain
|
targetDomain := nodeCfg.Domain
|
||||||
|
@ -2,7 +2,9 @@ package nodeprocessor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/usual2970/certimate/internal/domain"
|
"github.com/usual2970/certimate/internal/domain"
|
||||||
"github.com/usual2970/certimate/internal/notify"
|
"github.com/usual2970/certimate/internal/notify"
|
||||||
@ -28,9 +30,8 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *notifyNode) Process(ctx context.Context) error {
|
func (n *notifyNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to send notification ...")
|
|
||||||
|
|
||||||
nodeCfg := n.node.GetConfigForNotify()
|
nodeCfg := n.node.GetConfigForNotify()
|
||||||
|
n.logger.Info("ready to send notification ...", slog.Any("config", nodeCfg))
|
||||||
|
|
||||||
if nodeCfg.Provider == "" {
|
if nodeCfg.Provider == "" {
|
||||||
// Deprecated: v0.4.x 将废弃
|
// Deprecated: v0.4.x 将废弃
|
||||||
@ -59,6 +60,12 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测是否可以跳过本次执行
|
||||||
|
if skippable := n.checkCanSkip(ctx); skippable {
|
||||||
|
n.logger.Info(fmt.Sprintf("skip this notification, because all the previous nodes have been skipped"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化通知器
|
// 初始化通知器
|
||||||
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
|
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
|
||||||
Node: n.node,
|
Node: n.node,
|
||||||
@ -80,3 +87,21 @@ func (n *notifyNode) Process(ctx context.Context) error {
|
|||||||
n.logger.Info("notification completed")
|
n.logger.Info("notification completed")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *notifyNode) checkCanSkip(ctx context.Context) (_skip bool) {
|
||||||
|
thisNodeCfg := n.node.GetConfigForNotify()
|
||||||
|
if !thisNodeCfg.SkipOnAllPrevSkipped {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
prevNodeOutputs := GetAllNodeOutputs(ctx)
|
||||||
|
for _, nodeOutput := range prevNodeOutputs {
|
||||||
|
if nodeOutput[outputKeyForNodeSkipped] != nil {
|
||||||
|
if nodeOutput[outputKeyForNodeSkipped].(string) != strconv.FormatBool(true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -50,7 +50,6 @@ func (n *nodeOutputer) GetOutputs() map[string]any {
|
|||||||
|
|
||||||
type certificateRepository interface {
|
type certificateRepository interface {
|
||||||
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error)
|
||||||
GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error)
|
|
||||||
Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error)
|
Save(ctx context.Context, certificate *domain.Certificate) (*domain.Certificate, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ package nodeprocessor
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -32,9 +33,8 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (n *uploadNode) Process(ctx context.Context) error {
|
func (n *uploadNode) Process(ctx context.Context) error {
|
||||||
n.logger.Info("ready to upload certiticate ...")
|
|
||||||
|
|
||||||
nodeCfg := n.node.GetConfigForUpload()
|
nodeCfg := n.node.GetConfigForUpload()
|
||||||
|
n.logger.Info("ready to upload certiticate ...", slog.Any("config", nodeCfg))
|
||||||
|
|
||||||
// 查询上次执行结果
|
// 查询上次执行结果
|
||||||
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
|
||||||
@ -44,6 +44,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
|||||||
|
|
||||||
// 检测是否可以跳过本次执行
|
// 检测是否可以跳过本次执行
|
||||||
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
|
||||||
|
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(true)
|
||||||
n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))
|
n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))
|
||||||
return nil
|
return nil
|
||||||
} else if reason != "" {
|
} else if reason != "" {
|
||||||
@ -71,6 +72,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 记录中间结果
|
// 记录中间结果
|
||||||
|
n.outputs[outputKeyForNodeSkipped] = strconv.FormatBool(false)
|
||||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true)
|
||||||
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
|
n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10)
|
||||||
|
|
||||||
@ -81,16 +83,17 @@ func (n *uploadNode) Process(ctx context.Context) error {
|
|||||||
func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.WorkflowOutput) (_skip bool, _reason string) {
|
||||||
if lastOutput != nil && lastOutput.Succeeded {
|
if lastOutput != nil && lastOutput.Succeeded {
|
||||||
// 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致
|
// 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致
|
||||||
currentNodeConfig := n.node.GetConfigForUpload()
|
thisNodeCfg := n.node.GetConfigForUpload()
|
||||||
lastNodeConfig := lastOutput.Node.GetConfigForUpload()
|
lastNodeCfg := lastOutput.Node.GetConfigForUpload()
|
||||||
if strings.TrimSpace(currentNodeConfig.Certificate) != strings.TrimSpace(lastNodeConfig.Certificate) {
|
|
||||||
|
if strings.TrimSpace(thisNodeCfg.Certificate) != strings.TrimSpace(lastNodeCfg.Certificate) {
|
||||||
return false, "the configuration item 'Certificate' changed"
|
return false, "the configuration item 'Certificate' changed"
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(currentNodeConfig.PrivateKey) != strings.TrimSpace(lastNodeConfig.PrivateKey) {
|
if strings.TrimSpace(thisNodeCfg.PrivateKey) != strings.TrimSpace(lastNodeCfg.PrivateKey) {
|
||||||
return false, "the configuration item 'PrivateKey' changed"
|
return false, "the configuration item 'PrivateKey' changed"
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId)
|
lastCertificate, _ := n.certRepo.GetByWorkflowNodeId(ctx, lastOutput.NodeId)
|
||||||
if lastCertificate != nil {
|
if lastCertificate != nil {
|
||||||
daysLeft := int(time.Until(lastCertificate.ExpireAt).Hours() / 24)
|
daysLeft := int(time.Until(lastCertificate.ExpireAt).Hours() / 24)
|
||||||
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0)
|
n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0)
|
||||||
|
122
ui/src/components/workflow/WorkflowEditModal.tsx
Normal file
122
ui/src/components/workflow/WorkflowEditModal.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useControllableValue } from "ahooks";
|
||||||
|
import { Modal, notification } from "antd";
|
||||||
|
|
||||||
|
import ModalForm from "@/components/ModalForm";
|
||||||
|
import { useTriggerElement, useZustandShallowSelector } from "@/hooks";
|
||||||
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
|
import WorkflowForm, { type WorkflowFormInstance, type WorkflowFormProps } from "./WorkflowForm";
|
||||||
|
|
||||||
|
export type WorkflowEditModalProps = {
|
||||||
|
data?: WorkflowFormProps["initialValues"];
|
||||||
|
loading?: boolean;
|
||||||
|
open?: boolean;
|
||||||
|
usage?: WorkflowFormProps["usage"];
|
||||||
|
scene: WorkflowFormProps["scene"];
|
||||||
|
trigger?: React.ReactNode;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
afterSubmit?: (record: WorkflowModel) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkflowEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: WorkflowEditModalProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||||
|
|
||||||
|
const { createWorkflow, updateWorkflow } = useWorkflowesStore(useZustandShallowSelector(["createWorkflow", "updateWorkflow"]));
|
||||||
|
|
||||||
|
const [open, setOpen] = useControllableValue<boolean>(props, {
|
||||||
|
valuePropName: "open",
|
||||||
|
defaultValuePropName: "defaultOpen",
|
||||||
|
trigger: "onOpenChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) });
|
||||||
|
|
||||||
|
const formRef = useRef<WorkflowFormInstance>(null);
|
||||||
|
const [formPending, setFormPending] = useState(false);
|
||||||
|
|
||||||
|
const handleOkClick = async () => {
|
||||||
|
setFormPending(true);
|
||||||
|
try {
|
||||||
|
await formRef.current!.validateFields();
|
||||||
|
} catch (err) {
|
||||||
|
setFormPending(false);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let values: WorkflowModel = formRef.current!.getFieldsValue();
|
||||||
|
|
||||||
|
if (scene === "add") {
|
||||||
|
if (data?.id) {
|
||||||
|
throw "Invalid props: `data`";
|
||||||
|
}
|
||||||
|
|
||||||
|
values = await createWorkflow(values);
|
||||||
|
} else if (scene === "edit") {
|
||||||
|
if (!data?.id) {
|
||||||
|
throw "Invalid props: `data`";
|
||||||
|
}
|
||||||
|
|
||||||
|
values = await updateWorkflow({ ...data, ...values });
|
||||||
|
} else {
|
||||||
|
throw "Invalid props: `preset`";
|
||||||
|
}
|
||||||
|
|
||||||
|
afterSubmit?.(values);
|
||||||
|
setOpen(false);
|
||||||
|
} catch (err) {
|
||||||
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setFormPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelClick = () => {
|
||||||
|
if (formPending) return;
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{NotificationContextHolder}
|
||||||
|
|
||||||
|
{triggerEl}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
styles={{
|
||||||
|
content: {
|
||||||
|
maxHeight: "calc(80vh - 64px)",
|
||||||
|
overflowX: "hidden",
|
||||||
|
overflowY: "auto",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
afterClose={() => setOpen(false)}
|
||||||
|
cancelButtonProps={{ disabled: formPending }}
|
||||||
|
cancelText={t("common.button.cancel")}
|
||||||
|
closable
|
||||||
|
confirmLoading={formPending}
|
||||||
|
destroyOnHidden
|
||||||
|
loading={loading}
|
||||||
|
okText={scene === "edit" ? t("common.button.save") : t("common.button.submit")}
|
||||||
|
open={open}
|
||||||
|
title={t(`access.action.${scene}`)}
|
||||||
|
width={480}
|
||||||
|
onOk={handleOkClick}
|
||||||
|
onCancel={handleCancelClick}
|
||||||
|
>
|
||||||
|
<div className="pb-2 pt-4">
|
||||||
|
<WorkflowForm ref={formRef} initialValues={data} scene={scene === "add" ? "add" : "edit"} usage={usage} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WorkflowEditModal;
|
@ -28,7 +28,7 @@ import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSele
|
|||||||
import CAProviderSelect from "@/components/provider/CAProviderSelect";
|
import CAProviderSelect from "@/components/provider/CAProviderSelect";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } from "@/domain/provider";
|
import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } from "@/domain/provider";
|
||||||
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
|
import { type WorkflowNodeConfigForApply, defaultNodeConfigForApply } from "@/domain/workflow";
|
||||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||||
import { useAccessesStore } from "@/stores/access";
|
import { useAccessesStore } from "@/stores/access";
|
||||||
import { useContactEmailsStore } from "@/stores/contact";
|
import { useContactEmailsStore } from "@/stores/contact";
|
||||||
@ -59,11 +59,7 @@ export type ApplyNodeConfigFormInstance = {
|
|||||||
const MULTIPLE_INPUT_SEPARATOR = ";";
|
const MULTIPLE_INPUT_SEPARATOR = ";";
|
||||||
|
|
||||||
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
const initFormModel = (): ApplyNodeConfigFormFieldValues => {
|
||||||
return {
|
return defaultNodeConfigForApply();
|
||||||
challengeType: "dns-01",
|
|
||||||
keyAlgorithm: "RSA2048",
|
|
||||||
skipBeforeExpiryDays: 30,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeConfigFormProps>(
|
const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeConfigFormProps>(
|
||||||
|
@ -4,7 +4,7 @@ import { Form, type FormInstance } from "antd";
|
|||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow";
|
import { type Expr, type WorkflowNodeConfigForCondition, defaultNodeConfigForCondition } from "@/domain/workflow";
|
||||||
import { useAntdForm } from "@/hooks";
|
import { useAntdForm } from "@/hooks";
|
||||||
|
|
||||||
import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor";
|
import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor";
|
||||||
@ -29,7 +29,7 @@ export type ConditionNodeConfigFormInstance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initFormModel = (): ConditionNodeConfigFormFieldValues => {
|
const initFormModel = (): ConditionNodeConfigFormFieldValues => {
|
||||||
return {};
|
return defaultNodeConfigForCondition();
|
||||||
};
|
};
|
||||||
|
|
||||||
const ConditionNodeConfigForm = forwardRef<ConditionNodeConfigFormInstance, ConditionNodeConfigFormProps>(
|
const ConditionNodeConfigForm = forwardRef<ConditionNodeConfigFormInstance, ConditionNodeConfigFormProps>(
|
||||||
|
@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi
|
|||||||
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider";
|
import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider";
|
||||||
import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow";
|
import { type WorkflowNodeConfigForDeploy, WorkflowNodeType, defaultNodeConfigForDeploy } from "@/domain/workflow";
|
||||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||||
import { useWorkflowStore } from "@/stores/workflow";
|
import { useWorkflowStore } from "@/stores/workflow";
|
||||||
|
|
||||||
@ -117,9 +117,7 @@ export type DeployNodeConfigFormInstance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initFormModel = (): DeployNodeConfigFormFieldValues => {
|
const initFormModel = (): DeployNodeConfigFormFieldValues => {
|
||||||
return {
|
return defaultNodeConfigForDeploy();
|
||||||
skipOnLastSucceeded: true,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNodeConfigFormProps>(
|
const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNodeConfigFormProps>(
|
||||||
|
@ -43,7 +43,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({
|
|||||||
if (!v) return false;
|
if (!v) return false;
|
||||||
return String(v)
|
return String(v)
|
||||||
.split(MULTIPLE_INPUT_SEPARATOR)
|
.split(MULTIPLE_INPUT_SEPARATOR)
|
||||||
.every((e) => validDomainName(e));
|
.every((e) => validDomainName(e, { allowWildcard: true }));
|
||||||
}, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")),
|
}, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")),
|
||||||
});
|
});
|
||||||
const formRule = createSchemaFieldRule(formSchema);
|
const formRule = createSchemaFieldRule(formSchema);
|
||||||
|
@ -4,7 +4,7 @@ import { Alert, Form, type FormInstance, Input, InputNumber } from "antd";
|
|||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { type WorkflowNodeConfigForMonitor } from "@/domain/workflow";
|
import { type WorkflowNodeConfigForMonitor, defaultNodeConfigForMonitor } from "@/domain/workflow";
|
||||||
import { useAntdForm } from "@/hooks";
|
import { useAntdForm } from "@/hooks";
|
||||||
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
|
||||||
|
|
||||||
@ -25,11 +25,7 @@ export type MonitorNodeConfigFormInstance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initFormModel = (): MonitorNodeConfigFormFieldValues => {
|
const initFormModel = (): MonitorNodeConfigFormFieldValues => {
|
||||||
return {
|
return defaultNodeConfigForMonitor();
|
||||||
host: "",
|
|
||||||
port: 443,
|
|
||||||
requestPath: "/",
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const MonitorNodeConfigForm = forwardRef<MonitorNodeConfigFormInstance, MonitorNodeConfigFormProps>(
|
const MonitorNodeConfigForm = forwardRef<MonitorNodeConfigFormInstance, MonitorNodeConfigFormProps>(
|
||||||
|
@ -2,7 +2,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } f
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { PlusOutlined as PlusOutlinedIcon, RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
|
import { PlusOutlined as PlusOutlinedIcon, RightOutlined as RightOutlinedIcon } from "@ant-design/icons";
|
||||||
import { Button, Divider, Form, type FormInstance, Input, Select, Typography } from "antd";
|
import { Button, Divider, Flex, Form, type FormInstance, Input, Select, Switch, Typography } from "antd";
|
||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ import NotificationProviderSelect from "@/components/provider/NotificationProvid
|
|||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider";
|
import { ACCESS_USAGES, NOTIFICATION_PROVIDERS, accessProvidersMap, notificationProvidersMap } from "@/domain/provider";
|
||||||
import { notifyChannelsMap } from "@/domain/settings";
|
import { notifyChannelsMap } from "@/domain/settings";
|
||||||
import { type WorkflowNodeConfigForNotify } from "@/domain/workflow";
|
import { type WorkflowNodeConfigForNotify, defaultNodeConfigForNotify } from "@/domain/workflow";
|
||||||
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
|
||||||
import { useAccessesStore } from "@/stores/access";
|
import { useAccessesStore } from "@/stores/access";
|
||||||
import { useNotifyChannelsStore } from "@/stores/notify";
|
import { useNotifyChannelsStore } from "@/stores/notify";
|
||||||
@ -41,7 +41,7 @@ export type NotifyNodeConfigFormInstance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initFormModel = (): NotifyNodeConfigFormFieldValues => {
|
const initFormModel = (): NotifyNodeConfigFormFieldValues => {
|
||||||
return {};
|
return defaultNodeConfigForNotify();
|
||||||
};
|
};
|
||||||
|
|
||||||
const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNodeConfigFormProps>(
|
const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNodeConfigFormProps>(
|
||||||
@ -74,6 +74,7 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
|||||||
.string({ message: t("workflow_node.notify.form.provider_access.placeholder") })
|
.string({ message: t("workflow_node.notify.form.provider_access.placeholder") })
|
||||||
.nonempty(t("workflow_node.notify.form.provider_access.placeholder")),
|
.nonempty(t("workflow_node.notify.form.provider_access.placeholder")),
|
||||||
providerConfig: z.any().nullish(),
|
providerConfig: z.any().nullish(),
|
||||||
|
skipOnAllPrevSkipped: z.boolean().nullish(),
|
||||||
});
|
});
|
||||||
const formRule = createSchemaFieldRule(formSchema);
|
const formRule = createSchemaFieldRule(formSchema);
|
||||||
const { form: formInst, formProps } = useAntdForm({
|
const { form: formInst, formProps } = useAntdForm({
|
||||||
@ -281,6 +282,27 @@ const NotifyNodeConfigForm = forwardRef<NotifyNodeConfigFormInstance, NotifyNode
|
|||||||
|
|
||||||
{nestedFormEl}
|
{nestedFormEl}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
<Divider size="small">
|
||||||
|
<Typography.Text className="text-xs font-normal" type="secondary">
|
||||||
|
{t("workflow_node.notify.form.strategy_config.label")}
|
||||||
|
</Typography.Text>
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
|
||||||
|
<Form.Item label={t("workflow_node.notify.form.skip_on_all_prev_skipped.label")}>
|
||||||
|
<Flex align="center" gap={8} wrap="wrap">
|
||||||
|
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.prefix")}</div>
|
||||||
|
<Form.Item name="skipOnAllPrevSkipped" noStyle rules={[formRule]}>
|
||||||
|
<Switch
|
||||||
|
checkedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.on")}
|
||||||
|
unCheckedChildren={t("workflow_node.notify.form.skip_on_all_prev_skipped.switch.off")}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<div>{t("workflow_node.notify.form.skip_on_all_prev_skipped.suffix")}</div>
|
||||||
|
</Flex>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import dayjs from "dayjs";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType } from "@/domain/workflow";
|
import { WORKFLOW_TRIGGERS, type WorkflowNodeConfigForStart, type WorkflowTriggerType, defaultNodeConfigForStart } from "@/domain/workflow";
|
||||||
import { useAntdForm } from "@/hooks";
|
import { useAntdForm } from "@/hooks";
|
||||||
import { getNextCronExecutions, validCronExpression } from "@/utils/cron";
|
import { getNextCronExecutions, validCronExpression } from "@/utils/cron";
|
||||||
|
|
||||||
@ -27,10 +27,7 @@ export type StartNodeConfigFormInstance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initFormModel = (): StartNodeConfigFormFieldValues => {
|
const initFormModel = (): StartNodeConfigFormFieldValues => {
|
||||||
return {
|
return defaultNodeConfigForStart();
|
||||||
trigger: WORKFLOW_TRIGGERS.AUTO,
|
|
||||||
triggerCron: "0 0 * * *",
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const StartNodeConfigForm = forwardRef<StartNodeConfigFormInstance, StartNodeConfigFormProps>(
|
const StartNodeConfigForm = forwardRef<StartNodeConfigFormInstance, StartNodeConfigFormProps>(
|
||||||
|
@ -6,7 +6,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import { validateCertificate, validatePrivateKey } from "@/api/certificates";
|
import { validateCertificate, validatePrivateKey } from "@/api/certificates";
|
||||||
import TextFileInput from "@/components/TextFileInput";
|
import TextFileInput from "@/components/TextFileInput";
|
||||||
import { type WorkflowNodeConfigForUpload } from "@/domain/workflow";
|
import { type WorkflowNodeConfigForUpload, defaultNodeConfigForUpload } from "@/domain/workflow";
|
||||||
import { useAntdForm } from "@/hooks";
|
import { useAntdForm } from "@/hooks";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { getErrMsg } from "@/utils/error";
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ export type UploadNodeConfigFormInstance = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initFormModel = (): UploadNodeConfigFormFieldValues => {
|
const initFormModel = (): UploadNodeConfigFormFieldValues => {
|
||||||
return {};
|
return defaultNodeConfigForUpload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNodeConfigFormProps>(
|
const UploadNodeConfigForm = forwardRef<UploadNodeConfigFormInstance, UploadNodeConfigFormProps>(
|
||||||
|
@ -133,6 +133,13 @@ export type WorkflowNodeConfigForStart = {
|
|||||||
triggerCron?: string;
|
triggerCron?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForStart = (): Partial<WorkflowNodeConfigForStart> => {
|
||||||
|
return {
|
||||||
|
trigger: WORKFLOW_TRIGGERS.AUTO,
|
||||||
|
triggerCron: "0 0 * * *",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForApply = {
|
export type WorkflowNodeConfigForApply = {
|
||||||
domains: string;
|
domains: string;
|
||||||
contactEmail: string;
|
contactEmail: string;
|
||||||
@ -152,6 +159,14 @@ export type WorkflowNodeConfigForApply = {
|
|||||||
skipBeforeExpiryDays: number;
|
skipBeforeExpiryDays: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForApply = (): Partial<WorkflowNodeConfigForApply> => {
|
||||||
|
return {
|
||||||
|
challengeType: "dns-01",
|
||||||
|
keyAlgorithm: "RSA2048",
|
||||||
|
skipBeforeExpiryDays: 30,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForUpload = {
|
export type WorkflowNodeConfigForUpload = {
|
||||||
certificateId: string;
|
certificateId: string;
|
||||||
domains: string;
|
domains: string;
|
||||||
@ -159,6 +174,10 @@ export type WorkflowNodeConfigForUpload = {
|
|||||||
privateKey: string;
|
privateKey: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForUpload = (): Partial<WorkflowNodeConfigForUpload> => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForMonitor = {
|
export type WorkflowNodeConfigForMonitor = {
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
@ -166,6 +185,13 @@ export type WorkflowNodeConfigForMonitor = {
|
|||||||
requestPath?: string;
|
requestPath?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForMonitor = (): Partial<WorkflowNodeConfigForMonitor> => {
|
||||||
|
return {
|
||||||
|
port: 443,
|
||||||
|
requestPath: "/",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForDeploy = {
|
export type WorkflowNodeConfigForDeploy = {
|
||||||
certificate: string;
|
certificate: string;
|
||||||
provider: string;
|
provider: string;
|
||||||
@ -174,6 +200,12 @@ export type WorkflowNodeConfigForDeploy = {
|
|||||||
skipOnLastSucceeded: boolean;
|
skipOnLastSucceeded: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForDeploy = (): Partial<WorkflowNodeConfigForDeploy> => {
|
||||||
|
return {
|
||||||
|
skipOnLastSucceeded: true,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForNotify = {
|
export type WorkflowNodeConfigForNotify = {
|
||||||
subject: string;
|
subject: string;
|
||||||
message: string;
|
message: string;
|
||||||
@ -184,12 +216,21 @@ export type WorkflowNodeConfigForNotify = {
|
|||||||
provider: string;
|
provider: string;
|
||||||
providerAccessId: string;
|
providerAccessId: string;
|
||||||
providerConfig?: Record<string, unknown>;
|
providerConfig?: Record<string, unknown>;
|
||||||
|
skipOnAllPrevSkipped?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForNotify = (): Partial<WorkflowNodeConfigForNotify> => {
|
||||||
|
return {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForCondition = {
|
export type WorkflowNodeConfigForCondition = {
|
||||||
expression?: Expr;
|
expression?: Expr;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const defaultNodeConfigForCondition = (): Partial<WorkflowNodeConfigForCondition> => {
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowNodeConfigForBranch = never;
|
export type WorkflowNodeConfigForBranch = never;
|
||||||
|
|
||||||
export type WorkflowNodeConfigForEnd = never;
|
export type WorkflowNodeConfigForEnd = never;
|
||||||
@ -243,15 +284,18 @@ type InitWorkflowOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => {
|
export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => {
|
||||||
const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode;
|
const root = newNode(WorkflowNodeType.Start, {
|
||||||
root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL };
|
nodeConfig: { trigger: WORKFLOW_TRIGGERS.MANUAL },
|
||||||
|
});
|
||||||
|
|
||||||
switch (options.template) {
|
switch (options.template) {
|
||||||
case "standard":
|
case "standard":
|
||||||
{
|
{
|
||||||
let current = root;
|
let current = root;
|
||||||
|
|
||||||
const applyNode = newNode(WorkflowNodeType.Apply);
|
const applyNode = newNode(WorkflowNodeType.Apply, {
|
||||||
|
nodeConfig: defaultNodeConfigForApply(),
|
||||||
|
});
|
||||||
current.next = applyNode;
|
current.next = applyNode;
|
||||||
|
|
||||||
current = current.next;
|
current = current.next;
|
||||||
@ -260,6 +304,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
current = current.next!.branches![1];
|
current = current.next!.branches![1];
|
||||||
current.next = newNode(WorkflowNodeType.Notify, {
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
nodeConfig: {
|
nodeConfig: {
|
||||||
|
...defaultNodeConfigForNotify(),
|
||||||
subject: "[Certimate] Workflow Failure Alert!",
|
subject: "[Certimate] Workflow Failure Alert!",
|
||||||
message: "Your workflow run for the certificate application has failed. Please check the details.",
|
message: "Your workflow run for the certificate application has failed. Please check the details.",
|
||||||
} as WorkflowNodeConfigForNotify,
|
} as WorkflowNodeConfigForNotify,
|
||||||
@ -268,8 +313,8 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
current = applyNode.next!.branches![0];
|
current = applyNode.next!.branches![0];
|
||||||
current.next = newNode(WorkflowNodeType.Deploy, {
|
current.next = newNode(WorkflowNodeType.Deploy, {
|
||||||
nodeConfig: {
|
nodeConfig: {
|
||||||
|
...defaultNodeConfigForDeploy(),
|
||||||
certificate: `${applyNode.id}#certificate`,
|
certificate: `${applyNode.id}#certificate`,
|
||||||
skipOnLastSucceeded: true,
|
|
||||||
} as WorkflowNodeConfigForDeploy,
|
} as WorkflowNodeConfigForDeploy,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -279,6 +324,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
current = current.next!.branches![1];
|
current = current.next!.branches![1];
|
||||||
current.next = newNode(WorkflowNodeType.Notify, {
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
nodeConfig: {
|
nodeConfig: {
|
||||||
|
...defaultNodeConfigForNotify(),
|
||||||
subject: "[Certimate] Workflow Failure Alert!",
|
subject: "[Certimate] Workflow Failure Alert!",
|
||||||
message: "Your workflow run for the certificate deployment has failed. Please check the details.",
|
message: "Your workflow run for the certificate deployment has failed. Please check the details.",
|
||||||
} as WorkflowNodeConfigForNotify,
|
} as WorkflowNodeConfigForNotify,
|
||||||
@ -290,7 +336,9 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
{
|
{
|
||||||
let current = root;
|
let current = root;
|
||||||
|
|
||||||
const monitorNode = newNode(WorkflowNodeType.Monitor);
|
const monitorNode = newNode(WorkflowNodeType.Monitor, {
|
||||||
|
nodeConfig: defaultNodeConfigForMonitor(),
|
||||||
|
});
|
||||||
current.next = monitorNode;
|
current.next = monitorNode;
|
||||||
|
|
||||||
current = current.next;
|
current = current.next;
|
||||||
@ -299,6 +347,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
current = current.next!.branches![1];
|
current = current.next!.branches![1];
|
||||||
current.next = newNode(WorkflowNodeType.Notify, {
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
nodeConfig: {
|
nodeConfig: {
|
||||||
|
...defaultNodeConfigForNotify(),
|
||||||
subject: "[Certimate] Workflow Failure Alert!",
|
subject: "[Certimate] Workflow Failure Alert!",
|
||||||
message: "Your workflow run for the certificate monitoring has failed. Please check the details.",
|
message: "Your workflow run for the certificate monitoring has failed. Please check the details.",
|
||||||
} as WorkflowNodeConfigForNotify,
|
} as WorkflowNodeConfigForNotify,
|
||||||
@ -352,6 +401,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
} as WorkflowNodeConfigForCondition;
|
} as WorkflowNodeConfigForCondition;
|
||||||
current.next = newNode(WorkflowNodeType.Notify, {
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
nodeConfig: {
|
nodeConfig: {
|
||||||
|
...defaultNodeConfigForNotify(),
|
||||||
subject: "[Certimate] Certificate Expiry Alert!",
|
subject: "[Certimate] Certificate Expiry Alert!",
|
||||||
message: "The certificate will expire soon. Please pay attention to your website.",
|
message: "The certificate will expire soon. Please pay attention to your website.",
|
||||||
} as WorkflowNodeConfigForNotify,
|
} as WorkflowNodeConfigForNotify,
|
||||||
@ -380,6 +430,7 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel =
|
|||||||
} as WorkflowNodeConfigForCondition;
|
} as WorkflowNodeConfigForCondition;
|
||||||
current.next = newNode(WorkflowNodeType.Notify, {
|
current.next = newNode(WorkflowNodeType.Notify, {
|
||||||
nodeConfig: {
|
nodeConfig: {
|
||||||
|
...defaultNodeConfigForNotify(),
|
||||||
subject: "[Certimate] Certificate Expiry Alert!",
|
subject: "[Certimate] Certificate Expiry Alert!",
|
||||||
message: "The certificate has already expired. Please pay attention to your website.",
|
message: "The certificate has already expired. Please pay attention to your website.",
|
||||||
} as WorkflowNodeConfigForNotify,
|
} as WorkflowNodeConfigForNotify,
|
||||||
@ -458,18 +509,22 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {}
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => {
|
type CloneNodeOptions = {
|
||||||
|
withCopySuffix?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cloneNode = (sourceNode: WorkflowNode, { withCopySuffix }: CloneNodeOptions = { withCopySuffix: true }): WorkflowNode => {
|
||||||
const { produce } = new Immer({ autoFreeze: false });
|
const { produce } = new Immer({ autoFreeze: false });
|
||||||
const deepClone = (node: WorkflowNode): WorkflowNode => {
|
const deepClone = (node: WorkflowNode): WorkflowNode => {
|
||||||
return produce(node, (draft) => {
|
return produce(node, (draft) => {
|
||||||
draft.id = nanoid();
|
draft.id = nanoid();
|
||||||
|
|
||||||
if (draft.next) {
|
if (draft.next) {
|
||||||
draft.next = cloneNode(draft.next);
|
draft.next = cloneNode(draft.next, { withCopySuffix });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (draft.branches) {
|
if (draft.branches) {
|
||||||
draft.branches = draft.branches.map((branch) => cloneNode(branch));
|
draft.branches = draft.branches.map((branch) => cloneNode(branch, { withCopySuffix }));
|
||||||
}
|
}
|
||||||
|
|
||||||
return draft;
|
return draft;
|
||||||
@ -477,7 +532,7 @@ export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const copyNode = produce(sourceNode, (draft) => {
|
const copyNode = produce(sourceNode, (draft) => {
|
||||||
draft.name = `${draft.name}-copy`;
|
draft.name = withCopySuffix ? `${draft.name}-copy` : draft.name;
|
||||||
});
|
});
|
||||||
return deepClone(copyNode);
|
return deepClone(copyNode);
|
||||||
};
|
};
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
"workflow.action.create": "Create workflow",
|
"workflow.action.create": "Create workflow",
|
||||||
"workflow.action.edit": "Edit workflow",
|
"workflow.action.edit": "Edit workflow",
|
||||||
|
"workflow.action.duplicate": "Duplicate workflow",
|
||||||
|
"workflow.action.duplicate.confirm": "Are you sure to duplicate this workflow?",
|
||||||
"workflow.action.delete": "Delete workflow",
|
"workflow.action.delete": "Delete workflow",
|
||||||
"workflow.action.delete.confirm": "Are you sure to delete this workflow?",
|
"workflow.action.delete.confirm": "Are you sure to delete this workflow?",
|
||||||
"workflow.action.enable": "Enable",
|
"workflow.action.enable": "Enable",
|
||||||
|
@ -878,6 +878,12 @@
|
|||||||
"workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
|
"workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
|
||||||
"workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.",
|
"workflow_node.notify.form.webhook_data.guide": "<details><summary>Supported variables: </summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>: The subject of notification.</li><li><strong>${MESSAGE}</strong>: The message of notification.</li></ol></details><br>Please visit the authorization management page for addtional notes.",
|
||||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||||
|
"workflow_node.notify.form.strategy_config.label": "Strategy settings",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.label": "Silent behavior",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "If all the previous nodes were skipped, ",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.suffix": " to notify.",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "skip",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "not skip",
|
||||||
|
|
||||||
"workflow_node.end.label": "End",
|
"workflow_node.end.label": "End",
|
||||||
"workflow_node.end.default_name": "End",
|
"workflow_node.end.default_name": "End",
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
|
|
||||||
"workflow.action.create": "新建工作流",
|
"workflow.action.create": "新建工作流",
|
||||||
"workflow.action.edit": "编辑工作流",
|
"workflow.action.edit": "编辑工作流",
|
||||||
|
"workflow.action.duplicate": "复制工作流",
|
||||||
|
"workflow.action.duplicate.confirm": "确定要复制此工作流吗?",
|
||||||
"workflow.action.delete": "删除工作流",
|
"workflow.action.delete": "删除工作流",
|
||||||
"workflow.action.delete.confirm": "确定要删除此工作流吗?",
|
"workflow.action.delete.confirm": "确定要删除此工作流吗?",
|
||||||
"workflow.action.enable": "启用",
|
"workflow.action.enable": "启用",
|
||||||
|
@ -795,7 +795,7 @@
|
|||||||
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名",
|
"workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名",
|
||||||
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
|
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
|
||||||
"workflow_node.deploy.form.wangsu_cdn_domains.label": "网宿云 CDN 加速域名",
|
"workflow_node.deploy.form.wangsu_cdn_domains.label": "网宿云 CDN 加速域名",
|
||||||
"workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "请输入网宿云 CDN 加速域名(多个值请用半角分号隔开)",
|
"workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "请输入网宿云 CDN 加速域名(支持泛域名;多个值请用半角分号隔开)",
|
||||||
"workflow_node.deploy.form.wangsu_cdn_domains.tooltip": "这是什么?请参阅 <a href=\"https://cdn.console.wangsu.com/v2/index/#/property/list\" target=\"_blank\">https://cdn.console.wangsu.com/v2/index/#/property/list</a>",
|
"workflow_node.deploy.form.wangsu_cdn_domains.tooltip": "这是什么?请参阅 <a href=\"https://cdn.console.wangsu.com/v2/index/#/property/list\" target=\"_blank\">https://cdn.console.wangsu.com/v2/index/#/property/list</a>",
|
||||||
"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title": "修改网宿云 CDN 加速域名",
|
"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.title": "修改网宿云 CDN 加速域名",
|
||||||
"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder": "请输入网宿云 CDN 加速域名",
|
"workflow_node.deploy.form.wangsu_cdn_domains.multiple_input_modal.placeholder": "请输入网宿云 CDN 加速域名",
|
||||||
@ -845,7 +845,7 @@
|
|||||||
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
|
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
|
||||||
"workflow_node.notify.form.message.label": "通知内容",
|
"workflow_node.notify.form.message.label": "通知内容",
|
||||||
"workflow_node.notify.form.message.placeholder": "请输入通知内容",
|
"workflow_node.notify.form.message.placeholder": "请输入通知内容",
|
||||||
"workflow_node.notify.form.channel.label": "通知渠道(已废弃,请使用「通知渠道授权」字段)",
|
"workflow_node.notify.form.channel.label": "通知渠道(即将废弃,请使用「通知渠道授权」字段)",
|
||||||
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
|
"workflow_node.notify.form.channel.placeholder": "请选择通知渠道",
|
||||||
"workflow_node.notify.form.channel.button": "设置",
|
"workflow_node.notify.form.channel.button": "设置",
|
||||||
"workflow_node.notify.form.provider.label": "通知渠道",
|
"workflow_node.notify.form.provider.label": "通知渠道",
|
||||||
@ -877,6 +877,12 @@
|
|||||||
"workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",
|
"workflow_node.notify.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",
|
||||||
"workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。",
|
"workflow_node.notify.form.webhook_data.guide": "<details><summary>支持的变量:</summary><ol style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>${SUBJECT}</strong>:通知主题。</li><li><strong>${MESSAGE}</strong>:通知内容。</ol></details><br>其他注意事项请前往授权管理页面查看。",
|
||||||
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||||
|
"workflow_node.notify.form.strategy_config.label": "执行策略",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.label": "静默行为",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "当前序申请、上传、部署等节点均已跳过执行时,",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.suffix": "此通知节点。",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.on": "跳过",
|
||||||
|
"workflow_node.notify.form.skip_on_all_prev_skipped.switch.off": "不跳过",
|
||||||
|
|
||||||
"workflow_node.end.label": "结束",
|
"workflow_node.end.label": "结束",
|
||||||
"workflow_node.end.default_name": "结束",
|
"workflow_node.end.default_name": "结束",
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
EditOutlined as EditOutlinedIcon,
|
EditOutlined as EditOutlinedIcon,
|
||||||
PlusOutlined as PlusOutlinedIcon,
|
PlusOutlined as PlusOutlinedIcon,
|
||||||
ReloadOutlined as ReloadOutlinedIcon,
|
ReloadOutlined as ReloadOutlinedIcon,
|
||||||
|
SnippetsOutlined as SnippetsOutlinedIcon,
|
||||||
StopOutlined as StopOutlinedIcon,
|
StopOutlined as StopOutlinedIcon,
|
||||||
SyncOutlined as SyncOutlinedIcon,
|
SyncOutlined as SyncOutlinedIcon,
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
@ -39,7 +40,7 @@ import {
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { ClientResponseError } from "pocketbase";
|
import { ClientResponseError } from "pocketbase";
|
||||||
|
|
||||||
import { WORKFLOW_TRIGGERS, type WorkflowModel, isAllNodesValidated } from "@/domain/workflow";
|
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
|
||||||
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
|
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
|
||||||
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||||
import { getErrMsg } from "@/utils/error";
|
import { getErrMsg } from "@/utils/error";
|
||||||
@ -219,6 +220,17 @@ const WorkflowList = () => {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title={t("workflow.action.duplicate")}>
|
||||||
|
<Button
|
||||||
|
color="primary"
|
||||||
|
icon={<SnippetsOutlinedIcon />}
|
||||||
|
variant="text"
|
||||||
|
onClick={() => {
|
||||||
|
handleDuplicateClick(record);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip title={t("workflow.action.delete")}>
|
<Tooltip title={t("workflow.action.delete")}>
|
||||||
<Button
|
<Button
|
||||||
color="danger"
|
color="danger"
|
||||||
@ -321,6 +333,36 @@ const WorkflowList = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDuplicateClick = (workflow: WorkflowModel) => {
|
||||||
|
modalApi.confirm({
|
||||||
|
title: t("workflow.action.duplicate"),
|
||||||
|
content: t("workflow.action.duplicate.confirm"),
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const workflowCopy = {
|
||||||
|
name: `${workflow.name}-copy`,
|
||||||
|
description: workflow.description,
|
||||||
|
trigger: workflow.trigger,
|
||||||
|
triggerCron: workflow.triggerCron,
|
||||||
|
draft: workflow.content
|
||||||
|
? cloneNode(workflow.content, { withCopySuffix: false })
|
||||||
|
: workflow.draft
|
||||||
|
? cloneNode(workflow.draft, { withCopySuffix: false })
|
||||||
|
: initWorkflow().draft,
|
||||||
|
hasDraft: true,
|
||||||
|
} as WorkflowModel;
|
||||||
|
const resp = await saveWorkflow(workflowCopy);
|
||||||
|
if (resp) {
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteClick = (workflow: WorkflowModel) => {
|
const handleDeleteClick = (workflow: WorkflowModel) => {
|
||||||
modalApi.confirm({
|
modalApi.confirm({
|
||||||
title: t("workflow.action.delete"),
|
title: t("workflow.action.delete"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user