diff --git a/.goreleaser.yml b/.goreleaser.yml index d65550fd..65ce8d48 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -30,8 +30,8 @@ builds: - goos: darwin goarch: arm -upx: - - enabled: true +# upx: +# - enabled: true release: draft: true diff --git a/README.md b/README.md index 1e1091dc..42771d0e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ 做个人产品或者在中小企业里负责运维的同学,会遇到要管理多个域名的情况,需要给域名申请证书。但是手动申请证书有以下缺点: -- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,犹其是你有多个域名需要维护的时候。 +- 😱 麻烦:申请证书并部署到服务的流程虽不复杂,但也挺麻烦的,尤其是你有多个域名需要维护的时候。 - 😭 易忘:另外当前免费证书的有效期只有 90 天,这就要求你定期的操作,增加了工作量的同时,你也很容易忘掉续期,从而导致网站访问不了。 Certimate 就是为了解决上述问题而产生的,它具有以下优势: diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index f1200094..d361cf83 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -53,35 +53,35 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply)) } - nodeConfig := config.Node.GetConfigForApply() + nodeCfg := config.Node.GetConfigForApply() options := &applicantProviderOptions{ - Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), - ContactEmail: nodeConfig.ContactEmail, - Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider), + Domains: sliceutil.Filter(strings.Split(nodeCfg.Domains, ";"), func(s string) bool { return s != "" }), + ContactEmail: nodeCfg.ContactEmail, + Provider: domain.ACMEDns01ProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, - CAProvider: domain.CAProviderType(nodeConfig.CAProvider), + ProviderServiceConfig: nodeCfg.ProviderConfig, + CAProvider: domain.CAProviderType(nodeCfg.CAProvider), CAProviderAccessConfig: make(map[string]any), - CAProviderServiceConfig: nodeConfig.CAProviderConfig, - KeyAlgorithm: nodeConfig.KeyAlgorithm, - Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), - DnsPropagationWait: nodeConfig.DnsPropagationWait, - DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, - DnsTTL: nodeConfig.DnsTTL, - DisableFollowCNAME: nodeConfig.DisableFollowCNAME, + CAProviderServiceConfig: nodeCfg.CAProviderConfig, + KeyAlgorithm: nodeCfg.KeyAlgorithm, + Nameservers: sliceutil.Filter(strings.Split(nodeCfg.Nameservers, ";"), func(s string) bool { return s != "" }), + DnsPropagationWait: nodeCfg.DnsPropagationWait, + DnsPropagationTimeout: nodeCfg.DnsPropagationTimeout, + DnsTTL: nodeCfg.DnsTTL, + DisableFollowCNAME: nodeCfg.DisableFollowCNAME, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + if nodeCfg.ProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } } - if nodeConfig.CAProviderAccessId != "" { - if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err) + if nodeCfg.CAProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeCfg.CAProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.CAProviderAccessId, err) } else { options.CAProviderAccessId = access.Id options.CAProviderAccessConfig = access.Config diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index e4a28746..c73120ba 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -29,18 +29,18 @@ func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy)) } - nodeConfig := config.Node.GetConfigForDeploy() + nodeCfg := config.Node.GetConfigForDeploy() options := &deployerProviderOptions{ - Provider: domain.DeploymentProviderType(nodeConfig.Provider), + Provider: domain.DeploymentProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, + ProviderServiceConfig: nodeCfg.ProviderConfig, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if nodeCfg.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId) if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 7f9bae91..e67c29e0 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -157,6 +157,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunALB.NewDeployer(&pAliyunALB.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceType: pAliyunALB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), @@ -169,6 +170,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunAPIGW.NewDeployer(&pAliyunAPIGW.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ServiceType: pAliyunAPIGW.ServiceType(maputil.GetString(options.ProviderServiceConfig, "serviceType")), GatewayId: maputil.GetString(options.ProviderServiceConfig, "gatewayId"), @@ -181,6 +183,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCAS.NewDeployer(&pAliyunCAS.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), }) return deployer, err @@ -189,6 +192,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCASDeploy.NewDeployer(&pAliyunCASDeploy.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceIds: sliceutil.Filter(strings.Split(maputil.GetString(options.ProviderServiceConfig, "resourceIds"), ";"), func(s string) bool { return s != "" }), ContactIds: sliceutil.Filter(strings.Split(maputil.GetString(options.ProviderServiceConfig, "contactIds"), ";"), func(s string) bool { return s != "" }), @@ -199,6 +203,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCDN.NewDeployer(&pAliyunCDN.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err @@ -207,6 +212,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunCLB.NewDeployer(&pAliyunCLB.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceType: pAliyunCLB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), @@ -219,6 +225,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunDCDN.NewDeployer(&pAliyunDCDN.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err @@ -227,6 +234,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunDDoS.NewDeployer(&pAliyunDDoS.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) @@ -245,6 +253,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunFC.NewDeployer(&pAliyunFC.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ServiceVersion: maputil.GetOrDefaultString(options.ProviderServiceConfig, "serviceVersion", "3.0"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), @@ -255,6 +264,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunGA.NewDeployer(&pAliyunGA.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, ResourceType: pAliyunGA.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), AcceleratorId: maputil.GetString(options.ProviderServiceConfig, "acceleratorId"), ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"), @@ -275,6 +285,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunNLB.NewDeployer(&pAliyunNLB.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ResourceType: pAliyunNLB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), @@ -286,6 +297,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunOSS.NewDeployer(&pAliyunOSS.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), Bucket: maputil.GetString(options.ProviderServiceConfig, "bucket"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), @@ -296,6 +308,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunVOD.NewDeployer(&pAliyunVOD.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) @@ -305,6 +318,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer deployer, err := pAliyunWAF.NewDeployer(&pAliyunWAF.DeployerConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, + ResourceGroupId: access.ResourceGroupId, Region: maputil.GetString(options.ProviderServiceConfig, "region"), ServiceVersion: maputil.GetOrDefaultString(options.ProviderServiceConfig, "serviceVersion", "3.0"), InstanceId: maputil.GetString(options.ProviderServiceConfig, "instanceId"), @@ -676,40 +690,44 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer switch options.Provider { case domain.DeploymentProviderTypeHuaweiCloudCDN: deployer, err := pHuaweiCloudCDN.NewDeployer(&pHuaweiCloudCDN.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderServiceConfig, "region"), - Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, + Region: maputil.GetString(options.ProviderServiceConfig, "region"), + Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err case domain.DeploymentProviderTypeHuaweiCloudELB: deployer, err := pHuaweiCloudELB.NewDeployer(&pHuaweiCloudELB.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderServiceConfig, "region"), - ResourceType: pHuaweiCloudELB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), - CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), - LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), - ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, + Region: maputil.GetString(options.ProviderServiceConfig, "region"), + ResourceType: pHuaweiCloudELB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), + CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), + LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"), + ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"), }) return deployer, err case domain.DeploymentProviderTypeHuaweiCloudSCM: deployer, err := pHuaweiCloudSCM.NewDeployer(&pHuaweiCloudSCM.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, }) return deployer, err case domain.DeploymentProviderTypeHuaweiCloudWAF: deployer, err := pHuaweiCloudWAF.NewDeployer(&pHuaweiCloudWAF.DeployerConfig{ - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderServiceConfig, "region"), - ResourceType: pHuaweiCloudWAF.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), - CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), - Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + EnterpriseProjectId: access.EnterpriseProjectId, + Region: maputil.GetString(options.ProviderServiceConfig, "region"), + ResourceType: pHuaweiCloudWAF.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")), + CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), + Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), }) return deployer, err diff --git a/internal/domain/access.go b/internal/domain/access.go index e31bb1a0..0ed9f0ee 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -38,6 +38,7 @@ type AccessConfigForACMEHttpReq struct { type AccessConfigForAliyun struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` + ResourceGroupId string `json:"resourceGroupId,omitempty"` } type AccessConfigForAWS struct { @@ -199,8 +200,9 @@ type AccessConfigForHetzner struct { } type AccessConfigForHuaweiCloud struct { - AccessKeyId string `json:"accessKeyId"` - SecretAccessKey string `json:"secretAccessKey"` + AccessKeyId string `json:"accessKeyId"` + SecretAccessKey string `json:"secretAccessKey"` + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } type AccessConfigForJDCloud struct { @@ -384,7 +386,7 @@ type AccessConfigForWeComBot struct { type AccessConfigForWestcn struct { Username string `json:"username"` - ApiPassword string `json:"password"` + ApiPassword string `json:"apiPassword"` } type AccessConfigForZeroSSL struct { diff --git a/internal/domain/expr/expr.go b/internal/domain/expr/expr.go new file mode 100644 index 00000000..755a876c --- /dev/null +++ b/internal/domain/expr/expr.go @@ -0,0 +1,630 @@ +package expr + +import ( + "encoding/json" + "fmt" + "strconv" +) + +type ( + ExprType string + ExprComparisonOperator string + ExprLogicalOperator string + ExprValueType string +) + +const ( + GreaterThan ExprComparisonOperator = "gt" + GreaterOrEqual ExprComparisonOperator = "gte" + LessThan ExprComparisonOperator = "lt" + LessOrEqual ExprComparisonOperator = "lte" + Equal ExprComparisonOperator = "eq" + NotEqual ExprComparisonOperator = "neq" + + And ExprLogicalOperator = "and" + Or ExprLogicalOperator = "or" + Not ExprLogicalOperator = "not" + + Number ExprValueType = "number" + String ExprValueType = "string" + Boolean ExprValueType = "boolean" + + ConstantExprType ExprType = "const" + VariantExprType ExprType = "var" + ComparisonExprType ExprType = "comparison" + LogicalExprType ExprType = "logical" + NotExprType ExprType = "not" +) + +type EvalResult struct { + Type ExprValueType + Value any +} + +func (e *EvalResult) GetFloat64() (float64, error) { + if e.Type != Number { + return 0, fmt.Errorf("type mismatch: %s", e.Type) + } + + stringValue, ok := e.Value.(string) + if !ok { + return 0, fmt.Errorf("value is not a string: %v", e.Value) + } + + floatValue, err := strconv.ParseFloat(stringValue, 64) + if err != nil { + return 0, fmt.Errorf("failed to parse float64: %v", err) + } + return floatValue, nil +} + +func (e *EvalResult) GetBool() (bool, error) { + if e.Type != Boolean { + return false, fmt.Errorf("type mismatch: %s", e.Type) + } + + strValue, ok := e.Value.(string) + if ok { + if strValue == "true" { + return true, nil + } else if strValue == "false" { + return false, nil + } + return false, fmt.Errorf("value is not a boolean: %v", e.Value) + } + + boolValue, ok := e.Value.(bool) + if !ok { + return false, fmt.Errorf("value is not a boolean: %v", e.Value) + } + + return boolValue, nil +} + +func (e *EvalResult) GreaterThan(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) > other.Value.(string), + }, nil + + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left > right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) GreaterOrEqual(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) >= other.Value.(string), + }, nil + + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left >= right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) LessThan(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) < other.Value.(string), + }, nil + + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left < right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) LessOrEqual(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) <= other.Value.(string), + }, nil + + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left <= right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) Equal(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) == other.Value.(string), + }, nil + + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left == right, + }, nil + + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left == right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) NotEqual(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case String: + return &EvalResult{ + Type: Boolean, + Value: e.Value.(string) != other.Value.(string), + }, nil + + case Number: + left, err := e.GetFloat64() + if err != nil { + return nil, err + } + + right, err := other.GetFloat64() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left != right, + }, nil + + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left != right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) And(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left && right, + }, nil + + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) Or(other *EvalResult) (*EvalResult, error) { + if e.Type != other.Type { + return nil, fmt.Errorf("type mismatch: %s vs %s", e.Type, other.Type) + } + + switch e.Type { + case Boolean: + left, err := e.GetBool() + if err != nil { + return nil, err + } + + right, err := other.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: left || right, + }, nil + default: + return nil, fmt.Errorf("unsupported value type: %s", e.Type) + } +} + +func (e *EvalResult) Not() (*EvalResult, error) { + if e.Type != Boolean { + return nil, fmt.Errorf("type mismatch: %s", e.Type) + } + + boolValue, err := e.GetBool() + if err != nil { + return nil, err + } + + return &EvalResult{ + Type: Boolean, + Value: !boolValue, + }, nil +} + +type Expr interface { + GetType() ExprType + Eval(variables map[string]map[string]any) (*EvalResult, error) +} + +type ExprValueSelector struct { + Id string `json:"id"` + Name string `json:"name"` + Type ExprValueType `json:"type"` +} + +type ConstantExpr struct { + Type ExprType `json:"type"` + Value string `json:"value"` + ValueType ExprValueType `json:"valueType"` +} + +func (c ConstantExpr) GetType() ExprType { return c.Type } + +func (c ConstantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { + return &EvalResult{ + Type: c.ValueType, + Value: c.Value, + }, nil +} + +type VariantExpr struct { + Type ExprType `json:"type"` + Selector ExprValueSelector `json:"selector"` +} + +func (v VariantExpr) GetType() ExprType { return v.Type } + +func (v VariantExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { + if v.Selector.Id == "" { + return nil, fmt.Errorf("node id is empty") + } + if v.Selector.Name == "" { + return nil, fmt.Errorf("name is empty") + } + + if _, ok := variables[v.Selector.Id]; !ok { + return nil, fmt.Errorf("node %s not found", v.Selector.Id) + } + + if _, ok := variables[v.Selector.Id][v.Selector.Name]; !ok { + return nil, fmt.Errorf("variable %s not found in node %s", v.Selector.Name, v.Selector.Id) + } + return &EvalResult{ + Type: v.Selector.Type, + Value: variables[v.Selector.Id][v.Selector.Name], + }, nil +} + +type ComparisonExpr struct { + Type ExprType `json:"type"` // compare + Operator ExprComparisonOperator `json:"operator"` + Left Expr `json:"left"` + Right Expr `json:"right"` +} + +func (c ComparisonExpr) GetType() ExprType { return c.Type } + +func (c ComparisonExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { + left, err := c.Left.Eval(variables) + if err != nil { + return nil, err + } + right, err := c.Right.Eval(variables) + if err != nil { + return nil, err + } + + switch c.Operator { + case GreaterThan: + return left.GreaterThan(right) + case LessThan: + return left.LessThan(right) + case GreaterOrEqual: + return left.GreaterOrEqual(right) + case LessOrEqual: + return left.LessOrEqual(right) + case Equal: + return left.Equal(right) + case NotEqual: + return left.NotEqual(right) + default: + return nil, fmt.Errorf("unknown expression operator: %s", c.Operator) + } +} + +type LogicalExpr struct { + Type ExprType `json:"type"` // logical + Operator ExprLogicalOperator `json:"operator"` + Left Expr `json:"left"` + Right Expr `json:"right"` +} + +func (l LogicalExpr) GetType() ExprType { return l.Type } + +func (l LogicalExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { + left, err := l.Left.Eval(variables) + if err != nil { + return nil, err + } + right, err := l.Right.Eval(variables) + if err != nil { + return nil, err + } + + switch l.Operator { + case And: + return left.And(right) + case Or: + return left.Or(right) + default: + return nil, fmt.Errorf("unknown expression operator: %s", l.Operator) + } +} + +type NotExpr struct { + Type ExprType `json:"type"` // not + Expr Expr `json:"expr"` +} + +func (n NotExpr) GetType() ExprType { return n.Type } + +func (n NotExpr) Eval(variables map[string]map[string]any) (*EvalResult, error) { + inner, err := n.Expr.Eval(variables) + if err != nil { + return nil, err + } + return inner.Not() +} + +type rawExpr struct { + Type ExprType `json:"type"` +} + +func MarshalExpr(e Expr) ([]byte, error) { + return json.Marshal(e) +} + +func UnmarshalExpr(data []byte) (Expr, error) { + var typ rawExpr + if err := json.Unmarshal(data, &typ); err != nil { + return nil, err + } + + switch typ.Type { + case ConstantExprType: + var e ConstantExpr + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e, nil + case VariantExprType: + var e VariantExpr + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e, nil + case ComparisonExprType: + var e ComparisonExprRaw + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e.ToComparisonExpr() + case LogicalExprType: + var e LogicalExprRaw + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e.ToLogicalExpr() + case NotExprType: + var e NotExprRaw + if err := json.Unmarshal(data, &e); err != nil { + return nil, err + } + return e.ToNotExpr() + default: + return nil, fmt.Errorf("unknown expression type: %s", typ.Type) + } +} + +type ComparisonExprRaw struct { + Type ExprType `json:"type"` + Operator ExprComparisonOperator `json:"operator"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` +} + +func (r ComparisonExprRaw) ToComparisonExpr() (ComparisonExpr, error) { + leftExpr, err := UnmarshalExpr(r.Left) + if err != nil { + return ComparisonExpr{}, err + } + rightExpr, err := UnmarshalExpr(r.Right) + if err != nil { + return ComparisonExpr{}, err + } + return ComparisonExpr{ + Type: r.Type, + Operator: r.Operator, + Left: leftExpr, + Right: rightExpr, + }, nil +} + +type LogicalExprRaw struct { + Type ExprType `json:"type"` + Operator ExprLogicalOperator `json:"operator"` + Left json.RawMessage `json:"left"` + Right json.RawMessage `json:"right"` +} + +func (r LogicalExprRaw) ToLogicalExpr() (LogicalExpr, error) { + left, err := UnmarshalExpr(r.Left) + if err != nil { + return LogicalExpr{}, err + } + right, err := UnmarshalExpr(r.Right) + if err != nil { + return LogicalExpr{}, err + } + return LogicalExpr{ + Type: r.Type, + Operator: r.Operator, + Left: left, + Right: right, + }, nil +} + +type NotExprRaw struct { + Type ExprType `json:"type"` + Expr json.RawMessage `json:"expr"` +} + +func (r NotExprRaw) ToNotExpr() (NotExpr, error) { + inner, err := UnmarshalExpr(r.Expr) + if err != nil { + return NotExpr{}, err + } + return NotExpr{ + Type: r.Type, + Expr: inner, + }, nil +} diff --git a/internal/domain/expr/expr_test.go b/internal/domain/expr/expr_test.go new file mode 100644 index 00000000..fb76d98c --- /dev/null +++ b/internal/domain/expr/expr_test.go @@ -0,0 +1,127 @@ +package expr + +import ( + "testing" +) + +func TestLogicalEval(t *testing.T) { + // 测试逻辑表达式 and + logicalExpr := LogicalExpr{ + Left: ConstantExpr{ + Type: "const", + Value: "true", + ValueType: "boolean", + }, + Operator: And, + Right: ConstantExpr{ + Type: "const", + Value: "true", + ValueType: "boolean", + }, + } + result, err := logicalExpr.Eval(nil) + if err != nil { + t.Errorf("failed to evaluate logical expression: %v", err) + } + if result.Value != true { + t.Errorf("expected true, got %v", result) + } + + // 测试逻辑表达式 or + orExpr := LogicalExpr{ + Left: ConstantExpr{ + Type: "const", + Value: "true", + ValueType: "boolean", + }, + Operator: Or, + Right: ConstantExpr{ + Type: "const", + Value: "true", + ValueType: "boolean", + }, + } + result, err = orExpr.Eval(nil) + if err != nil { + t.Errorf("failed to evaluate logical expression: %v", err) + } + if result.Value != true { + t.Errorf("expected true, got %v", result) + } +} + +func TestUnmarshalExpr(t *testing.T) { + type args struct { + data []byte + } + tests := []struct { + name string + args args + want Expr + wantErr bool + }{ + { + name: "test1", + args: args{ + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := UnmarshalExpr(tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("UnmarshalExpr() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got == nil { + t.Errorf("UnmarshalExpr() got = nil, want %v", tt.want) + return + } + }) + } +} + +func TestExpr_Eval(t *testing.T) { + type args struct { + variables map[string]map[string]any + data []byte + } + tests := []struct { + name string + args args + want *EvalResult + wantErr bool + }{ + { + name: "test1", + args: args{ + variables: map[string]map[string]any{ + "ODnYSOXB6HQP2_vz6JcZE": { + "certificate.validity": true, + "certificate.daysLeft": 2, + }, + }, + data: []byte(`{"left":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.validity","type":"boolean"},"type":"var"},"operator":"is","right":{"type":"const","value":true,"valueType":"boolean"},"type":"comparison"},"operator":"and","right":{"left":{"selector":{"id":"ODnYSOXB6HQP2_vz6JcZE","name":"certificate.daysLeft","type":"number"},"type":"var"},"operator":"eq","right":{"type":"const","value":2,"valueType":"number"},"type":"comparison"},"type":"logical"}`), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := UnmarshalExpr(tt.args.data) + if err != nil { + t.Errorf("UnmarshalExpr() error = %v", err) + return + } + got, err := c.Eval(tt.args.variables) + t.Log("got:", got) + if (err != nil) != tt.wantErr { + t.Errorf("ConstExpr.Eval() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got.Value != true { + t.Errorf("ConstExpr.Eval() got = %v, want %v", got.Value, true) + } + }) + } +} diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 6a96dd81..a2e049e2 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -1,8 +1,10 @@ package domain import ( + "encoding/json" "time" + "github.com/usual2970/certimate/internal/domain/expr" maputil "github.com/usual2970/certimate/internal/pkg/utils/map" ) @@ -30,6 +32,7 @@ const ( WorkflowNodeTypeEnd = WorkflowNodeType("end") WorkflowNodeTypeApply = WorkflowNodeType("apply") WorkflowNodeTypeUpload = WorkflowNodeType("upload") + WorkflowNodeTypeMonitor = WorkflowNodeType("monitor") WorkflowNodeTypeDeploy = WorkflowNodeType("deploy") WorkflowNodeTypeNotify = WorkflowNodeType("notify") WorkflowNodeTypeBranch = WorkflowNodeType("branch") @@ -68,23 +71,30 @@ type WorkflowNodeConfigForApply struct { Provider string `json:"provider"` // DNS 提供商 ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 - CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值将使用全局配置) + CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置) CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 KeyAlgorithm string `json:"keyAlgorithm"` // 证书算法 Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数 - DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间(零值取决于提供商的默认值) - DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL(零值取决于提供商的默认值) + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播检查超时时间(零值时使用提供商的默认值) + DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS 解析记录 TTL(零值时使用提供商的默认值) DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随 DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI - SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) + SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值时默认值 30) } 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 { @@ -104,6 +114,10 @@ type WorkflowNodeConfigForNotify struct { Message string `json:"message"` // 通知内容 } +type WorkflowNodeConfigForCondition struct { + Expression expr.Expr `json:"expression"` // 条件表达式 +} + func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { return WorkflowNodeConfigForApply{ Domains: maputil.GetString(n.Config, "domains"), @@ -133,6 +147,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"), @@ -154,6 +178,23 @@ func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { } } +func (n *WorkflowNode) GetConfigForCondition() WorkflowNodeConfigForCondition { + expression := n.Config["expression"] + if expression == nil { + return WorkflowNodeConfigForCondition{} + } + + exprRaw, _ := json.Marshal(expression) + expr, err := expr.UnmarshalExpr([]byte(exprRaw)) + if err != nil { + return WorkflowNodeConfigForCondition{} + } + + return WorkflowNodeConfigForCondition{ + Expression: expr, + } +} + type WorkflowNodeIO struct { Label string `json:"label"` Name string `json:"name"` @@ -163,9 +204,6 @@ type WorkflowNodeIO struct { ValueSelector WorkflowNodeIOValueSelector `json:"valueSelector"` } -type WorkflowNodeIOValueSelector struct { - Id string `json:"id"` - Name string `json:"name"` -} +type WorkflowNodeIOValueSelector = expr.ExprValueSelector const WorkflowNodeIONameCertificate string = "certificate" diff --git a/internal/notify/notifier.go b/internal/notify/notifier.go index ee3fbd2f..5e957841 100644 --- a/internal/notify/notifier.go +++ b/internal/notify/notifier.go @@ -29,18 +29,18 @@ func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify)) } - nodeConfig := config.Node.GetConfigForNotify() + nodeCfg := config.Node.GetConfigForNotify() options := ¬ifierProviderOptions{ - Provider: domain.NotificationProviderType(nodeConfig.Provider), + Provider: domain.NotificationProviderType(nodeCfg.Provider), ProviderAccessConfig: make(map[string]any), - ProviderServiceConfig: nodeConfig.ProviderConfig, + ProviderServiceConfig: nodeCfg.ProviderConfig, } accessRepo := repository.NewAccessRepository() - if nodeConfig.ProviderAccessId != "" { - access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId) + if nodeCfg.ProviderAccessId != "" { + access, err := accessRepo.GetById(context.Background(), nodeCfg.ProviderAccessId) if err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeCfg.ProviderAccessId, err) } else { options.ProviderAccessConfig = access.Config } diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go index b34516d4..7c87536c 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns/powerdns.go @@ -29,6 +29,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, providerConfig.APIKey = config.ApiKey if config.AllowInsecureConnections { providerConfig.HTTPClient.Transport = &http.Transport{ + Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, }, diff --git a/internal/pkg/core/deployer/deployer.go b/internal/pkg/core/deployer/deployer.go index 67ce7ef7..85a4e156 100644 --- a/internal/pkg/core/deployer/deployer.go +++ b/internal/pkg/core/deployer/deployer.go @@ -20,7 +20,7 @@ type Deployer interface { // 出参: // - res:部署结果。 // - err: 错误。 - Deploy(ctx context.Context, certPEM string, privkeyPEM string) (res *DeployResult, err error) + Deploy(ctx context.Context, certPEM string, privkeyPEM string) (_res *DeployResult, _err error) } // 表示证书部署结果的数据结构。 diff --git a/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go b/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go index cdeb8af5..b1df4153 100644 --- a/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go +++ b/internal/pkg/core/deployer/providers/1panel-console/1panel_console.go @@ -53,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go index 07d124a3..0f721c3f 100644 --- a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go +++ b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go index 35b4997c..0f22091a 100644 --- a/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go +++ b/internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go @@ -25,6 +25,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 @@ -64,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk clients: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -79,7 +81,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -423,7 +425,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients // 接入点一览 https://api.aliyun.com/product/Alb var albEndpoint string switch region { - case "cn-hangzhou-finance": + case "", "cn-hangzhou-finance": albEndpoint = "alb.cn-hangzhou.aliyuncs.com" default: albEndpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region) @@ -463,7 +465,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients }, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 ALB 服务的 @@ -479,6 +481,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go index d74c7c27..12f0f3d7 100644 --- a/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go +++ b/internal/pkg/core/deployer/providers/aliyun-apigw/aliyun_apigw.go @@ -16,6 +16,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type DeployerConfig struct { @@ -23,6 +24,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务类型。 @@ -61,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk clients: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -76,7 +79,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -149,10 +152,11 @@ func (d *DeployerProvider) deployToCloudNative(ctx context.Context, certPEM stri } listDomainsReq := &aliapig.ListDomainsRequest{ - GatewayId: tea.String(d.config.GatewayId), - NameLike: tea.String(d.config.Domain), - PageNumber: tea.Int32(listDomainsPageNumber), - PageSize: tea.Int32(listDomainsPageSize), + ResourceGroupId: typeutil.ToPtrOrZeroNil(d.config.ResourceGroupId), + GatewayId: tea.String(d.config.GatewayId), + NameLike: tea.String(d.config.Domain), + PageNumber: tea.Int32(listDomainsPageNumber), + PageSize: tea.Int32(listDomainsPageSize), } listDomainsResp, err := d.sdkClients.CloudNativeAPIGateway.ListDomains(listDomainsReq) d.logger.Debug("sdk request 'apig.ListDomains'", slog.Any("request", listDomainsReq), slog.Any("response", listDomainsResp)) @@ -223,7 +227,7 @@ func (d *DeployerProvider) deployToCloudNative(ctx context.Context, certPEM stri func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients, error) { // 接入点一览 https://api.aliyun.com/product/APIG - cloudNativeAPIGEndpoint := fmt.Sprintf("apig.%s.aliyuncs.com", region) + cloudNativeAPIGEndpoint := strings.ReplaceAll(fmt.Sprintf("apig.%s.aliyuncs.com", region), "..", ".") cloudNativeAPIGConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), @@ -235,7 +239,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients } // 接入点一览 https://api.aliyun.com/product/CloudAPI - traditionalAPIGEndpoint := fmt.Sprintf("apigateway.%s.aliyuncs.com", region) + traditionalAPIGEndpoint := strings.ReplaceAll(fmt.Sprintf("apigateway.%s.aliyuncs.com", region), "..", ".") traditionalAPIGConfig := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), @@ -252,7 +256,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients }, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 APIGateway 服务的 @@ -268,6 +272,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go index 077dea5c..cfcdaa18 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas-deploy/aliyun_cas_deploy.go @@ -22,12 +22,14 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 阿里云云产品资源 ID 数组。 ResourceIds []string `json:"resourceIds"` // 阿里云云联系人 ID 数组。 - // 零值时默认使用账号下第一个联系人。 + // 零值时使用账号下第一个联系人。 ContactIds []string `json:"contactIds"` } @@ -50,11 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - AccessKeySecret: config.AccessKeySecret, - Region: config.Region, - }) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -69,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -94,9 +92,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE if len(contactIds) == 0 { // 获取联系人列表 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-listcontact - listContactReq := &alicas.ListContactRequest{} - listContactReq.ShowSize = tea.Int32(1) - listContactReq.CurrentPage = tea.Int32(1) + listContactReq := &alicas.ListContactRequest{ + ShowSize: tea.Int32(1), + CurrentPage: tea.Int32(1), + } listContactResp, err := d.sdkClient.ListContact(listContactReq) d.logger.Debug("sdk request 'cas.ListContact'", slog.Any("request", listContactReq), slog.Any("response", listContactResp)) if err != nil { @@ -157,14 +156,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE } func createSdkClient(accessKeyId, accessKeySecret, region string) (*alicas.Client, error) { - if region == "" { - region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 - } - // 接入点一览 https://api.aliyun.com/product/cas var endpoint string switch region { - case "cn-hangzhou": + case "", "cn-hangzhou": endpoint = "cas.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) @@ -183,3 +178,25 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alicas.Clien return client, nil } + +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { + casRegion := region + if casRegion != "" { + // 阿里云 CAS 服务接入点是独立于其他服务的 + // 国内版固定接入点:华东一杭州 + // 国际版固定接入点:亚太东南一新加坡 + if !strings.HasPrefix(casRegion, "cn-") { + casRegion = "ap-southeast-1" + } else { + casRegion = "cn-hangzhou" + } + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: accessKeyId, + AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, + Region: casRegion, + }) + return uploader, err +} diff --git a/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go index 56681e57..f1cc8811 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/deployer/providers/aliyun-cas/aliyun_cas.go @@ -15,6 +15,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } @@ -35,6 +37,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, + ResourceGroupId: config.ResourceGroupId, Region: config.Region, }) if err != nil { @@ -50,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go b/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go index ce5f9fd8..96dd211f 100644 --- a/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go +++ b/internal/pkg/core/deployer/providers/aliyun-cdn/aliyun_cdn.go @@ -19,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } @@ -50,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go index 34c3a49e..1722e4fd 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go @@ -20,6 +20,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 @@ -54,7 +56,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -69,7 +71,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -283,7 +285,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien // 接入点一览 https://api.aliyun.com/product/Slb var endpoint string switch region { - case + case "", "cn-hangzhou", "cn-hangzhou-finance", "cn-shanghai-finance-1", @@ -307,10 +309,11 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: region, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go b/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go index 4eb411fd..a5109163 100644 --- a/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go +++ b/internal/pkg/core/deployer/providers/aliyun-dcdn/aliyun_dcdn.go @@ -19,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } @@ -50,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go index d1cb5b61..f0bd3476 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go +++ b/internal/pkg/core/deployer/providers/aliyun-ddos/aliyun_ddos.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 网站域名(支持泛域名)。 @@ -47,7 +49,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -62,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -104,7 +106,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliddos.Clie config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String(fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region)), + Endpoint: tea.String(strings.ReplaceAll(fmt.Sprintf("ddoscoo.%s.aliyuncs.com", region), "..", ".")), } client, err := aliddos.NewClient(config) @@ -115,7 +117,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliddos.Clie return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 Anti-DDoS 服务的 @@ -131,6 +133,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go index 1f29756f..e4906fb5 100644 --- a/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go +++ b/internal/pkg/core/deployer/providers/aliyun-esa/aliyun_esa.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 阿里云 ESA 站点 ID。 @@ -47,7 +49,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -62,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -105,7 +107,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliesa.Clien config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String(fmt.Sprintf("esa.%s.aliyuncs.com", region)), + Endpoint: tea.String(strings.ReplaceAll(fmt.Sprintf("esa.%s.aliyuncs.com", region), "..", ".")), } client, err := aliesa.NewClient(config) @@ -116,7 +118,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliesa.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 ESA 服务的 @@ -132,6 +134,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go index 426aa3a6..1ff046c3 100644 --- a/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go +++ b/internal/pkg/core/deployer/providers/aliyun-fc/aliyun_fc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" @@ -19,6 +20,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务版本。 @@ -60,7 +63,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -108,6 +111,9 @@ func (d *DeployerProvider) deployToFC3(ctx context.Context, certPEM string, priv TlsConfig: getCustomDomainResp.Body.TlsConfig, }, } + if tea.StringValue(updateCustomDomainReq.Body.Protocol) == "HTTP" { + updateCustomDomainReq.Body.Protocol = tea.String("HTTP,HTTPS") + } updateCustomDomainResp, err := d.sdkClients.FC3.UpdateCustomDomain(tea.String(d.config.Domain), updateCustomDomainReq) d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { @@ -137,6 +143,9 @@ func (d *DeployerProvider) deployToFC2(ctx context.Context, certPEM string, priv Protocol: getCustomDomainResp.Body.Protocol, TlsConfig: getCustomDomainResp.Body.TlsConfig, } + if tea.StringValue(updateCustomDomainReq.Protocol) == "HTTP" { + updateCustomDomainReq.Protocol = tea.String("HTTP,HTTPS") + } updateCustomDomainResp, err := d.sdkClients.FC2.UpdateCustomDomain(tea.String(d.config.Domain), updateCustomDomainReq) d.logger.Debug("sdk request 'fc.UpdateCustomDomain'", slog.Any("request", updateCustomDomainReq), slog.Any("response", updateCustomDomainResp)) if err != nil { @@ -150,6 +159,8 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients // 接入点一览 https://api.aliyun.com/product/FC-Open var fc2Endpoint string switch region { + case "": + fc2Endpoint = "fc.aliyuncs.com" case "cn-hangzhou-finance": fc2Endpoint = fmt.Sprintf("%s.fc.aliyuncs.com", region) default: @@ -167,7 +178,7 @@ func createSdkClients(accessKeyId, accessKeySecret, region string) (*wSdkClients } // 接入点一览 https://api.aliyun.com/product/FC-Open - fc3Endpoint := fmt.Sprintf("fcv3.%s.aliyuncs.com", region) + fc3Endpoint := strings.ReplaceAll(fmt.Sprintf("fcv3.%s.aliyuncs.com", region), "..", ".") fc3Config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), diff --git a/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go b/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go index f69660a8..6ea13077 100644 --- a/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go +++ b/internal/pkg/core/deployer/providers/aliyun-ga/aliyun_ga.go @@ -22,6 +22,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 部署资源类型。 ResourceType ResourceType `json:"resourceType"` // 全球加速实例 ID。 @@ -53,7 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -68,7 +70,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -312,10 +314,11 @@ func createSdkClient(accessKeyId, accessKeySecret string) (*aliga.Client, error) return client, nil } -func createSslUploader(accessKeyId, accessKeySecret string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId string) (uploader.Uploader, error) { uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: "cn-hangzhou", }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go b/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go index 354c9601..0fab9485 100644 --- a/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go +++ b/internal/pkg/core/deployer/providers/aliyun-live/aliyun_live.go @@ -19,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 直播流域名(支持泛域名)。 @@ -52,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -86,7 +88,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alilive.Clie // 接入点一览 https://api.aliyun.com/product/live var endpoint string switch region { - case + case "", "cn-qingdao", "cn-beijing", "cn-shanghai", diff --git a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go index 58015f3d..dd83f514 100644 --- a/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go +++ b/internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go @@ -21,6 +21,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 部署资源类型。 @@ -52,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -67,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -224,12 +226,7 @@ func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudL func createSdkClient(accessKeyId, accessKeySecret, region string) (*alinlb.Client, error) { // 接入点一览 https://api.aliyun.com/product/Nlb - var endpoint string - switch region { - default: - endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region) - } - + endpoint := strings.ReplaceAll(fmt.Sprintf("nlb.%s.aliyuncs.com", region), "..", ".") config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), @@ -244,7 +241,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alinlb.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 NLB 服务的 @@ -260,6 +257,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go b/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go index 474fe5b3..6a698cf0 100644 --- a/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go +++ b/internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go @@ -16,6 +16,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 存储桶名。 @@ -51,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go b/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go index 48e52c26..ab02fa89 100644 --- a/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go +++ b/internal/pkg/core/deployer/providers/aliyun-vod/aliyun_vod.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "strings" "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" @@ -18,6 +19,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 点播加速域名(不支持泛域名)。 @@ -51,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -80,8 +83,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE func createSdkClient(accessKeyId, accessKeySecret, region string) (*alivod.Client, error) { // 接入点一览 https://api.aliyun.com/product/vod - endpoint := fmt.Sprintf("vod.%s.aliyuncs.com", region) - + endpoint := strings.ReplaceAll(fmt.Sprintf("vod.%s.aliyuncs.com", region), "..", ".") config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), diff --git a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go index 26dbd008..cb3c70e9 100644 --- a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go +++ b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go @@ -15,6 +15,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type DeployerConfig struct { @@ -22,6 +23,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` // 服务版本。 @@ -51,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { return nil, fmt.Errorf("failed to create sdk client: %w", err) } - uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.Region) + uploader, err := createSslUploader(config.AccessKeyId, config.AccessKeySecret, config.ResourceGroupId, config.Region) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) } @@ -66,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -107,8 +110,9 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPEM string, pri // 查询默认 SSL/TLS 设置 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-describedefaulthttps describeDefaultHttpsReq := &aliwaf.DescribeDefaultHttpsRequest{ - InstanceId: tea.String(d.config.InstanceId), - RegionId: tea.String(d.config.Region), + ResourceManagerResourceGroupId: typeutil.ToPtrOrZeroNil(d.config.ResourceGroupId), + InstanceId: tea.String(d.config.InstanceId), + RegionId: tea.String(d.config.Region), } describeDefaultHttpsResp, err := d.sdkClient.DescribeDefaultHttps(describeDefaultHttpsReq) d.logger.Debug("sdk request 'waf.DescribeDefaultHttps'", slog.Any("request", describeDefaultHttpsReq), slog.Any("response", describeDefaultHttpsResp)) @@ -119,11 +123,12 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPEM string, pri // 修改默认 SSL/TLS 设置 // REF: https://help.aliyun.com/zh/waf/web-application-firewall-3-0/developer-reference/api-waf-openapi-2021-10-01-modifydefaulthttps modifyDefaultHttpsReq := &aliwaf.ModifyDefaultHttpsRequest{ - InstanceId: tea.String(d.config.InstanceId), - RegionId: tea.String(d.config.Region), - CertId: tea.String(upres.CertId), - TLSVersion: tea.String("tlsv1"), - EnableTLSv3: tea.Bool(false), + ResourceManagerResourceGroupId: typeutil.ToPtrOrZeroNil(d.config.ResourceGroupId), + InstanceId: tea.String(d.config.InstanceId), + RegionId: tea.String(d.config.Region), + CertId: tea.String(upres.CertId), + TLSVersion: tea.String("tlsv1"), + EnableTLSv3: tea.Bool(false), } if describeDefaultHttpsResp.Body != nil && describeDefaultHttpsResp.Body.DefaultHttps != nil { modifyDefaultHttpsReq.TLSVersion = describeDefaultHttpsResp.Body.DefaultHttps.TLSVersion @@ -172,10 +177,11 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPEM string, pri func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliwaf.Client, error) { // 接入点一览:https://api.aliyun.com/product/waf-openapi + endpoint := strings.ReplaceAll(fmt.Sprintf("wafopenapi.%s.aliyuncs.com", region), "..", ".") config := &aliopen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), - Endpoint: tea.String(fmt.Sprintf("wafopenapi.%s.aliyuncs.com", region)), + Endpoint: tea.String(endpoint), } client, err := aliwaf.NewClient(config) @@ -186,7 +192,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliwaf.Clien return client, nil } -func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Uploader, error) { +func createSslUploader(accessKeyId, accessKeySecret, resourceGroupId, region string) (uploader.Uploader, error) { casRegion := region if casRegion != "" { // 阿里云 CAS 服务接入点是独立于 WAF 服务的 @@ -202,6 +208,7 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ AccessKeyId: accessKeyId, AccessKeySecret: accessKeySecret, + ResourceGroupId: resourceGroupId, Region: casRegion, }) return uploader, err diff --git a/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go b/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go index a9e90b60..0c9c5d57 100644 --- a/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/deployer/providers/aws-acm/aws_acm.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go b/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go index 0808a4fb..7ec17044 100644 --- a/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go +++ b/internal/pkg/core/deployer/providers/aws-cloudfront/aws_cloudfront.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go index b8f8df99..1331bbf6 100644 --- a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go @@ -76,7 +76,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go index 3318135f..3bb965ca 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go index a16ea102..0490b9ad 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go b/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go index 7ef78fb1..ccd11f9b 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go +++ b/internal/pkg/core/deployer/providers/baiducloud-cdn/baiducloud_cdn.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go index 200d34ec..f2295593 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go +++ b/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go @@ -47,7 +47,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go b/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go index e3efa6e4..b056b076 100644 --- a/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go +++ b/internal/pkg/core/deployer/providers/baishan-cdn/baishan_cdn.go @@ -51,7 +51,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go b/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go index 5709f82d..403b96e8 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go +++ b/internal/pkg/core/deployer/providers/baotapanel-console/baotapanel_console.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go index d6ee1533..78fc3e96 100644 --- a/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go +++ b/internal/pkg/core/deployer/providers/baotapanel-site/baotapanel_site.go @@ -55,7 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go b/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go index 482ca8e4..dbdbf811 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go +++ b/internal/pkg/core/deployer/providers/baotawaf-console/baotawaf_console.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go index 435f7a69..daf482ac 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go +++ b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site.go @@ -23,7 +23,7 @@ type DeployerConfig struct { // 网站名称。 SiteName string `json:"siteName"` // 网站 SSL 端口。 - // 零值时默认为 443。 + // 零值时默认值 443。 SitePort int32 `json:"sitePort,omitempty"` } @@ -54,7 +54,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -116,7 +116,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE SiteId: siteId, Type: typeutil.ToPtr("openCert"), Server: &btsdk.SiteServerInfo{ - ListenSSLPort: typeutil.ToPtr(d.config.SitePort), + ListenSSLPorts: typeutil.ToPtr([]int32{d.config.SitePort}), SSL: &btsdk.SiteServerSSLInfo{ IsSSL: typeutil.ToPtr(int32(1)), FullChain: typeutil.ToPtr(certPEM), diff --git a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go index 6bead4b5..e9b4b836 100644 --- a/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go +++ b/internal/pkg/core/deployer/providers/baotawaf-site/baotawaf_site_test.go @@ -39,7 +39,7 @@ Shell command to run this test: --CERTIMATE_DEPLOYER_BAOTAWAFSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SERVERURL="http://127.0.0.1:8888" \ --CERTIMATE_DEPLOYER_BAOTAWAFSITE_APIKEY="your-api-key" \ - --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name"\ + --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITENAME="your-site-name" \ --CERTIMATE_DEPLOYER_BAOTAWAFSITE_SITEPORT=443 */ func TestDeploy(t *testing.T) { diff --git a/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go b/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go index e2bfd696..cdc39baa 100644 --- a/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go +++ b/internal/pkg/core/deployer/providers/bunny-cdn/bunny_cdn.go @@ -41,7 +41,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go b/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go index e659c9a1..a11bbaf7 100644 --- a/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go +++ b/internal/pkg/core/deployer/providers/byteplus-cdn/byteplus_cdn.go @@ -59,7 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/cachefly/cachefly.go b/internal/pkg/core/deployer/providers/cachefly/cachefly.go index 21cb4dd0..fa1cce13 100644 --- a/internal/pkg/core/deployer/providers/cachefly/cachefly.go +++ b/internal/pkg/core/deployer/providers/cachefly/cachefly.go @@ -42,7 +42,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go b/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go index 1ced8caf..25fb6a54 100644 --- a/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go +++ b/internal/pkg/core/deployer/providers/cdnfly/cdnfly.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go b/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go index efcf4b7c..9401285f 100644 --- a/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go +++ b/internal/pkg/core/deployer/providers/dogecloud-cdn/dogecloud_cdn.go @@ -55,7 +55,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index 195c202e..a4a60c98 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go index 8b692e90..3c957071 100644 --- a/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go +++ b/internal/pkg/core/deployer/providers/flexcdn/flexcdn.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go index 780f91a7..0d652df9 100644 --- a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/goedge/goedge.go b/internal/pkg/core/deployer/providers/goedge/goedge.go index 25caeb01..0b7ff2b5 100644 --- a/internal/pkg/core/deployer/providers/goedge/goedge.go +++ b/internal/pkg/core/deployer/providers/goedge/goedge.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go index d33dafff..cbdff322 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go @@ -21,6 +21,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 加速域名(不支持泛域名)。 @@ -51,8 +53,9 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -68,7 +71,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -88,7 +91,8 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE // 查询加速域名配置 // REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html showDomainFullConfigReq := &hccdnmodel.ShowDomainFullConfigRequest{ - DomainName: d.config.Domain, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + DomainName: d.config.Domain, } showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq) d.logger.Debug("sdk request 'cdn.ShowDomainFullConfig'", slog.Any("request", showDomainFullConfigReq), slog.Any("response", showDomainFullConfigResp)) @@ -107,6 +111,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE updateDomainMultiCertificatesReqBodyContent.CertName = typeutil.ToPtr(upres.CertName) updateDomainMultiCertificatesReqBodyContent = assign(updateDomainMultiCertificatesReqBodyContent, showDomainFullConfigResp.Configs) updateDomainMultiCertificatesReq := &hccdnmodel.UpdateDomainMultiCertificatesRequest{ + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), Body: &hccdnmodel.UpdateDomainMultiCertificatesRequestBody{ Https: updateDomainMultiCertificatesReqBodyContent, }, diff --git a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go index 23ec4a92..52cbcab5 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -27,6 +27,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 部署资源类型。 @@ -62,9 +64,10 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, - Region: config.Region, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, + Region: config.Region, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -80,7 +83,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -172,6 +175,9 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, certPEM str Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"}, LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id}, } + if d.config.EnterpriseProjectId != "" { + listListenersReq.EnterpriseProjectId = typeutil.ToPtr([]string{d.config.EnterpriseProjectId}) + } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) d.logger.Debug("sdk request 'elb.ListListeners'", slog.Any("request", listListenersReq), slog.Any("response", listListenersResp)) if err != nil { diff --git a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go index c8c208ad..0ba5816a 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go @@ -15,6 +15,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` } type DeployerProvider struct { @@ -31,8 +33,9 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -47,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go index 8fe96ee0..8afb2049 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-waf/huaweicloud_waf.go @@ -27,6 +27,8 @@ type DeployerConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` // 部署资源类型。 @@ -59,9 +61,10 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - AccessKeyId: config.AccessKeyId, - SecretAccessKey: config.SecretAccessKey, - Region: config.Region, + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + EnterpriseProjectId: config.EnterpriseProjectId, + Region: config.Region, }) if err != nil { return nil, fmt.Errorf("failed to create ssl uploader: %w", err) @@ -77,7 +80,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -126,7 +129,8 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri // 查询证书 // REF: https://support.huaweicloud.com/api-waf/ShowCertificate.html showCertificateReq := &hcwafmodel.ShowCertificateRequest{ - CertificateId: d.config.CertificateId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + CertificateId: d.config.CertificateId, } showCertificateResp, err := d.sdkClient.ShowCertificate(showCertificateReq) d.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", showCertificateReq), slog.Any("response", showCertificateResp)) @@ -137,7 +141,8 @@ func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM stri // 更新证书 // REF: https://support.huaweicloud.com/api-waf/UpdateCertificate.html updateCertificateReq := &hcwafmodel.UpdateCertificateRequest{ - CertificateId: d.config.CertificateId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + CertificateId: d.config.CertificateId, Body: &hcwafmodel.UpdateCertificateRequestBody{ Name: *showCertificateResp.Name, Content: typeutil.ToPtr(certPEM), @@ -179,9 +184,10 @@ func (d *DeployerProvider) deployToCloudServer(ctx context.Context, certPEM stri } listHostReq := &hcwafmodel.ListHostRequest{ - Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), - Page: typeutil.ToPtr(listHostPage), - Pagesize: typeutil.ToPtr(listHostPageSize), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), + Page: typeutil.ToPtr(listHostPage), + Pagesize: typeutil.ToPtr(listHostPageSize), } listHostResp, err := d.sdkClient.ListHost(listHostReq) d.logger.Debug("sdk request 'waf.ListHost'", slog.Any("request", listHostReq), slog.Any("response", listHostResp)) @@ -211,7 +217,8 @@ func (d *DeployerProvider) deployToCloudServer(ctx context.Context, certPEM stri // 更新云模式防护域名的配置 // REF: https://support.huaweicloud.com/api-waf/UpdateHost.html updateHostReq := &hcwafmodel.UpdateHostRequest{ - InstanceId: hostId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + InstanceId: hostId, Body: &hcwafmodel.UpdateHostRequestBody{ Certificateid: typeutil.ToPtr(upres.CertId), Certificatename: typeutil.ToPtr(upres.CertName), @@ -252,9 +259,10 @@ func (d *DeployerProvider) deployToPremiumHost(ctx context.Context, certPEM stri } listPremiumHostReq := &hcwafmodel.ListPremiumHostRequest{ - Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), - Page: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPage)), - Pagesize: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPageSize)), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")), + Page: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPage)), + Pagesize: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPageSize)), } listPremiumHostResp, err := d.sdkClient.ListPremiumHost(listPremiumHostReq) d.logger.Debug("sdk request 'waf.ListPremiumHost'", slog.Any("request", listPremiumHostReq), slog.Any("response", listPremiumHostResp)) @@ -284,7 +292,8 @@ func (d *DeployerProvider) deployToPremiumHost(ctx context.Context, certPEM stri // 修改独享模式域名配置 // REF: https://support.huaweicloud.com/api-waf/UpdatePremiumHost.html updatePremiumHostReq := &hcwafmodel.UpdatePremiumHostRequest{ - HostId: hostId, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(d.config.EnterpriseProjectId), + HostId: hostId, Body: &hcwafmodel.UpdatePremiumHostRequestBody{ Certificateid: typeutil.ToPtr(upres.CertId), Certificatename: typeutil.ToPtr(upres.CertName), diff --git a/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go b/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go index ca42126e..0f8a048d 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go +++ b/internal/pkg/core/deployer/providers/jdcloud-alb/jdcloud_alb.go @@ -76,7 +76,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go b/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go index 10ccf19d..7da0000b 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go +++ b/internal/pkg/core/deployer/providers/jdcloud-cdn/jdcloud_cdn.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go b/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go index 24e5bc7a..666ce101 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go +++ b/internal/pkg/core/deployer/providers/jdcloud-live/jdcloud_live.go @@ -48,7 +48,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go b/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go index 6f61625d..19e5e286 100644 --- a/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go +++ b/internal/pkg/core/deployer/providers/jdcloud-vod/jdcloud_vod.go @@ -51,7 +51,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go index de2e62be..e51bfcd8 100644 --- a/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go +++ b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/lecdn/lecdn.go b/internal/pkg/core/deployer/providers/lecdn/lecdn.go index c85f6558..4d9f4302 100644 --- a/internal/pkg/core/deployer/providers/lecdn/lecdn.go +++ b/internal/pkg/core/deployer/providers/lecdn/lecdn.go @@ -73,7 +73,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/local/local.go b/internal/pkg/core/deployer/providers/local/local.go index a71ad9d3..0b71da8a 100644 --- a/internal/pkg/core/deployer/providers/local/local.go +++ b/internal/pkg/core/deployer/providers/local/local.go @@ -15,7 +15,7 @@ import ( type DeployerConfig struct { // Shell 执行环境。 - // 零值时默认根据操作系统决定。 + // 零值时根据操作系统决定。 ShellEnv ShellEnvType `json:"shellEnv,omitempty"` // 前置命令。 PreCommand string `json:"preCommand,omitempty"` @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go b/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go index 908b78c3..3b2072d7 100644 --- a/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go +++ b/internal/pkg/core/deployer/providers/netlify-site/netlify_site.go @@ -45,7 +45,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go index 349c3a16..670ba02c 100644 --- a/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go +++ b/internal/pkg/core/deployer/providers/proxmoxve/proxmoxve.go @@ -13,6 +13,7 @@ import ( "github.com/luthermonson/go-proxmox" "github.com/usual2970/certimate/internal/pkg/core/deployer" + httputil "github.com/usual2970/certimate/internal/pkg/utils/http" ) type DeployerConfig struct { @@ -57,7 +58,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } @@ -101,15 +102,16 @@ func createSdkClient(serverUrl, apiToken, apiTokenSecret string, skipTlsVerify b } httpClient := &http.Client{ - Transport: http.DefaultTransport, + Transport: httputil.NewDefaultTransport(), Timeout: http.DefaultClient.Timeout, } if skipTlsVerify { - httpClient.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, + transport := httputil.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} } + transport.TLSClientConfig.InsecureSkipVerify = true + httpClient.Transport = transport } client := proxmox.NewClient( strings.TrimRight(serverUrl, "/")+"/api2/json", diff --git a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go index 573eeb94..8491ecc3 100644 --- a/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go +++ b/internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go @@ -57,7 +57,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go b/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go index db8d899e..ec6cfc4b 100644 --- a/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go +++ b/internal/pkg/core/deployer/providers/qiniu-pili/qiniu_pili.go @@ -57,7 +57,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go index 0b003bee..99321f82 100644 --- a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go +++ b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go @@ -58,7 +58,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go b/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go index 651ae0ac..85e7f530 100644 --- a/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go +++ b/internal/pkg/core/deployer/providers/ratpanel-console/ratpanel_console.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go b/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go index 8d605b3d..7e30daf6 100644 --- a/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go +++ b/internal/pkg/core/deployer/providers/ratpanel-site/ratpanel_site.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/safeline/safeline.go b/internal/pkg/core/deployer/providers/safeline/safeline.go index f1b6b039..253a8754 100644 --- a/internal/pkg/core/deployer/providers/safeline/safeline.go +++ b/internal/pkg/core/deployer/providers/safeline/safeline.go @@ -53,7 +53,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go index ae6e459f..a52c355e 100644 --- a/internal/pkg/core/deployer/providers/ssh/ssh.go +++ b/internal/pkg/core/deployer/providers/ssh/ssh.go @@ -19,10 +19,10 @@ import ( type JumpServerConfig struct { // SSH 主机。 - // 零值时默认为 "localhost"。 + // 零值时默认值 "localhost"。 SshHost string `json:"sshHost,omitempty"` // SSH 端口。 - // 零值时默认为 22。 + // 零值时默认值 22。 SshPort int32 `json:"sshPort,omitempty"` // SSH 登录用户名。 SshUsername string `json:"sshUsername,omitempty"` @@ -36,10 +36,10 @@ type JumpServerConfig struct { type DeployerConfig struct { // SSH 主机。 - // 零值时默认为 "localhost"。 + // 零值时默认值 "localhost"。 SshHost string `json:"sshHost,omitempty"` // SSH 端口。 - // 零值时默认为 22。 + // 零值时默认值 22。 SshPort int32 `json:"sshPort,omitempty"` // SSH 登录用户名。 SshUsername string `json:"sshUsername,omitempty"` @@ -103,7 +103,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go b/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go index a92e4eb1..1df67032 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-cdn/tencentcloud_cdn.go @@ -70,7 +70,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go b/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go index 2f7c0f22..5455e236 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-clb/tencentcloud_clb.go @@ -79,7 +79,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go b/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go index 99ee9b2f..2aa6b2d0 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-cos/tencentcloud_cos.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go b/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go index 7de626d9..a9056719 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index 88840f4a..d1ba3ce4 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go index 919339bb..138fb84a 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-eo/tencentcloud_eo.go @@ -69,7 +69,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go b/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go index bc8d8696..a0967f4e 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-scf/tencentcloud_scf.go @@ -62,7 +62,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go b/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go index 585000d9..5b4dd8d3 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy/tencentcloud_ssl_deploy.go @@ -66,7 +66,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go b/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go index 5fbdb7c6..09ac14cd 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ssl/tencentcloud_ssl.go @@ -47,7 +47,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go b/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go index 1b8553c5..b7c2a3ad 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-vod/tencentcloud_vod.go @@ -62,7 +62,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go b/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go index 18380289..1c8e7272 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-waf/tencentcloud_waf.go @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go b/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go index 5f90b943..532efb85 100644 --- a/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go +++ b/internal/pkg/core/deployer/providers/ucloud-ucdn/ucloud_ucdn.go @@ -65,7 +65,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go b/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go index 5564e6a8..42a51cb4 100644 --- a/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go +++ b/internal/pkg/core/deployer/providers/ucloud-us3/ucloud_us3.go @@ -67,7 +67,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go b/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go index e24708bd..82946bf1 100644 --- a/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go +++ b/internal/pkg/core/deployer/providers/unicloud-webhost/unicloud_webhost.go @@ -52,7 +52,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go b/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go index 4c9987a3..2fbe52b8 100644 --- a/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go +++ b/internal/pkg/core/deployer/providers/upyun-cdn/upyun_cdn.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go index b17ae729..e4d76ab1 100644 --- a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go +++ b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go @@ -74,7 +74,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go b/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go index e9b2c325..e67e8885 100644 --- a/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go +++ b/internal/pkg/core/deployer/providers/volcengine-cdn/volcengine_cdn.go @@ -59,7 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go b/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go index 3989a000..8bb40d5b 100644 --- a/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go +++ b/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go @@ -50,7 +50,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go index 3b6a37bf..bc2dc9e0 100644 --- a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go +++ b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go @@ -70,7 +70,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go b/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go index 707ccde3..82021205 100644 --- a/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go +++ b/internal/pkg/core/deployer/providers/volcengine-dcdn/volcengine_dcdn.go @@ -64,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go b/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go index 2f419752..a7c974b4 100644 --- a/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go +++ b/internal/pkg/core/deployer/providers/volcengine-imagex/volcengine_imagex.go @@ -65,7 +65,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go b/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go index 46c0b9dc..3195d810 100644 --- a/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go +++ b/internal/pkg/core/deployer/providers/volcengine-live/volcengine_live.go @@ -60,7 +60,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go b/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go index 365d95f2..674106e1 100644 --- a/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go +++ b/internal/pkg/core/deployer/providers/volcengine-tos/volcengine_tos.go @@ -64,7 +64,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go b/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go index 43c65de2..f889b996 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdn/wangsu_cdn.go @@ -59,7 +59,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go index 4d5f2e10..0780f80d 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go @@ -68,7 +68,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go index 3f691489..51fa7076 100644 --- a/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/deployer/providers/webhook/webhook.go b/internal/pkg/core/deployer/providers/webhook/webhook.go index 49b07b47..7ad6d6b0 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook.go @@ -23,7 +23,7 @@ type DeployerConfig struct { // Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。 WebhookData string `json:"webhookData,omitempty"` // 请求谓词。 - // 零值时默认为 "POST"。 + // 零值时默认值 "POST"。 Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` @@ -61,7 +61,7 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { if logger == nil { - d.logger = slog.Default() + d.logger = slog.New(slog.DiscardHandler) } else { d.logger = logger } diff --git a/internal/pkg/core/notifier/notifier.go b/internal/pkg/core/notifier/notifier.go index 876b5d48..f04084aa 100644 --- a/internal/pkg/core/notifier/notifier.go +++ b/internal/pkg/core/notifier/notifier.go @@ -19,7 +19,7 @@ type Notifier interface { // 出参: // - res:发送结果。 // - err: 错误。 - Notify(ctx context.Context, subject string, message string) (res *NotifyResult, err error) + Notify(ctx context.Context, subject string, message string) (_res *NotifyResult, _err error) } // 表示通知发送结果的数据结构。 diff --git a/internal/pkg/core/notifier/providers/bark/bark.go b/internal/pkg/core/notifier/providers/bark/bark.go index ec0d44f3..805a72b0 100644 --- a/internal/pkg/core/notifier/providers/bark/bark.go +++ b/internal/pkg/core/notifier/providers/bark/bark.go @@ -12,7 +12,7 @@ import ( type NotifierConfig struct { // Bark 服务地址。 - // 零值时默认使用官方服务器。 + // 零值时使用官方服务器。 ServerUrl string `json:"serverUrl"` // Bark 设备密钥。 DeviceKey string `json:"deviceKey"` @@ -42,14 +42,14 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { const defaultServerURL = "https://api.day.app/" serverUrl := defaultServerURL if n.config.ServerUrl != "" { diff --git a/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go b/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go index d6d8b096..81358ef4 100644 --- a/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go +++ b/internal/pkg/core/notifier/providers/dingtalkbot/dingtalkbot.go @@ -38,14 +38,14 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { return nil, fmt.Errorf("dingtalk api error: invalid webhook url: %w", err) diff --git a/internal/pkg/core/notifier/providers/discordbot/discordbot.go b/internal/pkg/core/notifier/providers/discordbot/discordbot.go index 3ed0cab7..704e7c79 100644 --- a/internal/pkg/core/notifier/providers/discordbot/discordbot.go +++ b/internal/pkg/core/notifier/providers/discordbot/discordbot.go @@ -41,19 +41,20 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://discord.com/developers/docs/resources/message#create-message req := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bot "+n.config.BotToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "content": subject + "\n" + message, }) diff --git a/internal/pkg/core/notifier/providers/email/email.go b/internal/pkg/core/notifier/providers/email/email.go index 69d39012..c8405554 100644 --- a/internal/pkg/core/notifier/providers/email/email.go +++ b/internal/pkg/core/notifier/providers/email/email.go @@ -50,14 +50,14 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { var smtpAuth smtp.Auth if n.config.Username != "" || n.config.Password != "" { smtpAuth = smtp.PlainAuth("", n.config.Username, n.config.Password, n.config.SmtpHost) @@ -76,10 +76,11 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s var yak *mailyak.MailYak if n.config.SmtpTls { - yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig()) + yakWithTls, err := mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig()) if err != nil { return nil, err } + yak = yakWithTls } else { yak = mailyak.New(smtpAddr, smtpAuth) } @@ -89,8 +90,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s yak.Subject(subject) yak.Plain().Set(message) - err = yak.Send() - if err != nil { + if err := yak.Send(); err != nil { return nil, err } diff --git a/internal/pkg/core/notifier/providers/gotify/gotify.go b/internal/pkg/core/notifier/providers/gotify/gotify.go index c82cd5a5..75d8737b 100644 --- a/internal/pkg/core/notifier/providers/gotify/gotify.go +++ b/internal/pkg/core/notifier/providers/gotify/gotify.go @@ -44,21 +44,22 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { serverUrl := strings.TrimRight(n.config.ServerUrl, "/") // REF: https://gotify.net/api-docs#/message/createMessage req := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+n.config.Token). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "title": subject, "message": message, diff --git a/internal/pkg/core/notifier/providers/larkbot/larkbot.go b/internal/pkg/core/notifier/providers/larkbot/larkbot.go index 7d3e8a55..8c5022c0 100644 --- a/internal/pkg/core/notifier/providers/larkbot/larkbot.go +++ b/internal/pkg/core/notifier/providers/larkbot/larkbot.go @@ -35,14 +35,14 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { bot := lark.NewNotificationBot(n.config.WebhookUrl) content := lark.NewPostBuilder(). Title(subject). diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go index 81283f7c..de72d192 100644 --- a/internal/pkg/core/notifier/providers/mattermost/mattermost.go +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -46,20 +46,21 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { serverUrl := strings.TrimRight(n.config.ServerUrl, "/") // REF: https://developers.mattermost.com/api-documentation/#/operations/Login loginReq := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "login_id": n.config.Username, "password": n.config.Password, @@ -76,8 +77,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s // REF: https://developers.mattermost.com/api-documentation/#/operations/CreatePost postReq := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+loginResp.Header().Get("Token")). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "channel_id": n.config.ChannelId, "props": map[string]interface{}{ diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go index 827a45d6..aedf8d3a 100644 --- a/internal/pkg/core/notifier/providers/pushover/pushover.go +++ b/internal/pkg/core/notifier/providers/pushover/pushover.go @@ -41,18 +41,19 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://pushover.net/api req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "title": subject, "message": message, diff --git a/internal/pkg/core/notifier/providers/pushplus/pushplus.go b/internal/pkg/core/notifier/providers/pushplus/pushplus.go index 79a27d49..9f565ce5 100644 --- a/internal/pkg/core/notifier/providers/pushplus/pushplus.go +++ b/internal/pkg/core/notifier/providers/pushplus/pushplus.go @@ -40,18 +40,19 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://pushplus.plus/doc/guide/api.html#%E4%B8%80%E3%80%81%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF%E6%8E%A5%E5%8F%A3 req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "title": subject, "content": message, diff --git a/internal/pkg/core/notifier/providers/serverchan/serverchan.go b/internal/pkg/core/notifier/providers/serverchan/serverchan.go index d1897ab4..ea6adf2b 100644 --- a/internal/pkg/core/notifier/providers/serverchan/serverchan.go +++ b/internal/pkg/core/notifier/providers/serverchan/serverchan.go @@ -39,18 +39,19 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://sct.ftqq.com/ req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "text": subject, "desp": message, diff --git a/internal/pkg/core/notifier/providers/slackbot/slackbot.go b/internal/pkg/core/notifier/providers/slackbot/slackbot.go index 7b16ad25..92db106c 100644 --- a/internal/pkg/core/notifier/providers/slackbot/slackbot.go +++ b/internal/pkg/core/notifier/providers/slackbot/slackbot.go @@ -41,19 +41,20 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://docs.slack.dev/messaging/sending-and-scheduling-messages#publishing req := n.httpClient.R(). SetContext(ctx). - SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+n.config.BotToken). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "token": n.config.BotToken, "channel": n.config.ChannelId, diff --git a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go index 39e1f705..80d03a21 100644 --- a/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go +++ b/internal/pkg/core/notifier/providers/telegrambot/telegrambot.go @@ -41,18 +41,19 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://core.telegram.org/bots/api#sendmessage req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "chat_id": n.config.ChatId, "text": subject + "\n" + message, diff --git a/internal/pkg/core/notifier/providers/webhook/webhook.go b/internal/pkg/core/notifier/providers/webhook/webhook.go index 8850ea73..523f7b4d 100644 --- a/internal/pkg/core/notifier/providers/webhook/webhook.go +++ b/internal/pkg/core/notifier/providers/webhook/webhook.go @@ -22,7 +22,7 @@ type NotifierConfig struct { // Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。 WebhookData string `json:"webhookData,omitempty"` // 请求谓词。 - // 零值时默认为 "POST"。 + // 零值时默认值 "POST"。 Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` @@ -60,14 +60,14 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // 处理 Webhook URL webhookUrl, err := url.Parse(n.config.WebhookUrl) if err != nil { diff --git a/internal/pkg/core/notifier/providers/wecombot/wecombot.go b/internal/pkg/core/notifier/providers/wecombot/wecombot.go index 8f51a70a..93b03c4d 100644 --- a/internal/pkg/core/notifier/providers/wecombot/wecombot.go +++ b/internal/pkg/core/notifier/providers/wecombot/wecombot.go @@ -39,18 +39,19 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { if logger == nil { - n.logger = slog.Default() + n.logger = slog.New(slog.DiscardHandler) } else { n.logger = logger } return n } -func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (*notifier.NotifyResult, error) { // REF: https://developer.work.weixin.qq.com/document/path/91770 req := n.httpClient.R(). SetContext(ctx). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetBody(map[string]any{ "msgtype": "text", "text": map[string]string{ diff --git a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go index ca3c7303..7391129d 100644 --- a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go +++ b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go @@ -52,14 +52,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 遍历证书列表,避免重复上传 if res, err := u.getCertIfExists(ctx, certPEM, privkeyPEM); err != nil { return nil, err @@ -94,7 +94,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } } -func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { searchWebsiteSSLPageNumber := int32(1) searchWebsiteSSLPageSize := int32(100) for { diff --git a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go index 9d7be223..ea0968eb 100644 --- a/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go +++ b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go @@ -13,6 +13,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type UploaderConfig struct { @@ -20,6 +21,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } @@ -51,14 +54,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { @@ -78,9 +81,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } listUserCertificateOrderReq := &alicas.ListUserCertificateOrderRequest{ - CurrentPage: tea.Int64(listUserCertificateOrderPage), - ShowSize: tea.Int64(listUserCertificateOrderLimit), - OrderType: tea.String("CERT"), + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), + CurrentPage: tea.Int64(listUserCertificateOrderPage), + ShowSize: tea.Int64(listUserCertificateOrderLimit), + OrderType: tea.String("CERT"), } listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrder(listUserCertificateOrderReq) u.logger.Debug("sdk request 'cas.ListUserCertificateOrder'", slog.Any("request", listUserCertificateOrderReq), slog.Any("response", listUserCertificateOrderResp)) @@ -143,9 +147,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 上传新证书 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate uploadUserCertificateReq := &alicas.UploadUserCertificateRequest{ - Name: tea.String(certName), - Cert: tea.String(certPEM), - Key: tea.String(privkeyPEM), + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), + Name: tea.String(certName), + Cert: tea.String(certPEM), + Key: tea.String(privkeyPEM), } uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificate(uploadUserCertificateReq) u.logger.Debug("sdk request 'cas.UploadUserCertificate'", slog.Any("request", uploadUserCertificateReq), slog.Any("response", uploadUserCertificateResp)) @@ -176,14 +181,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } func createSdkClient(accessKeyId, accessKeySecret, region string) (*alicas.Client, error) { - if region == "" { - region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 - } - // 接入点一览 https://api.aliyun.com/product/cas var endpoint string switch region { - case "cn-hangzhou": + case "", "cn-hangzhou": endpoint = "cas.aliyuncs.com" default: endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region) diff --git a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go index cc1544c1..dac9c7bd 100644 --- a/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go +++ b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go @@ -16,6 +16,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) type UploaderConfig struct { @@ -23,6 +24,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 阿里云 AccessKeySecret。 AccessKeySecret string `json:"accessKeySecret"` + // 阿里云资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` // 阿里云地域。 Region string `json:"region"` } @@ -54,14 +57,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { @@ -71,7 +74,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 查询证书列表,避免重复上传 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates describeServerCertificatesReq := &alislb.DescribeServerCertificatesRequest{ - RegionId: tea.String(u.config.Region), + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), + RegionId: tea.String(u.config.Region), } describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificates(describeServerCertificatesReq) u.logger.Debug("sdk request 'slb.DescribeServerCertificates'", slog.Any("request", describeServerCertificatesReq), slog.Any("response", describeServerCertificatesResp)) @@ -110,6 +114,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 上传新证书 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate uploadServerCertificateReq := &alislb.UploadServerCertificateRequest{ + ResourceGroupId: typeutil.ToPtrOrZeroNil(u.config.ResourceGroupId), RegionId: tea.String(u.config.Region), ServerCertificateName: tea.String(certName), ServerCertificate: tea.String(certPEM), @@ -132,7 +137,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*alislb.Clien // 接入点一览 https://api.aliyun.com/product/Slb var endpoint string switch region { - case + case "", "cn-hangzhou", "cn-hangzhou-finance", "cn-shanghai-finance-1", diff --git a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go index 05cc70e3..f68ebadc 100644 --- a/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go +++ b/internal/pkg/core/uploader/providers/aws-acm/aws_acm.go @@ -51,14 +51,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go index 5ac68d69..eb67fd2f 100644 --- a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go @@ -58,14 +58,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go index 727aa03f..b0fca821 100644 --- a/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go @@ -46,14 +46,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go index 1235893c..a654db31 100644 --- a/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go +++ b/internal/pkg/core/uploader/providers/byteplus-cdn/byteplus_cdn.go @@ -49,14 +49,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go b/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go index ca98fc90..1b0b963f 100644 --- a/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go +++ b/internal/pkg/core/uploader/providers/dogecloud/dogecloud.go @@ -44,14 +44,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 生成新证书名(需符合多吉云命名规则) var certId, certName string certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) diff --git a/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go index 5987136e..f3127602 100644 --- a/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/uploader/providers/gcore-cdn/gcore_cdn.go @@ -46,14 +46,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 生成新证书名(需符合 Gcore 命名规则) var certId, certName string certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) diff --git a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index 9369144e..d429c259 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -26,6 +26,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } @@ -57,14 +59,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { @@ -141,10 +143,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE createCertificateReq := &hcelbmodel.CreateCertificateRequest{ Body: &hcelbmodel.CreateCertificateRequestBody{ Certificate: &hcelbmodel.CreateCertificateOption{ - ProjectId: typeutil.ToPtr(projectId), - Name: typeutil.ToPtr(certName), - Certificate: typeutil.ToPtr(certPEM), - PrivateKey: typeutil.ToPtr(privkeyPEM), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + ProjectId: typeutil.ToPtr(projectId), + Name: typeutil.ToPtr(certName), + Certificate: typeutil.ToPtr(certPEM), + PrivateKey: typeutil.ToPtr(privkeyPEM), }, }, } diff --git a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index f8435733..4e35562e 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -21,6 +21,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } @@ -52,14 +54,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { @@ -79,10 +81,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } listCertificatesReq := &hcscmmodel.ListCertificatesRequest{ - Limit: typeutil.ToPtr(listCertificatesLimit), - Offset: typeutil.ToPtr(listCertificatesOffset), - SortDir: typeutil.ToPtr("DESC"), - SortKey: typeutil.ToPtr("certExpiredTime"), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + Limit: typeutil.ToPtr(listCertificatesLimit), + Offset: typeutil.ToPtr(listCertificatesOffset), + SortDir: typeutil.ToPtr("DESC"), + SortKey: typeutil.ToPtr("certExpiredTime"), } listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) u.logger.Debug("sdk request 'scm.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) @@ -142,9 +145,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html importCertificateReq := &hcscmmodel.ImportCertificateRequest{ Body: &hcscmmodel.ImportCertificateRequestBody{ - Name: certName, - Certificate: certPEM, - PrivateKey: privkeyPEM, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + Name: certName, + Certificate: certPEM, + PrivateKey: privkeyPEM, }, } importCertificateResp, err := u.sdkClient.ImportCertificate(importCertificateReq) diff --git a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go index d0c61775..789876ba 100644 --- a/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-waf/huaweicloud_waf.go @@ -26,6 +26,8 @@ type UploaderConfig struct { AccessKeyId string `json:"accessKeyId"` // 华为云 SecretAccessKey。 SecretAccessKey string `json:"secretAccessKey"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` // 华为云区域。 Region string `json:"region"` } @@ -57,14 +59,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { @@ -84,8 +86,9 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } listCertificatesReq := &hcwafmodel.ListCertificatesRequest{ - Page: typeutil.ToPtr(listCertificatesPage), - Pagesize: typeutil.ToPtr(listCertificatesPageSize), + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + Page: typeutil.ToPtr(listCertificatesPage), + Pagesize: typeutil.ToPtr(listCertificatesPageSize), } listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) u.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) @@ -96,7 +99,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE if listCertificatesResp.Items != nil { for _, certItem := range *listCertificatesResp.Items { showCertificateReq := &hcwafmodel.ShowCertificateRequest{ - CertificateId: certItem.Id, + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), + CertificateId: certItem.Id, } showCertificateResp, err := u.sdkClient.ShowCertificate(showCertificateReq) u.logger.Debug("sdk request 'waf.ShowCertificate'", slog.Any("request", showCertificateReq), slog.Any("response", showCertificateResp)) @@ -141,6 +145,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE // 创建证书 // REF: https://support.huaweicloud.com/api-waf/CreateCertificate.html createCertificateReq := &hcwafmodel.CreateCertificateRequest{ + EnterpriseProjectId: typeutil.ToPtrOrZeroNil(u.config.EnterpriseProjectId), Body: &hcwafmodel.CreateCertificateRequestBody{ Name: certName, Content: certPEM, diff --git a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go index b26755a6..44ed7f29 100644 --- a/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go +++ b/internal/pkg/core/uploader/providers/jdcloud-ssl/jdcloud_ssl.go @@ -52,14 +52,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go index 8dc2fefe..99a1a0b5 100644 --- a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -48,14 +48,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go index cb493110..613fc7a9 100644 --- a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go @@ -44,14 +44,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { if res, err := u.getCertIfExists(ctx, certPEM); err != nil { return nil, err } else if res != nil { @@ -80,7 +80,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } } -func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go index 59067de4..db4e92f4 100644 --- a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go +++ b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go @@ -46,14 +46,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 上传新证书 // REF: https://cloud.tencent.com/document/product/400/41665 uploadCertificateReq := tcssl.NewUploadCertificateRequest() diff --git a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go index 90eb1683..acfbb214 100644 --- a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go +++ b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go @@ -56,14 +56,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 生成新证书名(需符合优刻得命名规则) var certId, certName string certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) @@ -111,7 +111,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE }, nil } -func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go index 7a8bd3a0..6b45e130 100644 --- a/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go +++ b/internal/pkg/core/uploader/providers/upyun-ssl/upyun_ssl.go @@ -44,14 +44,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 上传证书 uploadHttpsCertificateReq := &upyunsdk.UploadHttpsCertificateRequest{ Certificate: certPEM, diff --git a/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go b/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go index b529e84a..00ac07ae 100644 --- a/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go +++ b/internal/pkg/core/uploader/providers/volcengine-cdn/volcengine_cdn.go @@ -50,14 +50,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go b/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go index 99511ebf..9accc17d 100644 --- a/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go +++ b/internal/pkg/core/uploader/providers/volcengine-certcenter/volcengine_certcenter.go @@ -49,14 +49,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 上传证书 // REF: https://www.volcengine.com/docs/6638/1365580 importCertificateReq := &veccsdk.ImportCertificateInput{ diff --git a/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go b/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go index de5ec27d..d758fbb4 100644 --- a/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go +++ b/internal/pkg/core/uploader/providers/volcengine-live/volcengine_live.go @@ -47,14 +47,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go index b512be09..6a12ceda 100644 --- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go @@ -50,14 +50,14 @@ func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { if logger == nil { - u.logger = slog.Default() + u.logger = slog.New(slog.DiscardHandler) } else { u.logger = logger } return u } -func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPEM) if err != nil { @@ -65,7 +65,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE } // 查询证书列表,避免重复上传 - // REF: https://www.wangsu.com/document/api-doc/26426 + // REF: https://www.wangsu.com/document/api-doc/22675?productCode=certificatemanagement listCertificatesResp, err := u.sdkClient.ListCertificates() u.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp)) if err != nil { diff --git a/internal/pkg/core/uploader/uploader.go b/internal/pkg/core/uploader/uploader.go index 34d2813a..0a1681a7 100644 --- a/internal/pkg/core/uploader/uploader.go +++ b/internal/pkg/core/uploader/uploader.go @@ -21,7 +21,7 @@ type Uploader interface { // 出参: // - res:上传结果。 // - err: 错误。 - Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *UploadResult, err error) + Upload(ctx context.Context, certPEM string, privkeyPEM string) (_res *UploadResult, _err error) } // 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。 diff --git a/internal/pkg/sdk3rd/1panel/client.go b/internal/pkg/sdk3rd/1panel/client.go index 3fe549a0..8090340e 100644 --- a/internal/pkg/sdk3rd/1panel/client.go +++ b/internal/pkg/sdk3rd/1panel/client.go @@ -14,8 +14,6 @@ import ( ) type Client struct { - apiKey string - client *resty.Client } @@ -25,7 +23,8 @@ func NewClient(serverUrl, apiVersion, apiKey string) *Client { } client := resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/api/" + apiVersion). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api/"+apiVersion). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) tokenMd5 := md5.Sum([]byte("1panel" + apiKey + timestamp)) diff --git a/internal/pkg/sdk3rd/baishan/client.go b/internal/pkg/sdk3rd/baishan/client.go index b3e428ee..7922096e 100644 --- a/internal/pkg/sdk3rd/baishan/client.go +++ b/internal/pkg/sdk3rd/baishan/client.go @@ -19,7 +19,8 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://cdn.api.baishan.com"). - SetHeader("token", apiToken) + SetHeader("User-Agent", "certimate"). + SetHeader("Token", apiToken) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/btpanel/client.go b/internal/pkg/sdk3rd/btpanel/client.go index aafee04f..7faa46c0 100644 --- a/internal/pkg/sdk3rd/btpanel/client.go +++ b/internal/pkg/sdk3rd/btpanel/client.go @@ -21,7 +21,9 @@ type Client struct { func NewClient(serverUrl, apiKey string) *Client { client := resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/")) + SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", "certimate") return &Client{ apiKey: apiKey, @@ -77,9 +79,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, data["request_time"] = fmt.Sprintf("%d", timestamp) data["request_token"] = c.generateSignature(fmt.Sprintf("%d", timestamp)) - req := c.client.R(). - SetHeader("Content-Type", "application/x-www-form-urlencoded"). - SetFormData(data) + req := c.client.R().SetFormData(data) resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("baota api error: failed to send request: %w", err) diff --git a/internal/pkg/sdk3rd/btwaf/client.go b/internal/pkg/sdk3rd/btwaf/client.go index 083db0c1..4bf76b16 100644 --- a/internal/pkg/sdk3rd/btwaf/client.go +++ b/internal/pkg/sdk3rd/btwaf/client.go @@ -19,7 +19,9 @@ type Client struct { func NewClient(serverUrl, apiKey string) *Client { client := resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/api"). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { timestamp := fmt.Sprintf("%d", time.Now().Unix()) keyMd5 := md5.Sum([]byte(apiKey)) @@ -48,9 +50,7 @@ func (c *Client) WithTLSConfig(config *tls.Config) *Client { } func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, error) { - req := c.client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(params) + req := c.client.R().SetBody(params) resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("baota api error: failed to send request: %w", err) diff --git a/internal/pkg/sdk3rd/btwaf/models.go b/internal/pkg/sdk3rd/btwaf/models.go index 16290e88..6217e1a5 100644 --- a/internal/pkg/sdk3rd/btwaf/models.go +++ b/internal/pkg/sdk3rd/btwaf/models.go @@ -37,8 +37,8 @@ type GetSiteListResponse struct { } type SiteServerInfo struct { - ListenSSLPort *int32 `json:"listen_ssl_port,omitempty"` - SSL *SiteServerSSLInfo `json:"ssl,omitempty"` + ListenSSLPorts *[]int32 `json:"listen_ssl_port,omitempty"` + SSL *SiteServerSSLInfo `json:"ssl,omitempty"` } type SiteServerSSLInfo struct { diff --git a/internal/pkg/sdk3rd/bunny/client.go b/internal/pkg/sdk3rd/bunny/client.go index 8d50e1fc..1efa2236 100644 --- a/internal/pkg/sdk3rd/bunny/client.go +++ b/internal/pkg/sdk3rd/bunny/client.go @@ -17,6 +17,7 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://api.bunny.net"). + SetHeader("User-Agent", "certimate"). SetHeader("AccessKey", apiToken) return &Client{ diff --git a/internal/pkg/sdk3rd/cachefly/client.go b/internal/pkg/sdk3rd/cachefly/client.go index 342e329d..cf29e833 100644 --- a/internal/pkg/sdk3rd/cachefly/client.go +++ b/internal/pkg/sdk3rd/cachefly/client.go @@ -17,7 +17,7 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://api.cachefly.com/api/2.5"). - SetHeader("x-cf-authorization", "Bearer "+apiToken) + SetHeader("X-CF-Authorization", "Bearer "+apiToken) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/cdnfly/client.go b/internal/pkg/sdk3rd/cdnfly/client.go index 2dabf6fd..6026d246 100644 --- a/internal/pkg/sdk3rd/cdnfly/client.go +++ b/internal/pkg/sdk3rd/cdnfly/client.go @@ -18,8 +18,9 @@ type Client struct { func NewClient(serverUrl, apiKey, apiSecret string) *Client { client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). - SetHeader("api-key", apiKey). - SetHeader("api-secret", apiSecret) + SetHeader("User-Agent", "certimate"). + SetHeader("API-Key", apiKey). + SetHeader("API-Secret", apiSecret) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/dcloud/unicloud/client.go b/internal/pkg/sdk3rd/dcloud/unicloud/client.go index 1e0f3728..8db4a792 100644 --- a/internal/pkg/sdk3rd/dcloud/unicloud/client.go +++ b/internal/pkg/sdk3rd/dcloud/unicloud/client.go @@ -51,6 +51,7 @@ func NewClient(username, password string) *Client { client.serverlessClient = resty.New() client.apiClient = resty.New(). SetBaseURL("https://unicloud-api.dcloud.net.cn/unicloud/api"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.apiUserToken != "" { req.Header.Set("Token", client.apiUserToken) @@ -173,9 +174,9 @@ func (c *Client) invokeServerless(endpoint, clientSecret, appId, spaceId, target sign := c.generateSignature(payload, clientSecret) req := c.serverlessClient.R(). + SetHeader("Content-Type", "application/json"). SetHeader("Origin", "https://unicloud.dcloud.net.cn"). SetHeader("Referer", "https://unicloud.dcloud.net.cn"). - SetHeader("Content-Type", "application/json"). SetHeader("X-Client-Info", string(clientInfoJsonb)). SetHeader("X-Client-Token", c.serverlessJwtToken). SetHeader("X-Serverless-Sign", sign). diff --git a/internal/pkg/sdk3rd/dnsla/client.go b/internal/pkg/sdk3rd/dnsla/client.go index d9a86fc5..accd36d9 100644 --- a/internal/pkg/sdk3rd/dnsla/client.go +++ b/internal/pkg/sdk3rd/dnsla/client.go @@ -17,7 +17,8 @@ type Client struct { func NewClient(apiId, apiSecret string) *Client { client := resty.New(). SetBaseURL("https://api.dns.la/api"). - SetBasicAuth(apiId, apiSecret) + SetBasicAuth(apiId, apiSecret). + SetHeader("User-Agent", "certimate") return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/flexcdn/client.go b/internal/pkg/sdk3rd/flexcdn/client.go index b478ffac..0844ffa5 100644 --- a/internal/pkg/sdk3rd/flexcdn/client.go +++ b/internal/pkg/sdk3rd/flexcdn/client.go @@ -32,6 +32,7 @@ func NewClient(serverUrl, apiRole, accessKeyId, accessKey string) *Client { } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Cloud-Access-Token", client.accessToken) diff --git a/internal/pkg/sdk3rd/gname/client.go b/internal/pkg/sdk3rd/gname/client.go index ef00e699..843785a5 100644 --- a/internal/pkg/sdk3rd/gname/client.go +++ b/internal/pkg/sdk3rd/gname/client.go @@ -20,7 +20,10 @@ type Client struct { } func NewClient(appId, appKey string) *Client { - client := resty.New() + client := resty.New(). + SetBaseURL("http://api.gname.com"). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", "certimate") return &Client{ appId: appId, @@ -74,11 +77,8 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, data["gntime"] = fmt.Sprintf("%d", time.Now().Unix()) data["gntoken"] = c.generateSignature(data) - url := "http://api.gname.com" + path - req := c.client.R(). - SetHeader("Content-Type", "application/x-www-form-urlencoded"). - SetFormData(data) - resp, err := req.Post(url) + req := c.client.R().SetFormData(data) + resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("gname api error: failed to send request: %w", err) } else if resp.IsError() { diff --git a/internal/pkg/sdk3rd/goedge/client.go b/internal/pkg/sdk3rd/goedge/client.go index 3cd4900a..bc87734a 100644 --- a/internal/pkg/sdk3rd/goedge/client.go +++ b/internal/pkg/sdk3rd/goedge/client.go @@ -32,6 +32,7 @@ func NewClient(serverUrl, apiRole, accessKeyId, accessKey string) *Client { } client.client = resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("X-Edge-Access-Token", client.accessToken) diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/client.go b/internal/pkg/sdk3rd/lecdn/v3/client/client.go index 3fa822eb..4af04d4f 100644 --- a/internal/pkg/sdk3rd/lecdn/v3/client/client.go +++ b/internal/pkg/sdk3rd/lecdn/v3/client/client.go @@ -28,7 +28,8 @@ func NewClient(serverUrl, username, password string) *Client { password: password, } client.client = resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/prod-api"). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/prod-api"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) diff --git a/internal/pkg/sdk3rd/lecdn/v3/master/client.go b/internal/pkg/sdk3rd/lecdn/v3/master/client.go index ee1abaca..dc033634 100644 --- a/internal/pkg/sdk3rd/lecdn/v3/master/client.go +++ b/internal/pkg/sdk3rd/lecdn/v3/master/client.go @@ -28,7 +28,8 @@ func NewClient(serverUrl, username, password string) *Client { password: password, } client.client = resty.New(). - SetBaseURL(strings.TrimRight(serverUrl, "/") + "/prod-api"). + SetBaseURL(strings.TrimRight(serverUrl, "/")+"/prod-api"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.accessToken != "" { req.Header.Set("Authorization", "Bearer "+client.accessToken) diff --git a/internal/pkg/sdk3rd/netlify/client.go b/internal/pkg/sdk3rd/netlify/client.go index d270e35e..bf3f4ad6 100644 --- a/internal/pkg/sdk3rd/netlify/client.go +++ b/internal/pkg/sdk3rd/netlify/client.go @@ -17,7 +17,8 @@ type Client struct { func NewClient(apiToken string) *Client { client := resty.New(). SetBaseURL("https://api.netlify.com/api/v1"). - SetHeader("Authorization", "Bearer "+apiToken) + SetHeader("Authorization", "Bearer "+apiToken). + SetHeader("User-Agent", "certimate") return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/qiniu/auth.go b/internal/pkg/sdk3rd/qiniu/auth.go index 7f053f56..6df13752 100644 --- a/internal/pkg/sdk3rd/qiniu/auth.go +++ b/internal/pkg/sdk3rd/qiniu/auth.go @@ -18,10 +18,10 @@ func newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport { return &transport{tr, mac} } -func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error) { +func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { token, err := t.mac.SignRequestV2(req) if err != nil { - return + return nil, err } req.Header.Set("Authorization", "Qiniu "+token) diff --git a/internal/pkg/sdk3rd/rainyun/client.go b/internal/pkg/sdk3rd/rainyun/client.go index 80113f0d..cf9e1895 100644 --- a/internal/pkg/sdk3rd/rainyun/client.go +++ b/internal/pkg/sdk3rd/rainyun/client.go @@ -17,7 +17,8 @@ type Client struct { func NewClient(apiKey string) *Client { client := resty.New(). SetBaseURL("https://api.v2.rainyun.com"). - SetHeader("x-api-key", apiKey) + SetHeader("User-Agent", "certimate"). + SetHeader("X-API-Key", apiKey) return &Client{ client: client, diff --git a/internal/pkg/sdk3rd/ratpanel/client.go b/internal/pkg/sdk3rd/ratpanel/client.go index f1d20359..e1abb6c5 100644 --- a/internal/pkg/sdk3rd/ratpanel/client.go +++ b/internal/pkg/sdk3rd/ratpanel/client.go @@ -25,6 +25,7 @@ func NewClient(serverUrl string, accessTokenId int32, accessToken string) *Clien SetBaseURL(strings.TrimRight(serverUrl, "/")+"/api"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { var body []byte var err error diff --git a/internal/pkg/sdk3rd/safeline/client.go b/internal/pkg/sdk3rd/safeline/client.go index 93d884a3..05ee6d1a 100644 --- a/internal/pkg/sdk3rd/safeline/client.go +++ b/internal/pkg/sdk3rd/safeline/client.go @@ -17,6 +17,8 @@ type Client struct { func NewClient(serverUrl, apiToken string) *Client { client := resty.New(). SetBaseURL(strings.TrimRight(serverUrl, "/")). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", "certimate"). SetHeader("X-SLCE-API-TOKEN", apiToken) return &Client{ @@ -35,9 +37,7 @@ func (c *Client) WithTLSConfig(config *tls.Config) *Client { } func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, error) { - req := c.client.R(). - SetHeader("Content-Type", "application/json"). - SetBody(params) + req := c.client.R().SetBody(params) resp, err := req.Post(path) if err != nil { return resp, fmt.Errorf("safeline api error: failed to send request: %w", err) diff --git a/internal/pkg/sdk3rd/upyun/console/client.go b/internal/pkg/sdk3rd/upyun/console/client.go index b207549e..e9202d91 100644 --- a/internal/pkg/sdk3rd/upyun/console/client.go +++ b/internal/pkg/sdk3rd/upyun/console/client.go @@ -26,6 +26,7 @@ func NewClient(username, password string) *Client { } client.client = resty.New(). SetBaseURL("https://console.upyun.com"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { if client.loginCookie != "" { req.Header.Set("Cookie", client.loginCookie) diff --git a/internal/pkg/sdk3rd/wangsu/cdnpro/api.go b/internal/pkg/sdk3rd/wangsu/cdnpro/api.go index c6f8da04..c45e6921 100644 --- a/internal/pkg/sdk3rd/wangsu/cdnpro/api.go +++ b/internal/pkg/sdk3rd/wangsu/cdnpro/api.go @@ -11,7 +11,7 @@ import ( func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { resp := &CreateCertificateResponse{} rres, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) { - r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + r.SetHeader("X-CNC-Timestamp", fmt.Sprintf("%d", req.Timestamp)) }) if err != nil { return resp, err @@ -28,7 +28,7 @@ func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateR resp := &UpdateCertificateResponse{} rres, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) { - r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + r.SetHeader("X-CNC-Timestamp", fmt.Sprintf("%d", req.Timestamp)) }) if err != nil { return resp, err diff --git a/internal/pkg/sdk3rd/wangsu/certificate/api.go b/internal/pkg/sdk3rd/wangsu/certificate/api.go index 037fb6e7..22172d4e 100644 --- a/internal/pkg/sdk3rd/wangsu/certificate/api.go +++ b/internal/pkg/sdk3rd/wangsu/certificate/api.go @@ -8,7 +8,7 @@ import ( func (c *Client) ListCertificates() (*ListCertificatesResponse, error) { resp := &ListCertificatesResponse{} - _, err := c.client.SendRequestWithResult(http.MethodGet, "/api/certificate", nil, resp) + _, err := c.client.SendRequestWithResult(http.MethodGet, "/api/ssl/certificate", nil, resp) if err != nil { return resp, err } diff --git a/internal/pkg/sdk3rd/wangsu/openapi/client.go b/internal/pkg/sdk3rd/wangsu/openapi/client.go index a8f4f2af..09723032 100644 --- a/internal/pkg/sdk3rd/wangsu/openapi/client.go +++ b/internal/pkg/sdk3rd/wangsu/openapi/client.go @@ -30,9 +30,10 @@ type Result interface { func NewClient(accessKey, secretKey string) *Client { client := resty.New(). SetBaseURL("https://open.chinanetcenter.com"). - SetHeader("Host", "open.chinanetcenter.com"). SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). + SetHeader("Host", "open.chinanetcenter.com"). + SetHeader("User-Agent", "certimate"). SetPreRequestHook(func(c *resty.Client, req *http.Request) error { // Step 1: Get request method method := req.Method @@ -85,7 +86,7 @@ func NewClient(accessKey, secretKey string) *Client { // Step 6: Get timestamp var reqtime time.Time - timestampString := req.Header.Get("x-cnc-timestamp") + timestampString := req.Header.Get("X-CNC-Timestamp") if timestampString == "" { reqtime = time.Now().UTC() timestampString = fmt.Sprintf("%d", reqtime.Unix()) @@ -111,9 +112,9 @@ func NewClient(accessKey, secretKey string) *Client { signHex := strings.ToLower(hex.EncodeToString(sign)) // Step 9: Add headers to request - req.Header.Set("x-cnc-accesskey", accessKey) - req.Header.Set("x-cnc-timestamp", timestampString) - req.Header.Set("x-cnc-auth-method", "AKSK") + req.Header.Set("X-CNC-AccessKey", accessKey) + req.Header.Set("X-CNC-Timestamp", timestampString) + req.Header.Set("X-CNC-Auth-Method", "AKSK") req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex)) req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT")) @@ -173,7 +174,7 @@ func (c *Client) SendRequestWithResult(method string, path string, params interf if err != nil { if resp != nil { json.Unmarshal(resp.Body(), &result) - result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + result.SetRequestId(resp.Header().Get("X-CNC-Request-Id")) } return resp, err } @@ -185,6 +186,6 @@ func (c *Client) SendRequestWithResult(method string, path string, params interf } } - result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + result.SetRequestId(resp.Header().Get("X-CNC-Request-Id")) return resp, nil } diff --git a/internal/pkg/utils/cert/converter.go b/internal/pkg/utils/cert/converter.go index b726e86d..0d7e4c53 100644 --- a/internal/pkg/utils/cert/converter.go +++ b/internal/pkg/utils/cert/converter.go @@ -16,7 +16,7 @@ import ( // 出参: // - certPEM: 证书 PEM 内容。 // - err: 错误。 -func ConvertCertificateToPEM(cert *x509.Certificate) (certPEM string, err error) { +func ConvertCertificateToPEM(cert *x509.Certificate) (_certPEM string, _err error) { if cert == nil { return "", errors.New("`cert` is nil") } @@ -37,14 +37,14 @@ func ConvertCertificateToPEM(cert *x509.Certificate) (certPEM string, err error) // 出参: // - privkeyPEM: 私钥 PEM 内容。 // - err: 错误。 -func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPEM string, err error) { +func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (_privkeyPEM string, _err error) { if privkey == nil { return "", errors.New("`privkey` is nil") } - data, err := x509.MarshalECPrivateKey(privkey) - if err != nil { - return "", fmt.Errorf("failed to marshal EC private key: %w", err) + data, _err := x509.MarshalECPrivateKey(privkey) + if _err != nil { + return "", fmt.Errorf("failed to marshal EC private key: %w", _err) } block := &pem.Block{ diff --git a/internal/pkg/utils/cert/extractor.go b/internal/pkg/utils/cert/extractor.go index 94d0a8da..a4077d37 100644 --- a/internal/pkg/utils/cert/extractor.go +++ b/internal/pkg/utils/cert/extractor.go @@ -14,7 +14,7 @@ import ( // - serverCertPEM: 服务器证书的 PEM 内容。 // - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 -func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermediaCertPEM string, err error) { +func ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) { pemBlocks := make([]*pem.Block, 0) pemData := []byte(certPEM) for { @@ -27,8 +27,8 @@ func ExtractCertificatesFromPEM(certPEM string) (serverCertPEM string, intermedi pemData = rest } - serverCertPEM = "" - intermediaCertPEM = "" + serverCertPEM := "" + intermediaCertPEM := "" if len(pemBlocks) == 0 { return "", "", errors.New("failed to decode PEM block") diff --git a/internal/pkg/utils/cert/parser.go b/internal/pkg/utils/cert/parser.go index eb743f78..3ecb8639 100644 --- a/internal/pkg/utils/cert/parser.go +++ b/internal/pkg/utils/cert/parser.go @@ -21,7 +21,7 @@ import ( // 出参: // - cert: x509.Certificate 对象。 // - err: 错误。 -func ParseCertificateFromPEM(certPEM string) (cert *x509.Certificate, err error) { +func ParseCertificateFromPEM(certPEM string) (_cert *x509.Certificate, _err error) { pemData := []byte(certPEM) block, _ := pem.Decode(pemData) @@ -29,7 +29,7 @@ func ParseCertificateFromPEM(certPEM string) (cert *x509.Certificate, err error) return nil, errors.New("failed to decode PEM block") } - cert, err = x509.ParseCertificate(block.Bytes) + cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } @@ -45,7 +45,7 @@ func ParseCertificateFromPEM(certPEM string) (cert *x509.Certificate, err error) // 出参: // - privkey: crypto.PrivateKey 对象,可能是 rsa.PrivateKey、ecdsa.PrivateKey 或 ed25519.PrivateKey。 // - err: 错误。 -func ParsePrivateKeyFromPEM(privkeyPEM string) (privkey crypto.PrivateKey, err error) { +func ParsePrivateKeyFromPEM(privkeyPEM string) (_privkey crypto.PrivateKey, _err error) { pemData := []byte(privkeyPEM) return certcrypto.ParsePEMPrivateKey(pemData) } @@ -58,7 +58,7 @@ func ParsePrivateKeyFromPEM(privkeyPEM string) (privkey crypto.PrivateKey, err e // 出参: // - privkey: ecdsa.PrivateKey 对象。 // - err: 错误。 -func ParseECPrivateKeyFromPEM(privkeyPEM string) (privkey *ecdsa.PrivateKey, err error) { +func ParseECPrivateKeyFromPEM(privkeyPEM string) (_privkey *ecdsa.PrivateKey, _err error) { pemData := []byte(privkeyPEM) block, _ := pem.Decode(pemData) @@ -66,7 +66,7 @@ func ParseECPrivateKeyFromPEM(privkeyPEM string) (privkey *ecdsa.PrivateKey, err return nil, errors.New("failed to decode PEM block") } - privkey, err = x509.ParseECPrivateKey(block.Bytes) + privkey, err := x509.ParseECPrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } @@ -82,7 +82,7 @@ func ParseECPrivateKeyFromPEM(privkeyPEM string) (privkey *ecdsa.PrivateKey, err // 出参: // - privkey: rsa.PrivateKey 对象。 // - err: 错误。 -func ParsePKCS1PrivateKeyFromPEM(privkeyPEM string) (privkey *rsa.PrivateKey, err error) { +func ParsePKCS1PrivateKeyFromPEM(privkeyPEM string) (_privkey *rsa.PrivateKey, _err error) { pemData := []byte(privkeyPEM) block, _ := pem.Decode(pemData) @@ -90,7 +90,7 @@ func ParsePKCS1PrivateKeyFromPEM(privkeyPEM string) (privkey *rsa.PrivateKey, er return nil, errors.New("failed to decode PEM block") } - privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + privkey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse private key: %w", err) } diff --git a/internal/pkg/utils/http/transport.go b/internal/pkg/utils/http/transport.go new file mode 100644 index 00000000..ff8c8804 --- /dev/null +++ b/internal/pkg/utils/http/transport.go @@ -0,0 +1,33 @@ +package httputil + +import ( + "net" + "net/http" + "time" +) + +// 创建并返回一个 [http.DefaultTransport] 对象副本。 +// +// 出参: +// - transport: [http.DefaultTransport] 对象副本。 +func NewDefaultTransport() *http.Transport { + if http.DefaultTransport != nil { + if t, ok := http.DefaultTransport.(*http.Transport); ok { + return t.Clone() + } + } + + return &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + ForceAttemptHTTP2: true, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + } +} diff --git a/internal/pkg/utils/type/cast.go b/internal/pkg/utils/type/cast.go index 684e262e..77eb9dad 100644 --- a/internal/pkg/utils/type/cast.go +++ b/internal/pkg/utils/type/cast.go @@ -1,5 +1,7 @@ package typeutil +import "reflect" + // 将对象转换为指针。 // // 入参: @@ -11,6 +13,22 @@ func ToPtr[T any](v T) (p *T) { return &v } +// 将非零值的对象转换为指针。 +// 与 [ToPtr] 不同的是,如果对象的值为零值,则返回 nil。 +// +// 入参: +// - 待转换的对象。 +// +// 出参: +// - 返回对象的指针。 +func ToPtrOrZeroNil[T any](v T) (p *T) { + if reflect.ValueOf(v).IsZero() { + return nil + } + + return &v +} + // 将指针转换为对象。 // // 入参: diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go index c644b26b..b6e4a4db 100644 --- a/internal/workflow/dispatcher/invoker.go +++ b/internal/workflow/dispatcher/invoker.go @@ -98,16 +98,26 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow procErr = processor.Process(ctx) if procErr != nil { - processor.GetLogger().Error(procErr.Error()) + if current.Type != domain.WorkflowNodeTypeCondition { + processor.GetLogger().Error(procErr.Error()) + } break } + + nodeOutputs := processor.GetOutputs() + if len(nodeOutputs) > 0 { + ctx = nodes.AddNodeOutput(ctx, current.Id, nodeOutputs) + } } break } - // TODO: 优化可读性 - if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { + if procErr != nil && current.Type == domain.WorkflowNodeTypeCondition { + current = nil + procErr = nil + return nil + } else if procErr != nil && current.Next != nil && current.Next.Type != domain.WorkflowNodeTypeExecuteResultBranch { return procErr } else if procErr != nil && current.Next != nil && current.Next.Type == domain.WorkflowNodeTypeExecuteResultBranch { current = w.getBranchByType(current.Next.Branches, domain.WorkflowNodeTypeExecuteFailure) diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index d38ece89..8616fbd9 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -3,6 +3,7 @@ package nodeprocessor import ( "context" "fmt" + "strconv" "time" "golang.org/x/exp/maps" @@ -16,6 +17,7 @@ import ( type applyNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer certRepo certificateRepository outputRepo workflowOutputRepository @@ -25,6 +27,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { return &applyNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), @@ -32,7 +35,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) @@ -61,7 +64,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 } @@ -71,6 +74,7 @@ func (n *applyNode) Process(ctx context.Context) error { n.logger.Warn("failed to parse certificate, may be the CA responded error") return err } + certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeWorkflow, Certificate: applyResult.FullChainCertificate, @@ -105,12 +109,15 @@ func (n *applyNode) Process(ctx context.Context) error { } } - n.logger.Info("apply completed") + // 记录中间结果 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) + n.logger.Info("application completed") return nil } -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 { // 比较和上次申请时的关键配置(即影响证书签发的)参数是否一致 currentNodeConfig := n.node.GetConfigForApply() @@ -148,7 +155,12 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + daysLeft := int(expirationTime.Hours() / 24) + // TODO: 优化此处逻辑,[checkCanSkip] 方法不应该修改中间结果,违背单一职责 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + 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) } } } diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index 2bac55fa..d9e8126d 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -2,23 +2,51 @@ package nodeprocessor import ( "context" + "errors" + "fmt" "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/domain/expr" ) type conditionNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewConditionNode(node *domain.WorkflowNode) *conditionNode { return &conditionNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } func (n *conditionNode) Process(ctx context.Context) error { - // 此类型节点不需要执行任何操作,直接返回 + nodeCfg := n.node.GetConfigForCondition() + if nodeCfg.Expression == nil { + n.logger.Info("without any conditions, enter this branch") + return nil + } + + rs, err := n.evalExpr(ctx, nodeCfg.Expression) + if err != nil { + n.logger.Warn(fmt.Sprintf("failed to eval condition expression: %w", err)) + return err + } + + if rs.Value == false { + n.logger.Info("condition not met, skip this branch") + return errors.New("condition not met") // TODO: 错误处理 + } else { + n.logger.Info("condition met, enter this branch") + } + return nil } + +func (n *conditionNode) evalExpr(ctx context.Context, expression expr.Expr) (*expr.EvalResult, error) { + variables := GetNodeOutputs(ctx) + return expression.Eval(variables) +} diff --git a/internal/workflow/node-processor/const.go b/internal/workflow/node-processor/const.go new file mode 100644 index 00000000..62d2d56b --- /dev/null +++ b/internal/workflow/node-processor/const.go @@ -0,0 +1,6 @@ +package nodeprocessor + +const ( + outputKeyForCertificateValidity = "certificate.validity" + outputKeyForCertificateDaysLeft = "certificate.daysLeft" +) diff --git a/internal/workflow/node-processor/context.go b/internal/workflow/node-processor/context.go new file mode 100644 index 00000000..96c40487 --- /dev/null +++ b/internal/workflow/node-processor/context.go @@ -0,0 +1,128 @@ +package nodeprocessor + +import ( + "context" + "sync" +) + +// 定义上下文键类型,避免键冲突 +type workflowContextKey string + +const ( + nodeOutputsKey workflowContextKey = "node_outputs" +) + +// 带互斥锁的节点输出容器 +type nodeOutputsContainer struct { + sync.RWMutex + outputs map[string]map[string]any +} + +// 创建新的并发安全的节点输出容器 +func newNodeOutputsContainer() *nodeOutputsContainer { + return &nodeOutputsContainer{ + outputs: make(map[string]map[string]any), + } +} + +// 添加节点输出到上下文 +func AddNodeOutput(ctx context.Context, nodeId string, output map[string]any) context.Context { + container := getNodeOutputsContainer(ctx) + if container == nil { + container = newNodeOutputsContainer() + } + + container.Lock() + defer container.Unlock() + + // 创建输出的深拷贝 + // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 + outputCopy := make(map[string]any, len(output)) + for k, v := range output { + outputCopy[k] = v + } + + container.outputs[nodeId] = outputCopy + return context.WithValue(ctx, nodeOutputsKey, container) +} + +// 从上下文获取节点输出 +func GetNodeOutput(ctx context.Context, nodeId string) map[string]any { + container := getNodeOutputsContainer(ctx) + if container == nil { + return nil + } + + container.RLock() + defer container.RUnlock() + + output, exists := container.outputs[nodeId] + if !exists { + return nil + } + + outputCopy := make(map[string]any, len(output)) + for k, v := range output { + outputCopy[k] = v + } + + 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 { + container := getNodeOutputsContainer(ctx) + if container == nil { + return nil + } + + container.RLock() + defer container.RUnlock() + + // 创建所有输出的深拷贝 + // TODO: 暂时使用浅拷贝,等后续值类型扩充后修改 + allOutputs := make(map[string]map[string]any, len(container.outputs)) + for nodeId, output := range container.outputs { + nodeCopy := make(map[string]any, len(output)) + for k, v := range output { + nodeCopy[k] = v + } + allOutputs[nodeId] = nodeCopy + } + + 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 +} diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index d60a5a7a..f89f4a1f 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -15,6 +15,7 @@ import ( type deployNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer certRepo certificateRepository outputRepo workflowOutputRepository @@ -24,6 +25,7 @@ func NewDeployNode(node *domain.WorkflowNode) *deployNode { return &deployNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), @@ -31,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) @@ -40,8 +42,9 @@ func (n *deployNode) Process(ctx context.Context) error { } // 获取前序节点输出证书 + const DELIMITER = "#" previousNodeOutputCertificateSource := n.node.GetConfigForDeploy().Certificate - previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, "#") + previousNodeOutputCertificateSourceSlice := strings.Split(previousNodeOutputCertificateSource, DELIMITER) if len(previousNodeOutputCertificateSourceSlice) != 2 { n.logger.Warn("invalid certificate source", slog.String("certificate.source", previousNodeOutputCertificateSource)) return fmt.Errorf("invalid certificate source: %s", previousNodeOutputCertificateSource) @@ -76,7 +79,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 } @@ -93,12 +96,11 @@ func (n *deployNode) Process(ctx context.Context) error { return err } - n.logger.Info("deploy completed") - + n.logger.Info("deployment completed") return nil } -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 { // 比较和上次部署时的关键配置(即影响证书部署的)参数是否一致 currentNodeConfig := n.node.GetConfigForDeploy() diff --git a/internal/workflow/node-processor/execute_failure_node.go b/internal/workflow/node-processor/execute_failure_node.go index 59f6a5bd..40be18ed 100644 --- a/internal/workflow/node-processor/execute_failure_node.go +++ b/internal/workflow/node-processor/execute_failure_node.go @@ -9,18 +9,19 @@ import ( type executeFailureNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewExecuteFailureNode(node *domain.WorkflowNode) *executeFailureNode { return &executeFailureNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } func (n *executeFailureNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("the previous node execution was failed") return nil } diff --git a/internal/workflow/node-processor/execute_success_node.go b/internal/workflow/node-processor/execute_success_node.go index e5b65860..2cd78ff3 100644 --- a/internal/workflow/node-processor/execute_success_node.go +++ b/internal/workflow/node-processor/execute_success_node.go @@ -9,18 +9,19 @@ import ( type executeSuccessNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewExecuteSuccessNode(node *domain.WorkflowNode) *executeSuccessNode { return &executeSuccessNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } func (n *executeSuccessNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("the previous node execution was succeeded") return nil } diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go new file mode 100644 index 00000000..4b875f26 --- /dev/null +++ b/internal/workflow/node-processor/monitor_node.go @@ -0,0 +1,147 @@ +package nodeprocessor + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "math" + "net/http" + "strconv" + "strings" + "time" + + "github.com/usual2970/certimate/internal/domain" + httputil "github.com/usual2970/certimate/internal/pkg/utils/http" +) + +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 ...") + + nodeCfg := n.node.GetConfigForMonitor() + + targetAddr := fmt.Sprintf("%s:%d", nodeCfg.Host, nodeCfg.Port) + if nodeCfg.Port == 0 { + targetAddr = fmt.Sprintf("%s:443", nodeCfg.Host) + } + + targetDomain := nodeCfg.Domain + if targetDomain == "" { + targetDomain = nodeCfg.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 certs []*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): + } + } + + certs, err = n.tryRetrievePeerCertificates(ctx, targetAddr, targetDomain, nodeCfg.RequestPath) + if err == nil { + break + } + } + + if err != nil { + n.logger.Warn("failed to monitor certificate") + return err + } else { + if len(certs) == 0 { + n.logger.Warn("no ssl certificates retrieved in http response") + + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(false) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(0, 10) + } else { + cert := certs[0] // 只取证书链中的第一个证书,即服务器证书 + 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)) + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(validated) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) + + 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) tryRetrievePeerCertificates(ctx context.Context, addr, domain, requestPath string) ([]*x509.Certificate, error) { + transport := httputil.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Timeout: 30 * time.Second, + Transport: transport, + } + + 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(err.Error()) + 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(err.Error()) + return nil, err + } + defer resp.Body.Close() + + if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 { + return make([]*x509.Certificate, 0), nil + } + return resp.TLS.PeerCertificates, nil +} 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 1840938b..dabfd034 100644 --- a/internal/workflow/node-processor/notify_node.go +++ b/internal/workflow/node-processor/notify_node.go @@ -12,6 +12,7 @@ import ( type notifyNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer settingsRepo settingsRepository } @@ -20,17 +21,18 @@ func NewNotifyNode(node *domain.WorkflowNode) *notifyNode { return ¬ifyNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), settingsRepo: repository.NewSettingsRepository(), } } 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() + nodeCfg := n.node.GetConfigForNotify() - if nodeConfig.Provider == "" { + if nodeCfg.Provider == "" { // Deprecated: v0.4.x 将废弃 // 兼容旧版本的通知渠道 n.logger.Warn("WARNING! you are using the notification channel from global settings, which will be deprecated in the future") @@ -42,18 +44,18 @@ func (n *notifyNode) Process(ctx context.Context) error { } // 获取通知渠道 - channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel) + channelConfig, err := settings.GetNotifyChannelConfig(nodeCfg.Channel) if err != nil { return err } // 发送通知 - if err := notify.SendToChannel(nodeConfig.Subject, nodeConfig.Message, nodeConfig.Channel, channelConfig); err != nil { - n.logger.Warn("failed to notify", slog.String("channel", nodeConfig.Channel)) + if err := notify.SendToChannel(nodeCfg.Subject, nodeCfg.Message, nodeCfg.Channel, channelConfig); err != nil { + n.logger.Warn("failed to send notification", slog.String("channel", nodeCfg.Channel)) return err } - n.logger.Info("notify completed") + n.logger.Info("notification completed") return nil } @@ -61,8 +63,8 @@ func (n *notifyNode) Process(ctx context.Context) error { deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{ Node: n.node, Logger: n.logger, - Subject: nodeConfig.Subject, - Message: nodeConfig.Message, + Subject: nodeCfg.Subject, + Message: nodeCfg.Message, }) if err != nil { n.logger.Warn("failed to create notifier provider") @@ -71,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 f98aebae..d375883f 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -14,6 +14,8 @@ type NodeProcessor interface { SetLogger(*slog.Logger) Process(ctx context.Context) error + + GetOutputs() map[string]any } type nodeProcessor struct { @@ -32,6 +34,20 @@ func (n *nodeProcessor) SetLogger(logger *slog.Logger) { n.logger = logger } +type nodeOutputer struct { + outputs map[string]any +} + +func newNodeOutputer() *nodeOutputer { + return &nodeOutputer{ + outputs: make(map[string]any), + } +} + +func (n *nodeOutputer) GetOutputs() map[string]any { + return n.outputs +} + type certificateRepository interface { GetByWorkflowNodeId(ctx context.Context, workflowNodeId string) (*domain.Certificate, error) GetByWorkflowRunId(ctx context.Context, workflowRunId string) (*domain.Certificate, error) @@ -58,23 +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 } - 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/start_node.go b/internal/workflow/node-processor/start_node.go index 5bbc1c09..bdfea1b7 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -9,18 +9,20 @@ import ( type startNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer } func NewStartNode(node *domain.WorkflowNode) *startNode { return &startNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), } } func (n *startNode) Process(ctx context.Context) error { // 此类型节点不需要执行任何操作,直接返回 - n.logger.Info("ready to start ...") + n.logger.Info("workflow is started") return nil } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index a1878a41..9431d31a 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -3,7 +3,9 @@ package nodeprocessor import ( "context" "fmt" + "strconv" "strings" + "time" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/repository" @@ -12,6 +14,7 @@ import ( type uploadNode struct { node *domain.WorkflowNode *nodeProcessor + *nodeOutputer certRepo certificateRepository outputRepo workflowOutputRepository @@ -21,6 +24,7 @@ func NewUploadNode(node *domain.WorkflowNode) *uploadNode { return &uploadNode{ node: node, nodeProcessor: newNodeProcessor(node), + nodeOutputer: newNodeOutputer(), certRepo: repository.NewCertificateRepository(), outputRepo: repository.NewWorkflowOutputRepository(), @@ -28,9 +32,9 @@ 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() + nodeCfg := n.node.GetConfigForUpload() // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) @@ -40,7 +44,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)) @@ -50,7 +54,7 @@ func (n *uploadNode) Process(ctx context.Context) error { certificate := &domain.Certificate{ Source: domain.CertificateSourceTypeUpload, } - certificate.PopulateFromPEM(nodeConfig.Certificate, nodeConfig.PrivateKey) + certificate.PopulateFromPEM(nodeCfg.Certificate, nodeCfg.PrivateKey) // 保存执行结果 output := &domain.WorkflowOutput{ @@ -66,12 +70,15 @@ func (n *uploadNode) Process(ctx context.Context) error { return err } - n.logger.Info("upload completed") + // 记录中间结果 + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(true) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(time.Until(certificate.ExpireAt).Hours()/24), 10) + n.logger.Info("uploading completed") return nil } -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 { // 比较和上次上传时的关键配置(即影响证书上传的)参数是否一致 currentNodeConfig := n.node.GetConfigForUpload() @@ -85,6 +92,10 @@ func (n *uploadNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workfl lastCertificate, _ := n.certRepo.GetByWorkflowRunId(ctx, lastOutput.RunId) if lastCertificate != nil { + daysLeft := int(time.Until(lastCertificate.ExpireAt).Hours() / 24) + n.outputs[outputKeyForCertificateValidity] = strconv.FormatBool(daysLeft > 0) + n.outputs[outputKeyForCertificateDaysLeft] = strconv.FormatInt(int64(daysLeft), 10) + return true, "the certificate has already been uploaded" } } diff --git a/main.go b/main.go index 76f7f1c0..18e88bed 100644 --- a/main.go +++ b/main.go @@ -26,9 +26,7 @@ func main() { app := app.GetApp().(*pocketbase.PocketBase) var flagHttp string - var flagDir string flag.StringVar(&flagHttp, "http", "127.0.0.1:8090", "HTTP server address") - flag.StringVar(&flagDir, "dir", "/pb_data/database", "Pocketbase data directory") if len(os.Args) < 2 { slog.Error("[CERTIMATE] missing exec args") os.Exit(1) @@ -59,14 +57,17 @@ func main() { Priority: 999, }) + app.OnServe().BindFunc(func(e *core.ServeEvent) error { + slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) + return e.Next() + }) + app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error { routes.Unregister() slog.Info("[CERTIMATE] Exit!") return e.Next() }) - slog.Info("[CERTIMATE] Visit the website: http://" + flagHttp) - if err := app.Start(); err != nil { slog.Error("[CERTIMATE] Start failed.", "err", err) } diff --git a/ui/public/imgs/workflow/tpl-blank.png b/ui/public/imgs/workflow/tpl-blank.png index 8f683ce6..ee6568a9 100644 Binary files a/ui/public/imgs/workflow/tpl-blank.png and b/ui/public/imgs/workflow/tpl-blank.png differ diff --git a/ui/public/imgs/workflow/tpl-certtest.png b/ui/public/imgs/workflow/tpl-certtest.png new file mode 100644 index 00000000..da7bfada Binary files /dev/null and b/ui/public/imgs/workflow/tpl-certtest.png differ diff --git a/ui/public/imgs/workflow/tpl-standard.png b/ui/public/imgs/workflow/tpl-standard.png index 46698a87..c14238ff 100644 Binary files a/ui/public/imgs/workflow/tpl-standard.png and b/ui/public/imgs/workflow/tpl-standard.png differ diff --git a/ui/src/components/MultipleInput.tsx b/ui/src/components/MultipleInput.tsx index d28db745..d8be12fa 100644 --- a/ui/src/components/MultipleInput.tsx +++ b/ui/src/components/MultipleInput.tsx @@ -152,10 +152,10 @@ const MultipleInput = ({ value={element} onBlur={() => handleInputBlur(index)} onChange={(val) => handleChange(index, val)} - onClickAdd={() => handleClickAdd(index)} - onClickDown={() => handleClickDown(index)} - onClickUp={() => handleClickUp(index)} - onClickRemove={() => handleClickRemove(index)} + onEntryAdd={() => handleClickAdd(index)} + onEntryDown={() => handleClickDown(index)} + onEntryUp={() => handleClickUp(index)} + onEntryRemove={() => handleClickRemove(index)} /> ); })} @@ -174,10 +174,10 @@ type MultipleInputItemProps = Omit< defaultValue?: string; value?: string; onChange?: (value: string) => void; - onClickAdd?: () => void; - onClickDown?: () => void; - onClickUp?: () => void; - onClickRemove?: () => void; + onEntryAdd?: () => void; + onEntryDown?: () => void; + onEntryUp?: () => void; + onEntryRemove?: () => void; }; type MultipleInputItemInstance = { @@ -197,10 +197,10 @@ const MultipleInputItem = forwardRef { if (!showSortButton) return null; - return diff --git a/ui/src/components/access/AccessFormWebhookConfig.tsx b/ui/src/components/access/AccessFormWebhookConfig.tsx index d79f848d..7ceec29a 100644 --- a/ui/src/components/access/AccessFormWebhookConfig.tsx +++ b/ui/src/components/access/AccessFormWebhookConfig.tsx @@ -224,7 +224,24 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa ); break; - case "serverchan": + case "serverchan3": + formInst.setFieldValue("url", "https://.push.ft07.com/send/.send"); + formInst.setFieldValue("method", "POST"); + formInst.setFieldValue("headers", "Content-Type: application/json"); + formInst.setFieldValue( + "defaultDataForNotification", + JSON.stringify( + { + title: "${SUBJECT}", + desp: "${MESSAGE}", + }, + null, + 2 + ) + ); + break; + + case "serverchanturbo": formInst.setFieldValue("url", "https://sctapi.ftqq.com/.send"); formInst.setFieldValue("method", "POST"); formInst.setFieldValue("headers", "Content-Type: application/json"); @@ -329,9 +346,9 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
({ + items: ["bark", "ntfy", "gotify", "pushover", "pushplus", "serverchan3", "serverchanturbo", "common"].map((key) => ({ key, - label: t(`access.form.webhook_preset_data.option.${key}.label`), + label: , onClick: () => handlePresetDataForNotificationClick(key), })), }} diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx index 7c112bbe..01f30249 100644 --- a/ui/src/components/access/AccessSelect.tsx +++ b/ui/src/components/access/AccessSelect.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type AccessModel } from "@/domain/access"; import { accessProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type AccessTypeSelectProps = Omit< }; const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { + const { token: themeToken } = theme.useToken(); + const { accesses, loadedAtOnce, fetchAccesses } = useAccessesStore(useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses"])); useEffect(() => { fetchAccesses(); @@ -37,7 +39,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { if (!access) { return ( - + {key} @@ -48,7 +50,7 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { const provider = accessProvidersMap.get(access.provider); return ( - + {access.name} @@ -65,12 +67,12 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { const value = inputValue.toLowerCase(); return option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (label) { + labelRender={({ value }) => { + if (value != null) { return renderOption(value as string); } - return {props.placeholder}; + return {props.placeholder}; }} loading={!loadedAtOnce} options={options} 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..227bfcdd 100644 --- a/ui/src/components/provider/ACMEDns01ProviderSelect.tsx +++ b/ui/src/components/provider/ACMEDns01ProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type ACMEDns01ProviderSelectProps = Omit< const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(acmeDns01ProvidersMap.values()); @@ -32,7 +34,7 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr const provider = acmeDns01ProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} @@ -49,12 +51,12 @@ const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectPr const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} 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..055b3ddc 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Tag, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Tag, Typography, theme } from "antd"; import Show from "@/components/Show"; import { ACCESS_USAGES, type AccessProvider, type AccessUsageType, accessProvidersMap } from "@/domain/provider"; @@ -16,6 +16,8 @@ export type AccessProviderSelectProps = Omit< const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProviderSelectProps = { showOptionTags: true }) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(accessProvidersMap.values()); @@ -49,12 +51,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid return (
- + {t(provider.name)} -
+
{t("access.props.provider.builtin")} @@ -84,12 +86,12 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx index 15d31230..d1fdbba9 100644 --- a/ui/src/components/provider/CAProviderSelect.tsx +++ b/ui/src/components/provider/CAProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type CAProvider, caProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type CAProviderSelectProps = Omit< const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(caProvidersMap.values()); @@ -48,7 +50,7 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const provider = caProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} @@ -65,12 +67,12 @@ const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder || t("provider.default_ca_provider.label")}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} 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..07fa4577 100644 --- a/ui/src/components/provider/DeploymentProviderSelect.tsx +++ b/ui/src/components/provider/DeploymentProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type DeploymentProviderSelectProps = Omit< const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(deploymentProvidersMap.values()); @@ -32,7 +34,7 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect const provider = deploymentProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} @@ -49,12 +51,12 @@ const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelect const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/provider/NotificationProviderSelect.tsx b/ui/src/components/provider/NotificationProviderSelect.tsx index 98a1005c..8b0dd353 100644 --- a/ui/src/components/provider/NotificationProviderSelect.tsx +++ b/ui/src/components/provider/NotificationProviderSelect.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; +import { Avatar, Select, type SelectProps, Space, Typography, theme } from "antd"; import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider"; @@ -14,6 +14,8 @@ export type NotificationProviderSelectProps = Omit< const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSelectProps) => { const { t } = useTranslation(); + const { token: themeToken } = theme.useToken(); + const [options, setOptions] = useState>([]); useEffect(() => { const allItems = Array.from(notificationProvidersMap.values()); @@ -32,7 +34,7 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe const provider = notificationProvidersMap.get(key); return ( - + {t(provider?.name ?? "")} @@ -49,12 +51,12 @@ const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSe const value = inputValue.toLowerCase(); return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value); }} - labelRender={({ label, value }) => { - if (!label) { - return {props.placeholder}; + labelRender={({ value }) => { + if (value != null) { + return renderOption(value as string); } - return renderOption(value as string); + return {props.placeholder}; }} options={options} optionFilterProp={undefined} diff --git a/ui/src/components/workflow/WorkflowElement.tsx b/ui/src/components/workflow/WorkflowElement.tsx index 3aa70ff3..86720f6d 100644 --- a/ui/src/components/workflow/WorkflowElement.tsx +++ b/ui/src/components/workflow/WorkflowElement.tsx @@ -9,8 +9,10 @@ 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"; export type WorkflowElementProps = { @@ -32,6 +34,9 @@ const WorkflowElement = ({ node, disabled, branchId, branchIndex }: WorkflowElem case WorkflowNodeType.Upload: return ; + case WorkflowNodeType.Monitor: + return ; + case WorkflowNodeType.Deploy: return ; @@ -56,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/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 2d421880..746adb4c 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -36,7 +36,7 @@ import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; -import type { WorkflowLogModel } from "@/domain/workflowLog"; +import { type WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; import { useBrowserTheme } from "@/hooks"; import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; diff --git a/ui/src/components/workflow/node/AddNode.tsx b/ui/src/components/workflow/node/AddNode.tsx index fb697e19..207ec7c7 100644 --- a/ui/src/components/workflow/node/AddNode.tsx +++ b/ui/src/components/workflow/node/AddNode.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { CloudUploadOutlined as CloudUploadOutlinedIcon, DeploymentUnitOutlined as DeploymentUnitOutlinedIcon, + MonitorOutlined as MonitorOutlinedIcon, PlusOutlined as PlusOutlinedIcon, SendOutlined as SendOutlinedIcon, SisternodeOutlined as SisternodeOutlinedIcon, @@ -27,13 +28,21 @@ const AddNode = ({ node, disabled }: AddNodeProps) => { return [ [WorkflowNodeType.Apply, "workflow_node.apply.label", ], [WorkflowNodeType.Upload, "workflow_node.upload.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", ], [WorkflowNodeType.ExecuteResultBranch, "workflow_node.execute_result_branch.label", ], ] .filter(([type]) => { - if (node.type !== WorkflowNodeType.Apply && node.type !== WorkflowNodeType.Deploy && node.type !== WorkflowNodeType.Notify) { + const hasExecuteResult = [ + WorkflowNodeType.Apply, + WorkflowNodeType.Upload, + WorkflowNodeType.Monitor, + WorkflowNodeType.Deploy, + WorkflowNodeType.Notify, + ].includes(node.type); + if (!hasExecuteResult) { return type !== WorkflowNodeType.ExecuteResultBranch; } diff --git a/ui/src/components/workflow/node/ApplyNode.tsx b/ui/src/components/workflow/node/ApplyNode.tsx index c250fd89..ff0d64bf 100644 --- a/ui/src/components/workflow/node/ApplyNode.tsx +++ b/ui/src/components/workflow/node/ApplyNode.tsx @@ -38,9 +38,9 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForApply; const handleDrawerConfirm = async () => { setFormPending(true); @@ -74,12 +74,12 @@ const ApplyNode = ({ node, disabled }: ApplyNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index 7faa148e..ae56efc3 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -56,7 +56,7 @@ export type ApplyNodeConfigFormInstance = { validateFields: FormInstance["validateFields"]; }; -const MULTIPLE_INPUT_DELIMITER = ";"; +const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): ApplyNodeConfigFormFieldValues => { return { @@ -76,7 +76,7 @@ const ApplyNodeConfigForm = forwardRef { if (!v) return false; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validDomainName(e, { allowWildcard: true })); }, t("common.errmsg.domain_invalid")), contactEmail: z.string({ message: t("workflow_node.apply.form.contact_email.placeholder") }).email(t("common.errmsg.email_invalid")), @@ -106,7 +106,7 @@ const ApplyNodeConfigForm = forwardRef { if (!v) return true; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validIPv4Address(e) || validIPv6Address(e) || validDomainName(e)); }, t("common.errmsg.host_invalid")), dnsPropagationWait: z.preprocess( diff --git a/ui/src/components/workflow/node/ConditionNode.tsx b/ui/src/components/workflow/node/ConditionNode.tsx index 56639692..2c8b3d81 100644 --- a/ui/src/components/workflow/node/ConditionNode.tsx +++ b/ui/src/components/workflow/node/ConditionNode.tsx @@ -1,9 +1,14 @@ -import { memo } from "react"; -import { MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; +import { memo, useRef, useState } from "react"; +import { FilterFilled as FilterFilledIcon, FilterOutlined as FilterOutlinedIcon, MoreOutlined as MoreOutlinedIcon } from "@ant-design/icons"; import { Button, Card, Popover } from "antd"; +import { produce } from "immer"; + +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; import SharedNode, { type SharedNodeProps } from "./_SharedNode"; import AddNode from "./AddNode"; +import ConditionNodeConfigForm, { type ConditionNodeConfigFormFieldValues, type ConditionNodeConfigFormInstance } from "./ConditionNodeConfigForm"; export type ConditionNodeProps = SharedNodeProps & { branchId: string; @@ -11,12 +16,41 @@ export type ConditionNodeProps = SharedNodeProps & { }; const ConditionNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - // TODO: 条件分支 + const { updateNode } = useWorkflowStore(useZustandShallowSelector(["updateNode"])); + + const [formPending, setFormPending] = useState(false); + const formRef = useRef(null); + const getFormValues = () => formRef.current!.getFieldsValue() as ConditionNodeConfigFormFieldValues; + + const [drawerOpen, setDrawerOpen] = useState(false); + + const handleDrawerConfirm = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + throw err; + } + + try { + const newValues = getFormValues(); + const newNode = produce(node, (draft) => { + draft.config = { + ...newValues, + }; + draft.validated = true; + }); + await updateNode(newNode); + } finally { + setFormPending(false); + } + }; return ( <> - + setDrawerOpen(true)}>
- +
e.stopPropagation()}> + +
setDrawerOpen(true)}> + {node.config?.expression ? ( +
+
+ setDrawerOpen(open)} + > + + + ); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx new file mode 100644 index 00000000..3cd92d7b --- /dev/null +++ b/ui/src/components/workflow/node/ConditionNodeConfigForm.tsx @@ -0,0 +1,76 @@ +import { forwardRef, memo, useImperativeHandle, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type Expr, type WorkflowNodeConfigForCondition } from "@/domain/workflow"; +import { useAntdForm } from "@/hooks"; + +import ConditionNodeConfigFormExpressionEditor, { type ConditionNodeConfigFormExpressionEditorInstance } from "./ConditionNodeConfigFormExpressionEditor"; + +export type ConditionNodeConfigFormFieldValues = { + expression?: Expr | undefined; +}; + +export type ConditionNodeConfigFormProps = { + className?: string; + style?: React.CSSProperties; + disabled?: boolean; + initialValues?: Partial; + nodeId: string; + onValuesChange?: (values: WorkflowNodeConfigForCondition) => void; +}; + +export type ConditionNodeConfigFormInstance = { + getFieldsValue: () => ReturnType["getFieldsValue"]>; + resetFields: FormInstance["resetFields"]; + validateFields: FormInstance["validateFields"]; +}; + +const initFormModel = (): ConditionNodeConfigFormFieldValues => { + return {}; +}; + +const ConditionNodeConfigForm = forwardRef( + ({ className, style, disabled, initialValues, nodeId, onValuesChange }, ref) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + expression: z.any().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm({ + name: "workflowNodeConditionConfigForm", + initialValues: initialValues ?? initFormModel(), + }); + + const editorRef = useRef(null); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + useImperativeHandle(ref, () => { + return { + getFieldsValue: formInst.getFieldsValue, + resetFields: formInst.resetFields, + validateFields: (nameList, config) => { + const t1 = formInst.validateFields(nameList, config); + const t2 = editorRef.current!.validate(); + return Promise.all([t1, t2]).then(() => t1); + }, + } as ConditionNodeConfigFormInstance; + }); + + return ( +
+ + + +
+ ); + } +); + +export default memo(ConditionNodeConfigForm); diff --git a/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx new file mode 100644 index 00000000..1696d46c --- /dev/null +++ b/ui/src/components/workflow/node/ConditionNodeConfigFormExpressionEditor.tsx @@ -0,0 +1,400 @@ +import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { CloseOutlined as CloseOutlinedIcon, PlusOutlined } from "@ant-design/icons"; +import { useControllableValue } from "ahooks"; +import { Button, Form, Input, Radio, Select, theme } from "antd"; + +import Show from "@/components/Show"; +import type { Expr, ExprComparisonOperator, ExprLogicalOperator, ExprValue, ExprValueSelector, ExprValueType } from "@/domain/workflow"; +import { ExprType } from "@/domain/workflow"; +import { useAntdFormName, useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +export type ConditionNodeConfigFormExpressionEditorProps = { + className?: string; + style?: React.CSSProperties; + defaultValue?: Expr; + disabled?: boolean; + nodeId: string; + value?: Expr; + onChange?: (value: Expr) => void; +}; + +export type ConditionNodeConfigFormExpressionEditorInstance = { + validate: () => Promise; +}; + +// 表单内部使用的扁平结构 +type ConditionItem = { + // 选择器,格式为 "${nodeId}#${outputName}#${valueType}" + // 将 [ExprValueSelector] 转为字符串形式,以便于结构化存储。 + leftSelector?: string; + // 比较运算符。 + operator?: ExprComparisonOperator; + // 值。 + // 将 [ExprValue] 转为字符串形式,以便于结构化存储。 + rightValue?: string; +}; + +type ConditionFormValues = { + conditions: ConditionItem[]; + logicalOperator: ExprLogicalOperator; +}; + +const initFormModel = (): ConditionFormValues => { + return { + conditions: [{}], + logicalOperator: "and", + }; +}; + +const exprToFormValues = (expr?: Expr): ConditionFormValues => { + if (!expr) return initFormModel(); + + const conditions: ConditionItem[] = []; + let logicalOp: ExprLogicalOperator = "and"; + + const extractExpr = (expr: Expr): void => { + if (expr.type === ExprType.Comparison) { + if (expr.left.type == ExprType.Variant && expr.right.type == ExprType.Constant) { + conditions.push({ + leftSelector: expr.left.selector?.id != null ? `${expr.left.selector.id}#${expr.left.selector.name}#${expr.left.selector.type}` : undefined, + operator: expr.operator != null ? expr.operator : undefined, + rightValue: expr.right?.value != null ? String(expr.right.value) : undefined, + }); + } else { + console.warn("[certimate] invalid comparison expression: left must be a variant and right must be a constant", expr); + } + } else if (expr.type === ExprType.Logical) { + logicalOp = expr.operator || "and"; + extractExpr(expr.left); + extractExpr(expr.right); + } + }; + + extractExpr(expr); + + return { + conditions: conditions, + logicalOperator: logicalOp, + }; +}; + +const formValuesToExpr = (values: ConditionFormValues): Expr | undefined => { + const wrapExpr = (condition: ConditionItem): Expr => { + const [id, name, type] = (condition.leftSelector?.split("#") ?? ["", "", ""]) as [string, string, ExprValueType]; + const valid = !!id && !!name && !!type; + + const left: Expr = { + type: ExprType.Variant, + selector: valid + ? { + id: id, + name: name, + type: type, + } + : ({} as ExprValueSelector), + }; + + const right: Expr = { + type: ExprType.Constant, + value: condition.rightValue!, + valueType: type, + }; + + return { + type: ExprType.Comparison, + operator: condition.operator!, + left, + right, + }; + }; + + if (values.conditions.length === 0) { + return undefined; + } + + // 只有一个条件时,直接返回比较表达式 + if (values.conditions.length === 1) { + const { leftSelector, operator, rightValue } = values.conditions[0]; + if (!leftSelector || !operator || !rightValue) { + return undefined; + } + return wrapExpr(values.conditions[0]); + } + + // 多个条件时,通过逻辑运算符连接 + let expr: Expr = wrapExpr(values.conditions[0]); + for (let i = 1; i < values.conditions.length; i++) { + expr = { + type: ExprType.Logical, + operator: values.logicalOperator, + left: expr, + right: wrapExpr(values.conditions[i]), + }; + } + return expr; +}; + +const ConditionNodeConfigFormExpressionEditor = forwardRef( + ({ className, style, disabled, nodeId, ...props }, ref) => { + const { t } = useTranslation(); + + const { token: themeToken } = theme.useToken(); + + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + + const [value, setValue] = useControllableValue(props, { + valuePropName: "value", + defaultValuePropName: "defaultValue", + trigger: "onChange", + }); + + const [formInst] = Form.useForm(); + const formName = useAntdFormName({ form: formInst, name: "workflowNodeConditionConfigFormExpressionEditorForm" }); + const [formModel, setFormModel] = useState(initFormModel()); + + useEffect(() => { + if (value) { + const formValues = exprToFormValues(value); + formInst.setFieldsValue(formValues); + setFormModel(formValues); + } else { + formInst.resetFields(); + setFormModel(initFormModel()); + } + }, [value]); + + const ciSelectorCandidates = useMemo(() => { + const previousNodes = getWorkflowOuptutBeforeId(nodeId); + return previousNodes + .map((node) => { + const group = { + label: node.name, + options: Array<{ label: string; value: string }>(), + }; + + for (const output of node.outputs ?? []) { + switch (output.type) { + case "certificate": + group.options.push({ + label: `${output.label} - ${t("workflow.variables.selector.validity.label")}`, + value: `${node.id}#${output.name}.validity#boolean`, + }); + group.options.push({ + label: `${output.label} - ${t("workflow.variables.selector.days_left.label")}`, + value: `${node.id}#${output.name}.daysLeft#number`, + }); + break; + + default: + group.options.push({ + label: `${output.label}`, + value: `${node.id}#${output.name}#${output.type}`, + }); + console.warn("[certimate] invalid workflow output type in condition expressions", output); + break; + } + } + + return group; + }) + .filter((item) => item.options.length > 0); + }, [nodeId]); + + const getValueTypeBySelector = (selector: string): ExprValueType | undefined => { + if (!selector) return; + + const parts = selector.split("#"); + if (parts.length >= 3) { + return parts[2].toLowerCase() as ExprValueType; + } + }; + + const getOperatorsBySelector = (selector: string): { value: ExprComparisonOperator; label: string }[] => { + const valueType = getValueTypeBySelector(selector); + return getOperatorsByValueType(valueType!); + }; + + const getOperatorsByValueType = (valueType: ExprValue): { value: ExprComparisonOperator; label: string }[] => { + switch (valueType) { + case "number": + return [ + { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") }, + { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") }, + { value: "gt", label: t("workflow_node.condition.form.expression.operator.option.gt.label") }, + { value: "gte", label: t("workflow_node.condition.form.expression.operator.option.gte.label") }, + { value: "lt", label: t("workflow_node.condition.form.expression.operator.option.lt.label") }, + { value: "lte", label: t("workflow_node.condition.form.expression.operator.option.lte.label") }, + ]; + + case "string": + return [ + { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.label") }, + { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.label") }, + ]; + + case "boolean": + return [ + { value: "eq", label: t("workflow_node.condition.form.expression.operator.option.eq.alias_is_label") }, + { value: "neq", label: t("workflow_node.condition.form.expression.operator.option.neq.alias_not_label") }, + ]; + + default: + return []; + } + }; + + const handleFormChange = (_: undefined, values: ConditionFormValues) => { + setValue(formValuesToExpr(values)); + }; + + useImperativeHandle(ref, () => { + return { + validate: async () => { + await formInst.validateFields(); + }, + } as ConditionNodeConfigFormExpressionEditorInstance; + }); + + return ( +
+ 1}> + + + {t("workflow_node.condition.form.expression.logical_operator.option.and.label")} + {t("workflow_node.condition.form.expression.logical_operator.option.or.label")} + + + + + + {(fields, { add, remove }) => ( +
+ {fields.map(({ key, name: index, ...rest }) => ( +
+ {/* 左:变量选择器 */} + + + + ); + }} + + + {/* 右:输入控件,根据变量类型决定组件 */} + { + return prevValues.conditions?.[index]?.leftSelector !== currentValues.conditions?.[index]?.leftSelector; + }} + > + {({ getFieldValue }) => { + const leftSelector = getFieldValue(["conditions", index, "leftSelector"]); + const valueType = getValueTypeBySelector(leftSelector); + + return ( + + {valueType === "string" ? ( + + ) : valueType === "number" ? ( + + ) : valueType === "boolean" ? ( + + ) : ( + + )} + + ); + }} + + +
+ ))} + + + + +
+ )} +
+
+ ); + } +); + +export default ConditionNodeConfigFormExpressionEditor; diff --git a/ui/src/components/workflow/node/DeployNode.tsx b/ui/src/components/workflow/node/DeployNode.tsx index 92eb2890..f495c040 100644 --- a/ui/src/components/workflow/node/DeployNode.tsx +++ b/ui/src/components/workflow/node/DeployNode.tsx @@ -24,10 +24,10 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy; const [drawerOpen, setDrawerOpen] = useState(false); const [drawerFooterShow, setDrawerFooterShow] = useState(true); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForDeploy; useEffect(() => { setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); @@ -46,7 +46,7 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { const provider = deploymentProvidersMap.get(config.provider); return ( - + {t(provider?.name ?? "")} ); @@ -86,8 +86,9 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => { { setDrawerFooterShow(!!(node.config as WorkflowNodeConfigForDeploy)?.provider); setDrawerOpen(open); }} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 0443327e..33fefcf0 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -1,7 +1,7 @@ import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons"; -import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography } from "antd"; +import { Button, Divider, Flex, Form, type FormInstance, Select, Switch, Tooltip, Typography, theme } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -11,7 +11,7 @@ import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPi import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx"; import Show from "@/components/Show"; import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider"; -import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow"; +import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow"; import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks"; import { useWorkflowStore } from "@/stores/workflow"; @@ -125,14 +125,9 @@ const DeployNodeConfigForm = forwardRef { const { t } = useTranslation(); - const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); + const { token: themeToken } = theme.useToken(); - // TODO: 优化此处逻辑 - const [previousNodes, setPreviousNodes] = useState([]); - useEffect(() => { - const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate"); - setPreviousNodes(previousNodes); - }, [nodeId]); + const { getWorkflowOuptutBeforeId } = useWorkflowStore(useZustandShallowSelector(["updateNode", "getWorkflowOuptutBeforeId"])); const formSchema = z.object({ certificate: z @@ -170,6 +165,24 @@ const DeployNodeConfigForm = forwardRef { + const previousNodes = getWorkflowOuptutBeforeId(nodeId, "certificate"); + return previousNodes + .filter((node) => node.type === WorkflowNodeType.Apply || node.type === WorkflowNodeType.Upload) + .map((item) => { + return { + label: item.name, + options: (item.outputs ?? [])?.map((output) => { + return { + label: output.label, + value: `${item.id}#${output.name}`, + }; + }), + }; + }) + .filter((group) => group.options.length > 0); + }, [nodeId]); + const [nestedFormInst] = Form.useForm(); const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeDeployConfigFormProviderConfigForm" }); const nestedFormEl = useMemo(() => { @@ -487,17 +500,15 @@ const DeployNodeConfigForm = forwardRef} > ({ diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx index d64e6eba..36d663b5 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNConfig.tsx @@ -18,7 +18,7 @@ export type DeployNodeConfigFormWangsuCDNConfigProps = { onValuesChange?: (values: DeployNodeConfigFormWangsuCDNConfigFieldValues) => void; }; -const MULTIPLE_INPUT_DELIMITER = ";"; +const MULTIPLE_INPUT_SEPARATOR = ";"; const initFormModel = (): DeployNodeConfigFormWangsuCDNConfigFieldValues => { return { @@ -42,7 +42,7 @@ const DeployNodeConfigFormWangsuCDNConfig = ({ .refine((v) => { if (!v) return false; return String(v) - .split(MULTIPLE_INPUT_DELIMITER) + .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => validDomainName(e)); }, t("workflow_node.deploy.form.wangsu_cdn_domains.placeholder")), }); diff --git a/ui/src/components/workflow/node/ExecuteResultNode.tsx b/ui/src/components/workflow/node/ExecuteResultNode.tsx index 69a0949c..ce991d95 100644 --- a/ui/src/components/workflow/node/ExecuteResultNode.tsx +++ b/ui/src/components/workflow/node/ExecuteResultNode.tsx @@ -1,5 +1,4 @@ import { memo } from "react"; -import { useTranslation } from "react-i18next"; import { CheckCircleOutlined as CheckCircleOutlinedIcon, CloseCircleOutlined as CloseCircleOutlinedIcon, @@ -17,8 +16,6 @@ export type ConditionNodeProps = SharedNodeProps & { }; const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionNodeProps) => { - const { t } = useTranslation(); - const { token: themeToken } = theme.useToken(); return ( @@ -42,16 +39,15 @@ const ExecuteResultNode = ({ node, disabled, branchId, branchIndex }: ConditionN
{node.type === WorkflowNodeType.ExecuteSuccess ? ( - <> - -
{t("workflow_node.execute_success.label")}
- + ) : ( - <> - -
{t("workflow_node.execute_failure.label")}
- + )} +
diff --git a/ui/src/components/workflow/node/MonitorNode.tsx b/ui/src/components/workflow/node/MonitorNode.tsx new file mode 100644 index 00000000..39fb159e --- /dev/null +++ b/ui/src/components/workflow/node/MonitorNode.tsx @@ -0,0 +1,90 @@ +import { memo, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Flex, Typography } from "antd"; +import { produce } from "immer"; + +import { type WorkflowNodeConfigForMonitor, WorkflowNodeType } from "@/domain/workflow"; +import { useZustandShallowSelector } from "@/hooks"; +import { useWorkflowStore } from "@/stores/workflow"; + +import SharedNode, { type SharedNodeProps } from "./_SharedNode"; +import MonitorNodeConfigForm, { type MonitorNodeConfigFormInstance } from "./MonitorNodeConfigForm"; + +export type MonitorNodeProps = SharedNodeProps; + +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 [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForMonitor; + + const [drawerOpen, setDrawerOpen] = useState(false); + + const wrappedEl = useMemo(() => { + 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 WorkflowNodeConfigForMonitor) ?? {}; + return ( + + {config.domain || config.host || ""} + + ); + }, [node]); + + const handleDrawerConfirm = async () => { + setFormPending(true); + try { + await formRef.current!.validateFields(); + } catch (err) { + setFormPending(false); + throw err; + } + + try { + const newValues = getFormValues(); + const newNode = produce(node, (draft) => { + draft.config = { + ...newValues, + }; + draft.validated = true; + }); + await updateNode(newNode); + } finally { + setFormPending(false); + } + }; + + return ( + <> + setDrawerOpen(true)}> + {wrappedEl} + + + setDrawerOpen(open)} + > + + + + ); +}; + +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..89326b50 100644 --- a/ui/src/components/workflow/node/NotifyNode.tsx +++ b/ui/src/components/workflow/node/NotifyNode.tsx @@ -25,9 +25,9 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForNotify; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Notify) { @@ -43,7 +43,7 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { const provider = notificationProvidersMap.get(config.provider); return ( - + {t(channel?.name ?? provider?.name ?? " ")} {config.subject ?? ""} @@ -82,12 +82,12 @@ const NotifyNode = ({ node, disabled }: NotifyNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/StartNode.tsx b/ui/src/components/workflow/node/StartNode.tsx index 900793fa..4b920bd9 100644 --- a/ui/src/components/workflow/node/StartNode.tsx +++ b/ui/src/components/workflow/node/StartNode.tsx @@ -23,9 +23,9 @@ const StartNode = ({ node, disabled }: StartNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForStart; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Start) { @@ -83,12 +83,12 @@ const StartNode = ({ node, disabled }: StartNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/UnknownNode.tsx b/ui/src/components/workflow/node/UnknownNode.tsx new file mode 100644 index 00000000..2586c6e2 --- /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); + }; + + return ( + <> + +
+ +
+ INVALID NODE +
+ PLEASE REMOVE +
+
+ } + /> +
+ + + + + ); +}; + +export default memo(UnknownNode); diff --git a/ui/src/components/workflow/node/UploadNode.tsx b/ui/src/components/workflow/node/UploadNode.tsx index 0197a8c4..6936f147 100644 --- a/ui/src/components/workflow/node/UploadNode.tsx +++ b/ui/src/components/workflow/node/UploadNode.tsx @@ -23,9 +23,9 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => { const formRef = useRef(null); const [formPending, setFormPending] = useState(false); + const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; const [drawerOpen, setDrawerOpen] = useState(false); - const getFormValues = () => formRef.current!.getFieldsValue() as WorkflowNodeConfigForUpload; const wrappedEl = useMemo(() => { if (node.type !== WorkflowNodeType.Upload) { @@ -74,12 +74,12 @@ const UploadNode = ({ node, disabled }: UploadNodeProps) => { setDrawerOpen(open)} - getFormValues={() => formRef.current!.getFieldsValue()} > diff --git a/ui/src/components/workflow/node/_SharedNode.tsx b/ui/src/components/workflow/node/_SharedNode.tsx index cd067e36..41bb1b8b 100644 --- a/ui/src/components/workflow/node/_SharedNode.tsx +++ b/ui/src/components/workflow/node/_SharedNode.tsx @@ -5,6 +5,7 @@ import { EllipsisOutlined as EllipsisOutlinedIcon, FormOutlined as FormOutlinedIcon, MoreOutlined as MoreOutlinedIcon, + SnippetsOutlined as SnippetsOutlinedIcon, } from "@ant-design/icons"; import { useControllableValue } from "ahooks"; import { Button, Card, Drawer, Dropdown, Input, type InputRef, type MenuProps, Modal, Popover, Space } from "antd"; @@ -33,7 +34,7 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr const handleBlur = (e: React.FocusEvent) => { const oldName = node.name; - const newName = e.target.innerText.trim().substring(0, 64) || oldName; + const newName = e.target.innerText.replaceAll("\r", "").replaceAll("\n", "").trim().substring(0, 64) || oldName; if (oldName === newName) { return; } @@ -45,9 +46,16 @@ const SharedNodeTitle = ({ className, style, node, disabled }: SharedNodeTitlePr ); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.code === "Enter") { + e.preventDefault(); + e.currentTarget.blur(); + } + }; + return (
-
+
{node.name}
@@ -75,14 +83,27 @@ const isNodeBranchLike = (node: WorkflowNode) => { ); }; -const isNodeReadOnly = (node: WorkflowNode) => { +const isNodeUnduplicatable = (node: WorkflowNode) => { + return ( + node.type === WorkflowNodeType.Start || + node.type === WorkflowNodeType.End || + node.type === WorkflowNodeType.Branch || + node.type === WorkflowNodeType.ExecuteResultBranch || + node.type === WorkflowNodeType.ExecuteSuccess || + node.type === WorkflowNodeType.ExecuteFailure + ); +}; + +const isNodeUnremovable = (node: WorkflowNode) => { return node.type === WorkflowNodeType.Start || node.type === WorkflowNodeType.End; }; const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, afterUpdate, afterDelete }: SharedNodeMenuProps) => { const { t } = useTranslation(); - const { updateNode, removeNode, removeBranch } = useWorkflowStore(useZustandShallowSelector(["updateNode", "removeNode", "removeBranch"])); + const { duplicateNode, updateNode, removeNode, duplicateBranch, removeBranch } = useWorkflowStore( + useZustandShallowSelector(["duplicateNode", "updateNode", "removeNode", "duplicateBranch", "removeBranch"]) + ); const [modalApi, ModelContextHolder] = Modal.useModal(); @@ -91,7 +112,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, const handleRenameConfirm = async () => { const oldName = node.name; - const newName = nameRef.current?.trim()?.substring(0, 64) || oldName; + const newName = nameRef.current?.replaceAll("\r", "")?.replaceAll("\n", "").trim()?.substring(0, 64) || oldName; if (oldName === newName) { return; } @@ -105,11 +126,19 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, afterUpdate?.(); }; - const handleDeleteClick = async () => { + const handleDuplicateClick = async () => { + if (isNodeBranchLike(node)) { + await duplicateBranch(branchId!, branchIndex!); + } else { + await duplicateNode(node); + } + }; + + const handleRemoveClick = async () => { if (isNodeBranchLike(node)) { await removeBranch(branchId!, branchIndex!); } else { - await removeNode(node.id); + await removeNode(node); } afterDelete?.(); @@ -148,16 +177,23 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, setTimeout(() => nameInputRef.current?.focus(), 1); }, }, + { + key: "duplicate", + disabled: disabled || isNodeUnduplicatable(node), + label: isNodeBranchLike(node) ? t("workflow_node.action.duplicate_branch") : t("workflow_node.action.duplicate_node"), + icon: , + onClick: handleDuplicateClick, + }, { type: "divider", }, { key: "remove", - disabled: disabled || isNodeReadOnly(node), + disabled: disabled || isNodeUnremovable(node), label: isNodeBranchLike(node) ? t("workflow_node.action.remove_branch") : t("workflow_node.action.remove_node"), icon: , danger: true, - onClick: handleDeleteClick, + onClick: handleRemoveClick, }, ] satisfies MenuProps["items"]; @@ -195,7 +231,7 @@ const SharedNodeMenu = ({ menus, trigger, node, disabled, branchId, branchIndex, }; // #endregion -// #region Wrapper +// #region Block type SharedNodeBlockProps = SharedNodeProps & { children: React.ReactNode; onClick?: (e: React.MouseEvent) => void; @@ -245,7 +281,7 @@ type SharedNodeEditDrawerProps = SharedNodeProps & { pending?: boolean; onOpenChange?: (open: boolean) => void; onConfirm: () => void | Promise; - getFormValues: () => NonNullable; + getConfigNewValues: () => NonNullable; // 用于获取节点配置的新值,以便在抽屉关闭前进行对比,决定是否提示保存 }; const SharedNodeConfigDrawer = ({ @@ -256,7 +292,7 @@ const SharedNodeConfigDrawer = ({ loading, pending, onConfirm, - getFormValues, + getConfigNewValues, ...props }: SharedNodeEditDrawerProps) => { const { t } = useTranslation(); @@ -284,7 +320,7 @@ const SharedNodeConfigDrawer = ({ if (pending) return; const oldValues = JSON.parse(JSON.stringify(node.config ?? {})); - const newValues = JSON.parse(JSON.stringify(getFormValues())); + const newValues = JSON.parse(JSON.stringify(getConfigNewValues())); const changed = !isEqual(oldValues, {}) && !isEqual(oldValues, newValues); const { promise, resolve, reject } = Promise.withResolvers(); diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 69979aac..fe9f12e3 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -101,6 +101,7 @@ export type AccessConfigForACMEHttpReq = { export type AccessConfigForAliyun = { accessKeyId: string; accessKeySecret: string; + resourceGroupId?: string; }; export type AccessConfigForAWS = { @@ -264,6 +265,7 @@ export type AccessConfigForHetzner = { export type AccessConfigForHuaweiCloud = { accessKeyId: string; secretAccessKey: string; + enterpriseProjectId?: string; }; export type AccessConfigForJDCloud = { 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/version.ts b/ui/src/domain/version.ts index b7741526..9fbf6cfe 100644 --- a/ui/src/domain/version.ts +++ b/ui/src/domain/version.ts @@ -1 +1 @@ -export const version = "v0.3.14"; +export const version = "v0.3.15"; diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 06226425..0a71749b 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -1,5 +1,5 @@ import dayjs from "dayjs"; -import { produce } from "immer"; +import { Immer, produce } from "immer"; import { nanoid } from "nanoid"; import i18n from "@/i18n"; @@ -31,6 +31,7 @@ export enum WorkflowNodeType { End = "end", Apply = "apply", Upload = "upload", + Monitor = "monitor", Deploy = "deploy", Notify = "notify", Branch = "branch", @@ -42,22 +43,24 @@ 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.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")], ]); const workflowNodeTypeDefaultInputs: Map = new Map([ [WorkflowNodeType.Apply, []], + [WorkflowNodeType.Upload, []], + [WorkflowNodeType.Monitor, []], [ WorkflowNodeType.Deploy, [ @@ -65,7 +68,7 @@ const workflowNodeTypeDefaultInputs: Map = n name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -80,7 +83,7 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -91,7 +94,18 @@ const workflowNodeTypeDefaultOutputs: Map = name: "certificate", type: "certificate", required: true, - label: "证书", + label: i18n.t("workflow.variables.type.certificate.label"), + }, + ], + ], + [ + WorkflowNodeType.Monitor, + [ + { + name: "certificate", + type: "certificate", + required: true, + label: i18n.t("workflow.variables.type.certificate.label"), }, ], ], @@ -145,6 +159,13 @@ export type WorkflowNodeConfigForUpload = { privateKey: string; }; +export type WorkflowNodeConfigForMonitor = { + host: string; + port: number; + domain?: string; + requestPath?: string; +}; + export type WorkflowNodeConfigForDeploy = { certificate: string; provider: string; @@ -165,6 +186,10 @@ export type WorkflowNodeConfigForNotify = { providerConfig?: Record; }; +export type WorkflowNodeConfigForCondition = { + expression?: Expr; +}; + export type WorkflowNodeConfigForBranch = never; export type WorkflowNodeConfigForEnd = never; @@ -178,37 +203,189 @@ export type WorkflowNodeIO = { valueSelector?: WorkflowNodeIOValueSelector; }; -export type WorkflowNodeIOValueSelector = { - id: string; - name: string; -}; - +export type WorkflowNodeIOValueSelector = ExprValueSelector; // #endregion -const isBranchLike = (node: WorkflowNode) => { +// #region Expression +export enum ExprType { + Constant = "const", + Variant = "var", + Comparison = "comparison", + Logical = "logical", + Not = "not", +} + +export type ExprValue = string | number | boolean; +export type ExprValueType = "string" | "number" | "boolean"; +export type ExprValueSelector = { + id: string; + name: string; + type: ExprValueType; +}; + +export type ExprComparisonOperator = "gt" | "gte" | "lt" | "lte" | "eq" | "neq"; +export type ExprLogicalOperator = "and" | "or" | "not"; + +export type ConstantExpr = { type: ExprType.Constant; value: string; valueType: ExprValueType }; +export type VariantExpr = { type: ExprType.Variant; selector: ExprValueSelector }; +export type ComparisonExpr = { type: ExprType.Comparison; operator: ExprComparisonOperator; left: Expr; right: Expr }; +export type LogicalExpr = { type: ExprType.Logical; operator: ExprLogicalOperator; left: Expr; right: Expr }; +export type NotExpr = { type: ExprType.Not; expr: Expr }; +export type Expr = ConstantExpr | VariantExpr | ComparisonExpr | LogicalExpr | NotExpr; +// #endregion + +const isBranchNode = (node: WorkflowNode) => { return node.type === WorkflowNodeType.Branch || node.type === WorkflowNodeType.ExecuteResultBranch; }; type InitWorkflowOptions = { - template?: "standard"; + template?: "standard" | "certtest"; }; export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel => { const root = newNode(WorkflowNodeType.Start, {}) as WorkflowNode; root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL }; - if (options.template === "standard") { - let current = root; - current.next = newNode(WorkflowNodeType.Apply, {}); + switch (options.template) { + case "standard": + { + let current = root; - current = current.next; - current.next = newNode(WorkflowNodeType.Deploy, {}); + const applyNode = newNode(WorkflowNodeType.Apply); + current.next = applyNode; - current = current.next; - current.next = newNode(WorkflowNodeType.ExecuteResultBranch, {}); + current = current.next; + current.next = newNode(WorkflowNodeType.ExecuteResultBranch); - current = current.next!.branches![1]; - current.next = newNode(WorkflowNodeType.Notify, {}); + current = current.next!.branches![1]; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run for the certificate application has failed. Please check the details.", + } as WorkflowNodeConfigForNotify, + }); + + current = applyNode.next!.branches![0]; + current.next = newNode(WorkflowNodeType.Deploy, { + nodeConfig: { + certificate: `${applyNode.id}#certificate`, + skipOnLastSucceeded: true, + } as WorkflowNodeConfigForDeploy, + }); + + current = current.next; + current.next = newNode(WorkflowNodeType.ExecuteResultBranch); + + current = current.next!.branches![1]; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run for the certificate deployment has failed. Please check the details.", + } as WorkflowNodeConfigForNotify, + }); + } + break; + + case "certtest": + { + let current = root; + + const monitorNode = newNode(WorkflowNodeType.Monitor); + current.next = monitorNode; + + current = current.next; + current.next = newNode(WorkflowNodeType.ExecuteResultBranch); + + current = current.next!.branches![1]; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Workflow Failure Alert!", + message: "Your workflow run for the certificate monitoring has failed. Please check the details.", + } as WorkflowNodeConfigForNotify, + }); + + current = monitorNode.next!.branches![0]; + const branchNode = newNode(WorkflowNodeType.Branch); + current.next = branchNode; + + current = branchNode.branches![0]; + current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expire_soon"); + current.config = { + expression: { + left: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.validity", + type: "boolean", + }, + type: "var", + }, + operator: "eq", + right: { + type: "const", + value: "true", + valueType: "boolean", + }, + type: "comparison", + }, + operator: "and", + right: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.daysLeft", + type: "number", + }, + type: "var", + }, + operator: "lte", + right: { + type: "const", + value: "30", + valueType: "number", + }, + type: "comparison", + }, + type: "logical", + }, + } as WorkflowNodeConfigForCondition; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Certificate Expiry Alert!", + message: "The certificate will expire soon. Please pay attention to your website.", + } as WorkflowNodeConfigForNotify, + }); + + current = branchNode.branches![1]; + current.name = i18n.t("workflow_node.condition.default_name.template_certtest_on_expired"); + current.config = { + expression: { + left: { + selector: { + id: monitorNode.id, + name: "certificate.validity", + type: "boolean", + }, + type: "var", + }, + operator: "eq", + right: { + type: "const", + value: "false", + valueType: "boolean", + }, + type: "comparison", + }, + } as WorkflowNodeConfigForCondition; + current.next = newNode(WorkflowNodeType.Notify, { + nodeConfig: { + subject: "[Certimate] Certificate Expiry Alert!", + message: "The certificate has already expired. Please pay attention to your website.", + } as WorkflowNodeConfigForNotify, + }); + } + break; } return { @@ -225,6 +402,8 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = }; type NewNodeOptions = { + nodeName?: string; + nodeConfig?: Record; branchIndex?: number; }; @@ -234,13 +413,15 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} const node: WorkflowNode = { id: nanoid(), - name: nodeName, + name: options.nodeName ?? nodeName, type: nodeType, + config: options.nodeConfig, }; switch (nodeType) { case WorkflowNodeType.Apply: case WorkflowNodeType.Upload: + case WorkflowNodeType.Monitor: case WorkflowNodeType.Deploy: { node.inputs = workflowNodeTypeDefaultInputs.get(nodeType); @@ -277,8 +458,75 @@ export const newNode = (nodeType: WorkflowNodeType, options: NewNodeOptions = {} return node; }; -export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { - return produce(node, (draft) => { +export const cloneNode = (sourceNode: WorkflowNode): WorkflowNode => { + const { produce } = new Immer({ autoFreeze: false }); + const deepClone = (node: WorkflowNode): WorkflowNode => { + return produce(node, (draft) => { + draft.id = nanoid(); + + if (draft.next) { + draft.next = cloneNode(draft.next); + } + + if (draft.branches) { + draft.branches = draft.branches.map((branch) => cloneNode(branch)); + } + + return draft; + }); + }; + + const copyNode = produce(sourceNode, (draft) => { + draft.name = `${draft.name}-copy`; + }); + return deepClone(copyNode); +}; + +export const addNode = (root: WorkflowNode, targetNode: WorkflowNode, previousNodeId: string) => { + if (isBranchNode(targetNode)) { + throw new Error("Cannot add a branch node directly. Use `addBranch` instead."); + } + + return produce(root, (draft) => { + let current = draft; + while (current) { + if (current.id === previousNodeId && !isBranchNode(targetNode)) { + targetNode.next = current.next; + current.next = targetNode; + break; + } else if (current.id === previousNodeId && isBranchNode(targetNode)) { + targetNode.branches![0].next = current.next; + current.next = targetNode; + break; + } + + if (isBranchNode(current)) { + current.branches ??= []; + current.branches = current.branches.map((branch) => addNode(branch, targetNode, previousNodeId)); + } + + current = current.next as WorkflowNode; + } + + return draft; + }); +}; + +export const duplicateNode = (root: WorkflowNode, targetNode: WorkflowNode) => { + if (isBranchNode(targetNode)) { + throw new Error("Cannot duplicate a branch node directly. Use `duplicateBranch` instead."); + } + + const copiedNode = cloneNode(targetNode); + return addNode(root, copiedNode, targetNode.id); +}; + +export const updateNode = (root: WorkflowNode, targetNode: WorkflowNode) => { + if (isBranchNode(targetNode)) { + throw new Error("Cannot update a branch node directly. Use `updateBranch` instead."); + } + + return produce(root, (draft) => { let current = draft; while (current) { if (current.id === targetNode.id) { @@ -295,7 +543,7 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; current.branches = current.branches.map((branch) => updateNode(branch, targetNode)); } @@ -307,23 +555,18 @@ export const updateNode = (node: WorkflowNode, targetNode: WorkflowNode) => { }); }; -export const addNode = (node: WorkflowNode, previousNodeId: string, targetNode: WorkflowNode) => { - return produce(node, (draft) => { +export const removeNode = (root: WorkflowNode, targetNodeId: string) => { + return produce(root, (draft) => { let current = draft; while (current) { - if (current.id === previousNodeId && !isBranchLike(targetNode)) { - targetNode.next = current.next; - current.next = targetNode; - break; - } else if (current.id === previousNodeId && isBranchLike(targetNode)) { - targetNode.branches![0].next = current.next; - current.next = targetNode; + if (current.next?.id === targetNodeId) { + current.next = current.next.next; break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; - current.branches = current.branches.map((branch) => addNode(branch, previousNodeId, targetNode)); + current.branches = current.branches.map((branch) => removeNode(branch, targetNodeId)); } current = current.next as WorkflowNode; @@ -333,8 +576,8 @@ export const addNode = (node: WorkflowNode, previousNodeId: string, targetNode: }); }; -export const addBranch = (node: WorkflowNode, branchNodeId: string) => { - return produce(node, (draft) => { +export const addBranch = (root: WorkflowNode, branchNodeId: string) => { + return produce(root, (draft) => { let current = draft; while (current) { if (current.id === branchNodeId) { @@ -351,7 +594,7 @@ export const addBranch = (node: WorkflowNode, branchNodeId: string) => { break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; current.branches = current.branches.map((branch) => addBranch(branch, branchNodeId)); } @@ -363,29 +606,8 @@ export const addBranch = (node: WorkflowNode, branchNodeId: string) => { }); }; -export const removeNode = (node: WorkflowNode, targetNodeId: string) => { - return produce(node, (draft) => { - let current = draft; - while (current) { - if (current.next?.id === targetNodeId) { - current.next = current.next.next; - break; - } - - if (isBranchLike(current)) { - current.branches ??= []; - current.branches = current.branches.map((branch) => removeNode(branch, targetNodeId)); - } - - current = current.next as WorkflowNode; - } - - return draft; - }); -}; - -export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchIndex: number) => { - return produce(node, (draft) => { +export const duplicateBranch = (root: WorkflowNode, branchNodeId: string, branchIndex: number) => { + return produce(root, (draft) => { let current = draft; let last: WorkflowNode | undefined = { id: "", @@ -395,7 +617,41 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }; while (current && last) { if (current.id === branchNodeId) { - if (!isBranchLike(current)) { + if (!isBranchNode(current)) { + return draft; + } + + current.branches ??= []; + current.branches.splice(branchIndex + 1, 0, cloneNode(current.branches[branchIndex])); + + break; + } + + if (isBranchNode(current)) { + current.branches ??= []; + current.branches = current.branches.map((branch) => duplicateBranch(branch, branchNodeId, branchIndex)); + } + + current = current.next as WorkflowNode; + last = last.next; + } + + return draft; + }); +}; + +export const removeBranch = (root: WorkflowNode, branchNodeId: string, branchIndex: number) => { + return produce(root, (draft) => { + let current = draft; + let last: WorkflowNode | undefined = { + id: "", + name: "", + type: WorkflowNodeType.Start, + next: draft, + }; + while (current && last) { + if (current.id === branchNodeId) { + if (!isBranchNode(current)) { return draft; } @@ -420,7 +676,7 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd break; } - if (isBranchLike(current)) { + if (isBranchNode(current)) { current.branches ??= []; current.branches = current.branches.map((branch) => removeBranch(branch, branchNodeId, branchIndex)); } @@ -433,10 +689,24 @@ export const removeBranch = (node: WorkflowNode, branchNodeId: string, branchInd }); }; -export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: string): WorkflowNode[] => { +export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, typeFilter?: string | string[]): WorkflowNode[] => { // 某个分支的节点,不应该能获取到相邻分支上节点的输出 const outputs: WorkflowNode[] = []; + const filter = (io: WorkflowNodeIO) => { + if (typeFilter == null) { + return true; + } + + if (Array.isArray(typeFilter) && typeFilter.includes(io.type)) { + return true; + } else if (io.type === typeFilter) { + return true; + } + + return false; + }; + const traverse = (current: WorkflowNode, output: WorkflowNode[]) => { if (!current) { return false; @@ -445,14 +715,14 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: return true; } - if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => io.type === type)) { + if (current.type !== WorkflowNodeType.Branch && current.outputs && current.outputs.some((io) => filter(io))) { output.push({ ...current, - outputs: current.outputs.filter((io) => io.type === type), + outputs: current.outputs.filter((io) => filter(io)), }); } - if (isBranchLike(current)) { + if (isBranchNode(current)) { let currentLength = output.length; const latestOutput = output.length > 0 ? output[output.length - 1] : null; for (const branch of current.branches!) { @@ -484,7 +754,7 @@ export const getOutputBeforeNodeId = (root: WorkflowNode, nodeId: string, type: export const isAllNodesValidated = (node: WorkflowNode): boolean => { let current = node as typeof node | undefined; while (current) { - if (isBranchLike(current)) { + if (isBranchNode(current)) { for (const branch of current.branches!) { if (!isAllNodesValidated(branch)) { return false; diff --git a/ui/src/i18n/locales/en/index.ts b/ui/src/i18n/locales/en/index.ts index f038efc7..4eaeced5 100644 --- a/ui/src/i18n/locales/en/index.ts +++ b/ui/src/i18n/locales/en/index.ts @@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json"; import nlsWorkflow from "./nls.workflow.json"; import nlsWorkflowNodes from "./nls.workflow.nodes.json"; import nlsWorkflowRuns from "./nls.workflow.runs.json"; +import nlsWorkflowVars from "./nls.workflow.vars.json"; export default Object.freeze({ ...nlsCommon, @@ -16,8 +17,9 @@ export default Object.freeze({ ...nlsSettings, ...nlsProvider, ...nlsAccess, + ...nlsCertificate, ...nlsWorkflow, ...nlsWorkflowNodes, ...nlsWorkflowRuns, - ...nlsCertificate, + ...nlsWorkflowVars, }); diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index bf453f1d..59bee417 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -69,6 +69,9 @@ "access.form.aliyun_access_key_secret.label": "Aliyun AccessKeySecret", "access.form.aliyun_access_key_secret.placeholder": "Please enter Aliyun AccessKeySecret", "access.form.aliyun_access_key_secret.tooltip": "For more information, see https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair", + "access.form.aliyun_resource_group_id.label": "Aliyun resource group ID (Optional)", + "access.form.aliyun_resource_group_id.placeholder": "Please enter Aliyun resource group ID", + "access.form.aliyun_resource_group_id.tooltip": "For more information, see https://www.alibabacloud.com/help/en/resource-management/product-overview", "access.form.aws_access_key_id.label": "AWS AccessKeyId", "access.form.aws_access_key_id.placeholder": "Please enter AWS AccessKeyId", "access.form.aws_access_key_id.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html", @@ -252,6 +255,9 @@ "access.form.huaweicloud_secret_access_key.label": "Huawei Cloud SecretAccessKey", "access.form.huaweicloud_secret_access_key.placeholder": "Please enter Huawei Cloud SecretAccessKey", "access.form.huaweicloud_secret_access_key.tooltip": "For more information, see https://support.huaweicloud.com/intl/en-us/usermanual-ca/ca_01_0003.html", + "access.form.huaweicloud_enterprise_project_id.label": "Huawei Cloud enterprise project ID (Optional)", + "access.form.huaweicloud_enterprise_project_id.placeholder": "Please enter Huawei Cloud enterprise project ID", + "access.form.huaweicloud_enterprise_project_id.tooltip": "For more information, see https://support.huaweicloud.com/intl/en-us/usermanual-em/em_03_0000.html", "access.form.jdcloud_access_key_id.label": "JD Cloud AccessKeyId", "access.form.jdcloud_access_key_id.placeholder": "Please enter JD Cloud AccessKeyId", "access.form.jdcloud_access_key_id.tooltip": "For more information, see https://docs.jdcloud.com/en/account-management/accesskey-management", @@ -459,7 +465,8 @@ "access.form.webhook_preset_data.option.ntfy.label": "ntfy", "access.form.webhook_preset_data.option.pushover.label": "Pushover", "access.form.webhook_preset_data.option.pushplus.label": "PushPlus", - "access.form.webhook_preset_data.option.serverchan.label": "ServerChan", + "access.form.webhook_preset_data.option.serverchan3.label": "ServerChan3", + "access.form.webhook_preset_data.option.serverchanturbo.label": "ServerChanTurbo", "access.form.webhook_preset_data.option.common.label": "General template", "access.form.wecombot_webhook_url.label": "WeCom bot Webhook URL", "access.form.wecombot_webhook_url.placeholder": "Please enter WeCom bot Webhook URL", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index b4f9d7e6..b086e25f 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -30,6 +30,8 @@ "workflow.new.templates.title": "Choose a Workflow Template", "workflow.new.templates.template.standard.title": "Standard template", "workflow.new.templates.template.standard.description": "A standard operating procedure that includes application, deployment, and notification steps.", + "workflow.new.templates.template.certtest.title": "Monitoring template", + "workflow.new.templates.template.certtest.description": "A monitoring operating procedure that includes monitoring, and notification steps.", "workflow.new.templates.template.blank.title": "Blank template", "workflow.new.templates.template.blank.description": "Customize all the contents of the workflow from the beginning.", "workflow.new.modal.title": "Create workflow", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 6de6eda8..0c44f107 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -2,14 +2,17 @@ "workflow_node.action.configure_node": "Configure node", "workflow_node.action.add_node": "Add node", "workflow_node.action.rename_node": "Rename node", + "workflow_node.action.duplicate_node": "Duplicate node", "workflow_node.action.remove_node": "Delete node", "workflow_node.action.add_branch": "Add branch", "workflow_node.action.rename_branch": "Rename branch", + "workflow_node.action.duplicate_branch": "Duplicate branch", "workflow_node.action.remove_branch": "Delete branch", "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 +25,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 +101,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,15 +819,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.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", @@ -852,14 +871,43 @@ "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.label": "Parallel/Conditional branch", + "workflow_node.branch.default_name": "Branch", "workflow_node.condition.label": "Branch", + "workflow_node.condition.default_name": "Branch", + "workflow_node.condition.default_name.template_certtest_on_expire_soon": "If the certificate will expire soon ...", + "workflow_node.condition.default_name.template_certtest_on_expired": "If the certificate has expired ...", + "workflow_node.condition.form.expression.label": "Conditions to enter the branch", + "workflow_node.condition.form.expression.logical_operator.errmsg": "Please select logical operator of conditions", + "workflow_node.condition.form.expression.logical_operator.option.and.label": "Meeting all of the conditions (AND)", + "workflow_node.condition.form.expression.logical_operator.option.or.label": "Meeting any of the conditions (OR)", + "workflow_node.condition.form.expression.variable.placeholder": "Please select", + "workflow_node.condition.form.expression.variable.errmsg": "Please select variable", + "workflow_node.condition.form.expression.operator.placeholder": "Please select", + "workflow_node.condition.form.expression.operator.errmsg": "Please select operator", + "workflow_node.condition.form.expression.operator.option.eq.label": "equal to", + "workflow_node.condition.form.expression.operator.option.eq.alias_is_label": "is", + "workflow_node.condition.form.expression.operator.option.neq.label": "not equal to", + "workflow_node.condition.form.expression.operator.option.neq.alias_not_label": "is not", + "workflow_node.condition.form.expression.operator.option.gt.label": "greater than", + "workflow_node.condition.form.expression.operator.option.gte.label": "greater than or equal to", + "workflow_node.condition.form.expression.operator.option.lt.label": "less than", + "workflow_node.condition.form.expression.operator.option.lte.label": "less than or equal to", + "workflow_node.condition.form.expression.value.placeholder": "Please enter", + "workflow_node.condition.form.expression.value.errmsg": "Please enter value", + "workflow_node.condition.form.expression.value.option.true.label": "True", + "workflow_node.condition.form.expression.value.option.false.label": "False", + "workflow_node.condition.form.expression.add_condition.button": "Add condition", "workflow_node.execute_result_branch.label": "Execution result branch", + "workflow_node.execute_result_branch.default_name": "Branch", "workflow_node.execute_success.label": "If the previous node succeeded ...", + "workflow_node.execute_success.default_name": "On 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": "On Failed" } diff --git a/ui/src/i18n/locales/en/nls.workflow.vars.json b/ui/src/i18n/locales/en/nls.workflow.vars.json new file mode 100644 index 00000000..a96d8ba5 --- /dev/null +++ b/ui/src/i18n/locales/en/nls.workflow.vars.json @@ -0,0 +1,6 @@ +{ + "workflow.variables.type.certificate.label": "Certificate", + + "workflow.variables.selector.validity.label": "Validity", + "workflow.variables.selector.days_left.label": "Days left" +} diff --git a/ui/src/i18n/locales/zh/index.ts b/ui/src/i18n/locales/zh/index.ts index f038efc7..4eaeced5 100644 --- a/ui/src/i18n/locales/zh/index.ts +++ b/ui/src/i18n/locales/zh/index.ts @@ -8,6 +8,7 @@ import nlsSettings from "./nls.settings.json"; import nlsWorkflow from "./nls.workflow.json"; import nlsWorkflowNodes from "./nls.workflow.nodes.json"; import nlsWorkflowRuns from "./nls.workflow.runs.json"; +import nlsWorkflowVars from "./nls.workflow.vars.json"; export default Object.freeze({ ...nlsCommon, @@ -16,8 +17,9 @@ export default Object.freeze({ ...nlsSettings, ...nlsProvider, ...nlsAccess, + ...nlsCertificate, ...nlsWorkflow, ...nlsWorkflowNodes, ...nlsWorkflowRuns, - ...nlsCertificate, + ...nlsWorkflowVars, }); diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index fb51668f..66563f32 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": "请选择证书颁发机构", @@ -69,6 +69,9 @@ "access.form.aliyun_access_key_secret.label": "阿里云 AccessKeySecret", "access.form.aliyun_access_key_secret.placeholder": "请输入阿里云 AccessKeySecret", "access.form.aliyun_access_key_secret.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair", + "access.form.aliyun_resource_group_id.label": "阿里云资源组 ID(可选)", + "access.form.aliyun_resource_group_id.placeholder": "请输入阿里云资源组 ID", + "access.form.aliyun_resource_group_id.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/resource-management/resource-group/product-overview", "access.form.aws_access_key_id.label": "AWS AccessKeyId", "access.form.aws_access_key_id.placeholder": "请输入 AWS AccessKeyId", "access.form.aws_access_key_id.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html", @@ -252,6 +255,9 @@ "access.form.huaweicloud_secret_access_key.label": "华为云 SecretAccessKey", "access.form.huaweicloud_secret_access_key.placeholder": "请输入华为云 SecretAccessKey", "access.form.huaweicloud_secret_access_key.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/usermanual-ca/ca_01_0003.html", + "access.form.huaweicloud_enterprise_project_id.label": "华为云企业项目 ID(可选)", + "access.form.huaweicloud_enterprise_project_id.placeholder": "请输入华为云企业项目 ID", + "access.form.huaweicloud_enterprise_project_id.tooltip": "这是什么?请参阅 https://support.huaweicloud.com/usermanual-em/zh-cn_topic_0126101490.html", "access.form.jdcloud_access_key_id.label": "京东云 AccessKeyId", "access.form.jdcloud_access_key_id.placeholder": "请输入京东云 AccessKeyId", "access.form.jdcloud_access_key_id.tooltip": "这是什么?请参阅 https://docs.jdcloud.com/cn/account-management/accesskey-management", @@ -459,7 +465,8 @@ "access.form.webhook_preset_data.option.ntfy.label": "ntfy", "access.form.webhook_preset_data.option.pushover.label": "Pushover", "access.form.webhook_preset_data.option.pushplus.label": "PushPlus 推送加", - "access.form.webhook_preset_data.option.serverchan.label": "Server 酱", + "access.form.webhook_preset_data.option.serverchan3.label": "Server 酱 3", + "access.form.webhook_preset_data.option.serverchanturbo.label": "Server酱 Turbo", "access.form.webhook_preset_data.option.common.label": "通用模板", "access.form.wecombot_webhook_url.label": "企业微信群机器人 Webhook 地址", "access.form.wecombot_webhook_url.placeholder": "请输入企业微信群机器人 Webhook 地址", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 46cdc228..9ff12aac 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -29,7 +29,9 @@ "workflow.new.subtitle": "使用工作流来申请证书、部署上传和发送通知", "workflow.new.templates.title": "选择工作流模板", "workflow.new.templates.template.standard.title": "标准模板", - "workflow.new.templates.template.standard.description": "一个包含申请 + 部署 + 通知步骤的标准工作流程。", + "workflow.new.templates.template.standard.description": "一个包含证书申请 + 证书部署 + 消息通知步骤的工作流程。", + "workflow.new.templates.template.certtest.title": "监控模板", + "workflow.new.templates.template.certtest.description": "一个包含证书监控 + 消息通知步骤的工作流程。", "workflow.new.templates.template.blank.title": "空白模板", "workflow.new.templates.template.blank.description": "从零开始自定义工作流的任务内容。", "workflow.new.modal.title": "新建工作流", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 6f8f09a6..9f244ef2 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -2,14 +2,17 @@ "workflow_node.action.configure_node": "配置节点", "workflow_node.branch.add_node": "添加节点", "workflow_node.action.rename_node": "重命名", + "workflow_node.action.duplicate_node": "复制节点", "workflow_node.action.remove_node": "删除节点", - "workflow_node.action.add_branch": "添加并行分支", + "workflow_node.action.add_branch": "添加分支", "workflow_node.action.rename_branch": "重命名", + "workflow_node.action.duplicate_branch": "复制分支", "workflow_node.action.remove_branch": "删除分支", "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 +25,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 +100,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": "搜索部署目标……", @@ -695,7 +709,7 @@ "workflow_node.deploy.form.ucloud_us3_domain.label": "优刻得 US3 自定义域名", "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "请输入优刻得 US3 自定义域名", "workflow_node.deploy.form.ucloud_us3_domain.tooltip": "这是什么?请参阅 https://console.ucloud.cn/ufile", - "workflow_node.deploy.form.unicloud_webhost.guide": "小贴士:由于 uniCloud 未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇 uniCloud 接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.unicloud_webhost.guide": "小贴士:由于 uniCloud 未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇 uniCloud 接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.unicloud_webhost_space_provider.label": "uniCloud 服务空间提供商", "workflow_node.deploy.form.unicloud_webhost_space_provider.placeholder": "请选择 uniCloud 服务空间提供商", "workflow_node.deploy.form.unicloud_webhost_space_provider.option.aliyun.label": "阿里云", @@ -705,11 +719,11 @@ "workflow_node.deploy.form.unicloud_webhost_space_id.tooltip": "这是什么?请参阅 https://doc.dcloud.net.cn/uniCloud/concepts/space.html", "workflow_node.deploy.form.unicloud_webhost_domain.label": "uniCloud 前端网页托管网站域名", "workflow_node.deploy.form.unicloud_webhost_domain.placeholder": "请输入 uniCloud 前端网页托管网站域名", - "workflow_node.deploy.form.upyun_cdn.guide": "小贴士:由于又拍云未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.upyun_cdn.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.upyun_cdn_domain.label": "又拍云 CDN 加速域名", "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/cdn/", - "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未提供相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", + "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名", "workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/", @@ -804,15 +818,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.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": "通知内容", @@ -851,14 +870,43 @@ "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.label": "并行/条件分支", + "workflow_node.branch.default_name": "分支", "workflow_node.condition.label": "分支", + "workflow_node.condition.default_name": "分支", + "workflow_node.condition.default_name.template_certtest_on_expire_soon": "若网站证书即将到期 ...", + "workflow_node.condition.default_name.template_certtest_on_expired": "若网站证书已到期 ...", + "workflow_node.condition.form.expression.label": "分支进入条件", + "workflow_node.condition.form.expression.logical_operator.errmsg": "请选择条件组合方式", + "workflow_node.condition.form.expression.logical_operator.option.and.label": "满足以下所有条件 (AND)", + "workflow_node.condition.form.expression.logical_operator.option.or.label": "满足以下任一条件 (OR)", + "workflow_node.condition.form.expression.variable.placeholder": "请选择", + "workflow_node.condition.form.expression.variable.errmsg": "请选择变量", + "workflow_node.condition.form.expression.operator.placeholder": "请选择", + "workflow_node.condition.form.expression.operator.errmsg": "请选择运算符", + "workflow_node.condition.form.expression.operator.option.eq.label": "等于", + "workflow_node.condition.form.expression.operator.option.eq.alias_is_label": "为", + "workflow_node.condition.form.expression.operator.option.neq.label": "不等于", + "workflow_node.condition.form.expression.operator.option.neq.alias_not_label": "不为", + "workflow_node.condition.form.expression.operator.option.gt.label": "大于", + "workflow_node.condition.form.expression.operator.option.gte.label": "大于等于", + "workflow_node.condition.form.expression.operator.option.lt.label": "小于", + "workflow_node.condition.form.expression.operator.option.lte.label": "小于等于", + "workflow_node.condition.form.expression.value.placeholder": "请输入", + "workflow_node.condition.form.expression.value.errmsg": "请输入值", + "workflow_node.condition.form.expression.value.option.true.label": "真", + "workflow_node.condition.form.expression.value.option.false.label": "假", + "workflow_node.condition.form.expression.add_condition.button": "添加条件", "workflow_node.execute_result_branch.label": "执行结果分支", + "workflow_node.execute_result_branch.default_name": "分支", - "workflow_node.execute_success.label": "若前序节点执行成功…", + "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/i18n/locales/zh/nls.workflow.vars.json b/ui/src/i18n/locales/zh/nls.workflow.vars.json new file mode 100644 index 00000000..eddfc585 --- /dev/null +++ b/ui/src/i18n/locales/zh/nls.workflow.vars.json @@ -0,0 +1,6 @@ +{ + "workflow.variables.type.certificate.label": "证书", + + "workflow.variables.selector.validity.label": "有效性", + "workflow.variables.selector.days_left.label": "剩余天数" +} 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 ?? "")} ); diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 832269d0..91e8d746 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -265,7 +265,7 @@ const WorkflowDetail = () => { body: { position: "relative", height: "100%", - padding: 0, + padding: initialized ? 0 : undefined, }, }} loading={!initialized} diff --git a/ui/src/pages/workflows/WorkflowNew.tsx b/ui/src/pages/workflows/WorkflowNew.tsx index 5f6af27b..9877dcc2 100644 --- a/ui/src/pages/workflows/WorkflowNew.tsx +++ b/ui/src/pages/workflows/WorkflowNew.tsx @@ -12,9 +12,10 @@ import { useAntdForm } from "@/hooks"; import { save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; -const TEMPLATE_KEY_BLANK = "blank" as const; const TEMPLATE_KEY_STANDARD = "standard" as const; -type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_STANDARD; +const TEMPLATE_KEY_CERTTEST = "monitor" as const; +const TEMPLATE_KEY_BLANK = "blank" as const; +type TemplateKeys = typeof TEMPLATE_KEY_BLANK | typeof TEMPLATE_KEY_CERTTEST | typeof TEMPLATE_KEY_STANDARD; const WorkflowNew = () => { const navigate = useNavigate(); @@ -27,8 +28,8 @@ const WorkflowNew = () => { xs: { flex: "100%" }, md: { flex: "100%" }, lg: { flex: "50%" }, - xl: { flex: "50%" }, - xxl: { flex: "50%" }, + xl: { flex: "33.3333%" }, + xxl: { flex: "33.3333%" }, }; const [templateSelectKey, setTemplateSelectKey] = useState(); @@ -64,6 +65,10 @@ const WorkflowNew = () => { workflow = initWorkflow({ template: "standard" }); break; + case TEMPLATE_KEY_CERTTEST: + workflow = initWorkflow({ template: "certtest" }); + break; + default: throw "Invalid state: `templateSelectKey`"; } @@ -116,7 +121,7 @@ const WorkflowNew = () => {
-
+
{t("workflow.new.templates.title")}
@@ -139,6 +144,25 @@ const WorkflowNew = () => {
+ + + } + hoverable + onClick={() => handleTemplateClick(TEMPLATE_KEY_CERTTEST)} + > +
+ + +
+
+ + void; + duplicateNode: (node: WorkflowNode) => void; updateNode: (node: WorkflowNode) => void; - removeNode: (nodeId: string) => void; + removeNode: (node: WorkflowNode) => void; addBranch: (branchId: string) => void; + duplicateBranch: (branchId: string, index: number) => void; removeBranch: (branchId: string, index: number) => void; - getWorkflowOuptutBeforeId: (nodeId: string, type: string) => WorkflowNode[]; + getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => WorkflowNode[]; }; export const useWorkflowStore = create((set, get) => ({ @@ -146,7 +150,27 @@ export const useWorkflowStore = create((set, get) => ({ addNode: async (node: WorkflowNode, previousNodeId: string) => { if (!get().initialized) throw "Workflow not initialized yet"; - const root = addNode(get().workflow.draft!, previousNodeId, node); + const root = addNode(get().workflow.draft!, node, previousNodeId); + const resp = await saveWorkflow({ + id: get().workflow.id!, + draft: root, + hasDraft: true, + }); + + set((state: WorkflowState) => { + return { + workflow: produce(state.workflow, (draft) => { + draft.draft = resp.draft; + draft.hasDraft = resp.hasDraft; + }), + }; + }); + }, + + duplicateNode: async (node: WorkflowNode) => { + if (!get().initialized) throw "Workflow not initialized yet"; + + const root = duplicateNode(get().workflow.draft!, node); const resp = await saveWorkflow({ id: get().workflow.id!, draft: root, @@ -183,10 +207,10 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - removeNode: async (nodeId: string) => { + removeNode: async (node: WorkflowNode) => { if (!get().initialized) throw "Workflow not initialized yet"; - const root = removeNode(get().workflow.draft!, nodeId); + const root = removeNode(get().workflow.draft!, node.id); const resp = await saveWorkflow({ id: get().workflow.id!, draft: root, @@ -223,6 +247,26 @@ export const useWorkflowStore = create((set, get) => ({ }); }, + duplicateBranch: async (branchId: string, index: number) => { + if (!get().initialized) throw "Workflow not initialized yet"; + + const root = duplicateBranch(get().workflow.draft!, branchId, index); + const resp = await saveWorkflow({ + id: get().workflow.id!, + draft: root, + hasDraft: true, + }); + + set((state: WorkflowState) => { + return { + workflow: produce(state.workflow, (draft) => { + draft.draft = resp.draft; + draft.hasDraft = resp.hasDraft; + }), + }; + }); + }, + removeBranch: async (branchId: string, index: number) => { if (!get().initialized) throw "Workflow not initialized yet"; @@ -243,7 +287,7 @@ export const useWorkflowStore = create((set, get) => ({ }); }, - getWorkflowOuptutBeforeId: (nodeId: string, type: string) => { - return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, type); + getWorkflowOuptutBeforeId: (nodeId: string, typeFilter?: string | string[]) => { + return getOutputBeforeNodeId(get().workflow.draft as WorkflowNode, nodeId, typeFilter); }, }));