diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go
index afa379a8..7d7355c5 100644
--- a/internal/domain/workflow.go
+++ b/internal/domain/workflow.go
@@ -31,6 +31,7 @@ const (
WorkflowNodeTypeEnd = WorkflowNodeType("end")
WorkflowNodeTypeApply = WorkflowNodeType("apply")
WorkflowNodeTypeUpload = WorkflowNodeType("upload")
+ WorkflowNodeTypeMonitor = WorkflowNodeType("monitor")
WorkflowNodeTypeDeploy = WorkflowNodeType("deploy")
WorkflowNodeTypeNotify = WorkflowNodeType("notify")
WorkflowNodeTypeBranch = WorkflowNodeType("branch")
@@ -38,7 +39,6 @@ const (
WorkflowNodeTypeExecuteResultBranch = WorkflowNodeType("execute_result_branch")
WorkflowNodeTypeExecuteSuccess = WorkflowNodeType("execute_success")
WorkflowNodeTypeExecuteFailure = WorkflowNodeType("execute_failure")
- WorkflowNodeTypeInspect = WorkflowNodeType("inspect")
)
type WorkflowTriggerType string
@@ -83,21 +83,17 @@ type WorkflowNodeConfigForApply struct {
SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30)
}
-type WorkflowNodeConfigForCondition struct {
- Expression Expr `json:"expression"` // 条件表达式
-}
-
-type WorkflowNodeConfigForInspect struct {
- Host string `json:"host"` // 主机
- Domain string `json:"domain"` // 域名
- Port string `json:"port"` // 端口
- Path string `json:"path"` // 路径
-}
-
type WorkflowNodeConfigForUpload struct {
- Certificate string `json:"certificate"`
- PrivateKey string `json:"privateKey"`
- Domains string `json:"domains"`
+ Certificate string `json:"certificate"` // 证书 PEM 内容
+ PrivateKey string `json:"privateKey"` // 私钥 PEM 内容
+ Domains string `json:"domains,omitempty"`
+}
+
+type WorkflowNodeConfigForMonitor struct {
+ Host string `json:"host"` // 主机地址
+ Port int32 `json:"port,omitempty"` // 端口(零值时默认值 443)
+ Domain string `json:"domain,omitempty"` // 域名(零值时默认值 [Host])
+ RequestPath string `json:"requestPath,omitempty"` // 请求路径
}
type WorkflowNodeConfigForDeploy struct {
@@ -117,48 +113,8 @@ type WorkflowNodeConfigForNotify struct {
Message string `json:"message"` // 通知内容
}
-func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
- expression := n.Config["expression"]
- if expression == nil {
- return WorkflowNodeConfigForCondition{}
- }
-
- raw, _ := json.Marshal(expression)
-
- expr, err := UnmarshalExpr([]byte(raw))
- if err != nil {
- return WorkflowNodeConfigForCondition{}
- }
-
- return WorkflowNodeConfigForCondition{
- Expression: expr,
- }
-}
-
-func (n *WorkflowNode) GetConfigForInspect() WorkflowNodeConfigForInspect {
- host := maputil.GetString(n.Config, "host")
- if host == "" {
- return WorkflowNodeConfigForInspect{}
- }
-
- domain := maputil.GetString(n.Config, "domain")
- if domain == "" {
- domain = host
- }
-
- port := maputil.GetString(n.Config, "port")
- if port == "" {
- port = "443"
- }
-
- path := maputil.GetString(n.Config, "path")
-
- return WorkflowNodeConfigForInspect{
- Domain: domain,
- Port: port,
- Host: host,
- Path: path,
- }
+type WorkflowNodeConfigForCondition struct {
+ Expression Expr `json:"expression"` // 条件表达式
}
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
@@ -190,6 +146,16 @@ func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload {
}
}
+func (n *WorkflowNode) GetConfigForMonitor() WorkflowNodeConfigForMonitor {
+ host := maputil.GetString(n.Config, "host")
+ return WorkflowNodeConfigForMonitor{
+ Host: host,
+ Port: maputil.GetOrDefaultInt32(n.Config, "port", 443),
+ Domain: maputil.GetOrDefaultString(n.Config, "domain", host),
+ RequestPath: maputil.GetString(n.Config, "path"),
+ }
+}
+
func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
return WorkflowNodeConfigForDeploy{
Certificate: maputil.GetString(n.Config, "certificate"),
@@ -211,6 +177,24 @@ func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
}
}
+func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition {
+ expression := n.Config["expression"]
+ if expression == nil {
+ return WorkflowNodeConfigForCondition{}
+ }
+
+ raw, _ := json.Marshal(expression)
+
+ expr, err := UnmarshalExpr([]byte(raw))
+ if err != nil {
+ return WorkflowNodeConfigForCondition{}
+ }
+
+ return WorkflowNodeConfigForCondition{
+ Expression: expr,
+ }
+}
+
type WorkflowNodeIO struct {
Label string `json:"label"`
Name string `json:"name"`
diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go
index e663e3fc..321d9fc8 100644
--- a/internal/workflow/node-processor/apply_node.go
+++ b/internal/workflow/node-processor/apply_node.go
@@ -34,7 +34,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode {
}
func (n *applyNode) Process(ctx context.Context) error {
- n.logger.Info("ready to apply ...")
+ n.logger.Info("ready to obtain certificiate ...")
// 查询上次执行结果
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
@@ -63,7 +63,7 @@ func (n *applyNode) Process(ctx context.Context) error {
// 申请证书
applyResult, err := applicant.Apply(ctx)
if err != nil {
- n.logger.Warn("failed to apply")
+ n.logger.Warn("failed to obtain certificiate")
return err
}
@@ -112,7 +112,7 @@ func (n *applyNode) Process(ctx context.Context) error {
n.outputs[outputCertificateValidatedKey] = "true"
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24))
- n.logger.Info("apply completed")
+ n.logger.Info("application completed")
return nil
}
@@ -156,7 +156,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo
if expirationTime > renewalInterval {
n.outputs[outputCertificateValidatedKey] = "true"
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(expirationTime.Hours()/24))
- return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
+ return true, fmt.Sprintf("the certificate has already been issued (expires in %d day(s), next renewal in %d day(s))", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays)
}
}
}
diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go
index 3819b4a2..f0ded21d 100644
--- a/internal/workflow/node-processor/deploy_node.go
+++ b/internal/workflow/node-processor/deploy_node.go
@@ -33,7 +33,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode {
}
func (n *deployNode) Process(ctx context.Context) error {
- n.logger.Info("ready to deploy ...")
+ n.logger.Info("ready to deploy certificate ...")
// 查询上次执行结果
lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id)
@@ -78,7 +78,7 @@ func (n *deployNode) Process(ctx context.Context) error {
// 部署证书
if err := deployer.Deploy(ctx); err != nil {
- n.logger.Warn("failed to deploy")
+ n.logger.Warn("failed to deploy certificate")
return err
}
@@ -95,8 +95,7 @@ func (n *deployNode) Process(ctx context.Context) error {
return err
}
- n.logger.Info("deploy completed")
-
+ n.logger.Info("deployment completed")
return nil
}
diff --git a/internal/workflow/node-processor/inspect_node.go b/internal/workflow/node-processor/inspect_node.go
deleted file mode 100644
index a8661f37..00000000
--- a/internal/workflow/node-processor/inspect_node.go
+++ /dev/null
@@ -1,191 +0,0 @@
-package nodeprocessor
-
-import (
- "context"
- "crypto/tls"
- "crypto/x509"
- "fmt"
- "math"
- "net"
- "net/http"
- "strings"
- "time"
-
- "github.com/usual2970/certimate/internal/domain"
-)
-
-type inspectNode struct {
- node *domain.WorkflowNode
- *nodeProcessor
- *nodeOutputer
-}
-
-func NewInspectNode(node *domain.WorkflowNode) *inspectNode {
- return &inspectNode{
- node: node,
- nodeProcessor: newNodeProcessor(node),
- nodeOutputer: newNodeOutputer(),
- }
-}
-
-func (n *inspectNode) Process(ctx context.Context) error {
- n.logger.Info("entering inspect certificate node...")
-
- nodeConfig := n.node.GetConfigForInspect()
-
- err := n.inspect(ctx, nodeConfig)
- if err != nil {
- n.logger.Warn("inspect certificate failed: " + err.Error())
- return err
- }
-
- return nil
-}
-
-func (n *inspectNode) inspect(ctx context.Context, nodeConfig domain.WorkflowNodeConfigForInspect) error {
- maxRetries := 3
- retryInterval := 2 * time.Second
-
- var lastError error
- var certInfo *x509.Certificate
-
- host := nodeConfig.Host
-
- port := nodeConfig.Port
- if port == "" {
- port = "443"
- }
-
- domain := nodeConfig.Domain
- if domain == "" {
- domain = host
- }
-
- path := nodeConfig.Path
- if path != "" && !strings.HasPrefix(path, "/") {
- path = "/" + path
- }
-
- targetAddr := fmt.Sprintf("%s:%s", host, port)
- n.logger.Info(fmt.Sprintf("Inspecting certificate at %s (validating domain: %s)", targetAddr, domain))
-
- for attempt := 0; attempt < maxRetries; attempt++ {
- if attempt > 0 {
- n.logger.Info(fmt.Sprintf("Retry #%d connecting to %s", attempt, targetAddr))
- select {
- case <-ctx.Done():
- return ctx.Err()
- case <-time.After(retryInterval):
- // Wait for retry interval
- }
- }
-
- transport := &http.Transport{
- DialContext: (&net.Dialer{
- Timeout: 10 * time.Second,
- }).DialContext,
- TLSClientConfig: &tls.Config{
- InsecureSkipVerify: true,
- ServerName: domain, // Set SNI to domain for proper certificate selection
- },
- ForceAttemptHTTP2: false,
- DisableKeepAlives: true,
- }
-
- client := &http.Client{
- Transport: transport,
- Timeout: 15 * time.Second,
- CheckRedirect: func(req *http.Request, via []*http.Request) error {
- return http.ErrUseLastResponse
- },
- }
-
- scheme := "https"
- urlStr := fmt.Sprintf("%s://%s", scheme, targetAddr)
- if path != "" {
- urlStr = urlStr + path
- }
-
- req, err := http.NewRequestWithContext(ctx, "HEAD", urlStr, nil)
- if err != nil {
- lastError = fmt.Errorf("failed to create HTTP request: %w", err)
- n.logger.Warn(fmt.Sprintf("Request creation attempt #%d failed: %s", attempt+1, lastError.Error()))
- continue
- }
-
- if domain != host {
- req.Host = domain
- }
-
- req.Header.Set("User-Agent", "CertificateValidator/1.0")
- req.Header.Set("Accept", "*/*")
-
- resp, err := client.Do(req)
- if err != nil {
- lastError = fmt.Errorf("HTTP request failed: %w", err)
- n.logger.Warn(fmt.Sprintf("Connection attempt #%d failed: %s", attempt+1, lastError.Error()))
- continue
- }
-
- if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
- resp.Body.Close()
- lastError = fmt.Errorf("no TLS certificates received in HTTP response")
- n.logger.Warn(fmt.Sprintf("Certificate retrieval attempt #%d failed: %s", attempt+1, lastError.Error()))
- continue
- }
-
- certInfo = resp.TLS.PeerCertificates[0]
- resp.Body.Close()
-
- lastError = nil
- n.logger.Info(fmt.Sprintf("Successfully retrieved certificate from %s", targetAddr))
- break
- }
-
- if lastError != nil {
- return fmt.Errorf("failed to retrieve certificate after %d attempts: %w", maxRetries, lastError)
- }
-
- if certInfo == nil {
- outputs := map[string]any{
- outputCertificateValidatedKey: "false",
- outputCertificateDaysLeftKey: "0",
- }
- n.setOutputs(outputs)
- return nil
- }
-
- now := time.Now()
-
- isValidTime := now.Before(certInfo.NotAfter) && now.After(certInfo.NotBefore)
-
- domainMatch := true
- if err := certInfo.VerifyHostname(domain); err != nil {
- domainMatch = false
- }
-
- isValid := isValidTime && domainMatch
-
- daysRemaining := math.Floor(certInfo.NotAfter.Sub(now).Hours() / 24)
-
- isValidStr := "false"
- if isValid {
- isValidStr = "true"
- }
-
- outputs := map[string]any{
- outputCertificateValidatedKey: isValidStr,
- outputCertificateDaysLeftKey: fmt.Sprintf("%d", int(daysRemaining)),
- }
-
- n.setOutputs(outputs)
-
- n.logger.Info(fmt.Sprintf("Certificate inspection completed - Target: %s, Domain: %s, Valid: %s, Days Remaining: %d",
- targetAddr, domain, isValidStr, int(daysRemaining)))
-
- return nil
-}
-
-func (n *inspectNode) setOutputs(outputs map[string]any) {
- n.outputs = outputs
-}
diff --git a/internal/workflow/node-processor/inspect_node_test.go b/internal/workflow/node-processor/inspect_node_test.go
deleted file mode 100644
index 5cb826c1..00000000
--- a/internal/workflow/node-processor/inspect_node_test.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package nodeprocessor
-
-import (
- "context"
- "testing"
-
- "github.com/usual2970/certimate/internal/domain"
-)
-
-func Test_inspectWebsiteCertificateNode_inspect(t *testing.T) {
- type args struct {
- ctx context.Context
- nodeConfig domain.WorkflowNodeConfigForInspect
- }
- tests := []struct {
- name string
- args args
- wantErr bool
- }{
- {
- name: "test1",
- args: args{
- ctx: context.Background(),
- nodeConfig: domain.WorkflowNodeConfigForInspect{
- Domain: "baidu.com",
- Port: "443",
- },
- },
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- n := NewInspectNode(&domain.WorkflowNode{})
- if err := n.inspect(tt.args.ctx, tt.args.nodeConfig); (err != nil) != tt.wantErr {
- t.Errorf("inspectWebsiteCertificateNode.inspect() error = %v, wantErr %v", err, tt.wantErr)
- }
- })
- }
-}
diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go
new file mode 100644
index 00000000..f8c1adae
--- /dev/null
+++ b/internal/workflow/node-processor/monitor_node.go
@@ -0,0 +1,164 @@
+package nodeprocessor
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "fmt"
+ "math"
+ "net"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/usual2970/certimate/internal/domain"
+)
+
+type monitorNode struct {
+ node *domain.WorkflowNode
+ *nodeProcessor
+ *nodeOutputer
+}
+
+func NewMonitorNode(node *domain.WorkflowNode) *monitorNode {
+ return &monitorNode{
+ node: node,
+ nodeProcessor: newNodeProcessor(node),
+ nodeOutputer: newNodeOutputer(),
+ }
+}
+
+func (n *monitorNode) Process(ctx context.Context) error {
+ n.logger.Info("ready to monitor certificate ...")
+
+ nodeConfig := n.node.GetConfigForMonitor()
+
+ targetAddr := fmt.Sprintf("%s:%d", nodeConfig.Host, nodeConfig.Port)
+ if nodeConfig.Port == 0 {
+ targetAddr = fmt.Sprintf("%s:443", nodeConfig.Host)
+ }
+
+ targetDomain := nodeConfig.Domain
+ if targetDomain == "" {
+ targetDomain = nodeConfig.Host
+ }
+
+ n.logger.Info(fmt.Sprintf("retrieving certificate at %s (domain: %s)", targetAddr, targetDomain))
+
+ const MAX_ATTEMPTS = 3
+ const RETRY_INTERVAL = 2 * time.Second
+ var cert *x509.Certificate
+ var err error
+ for attempt := 0; attempt < MAX_ATTEMPTS; attempt++ {
+ if attempt > 0 {
+ n.logger.Info(fmt.Sprintf("retry %d time(s) ...", attempt, targetAddr))
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(RETRY_INTERVAL):
+ }
+ }
+
+ cert, err = n.tryRetrieveCert(ctx, targetAddr, targetDomain, nodeConfig.RequestPath)
+ if err == nil {
+ break
+ }
+ }
+
+ if err != nil {
+ n.logger.Warn("failed to monitor certificate")
+ return err
+ } else {
+ if cert == nil {
+ n.logger.Warn("no ssl certificates retrieved in http response")
+
+ outputs := map[string]any{
+ outputCertificateValidatedKey: strconv.FormatBool(false),
+ outputCertificateDaysLeftKey: strconv.FormatInt(0, 10),
+ }
+ n.setOutputs(outputs)
+ } else {
+ n.logger.Info(fmt.Sprintf("ssl certificate retrieved (serial='%s', subject='%s', issuer='%s', not_before='%s', not_after='%s', sans='%s')",
+ cert.SerialNumber, cert.Subject.String(), cert.Issuer.String(),
+ cert.NotBefore.Format(time.RFC3339), cert.NotAfter.Format(time.RFC3339),
+ strings.Join(cert.DNSNames, ";")),
+ )
+
+ now := time.Now()
+ isCertPeriodValid := now.Before(cert.NotAfter) && now.After(cert.NotBefore)
+ isCertHostMatched := true
+ if err := cert.VerifyHostname(targetDomain); err != nil {
+ isCertHostMatched = false
+ }
+
+ validated := isCertPeriodValid && isCertHostMatched
+ daysLeft := int(math.Floor(cert.NotAfter.Sub(now).Hours() / 24))
+ outputs := map[string]any{
+ outputCertificateValidatedKey: strconv.FormatBool(validated),
+ outputCertificateDaysLeftKey: strconv.FormatInt(int64(daysLeft), 10),
+ }
+ n.setOutputs(outputs)
+
+ if validated {
+ n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft))
+ } else {
+ n.logger.Warn(fmt.Sprintf("the certificate is invalid", validated))
+ }
+ }
+ }
+
+ n.logger.Info("monitoring completed")
+ return nil
+}
+
+func (n *monitorNode) tryRetrieveCert(ctx context.Context, addr, domain, requestPath string) (_cert *x509.Certificate, _err error) {
+ transport := &http.Transport{
+ DialContext: (&net.Dialer{
+ Timeout: 10 * time.Second,
+ }).DialContext,
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: true,
+ },
+ ForceAttemptHTTP2: false,
+ DisableKeepAlives: true,
+ Proxy: http.ProxyFromEnvironment,
+ }
+
+ client := &http.Client{
+ Transport: transport,
+ Timeout: 15 * time.Second,
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ return http.ErrUseLastResponse
+ },
+ }
+
+ url := fmt.Sprintf("https://%s/%s", addr, strings.TrimLeft(requestPath, "/"))
+ req, err := http.NewRequestWithContext(ctx, http.MethodHead, url, nil)
+ if err != nil {
+ _err = fmt.Errorf("failed to create http request: %w", err)
+ n.logger.Warn(fmt.Sprintf("failed to create http request: %w", err))
+ return nil, _err
+ }
+
+ req.Header.Set("User-Agent", "certimate")
+ resp, err := client.Do(req)
+ if err != nil {
+ _err = fmt.Errorf("failed to send http request: %w", err)
+ n.logger.Warn(fmt.Sprintf("failed to send http request: %w", err))
+ return nil, _err
+ }
+ defer resp.Body.Close()
+
+ if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
+ return nil, _err
+ }
+
+ _cert = resp.TLS.PeerCertificates[0]
+ return _cert, nil
+}
+
+func (n *monitorNode) setOutputs(outputs map[string]any) {
+ n.outputs = outputs
+}
diff --git a/internal/workflow/node-processor/monitor_node_test.go b/internal/workflow/node-processor/monitor_node_test.go
new file mode 100644
index 00000000..1cc0c876
--- /dev/null
+++ b/internal/workflow/node-processor/monitor_node_test.go
@@ -0,0 +1,28 @@
+package nodeprocessor_test
+
+import (
+ "context"
+ "log/slog"
+ "testing"
+
+ "github.com/usual2970/certimate/internal/domain"
+ nodeprocessor "github.com/usual2970/certimate/internal/workflow/node-processor"
+)
+
+func Test_MonitorNode(t *testing.T) {
+ t.Run("Monitor", func(t *testing.T) {
+ node := nodeprocessor.NewMonitorNode(&domain.WorkflowNode{
+ Id: "test",
+ Type: domain.WorkflowNodeTypeMonitor,
+ Name: "test",
+ Config: map[string]any{
+ "host": "baidu.com",
+ "port": 443,
+ },
+ })
+ node.SetLogger(slog.Default())
+ if err := node.Process(context.Background()); err != nil {
+ t.Errorf("err: %+v", err)
+ }
+ })
+}
diff --git a/internal/workflow/node-processor/notify_node.go b/internal/workflow/node-processor/notify_node.go
index 8f336931..f084cb4f 100644
--- a/internal/workflow/node-processor/notify_node.go
+++ b/internal/workflow/node-processor/notify_node.go
@@ -28,7 +28,7 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode {
}
func (n *notifyNode) Process(ctx context.Context) error {
- n.logger.Info("ready to notify ...")
+ n.logger.Info("ready to send notification ...")
nodeConfig := n.node.GetConfigForNotify()
@@ -51,11 +51,11 @@ func (n *notifyNode) Process(ctx context.Context) error {
// 发送通知
if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil {
- n.logger.Warn("failed to notify", slog.String("channel", nodeConfig.Channel))
+ n.logger.Warn("failed to send notification", slog.String("channel", nodeConfig.Channel))
return err
}
- n.logger.Info("notify completed")
+ n.logger.Info("notification completed")
return nil
}
@@ -73,9 +73,10 @@ func (n *notifyNode) Process(ctx context.Context) error {
// 推送通知
if err := deployer.Notify(ctx); err != nil {
- n.logger.Warn("failed to notify")
+ n.logger.Warn("failed to send notification")
return err
}
+ n.logger.Info("notification completed")
return nil
}
diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go
index 24de76d1..d375883f 100644
--- a/internal/workflow/node-processor/processor.go
+++ b/internal/workflow/node-processor/processor.go
@@ -74,25 +74,25 @@ 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
case domain.WorkflowNodeTypeUpload:
return NewUploadNode(node), nil
+ case domain.WorkflowNodeTypeMonitor:
+ return NewMonitorNode(node), nil
case domain.WorkflowNodeTypeDeploy:
return NewDeployNode(node), nil
case domain.WorkflowNodeTypeNotify:
return NewNotifyNode(node), nil
+ case domain.WorkflowNodeTypeCondition:
+ return NewConditionNode(node), nil
case domain.WorkflowNodeTypeExecuteSuccess:
return NewExecuteSuccessNode(node), nil
case domain.WorkflowNodeTypeExecuteFailure:
return NewExecuteFailureNode(node), nil
- case domain.WorkflowNodeTypeInspect:
- return NewInspectNode(node), nil
}
- return nil, fmt.Errorf("supported node type: %s", string(node.Type))
+ return nil, fmt.Errorf("unsupported node type: %s", string(node.Type))
}
func getContextWorkflowId(ctx context.Context) string {
diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go
index 6a59ca74..8e59b009 100644
--- a/internal/workflow/node-processor/upload_node.go
+++ b/internal/workflow/node-processor/upload_node.go
@@ -31,7 +31,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode {
}
func (n *uploadNode) Process(ctx context.Context) error {
- n.logger.Info("ready to upload ...")
+ n.logger.Info("ready to upload certiticate ...")
nodeConfig := n.node.GetConfigForUpload()
@@ -43,7 +43,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
// 检测是否可以跳过本次执行
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
- n.logger.Info(fmt.Sprintf("skip this upload, because %s", reason))
+ n.logger.Info(fmt.Sprintf("skip this uploading, because %s", reason))
return nil
} else if reason != "" {
n.logger.Info(fmt.Sprintf("re-upload, because %s", reason))
@@ -72,7 +72,7 @@ func (n *uploadNode) Process(ctx context.Context) error {
n.outputs[outputCertificateValidatedKey] = "true"
n.outputs[outputCertificateDaysLeftKey] = fmt.Sprintf("%d", int(time.Until(certificate.ExpireAt).Hours()/24))
- n.logger.Info("upload completed")
+ n.logger.Info("uploading completed")
return nil
}
diff --git a/ui/src/components/access/AccessFormSSHConfig.tsx b/ui/src/components/access/AccessFormSSHConfig.tsx
index 32d1b8bc..c964b1b3 100644
--- a/ui/src/components/access/AccessFormSSHConfig.tsx
+++ b/ui/src/components/access/AccessFormSSHConfig.tsx
@@ -120,7 +120,7 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
-
+
diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx
index 7c112bbe..0a570699 100644
--- a/ui/src/components/access/AccessSelect.tsx
+++ b/ui/src/components/access/AccessSelect.tsx
@@ -37,7 +37,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
if (!access) {
return (
-
+
{key}
@@ -48,7 +48,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => {
const provider = accessProvidersMap.get(access.provider);
return (
-
+
{access.name}
diff --git a/ui/src/components/provider/ACMEDns01ProviderPicker.tsx b/ui/src/components/provider/ACMEDns01ProviderPicker.tsx
index 0f20b296..5a5be8ca 100644
--- a/ui/src/components/provider/ACMEDns01ProviderPicker.tsx
+++ b/ui/src/components/provider/ACMEDns01ProviderPicker.tsx
@@ -67,7 +67,7 @@ const ACMEDns01ProviderPicker = ({ className, style, autoFocus, filter, placehol
}}
>
-
+
{t(provider.name)}
diff --git a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx
index b03adf7b..e2408eeb 100644
--- a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx
+++ b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx
@@ -32,7 +32,7 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr
const provider = acmeDns01ProvidersMap.get(key);
return (
-
+
{t(provider?.name ?? "")}
diff --git a/ui/src/components/provider/AccessProviderPicker.tsx b/ui/src/components/provider/AccessProviderPicker.tsx
index 002d2519..507a95c8 100644
--- a/ui/src/components/provider/AccessProviderPicker.tsx
+++ b/ui/src/components/provider/AccessProviderPicker.tsx
@@ -86,12 +86,12 @@ const AccessProviderPicker = ({ className, style, autoFocus, filter, placeholder
}}
>
-
+
{t(provider.name)}
-
+
{t("access.props.provider.builtin")}
diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx
index 37f1626d..bf4ff6e7 100644
--- a/ui/src/components/provider/AccessProviderSelect.tsx
+++ b/ui/src/components/provider/AccessProviderSelect.tsx
@@ -49,12 +49,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
return (
-
+
{t(provider.name)}
-
+
{t("access.props.provider.builtin")}
diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx
index 15d31230..e5477c21 100644
--- a/ui/src/components/provider/CAProviderSelect.tsx
+++ b/ui/src/components/provider/CAProviderSelect.tsx
@@ -48,7 +48,7 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
const provider = caProvidersMap.get(key);
return (
-
+
{t(provider?.name ?? "")}
diff --git a/ui/src/components/provider/DeploymentProviderPicker.tsx b/ui/src/components/provider/DeploymentProviderPicker.tsx
index b1bcd6fe..bb569acd 100644
--- a/ui/src/components/provider/DeploymentProviderPicker.tsx
+++ b/ui/src/components/provider/DeploymentProviderPicker.tsx
@@ -104,7 +104,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho
>
-
+
{t(provider.name)}
diff --git a/ui/src/components/provider/DeploymentProviderSelect.tsx b/ui/src/components/provider/DeploymentProviderSelect.tsx
index 0b38cedf..89173243 100644
--- a/ui/src/components/provider/DeploymentProviderSelect.tsx
+++ b/ui/src/components/provider/DeploymentProviderSelect.tsx
@@ -32,7 +32,7 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect
const provider = deploymentProvidersMap.get(key);
return (
-
+
{t(provider?.name ?? "")}
diff --git a/ui/src/components/provider/NotificationProviderSelect.tsx b/ui/src/components/provider/NotificationProviderSelect.tsx
index 98a1005c..f30a8f6f 100644
--- a/ui/src/components/provider/NotificationProviderSelect.tsx
+++ b/ui/src/components/provider/NotificationProviderSelect.tsx
@@ -32,7 +32,7 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe
const provider = notificationProvidersMap.get(key);
return (
-
+
{t(provider?.name ?? "")}
diff --git a/ui/src/components/workflow/WorkflowElement.tsx b/ui/src/components/workflow/WorkflowElement.tsx
index d36029df..86720f6d 100644
--- a/ui/src/components/workflow/WorkflowElement.tsx
+++ b/ui/src/components/workflow/WorkflowElement.tsx
@@ -9,10 +9,11 @@ import DeployNode from "./node/DeployNode";
import EndNode from "./node/EndNode";
import ExecuteResultBranchNode from "./node/ExecuteResultBranchNode";
import ExecuteResultNode from "./node/ExecuteResultNode";
+import MonitorNode from "./node/MonitorNode";
import NotifyNode from "./node/NotifyNode";
import StartNode from "./node/StartNode";
+import UnknownNode from "./node/UnknownNode";
import UploadNode from "./node/UploadNode";
-import InspectNode from "./node/InspectNode";
export type WorkflowElementProps = {
node: WorkflowNode;
@@ -32,9 +33,9 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem
case WorkflowNodeType.Upload:
return ;
-
- case WorkflowNodeType.Inspect:
- return ;
+
+ case WorkflowNodeType.Monitor:
+ return ;
case WorkflowNodeType.Deploy:
return ;
@@ -60,7 +61,7 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem
default:
console.warn(`[certimate] unsupported workflow node type: ${node.type}`);
- return <>>;
+ return ;
}
}, [node, disabled, branchId, branchIndex]);
diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx
index bf4c5be2..86a45134 100644
--- a/ui/src/components/workflow/node/AddNode.tsx
+++ b/ui/src/components/workflow/node/AddNode.tsx
@@ -3,11 +3,11 @@ import { useTranslation } from "react-i18next";
import {
CloudUploadOutlined as CloudUploadOutlinedIcon,
DeploymentUnitOutlined as DeploymentUnitOutlinedIcon,
+ MonitorOutlined as MonitorOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
SendOutlined as SendOutlinedIcon,
SisternodeOutlined as SisternodeOutlinedIcon,
SolutionOutlined as SolutionOutlinedIcon,
- MonitorOutlined as MonitorOutlinedIcon,
} from "@ant-design/icons";
import { Dropdown } from "antd";
@@ -28,7 +28,7 @@ const AddNode = ({ node, disabled }: AddNodeProps) => {
return [
[WorkflowNodeType.Apply, "workflow_node.apply.label", ],
[WorkflowNodeType.Upload, "workflow_node.upload.label", ],
- [WorkflowNodeType.Inspect, "workflow_node.inspect.label", ],
+ [WorkflowNodeType.Monitor, "workflow_node.monitor.label", ],
[WorkflowNodeType.Deploy, "workflow_node.deploy.label", ],
[WorkflowNodeType.Notify, "workflow_node.notify.label", ],
[WorkflowNodeType.Branch, "workflow_node.branch.label", ],
diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx
index bcd58c77..bc5b5918 100644
--- a/ui/src/components/workflow/node/ConditionNode.tsx
+++ b/ui/src/components/workflow/node/ConditionNode.tsx
@@ -1,14 +1,17 @@
import { memo, useRef, useState } from "react";
import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons";
import { Button, Card, Popover } from "antd";
+import { produce } from "immer";
+
+import type { Expr, WorkflowNodeIoValueType } from "@/domain/workflow";
+import { ExprType } from "@/domain/workflow";
+import { useZustandShallowSelector } from "@/hooks";
+import { useWorkflowStore } from "@/stores/workflow";
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
import AddNode from "./AddNode";
-import ConditionNodeConfigForm, { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
-import { Expr, WorkflowNodeIoValueType, ExprType } from "@/domain/workflow";
-import { produce } from "immer";
-import { useWorkflowStore } from "@/stores/workflow";
-import { useZustandShallowSelector } from "@/hooks";
+import type { ConditionItem, ConditionNodeConfigFormFieldValues, ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm";
+import ConditionNodeConfigForm from "./ConditionNodeConfigForm";
export type ConditionNodeProps = SharedNodeProps & {
branchId: string;
diff --git a/ui/src/components/workflow/node/DeployNode.tsx b/ui/src/components/workflow/node/DeployNode.tsx
index 92eb2890..b04516fb 100644
--- a/ui/src/components/workflow/node/DeployNode.tsx
+++ b/ui/src/components/workflow/node/DeployNode.tsx
@@ -46,7 +46,7 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
const provider = deploymentProvidersMap.get(config.provider);
return (
-
+
{t(provider?.name ?? "")}
);
diff --git a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx b/ui/src/components/workflow/node/InspectNodeConfigForm.tsx
deleted file mode 100644
index 2d7d83b0..00000000
--- a/ui/src/components/workflow/node/InspectNodeConfigForm.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import { forwardRef, memo, useImperativeHandle } from "react";
-import { useTranslation } from "react-i18next";
-import { Form, type FormInstance, Input } from "antd";
-import { createSchemaFieldRule } from "antd-zod";
-import { z } from "zod";
-
-import { type WorkflowNodeConfigForInspect } from "@/domain/workflow";
-import { useAntdForm } from "@/hooks";
-
-import { validDomainName, validIPv4Address, validPortNumber } from "@/utils/validators";
-
-type InspectNodeConfigFormFieldValues = Partial;
-
-export type InspectNodeConfigFormProps = {
- className?: string;
- style?: React.CSSProperties;
- disabled?: boolean;
- initialValues?: InspectNodeConfigFormFieldValues;
- onValuesChange?: (values: InspectNodeConfigFormFieldValues) => void;
-};
-
-export type InspectNodeConfigFormInstance = {
- getFieldsValue: () => ReturnType["getFieldsValue"]>;
- resetFields: FormInstance["resetFields"];
- validateFields: FormInstance["validateFields"];
-};
-
-const initFormModel = (): InspectNodeConfigFormFieldValues => {
- return {
- domain: "",
- port: "443",
- path: "",
- host: "",
- };
-};
-
-const InspectNodeConfigForm = forwardRef(
- ({ className, style, disabled, initialValues, onValuesChange }, ref) => {
- const { t } = useTranslation();
-
- const formSchema = z.object({
- host: z.string().refine((val) => validIPv4Address(val) || validDomainName(val), {
- message: t("workflow_node.inspect.form.host.placeholder"),
- }),
- domain: z.string().optional(),
- port: z.string().refine((val) => validPortNumber(val), {
- message: t("workflow_node.inspect.form.port.placeholder"),
- }),
- path: z.string().optional(),
- });
- const formRule = createSchemaFieldRule(formSchema);
- const { form: formInst, formProps } = useAntdForm({
- name: "workflowNodeInspectConfigForm",
- initialValues: initialValues ?? initFormModel(),
- });
-
- const handleFormChange = (_: unknown, values: z.infer) => {
- onValuesChange?.(values as InspectNodeConfigFormFieldValues);
- };
-
- useImperativeHandle(ref, () => {
- return {
- getFieldsValue: () => {
- return formInst.getFieldsValue(true);
- },
- resetFields: (fields) => {
- return formInst.resetFields(fields as (keyof InspectNodeConfigFormFieldValues)[]);
- },
- validateFields: (nameList, config) => {
- return formInst.validateFields(nameList, config);
- },
- } as InspectNodeConfigFormInstance;
- });
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-);
-
-export default memo(InspectNodeConfigForm);
diff --git a/ui/src/components/workflow/node/InspectNode.tsx b/ui/src/components/workflow/node/MonitorNode.tsx
similarity index 72%
rename from ui/src/components/workflow/node/InspectNode.tsx
rename to ui/src/components/workflow/node/MonitorNode.tsx
index 0d038894..68feb842 100644
--- a/ui/src/components/workflow/node/InspectNode.tsx
+++ b/ui/src/components/workflow/node/MonitorNode.tsx
@@ -3,43 +3,43 @@ import { useTranslation } from "react-i18next";
import { Flex, Typography } from "antd";
import { produce } from "immer";
-import { type WorkflowNodeConfigForInspect, WorkflowNodeType } from "@/domain/workflow";
+import { type WorkflowNodeConfigForMonitor, WorkflowNodeType } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
import SharedNode, { type SharedNodeProps } from "./_SharedNode";
-import InspectNodeConfigForm, { type InspectNodeConfigFormInstance } from "./InspectNodeConfigForm";
+import MonitorNodeConfigForm, { type MonitorNodeConfigFormInstance } from "./MonitorNodeConfigForm";
-export type InspectNodeProps = SharedNodeProps;
+export type MonitorNodeProps = SharedNodeProps;
-const InspectNode = ({ node, disabled }: InspectNodeProps) => {
- if (node.type !== WorkflowNodeType.Inspect) {
- console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`);
+const MonitorNode = ({ node, disabled }: MonitorNodeProps) => {
+ if (node.type !== WorkflowNodeType.Monitor) {
+ console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`);
}
const { t } = useTranslation();
const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"]));
- const formRef = useRef(null);
+ const formRef = useRef(null);
const [formPending, setFormPending] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
- const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForInspect;
+ const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor;
const wrappedEl = useMemo(() => {
- if (node.type !== WorkflowNodeType.Inspect) {
- console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Inspect}`);
+ if (node.type !== WorkflowNodeType.Monitor) {
+ console.warn(`[certimate] current workflow node type is not: ${WorkflowNodeType.Monitor}`);
}
if (!node.validated) {
return {t("workflow_node.action.configure_node")};
}
- const config = (node.config as WorkflowNodeConfigForInspect) ?? {};
+ const config = (node.config as WorkflowNodeConfigForMonitor) ?? {};
return (
- {config.host ?? ""}
+ {config.domain || config.host || ""}
);
}, [node]);
@@ -81,10 +81,10 @@ const InspectNode = ({ node, disabled }: InspectNodeProps) => {
onOpenChange={(open) => setDrawerOpen(open)}
getFormValues={() => formRef.current!.getFieldsValue()}
>
-
+
>
);
};
-export default memo(InspectNode);
+export default memo(MonitorNode);
diff --git a/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx
new file mode 100644
index 00000000..883124f9
--- /dev/null
+++ b/ui/src/components/workflow/node/MonitorNodeConfigForm.tsx
@@ -0,0 +1,115 @@
+import { forwardRef, memo, useImperativeHandle } from "react";
+import { useTranslation } from "react-i18next";
+import { Alert, Form, type FormInstance, Input, InputNumber } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { type WorkflowNodeConfigForMonitor } from "@/domain/workflow";
+import { useAntdForm } from "@/hooks";
+import { validDomainName, validIPv4Address, validIPv6Address, validPortNumber } from "@/utils/validators";
+
+type MonitorNodeConfigFormFieldValues = Partial;
+
+export type MonitorNodeConfigFormProps = {
+ className?: string;
+ style?: React.CSSProperties;
+ disabled?: boolean;
+ initialValues?: MonitorNodeConfigFormFieldValues;
+ onValuesChange?: (values: MonitorNodeConfigFormFieldValues) => void;
+};
+
+export type MonitorNodeConfigFormInstance = {
+ getFieldsValue: () => ReturnType["getFieldsValue"]>;
+ resetFields: FormInstance["resetFields"];
+ validateFields: FormInstance["validateFields"];
+};
+
+const initFormModel = (): MonitorNodeConfigFormFieldValues => {
+ return {
+ host: "",
+ port: 443,
+ requestPath: "/",
+ };
+};
+
+const MonitorNodeConfigForm = forwardRef(
+ ({ className, style, disabled, initialValues, onValuesChange }, ref) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ host: z.string().refine((v) => {
+ return validDomainName(v) || validIPv4Address(v) || validIPv6Address(v);
+ }, t("common.errmsg.host_invalid")),
+ port: z.preprocess(
+ (v) => Number(v),
+ z
+ .number()
+ .int(t("workflow_node.monitor.form.port.placeholder"))
+ .refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
+ ),
+ domain: z
+ .string()
+ .nullish()
+ .refine((v) => {
+ if (!v) return true;
+ return validDomainName(v);
+ }, t("common.errmsg.domain_invalid")),
+ requestPath: z.string().nullish(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+ const { form: formInst, formProps } = useAntdForm({
+ name: "workflowNodeMonitorConfigForm",
+ initialValues: initialValues ?? initFormModel(),
+ });
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values as MonitorNodeConfigFormFieldValues);
+ };
+
+ useImperativeHandle(ref, () => {
+ return {
+ getFieldsValue: () => {
+ return formInst.getFieldsValue(true);
+ },
+ resetFields: (fields) => {
+ return formInst.resetFields(fields as (keyof MonitorNodeConfigFormFieldValues)[]);
+ },
+ validateFields: (nameList, config) => {
+ return formInst.validateFields(nameList, config);
+ },
+ } as MonitorNodeConfigFormInstance;
+ });
+
+ return (
+
+ } />
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+export default memo(MonitorNodeConfigForm);
diff --git a/ui/src/components/workflow/node/NotifyNode.tsx b/ui/src/components/workflow/node/NotifyNode.tsx
index 16132539..da48552d 100644
--- a/ui/src/components/workflow/node/NotifyNode.tsx
+++ b/ui/src/components/workflow/node/NotifyNode.tsx
@@ -43,7 +43,7 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => {
const provider = notificationProvidersMap.get(config.provider);
return (
-
+
{t(channel?.name ?? provider?.name ?? " ")}
{config.subject ?? ""}
diff --git a/ui/src/components/workflow/node/UnknownNode.tsx b/ui/src/components/workflow/node/UnknownNode.tsx
new file mode 100644
index 00000000..7cb64aae
--- /dev/null
+++ b/ui/src/components/workflow/node/UnknownNode.tsx
@@ -0,0 +1,45 @@
+import { memo } from "react";
+import { CloseCircleOutlined as CloseCircleOutlinedIcon } from "@ant-design/icons";
+import { Alert, Button, Card } from "antd";
+
+import { useZustandShallowSelector } from "@/hooks";
+import { useWorkflowStore } from "@/stores/workflow";
+
+import { type SharedNodeProps } from "./_SharedNode";
+import AddNode from "./AddNode";
+
+export type MonitorNodeProps = SharedNodeProps;
+
+const UnknownNode = ({ node, disabled }: MonitorNodeProps) => {
+ const { removeNode } = useWorkflowStore(useZustandShallowSelector(["removeNode"]));
+
+ const handleClickRemove = () => {
+ removeNode(node.id);
+ };
+
+ return (
+ <>
+
+
+
+
+ INVALID NODE
+
+ PLEASE REMOVE
+
+ } variant="text" onClick={handleClickRemove} />
+
+ }
+ />
+
+
+
+
+ >
+ );
+};
+
+export default memo(UnknownNode);
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 594674f1..bb550691 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -553,6 +553,14 @@ export const deploymentProvidersMap: Map
[
type,
diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts
index 5a3e9821..4dea7f64 100644
--- a/ui/src/domain/workflow.ts
+++ b/ui/src/domain/workflow.ts
@@ -31,7 +31,7 @@ export enum WorkflowNodeType {
End = "end",
Apply = "apply",
Upload = "upload",
- Inspect = "inspect",
+ Monitor = "monitor",
Deploy = "deploy",
Notify = "notify",
Branch = "branch",
@@ -43,23 +43,25 @@ export enum WorkflowNodeType {
}
const workflowNodeTypeDefaultNames: Map = new Map([
- [WorkflowNodeType.Start, i18n.t("workflow_node.start.label")],
- [WorkflowNodeType.End, i18n.t("workflow_node.end.label")],
- [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.label")],
- [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.label")],
- [WorkflowNodeType.Inspect, i18n.t("workflow_node.inspect.label")],
- [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.label")],
- [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.label")],
- [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.label")],
- [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.label")],
- [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.label")],
- [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.label")],
- [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.label")],
- [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.title")],
+ [WorkflowNodeType.Start, i18n.t("workflow_node.start.default_name")],
+ [WorkflowNodeType.End, i18n.t("workflow_node.end.default_name")],
+ [WorkflowNodeType.Apply, i18n.t("workflow_node.apply.default_name")],
+ [WorkflowNodeType.Upload, i18n.t("workflow_node.upload.default_name")],
+ [WorkflowNodeType.Monitor, i18n.t("workflow_node.monitor.default_name")],
+ [WorkflowNodeType.Deploy, i18n.t("workflow_node.deploy.default_name")],
+ [WorkflowNodeType.Notify, i18n.t("workflow_node.notify.default_name")],
+ [WorkflowNodeType.Branch, i18n.t("workflow_node.branch.default_name")],
+ [WorkflowNodeType.Condition, i18n.t("workflow_node.condition.default_name")],
+ [WorkflowNodeType.ExecuteResultBranch, i18n.t("workflow_node.execute_result_branch.default_name")],
+ [WorkflowNodeType.ExecuteSuccess, i18n.t("workflow_node.execute_success.default_name")],
+ [WorkflowNodeType.ExecuteFailure, i18n.t("workflow_node.execute_failure.default_name")],
+ [WorkflowNodeType.Custom, i18n.t("workflow_node.custom.default_name")],
]);
const workflowNodeTypeDefaultInputs: Map = new Map([
[WorkflowNodeType.Apply, []],
+ [WorkflowNodeType.Upload, []],
+ [WorkflowNodeType.Monitor, []],
[
WorkflowNodeType.Deploy,
[
@@ -98,7 +100,7 @@ const workflowNodeTypeDefaultOutputs: Map =
],
],
[
- WorkflowNodeType.Inspect,
+ WorkflowNodeType.Monitor,
[
{
name: "certificate",
@@ -158,11 +160,11 @@ export type WorkflowNodeConfigForUpload = {
privateKey: string;
};
-export type WorkflowNodeConfigForInspect = {
- domain: string;
- port: string;
+export type WorkflowNodeConfigForMonitor = {
host: string;
- path: string;
+ port: number;
+ domain?: string;
+ requestPath?: string;
};
export type WorkflowNodeConfigForDeploy = {
@@ -351,7 +353,7 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {}
case WorkflowNodeType.Apply:
case WorkflowNodeType.Upload:
case WorkflowNodeType.Deploy:
- case WorkflowNodeType.Inspect:
+ case WorkflowNodeType.Monitor:
{
node.inputs = workflowNodeTypeDefaultInputs.get(nodeType);
node.outputs = workflowNodeTypeDefaultOutputs.get(nodeType);
diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json
index edd703f2..5b6c870c 100644
--- a/ui/src/i18n/locales/en/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json
@@ -10,6 +10,7 @@
"workflow_node.unsaved_changes.confirm": "You have unsaved changes. Do you really want to close the panel and drop those changes?",
"workflow_node.start.label": "Start",
+ "workflow_node.start.default_name": "Start",
"workflow_node.start.form.trigger.label": "Trigger",
"workflow_node.start.form.trigger.placeholder": "Please select trigger",
"workflow_node.start.form.trigger.tooltip": "Auto: Time triggered based on cron expression.
Manual: Manually triggered.",
@@ -22,7 +23,8 @@
"workflow_node.start.form.trigger_cron.extra": "Expected execution time for the last 5 times:",
"workflow_node.start.form.trigger_cron.guide": "Tips: If you have multiple workflows, it is recommended to set them to run at multiple times of the day instead of always running at specific times. Don't always set it to midnight every day to avoid spikes in traffic.
Reference links:
1. Let’s Encrypt rate limits
2. Why should my Let’s Encrypt (ACME) client run at a random time?",
- "workflow_node.apply.label": "Application",
+ "workflow_node.apply.label": "Obtain certificate",
+ "workflow_node.apply.default_name": "Application",
"workflow_node.apply.form.domains.label": "Domains",
"workflow_node.apply.form.domains.placeholder": "Please enter domains (separated by semicolons)",
"workflow_node.apply.form.domains.tooltip": "Wildcard domain: *.example.com",
@@ -97,7 +99,17 @@
"workflow_node.apply.form.skip_before_expiry_days.unit": "days",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.",
- "workflow_node.deploy.label": "Deployment",
+ "workflow_node.upload.label": "Upload certificate",
+ "workflow_node.upload.default_name": "Uploading",
+ "workflow_node.upload.form.domains.label": "Domains",
+ "workflow_node.upload.form.domains.placholder": "Please select certificate file",
+ "workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
+ "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
+ "workflow_node.upload.form.private_key.label": "Private key (PEM format)",
+ "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
+
+ "workflow_node.deploy.label": "Deploy certificate",
+ "workflow_node.deploy.default_name": "Deployment",
"workflow_node.deploy.form.provider.label": "Deploy target",
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
@@ -805,25 +817,20 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip",
- "workflow_node.upload.label": "Upload",
- "workflow_node.upload.form.domains.label": "Domains",
- "workflow_node.upload.form.domains.placholder": "Please select certificate file",
- "workflow_node.upload.form.certificate.label": "Certificate (PEM format)",
- "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
- "workflow_node.upload.form.private_key.label": "Private key (PEM format)",
- "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
+ "workflow_node.monitor.label": "Monitor certificate",
+ "workflow_node.monitor.default_name": "Monitoring",
+ "workflow_node.monitor.form.guide": "Tips: Certimate will send a HEAD request to the target address to obtain the certificate. Please ensure that the address is accessible through HTTPS protocol.",
+ "workflow_node.monitor.form.host.label": "Host",
+ "workflow_node.monitor.form.host.placeholder": "Please enter host",
+ "workflow_node.monitor.form.port.label": "Port",
+ "workflow_node.monitor.form.port.placeholder": "Please enter port",
+ "workflow_node.monitor.form.domain.label": "Domain (Optional)",
+ "workflow_node.monitor.form.domain.placeholder": "Please enter domain name",
+ "workflow_node.monitor.form.request_path.label": "Request path (Optional)",
+ "workflow_node.monitor.form.request_path.placeholder": "Please enter request path",
- "workflow_node.inspect.label": "Inspect certificate",
- "workflow_node.inspect.form.domain.label": "Domain",
- "workflow_node.inspect.form.domain.placeholder": "Please enter domain name",
- "workflow_node.inspect.form.port.label": "Port",
- "workflow_node.inspect.form.port.placeholder": "Please enter port",
- "workflow_node.inspect.form.host.label": "Host",
- "workflow_node.inspect.form.host.placeholder": "Please enter host",
- "workflow_node.inspect.form.path.label": "Path",
- "workflow_node.inspect.form.path.placeholder": "Please enter path",
-
- "workflow_node.notify.label": "Notification",
+ "workflow_node.notify.label": "Send notification",
+ "workflow_node.notify.default_name": "Notification",
"workflow_node.notify.form.subject.label": "Subject",
"workflow_node.notify.form.subject.placeholder": "Please enter subject",
"workflow_node.notify.form.message.label": "Message",
@@ -862,10 +869,13 @@
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
"workflow_node.end.label": "End",
+ "workflow_node.end.default_name": "End",
"workflow_node.branch.label": "Parallel branch",
+ "workflow_node.branch.default_name": "Parallel",
"workflow_node.condition.label": "Branch",
+ "workflow_node.condition.default_name": "Branch",
"workflow_node.condition.form.variable.placeholder": "Please select variable",
"workflow_node.condition.form.variable.errmsg": "Please select variable",
"workflow_node.condition.form.operator.errmsg": "Please select operator",
@@ -888,8 +898,11 @@
"workflow_node.condition.form.comparison.is": "Is",
"workflow_node.execute_result_branch.label": "Execution result branch",
+ "workflow_node.execute_result_branch.default_name": "Execution result branch",
"workflow_node.execute_success.label": "If the previous node succeeded ...",
+ "workflow_node.execute_success.default_name": "If the previous node succeeded ...",
- "workflow_node.execute_failure.label": "If the previous node failed ..."
+ "workflow_node.execute_failure.label": "If the previous node failed ...",
+ "workflow_node.execute_failure.default_name": "If the previous node failed ..."
}
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index fb51668f..5f28a950 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -28,7 +28,7 @@
"access.form.name.placeholder": "请输入授权名称",
"access.form.provider.label": "提供商",
"access.form.provider.placeholder": "请选择提供商",
- "access.form.provider.tooltip": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理您的域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。
该字段保存后不可修改。",
+ "access.form.provider.tooltip": "提供商分为两种类型:
【DNS 提供商】你的 DNS 托管方,通常等同于域名注册商,用于在申请证书时管理你的域名解析记录。
【主机提供商】你的服务器或云服务的托管方,用于部署签发的证书。
该字段保存后不可修改。",
"access.form.provider.search.placeholder": "搜索提供商……",
"access.form.certificate_authority.label": "证书颁发机构",
"access.form.certificate_authority.placeholder": "请选择证书颁发机构",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
index ef61e5a5..0d7ce68c 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
@@ -10,6 +10,7 @@
"workflow_node.unsaved_changes.confirm": "你有尚未保存的更改。确定要关闭面板吗?",
"workflow_node.start.label": "开始",
+ "workflow_node.start.default_name": "开始",
"workflow_node.start.form.trigger.label": "触发方式",
"workflow_node.start.form.trigger.placeholder": "请选择触发方式",
"workflow_node.start.form.trigger.tooltip": "自动触发:基于 Cron 表达式定时触发。
手动触发:手动点击执行触发。",
@@ -22,7 +23,8 @@
"workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:",
"workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。
参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?",
- "workflow_node.apply.label": "申请证书",
+ "workflow_node.apply.label": "申请签发证书",
+ "workflow_node.apply.default_name": "申请",
"workflow_node.apply.form.domains.label": "域名",
"workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)",
"workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com",
@@ -96,7 +98,17 @@
"workflow_node.apply.form.skip_before_expiry_days.unit": "天",
"workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。",
- "workflow_node.deploy.label": "部署证书",
+ "workflow_node.upload.label": "上传自有证书",
+ "workflow_node.upload.default_name": "上传",
+ "workflow_node.upload.form.domains.label": "域名",
+ "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
+ "workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)",
+ "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
+ "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
+ "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
+
+ "workflow_node.deploy.label": "部署证书到 ...",
+ "workflow_node.deploy.default_name": "部署",
"workflow_node.deploy.form.provider.label": "部署目标",
"workflow_node.deploy.form.provider.placeholder": "请选择部署目标",
"workflow_node.deploy.form.provider.search.placeholder": "搜索部署目标……",
@@ -804,25 +816,20 @@
"workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过",
"workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过",
- "workflow_node.upload.label": "上传证书",
- "workflow_node.upload.form.domains.label": "域名",
- "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示",
- "workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)",
- "workflow_node.upload.form.certificate.placeholder": "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----",
- "workflow_node.upload.form.private_key.label": "私钥文件(PEM 格式)",
- "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----",
-
- "workflow_node.inspect.label": "检查网站证书",
- "workflow_node.inspect.form.domain.label": "域名",
- "workflow_node.inspect.form.domain.placeholder": "请输入要检查的网站域名",
- "workflow_node.inspect.form.port.label": "端口号",
- "workflow_node.inspect.form.port.placeholder": "请输入要检查的端口号",
- "workflow_node.inspect.form.host.label": "Host",
- "workflow_node.inspect.form.host.placeholder": "请输入 Host",
- "workflow_node.inspect.form.path.label": "Path",
- "workflow_node.inspect.form.path.placeholder": "请输入 Path",
+ "workflow_node.monitor.label": "监控网站证书",
+ "workflow_node.monitor.default_name": "监控",
+ "workflow_node.monitor.form.guide": "小贴士:Certimate 将向目标地址发送一个 HEAD 请求来获取相应的域名证书,请确保该地址可通过 HTTPS 协议访问。",
+ "workflow_node.monitor.form.host.label": "主机地址",
+ "workflow_node.monitor.form.host.placeholder": "请输入主机地址(可以是域名或 IP)",
+ "workflow_node.monitor.form.port.label": "主机端口",
+ "workflow_node.monitor.form.port.placeholder": "请输入主机端口",
+ "workflow_node.monitor.form.domain.label": "域名(可选)",
+ "workflow_node.monitor.form.domain.placeholder": "请输入域名(仅当主机地址为 IP 时可选)",
+ "workflow_node.monitor.form.request_path.label": "请求路径(可选)",
+ "workflow_node.monitor.form.request_path.placeholder": "请输入请求路径",
"workflow_node.notify.label": "推送通知",
+ "workflow_node.notify.default_name": "通知",
"workflow_node.notify.form.subject.label": "通知主题",
"workflow_node.notify.form.subject.placeholder": "请输入通知主题",
"workflow_node.notify.form.message.label": "通知内容",
@@ -861,10 +868,13 @@
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
"workflow_node.end.label": "结束",
+ "workflow_node.end.default_name": "结束",
"workflow_node.branch.label": "并行分支",
+ "workflow_node.branch.default_name": "并行",
"workflow_node.condition.label": "分支",
+ "workflow_node.condition.default_name": "分支",
"workflow_node.condition.form.variable.placeholder": "选择变量",
"workflow_node.condition.form.variable.errmsg": "请选择变量",
"workflow_node.condition.form.operator.errmsg": "请选择操作符",
@@ -887,8 +897,11 @@
"workflow_node.condition.form.comparison.is": "为",
"workflow_node.execute_result_branch.label": "执行结果分支",
+ "workflow_node.execute_result_branch.default_name": "执行结果分支",
"workflow_node.execute_success.label": "若前序节点执行成功…",
+ "workflow_node.execute_success.default_name": "若前序节点执行成功…",
- "workflow_node.execute_failure.label": "若前序节点执行失败…"
+ "workflow_node.execute_failure.label": "若前序节点执行失败…",
+ "workflow_node.execute_failure.default_name": "若前序节点执行失败…"
}
diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx
index f815812e..a99dd588 100644
--- a/ui/src/pages/accesses/AccessList.tsx
+++ b/ui/src/pages/accesses/AccessList.tsx
@@ -56,7 +56,7 @@ const AccessList = () => {
render: (_, record) => {
return (
-
+
{t(accessProvidersMap.get(record.provider)?.name ?? "")}
);