Merge pull request #643 from fudiwei/main

Support configuring independent notification channel for each workflow
This commit is contained in:
Yoan.liu 2025-04-30 21:37:03 +08:00 committed by GitHub
commit fc064aa9f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
125 changed files with 3977 additions and 1363 deletions

2
go.mod
View File

@ -187,7 +187,7 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pkg/errors v0.9.1
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cobra v1.9.1 // indirect

View File

@ -3,12 +3,12 @@ package applicant
import "github.com/usual2970/certimate/internal/domain"
const (
sslProviderLetsEncrypt = string(domain.ApplyCAProviderTypeLetsEncrypt)
sslProviderLetsEncryptStaging = string(domain.ApplyCAProviderTypeLetsEncryptStaging)
sslProviderBuypass = string(domain.ApplyCAProviderTypeBuypass)
sslProviderGoogleTrustServices = string(domain.ApplyCAProviderTypeGoogleTrustServices)
sslProviderSSLCom = string(domain.ApplyCAProviderTypeSSLCom)
sslProviderZeroSSL = string(domain.ApplyCAProviderTypeZeroSSL)
sslProviderLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
sslProviderLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
sslProviderBuypass = string(domain.CAProviderTypeBuypass)
sslProviderGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
sslProviderSSLCom = string(domain.CAProviderTypeSSLCom)
sslProviderZeroSSL = string(domain.CAProviderTypeZeroSSL)
sslProviderDefault = sslProviderLetsEncrypt
)
@ -25,6 +25,6 @@ var sslProviderUrls = map[string]string{
}
type acmeSSLProviderConfig struct {
Config map[domain.ApplyCAProviderType]map[string]any `json:"config"`
Provider string `json:"provider"`
Config map[domain.CAProviderType]map[string]any `json:"config"`
Provider string `json:"provider"`
}

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"strconv"
"strings"
@ -22,7 +23,7 @@ import (
"github.com/usual2970/certimate/internal/repository"
)
type ApplyCertResult struct {
type ApplyResult struct {
CertificateFullChain string
IssuerCertificate string
PrivateKey string
@ -33,40 +34,30 @@ type ApplyCertResult struct {
}
type Applicant interface {
Apply() (*ApplyCertResult, error)
Apply(ctx context.Context) (*ApplyResult, error)
}
type applicantOptions struct {
Domains []string
ContactEmail string
Provider domain.ApplyDNSProviderType
ProviderAccessConfig map[string]any
ProviderExtendedConfig map[string]any
CAProvider domain.ApplyCAProviderType
CAProviderAccessConfig map[string]any
CAProviderExtendedConfig map[string]any
KeyAlgorithm string
Nameservers []string
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
ReplacedARIAcct string
ReplacedARICert string
type ApplicantWithWorkflowNodeConfig struct {
Node *domain.WorkflowNode
Logger *slog.Logger
}
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
if node.Type != domain.WorkflowNodeTypeApply {
func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, error) {
if config.Node == nil {
return nil, fmt.Errorf("node is nil")
}
if config.Node.Type != domain.WorkflowNodeTypeApply {
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeApply))
}
nodeConfig := node.GetConfigForApply()
options := &applicantOptions{
nodeConfig := config.Node.GetConfigForApply()
options := &applicantProviderOptions{
Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeConfig.ContactEmail,
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
Provider: domain.ACMEDns01ProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderExtendedConfig: nodeConfig.ProviderConfig,
CAProvider: domain.ApplyCAProviderType(nodeConfig.CAProvider),
CAProvider: domain.CAProviderType(nodeConfig.CAProvider),
CAProviderAccessConfig: make(map[string]any),
CAProviderExtendedConfig: nodeConfig.CAProviderConfig,
KeyAlgorithm: nodeConfig.KeyAlgorithm,
@ -97,7 +88,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
sslProviderConfig := &acmeSSLProviderConfig{
Config: make(map[domain.ApplyCAProviderType]map[string]any),
Config: make(map[domain.CAProviderType]map[string]any),
Provider: sslProviderDefault,
}
if settings != nil {
@ -108,12 +99,12 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
}
}
options.CAProvider = domain.ApplyCAProviderType(sslProviderConfig.Provider)
options.CAProvider = domain.CAProviderType(sslProviderConfig.Provider)
options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider]
}
certRepo := repository.NewCertificateRepository()
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), node.Id)
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), config.Node.Id)
if lastCertificate != nil {
newCertSan := slices.Clone(options.Domains)
oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";")
@ -130,18 +121,46 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
}
}
applicant, err := createApplicant(options)
applicant, err := createApplicantProvider(options)
if err != nil {
return nil, err
}
return &proxyApplicant{
return &applicantImpl{
applicant: applicant,
options: options,
}, nil
}
func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) {
type applicantImpl struct {
applicant challenge.Provider
options *applicantProviderOptions
}
var _ Applicant = (*applicantImpl)(nil)
func (d *applicantImpl) Apply(ctx context.Context) (*ApplyResult, error) {
limiter := getLimiter(fmt.Sprintf("apply_%s", d.options.ContactEmail))
if err := limiter.Wait(ctx); err != nil {
return nil, err
}
return applyUseLego(d.applicant, d.options)
}
const (
limitBurst = 300
limitRate float64 = float64(1) / float64(36)
)
var limiters sync.Map
func getLimiter(key string) *rate.Limiter {
limiter, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(limitRate), 300))
return limiter.(*rate.Limiter)
}
func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOptions) (*ApplyResult, error) {
user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail)
if err != nil {
return nil, err
@ -153,7 +172,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
// Create an ACME client config
config := lego.NewConfig(user)
config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
config.Certificate.KeyType = parseLegoKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
config.CADirURL = sslProviderUrls[user.CA]
if user.CA == sslProviderSSLCom {
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
@ -175,7 +194,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers)))
challengeOptions = append(challengeOptions, dns01.DisableAuthoritativeNssPropagationRequirement())
}
client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...)
client.Challenge.SetDNS01Provider(legoProvider, challengeOptions...)
// New users need to register first
if !user.hasRegistration() {
@ -199,7 +218,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
return nil, err
}
return &ApplyCertResult{
return &ApplyResult{
CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
@ -210,47 +229,20 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
}, nil
}
func parseKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType {
switch algo {
case domain.CertificateKeyAlgorithmTypeRSA2048:
return certcrypto.RSA2048
case domain.CertificateKeyAlgorithmTypeRSA3072:
return certcrypto.RSA3072
case domain.CertificateKeyAlgorithmTypeRSA4096:
return certcrypto.RSA4096
case domain.CertificateKeyAlgorithmTypeRSA8192:
return certcrypto.RSA8192
case domain.CertificateKeyAlgorithmTypeEC256:
return certcrypto.EC256
case domain.CertificateKeyAlgorithmTypeEC384:
return certcrypto.EC384
case domain.CertificateKeyAlgorithmTypeEC512:
return certcrypto.KeyType("P512")
func parseLegoKeyAlgorithm(algo domain.CertificateKeyAlgorithmType) certcrypto.KeyType {
alogMap := map[domain.CertificateKeyAlgorithmType]certcrypto.KeyType{
domain.CertificateKeyAlgorithmTypeRSA2048: certcrypto.RSA2048,
domain.CertificateKeyAlgorithmTypeRSA3072: certcrypto.RSA3072,
domain.CertificateKeyAlgorithmTypeRSA4096: certcrypto.RSA4096,
domain.CertificateKeyAlgorithmTypeRSA8192: certcrypto.RSA8192,
domain.CertificateKeyAlgorithmTypeEC256: certcrypto.EC256,
domain.CertificateKeyAlgorithmTypeEC384: certcrypto.EC384,
domain.CertificateKeyAlgorithmTypeEC512: certcrypto.KeyType("P512"),
}
if keyType, ok := alogMap[algo]; ok {
return keyType
}
return certcrypto.RSA2048
}
// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑
type proxyApplicant struct {
applicant challenge.Provider
options *applicantOptions
}
var limiters sync.Map
const (
limitBurst = 300
limitRate float64 = float64(1) / float64(36)
)
func getLimiter(key string) *rate.Limiter {
limiter, _ := limiters.LoadOrStore(key, rate.NewLimiter(rate.Limit(limitRate), 300))
return limiter.(*rate.Limiter)
}
func (d *proxyApplicant) Apply() (*ApplyCertResult, error) {
limiter := getLimiter(fmt.Sprintf("apply_%s", d.options.ContactEmail))
limiter.Wait(context.Background())
return apply(d.applicant, d.options)
}

View File

@ -38,13 +38,31 @@ import (
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
func createApplicant(options *applicantOptions) (challenge.Provider, error) {
type applicantProviderOptions struct {
Domains []string
ContactEmail string
Provider domain.ACMEDns01ProviderType
ProviderAccessConfig map[string]any
ProviderExtendedConfig map[string]any
CAProvider domain.CAProviderType
CAProviderAccessConfig map[string]any
CAProviderExtendedConfig map[string]any
KeyAlgorithm string
Nameservers []string
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
ReplacedARIAcct string
ReplacedARICert string
}
func createApplicantProvider(options *applicantProviderOptions) (challenge.Provider, error) {
/*
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
switch options.Provider {
case domain.ApplyDNSProviderTypeACMEHttpReq:
case domain.ACMEDns01ProviderTypeACMEHttpReq:
{
access := domain.AccessConfigForACMEHttpReq{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -61,7 +79,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeAliyun, domain.ApplyDNSProviderTypeAliyunDNS:
case domain.ACMEDns01ProviderTypeAliyun, domain.ACMEDns01ProviderTypeAliyunDNS:
{
access := domain.AccessConfigForAliyun{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -77,7 +95,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeAWS, domain.ApplyDNSProviderTypeAWSRoute53:
case domain.ACMEDns01ProviderTypeAWS, domain.ACMEDns01ProviderTypeAWSRoute53:
{
access := domain.AccessConfigForAWS{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -95,7 +113,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeAzure, domain.ApplyDNSProviderTypeAzureDNS:
case domain.ACMEDns01ProviderTypeAzure, domain.ACMEDns01ProviderTypeAzureDNS:
{
access := domain.AccessConfigForAzure{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -113,7 +131,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeBaiduCloud, domain.ApplyDNSProviderTypeBaiduCloudDNS:
case domain.ACMEDns01ProviderTypeBaiduCloud, domain.ACMEDns01ProviderTypeBaiduCloudDNS:
{
access := domain.AccessConfigForBaiduCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -129,7 +147,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeBunny:
case domain.ACMEDns01ProviderTypeBunny:
{
access := domain.AccessConfigForBunny{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -144,7 +162,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeCloudflare:
case domain.ACMEDns01ProviderTypeCloudflare:
{
access := domain.AccessConfigForCloudflare{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -160,7 +178,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeClouDNS:
case domain.ACMEDns01ProviderTypeClouDNS:
{
access := domain.AccessConfigForClouDNS{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -176,7 +194,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeCMCCCloud:
case domain.ACMEDns01ProviderTypeCMCCCloud:
{
access := domain.AccessConfigForCMCCCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -192,7 +210,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeDeSEC:
case domain.ACMEDns01ProviderTypeDeSEC:
{
access := domain.AccessConfigForDeSEC{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -207,7 +225,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeDNSLA:
case domain.ACMEDns01ProviderTypeDNSLA:
{
access := domain.AccessConfigForDNSLA{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -223,7 +241,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeDynv6:
case domain.ACMEDns01ProviderTypeDynv6:
{
access := domain.AccessConfigForDynv6{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -238,7 +256,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeGcore:
case domain.ACMEDns01ProviderTypeGcore:
{
access := domain.AccessConfigForGcore{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -253,7 +271,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeGname:
case domain.ACMEDns01ProviderTypeGname:
{
access := domain.AccessConfigForGname{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -269,7 +287,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeGoDaddy:
case domain.ACMEDns01ProviderTypeGoDaddy:
{
access := domain.AccessConfigForGoDaddy{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -285,7 +303,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeHuaweiCloud, domain.ApplyDNSProviderTypeHuaweiCloudDNS:
case domain.ACMEDns01ProviderTypeHuaweiCloud, domain.ACMEDns01ProviderTypeHuaweiCloudDNS:
{
access := domain.AccessConfigForHuaweiCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -302,7 +320,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeJDCloud, domain.ApplyDNSProviderTypeJDCloudDNS:
case domain.ACMEDns01ProviderTypeJDCloud, domain.ACMEDns01ProviderTypeJDCloudDNS:
{
access := domain.AccessConfigForJDCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -319,7 +337,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeNamecheap:
case domain.ACMEDns01ProviderTypeNamecheap:
{
access := domain.AccessConfigForNamecheap{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -335,7 +353,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeNameDotCom:
case domain.ACMEDns01ProviderTypeNameDotCom:
{
access := domain.AccessConfigForNameDotCom{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -351,7 +369,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeNameSilo:
case domain.ACMEDns01ProviderTypeNameSilo:
{
access := domain.AccessConfigForNameSilo{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -366,7 +384,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeNS1:
case domain.ACMEDns01ProviderTypeNS1:
{
access := domain.AccessConfigForNS1{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -381,7 +399,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypePorkbun:
case domain.ACMEDns01ProviderTypePorkbun:
{
access := domain.AccessConfigForPorkbun{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -397,7 +415,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypePowerDNS:
case domain.ACMEDns01ProviderTypePowerDNS:
{
access := domain.AccessConfigForPowerDNS{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -413,7 +431,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeRainYun:
case domain.ACMEDns01ProviderTypeRainYun:
{
access := domain.AccessConfigForRainYun{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -428,7 +446,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS, domain.ApplyDNSProviderTypeTencentCloudEO:
case domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS, domain.ACMEDns01ProviderTypeTencentCloudEO:
{
access := domain.AccessConfigForTencentCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -436,7 +454,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
}
switch options.Provider {
case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS:
case domain.ACMEDns01ProviderTypeTencentCloud, domain.ACMEDns01ProviderTypeTencentCloudDNS:
applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
@ -445,7 +463,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
})
return applicant, err
case domain.ApplyDNSProviderTypeTencentCloudEO:
case domain.ACMEDns01ProviderTypeTencentCloudEO:
applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
@ -460,7 +478,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
}
}
case domain.ApplyDNSProviderTypeVercel:
case domain.ACMEDns01ProviderTypeVercel:
{
access := domain.AccessConfigForVercel{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -476,7 +494,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeVolcEngine, domain.ApplyDNSProviderTypeVolcEngineDNS:
case domain.ACMEDns01ProviderTypeVolcEngine, domain.ACMEDns01ProviderTypeVolcEngineDNS:
{
access := domain.AccessConfigForVolcEngine{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@ -492,7 +510,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) {
return applicant, err
}
case domain.ApplyDNSProviderTypeWestcn:
case domain.ACMEDns01ProviderTypeWestcn:
{
access := domain.AccessConfigForWestcn{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {

View File

@ -11,31 +11,29 @@ import (
)
type Deployer interface {
SetLogger(*slog.Logger)
Deploy(ctx context.Context) error
}
type deployerOptions struct {
Provider domain.DeployProviderType
ProviderAccessConfig map[string]any
ProviderDeployConfig map[string]any
type DeployerWithWorkflowNodeConfig struct {
Node *domain.WorkflowNode
Logger *slog.Logger
CertificatePEM string
PrivateKeyPEM string
}
func NewWithDeployNode(node *domain.WorkflowNode, certdata struct {
Certificate string
PrivateKey string
},
) (Deployer, error) {
if node.Type != domain.WorkflowNodeTypeDeploy {
func NewWithWorkflowNode(config DeployerWithWorkflowNodeConfig) (Deployer, error) {
if config.Node == nil {
return nil, fmt.Errorf("node is nil")
}
if config.Node.Type != domain.WorkflowNodeTypeDeploy {
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeDeploy))
}
nodeConfig := node.GetConfigForDeploy()
options := &deployerOptions{
Provider: domain.DeployProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderDeployConfig: nodeConfig.ProviderConfig,
nodeConfig := config.Node.GetConfigForDeploy()
options := &deployerProviderOptions{
Provider: domain.DeploymentProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderExtendedConfig: nodeConfig.ProviderConfig,
}
accessRepo := repository.NewAccessRepository()
@ -48,34 +46,27 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct {
}
}
deployer, err := createDeployer(options)
deployerProvider, err := createDeployerProvider(options)
if err != nil {
return nil, err
}
return &proxyDeployer{
deployer: deployer,
deployCertificate: certdata.Certificate,
deployPrivateKey: certdata.PrivateKey,
return &deployerImpl{
provider: deployerProvider.WithLogger(config.Logger),
certPEM: config.CertificatePEM,
privkeyPEM: config.PrivateKeyPEM,
}, nil
}
// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑
type proxyDeployer struct {
deployer deployer.Deployer
deployCertificate string
deployPrivateKey string
type deployerImpl struct {
provider deployer.Deployer
certPEM string
privkeyPEM string
}
func (d *proxyDeployer) SetLogger(logger *slog.Logger) {
if logger == nil {
panic("logger is nil")
}
var _ Deployer = (*deployerImpl)(nil)
d.deployer.WithLogger(logger)
}
func (d *proxyDeployer) Deploy(ctx context.Context) error {
_, err := d.deployer.Deploy(ctx, d.deployCertificate, d.deployPrivateKey)
func (d *deployerImpl) Deploy(ctx context.Context) error {
_, err := d.provider.Deploy(ctx, d.certPEM, d.privkeyPEM)
return err
}

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ type Access struct {
Name string `json:"name" db:"name"`
Provider string `json:"provider" db:"provider"`
Config map[string]any `json:"config" db:"config"`
Reserve string `json:"reserve,omitempty" db:"reserve"`
DeletedAt *time.Time `json:"deleted" db:"deleted"`
}
@ -97,6 +98,11 @@ type AccessConfigForDeSEC struct {
Token string `json:"token"`
}
type AccessConfigForDingTalkBot struct {
WebhookUrl string `json:"webhookUrl"`
Secret string `json:"secret"`
}
type AccessConfigForDNSLA struct {
ApiId string `json:"apiId"`
ApiSecret string `json:"apiSecret"`
@ -116,6 +122,16 @@ type AccessConfigForEdgio struct {
ClientSecret string `json:"clientSecret"`
}
type AccessConfigForEmail struct {
SmtpHost string `json:"smtpHost"`
SmtpPort int32 `json:"smtpPort"`
SmtpTls bool `json:"smtpTls"`
Username string `json:"username"`
Password string `json:"password"`
DefaultSenderAddress string `json:"defaultSenderAddress,omitempty"`
DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"`
}
type AccessConfigForGcore struct {
ApiToken string `json:"apiToken"`
}
@ -149,6 +165,17 @@ type AccessConfigForKubernetes struct {
KubeConfig string `json:"kubeConfig,omitempty"`
}
type AccessConfigForLarkBot struct {
WebhookUrl string `json:"webhookUrl"`
}
type AccessConfigForMattermost struct {
ServerUrl string `json:"serverUrl"`
Username string `json:"username"`
Password string `json:"password"`
DefaultChannelId string `json:"defaultChannelId,omitempty"`
}
type AccessConfigForNamecheap struct {
Username string `json:"username"`
ApiKey string `json:"apiKey"`
@ -206,6 +233,11 @@ type AccessConfigForSSLCom struct {
EabHmacKey string `json:"eabHmacKey"`
}
type AccessConfigForTelegram struct {
BotToken string `json:"botToken"`
DefaultChatId int64 `json:"defaultChatId,omitempty"`
}
type AccessConfigForTencentCloud struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
@ -239,8 +271,16 @@ type AccessConfigForWangsu struct {
}
type AccessConfigForWebhook struct {
Url string `json:"url"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
Url string `json:"url"`
Method string `json:"method,omitempty"`
HeadersString string `json:"headers,omitempty"`
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
DefaultDataForDeployment string `json:"defaultDataForDeployment,omitempty"`
DefaultDataForNotification string `json:"defaultDataForNotification,omitempty"`
}
type AccessConfigForWeComBot struct {
WebhookUrl string `json:"webhookUrl"`
}
type AccessConfigForWestcn struct {

View File

@ -8,6 +8,7 @@ type NotifyChannelType string
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
// Deprecated: v0.4.x 将废弃
const (
NotifyChannelTypeBark = NotifyChannelType("bark")
NotifyChannelTypeDingTalk = NotifyChannelType("dingtalk")

View File

@ -19,6 +19,7 @@ const (
AccessProviderTypeBaishan = AccessProviderType("baishan")
AccessProviderTypeBaotaPanel = AccessProviderType("baotapanel")
AccessProviderTypeBytePlus = AccessProviderType("byteplus")
AccessProviderTypeBunny = AccessProviderType("bunny")
AccessProviderTypeBuypass = AccessProviderType("buypass")
AccessProviderTypeCacheFly = AccessProviderType("cachefly")
AccessProviderTypeCdnfly = AccessProviderType("cdnfly")
@ -28,10 +29,12 @@ const (
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 联通云(预留)
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 天翼云(预留)
AccessProviderTypeDeSEC = AccessProviderType("desec")
AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot")
AccessProviderTypeDNSLA = AccessProviderType("dnsla")
AccessProviderTypeDogeCloud = AccessProviderType("dogecloud")
AccessProviderTypeDynv6 = AccessProviderType("dynv6")
AccessProviderTypeEdgio = AccessProviderType("edgio")
AccessProviderTypeEmail = AccessProviderType("email")
AccessProviderTypeFastly = AccessProviderType("fastly") // Fastly预留
AccessProviderTypeGname = AccessProviderType("gname")
AccessProviderTypeGcore = AccessProviderType("gcore")
@ -41,9 +44,11 @@ const (
AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud")
AccessProviderTypeJDCloud = AccessProviderType("jdcloud")
AccessProviderTypeKubernetes = AccessProviderType("k8s")
AccessProviderTypeLarkBot = AccessProviderType("larkbot")
AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt")
AccessProviderTypeLetsEncryptStaging = AccessProviderType("letsencryptstaging")
AccessProviderTypeLocal = AccessProviderType("local")
AccessProviderTypeMattermost = AccessProviderType("mattermost")
AccessProviderTypeNamecheap = AccessProviderType("namecheap")
AccessProviderTypeNameDotCom = AccessProviderType("namedotcom")
AccessProviderTypeNameSilo = AccessProviderType("namesilo")
@ -56,6 +61,7 @@ const (
AccessProviderTypeSafeLine = AccessProviderType("safeline")
AccessProviderTypeSSH = AccessProviderType("ssh")
AccessProviderTypeSSLCOM = AccessProviderType("sslcom")
AccessProviderTypeTelegram = AccessProviderType("telegram")
AccessProviderTypeTencentCloud = AccessProviderType("tencentcloud")
AccessProviderTypeUCloud = AccessProviderType("ucloud")
AccessProviderTypeUpyun = AccessProviderType("upyun")
@ -63,78 +69,79 @@ const (
AccessProviderTypeVolcEngine = AccessProviderType("volcengine")
AccessProviderTypeWangsu = AccessProviderType("wangsu")
AccessProviderTypeWebhook = AccessProviderType("webhook")
AccessProviderTypeWeComBot = AccessProviderType("wecombot")
AccessProviderTypeWestcn = AccessProviderType("westcn")
AccessProviderTypeZeroSSL = AccessProviderType("zerossl")
)
type ApplyCAProviderType string
type CAProviderType string
/*
申请证书 CA 提供商常量值
始终等于授权提供商类型
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
ApplyCAProviderTypeBuypass = ApplyCAProviderType(string(AccessProviderTypeBuypass))
ApplyCAProviderTypeGoogleTrustServices = ApplyCAProviderType(string(AccessProviderTypeGoogleTrustServices))
ApplyCAProviderTypeLetsEncrypt = ApplyCAProviderType(string(AccessProviderTypeLetsEncrypt))
ApplyCAProviderTypeLetsEncryptStaging = ApplyCAProviderType(string(AccessProviderTypeLetsEncryptStaging))
ApplyCAProviderTypeSSLCom = ApplyCAProviderType(string(AccessProviderTypeSSLCOM))
ApplyCAProviderTypeZeroSSL = ApplyCAProviderType(string(AccessProviderTypeZeroSSL))
)
type ApplyDNSProviderType string
/*
申请证书 DNS 提供商常量值
证书颁发机构提供商常量值
短横线前的部分始终等于授权提供商类型
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
ApplyDNSProviderTypeACMEHttpReq = ApplyDNSProviderType("acmehttpreq")
ApplyDNSProviderTypeAliyun = ApplyDNSProviderType("aliyun") // 兼容旧值,等同于 [ApplyDNSProviderTypeAliyunDNS]
ApplyDNSProviderTypeAliyunDNS = ApplyDNSProviderType("aliyun-dns")
ApplyDNSProviderTypeAWS = ApplyDNSProviderType("aws") // 兼容旧值,等同于 [ApplyDNSProviderTypeAWSRoute53]
ApplyDNSProviderTypeAWSRoute53 = ApplyDNSProviderType("aws-route53")
ApplyDNSProviderTypeAzure = ApplyDNSProviderType("azure") // 兼容旧值,等同于 [ApplyDNSProviderTypeAzure]
ApplyDNSProviderTypeAzureDNS = ApplyDNSProviderType("azure-dns")
ApplyDNSProviderTypeBaiduCloud = ApplyDNSProviderType("baiducloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeBaiduCloudDNS]
ApplyDNSProviderTypeBaiduCloudDNS = ApplyDNSProviderType("baiducloud-dns")
ApplyDNSProviderTypeBunny = ApplyDNSProviderType("bunny")
ApplyDNSProviderTypeCloudflare = ApplyDNSProviderType("cloudflare")
ApplyDNSProviderTypeClouDNS = ApplyDNSProviderType("cloudns")
ApplyDNSProviderTypeCMCCCloud = ApplyDNSProviderType("cmcccloud")
ApplyDNSProviderTypeDeSEC = ApplyDNSProviderType("desec")
ApplyDNSProviderTypeDNSLA = ApplyDNSProviderType("dnsla")
ApplyDNSProviderTypeDynv6 = ApplyDNSProviderType("dynv6")
ApplyDNSProviderTypeGcore = ApplyDNSProviderType("gcore")
ApplyDNSProviderTypeGname = ApplyDNSProviderType("gname")
ApplyDNSProviderTypeGoDaddy = ApplyDNSProviderType("godaddy")
ApplyDNSProviderTypeHuaweiCloud = ApplyDNSProviderType("huaweicloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeHuaweiCloudDNS]
ApplyDNSProviderTypeHuaweiCloudDNS = ApplyDNSProviderType("huaweicloud-dns")
ApplyDNSProviderTypeJDCloud = ApplyDNSProviderType("jdcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeJDCloudDNS]
ApplyDNSProviderTypeJDCloudDNS = ApplyDNSProviderType("jdcloud-dns")
ApplyDNSProviderTypeNamecheap = ApplyDNSProviderType("namecheap")
ApplyDNSProviderTypeNameDotCom = ApplyDNSProviderType("namedotcom")
ApplyDNSProviderTypeNameSilo = ApplyDNSProviderType("namesilo")
ApplyDNSProviderTypeNS1 = ApplyDNSProviderType("ns1")
ApplyDNSProviderTypePorkbun = ApplyDNSProviderType("porkbun")
ApplyDNSProviderTypePowerDNS = ApplyDNSProviderType("powerdns")
ApplyDNSProviderTypeRainYun = ApplyDNSProviderType("rainyun")
ApplyDNSProviderTypeTencentCloud = ApplyDNSProviderType("tencentcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeTencentCloudDNS]
ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns")
ApplyDNSProviderTypeTencentCloudEO = ApplyDNSProviderType("tencentcloud-eo")
ApplyDNSProviderTypeVercel = ApplyDNSProviderType("vercel")
ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS]
ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns")
ApplyDNSProviderTypeWestcn = ApplyDNSProviderType("westcn")
CAProviderTypeBuypass = CAProviderType(AccessProviderTypeBuypass)
CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices)
CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt)
CAProviderTypeLetsEncryptStaging = CAProviderType(AccessProviderTypeLetsEncryptStaging)
CAProviderTypeSSLCom = CAProviderType(AccessProviderTypeSSLCOM)
CAProviderTypeZeroSSL = CAProviderType(AccessProviderTypeZeroSSL)
)
type DeployProviderType string
type ACMEDns01ProviderType string
/*
ACME DNS-01 提供商常量值
短横线前的部分始终等于授权提供商类型
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud)
ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
)
type DeploymentProviderType string
/*
部署证书主机提供商常量值
@ -144,78 +151,97 @@ type DeployProviderType string
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
DeployProviderType1PanelConsole = DeployProviderType("1panel-console")
DeployProviderType1PanelSite = DeployProviderType("1panel-site")
DeployProviderTypeAliyunALB = DeployProviderType("aliyun-alb")
DeployProviderTypeAliyunAPIGW = DeployProviderType("aliyun-apigw")
DeployProviderTypeAliyunCAS = DeployProviderType("aliyun-cas")
DeployProviderTypeAliyunCASDeploy = DeployProviderType("aliyun-casdeploy")
DeployProviderTypeAliyunCDN = DeployProviderType("aliyun-cdn")
DeployProviderTypeAliyunCLB = DeployProviderType("aliyun-clb")
DeployProviderTypeAliyunDCDN = DeployProviderType("aliyun-dcdn")
DeployProviderTypeAliyunESA = DeployProviderType("aliyun-esa")
DeployProviderTypeAliyunFC = DeployProviderType("aliyun-fc")
DeployProviderTypeAliyunLive = DeployProviderType("aliyun-live")
DeployProviderTypeAliyunNLB = DeployProviderType("aliyun-nlb")
DeployProviderTypeAliyunOSS = DeployProviderType("aliyun-oss")
DeployProviderTypeAliyunVOD = DeployProviderType("aliyun-vod")
DeployProviderTypeAliyunWAF = DeployProviderType("aliyun-waf")
DeployProviderTypeAWSACM = DeployProviderType("aws-acm")
DeployProviderTypeAWSCloudFront = DeployProviderType("aws-cloudfront")
DeployProviderTypeAzureKeyVault = DeployProviderType("azure-keyvault")
DeployProviderTypeBaiduCloudAppBLB = DeployProviderType("baiducloud-appblb")
DeployProviderTypeBaiduCloudBLB = DeployProviderType("baiducloud-blb")
DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn")
DeployProviderTypeBaiduCloudCert = DeployProviderType("baiducloud-cert")
DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn")
DeployProviderTypeBaotaPanelConsole = DeployProviderType("baotapanel-console")
DeployProviderTypeBaotaPanelSite = DeployProviderType("baotapanel-site")
DeployProviderTypeBunnyCDN = DeployProviderType("bunny-cdn")
DeployProviderTypeBytePlusCDN = DeployProviderType("byteplus-cdn")
DeployProviderTypeCacheFly = DeployProviderType("cachefly")
DeployProviderTypeCdnfly = DeployProviderType("cdnfly")
DeployProviderTypeDogeCloudCDN = DeployProviderType("dogecloud-cdn")
DeployProviderTypeEdgioApplications = DeployProviderType("edgio-applications")
DeployProviderTypeGcoreCDN = DeployProviderType("gcore-cdn")
DeployProviderTypeHuaweiCloudCDN = DeployProviderType("huaweicloud-cdn")
DeployProviderTypeHuaweiCloudELB = DeployProviderType("huaweicloud-elb")
DeployProviderTypeHuaweiCloudSCM = DeployProviderType("huaweicloud-scm")
DeployProviderTypeHuaweiCloudWAF = DeployProviderType("huaweicloud-waf")
DeployProviderTypeJDCloudALB = DeployProviderType("jdcloud-alb")
DeployProviderTypeJDCloudCDN = DeployProviderType("jdcloud-cdn")
DeployProviderTypeJDCloudLive = DeployProviderType("jdcloud-live")
DeployProviderTypeJDCloudVOD = DeployProviderType("jdcloud-vod")
DeployProviderTypeKubernetesSecret = DeployProviderType("k8s-secret")
DeployProviderTypeLocal = DeployProviderType("local")
DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn")
DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo")
DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili")
DeployProviderTypeRainYunRCDN = DeployProviderType("rainyun-rcdn")
DeployProviderTypeSafeLine = DeployProviderType("safeline")
DeployProviderTypeSSH = DeployProviderType("ssh")
DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn")
DeployProviderTypeTencentCloudCLB = DeployProviderType("tencentcloud-clb")
DeployProviderTypeTencentCloudCOS = DeployProviderType("tencentcloud-cos")
DeployProviderTypeTencentCloudCSS = DeployProviderType("tencentcloud-css")
DeployProviderTypeTencentCloudECDN = DeployProviderType("tencentcloud-ecdn")
DeployProviderTypeTencentCloudEO = DeployProviderType("tencentcloud-eo")
DeployProviderTypeTencentCloudSCF = DeployProviderType("tencentcloud-scf")
DeployProviderTypeTencentCloudSSL = DeployProviderType("tencentcloud-ssl")
DeployProviderTypeTencentCloudSSLDeploy = DeployProviderType("tencentcloud-ssldeploy")
DeployProviderTypeTencentCloudVOD = DeployProviderType("tencentcloud-vod")
DeployProviderTypeTencentCloudWAF = DeployProviderType("tencentcloud-waf")
DeployProviderTypeUCloudUCDN = DeployProviderType("ucloud-ucdn")
DeployProviderTypeUCloudUS3 = DeployProviderType("ucloud-us3")
DeployProviderTypeUpyunCDN = DeployProviderType("upyun-cdn")
DeployProviderTypeUpyunFile = DeployProviderType("upyun-file")
DeployProviderTypeVolcEngineALB = DeployProviderType("volcengine-alb")
DeployProviderTypeVolcEngineCDN = DeployProviderType("volcengine-cdn")
DeployProviderTypeVolcEngineCertCenter = DeployProviderType("volcengine-certcenter")
DeployProviderTypeVolcEngineCLB = DeployProviderType("volcengine-clb")
DeployProviderTypeVolcEngineDCDN = DeployProviderType("volcengine-dcdn")
DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex")
DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live")
DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos")
DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro")
DeployProviderTypeWebhook = DeployProviderType("webhook")
DeploymentProviderType1PanelConsole = DeploymentProviderType(AccessProviderType1Panel + "-console")
DeploymentProviderType1PanelSite = DeploymentProviderType(AccessProviderType1Panel + "-site")
DeploymentProviderTypeAliyunALB = DeploymentProviderType(AccessProviderTypeAliyun + "-alb")
DeploymentProviderTypeAliyunAPIGW = DeploymentProviderType(AccessProviderTypeAliyun + "-apigw")
DeploymentProviderTypeAliyunCAS = DeploymentProviderType(AccessProviderTypeAliyun + "-cas")
DeploymentProviderTypeAliyunCASDeploy = DeploymentProviderType(AccessProviderTypeAliyun + "-casdeploy")
DeploymentProviderTypeAliyunCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-cdn")
DeploymentProviderTypeAliyunCLB = DeploymentProviderType(AccessProviderTypeAliyun + "-clb")
DeploymentProviderTypeAliyunDCDN = DeploymentProviderType(AccessProviderTypeAliyun + "-dcdn")
DeploymentProviderTypeAliyunESA = DeploymentProviderType(AccessProviderTypeAliyun + "-esa")
DeploymentProviderTypeAliyunFC = DeploymentProviderType(AccessProviderTypeAliyun + "-fc")
DeploymentProviderTypeAliyunLive = DeploymentProviderType(AccessProviderTypeAliyun + "-live")
DeploymentProviderTypeAliyunNLB = DeploymentProviderType(AccessProviderTypeAliyun + "-nlb")
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod")
DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf")
DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm")
DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront")
DeploymentProviderTypeAzureKeyVault = DeploymentProviderType(AccessProviderTypeAzure + "-keyvault")
DeploymentProviderTypeBaiduCloudAppBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-appblb")
DeploymentProviderTypeBaiduCloudBLB = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-blb")
DeploymentProviderTypeBaiduCloudCDN = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cdn")
DeploymentProviderTypeBaiduCloudCert = DeploymentProviderType(AccessProviderTypeBaiduCloud + "-cert")
DeploymentProviderTypeBaishanCDN = DeploymentProviderType(AccessProviderTypeBaishan + "-cdn")
DeploymentProviderTypeBaotaPanelConsole = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-console")
DeploymentProviderTypeBaotaPanelSite = DeploymentProviderType(AccessProviderTypeBaotaPanel + "-site")
DeploymentProviderTypeBunnyCDN = DeploymentProviderType(AccessProviderTypeBunny + "-cdn")
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
DeploymentProviderTypeGcoreCDN = DeploymentProviderType(AccessProviderTypeGcore + "-cdn")
DeploymentProviderTypeHuaweiCloudCDN = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-cdn")
DeploymentProviderTypeHuaweiCloudELB = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-elb")
DeploymentProviderTypeHuaweiCloudSCM = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-scm")
DeploymentProviderTypeHuaweiCloudWAF = DeploymentProviderType(AccessProviderTypeHuaweiCloud + "-waf")
DeploymentProviderTypeJDCloudALB = DeploymentProviderType(AccessProviderTypeJDCloud + "-alb")
DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn")
DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live")
DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod")
DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret")
DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal)
DeploymentProviderTypeQiniuCDN = DeploymentProviderType(AccessProviderTypeQiniu + "-cdn")
DeploymentProviderTypeQiniuKodo = DeploymentProviderType(AccessProviderTypeQiniu + "-kodo")
DeploymentProviderTypeQiniuPili = DeploymentProviderType(AccessProviderTypeQiniu + "-pili")
DeploymentProviderTypeRainYunRCDN = DeploymentProviderType(AccessProviderTypeRainYun + "-rcdn")
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn")
DeploymentProviderTypeTencentCloudCLB = DeploymentProviderType(AccessProviderTypeTencentCloud + "-clb")
DeploymentProviderTypeTencentCloudCOS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cos")
DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css")
DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn")
DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo")
DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf")
DeploymentProviderTypeTencentCloudSSL = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssl")
DeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssldeploy")
DeploymentProviderTypeTencentCloudVOD = DeploymentProviderType(AccessProviderTypeTencentCloud + "-vod")
DeploymentProviderTypeTencentCloudWAF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-waf")
DeploymentProviderTypeUCloudUCDN = DeploymentProviderType(AccessProviderTypeUCloud + "-ucdn")
DeploymentProviderTypeUCloudUS3 = DeploymentProviderType(AccessProviderTypeUCloud + "-us3")
DeploymentProviderTypeUpyunCDN = DeploymentProviderType(AccessProviderTypeUpyun + "-cdn")
DeploymentProviderTypeUpyunFile = DeploymentProviderType(AccessProviderTypeUpyun + "-file")
DeploymentProviderTypeVolcEngineALB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-alb")
DeploymentProviderTypeVolcEngineCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-cdn")
DeploymentProviderTypeVolcEngineCertCenter = DeploymentProviderType(AccessProviderTypeVolcEngine + "-certcenter")
DeploymentProviderTypeVolcEngineCLB = DeploymentProviderType(AccessProviderTypeVolcEngine + "-clb")
DeploymentProviderTypeVolcEngineDCDN = DeploymentProviderType(AccessProviderTypeVolcEngine + "-dcdn")
DeploymentProviderTypeVolcEngineImageX = DeploymentProviderType(AccessProviderTypeVolcEngine + "-imagex")
DeploymentProviderTypeVolcEngineLive = DeploymentProviderType(AccessProviderTypeVolcEngine + "-live")
DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos")
DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro")
DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook)
)
type NotificationProviderType string
/*
消息通知提供商常量值
短横线前的部分始终等于授权提供商类型
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
NotificationProviderTypeDingTalkBot = NotificationProviderType(AccessProviderTypeDingTalkBot)
NotificationProviderTypeEmail = NotificationProviderType(AccessProviderTypeEmail)
NotificationProviderTypeLarkBot = NotificationProviderType(AccessProviderTypeLarkBot)
NotificationProviderTypeMattermost = NotificationProviderType(AccessProviderTypeMattermost)
NotificationProviderTypeTelegram = NotificationProviderType(AccessProviderTypeTelegram)
NotificationProviderTypeWebhook = NotificationProviderType(AccessProviderTypeWebhook)
NotificationProviderTypeWeComBot = NotificationProviderType(AccessProviderTypeWeComBot)
)

View File

@ -13,6 +13,7 @@ type Settings struct {
Content string `json:"content" db:"content"`
}
// Deprecated: v0.4.x 将废弃
type NotifyTemplatesSettingsContent struct {
NotifyTemplates []struct {
Subject string `json:"subject"`
@ -20,8 +21,10 @@ type NotifyTemplatesSettingsContent struct {
} `json:"notifyTemplates"`
}
// Deprecated: v0.4.x 将废弃
type NotifyChannelsSettingsContent map[string]map[string]any
// Deprecated: v0.4.x 将废弃
func (s *Settings) GetNotifyChannelConfig(channel string) (map[string]any, error) {
conf := &NotifyChannelsSettingsContent{}
if err := json.Unmarshal([]byte(s.Content), conf); err != nil {

View File

@ -71,7 +71,7 @@ type WorkflowNodeConfigForApply struct {
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"` // 密钥算法
KeyAlgorithm string `json:"keyAlgorithm"` // 证书算法
Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔
DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播超时时间(零值取决于提供商的默认值)
DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS TTL零值取决于提供商的默认值
@ -95,9 +95,12 @@ type WorkflowNodeConfigForDeploy struct {
}
type WorkflowNodeConfigForNotify struct {
Channel string `json:"channel"` // 通知渠道
Subject string `json:"subject"` // 通知主题
Message string `json:"message"` // 通知内容
Channel string `json:"channel,omitempty"` // Deprecated: v0.4.x 将废弃
Provider string `json:"provider"` // 通知提供商
ProviderAccessId string `json:"providerAccessId"` // 通知提供商授权记录 ID
ProviderConfig map[string]any `json:"providerConfig,omitempty"` // 通知提供商额外配置
Subject string `json:"subject"` // 通知主题
Message string `json:"message"` // 通知内容
}
func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
@ -111,10 +114,10 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply {
ContactEmail: maputil.GetString(n.Config, "contactEmail"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
CAProvider: maputil.GetString(n.Config, "caProvider"),
CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"),
CAProviderConfig: maputil.GetAnyMap(n.Config, "caProviderConfig"),
CAProviderConfig: maputil.GetKVMapAny(n.Config, "caProviderConfig"),
KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"),
Nameservers: maputil.GetString(n.Config, "nameservers"),
DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"),
@ -138,16 +141,19 @@ func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy {
Certificate: maputil.GetString(n.Config, "certificate"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
SkipOnLastSucceeded: maputil.GetBool(n.Config, "skipOnLastSucceeded"),
}
}
func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify {
return WorkflowNodeConfigForNotify{
Channel: maputil.GetString(n.Config, "channel"),
Subject: maputil.GetString(n.Config, "subject"),
Message: maputil.GetString(n.Config, "message"),
Channel: maputil.GetString(n.Config, "channel"),
Provider: maputil.GetString(n.Config, "provider"),
ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"),
ProviderConfig: maputil.GetKVMapAny(n.Config, "providerConfig"),
Subject: maputil.GetString(n.Config, "subject"),
Message: maputil.GetString(n.Config, "message"),
}
}

View File

@ -0,0 +1,72 @@
package notify
import (
"context"
"fmt"
"log/slog"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
"github.com/usual2970/certimate/internal/repository"
)
type Notifier interface {
Notify(ctx context.Context) error
}
type NotifierWithWorkflowNodeConfig struct {
Node *domain.WorkflowNode
Logger *slog.Logger
Subject string
Message string
}
func NewWithWorkflowNode(config NotifierWithWorkflowNodeConfig) (Notifier, error) {
if config.Node == nil {
return nil, fmt.Errorf("node is nil")
}
if config.Node.Type != domain.WorkflowNodeTypeNotify {
return nil, fmt.Errorf("node type is not '%s'", string(domain.WorkflowNodeTypeNotify))
}
nodeConfig := config.Node.GetConfigForNotify()
options := &notifierProviderOptions{
Provider: domain.NotificationProviderType(nodeConfig.Provider),
ProviderAccessConfig: make(map[string]any),
ProviderExtendedConfig: nodeConfig.ProviderConfig,
}
accessRepo := repository.NewAccessRepository()
if nodeConfig.ProviderAccessId != "" {
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
if err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
} else {
options.ProviderAccessConfig = access.Config
}
}
notifierProvider, err := createNotifierProvider(options)
if err != nil {
return nil, err
}
return &notifierImpl{
provider: notifierProvider.WithLogger(config.Logger),
subject: config.Subject,
message: config.Message,
}, nil
}
type notifierImpl struct {
provider notifier.Notifier
subject string
message string
}
var _ Notifier = (*notifierImpl)(nil)
func (n *notifierImpl) Notify(ctx context.Context) error {
_, err := n.provider.Notify(ctx, n.subject, n.message)
return err
}

View File

@ -13,6 +13,7 @@ import (
"github.com/usual2970/certimate/internal/repository"
)
// Deprecated: v0.4.x 将废弃
func SendToAllChannels(subject, message string) error {
notifiers, err := getEnabledNotifiers()
if err != nil {
@ -38,8 +39,9 @@ func SendToAllChannels(subject, message string) error {
return err
}
// Deprecated: v0.4.x 将废弃
func SendToChannel(subject, message string, channel string, channelConfig map[string]any) error {
notifier, err := createNotifier(domain.NotifyChannelType(channel), channelConfig)
notifier, err := createNotifierProviderUseGlobalSettings(domain.NotifyChannelType(channel), channelConfig)
if err != nil {
return err
}
@ -48,6 +50,7 @@ func SendToChannel(subject, message string, channel string, channelConfig map[st
return err
}
// Deprecated: v0.4.x 将废弃
func getEnabledNotifiers() ([]notifier.Notifier, error) {
settingsRepo := repository.NewSettingsRepository()
settings, err := settingsRepo.GetByName(context.Background(), "notifyChannels")
@ -66,7 +69,7 @@ func getEnabledNotifiers() ([]notifier.Notifier, error) {
continue
}
notifier, err := createNotifier(domain.NotifyChannelType(k), v)
notifier, err := createNotifierProviderUseGlobalSettings(domain.NotifyChannelType(k), v)
if err != nil {
continue
}

View File

@ -2,105 +2,152 @@ package notify
import (
"fmt"
"net/http"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
httputil "github.com/usual2970/certimate/internal/pkg/utils/http"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]any) (notifier.Notifier, error) {
type notifierProviderOptions struct {
Provider domain.NotificationProviderType
ProviderAccessConfig map[string]any
ProviderExtendedConfig map[string]any
}
func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier, error) {
/*
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
switch channel {
case domain.NotifyChannelTypeBark:
return pBark.NewNotifier(&pBark.NotifierConfig{
DeviceKey: maputil.GetString(channelConfig, "deviceKey"),
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
})
switch options.Provider {
case domain.NotificationProviderTypeDingTalkBot:
{
access := domain.AccessConfigForDingTalkBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
case domain.NotifyChannelTypeDingTalk:
return pDingTalk.NewNotifier(&pDingTalk.NotifierConfig{
AccessToken: maputil.GetString(channelConfig, "accessToken"),
Secret: maputil.GetString(channelConfig, "secret"),
})
return pDingTalk.NewNotifier(&pDingTalk.NotifierConfig{
WebhookUrl: access.WebhookUrl,
Secret: access.Secret,
})
}
case domain.NotifyChannelTypeEmail:
return pEmail.NewNotifier(&pEmail.NotifierConfig{
SmtpHost: maputil.GetString(channelConfig, "smtpHost"),
SmtpPort: maputil.GetInt32(channelConfig, "smtpPort"),
SmtpTLS: maputil.GetOrDefaultBool(channelConfig, "smtpTLS", true),
Username: maputil.GetOrDefaultString(channelConfig, "username", maputil.GetString(channelConfig, "senderAddress")),
Password: maputil.GetString(channelConfig, "password"),
SenderAddress: maputil.GetString(channelConfig, "senderAddress"),
ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"),
})
case domain.NotificationProviderTypeEmail:
{
access := domain.AccessConfigForEmail{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
case domain.NotifyChannelTypeGotify:
return pGotify.NewNotifier(&pGotify.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
Token: maputil.GetString(channelConfig, "token"),
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
})
return pEmail.NewNotifier(&pEmail.NotifierConfig{
SmtpHost: access.SmtpHost,
SmtpPort: access.SmtpPort,
SmtpTls: access.SmtpTls,
Username: access.Username,
Password: access.Password,
SenderAddress: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "senderAddress", access.DefaultSenderAddress),
ReceiverAddress: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "receiverAddress", access.DefaultReceiverAddress),
})
}
case domain.NotifyChannelTypeLark:
return pLark.NewNotifier(&pLark.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
case domain.NotificationProviderTypeLarkBot:
{
access := domain.AccessConfigForLarkBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
case domain.NotifyChannelTypeMattermost:
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
ChannelId: maputil.GetString(channelConfig, "channelId"),
Username: maputil.GetString(channelConfig, "username"),
Password: maputil.GetString(channelConfig, "password"),
})
case domain.NotifyChannelTypePushover:
return pPushover.NewNotifier(&pPushover.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
User: maputil.GetString(channelConfig, "user"),
})
return pLark.NewNotifier(&pLark.NotifierConfig{
WebhookUrl: access.WebhookUrl,
})
}
case domain.NotifyChannelTypePushPlus:
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
})
case domain.NotificationProviderTypeMattermost:
{
access := domain.AccessConfigForMattermost{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
case domain.NotifyChannelTypeServerChan:
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
})
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: access.ServerUrl,
Username: access.Username,
Password: access.Password,
ChannelId: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "channelId", access.DefaultChannelId),
})
}
case domain.NotifyChannelTypeTelegram:
return pTelegram.NewNotifier(&pTelegram.NotifierConfig{
ApiToken: maputil.GetString(channelConfig, "apiToken"),
ChatId: maputil.GetInt64(channelConfig, "chatId"),
})
case domain.NotificationProviderTypeTelegram:
{
access := domain.AccessConfigForTelegram{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
case domain.NotifyChannelTypeWebhook:
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
AllowInsecureConnections: maputil.GetBool(channelConfig, "allowInsecureConnections"),
})
return pTelegram.NewNotifier(&pTelegram.NotifierConfig{
BotToken: access.BotToken,
ChatId: maputil.GetOrDefaultInt64(options.ProviderExtendedConfig, "chatId", access.DefaultChatId),
})
}
case domain.NotifyChannelTypeWeCom:
return pWeCom.NewNotifier(&pWeCom.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
case domain.NotificationProviderTypeWebhook:
{
access := domain.AccessConfigForWebhook{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
mergedHeaders := make(map[string]string)
if defaultHeadersString := access.HeadersString; defaultHeadersString != "" {
h, err := httputil.ParseHeaders(defaultHeadersString)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
}
for key := range h {
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
}
}
if extendedHeadersString := maputil.GetString(options.ProviderExtendedConfig, "headers"); extendedHeadersString != "" {
h, err := httputil.ParseHeaders(extendedHeadersString)
if err != nil {
return nil, fmt.Errorf("failed to parse webhook headers: %w", err)
}
for key := range h {
mergedHeaders[http.CanonicalHeaderKey(key)] = h.Get(key)
}
}
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
WebhookUrl: access.Url,
WebhookData: maputil.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", access.DefaultDataForNotification),
Method: access.Method,
Headers: mergedHeaders,
AllowInsecureConnections: access.AllowInsecureConnections,
})
}
case domain.NotificationProviderTypeWeComBot:
{
access := domain.AccessConfigForWeComBot{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
return pWeCom.NewNotifier(&pWeCom.NotifierConfig{
WebhookUrl: access.WebhookUrl,
})
}
}
return nil, fmt.Errorf("unsupported notifier channel '%s'", channelConfig)
return nil, fmt.Errorf("unsupported notifier provider '%s'", options.Provider)
}

View File

@ -0,0 +1,108 @@
package notify
import (
"fmt"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
pBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
pDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify"
pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost"
pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover"
pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus"
pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
pWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
pWeCom "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/wecom"
maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
)
// Deprecated: v0.4.x 将废弃
func createNotifierProviderUseGlobalSettings(channel domain.NotifyChannelType, channelConfig map[string]any) (notifier.Notifier, error) {
/*
注意如果追加新的常量值请保持以 ASCII 排序
NOTICE: If you add new constant, please keep ASCII order.
*/
switch channel {
case domain.NotifyChannelTypeBark:
return pBark.NewNotifier(&pBark.NotifierConfig{
DeviceKey: maputil.GetString(channelConfig, "deviceKey"),
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
})
case domain.NotifyChannelTypeDingTalk:
return pDingTalk.NewNotifier(&pDingTalk.NotifierConfig{
WebhookUrl: "https://oapi.dingtalk.com/robot/send?access_token=" + maputil.GetString(channelConfig, "accessToken"),
Secret: maputil.GetString(channelConfig, "secret"),
})
case domain.NotifyChannelTypeEmail:
return pEmail.NewNotifier(&pEmail.NotifierConfig{
SmtpHost: maputil.GetString(channelConfig, "smtpHost"),
SmtpPort: maputil.GetInt32(channelConfig, "smtpPort"),
SmtpTls: maputil.GetOrDefaultBool(channelConfig, "smtpTLS", true),
Username: maputil.GetOrDefaultString(channelConfig, "username", maputil.GetString(channelConfig, "senderAddress")),
Password: maputil.GetString(channelConfig, "password"),
SenderAddress: maputil.GetString(channelConfig, "senderAddress"),
ReceiverAddress: maputil.GetString(channelConfig, "receiverAddress"),
})
case domain.NotifyChannelTypeGotify:
return pGotify.NewNotifier(&pGotify.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
Token: maputil.GetString(channelConfig, "token"),
Priority: maputil.GetOrDefaultInt64(channelConfig, "priority", 1),
})
case domain.NotifyChannelTypeLark:
return pLark.NewNotifier(&pLark.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
case domain.NotifyChannelTypeMattermost:
return pMattermost.NewNotifier(&pMattermost.NotifierConfig{
ServerUrl: maputil.GetString(channelConfig, "serverUrl"),
ChannelId: maputil.GetString(channelConfig, "channelId"),
Username: maputil.GetString(channelConfig, "username"),
Password: maputil.GetString(channelConfig, "password"),
})
case domain.NotifyChannelTypePushover:
return pPushover.NewNotifier(&pPushover.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
User: maputil.GetString(channelConfig, "user"),
})
case domain.NotifyChannelTypePushPlus:
return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{
Token: maputil.GetString(channelConfig, "token"),
})
case domain.NotifyChannelTypeServerChan:
return pServerChan.NewNotifier(&pServerChan.NotifierConfig{
Url: maputil.GetString(channelConfig, "url"),
})
case domain.NotifyChannelTypeTelegram:
return pTelegram.NewNotifier(&pTelegram.NotifierConfig{
BotToken: maputil.GetString(channelConfig, "apiToken"),
ChatId: maputil.GetInt64(channelConfig, "chatId"),
})
case domain.NotifyChannelTypeWebhook:
return pWebhook.NewNotifier(&pWebhook.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "url"),
AllowInsecureConnections: maputil.GetBool(channelConfig, "allowInsecureConnections"),
})
case domain.NotifyChannelTypeWeCom:
return pWeCom.NewNotifier(&pWeCom.NotifierConfig{
WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"),
})
}
return nil, fmt.Errorf("unsupported notifier channel '%s'", channelConfig)
}

View File

@ -8,25 +8,30 @@ import (
"github.com/usual2970/certimate/internal/domain/dtos"
)
// Deprecated: v0.4.x 将废弃
const (
notifyTestTitle = "测试通知"
notifyTestBody = "欢迎使用 Certimate ,这是一条测试通知。"
)
// Deprecated: v0.4.x 将废弃
type settingsRepository interface {
GetByName(ctx context.Context, name string) (*domain.Settings, error)
}
// Deprecated: v0.4.x 将废弃
type NotifyService struct {
settingsRepo settingsRepository
}
// Deprecated: v0.4.x 将废弃
func NewNotifyService(settingsRepo settingsRepository) *NotifyService {
return &NotifyService{
settingsRepo: settingsRepo,
}
}
// Deprecated: v0.4.x 将废弃
func (n *NotifyService) Test(ctx context.Context, req *dtos.NotifyTestPushReq) error {
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
if err != nil {

View File

@ -137,6 +137,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &alialb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
@ -166,6 +172,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
listListenersToken = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &alialb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
@ -262,6 +274,12 @@ func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudL
listListenerCertificatesLimit := int32(100)
var listListenerCertificatesToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenerCertificatesReq := &alialb.ListListenerCertificatesRequest{
NextToken: listListenerCertificatesToken,
MaxResults: tea.Int32(listListenerCertificatesLimit),

View File

@ -142,6 +142,12 @@ func (d *DeployerProvider) deployToCloudNative(ctx context.Context, certPEM stri
listDomainsPageNumber := int32(1)
listDomainsPageSize := int32(10)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listDomainsReq := &aliapig.ListDomainsRequest{
GatewayId: tea.String(d.config.GatewayId),
NameLike: tea.String(d.config.Domain),

View File

@ -126,8 +126,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
// 循环获取部署任务详情,等待任务状态变更
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-describedeploymentjob
for {
if ctx.Err() != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeDeploymentJobReq := &alicas.DescribeDeploymentJobRequest{

View File

@ -132,6 +132,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
describeLoadBalancerListenersLimit := int32(100)
var describeLoadBalancerListenersToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeLoadBalancerListenersReq := &alislb.DescribeLoadBalancerListenersRequest{
RegionId: tea.String(d.config.Region),
MaxResults: tea.Int32(describeLoadBalancerListenersLimit),
@ -166,8 +172,14 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listenerPort := range listenerPorts {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -125,6 +125,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &alinlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
@ -158,8 +164,14 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -152,8 +152,14 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
@ -209,8 +215,14 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -152,8 +152,14 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}
@ -209,8 +215,14 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str
var errs []error
for _, listener := range listeners {
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -117,16 +117,22 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
var errs []error
for _, domain := range domains {
// 关联证书与加速域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert
batchDeployCertReq := &bpcdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp))
if err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
// 关联证书与加速域名
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert
batchDeployCertReq := &bpcdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp))
if err != nil {
errs = append(errs, err)
}
}
}

View File

@ -160,6 +160,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, certPEM str
listListenersLimit := int32(2000)
var listListenersMarker *string = nil
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listListenersReq := &hcelbmodel.ListListenersRequest{
Limit: typeutil.ToPtr(listListenersLimit),
Marker: listListenersMarker,
@ -201,8 +207,14 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, certPEM str
var errs []error
for _, listenerId := range listenerIds {
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -172,6 +172,12 @@ func (d *DeployerProvider) deployToCloudServer(ctx context.Context, certPEM stri
listHostPage := int32(1)
listHostPageSize := int32(100)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listHostReq := &hcwafmodel.ListHostRequest{
Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")),
Page: typeutil.ToPtr(listHostPage),
@ -239,6 +245,12 @@ func (d *DeployerProvider) deployToPremiumHost(ctx context.Context, certPEM stri
listPremiumHostPage := int32(1)
listPremiumHostPageSize := int32(100)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
listPremiumHostReq := &hcwafmodel.ListPremiumHostRequest{
Hostname: typeutil.ToPtr(strings.TrimPrefix(d.config.Domain, "*")),
Page: typeutil.ToPtr(fmt.Sprintf("%d", listPremiumHostPage)),

View File

@ -132,6 +132,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
describeListenersPageNumber := 1
describeListenersPageSize := 100
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeListenersReq := jdlbapi.NewDescribeListenersRequest(d.config.RegionId)
describeListenersReq.SetFilters([]jdcommon.Filter{{Name: "loadBalancerId", Values: []string{d.config.LoadbalancerId}}})
describeListenersReq.SetPageSize(describeListenersPageNumber)
@ -164,8 +170,13 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -65,6 +65,12 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
listDomainsPageNumber := 1
listDomainsPageSize := 100
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listDomainsReq := jdvodapi.NewListDomainsRequest()
listDomainsReq.SetPageNumber(1)
listDomainsReq.SetPageSize(100)

View File

@ -188,8 +188,13 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listenerId := range listenerIds {
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -108,8 +108,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
// 循环获取部署任务详情,等待任务状态变更
// REF: https://cloud.tencent.com.cn/document/api/400/91658
for {
if ctx.Err() != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeHostDeployRecordDetailReq := tcssl.NewDescribeHostDeployRecordDetailRequest()

View File

@ -132,6 +132,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
describeListenersPageSize := int64(100)
describeListenersPageNumber := int64(1)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeListenersReq := &vealb.DescribeListenersInput{
LoadBalancerId: ve.String(d.config.LoadbalancerId),
Protocol: ve.String("HTTPS"),
@ -163,8 +169,13 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -117,16 +117,21 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
var errs []error
for _, domain := range domains {
// 关联证书与加速域名
// REF: https://www.volcengine.com/docs/6454/125712
batchDeployCertReq := &vecdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp))
if err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
// 关联证书与加速域名
// REF: https://www.volcengine.com/docs/6454/125712
batchDeployCertReq := &vecdn.BatchDeployCertRequest{
CertId: upres.CertId,
Domain: domain,
}
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
d.logger.Debug("sdk request 'cdn.BatchDeployCert'", slog.Any("request", batchDeployCertReq), slog.Any("response", batchDeployCertResp))
if err != nil {
errs = append(errs, err)
}
}
}

View File

@ -128,6 +128,12 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
describeListenersPageSize := int64(100)
describeListenersPageNumber := int64(1)
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeListenersReq := &veclb.DescribeListenersInput{
LoadBalancerId: ve.String(d.config.LoadbalancerId),
Protocol: ve.String("HTTPS"),
@ -159,8 +165,13 @@ func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId
var errs []error
for _, listenerId := range listenerIds {
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
errs = append(errs, err)
}
}
}

View File

@ -125,17 +125,22 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
var errs []error
for _, domain := range domains {
// 绑定证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6
bindCertReq := &velive.BindCertBody{
ChainID: upres.CertId,
Domain: domain,
HTTPS: ve.Bool(true),
}
bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq)
d.logger.Debug("sdk request 'live.BindCert'", slog.Any("request", bindCertReq), slog.Any("response", bindCertResp))
if err != nil {
errs = append(errs, err)
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
// 绑定证书
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6
bindCertReq := &velive.BindCertBody{
ChainID: upres.CertId,
Domain: domain,
HTTPS: ve.Bool(true),
}
bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq)
d.logger.Debug("sdk request 'live.BindCert'", slog.Any("request", bindCertReq), slog.Any("response", bindCertResp))
if err != nil {
errs = append(errs, err)
}
}
}

View File

@ -199,8 +199,10 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
wangsuTaskId = wangsuTaskMatches[1]
}
for {
if ctx.Err() != nil {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId)

View File

@ -6,6 +6,8 @@ import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
@ -18,8 +20,13 @@ import (
type DeployerConfig struct {
// Webhook URL。
WebhookUrl string `json:"webhookUrl"`
// Webhook 回调数据(JSON 格式)。
// Webhook 回调数据(application/json 或 application/x-www-form-urlencoded 格式)。
WebhookData string `json:"webhookData,omitempty"`
// 请求谓词。
// 零值时默认为 "POST"。
Method string `json:"method,omitempty"`
// 请求标头。
Headers map[string]string `json:"headers,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
@ -62,31 +69,111 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
}
func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse x509: %w", err)
}
var webhookData interface{}
err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)
// 处理 Webhook URL
webhookUrl, err := url.Parse(d.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("failed to unmarshall webhook data: %w", err)
return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme)
} else {
webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${DOMAIN}", url.PathEscape(certX509.Subject.CommonName))
}
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${SUBJECT_ALT_NAMES}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM)
// 处理 Webhook 请求谓词
webhookMethod := strings.ToUpper(d.config.Method)
if webhookMethod == "" {
webhookMethod = http.MethodPost
} else if webhookMethod != http.MethodGet &&
webhookMethod != http.MethodPost &&
webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod)
}
resp, err := d.httpClient.R().
// 处理 Webhook 请求标头
webhookHeaders := make(http.Header)
for k, v := range d.config.Headers {
webhookHeaders.Set(k, v)
}
// 处理 Webhook 请求内容类型
const CONTENT_TYPE_JSON = "application/json"
const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
const CONTENT_TYPE_MULTIPART = "multipart/form-data"
webhookContentType := webhookHeaders.Get("Content-Type")
if webhookContentType == "" {
webhookContentType = CONTENT_TYPE_JSON
webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON)
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType)
}
// 处理 Webhook 请求数据
var webhookData interface{}
if d.config.WebhookData == "" {
webhookData = map[string]string{
"name": strings.Join(certX509.DNSNames, ";"),
"cert": certPEM,
"privkey": privkeyPEM,
}
} else {
err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
}
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPEM)
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPEM)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {
temp := make(map[string]string)
jsonb, err := json.Marshal(webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else if err := json.Unmarshal(jsonb, &temp); err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else {
webhookData = temp
}
}
}
// 生成请求
// 其中 GET 请求需转换为查询参数
req := d.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetBody(webhookData).
Post(d.config.WebhookUrl)
SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
req.SetQueryParams(webhookData.(map[string]string))
} else {
switch webhookContentType {
case CONTENT_TYPE_JSON:
req.SetBody(webhookData)
case CONTENT_TYPE_FORM:
req.SetFormData(webhookData.(map[string]string))
case CONTENT_TYPE_MULTIPART:
req.SetMultipartFormData(webhookData.(map[string]string))
}
}
// 发送请求
resp, err := req.SetDebug(true).Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.StatusCode() != 200 {
} else if resp.IsError() {
return nil, fmt.Errorf("unexpected webhook response status code: %d", resp.StatusCode())
}

View File

@ -12,10 +12,11 @@ import (
)
var (
fInputCertPath string
fInputKeyPath string
fWebhookUrl string
fWebhookData string
fInputCertPath string
fInputKeyPath string
fWebhookUrl string
fWebhookContentType string
fWebhookData string
)
func init() {
@ -24,6 +25,7 @@ func init() {
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "")
flag.StringVar(&fWebhookData, argsPrefix+"DATA", "", "")
}
@ -34,7 +36,8 @@ Shell command to run this test:
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_DEPLOYER_WEBHOOK_URL="https://example.com/your-webhook-url" \
--CERTIMATE_DEPLOYER_WEBHOOK_DATA="{\"certificate\":\"${Certificate}\",\"privateKey\":\"${PrivateKey}\"}"
--CERTIMATE_DEPLOYER_WEBHOOK_CONTENTTYPE="application/json" \
--CERTIMATE_DEPLOYER_WEBHOOK_DATA="{\"certificate\":\"${CERTIFICATE}\",\"privateKey\":\"${PRIVATE_KEY}\"}"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
@ -45,12 +48,17 @@ func TestDeploy(t *testing.T) {
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
fmt.Sprintf("WEBHOOKCONTENTTYPE: %v", fWebhookContentType),
fmt.Sprintf("WEBHOOKDATA: %v", fWebhookData),
}, "\n"))
deployer, err := provider.NewDeployer(&provider.DeployerConfig{
WebhookUrl: fWebhookUrl,
WebhookData: fWebhookData,
WebhookUrl: fWebhookUrl,
WebhookData: fWebhookData,
Method: "POST",
Headers: map[string]string{
"Content-Type": fWebhookContentType,
},
AllowInsecureConnections: true,
})
if err != nil {

View File

@ -32,6 +32,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}

View File

@ -2,7 +2,9 @@ package dingtalk
import (
"context"
"fmt"
"log/slog"
"net/url"
"github.com/nikoksr/notify/service/dingding"
@ -10,8 +12,8 @@ import (
)
type NotifierConfig struct {
// 钉钉机器人的 Token
AccessToken string `json:"accessToken"`
// 钉钉机器人的 Webhook 地址
WebhookUrl string `json:"webhookUrl"`
// 钉钉机器人的 Secret。
Secret string `json:"secret"`
}
@ -30,6 +32,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
@ -43,8 +46,13 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
webhookUrl, err := url.Parse(n.config.WebhookUrl)
if err != nil {
return nil, fmt.Errorf("invalid webhook url: %w", err)
}
srv := dingding.New(&dingding.Config{
Token: n.config.AccessToken,
Token: webhookUrl.Query().Get("access_token"),
Secret: n.config.Secret,
})

View File

@ -19,7 +19,7 @@ type NotifierConfig struct {
// 零值时根据是否启用 TLS 决定。
SmtpPort int32 `json:"smtpPort"`
// 是否启用 TLS。
SmtpTLS bool `json:"smtpTLS"`
SmtpTls bool `json:"smtpTls"`
// 用户名。
Username string `json:"username"`
// 密码。
@ -44,6 +44,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
@ -64,7 +65,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
var smtpAddr string
if n.config.SmtpPort == 0 {
if n.config.SmtpTLS {
if n.config.SmtpTls {
smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
} else {
smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
@ -74,7 +75,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
}
var yak *mailyak.MailYak
if n.config.SmtpTLS {
if n.config.SmtpTls {
yak, err = mailyak.NewWithTLS(smtpAddr, smtpAuth, newTlsConfig())
if err != nil {
return nil, err

View File

@ -67,7 +67,7 @@ func TestNotify(t *testing.T) {
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
SmtpHost: fSmtpHost,
SmtpPort: int32(fSmtpPort),
SmtpTLS: fSmtpTLS,
SmtpTls: fSmtpTLS,
Username: fUsername,
Password: fPassword,
SenderAddress: fSenderAddress,

View File

@ -9,25 +9,21 @@ import (
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// Gotify 服务地址
// 示例https://gotify.example.com
// Gotify 服务地址。
Url string `json:"url"`
// Gotify Token
// Gotify Token
Token string `json:"token"`
// Gotify 消息优先级
Priority int64 `json:"priority"`
// Gotify 消息优先级
Priority int64 `json:"priority,omitempty"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
config *NotifierConfig
logger *slog.Logger
httpClient *http.Client
}
@ -40,6 +36,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: http.DefaultClient,
}, nil
}
@ -54,7 +51,6 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// Gotify 原生实现, notify 库没有实现, 等待合并
reqBody := &struct {
Title string `json:"title"`
Message string `json:"message"`
@ -65,10 +61,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
Priority: n.config.Priority,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
return nil, fmt.Errorf("gotify api error: failed to encode message body: %w", err)
}
req, err := http.NewRequestWithContext(
@ -78,27 +73,24 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
return nil, fmt.Errorf("gotify api error: failed to create new request: %w", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", n.config.Token))
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to gotify service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to gotify server")
return nil, fmt.Errorf("gotify api error: failed to send request: %w", err)
}
defer resp.Body.Close()
// Read response and verify success
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
return nil, fmt.Errorf("gotify api error: failed to read response: %w", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gotify api error: unexpected status code: %d, resp: %s", resp.StatusCode, string(result))
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("gotify returned status code %d: %s", resp.StatusCode, string(result))
}
return &notifier.NotifyResult{}, nil
}

View File

@ -28,6 +28,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}

View File

@ -7,20 +7,21 @@ import (
"io"
"log/slog"
"net/http"
"strings"
"github.com/nikoksr/notify/service/mattermost"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// Mattermost 服务地址。
// 服务地址。
ServerUrl string `json:"serverUrl"`
// 频道ID
ChannelId string `json:"channelId"`
// 用户名
// 用户名。
Username string `json:"username"`
// 密码
// 密码。
Password string `json:"password"`
// 频道 ID。
ChannelId string `json:"channelId"`
}
type NotifierProvider struct {
@ -37,6 +38,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
@ -50,7 +52,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := mattermost.New(n.config.ServerUrl)
srv := mattermost.New(strings.TrimRight(n.config.ServerUrl, "/"))
if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil {
return nil, err

View File

@ -9,20 +9,19 @@ import (
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
Token string `json:"token"` // 应用 API Token
User string `json:"user"` // 用户/分组 Key
// Pushover API Token。
Token string `json:"token"`
// 用户或分组标识。
User string `json:"user"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
config *NotifierConfig
logger *slog.Logger
httpClient *http.Client
}
@ -35,6 +34,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: http.DefaultClient,
}, nil
}
@ -48,10 +48,8 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
return n
}
// Notify 发送通知
// 参考文档https://pushover.net/api
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// 请求体
// REF: https://pushover.net/api
reqBody := &struct {
Token string `json:"token"`
User string `json:"user"`
@ -64,10 +62,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
Message: message,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
return nil, fmt.Errorf("pushover api error: failed to encode message body: %w", err)
}
req, err := http.NewRequestWithContext(
@ -77,25 +74,22 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
return nil, fmt.Errorf("pushover api error: failed to create new request: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to pushover service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to pushover server")
return nil, fmt.Errorf("pushover api error: failed to send request: %w", err)
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result))
return nil, fmt.Errorf("pushover api error: failed to read response: %w", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushover api error: unexpected status code: %d, resp: %s", resp.StatusCode, string(result))
}
return &notifier.NotifyResult{}, nil

View File

@ -9,20 +9,17 @@ import (
"log/slog"
"net/http"
"github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// PushPlus Token
// PushPlus Token
Token string `json:"token"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
// 未来将移除
config *NotifierConfig
logger *slog.Logger
httpClient *http.Client
}
@ -35,6 +32,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
httpClient: http.DefaultClient,
}, nil
}
@ -48,10 +46,8 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
return n
}
// Notify 发送通知
// 参考文档https://pushplus.plus/doc/guide/api.html
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
// 请求体
// REF: https://pushplus.plus/doc/guide/api.html
reqBody := &struct {
Token string `json:"token"`
Title string `json:"title"`
@ -62,10 +58,9 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
Content: message,
}
// Make request
body, err := json.Marshal(reqBody)
if err != nil {
return nil, errors.Wrap(err, "encode message body")
return nil, fmt.Errorf("pushplus api error: failed to encode message body: %w", err)
}
req, err := http.NewRequestWithContext(
@ -75,38 +70,32 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
bytes.NewReader(body),
)
if err != nil {
return nil, errors.Wrap(err, "create new request")
return nil, fmt.Errorf("pushplus api error: failed to create new request: %w", err)
}
req.Header.Set("Content-Type", "application/json; charset=utf-8")
// Send request to pushplus service
resp, err := n.httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "send request to pushplus server")
return nil, fmt.Errorf("pushplus api error: failed to send request: %w", err)
}
defer resp.Body.Close()
result, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "read response")
return nil, fmt.Errorf("pushplus api error: failed to read response: %w", err)
} else if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushplus api error: unexpected status code: %d, resp: %s", resp.StatusCode, string(result))
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("pushplus returned status code %d: %s", resp.StatusCode, string(result))
}
// 解析响应
var errorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
if err := json.Unmarshal(result, &errorResponse); err != nil {
return nil, errors.Wrap(err, "decode response")
}
if errorResponse.Code != 200 {
return nil, fmt.Errorf("pushplus returned error: %s", errorResponse.Msg)
return nil, fmt.Errorf("pushplus api error: failed to decode response: %w", err)
} else if errorResponse.Code != 200 {
return nil, fmt.Errorf("pushplus api error: unexpected response code: %d, msg: %s", errorResponse.Code, errorResponse.Msg)
}
return &notifier.NotifyResult{}, nil

View File

@ -29,6 +29,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}

View File

@ -10,8 +10,8 @@ import (
)
type NotifierConfig struct {
// Telegram API Token。
ApiToken string `json:"apiToken"`
// Telegram Bot API Token。
BotToken string `json:"botToken"`
// Telegram Chat ID。
ChatId int64 `json:"chatId"`
}
@ -30,6 +30,7 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
return &NotifierProvider{
config: config,
logger: slog.Default(),
}, nil
}
@ -43,7 +44,7 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv, err := telegram.New(n.config.ApiToken)
srv, err := telegram.New(n.config.BotToken)
if err != nil {
return nil, err
}

View File

@ -45,7 +45,7 @@ func TestNotify(t *testing.T) {
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
ApiToken: fApiToken,
BotToken: fApiToken,
ChatId: fChartId,
})
if err != nil {

View File

@ -3,24 +3,37 @@ package webhook
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
"time"
webhook "github.com/nikoksr/notify/service/http"
"github.com/go-resty/resty/v2"
"github.com/usual2970/certimate/internal/pkg/core/notifier"
)
type NotifierConfig struct {
// Webhook URL。
Url string `json:"url"`
WebhookUrl string `json:"webhookUrl"`
// Webhook 回调数据application/json 或 application/x-www-form-urlencoded 格式)。
WebhookData string `json:"webhookData,omitempty"`
// 请求谓词。
// 零值时默认为 "POST"。
Method string `json:"method,omitempty"`
// 请求标头。
Headers map[string]string `json:"headers,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type NotifierProvider struct {
config *NotifierConfig
logger *slog.Logger
config *NotifierConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ notifier.Notifier = (*NotifierProvider)(nil)
@ -30,8 +43,18 @@ func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) {
panic("config is nil")
}
client := resty.New().
SetTimeout(30 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(5 * time.Second)
if config.AllowInsecureConnections {
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
return &NotifierProvider{
config: config,
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
@ -45,20 +68,120 @@ func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier {
}
func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) {
srv := webhook.New()
srv.AddReceiversURLs(n.config.Url)
if n.config.AllowInsecureConnections {
tlsConfig := &tls.Config{InsecureSkipVerify: true}
transport := &http.Transport{TLSClientConfig: tlsConfig}
client := &http.Client{Transport: transport}
srv.WithClient(client)
}
err = srv.Send(ctx, subject, message)
// 处理 Webhook URL
webhookUrl, err := url.Parse(n.config.WebhookUrl)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to parse webhook url: %w", err)
} else if webhookUrl.Scheme != "http" && webhookUrl.Scheme != "https" {
return nil, fmt.Errorf("unsupported webhook url scheme '%s'", webhookUrl.Scheme)
}
// 处理 Webhook 请求谓词
webhookMethod := strings.ToUpper(n.config.Method)
if webhookMethod == "" {
webhookMethod = http.MethodPost
} else if webhookMethod != http.MethodGet &&
webhookMethod != http.MethodPost &&
webhookMethod != http.MethodPut &&
webhookMethod != http.MethodPatch &&
webhookMethod != http.MethodDelete {
return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod)
}
// 处理 Webhook 请求标头
webhookHeaders := make(http.Header)
for k, v := range n.config.Headers {
webhookHeaders.Set(k, v)
}
// 处理 Webhook 请求内容类型
const CONTENT_TYPE_JSON = "application/json"
const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
const CONTENT_TYPE_MULTIPART = "multipart/form-data"
webhookContentType := webhookHeaders.Get("Content-Type")
if webhookContentType == "" {
webhookContentType = CONTENT_TYPE_JSON
webhookHeaders.Set("Content-Type", CONTENT_TYPE_JSON)
} else if strings.HasPrefix(webhookContentType, CONTENT_TYPE_JSON) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_FORM) &&
strings.HasPrefix(webhookContentType, CONTENT_TYPE_MULTIPART) {
return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType)
}
// 处理 Webhook 请求数据
var webhookData interface{}
if n.config.WebhookData == "" {
webhookData = map[string]string{
"subject": subject,
"message": message,
}
} else {
err = json.Unmarshal([]byte(n.config.WebhookData), &webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
}
replaceJsonValueRecursively(webhookData, "${SUBJECT}", subject)
replaceJsonValueRecursively(webhookData, "${MESSAGE}", message)
if webhookMethod == http.MethodGet || webhookContentType == CONTENT_TYPE_FORM || webhookContentType == CONTENT_TYPE_MULTIPART {
temp := make(map[string]string)
jsonb, err := json.Marshal(webhookData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else if err := json.Unmarshal(jsonb, &temp); err != nil {
return nil, fmt.Errorf("failed to unmarshal webhook data: %w", err)
} else {
webhookData = temp
}
}
}
// 生成请求
// 其中 GET 请求需转换为查询参数
req := n.httpClient.R().
SetContext(ctx).
SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
req.SetQueryParams(webhookData.(map[string]string))
} else {
switch webhookContentType {
case CONTENT_TYPE_JSON:
req.SetBody(webhookData)
case CONTENT_TYPE_FORM:
req.SetFormData(webhookData.(map[string]string))
case CONTENT_TYPE_MULTIPART:
req.SetMultipartFormData(webhookData.(map[string]string))
}
}
// 发送请求
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("failed to send webhook request: %w", err)
} else if resp.IsError() {
return nil, fmt.Errorf("unexpected webhook response status code: %d", resp.StatusCode())
}
n.logger.Debug("webhook responded", slog.Any("response", resp.String()))
return &notifier.NotifyResult{}, nil
}
func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {
switch v := data.(type) {
case map[string]any:
for k, val := range v {
v[k] = replaceJsonValueRecursively(val, oldStr, newStr)
}
case []any:
for i, val := range v {
v[i] = replaceJsonValueRecursively(val, oldStr, newStr)
}
case string:
return strings.ReplaceAll(v, oldStr, newStr)
}
return data
}

View File

@ -15,19 +15,24 @@ const (
mockMessage = "test_message"
)
var fUrl string
var (
fWebhookUrl string
fWebhookContentType string
)
func init() {
argsPrefix := "CERTIMATE_NOTIFIER_WEBHOOK_"
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "")
flag.StringVar(&fWebhookContentType, argsPrefix+"CONTENTTYPE", "application/json", "")
}
/*
Shell command to run this test:
go test -v ./webhook_test.go -args \
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url"
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url" \
--CERTIMATE_NOTIFIER_WEBHOOK_CONTENTTYPE="application/json"
*/
func TestNotify(t *testing.T) {
flag.Parse()
@ -35,11 +40,15 @@ func TestNotify(t *testing.T) {
t.Run("Notify", func(t *testing.T) {
t.Log(strings.Join([]string{
"args:",
fmt.Sprintf("URL: %v", fUrl),
fmt.Sprintf("URL: %v", fWebhookUrl),
}, "\n"))
notifier, err := provider.NewNotifier(&provider.NotifierConfig{
Url: fUrl,
WebhookUrl: fWebhookUrl,
Method: "POST",
Headers: map[string]string{
"Content-Type": fWebhookContentType,
},
AllowInsecureConnections: true,
})
if err != nil {

View File

@ -93,6 +93,12 @@ func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string,
searchWebsiteSSLPageNumber := int32(1)
searchWebsiteSSLPageSize := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
searchWebsiteSSLReq := &opsdk.SearchWebsiteSSLRequest{
Page: searchWebsiteSSLPageNumber,
PageSize: searchWebsiteSSLPageSize,

View File

@ -71,6 +71,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
listUserCertificateOrderPage := int64(1)
listUserCertificateOrderLimit := int64(50)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listUserCertificateOrderReq := &alicas.ListUserCertificateOrderRequest{
CurrentPage: tea.Int64(listUserCertificateOrderPage),
ShowSize: tea.Int64(listUserCertificateOrderLimit),

View File

@ -74,6 +74,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
var listCertificatesNextToken *string = nil
listCertificatesMaxItems := int32(1000)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertificatesReq := &awsacm.ListCertificatesInput{
NextToken: listCertificatesNextToken,
MaxItems: aws.Int32(listCertificatesMaxItems),

View File

@ -74,6 +74,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
Source: bytepluscdn.GetStrPtr("cert_center"),
}
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
u.logger.Debug("sdk request 'cdn.ListCertInfo'", slog.Any("request", listCertInfoReq), slog.Any("response", listCertInfoResp))
if err != nil {

View File

@ -76,6 +76,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
listCertificatesLimit := int32(2000)
var listCertificatesMarker *string = nil
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertificatesReq := &hcelbmodel.ListCertificatesRequest{
Limit: typeutil.ToPtr(listCertificatesLimit),
Marker: listCertificatesMarker,

View File

@ -72,6 +72,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
listCertificatesLimit := int32(50)
listCertificatesOffset := int32(0)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertificatesReq := &hcscmmodel.ListCertificatesRequest{
Limit: typeutil.ToPtr(listCertificatesLimit),
Offset: typeutil.ToPtr(listCertificatesOffset),

View File

@ -77,6 +77,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
listCertificatesPage := int32(1)
listCertificatesPageSize := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertificatesReq := &hcwafmodel.ListCertificatesRequest{
Page: typeutil.ToPtr(listCertificatesPage),
Pagesize: typeutil.ToPtr(listCertificatesPageSize),

View File

@ -77,6 +77,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
describeCertsPageNumber := 1
describeCertsPageSize := 10
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeCertsReq := jdsslapi.NewDescribeCertsRequest()
describeCertsReq.SetDomainName(certX509.Subject.CommonName)
describeCertsReq.SetPageNumber(describeCertsPageNumber)

View File

@ -93,6 +93,12 @@ func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string)
sslCenterListPage := int32(1)
sslCenterListPerPage := int32(100)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
sslCenterListReq := &rainyunsdk.SslCenterListRequest{
Filters: &rainyunsdk.SslCenterListFilters{
Domain: &certX509.Subject.CommonName,

View File

@ -124,6 +124,12 @@ func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string)
getCertificateListPage := int(1)
getCertificateListLimit := int(1000)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getCertificateListReq := u.sdkClient.NewGetCertificateListRequest()
getCertificateListReq.Mode = ucloud.String("trust")
getCertificateListReq.Domain = ucloud.String(certX509.Subject.CommonName)

View File

@ -75,6 +75,12 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
Source: "volc_cert_center",
}
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
u.logger.Debug("sdk request 'cdn.ListCertInfo'", slog.Any("request", listCertInfoReq), slog.Any("response", listCertInfoResp))
if err != nil {

View File

@ -0,0 +1,33 @@
package httputil
import (
"bufio"
"net/http"
"net/textproto"
"strings"
)
// 从表示 HTTP 标头的字符串解析并返回一个 http.Header 对象。
//
// 入参:
// - headers: 表示 HTTP 标头的字符串。
//
// 出参:
// - header: http.Header 对象。
// - err: 错误。
func ParseHeaders(headers string) (http.Header, error) {
str := strings.TrimSpace(headers) + "\r\n\r\n"
if len(str) == 4 {
return make(http.Header), nil
}
br := bufio.NewReader(strings.NewReader(str))
tp := textproto.NewReader(br)
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
return http.Header(mimeHeader), err
}

View File

@ -199,6 +199,28 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {
return defaultValue
}
// 以 `map[string]V` 形式从字典中获取指定键的值。
//
// 入参:
// - dict: 字典。
// - key: 键。
//
// 出参:
// - 字典中键对应的 `map[string]V` 对象。
func GetKVMap[V any](dict map[string]any, key string) map[string]V {
if dict == nil {
return make(map[string]V)
}
if val, ok := dict[key]; ok {
if result, ok := val.(map[string]V); ok {
return result
}
}
return make(map[string]V)
}
// 以 `map[string]any` 形式从字典中获取指定键的值。
//
// 入参:
@ -207,16 +229,6 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool {
//
// 出参:
// - 字典中键对应的 `map[string]any` 对象。
func GetAnyMap(dict map[string]any, key string) map[string]any {
if dict == nil {
return make(map[string]any)
}
if val, ok := dict[key]; ok {
if result, ok := val.(map[string]any); ok {
return result
}
}
return make(map[string]any)
func GetKVMapAny(dict map[string]any, key string) map[string]any {
return GetKVMap[any](dict, key)
}

View File

@ -53,6 +53,7 @@ func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Acces
Name: record.GetString("name"),
Provider: record.GetString("provider"),
Config: config,
Reserve: record.GetString("reserve"),
}
return access, nil
}

View File

@ -47,8 +47,10 @@ func (w *workflowInvoker) GetLogs() domain.WorkflowLogs {
func (w *workflowInvoker) processNode(ctx context.Context, node *domain.WorkflowNode) error {
current := node
for current != nil {
if ctx.Err() != nil {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if current.Type == domain.WorkflowNodeTypeBranch || current.Type == domain.WorkflowNodeTypeExecuteResultBranch {

View File

@ -41,22 +41,25 @@ func (n *applyNode) Process(ctx context.Context) error {
}
// 检测是否可以跳过本次执行
if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable {
n.logger.Info(fmt.Sprintf("skip this application, because %s", skipReason))
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
n.logger.Info(fmt.Sprintf("skip this application, because %s", reason))
return nil
} else if skipReason != "" {
n.logger.Info(fmt.Sprintf("re-apply, because %s", skipReason))
} else if reason != "" {
n.logger.Info(fmt.Sprintf("re-apply, because %s", reason))
}
// 初始化申请器
applicant, err := applicant.NewWithApplyNode(n.node)
applicant, err := applicant.NewWithWorkflowNode(applicant.ApplicantWithWorkflowNodeConfig{
Node: n.node,
Logger: n.logger,
})
if err != nil {
n.logger.Warn("failed to create applicant provider")
return err
}
// 申请证书
applyResult, err := applicant.Apply()
applyResult, err := applicant.Apply(ctx)
if err != nil {
n.logger.Warn("failed to apply")
return err

View File

@ -54,26 +54,27 @@ func (n *deployNode) Process(ctx context.Context) error {
// 检测是否可以跳过本次执行
if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) {
if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable {
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", skipReason))
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
n.logger.Info(fmt.Sprintf("skip this deployment, because %s", reason))
return nil
} else if skipReason != "" {
n.logger.Info(fmt.Sprintf("re-deploy, because %s", skipReason))
} else if reason != "" {
n.logger.Info(fmt.Sprintf("re-deploy, because %s", reason))
}
}
// 初始化部署器
deployer, err := deployer.NewWithDeployNode(n.node, struct {
Certificate string
PrivateKey string
}{Certificate: certificate.Certificate, PrivateKey: certificate.PrivateKey})
deployer, err := deployer.NewWithWorkflowNode(deployer.DeployerWithWorkflowNodeConfig{
Node: n.node,
Logger: n.logger,
CertificatePEM: certificate.Certificate,
PrivateKeyPEM: certificate.PrivateKey,
})
if err != nil {
n.logger.Warn("failed to create deployer provider")
return err
}
// 部署证书
deployer.SetLogger(n.logger)
if err := deployer.Deploy(ctx); err != nil {
n.logger.Warn("failed to deploy")
return err

View File

@ -30,25 +30,50 @@ func (n *notifyNode) Process(ctx context.Context) error {
nodeConfig := n.node.GetConfigForNotify()
// 获取通知配置
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
if nodeConfig.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")
// 获取通知配置
settings, err := n.settingsRepo.GetByName(ctx, "notifyChannels")
if err != nil {
return err
}
// 获取通知渠道
channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.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))
return err
}
n.logger.Info("notify completed")
return nil
}
// 初始化通知器
deployer, err := notify.NewWithWorkflowNode(notify.NotifierWithWorkflowNodeConfig{
Node: n.node,
Logger: n.logger,
Subject: nodeConfig.Subject,
Message: nodeConfig.Message,
})
if err != nil {
n.logger.Warn("failed to create notifier provider")
return err
}
// 获取通知渠道
channelConfig, err := settings.GetNotifyChannelConfig(nodeConfig.Channel)
if err != nil {
// 推送通知
if err := deployer.Notify(ctx); err != nil {
n.logger.Warn("failed to notify")
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))
return err
}
n.logger.Info("notify completed")
return nil
}

View File

@ -39,11 +39,11 @@ func (n *uploadNode) Process(ctx context.Context) error {
}
// 检测是否可以跳过本次执行
if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable {
n.logger.Info(fmt.Sprintf("skip this upload, because %s", skipReason))
if skippable, reason := n.checkCanSkip(ctx, lastOutput); skippable {
n.logger.Info(fmt.Sprintf("skip this upload, because %s", reason))
return nil
} else if skipReason != "" {
n.logger.Info(fmt.Sprintf("re-upload, because %s", skipReason))
} else if reason != "" {
n.logger.Info(fmt.Sprintf("re-upload, because %s", reason))
}
// 生成证书实体

View File

@ -0,0 +1,88 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
// update collection `access`
{
collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e")
if err != nil {
return err
}
if err := collection.Fields.AddMarshaledJSONAt(4, []byte(`{
"autogeneratePattern": "",
"hidden": false,
"id": "text2859962647",
"max": 0,
"min": 0,
"name": "reserve",
"pattern": "",
"presentable": false,
"primaryKey": false,
"required": false,
"system": false,
"type": "text"
}`)); err != nil {
return err
}
if err := app.Save(collection); err != nil {
return err
}
}
// migrate data
{
accesses, err := app.FindAllRecords("access")
if err != nil {
return err
}
for _, access := range accesses {
changed := false
if access.GetString("provider") == "buypass" {
access.Set("reserve", "ca")
changed = true
} else if access.GetString("provider") == "googletrustservices" {
access.Set("reserve", "ca")
changed = true
} else if access.GetString("provider") == "sslcom" {
access.Set("reserve", "ca")
changed = true
} else if access.GetString("provider") == "zerossl" {
access.Set("reserve", "ca")
changed = true
}
if access.GetString("provider") == "webhook" {
config := make(map[string]any)
if err := access.UnmarshalJSONField("config", &config); err != nil {
return err
}
config["method"] = "POST"
config["headers"] = "Content-Type: application/json"
access.Set("config", config)
changed = true
}
if changed {
err = app.Save(access)
if err != nil {
return err
}
}
}
}
return nil
}, func(app core.App) error {
return nil
})
}

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512.003 79C272.855 79 79 272.855 79 512.003 79 751.145 272.855 945 512.003 945 751.145 945 945 751.145 945 512.003 945 272.855 751.145 79 512.003 79z m200.075 375.014c-0.867 3.764-3.117 9.347-6.234 16.012h0.087l-0.347 0.648c-18.183 38.86-65.631 115.108-65.631 115.108l-0.215-0.52-13.856 24.147h66.8L565.063 779l29.002-115.368h-52.598l18.27-76.29c-14.76 3.55-32.253 8.436-52.945 15.1 0 0-27.967 16.36-80.607-31.5 0 0-35.501-31.29-14.891-39.078 8.744-3.33 42.466-7.573 69.004-11.122 35.93-4.845 57.965-7.441 57.965-7.441s-110.607 1.643-136.841-2.468c-26.237-4.11-59.525-47.905-66.626-86.377 0 0-10.953-21.117 23.595-11.122 34.547 10 177.535 38.95 177.535 38.95s-185.933-56.992-198.36-70.929c-12.381-13.846-36.406-75.902-33.289-113.981 0 0 1.343-9.521 11.127-6.926 0 0 137.49 62.75 231.475 97.152 94.028 34.403 175.76 51.885 165.2 96.414z" fill="#3AA2EB"></path></svg>

After

Width:  |  Height:  |  Size: 1022 B

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M832 128H192a128 128 0 0 0-128 128v512a128 128 0 0 0 128 128h384v-64H192a64 64 0 0 1-64-64V310.4l384 206.08 384-206.72V768h64V256a128 128 0 0 0-128-128zM512 443.52L131.2 240A64 64 0 0 1 192 192h640a64 64 0 0 1 60.8 46.72z" fill="#616971"></path><path d="M640 896h64v-64h-64z m256-64v64h64v-64z m-128 64h64v-64h-64z" fill="#FF8910"></path></svg>

After

Width:  |  Height:  |  Size: 500 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M515.465911 0.01242C300.718451-1.427543 99.826634 133.353979 27.187509 347.877444c-90.654661 267.771091 52.898635 558.325595 320.665726 648.947257 267.771091 90.654661 558.325595-52.898635 648.947257-320.665726 73.698099-217.594386-7.327811-450.199385-183.932255-578.421077l5.375862 108.670197c88.097727 97.374488 122.87783 235.163933 79.006961 364.760588C831.811749 764.541694 615.912319 866.204071 415.1165 798.204826c-200.860818-67.966246-310.617986-279.83378-245.115676-473.23879 43.998865-129.949647 155.932977-218.494363 285.592631-241.915759L525.641648 0.268413A211.822535 211.822535 0 0 0 516.199892 0.044419h-0.79998zM665.996027 46.700215h-0.351991a12.193685 12.193685 0 0 0-4.959872 1.055973l0.063998-0.031999-0.159996 0.031999a14.076637 14.076637 0 0 0-4.63988 3.039921c-6.175841 6.047844-28.031277 35.327089-28.031277 35.327089L580.298238 145.097676l-55.454569 67.614256-95.231543 118.429944s-43.678873 54.526593-34.047122 121.629862c9.631751 67.166267 59.550464 99.838424 98.297464 112.990085 38.687002 13.087662 98.206466 17.407551 146.685215-30.047225C688.967434 488.258823 687.395475 418.372626 687.395475 418.372626l-3.711905-151.901081-2.975923-87.454744-2.011948-75.742046s0.415989-36.511058-0.863978-45.086836a13.914641 13.914641 0 0 0-1.53596-4.639881l0.031999 0.063999-0.255993-0.511987-0.287993-0.479988a11.878694 11.878694 0 0 0-9.759748-5.983845H665.996027z" fill="#0072C6"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M679.424 746.862l84.005-395.996c7.424-34.852-12.581-48.567-35.438-40.009L234.277 501.138c-33.72 13.13-33.134 32-5.706 40.558l126.282 39.424 293.156-184.576c13.714-9.143 26.295-3.986 16.018 5.157L426.898 615.973l-9.143 130.304c13.13 0 18.871-5.706 25.71-12.581l61.696-59.429 128 94.282c23.442 13.129 40.01 6.29 46.3-21.724zM1024 512c0 282.843-229.157 512-512 512S0 794.843 0 512 229.157 0 512 0s512 229.157 512 512z" fill="#1296DB"></path></svg>

View File

@ -0,0 +1 @@
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M803.6 859.2c0 26.6-20.4 49.2-48.4 51.8-21.2 2-48.6-11-54.8-39.8-5.6-26-13.2-50.8-29-72.4-6-8.2-12.8-16-20-23.4-7.6-8-9.4-14-4.4-19.8 5-5.8 12.8-5 20.8 3.2 20.8 21 45.4 35.2 73.6 43.6 7.2 2.2 14.8 3.4 22.2 5.2 24.6 6.2 40 26.2 40 51.6z" fill="#FC6401"></path><path d="M698.2 549.8c0.2-28.4 20.8-50.2 49.6-52.6 25.6-2.2 50.6 17.6 55 45.2 6 36.2 22.8 66.2 48.4 92 3.2 3.2 5.6 9.2 5.2 13.8-0.4 7.2-9.8 10.6-16.2 6.4-3.4-2.2-6.2-5-9-7.8-25.4-24.6-55.6-39.4-90.4-45.4-24.8-4.6-42.6-26.2-42.6-51.6z" fill="#2DBD00"></path><path d="M595.4 765.2c-26.6 0-49.2-20.4-51.8-48.4-2-21.2 11-48.6 39.8-54.8 26-5.6 50.8-13.2 72.4-29 8.2-6 16-12.8 23.4-20 8-7.6 14-9.4 19.8-4.4 5.8 5 5 12.8-3.2 20.8-21 20.8-35.2 45.4-43.6 73.6-2.2 7.2-3.4 14.8-5.2 22.2-6.2 24.6-26.2 40-51.6 40z" fill="#FFCD00"></path><path d="M898.8 650c28.4 0.2 50.2 20.8 52.6 49.6 2.2 25.6-17.6 50.6-45.2 55-36.2 6-66.2 22.8-92 48.4-3.2 3.2-9.2 5.6-13.8 5.2-7.2-0.4-10.6-9.8-6.4-16.2 2.2-3.4 5-6.2 7.8-9 24.6-25.4 39.4-55.6 45.4-90.4 4.6-25 26.2-42.8 51.6-42.6z" fill="#0084F0"></path><path d="M734 208.6c-110.4-108.4-244.8-139.8-392.4-100C81.6 178.8 1.2 449.2 132.6 625.6c6.4 8.6 7.6 24.4 5.2 35.4-7 32.4-17.4 64.2-26 96.2-4.6 17.2-7.4 34.6 8 48.4 16.6 14.8 34.2 11.8 52.2 2.6 29.6-15 59.8-29.2 89-45 19-10.4 36.2-10.8 57.6-4.8 42.8 11.8 87.2 18.4 109.6 23 43.8-0.8 83.6-5.2 120.2-13.6-13.8-12-23-29.2-24.8-49-0.4-5.4-0.2-10.8 0.6-16.2-57.8 12.4-119.2 7.4-183.2-14-42.2-14.2-76.8-17.8-113.4 7-3.4 2.2-7.8 2.8-24.6 8.2 33.8-58.4 8.8-95-19.6-136.6-63.4-92-50.4-210.8 24.6-296.4 122.8-139.8 363-139.8 485.8 0.2 52.8 60.2 73.6 135.2 61.8 206.2 28 1.6 52.8 20.6 63 47.2 32-108.8 4-228.8-84.6-315.8z" fill="#0083EF"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -14,14 +14,14 @@ export type AccessEditDrawerProps = {
data?: AccessFormProps["initialValues"];
loading?: boolean;
open?: boolean;
range?: AccessFormProps["range"];
scene: AccessFormProps["scene"];
trigger?: React.ReactNode;
usage?: AccessFormProps["usage"];
onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void;
};
const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditDrawerProps) => {
const AccessEditDrawer = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: AccessEditDrawerProps) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
@ -109,7 +109,7 @@ const AccessEditDrawer = ({ data, loading, trigger, scene, range, afterSubmit, .
width={720}
onClose={() => setOpen(false)}
>
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
<AccessForm ref={formRef} initialValues={data} scene={scene === "add" ? "add" : "edit"} usage={usage} />
</Drawer>
</>
);

View File

@ -14,14 +14,14 @@ export type AccessEditModalProps = {
data?: AccessFormProps["initialValues"];
loading?: boolean;
open?: boolean;
range?: AccessFormProps["range"];
usage?: AccessFormProps["usage"];
scene: AccessFormProps["scene"];
trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void;
afterSubmit?: (record: AccessModel) => void;
};
const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ...props }: AccessEditModalProps) => {
const AccessEditModal = ({ data, loading, trigger, scene, usage, afterSubmit, ...props }: AccessEditModalProps) => {
const { t } = useTranslation();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
@ -105,7 +105,7 @@ const AccessEditModal = ({ data, loading, trigger, scene, range, afterSubmit, ..
onCancel={handleCancelClick}
>
<div className="pb-2 pt-4">
<AccessForm ref={formRef} initialValues={data} range={range} scene={scene === "add" ? "add" : "edit"} />
<AccessForm ref={formRef} initialValues={data} scene={scene === "add" ? "add" : "edit"} usage={usage} />
</div>
</Modal>
</>

View File

@ -25,10 +25,12 @@ import AccessFormCloudflareConfig from "./AccessFormCloudflareConfig";
import AccessFormClouDNSConfig from "./AccessFormClouDNSConfig";
import AccessFormCMCCCloudConfig from "./AccessFormCMCCCloudConfig";
import AccessFormDeSECConfig from "./AccessFormDeSECConfig";
import AccessFormDingTalkBotConfig from "./AccessFormDingTalkBotConfig";
import AccessFormDNSLAConfig from "./AccessFormDNSLAConfig";
import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig";
import AccessFormDynv6Config from "./AccessFormDynv6Config";
import AccessFormEdgioConfig from "./AccessFormEdgioConfig";
import AccessFormEmailConfig from "./AccessFormEmailConfig";
import AccessFormGcoreConfig from "./AccessFormGcoreConfig";
import AccessFormGnameConfig from "./AccessFormGnameConfig";
import AccessFormGoDaddyConfig from "./AccessFormGoDaddyConfig";
@ -36,6 +38,8 @@ import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServices
import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig";
import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig";
import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig";
import AccessFormLarkBotConfig from "./AccessFormLarkBotConfig";
import AccessFormMattermostConfig from "./AccessFormMattermostConfig";
import AccessFormNamecheapConfig from "./AccessFormNamecheapConfig";
import AccessFormNameDotComConfig from "./AccessFormNameDotComConfig";
import AccessFormNameSiloConfig from "./AccessFormNameSiloConfig";
@ -47,6 +51,7 @@ import AccessFormRainYunConfig from "./AccessFormRainYunConfig";
import AccessFormSafeLineConfig from "./AccessFormSafeLineConfig";
import AccessFormSSHConfig from "./AccessFormSSHConfig";
import AccessFormSSLComConfig from "./AccessFormSSLComConfig";
import AccessFormTelegramConfig from "./AccessFormTelegramConfig";
import AccessFormTencentCloudConfig from "./AccessFormTencentCloudConfig";
import AccessFormUCloudConfig from "./AccessFormUCloudConfig";
import AccessFormUpyunConfig from "./AccessFormUpyunConfig";
@ -54,20 +59,21 @@ import AccessFormVercelConfig from "./AccessFormVercelConfig";
import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig";
import AccessFormWangsuConfig from "./AccessFormWangsuConfig";
import AccessFormWebhookConfig from "./AccessFormWebhookConfig";
import AccessFormWeComBotConfig from "./AccessFormWeComBotConfig";
import AccessFormWestcnConfig from "./AccessFormWestcnConfig";
import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig";
type AccessFormFieldValues = Partial<MaybeModelRecord<AccessModel>>;
type AccessFormRanges = "both-dns-hosting" | "ca-only" | "notify-only";
type AccessFormScenes = "add" | "edit";
type AccessFormUsages = "both-dns-hosting" | "ca-only" | "notification-only";
export type AccessFormProps = {
className?: string;
style?: React.CSSProperties;
disabled?: boolean;
initialValues?: AccessFormFieldValues;
range?: AccessFormRanges;
scene: AccessFormScenes;
usage?: AccessFormUsages;
onValuesChange?: (values: AccessFormFieldValues) => void;
};
@ -77,7 +83,7 @@ export type AccessFormInstance = {
validateFields: FormInstance<AccessFormFieldValues>["validateFields"];
};
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, range, scene, onValuesChange }, ref) => {
const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className, style, disabled, initialValues, usage, scene, onValuesChange }, ref) => {
const { t } = useTranslation();
const formSchema = z.object({
@ -88,13 +94,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
.trim(),
provider: z.nativeEnum(ACCESS_PROVIDERS, {
message:
range === "ca-only"
usage === "ca-only"
? t("access.form.certificate_authority.placeholder")
: range === "notify-only"
: usage === "notification-only"
? t("access.form.notification_channel.placeholder")
: t("access.form.provider.placeholder"),
}),
config: z.any(),
reserve: z.string().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const { form: formInst, formProps } = useAntdForm({
@ -102,33 +109,33 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
});
const providerLabel = useMemo(() => {
switch (range) {
switch (usage) {
case "ca-only":
return t("access.form.certificate_authority.label");
case "notify-only":
case "notification-only":
return t("access.form.notification_channel.label");
}
return t("access.form.provider.label");
}, [range]);
}, [usage]);
const providerPlaceholder = useMemo(() => {
switch (range) {
switch (usage) {
case "ca-only":
return t("access.form.certificate_authority.placeholder");
case "notify-only":
case "notification-only":
return t("access.form.notification_channel.placeholder");
}
return t("access.form.provider.placeholder");
}, [range]);
}, [usage]);
const providerTooltip = useMemo(() => {
switch (range) {
switch (usage) {
case "both-dns-hosting":
return <span dangerouslySetInnerHTML={{ __html: t("access.form.provider.tooltip") }}></span>;
}
return undefined;
}, [range]);
}, [usage]);
const fieldProvider = Form.useWatch("provider", formInst);
@ -179,6 +186,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormCMCCCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.DESEC:
return <AccessFormDeSECConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.DINGTALKBOT:
return <AccessFormDingTalkBotConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.DNSLA:
return <AccessFormDNSLAConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.DOGECLOUD:
@ -195,12 +204,18 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormGoogleTrustServicesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.EDGIO:
return <AccessFormEdgioConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.EMAIL:
return <AccessFormEmailConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.HUAWEICLOUD:
return <AccessFormHuaweiCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.JDCLOUD:
return <AccessFormJDCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.KUBERNETES:
return <AccessFormKubernetesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.LARKBOT:
return <AccessFormLarkBotConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.MATTERMOST:
return <AccessFormMattermostConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMECHEAP:
return <AccessFormNamecheapConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.NAMEDOTCOM:
@ -221,6 +236,8 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormSafeLineConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.SSH:
return <AccessFormSSHConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.TELEGRAM:
return <AccessFormTelegramConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.SSLCOM:
return <AccessFormSSLComConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.TENCENTCLOUD:
@ -236,7 +253,14 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
case ACCESS_PROVIDERS.WANGSU:
return <AccessFormWangsuConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WEBHOOK:
return <AccessFormWebhookConfig {...nestedFormProps} />;
return (
<AccessFormWebhookConfig
usage={usage === "notification-only" ? "notification" : usage === "both-dns-hosting" ? "deployment" : "none"}
{...nestedFormProps}
/>
);
case ACCESS_PROVIDERS.WECOMBOT:
return <AccessFormWeComBotConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.WESTCN:
return <AccessFormWestcnConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.ZEROSSL:
@ -260,6 +284,7 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
getFieldsValue: () => {
const values = formInst.getFieldsValue(true);
values.config = nestedFormInst.getFieldsValue();
values.reserve = usage === "ca-only" ? "ca" : usage === "notification-only" ? "notification" : undefined;
return values;
},
resetFields: (fields) => {
@ -288,20 +313,20 @@ const AccessForm = forwardRef<AccessFormInstance, AccessFormProps>(({ className,
<Form.Item name="provider" label={providerLabel} rules={[formRule]} tooltip={providerTooltip}>
<AccessProviderSelect
filter={(record) => {
if (range == null) return true;
if (usage == null) return true;
switch (range) {
switch (usage) {
case "both-dns-hosting":
return record.usages.includes(ACCESS_USAGES.DNS) || record.usages.includes(ACCESS_USAGES.HOSTING);
case "ca-only":
return record.usages.includes(ACCESS_USAGES.CA);
case "notify-only":
case "notification-only":
return record.usages.includes(ACCESS_USAGES.NOTIFICATION);
}
}}
disabled={scene !== "add"}
placeholder={providerPlaceholder}
showOptionTags={range == null || (range === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
showOptionTags={usage == null || (usage === "both-dns-hosting" ? { [ACCESS_USAGES.DNS]: true, [ACCESS_USAGES.HOSTING]: true } : false)}
showSearch={!disabled}
/>
</Form.Item>

View File

@ -0,0 +1,68 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForDingTalkBot } from "@/domain/access";
type AccessFormDingTalkBotConfigFieldValues = Nullish<AccessConfigForDingTalkBot>;
export type AccessFormDingTalkBotConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormDingTalkBotConfigFieldValues;
onValuesChange?: (values: AccessFormDingTalkBotConfigFieldValues) => void;
};
const initFormModel = (): AccessFormDingTalkBotConfigFieldValues => {
return {
webhookUrl: "",
secret: "",
};
};
const AccessFormDingTalkBotConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormDingTalkBotConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
webhookUrl: z.string().url(t("common.errmsg.url_invalid")),
secret: z.string().nonempty(t("access.form.dingtalkbot_secret.placeholder")).trim(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="webhookUrl"
label={t("access.form.dingtalkbot_webhook_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.dingtalkbot_webhook_url.tooltip") }}></span>}
>
<Input placeholder={t("access.form.dingtalkbot_webhook_url.placeholder")} />
</Form.Item>
<Form.Item
name="secret"
label={t("access.form.dingtalkbot_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.dingtalkbot_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.dingtalkbot_secret.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormDingTalkBotConfig;

View File

@ -0,0 +1,125 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, InputNumber, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForEmail } from "@/domain/access";
import { validEmailAddress, validPortNumber } from "@/utils/validators";
type AccessFormEmailConfigFieldValues = Nullish<AccessConfigForEmail>;
export type AccessFormEmailConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormEmailConfigFieldValues;
onValuesChange?: (values: AccessFormEmailConfigFieldValues) => void;
};
const initFormModel = (): AccessFormEmailConfigFieldValues => {
return {
smtpHost: "",
smtpPort: 465,
smtpTls: true,
username: "",
password: "",
};
};
const AccessFormEmailConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormEmailConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
smtpHost: z
.string()
.min(1, t("access.form.email_smtp_host.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
smtpPort: z.preprocess(
(v) => Number(v),
z.number().refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),
smtpTls: z.boolean().nullish(),
username: z
.string()
.min(1, t("access.form.email_username.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
password: z
.string()
.min(1, t("access.form.email_password.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
defaultSenderAddress: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return validEmailAddress(v);
}, t("common.errmsg.email_invalid")),
defaultReceiverAddress: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return validEmailAddress(v);
}, t("common.errmsg.email_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleTlsSwitchChange = (checked: boolean) => {
const oldPort = formInst.getFieldValue("smtpPort");
const newPort = checked && (oldPort == null || oldPort === 25) ? 465 : !checked && (oldPort == null || oldPort === 465) ? 25 : oldPort;
if (newPort !== oldPort) {
formInst.setFieldValue("smtpPort", newPort);
}
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<div className="flex space-x-2">
<div className="w-3/5">
<Form.Item name="smtpHost" label={t("access.form.email_smtp_host.label")} rules={[formRule]}>
<Input placeholder={t("access.form.email_smtp_host.placeholder")} />
</Form.Item>
</div>
<div className="w-2/5">
<Form.Item name="smtpPort" label={t("access.form.email_smtp_port.label")} rules={[formRule]}>
<InputNumber className="w-full" placeholder={t("access.form.email_smtp_port.placeholder")} min={1} max={65535} />
</Form.Item>
</div>
</div>
<Form.Item name="smtpTls" label={t("access.form.email_smtp_tls.label")} rules={[formRule]}>
<Switch onChange={handleTlsSwitchChange} />
</Form.Item>
<Form.Item name="username" label={t("access.form.email_username.label")} rules={[formRule]}>
<Input autoComplete="new-password" placeholder={t("access.form.email_username.placeholder")} />
</Form.Item>
<Form.Item name="password" label={t("access.form.email_password.label")} rules={[formRule]}>
<Input.Password autoComplete="new-password" placeholder={t("access.form.email_password.placeholder")} />
</Form.Item>
<Form.Item name="defaultSenderAddress" label={t("access.form.email_default_sender_address.label")} rules={[formRule]}>
<Input type="email" allowClear placeholder={t("access.form.email_default_sender_address.placeholder")} />
</Form.Item>
<Form.Item name="defaultReceiverAddress" label={t("access.form.email_default_receiver_address.label")} rules={[formRule]}>
<Input type="email" allowClear placeholder={t("access.form.email_default_receiver_address.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormEmailConfig;

View File

@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForLarkBot } from "@/domain/access";
type AccessFormLarkBotConfigFieldValues = Nullish<AccessConfigForLarkBot>;
export type AccessFormLarkBotConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormLarkBotConfigFieldValues;
onValuesChange?: (values: AccessFormLarkBotConfigFieldValues) => void;
};
const initFormModel = (): AccessFormLarkBotConfigFieldValues => {
return {
webhookUrl: "",
};
};
const AccessFormLarkBotConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormLarkBotConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
webhookUrl: z.string().url(t("common.errmsg.url_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="webhookUrl"
label={t("access.form.larkbot_webhook_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.larkbot_webhook_url.tooltip") }}></span>}
>
<Input placeholder={t("access.form.larkbot_webhook_url.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormLarkBotConfig;

View File

@ -0,0 +1,79 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForMattermost } from "@/domain/access";
type AccessFormMattermostConfigFieldValues = Nullish<AccessConfigForMattermost>;
export type AccessFormMattermostConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormMattermostConfigFieldValues;
onValuesChange?: (values: AccessFormMattermostConfigFieldValues) => void;
};
const initFormModel = (): AccessFormMattermostConfigFieldValues => {
return {
serverUrl: "http://<your-host-addr>:8065/",
username: "",
password: "",
};
};
const AccessFormMattermostConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormMattermostConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
serverUrl: z.string().url(t("common.errmsg.url_invalid")),
username: z.string().nonempty(t("access.form.mattermost_username.placeholder")),
password: z.string().nonempty(t("access.form.mattermost_password.placeholder")),
defaultChannelId: z.string().nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="serverUrl"
label={t("access.form.mattermost_server_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.mattermost_server_url.tooltip") }}></span>}
>
<Input placeholder={t("access.form.mattermost_server_url.placeholder")} />
</Form.Item>
<Form.Item name="username" label={t("access.form.mattermost_username.label")} rules={[formRule]}>
<Input placeholder={t("access.form.mattermost_username.placeholder")} />
</Form.Item>
<Form.Item name="password" label={t("access.form.mattermost_password.label")} rules={[formRule]}>
<Input.Password placeholder={t("access.form.mattermost_password.placeholder")} />
</Form.Item>
<Form.Item
name="defaultChannelId"
label={t("access.form.mattermost_default_channel_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.mattermost_default_channel_id.tooltip") }}></span>}
>
<Input allowClear placeholder={t("access.form.mattermost_default_channel_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormMattermostConfig;

View File

@ -31,13 +31,11 @@ const AccessFormSSHConfig = ({ form: formInst, formName, disabled, initialValues
const { t } = useTranslation();
const formSchema = z.object({
host: z
.string({ message: t("access.form.ssh_host.placeholder") })
.refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
host: z.string().refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")),
port: z.preprocess(
(v) => Number(v),
z
.number({ message: t("access.form.ssh_port.placeholder") })
.number()
.int(t("access.form.ssh_port.placeholder"))
.refine((v) => validPortNumber(v), t("common.errmsg.port_invalid"))
),

View File

@ -0,0 +1,81 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForTelegram } from "@/domain/access";
type AccessFormTelegramConfigFieldValues = Nullish<AccessConfigForTelegram>;
export type AccessFormTelegramConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormTelegramConfigFieldValues;
onValuesChange?: (values: AccessFormTelegramConfigFieldValues) => void;
};
const initFormModel = (): AccessFormTelegramConfigFieldValues => {
return {
botToken: "",
};
};
const AccessFormTelegramConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormTelegramConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
botToken: z
.string({ message: t("access.form.telegram_bot_token.placeholder") })
.min(1, t("access.form.telegram_bot_token.placeholder"))
.max(256, t("common.errmsg.string_max", { max: 256 })),
defaultChatId: z
.preprocess(
(v) => (v == null || v === "" ? undefined : Number(v)),
z
.number()
.nullish()
.refine((v) => {
if (v == null || v + "" === "") return true;
return /^\d+$/.test(v + "") && +v! > 0;
}, t("access.form.telegram_default_chat_id.placeholder"))
)
.nullish(),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="botToken"
label={t("access.form.telegram_bot_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.telegram_bot_token.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.telegram_bot_token.placeholder")} />
</Form.Item>
<Form.Item
name="defaultChatId"
label={t("access.form.telegram_default_chat_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.telegram_default_chat_id.tooltip") }}></span>}
>
<Input type="number" allowClear placeholder={t("access.form.telegram_default_chat_id.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormTelegramConfig;

View File

@ -0,0 +1,57 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { type AccessConfigForWeComBot } from "@/domain/access";
type AccessFormWeComBotConfigFieldValues = Nullish<AccessConfigForWeComBot>;
export type AccessFormWeComBotConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormWeComBotConfigFieldValues;
onValuesChange?: (values: AccessFormWeComBotConfigFieldValues) => void;
};
const initFormModel = (): AccessFormWeComBotConfigFieldValues => {
return {
webhookUrl: "",
};
};
const AccessFormWeComBotConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormWeComBotConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
webhookUrl: z.string().url(t("common.errmsg.url_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item
name="webhookUrl"
label={t("access.form.wecombot_webhook_url.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.wecombot_webhook_url.tooltip") }}></span>}
>
<Input placeholder={t("access.form.wecombot_webhook_url.placeholder")} />
</Form.Item>
</Form>
);
};
export default AccessFormWeComBotConfig;

View File

@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { Form, type FormInstance, Input, Switch } from "antd";
import { DownOutlined as DownOutlinedIcon } from "@ant-design/icons";
import { Alert, Button, Dropdown, Form, type FormInstance, Input, Select, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { type AccessConfigForWebhook } from "@/domain/access";
type AccessFormWebhookConfigFieldValues = Nullish<AccessConfigForWebhook>;
@ -12,24 +14,241 @@ export type AccessFormWebhookConfigProps = {
formName: string;
disabled?: boolean;
initialValues?: AccessFormWebhookConfigFieldValues;
usage?: "deployment" | "notification" | "none";
onValuesChange?: (values: AccessFormWebhookConfigFieldValues) => void;
};
const initFormModel = (): AccessFormWebhookConfigFieldValues => {
return {
url: "",
method: "POST",
headers: "Content-Type: application/json",
allowInsecureConnections: false,
defaultDataForDeployment: JSON.stringify(
{
name: "${DOMAINS}",
cert: "${CERTIFICATE}",
privkey: "${PRIVATE_KEY}",
},
null,
2
),
defaultDataForNotification: JSON.stringify(
{
subject: "${SUBJECT}",
message: "${MESSAGE}",
},
null,
2
),
};
};
const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormWebhookConfigProps) => {
const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialValues, usage, onValuesChange }: AccessFormWebhookConfigProps) => {
const { t } = useTranslation();
const formSchema = z.object({
url: z.string({ message: t("access.form.webhook_url.placeholder") }).url(t("common.errmsg.url_invalid")),
url: z.string().url(t("common.errmsg.url_invalid")),
method: z.union([z.literal("GET"), z.literal("POST"), z.literal("PUT"), z.literal("PATCH"), z.literal("DELETE")], {
message: t("access.form.webhook_method.placeholder"),
}),
headers: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
const lines = v.split(/\r?\n/);
for (const line of lines) {
if (line.split(":").length < 2) {
return false;
}
}
return true;
}, t("access.form.webhook_headers.errmsg.invalid")),
allowInsecureConnections: z.boolean().nullish(),
defaultDataForDeployment: z
.string()
.nullish()
.refine((v) => {
if (usage && usage !== "deployment") return true;
if (!v) return true;
try {
const obj = JSON.parse(v);
return typeof obj === "object" && !Array.isArray(obj);
} catch {
return false;
}
}, t("access.form.webhook_default_data.errmsg.json_invalid")),
defaultDataForNotification: z
.string()
.nullish()
.refine((v) => {
if (usage && usage !== "notification") return true;
if (!v) return true;
try {
const obj = JSON.parse(v);
return typeof obj === "object" && !Array.isArray(obj);
} catch {
return false;
}
}, t("access.form.webhook_default_data.errmsg.json_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
const handleWebhookHeadersBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
let value = e.target.value;
value = value.trim();
value = value.replace(/(?<!\r)\n/g, "\r\n");
formInst.setFieldValue("headers", value);
};
const handleWebhookDataForDeploymentBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("defaultDataForDeployment", json);
} catch {
return;
}
};
const handleWebhookDataForNotificationBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue("defaultDataForNotification", json);
} catch {
return;
}
};
const handlePresetDataForDeploymentClick = () => {
formInst.setFieldValue("defaultDataForDeployment", initFormModel().defaultDataForDeployment);
};
const handlePresetDataForNotificationClick = (key: string) => {
switch (key) {
case "bark":
formInst.setFieldValue("url", "https://api.day.app/push");
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json\r\nAuthorization: Bearer <your-gotify-token>");
formInst.setFieldValue(
"defaultDataForNotification",
JSON.stringify(
{
title: "${SUBJECT}",
body: "${MESSAGE}",
group: "<your-bark-group>",
device_keys: "<your-bark-device-key>",
},
null,
2
)
);
break;
case "gotify":
formInst.setFieldValue("url", "https://<your-gotify-server>/");
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json\r\nAuthorization: Bearer <your-gotify-token>");
formInst.setFieldValue(
"defaultDataForNotification",
JSON.stringify(
{
title: "${SUBJECT}",
message: "${MESSAGE}",
priority: 1,
},
null,
2
)
);
break;
case "ntfy":
formInst.setFieldValue("url", "https://<your-ntfy-server>/");
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json");
formInst.setFieldValue(
"defaultDataForNotification",
JSON.stringify(
{
topic: "<your-ntfy-topic>",
title: "${SUBJECT}",
message: "${MESSAGE}",
priority: 1,
},
null,
2
)
);
break;
case "pushover":
formInst.setFieldValue("url", "https://api.pushover.net/1/messages.json");
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json");
formInst.setFieldValue(
"defaultDataForNotification",
JSON.stringify(
{
token: "<your-pushover-token>",
user: "<your-pushover-user>",
title: "${SUBJECT}",
message: "${MESSAGE}",
},
null,
2
)
);
break;
case "pushplus":
formInst.setFieldValue("url", "https://www.pushplus.plus/send");
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json");
formInst.setFieldValue(
"defaultDataForNotification",
JSON.stringify(
{
token: "<your-pushplus-token>",
title: "${SUBJECT}",
content: "${MESSAGE}",
},
null,
2
)
);
break;
case "serverchan":
formInst.setFieldValue("url", "https://sctapi.ftqq.com/<your-serverchan-key>.send");
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json");
formInst.setFieldValue(
"defaultDataForNotification",
JSON.stringify(
{
text: "${SUBJECT}",
desp: "${MESSAGE}",
},
null,
2
)
);
break;
default:
formInst.setFieldValue("method", "POST");
formInst.setFieldValue("headers", "Content-Type: application/json");
formInst.setFieldValue("defaultDataForNotification", initFormModel().defaultDataForNotification);
break;
}
};
const handleFormChange = (_: unknown, values: z.infer<typeof formSchema>) => {
onValuesChange?.(values);
};
@ -47,6 +266,92 @@ const AccessFormWebhookConfig = ({ form: formInst, formName, disabled, initialVa
<Input placeholder={t("access.form.webhook_url.placeholder")} />
</Form.Item>
<Form.Item name="method" label={t("access.form.webhook_method.label")} rules={[formRule]}>
<Select
options={["GET", "POST", "PUT", "PATCH", "DELETE"].map((s) => ({ label: s, value: s }))}
placeholder={t("access.form.webhook_method.placeholder")}
/>
</Form.Item>
<Form.Item
name="headers"
label={t("access.form.webhook_headers.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_headers.tooltip") }}></span>}
>
<Input.TextArea autoSize={{ minRows: 3, maxRows: 5 }} placeholder={t("access.form.webhook_headers.placeholder")} onBlur={handleWebhookHeadersBlur} />
</Form.Item>
<Show when={!usage || usage === "deployment"}>
<Form.Item className="mb-0">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate">
<span>{t("access.form.webhook_default_data_for_deployment.label")}</span>
</div>
<div className="text-right">
<Button size="small" type="link" onClick={handlePresetDataForDeploymentClick}>
{t("access.form.webhook_preset_data.button")}
</Button>
</div>
</div>
</label>
<Form.Item name="defaultDataForDeployment" rules={[formRule]}>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("access.form.webhook_default_data_for_deployment.placeholder")}
onBlur={handleWebhookDataForDeploymentBlur}
/>
</Form.Item>
</Form.Item>
<Form.Item>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_default_data_for_deployment.guide") }}></span>} />
</Form.Item>
</Show>
<Show when={!usage || usage === "notification"}>
<Form.Item className="mb-0">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate">
<span>{t("access.form.webhook_default_data_for_notification.label")}</span>
</div>
<div className="text-right">
<Dropdown
menu={{
items: ["bark", "ntfy", "gotify", "pushover", "pushplus", "serverchan", "common"].map((key) => ({
key,
label: t(`access.form.webhook_preset_data.option.${key}.label`),
onClick: () => handlePresetDataForNotificationClick(key),
})),
}}
trigger={["click"]}
>
<Button size="small" type="link">
{t("access.form.webhook_preset_data.button")}
<DownOutlinedIcon />
</Button>
</Dropdown>
</div>
</div>
</label>
<Form.Item name="defaultDataForNotification" rules={[formRule]}>
<Input.TextArea
allowClear
autoSize={{ minRows: 3, maxRows: 10 }}
placeholder={t("access.form.webhook_default_data_for_notification.placeholder")}
onBlur={handleWebhookDataForNotificationBlur}
/>
</Form.Item>
</Form.Item>
<Form.Item>
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("access.form.webhook_default_data_for_notification.guide") }}></span>} />
</Form.Item>
</Show>
<Form.Item
name="allowInsecureConnections"
label={t("access.form.webhook_allow_insecure_conns.label")}

View File

@ -34,6 +34,9 @@ export type NotifyChannelEditFormInstance = {
validateFields: FormInstance<NotifyChannelEditFormFieldValues>["validateFields"];
};
/**
* @deprecated
*/
const NotifyChannelEditForm = forwardRef<NotifyChannelEditFormInstance, NotifyChannelEditFormProps>(
({ className, style, channel, disabled, initialValues, onValuesChange }, ref) => {
const { form: formInst, formProps } = useAntdForm({

View File

@ -7,9 +7,7 @@ const NotifyChannelEditFormMattermostFields = () => {
const { t } = useTranslation();
const formSchema = z.object({
serverUrl: z
.string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") })
.url(t("common.errmsg.url_invalid")),
serverUrl: z.string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") }).url(t("common.errmsg.url_invalid")),
channelId: z
.string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") })
.nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")),
@ -42,19 +40,11 @@ const NotifyChannelEditFormMattermostFields = () => {
<Input placeholder={t("settings.notification.channel.form.mattermost_channel_id.placeholder")} />
</Form.Item>
<Form.Item
name="username"
label={t("settings.notification.channel.form.mattermost_username.label")}
rules={[formRule]}
>
<Form.Item name="username" label={t("settings.notification.channel.form.mattermost_username.label")} rules={[formRule]}>
<Input placeholder={t("settings.notification.channel.form.mattermost_username.placeholder")} />
</Form.Item>
<Form.Item
name="password"
label={t("settings.notification.channel.form.mattermost_password.label")}
rules={[formRule]}
>
<Form.Item name="password" label={t("settings.notification.channel.form.mattermost_password.label")} rules={[formRule]}>
<Input.Password placeholder={t("settings.notification.channel.form.mattermost_password.placeholder")} />
</Form.Item>
</>

View File

@ -3,9 +3,9 @@ import { useTranslation } from "react-i18next";
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography } from "antd";
import Show from "@/components/Show";
import { applyDNSProvidersMap } from "@/domain/provider";
import { acmeDns01ProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderPickerProps = {
export type ACMEDns01ProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@ -13,7 +13,7 @@ export type ApplyDNSProviderPickerProps = {
onSelect?: (value: string) => void;
};
const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: ApplyDNSProviderPickerProps) => {
const ACMEDns01ProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: ACMEDns01ProviderPickerProps) => {
const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>();
@ -25,7 +25,7 @@ const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSe
}, []);
const providers = useMemo(() => {
return Array.from(applyDNSProvidersMap.values()).filter((provider) => {
return Array.from(acmeDns01ProvidersMap.values()).filter((provider) => {
if (keyword) {
const value = keyword.toLowerCase();
return provider.type.toLowerCase().includes(value) || t(provider.name).toLowerCase().includes(value);
@ -72,4 +72,4 @@ const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSe
);
};
export default memo(ApplyDNSProviderPicker);
export default memo(ACMEDns01ProviderPicker);

View File

@ -2,21 +2,21 @@ import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type ApplyDNSProvider, applyDNSProvidersMap } from "@/domain/provider";
import { type ACMEDns01Provider, acmeDns01ProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderSelectProps = Omit<
export type ACMEDns01ProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: ApplyDNSProvider) => boolean;
filter?: (record: ACMEDns01Provider) => boolean;
};
const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProps) => {
const ACMEDns01ProviderSelect = ({ filter, ...props }: ACMEDns01ProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyDNSProvider }>>([]);
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ACMEDns01Provider }>>([]);
useEffect(() => {
const allItems = Array.from(applyDNSProvidersMap.values());
const allItems = Array.from(acmeDns01ProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions(
filteredItems.map((item) => ({
@ -29,7 +29,7 @@ const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProp
}, [filter]);
const renderOption = (key: string) => {
const provider = applyDNSProvidersMap.get(key);
const provider = acmeDns01ProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
@ -64,4 +64,4 @@ const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProp
);
};
export default memo(ApplyDNSProviderSelect);
export default memo(ACMEDns01ProviderSelect);

View File

@ -2,28 +2,28 @@ import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type ApplyCAProvider, applyCAProvidersMap } from "@/domain/provider";
import { type CAProvider, caProvidersMap } from "@/domain/provider";
export type ApplyCAProviderSelectProps = Omit<
export type CAProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: ApplyCAProvider) => boolean;
filter?: (record: CAProvider) => boolean;
};
const ApplyCAProviderSelect = ({ filter, ...props }: ApplyCAProviderSelectProps) => {
const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyCAProvider }>>([]);
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: CAProvider }>>([]);
useEffect(() => {
const allItems = Array.from(applyCAProvidersMap.values());
const allItems = Array.from(caProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions([
{
key: "",
value: "",
label: "provider.default_ca_provider.label",
data: {} as ApplyCAProvider,
label: t("provider.default_ca_provider.label"),
data: {} as CAProvider,
},
...filteredItems.map((item) => ({
key: item.type,
@ -45,7 +45,7 @@ const ApplyCAProviderSelect = ({ filter, ...props }: ApplyCAProviderSelectProps)
);
}
const provider = applyCAProvidersMap.get(key);
const provider = caProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
@ -80,4 +80,4 @@ const ApplyCAProviderSelect = ({ filter, ...props }: ApplyCAProviderSelectProps)
);
};
export default memo(ApplyCAProviderSelect);
export default memo(CAProviderSelect);

View File

@ -3,9 +3,9 @@ import { useTranslation } from "react-i18next";
import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tabs, Tooltip, Typography } from "antd";
import Show from "@/components/Show";
import { DEPLOY_CATEGORIES, deployProvidersMap } from "@/domain/provider";
import { DEPLOYMENT_CATEGORIES, deploymentProvidersMap } from "@/domain/provider";
export type DeployProviderPickerProps = {
export type DeploymentProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@ -13,10 +13,10 @@ export type DeployProviderPickerProps = {
onSelect?: (value: string) => void;
};
const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DeployProviderPickerProps) => {
const DeploymentProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DeploymentProviderPickerProps) => {
const { t } = useTranslation();
const [category, setCategory] = useState<string>(DEPLOY_CATEGORIES.ALL);
const [category, setCategory] = useState<string>(DEPLOYMENT_CATEGORIES.ALL);
const [keyword, setKeyword] = useState<string>();
const keywordInputRef = useRef<InputRef>(null);
@ -27,9 +27,9 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
}, []);
const providers = useMemo(() => {
return Array.from(deployProvidersMap.values())
return Array.from(deploymentProvidersMap.values())
.filter((provider) => {
if (category && category !== DEPLOY_CATEGORIES.ALL) {
if (category && category !== DEPLOYMENT_CATEGORIES.ALL) {
return provider.category === category;
}
@ -56,17 +56,17 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
<div className="mt-4">
<Flex>
<Tabs
defaultActiveKey={DEPLOY_CATEGORIES.ALL}
defaultActiveKey={DEPLOYMENT_CATEGORIES.ALL}
items={[
DEPLOY_CATEGORIES.ALL,
DEPLOY_CATEGORIES.CDN,
DEPLOY_CATEGORIES.STORAGE,
DEPLOY_CATEGORIES.LOADBALANCE,
DEPLOY_CATEGORIES.FIREWALL,
DEPLOY_CATEGORIES.AV,
DEPLOY_CATEGORIES.SERVERLESS,
DEPLOY_CATEGORIES.WEBSITE,
DEPLOY_CATEGORIES.OTHER,
DEPLOYMENT_CATEGORIES.ALL,
DEPLOYMENT_CATEGORIES.CDN,
DEPLOYMENT_CATEGORIES.STORAGE,
DEPLOYMENT_CATEGORIES.LOADBALANCE,
DEPLOYMENT_CATEGORIES.FIREWALL,
DEPLOYMENT_CATEGORIES.AV,
DEPLOYMENT_CATEGORIES.SERVERLESS,
DEPLOYMENT_CATEGORIES.WEBSITE,
DEPLOYMENT_CATEGORIES.OTHER,
].map((key) => ({
key: key,
label: t(`provider.category.${key}`),
@ -110,4 +110,4 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
);
};
export default memo(DeployProviderPicker);
export default memo(DeploymentProviderPicker);

View File

@ -2,21 +2,21 @@ import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type DeployProvider, deployProvidersMap } from "@/domain/provider";
import { type DeploymentProvider, deploymentProvidersMap } from "@/domain/provider";
export type DeployProviderSelectProps = Omit<
export type DeploymentProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: DeployProvider) => boolean;
filter?: (record: DeploymentProvider) => boolean;
};
const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) => {
const DeploymentProviderSelect = ({ filter, ...props }: DeploymentProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeployProvider }>>([]);
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeploymentProvider }>>([]);
useEffect(() => {
const allItems = Array.from(deployProvidersMap.values());
const allItems = Array.from(deploymentProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions(
filteredItems.map((item) => ({
@ -29,7 +29,7 @@ const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) =
}, [filter]);
const renderOption = (key: string) => {
const provider = deployProvidersMap.get(key);
const provider = deploymentProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
@ -64,4 +64,4 @@ const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) =
);
};
export default memo(DeployProviderSelect);
export default memo(DeploymentProviderSelect);

View File

@ -0,0 +1,67 @@
import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type NotificationProvider, notificationProvidersMap } from "@/domain/provider";
export type NotificationProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: NotificationProvider) => boolean;
};
const NotificationProviderSelect = ({ filter, ...props }: NotificationProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: NotificationProvider }>>([]);
useEffect(() => {
const allItems = Array.from(notificationProvidersMap.values());
const filteredItems = filter != null ? allItems.filter(filter) : allItems;
setOptions(
filteredItems.map((item) => ({
key: item.type,
value: item.type,
label: t(item.name),
data: item,
}))
);
}, [filter]);
const renderOption = (key: string) => {
const provider = notificationProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>
</Space>
);
};
return (
<Select
{...props}
filterOption={(inputValue, option) => {
if (!option) return false;
const value = inputValue.toLowerCase();
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
}}
labelRender={({ label, value }) => {
if (!label) {
return <Typography.Text type="secondary">{props.placeholder}</Typography.Text>;
}
return renderOption(value as string);
}}
options={options}
optionFilterProp={undefined}
optionLabelProp={undefined}
optionRender={(option) => renderOption(option.data.value)}
/>
);
};
export default memo(NotificationProviderSelect);

View File

@ -193,6 +193,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
const NEWLINE = "\n";
const logstr = listData
.map((group) => {
const escape = (str: string) => str.replaceAll("\r", "\\r").replaceAll("\n", "\\n");
return (
group.name +
NEWLINE +
@ -200,8 +201,9 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
.map((record) => {
const datetime = dayjs(record.timestamp).format("YYYY-MM-DDTHH:mm:ss.SSSZ");
const level = record.level;
const message = record.message.trim().replaceAll("\r", "\\r").replaceAll("\n", "\\n");
return `[${datetime}] [${level}] ${message}`;
const message = record.message;
const data = record.data && Object.keys(record.data).length > 0 ? JSON.stringify(record.data) : "";
return `[${datetime}] [${level}] ${escape(message)} ${escape(data)}`.trim();
})
.join(NEWLINE)
);

View File

@ -31,9 +31,10 @@ import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import ModalForm from "@/components/ModalForm";
import MultipleInput from "@/components/MultipleInput";
import ApplyCAProviderSelect from "@/components/provider/ApplyCAProviderSelect";
import ApplyDNSProviderSelect from "@/components/provider/ApplyDNSProviderSelect";
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyCAProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
import ACMEDns01ProviderSelect from "@/components/provider/ACMEDns01ProviderSelect";
import CAProviderSelect from "@/components/provider/CAProviderSelect";
import Show from "@/components/Show";
import { ACCESS_USAGES, ACME_DNS01_PROVIDERS, accessProvidersMap, acmeDns01ProvidersMap, caProvidersMap } from "@/domain/provider";
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
@ -98,7 +99,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
.refine((v) => {
if (!fieldCAProvider) return true;
const provider = applyCAProvidersMap.get(fieldCAProvider);
const provider = caProvidersMap.get(fieldCAProvider);
return !!provider?.builtin || !!v;
}, t("workflow_node.apply.form.ca_provider_access.placeholder")),
caProviderConfig: z.any().nullish(),
@ -154,7 +155,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
// 如果对应多个(如 AWS 的 Route53、Lightsail腾讯云的 DNS、EdgeOne 等),则显示。
if (fieldProviderAccessId) {
const access = accesses.find((e) => e.id === fieldProviderAccessId);
const providers = Array.from(applyDNSProvidersMap.values()).filter((e) => e.provider === access?.provider);
const providers = Array.from(acmeDns01ProvidersMap.values()).filter((e) => e.provider === access?.provider);
setShowProvider(providers.length > 1);
} else {
setShowProvider(false);
@ -165,7 +166,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
useEffect(() => {
// 内置的 CA 提供商(如 Let's Encrypt无需显示授权信息字段
if (fieldCAProvider) {
const provider = applyCAProvidersMap.get(fieldCAProvider);
const provider = caProvidersMap.get(fieldCAProvider);
setShowCAProviderAccess(!provider?.builtin);
} else {
setShowCAProviderAccess(false);
@ -187,16 +188,16 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
NOTICE: If you add new child component, please keep ASCII order.
*/
switch (fieldProvider) {
case APPLY_DNS_PROVIDERS.AWS:
case APPLY_DNS_PROVIDERS.AWS_ROUTE53:
case ACME_DNS01_PROVIDERS.AWS:
case ACME_DNS01_PROVIDERS.AWS_ROUTE53:
return <ApplyNodeConfigFormAWSRoute53Config {...nestedFormProps} />;
case APPLY_DNS_PROVIDERS.HUAWEICLOUD:
case APPLY_DNS_PROVIDERS.HUAWEICLOUD_DNS:
case ACME_DNS01_PROVIDERS.HUAWEICLOUD:
case ACME_DNS01_PROVIDERS.HUAWEICLOUD_DNS:
return <ApplyNodeConfigFormHuaweiCloudDNSConfig {...nestedFormProps} />;
case APPLY_DNS_PROVIDERS.JDCLOUD:
case APPLY_DNS_PROVIDERS.JDCLOUD_DNS:
case ACME_DNS01_PROVIDERS.JDCLOUD:
case ACME_DNS01_PROVIDERS.JDCLOUD_DNS:
return <ApplyNodeConfigFormJDCloudDNSConfig {...nestedFormProps} />;
case APPLY_DNS_PROVIDERS.TENCENTCLOUD_EO:
case ACME_DNS01_PROVIDERS.TENCENTCLOUD_EO:
return <ApplyNodeConfigFormTencentCloudEOConfig {...nestedFormProps} />;
}
}, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]);
@ -209,7 +210,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId);
onValuesChange?.(formInst.getFieldsValue(true));
} else {
if (applyDNSProvidersMap.get(fieldProvider)?.provider !== applyDNSProvidersMap.get(value)?.provider) {
if (acmeDns01ProvidersMap.get(fieldProvider)?.provider !== acmeDns01ProvidersMap.get(value)?.provider) {
formInst.setFieldValue("providerAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
@ -217,11 +218,9 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
};
const handleProviderAccessSelect = (value: string) => {
if (fieldProviderAccessId === value) return;
// 切换授权信息时联动 DNS 提供商
const access = accesses.find((access) => access.id === value);
const provider = Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider);
const provider = Array.from(acmeDns01ProvidersMap.values()).find((provider) => provider.provider === access?.provider);
if (fieldProvider !== provider?.type) {
formInst.setFieldValue("provider", provider?.type);
onValuesChange?.(formInst.getFieldsValue(true));
@ -229,8 +228,6 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
};
const handleCAProviderSelect = (value?: string | undefined) => {
if (fieldCAProvider === value) return;
// 切换 CA 提供商时联动授权信息
if (value === "") {
setTimeout(() => {
@ -242,7 +239,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
formInst.setFieldValue("caProviderAccessId", initialValues?.caProviderAccessId);
onValuesChange?.(formInst.getFieldsValue(true));
} else {
if (applyCAProvidersMap.get(fieldCAProvider)?.provider !== applyCAProvidersMap.get(value!)?.provider) {
if (caProvidersMap.get(fieldCAProvider)?.provider !== caProvidersMap.get(value!)?.provider) {
formInst.setFieldValue("caProviderAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
@ -327,7 +324,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Form.Item>
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden={!showProvider} rules={[formRule]}>
<ApplyDNSProviderSelect
<ACMEDns01ProviderSelect
disabled={!showProvider}
filter={(record) => {
if (fieldProviderAccessId) {
@ -355,7 +352,6 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</div>
<div className="text-right">
<AccessEditModal
range="both-dns-hosting"
scene="add"
trigger={
<Button size="small" type="link">
@ -363,10 +359,12 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<PlusOutlinedIcon className="text-xs" />
</Button>
}
usage="both-dns-hosting"
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.DNS)) {
formInst.setFieldValue("providerAccessId", record.id);
handleProviderAccessSelect(record.id);
}
}}
/>
@ -376,6 +374,8 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect
filter={(record) => {
if (record.reserve) return false;
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.DNS);
}}
@ -398,19 +398,23 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<Form.Item className="mb-0">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate">{t("workflow_node.apply.form.ca_provider.label")}</div>
<div className="max-w-full grow truncate">
<span>{t("workflow_node.apply.form.ca_provider.label")}</span>
</div>
<div className="text-right">
<Link className="ant-typography" to="/settings/ssl-provider" target="_blank">
<Button size="small" type="link">
{t("workflow_node.apply.form.ca_provider.button")}
<RightOutlinedIcon className="text-xs" />
</Button>
</Link>
<Show when={!fieldCAProvider}>
<Link className="ant-typography" to="/settings/ssl-provider" target="_blank">
<Button size="small" type="link">
{t("workflow_node.apply.form.ca_provider.button")}
<RightOutlinedIcon className="text-xs" />
</Button>
</Link>
</Show>
</div>
</div>
</label>
<Form.Item name="caProvider" rules={[formRule]}>
<ApplyCAProviderSelect
<CAProviderSelect
allowClear
placeholder={t("workflow_node.apply.form.ca_provider.placeholder")}
showSearch
@ -428,8 +432,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</div>
<div className="text-right">
<AccessEditModal
data={{ provider: applyCAProvidersMap.get(fieldCAProvider!)?.provider }}
range="ca-only"
data={{ provider: caProvidersMap.get(fieldCAProvider!)?.provider }}
scene="add"
trigger={
<Button size="small" type="link">
@ -437,6 +440,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<PlusOutlinedIcon className="text-xs" />
</Button>
}
usage="ca-only"
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.CA)) {
@ -450,9 +454,8 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<Form.Item name="caProviderAccessId" rules={[formRule]}>
<AccessSelect
filter={(record) => {
if (fieldCAProvider) {
return applyCAProvidersMap.get(fieldCAProvider)?.provider === record.provider;
}
if (record.reserve !== "ca") return false;
if (fieldCAProvider) return caProvidersMap.get(fieldCAProvider)?.provider === record.provider;
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.CA);

View File

@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
import { Avatar, Flex, Typography } from "antd";
import { produce } from "immer";
import { deployProvidersMap } from "@/domain/provider";
import { deploymentProvidersMap } from "@/domain/provider";
import { type WorkflowNodeConfigForDeploy, WorkflowNodeType } from "@/domain/workflow";
import { useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
@ -43,7 +43,7 @@ const DeployNode = ({ node, disabled }: DeployNodeProps) => {
}
const config = (node.config as WorkflowNodeConfigForDeploy) ?? {};
const provider = deployProvidersMap.get(config.provider);
const provider = deploymentProvidersMap.get(config.provider);
return (
<Flex className="size-full overflow-hidden" align="center" gap={8}>
<Avatar src={provider?.icon} size="small" />

View File

@ -7,10 +7,10 @@ import { z } from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import DeployProviderPicker from "@/components/provider/DeployProviderPicker.tsx";
import DeployProviderSelect from "@/components/provider/DeployProviderSelect.tsx";
import DeploymentProviderPicker from "@/components/provider/DeploymentProviderPicker.tsx";
import DeploymentProviderSelect from "@/components/provider/DeploymentProviderSelect.tsx";
import Show from "@/components/Show";
import { ACCESS_USAGES, DEPLOY_PROVIDERS, accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { ACCESS_USAGES, DEPLOYMENT_PROVIDERS, accessProvidersMap, deploymentProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
import { useWorkflowStore } from "@/stores/workflow";
@ -133,7 +133,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
.refine((v) => {
if (!fieldProvider) return true;
const provider = deployProvidersMap.get(fieldProvider);
const provider = deploymentProvidersMap.get(fieldProvider);
return !!provider?.builtin || !!v;
}, t("workflow_node.deploy.form.provider_access.placeholder")),
providerConfig: z.any().nullish(),
@ -151,7 +151,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
useEffect(() => {
// 内置的部署提供商(如本地部署)无需显示授权信息字段
if (fieldProvider) {
const provider = deployProvidersMap.get(fieldProvider);
const provider = deploymentProvidersMap.get(fieldProvider);
setShowProviderAccess(!provider?.builtin);
} else {
setShowProviderAccess(false);
@ -173,145 +173,145 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
NOTICE: If you add new child component, please keep ASCII order.
*/
switch (fieldProvider) {
case DEPLOY_PROVIDERS["1PANEL_CONSOLE"]:
case DEPLOYMENT_PROVIDERS["1PANEL_CONSOLE"]:
return <DeployNodeConfigForm1PanelConsoleConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS["1PANEL_SITE"]:
case DEPLOYMENT_PROVIDERS["1PANEL_SITE"]:
return <DeployNodeConfigForm1PanelSiteConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_ALB:
case DEPLOYMENT_PROVIDERS.ALIYUN_ALB:
return <DeployNodeConfigFormAliyunALBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_APIGW:
case DEPLOYMENT_PROVIDERS.ALIYUN_APIGW:
return <DeployNodeConfigFormAliyunAPIGWConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_CAS:
case DEPLOYMENT_PROVIDERS.ALIYUN_CAS:
return <DeployNodeConfigFormAliyunCASConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_CAS_DEPLOY:
case DEPLOYMENT_PROVIDERS.ALIYUN_CAS_DEPLOY:
return <DeployNodeConfigFormAliyunCASDeployConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_CLB:
case DEPLOYMENT_PROVIDERS.ALIYUN_CLB:
return <DeployNodeConfigFormAliyunCLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_CDN:
case DEPLOYMENT_PROVIDERS.ALIYUN_CDN:
return <DeployNodeConfigFormAliyunCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_DCDN:
case DEPLOYMENT_PROVIDERS.ALIYUN_DCDN:
return <DeployNodeConfigFormAliyunDCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_ESA:
case DEPLOYMENT_PROVIDERS.ALIYUN_ESA:
return <DeployNodeConfigFormAliyunESAConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_FC:
case DEPLOYMENT_PROVIDERS.ALIYUN_FC:
return <DeployNodeConfigFormAliyunFCConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_LIVE:
case DEPLOYMENT_PROVIDERS.ALIYUN_LIVE:
return <DeployNodeConfigFormAliyunLiveConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_NLB:
case DEPLOYMENT_PROVIDERS.ALIYUN_NLB:
return <DeployNodeConfigFormAliyunNLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_OSS:
case DEPLOYMENT_PROVIDERS.ALIYUN_OSS:
return <DeployNodeConfigFormAliyunOSSConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_VOD:
case DEPLOYMENT_PROVIDERS.ALIYUN_VOD:
return <DeployNodeConfigFormAliyunVODConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.ALIYUN_WAF:
case DEPLOYMENT_PROVIDERS.ALIYUN_WAF:
return <DeployNodeConfigFormAliyunWAFConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.AWS_ACM:
case DEPLOYMENT_PROVIDERS.AWS_ACM:
return <DeployNodeConfigFormAWSACMConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.AWS_CLOUDFRONT:
case DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT:
return <DeployNodeConfigFormAWSCloudFrontConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.AZURE_KEYVAULT:
case DEPLOYMENT_PROVIDERS.AZURE_KEYVAULT:
return <DeployNodeConfigFormAzureKeyVaultConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_APPBLB:
case DEPLOYMENT_PROVIDERS.BAIDUCLOUD_APPBLB:
return <DeployNodeConfigFormBaiduCloudAppBLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_BLB:
case DEPLOYMENT_PROVIDERS.BAIDUCLOUD_BLB:
return <DeployNodeConfigFormBaiduCloudBLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAIDUCLOUD_CDN:
case DEPLOYMENT_PROVIDERS.BAIDUCLOUD_CDN:
return <DeployNodeConfigFormBaiduCloudCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAISHAN_CDN:
case DEPLOYMENT_PROVIDERS.BAISHAN_CDN:
return <DeployNodeConfigFormBaishanCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAOTAPANEL_CONSOLE:
case DEPLOYMENT_PROVIDERS.BAOTAPANEL_CONSOLE:
return <DeployNodeConfigFormBaotaPanelConsoleConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BAOTAPANEL_SITE:
case DEPLOYMENT_PROVIDERS.BAOTAPANEL_SITE:
return <DeployNodeConfigFormBaotaPanelSiteConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BUNNY_CDN:
case DEPLOYMENT_PROVIDERS.BUNNY_CDN:
return <DeployNodeConfigFormBunnyCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.BYTEPLUS_CDN:
case DEPLOYMENT_PROVIDERS.BYTEPLUS_CDN:
return <DeployNodeConfigFormBytePlusCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.CDNFLY:
case DEPLOYMENT_PROVIDERS.CDNFLY:
return <DeployNodeConfigFormCdnflyConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.DOGECLOUD_CDN:
case DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN:
return <DeployNodeConfigFormDogeCloudCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.EDGIO_APPLICATIONS:
case DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS:
return <DeployNodeConfigFormEdgioApplicationsConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.GCORE_CDN:
case DEPLOYMENT_PROVIDERS.GCORE_CDN:
return <DeployNodeConfigFormGcoreCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.HUAWEICLOUD_CDN:
case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_CDN:
return <DeployNodeConfigFormHuaweiCloudCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.HUAWEICLOUD_ELB:
case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_ELB:
return <DeployNodeConfigFormHuaweiCloudELBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.HUAWEICLOUD_WAF:
case DEPLOYMENT_PROVIDERS.HUAWEICLOUD_WAF:
return <DeployNodeConfigFormHuaweiCloudWAFConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.JDCLOUD_ALB:
case DEPLOYMENT_PROVIDERS.JDCLOUD_ALB:
return <DeployNodeConfigFormJDCloudALBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.JDCLOUD_CDN:
case DEPLOYMENT_PROVIDERS.JDCLOUD_CDN:
return <DeployNodeConfigFormJDCloudCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.JDCLOUD_LIVE:
case DEPLOYMENT_PROVIDERS.JDCLOUD_LIVE:
return <DeployNodeConfigFormJDCloudLiveConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.JDCLOUD_VOD:
case DEPLOYMENT_PROVIDERS.JDCLOUD_VOD:
return <DeployNodeConfigFormJDCloudVODConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.KUBERNETES_SECRET:
case DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET:
return <DeployNodeConfigFormKubernetesSecretConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.LOCAL:
case DEPLOYMENT_PROVIDERS.LOCAL:
return <DeployNodeConfigFormLocalConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.QINIU_CDN:
case DEPLOYMENT_PROVIDERS.QINIU_CDN:
return <DeployNodeConfigFormQiniuCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.QINIU_KODO:
case DEPLOYMENT_PROVIDERS.QINIU_KODO:
return <DeployNodeConfigFormQiniuKodoConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.QINIU_PILI:
case DEPLOYMENT_PROVIDERS.QINIU_PILI:
return <DeployNodeConfigFormQiniuPiliConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.RAINYUN_RCDN:
case DEPLOYMENT_PROVIDERS.RAINYUN_RCDN:
return <DeployNodeConfigFormRainYunRCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.SAFELINE:
case DEPLOYMENT_PROVIDERS.SAFELINE:
return <DeployNodeConfigFormSafeLineConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.SSH:
case DEPLOYMENT_PROVIDERS.SSH:
return <DeployNodeConfigFormSSHConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_CDN:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CDN:
return <DeployNodeConfigFormTencentCloudCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_CLB:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CLB:
return <DeployNodeConfigFormTencentCloudCLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_COS:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_COS:
return <DeployNodeConfigFormTencentCloudCOSConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_CSS:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CSS:
return <DeployNodeConfigFormTencentCloudCSSConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_ECDN:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_ECDN:
return <DeployNodeConfigFormTencentCloudECDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_EO:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EO:
return <DeployNodeConfigFormTencentCloudEOConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_SCF:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SCF:
return <DeployNodeConfigFormTencentCloudSCFConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY:
return <DeployNodeConfigFormTencentCloudSSLDeployConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_VOD:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_VOD:
return <DeployNodeConfigFormTencentCloudVODConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.TENCENTCLOUD_WAF:
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_WAF:
return <DeployNodeConfigFormTencentCloudWAFConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.UCLOUD_UCDN:
case DEPLOYMENT_PROVIDERS.UCLOUD_UCDN:
return <DeployNodeConfigFormUCloudUCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.UCLOUD_US3:
case DEPLOYMENT_PROVIDERS.UCLOUD_US3:
return <DeployNodeConfigFormUCloudUS3Config {...nestedFormProps} />;
case DEPLOY_PROVIDERS.UPYUN_CDN:
case DEPLOYMENT_PROVIDERS.UPYUN_CDN:
return <DeployNodeConfigFormUpyunCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.UPYUN_FILE:
case DEPLOYMENT_PROVIDERS.UPYUN_FILE:
return <DeployNodeConfigFormUpyunFileConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_ALB:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_ALB:
return <DeployNodeConfigFormVolcEngineALBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_CDN:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_CDN:
return <DeployNodeConfigFormVolcEngineCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_CERTCENTER:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_CERTCENTER:
return <DeployNodeConfigFormVolcEngineCertCenterConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_CLB:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_CLB:
return <DeployNodeConfigFormVolcEngineCLBConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_DCDN:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_DCDN:
return <DeployNodeConfigFormVolcEngineDCDNConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_IMAGEX:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_IMAGEX:
return <DeployNodeConfigFormVolcEngineImageXConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_LIVE:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_LIVE:
return <DeployNodeConfigFormVolcEngineLiveConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.VOLCENGINE_TOS:
case DEPLOYMENT_PROVIDERS.VOLCENGINE_TOS:
return <DeployNodeConfigFormVolcEngineTOSConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.WANGSU_CDNPRO:
case DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO:
return <DeployNodeConfigFormWangsuCDNProConfig {...nestedFormProps} />;
case DEPLOY_PROVIDERS.WEBHOOK:
case DEPLOYMENT_PROVIDERS.WEBHOOK:
return <DeployNodeConfigFormWebhookConfig {...nestedFormProps} />;
}
}, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]);
@ -322,8 +322,6 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
};
const handleProviderSelect = (value?: string | undefined) => {
if (fieldProvider === value) return;
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
if (initialValues?.provider === value) {
formInst.resetFields();
@ -339,7 +337,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
}
formInst.setFieldsValue(newValues);
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value!)?.provider) {
if (deploymentProvidersMap.get(fieldProvider)?.provider !== deploymentProvidersMap.get(value!)?.provider) {
formInst.setFieldValue("providerAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
@ -384,10 +382,10 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Show
when={!!fieldProvider}
fallback={<DeployProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />}
fallback={<DeploymentProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />}
>
<Form.Item name="provider" label={t("workflow_node.deploy.form.provider.label")} rules={[formRule]}>
<DeployProviderSelect
<DeploymentProviderSelect
allowClear
disabled={!!initialValues?.provider}
placeholder={t("workflow_node.deploy.form.provider.placeholder")}
@ -410,8 +408,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
</div>
<div className="text-right">
<AccessEditModal
data={{ provider: deployProvidersMap.get(fieldProvider!)?.provider }}
range="both-dns-hosting"
data={{ provider: deploymentProvidersMap.get(fieldProvider!)?.provider }}
scene="add"
trigger={
<Button size="small" type="link">
@ -419,6 +416,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<PlusOutlinedIcon className="text-xs" />
</Button>
}
usage="both-dns-hosting"
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) {
@ -432,9 +430,8 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<Form.Item name="providerAccessId" rules={[formRule]}>
<AccessSelect
filter={(record) => {
if (fieldProvider) {
return deployProvidersMap.get(fieldProvider)?.provider === record.provider;
}
if (record.reserve) return false;
if (fieldProvider) return deploymentProvidersMap.get(fieldProvider)?.provider === record.provider;
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING);
@ -444,7 +441,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
</Form.Item>
</Form.Item>
<Show when={fieldProvider === DEPLOY_PROVIDERS.LOCAL}>
<Show when={fieldProvider === DEPLOYMENT_PROVIDERS.LOCAL}>
<Form.Item>
<Alert
type="info"

Some files were not shown because too many files have changed in this diff Show More