feat: deployer migration

This commit is contained in:
Yoan.liu
2026-07-28 18:07:25 +08:00
parent 8c7062e462
commit 6cfe28e431
40 changed files with 471 additions and 1757 deletions
+1 -9
View File
@@ -43,6 +43,7 @@ require (
github.com/baidubce/bce-sdk-go v0.9.270
github.com/byteplus-sdk/byteplus-go-sdk-v2 v1.0.71
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.71
github.com/fsnotify/fsnotify v1.10.1
github.com/go-acme/lego/v5 v5.2.2
github.com/go-cmd/cmd v1.4.3
github.com/go-resty/resty/v2 v2.17.2
@@ -63,7 +64,6 @@ require (
github.com/pocketbase/pocketbase v0.39.6
github.com/povsister/scp v0.0.0-20250701154629-777cf82de5df
github.com/pquerna/otp v1.5.0
github.com/qiniu/go-sdk/v7 v7.26.15
github.com/samber/lo v1.53.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
@@ -122,9 +122,6 @@ require (
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-openapi/jsonreference v0.21.5 // indirect
github.com/go-openapi/swag v0.25.5 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
@@ -134,7 +131,6 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
@@ -146,8 +142,6 @@ require (
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/qiniu/dyn v1.3.0 // indirect
github.com/qiniu/x v1.17.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/stretchr/objx v0.5.3 // indirect
@@ -174,7 +168,6 @@ require (
cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
github.com/alibabacloud-go/debug v1.0.1 // indirect
@@ -204,7 +197,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.19.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/ganigeorgiev/fexpr v0.5.0 // indirect
github.com/go-acme/alidns-20150109/v5 v5.4.1 // indirect
-19
View File
@@ -66,8 +66,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mo
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/G-Core/gcorelabscdn-go v1.0.37 h1:IHqiSfnHp3tnuUSU1FIH5+3XepNnSsyJn0JoWgvUYL8=
@@ -417,17 +415,9 @@ github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lE
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=
github.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk=
github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
@@ -675,8 +665,6 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
@@ -831,13 +819,6 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/qiniu/dyn v1.3.0 h1:s+xPTeV0H8yikgM4ZMBc7Rrefam8UNI3asBlkaOQg5o=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.26.15 h1:CaKVcP29ZnOp/pqE7U3RAZxqwL7CmtMTWlV1gAV71H0=
github.com/qiniu/go-sdk/v7 v7.26.15/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/qiniu/x v1.17.0 h1:OsyDKXzYp5vw9Hc7VAe4Cso1Sp50fLKGsBuDteyTevE=
github.com/qiniu/x v1.17.0/go.mod h1:AiovSOCaRijaf3fj+0CBOpR1457pn24b0Vdb1JpwhII=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
@@ -1,27 +0,0 @@
package deployers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-cdn"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeQiniuCDN, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForQiniu{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
AccessKey: credentials.AccessKey,
SecretKey: credentials.SecretKey,
DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"),
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
})
return provider, err
})
}
@@ -1,27 +0,0 @@
package deployers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-kodo"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeQiniuKodo, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForQiniu{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
AccessKey: credentials.AccessKey,
SecretKey: credentials.SecretKey,
Bucket: xmaps.GetString(options.ProviderExtendedConfig, "bucket"),
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
})
return provider, err
})
}
@@ -1,28 +0,0 @@
package deployers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-pili"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeQiniuPili, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForQiniu{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
AccessKey: credentials.AccessKey,
SecretKey: credentials.SecretKey,
Hub: xmaps.GetString(options.ProviderExtendedConfig, "hub"),
DomainMatchPattern: xmaps.GetString(options.ProviderExtendedConfig, "domainMatchPattern"),
Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"),
})
return provider, err
})
}
+51
View File
@@ -0,0 +1,51 @@
package deployers
import (
"fmt"
"net/http"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
dplyimpl "github.com/certimate-go/certimate/pkg/core/deployer/providers/webhook"
xhttp "github.com/certimate-go/certimate/pkg/utils/http"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeWebhook, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForWebhook{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
mergedHeaders := make(map[string]string)
if defaultHeadersString := credentials.HeadersString; defaultHeadersString != "" {
h, err := xhttp.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 := xmaps.GetString(options.ProviderExtendedConfig, "headers"); extendedHeadersString != "" {
h, err := xhttp.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)
}
}
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
WebhookUrl: credentials.Url,
WebhookData: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", credentials.DataString),
Method: credentials.Method,
Headers: mergedHeaders,
Timeout: xmaps.GetInt(options.ProviderExtendedConfig, "timeout"),
AllowInsecureConnections: credentials.AllowInsecureConnections,
})
return provider, err
})
}
@@ -1,170 +0,0 @@
package qiniusslcert
import (
"context"
"crypto/x509"
"fmt"
"log/slog"
"slices"
"strings"
"time"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/certimate-go/certimate/pkg/core"
qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type (
Provider = core.Certmgr
UploadResult = core.CertmgrUploadResult
ReplaceResult = core.CertmgrReplaceResult
)
type CertmgrConfig struct {
// 七牛云 AccessKey。
AccessKey string `json:"accessKey"`
// 七牛云 SecretKey。
SecretKey string `json:"secretKey"`
}
type Certmgr struct {
config *CertmgrConfig
logger *slog.Logger
sdkClient *qiniusdk.SslCertManager
}
var _ Provider = (*Certmgr)(nil)
func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the certmgr provider is nil")
}
client, err := createSDKClient(config.AccessKey, config.SecretKey)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
return &Certmgr{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (c *Certmgr) SetLogger(logger *slog.Logger) {
if logger == nil {
c.logger = slog.New(slog.DiscardHandler)
} else {
c.logger = logger
}
}
func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*UploadResult, error) {
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 生成新证书名(需符合七牛云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 查询已有证书,避免重复上传
getSslCertListMarker := ""
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getSslCertListResp, err := c.sdkClient.GetSslCertList(ctx, getSslCertListMarker, 200)
c.logger.Debug("sdk request 'sslcert.GetList'", slog.Any("params.marker", getSslCertListMarker), slog.Any("response", getSslCertListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcert.GetList': %w", err)
}
for _, sslItem := range getSslCertListResp.Certs {
// 对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, sslItem.CommonName) {
continue
}
// 对比证书备用名称
if !slices.Equal(certX509.DNSNames, sslItem.DnsNames) {
continue
}
// 对比证书有效期
if certX509.NotBefore.Unix() != sslItem.NotBefore {
continue
} else if certX509.NotAfter.Unix() != sslItem.NotAfter {
continue
}
// 对比证书公钥算法
switch certX509.PublicKeyAlgorithm {
case x509.RSA:
if !strings.EqualFold(sslItem.Encrypt, "RSA") {
continue
}
case x509.ECDSA:
if !strings.EqualFold(sslItem.Encrypt, "ECDSA") {
continue
}
case x509.Ed25519:
if !strings.EqualFold(sslItem.Encrypt, "Ed25519") {
continue
}
default:
continue
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
c.logger.Info("ssl certificate already exists")
return &UploadResult{
CertId: sslItem.CertID,
CertName: sslItem.Name,
}, nil
}
if len(getSslCertListResp.Certs) == 0 || getSslCertListResp.Marker == "" {
break
}
getSslCertListMarker = getSslCertListResp.Marker
}
// 上传新证书
// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate
uploadSslCertResp, err := c.sdkClient.UploadSslCert(ctx, certName, certX509.Subject.CommonName, certPEM, privkeyPEM)
c.logger.Debug("sdk request 'sslcert.Upload'", slog.Any("response", uploadSslCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'sslcert.Upload': %w", err)
}
return &UploadResult{
CertId: uploadSslCertResp.CertID,
CertName: certName,
}, nil
}
func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*ReplaceResult, error) {
return nil, core.ErrUnsupported
}
func createSDKClient(accessKey, secretKey string) (*qiniusdk.SslCertManager, error) {
if secretKey == "" {
return nil, fmt.Errorf("qiniu: invalid access key")
}
if secretKey == "" {
return nil, fmt.Errorf("qiniu: invalid secret key")
}
credential := auth.New(accessKey, secretKey)
client := qiniusdk.NewSslCertManager(credential)
return client, nil
}
@@ -1,49 +0,0 @@
package qiniusslcert_test
import (
"testing"
impl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert"
tester "github.com/certimate-go/certimate/pkg/core/certmgr/testing"
)
var (
fp = tester.Args("QINIUSSLCERT_")
fTestCertPath string
fTestKeyPath string
fAccessKey string
fSecretKey string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKey, "ACCESSKEY")
fp.DefineString(&fSecretKey, "SECRETKEY")
}
/*
Shell command to run this test:
go test -v ./qiniu_sslcert_test.go -args \
--QINIUSSLCERT_TESTCERTPATH="/path/to/your-test-cert.pem" \
--QINIUSSLCERT_TESTKEYPATH="/path/to/your-test-key.pem" \
--QINIUSSLCERT_ACCESSKEY="your-access-key" \
--QINIUSSLCERT_SECRETKEY="your-secret-key"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Upload", func(t *testing.T) {
provider, err := impl.NewCertmgr(&impl.CertmgrConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestUpload(t, provider, tester.TestUploadArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -1,10 +0,0 @@
package qiniucdn
const (
// 匹配模式:精确匹配。
DOMAIN_MATCH_PATTERN_EXACT = "exact"
// 匹配模式:通配符匹配。
DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard"
// 匹配模式:证书 SAN 匹配。
DOMAIN_MATCH_PATTERN_CERTSAN = "certsan"
)
@@ -1,231 +0,0 @@
package qiniucdn
import (
"context"
"fmt"
"log/slog"
"strings"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert"
qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu"
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// 七牛云 AccessKey。
AccessKey string `json:"accessKey"`
// 七牛云 SecretKey。
SecretKey string `json:"secretKey"`
// 域名匹配模式。
// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。
DomainMatchPattern string `json:"domainMatchPattern,omitempty"`
// 加速域名(支持泛域名)。
Domain string `json:"domain"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *qiniusdk.CdnManager
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client := qiniusdk.NewCdnManager(auth.New(config.AccessKey, config.SecretKey))
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkCertmgr: pcertmgr,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 获取待部署的域名列表
var domains []string
switch d.config.DomainMatchPattern {
case "", DOMAIN_MATCH_PATTERN_EXACT:
{
domain := normalizeDomain(d.config.Domain)
if domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
domains = []string{domain}
}
case DOMAIN_MATCH_PATTERN_WILDCARD:
{
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
if strings.HasPrefix(d.config.Domain, "*.") {
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return nil, err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatch(d.config.Domain, domain)
})
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by wildcard")
}
} else {
domains = []string{d.config.Domain}
}
}
case DOMAIN_MATCH_PATTERN_CERTSAN:
{
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return nil, err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatchByCertificatePEM(certPEM, domain)
})
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
default:
return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern)
}
// 批量更新域名证书
if len(domains) == 0 {
d.logger.Info("no cdn domains to deploy")
} else {
d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains))
if err := xloop.ForRangeAllWithContext(ctx, domains, func(ctx context.Context, domain string, _ int) error {
return d.updateDomainCertificate(ctx, domain, upres.CertId)
}); err != nil {
return nil, err
}
}
return &DeployResult{}, nil
}
func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {
domains := make([]string, 0)
// 查询域名列表
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
getDomainListMarker := ""
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getDomainListResp, err := d.sdkClient.GetDomainList(ctx, getDomainListMarker, 100)
d.logger.Debug("sdk request 'cdn.GetDomainList'", slog.String("params.marker", getDomainListMarker), slog.Any("response", getDomainListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainList': %w", err)
}
ignoredStatuses := []string{"frozen", "offlined"}
for _, domainItem := range getDomainListResp.Domains {
if lo.Contains(ignoredStatuses, domainItem.OperatingState) {
continue
}
domains = append(domains, domainItem.Name)
}
if len(getDomainListResp.Domains) == 0 || getDomainListResp.Marker == "" {
break
}
getDomainListMarker = getDomainListResp.Marker
}
return domains, nil
}
func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, cloudCertId string) error {
// 获取域名信息
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(ctx, domain)
d.logger.Debug("sdk request 'cdn.GetDomainInfo'", slog.String("params.domain", domain), slog.Any("response", getDomainInfoResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdn.GetDomainInfo': %w", err)
}
// 判断域名是否已启用 HTTPS
// 如果已启用,修改域名证书;否则,启用 HTTPS
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
if getDomainInfoResp.Https == nil || getDomainInfoResp.Https.CertID == "" {
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(ctx, domain, cloudCertId, true, true)
d.logger.Debug("sdk request 'cdn.EnableDomainHttps'", slog.String("params.domain", domain), slog.String("params.certId", cloudCertId), slog.Any("response", enableDomainHttpsResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdn.EnableDomainHttps': %w", err)
}
} else if getDomainInfoResp.Https.CertID != cloudCertId {
modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(ctx, domain, cloudCertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable)
d.logger.Debug("sdk request 'cdn.ModifyDomainHttpsConf'", slog.String("params.domain", domain), slog.String("params.certId", cloudCertId), slog.Any("response", modifyDomainHttpsConfResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainHttpsConf': %w", err)
}
}
return nil
}
func normalizeDomain(domain string) string {
// "*.example.com" → ".example.com",适配七牛云 CDN 的泛域名参数要求
if strings.HasPrefix(domain, "*.") {
return strings.TrimPrefix(domain, "*")
}
return domain
}
@@ -1,54 +0,0 @@
package qiniucdn_test
import (
"testing"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-cdn"
tester "github.com/certimate-go/certimate/pkg/core/deployer/testing"
)
var (
fp = tester.Args("QINIUCDN_")
fTestCertPath string
fTestKeyPath string
fAccessKey string
fSecretKey string
fDomain string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKey, "ACCESSKEY")
fp.DefineString(&fSecretKey, "SECRETKEY")
fp.DefineString(&fDomain, "DOMAIN")
}
/*
Shell command to run this test:
go test -v ./qiniu_cdn_test.go -args \
--QINIUCDN_TESTCERTPATH="/path/to/your-test-cert.pem" \
--QINIUCDN_TESTKEYPATH="/path/to/your-test-key.pem" \
--QINIUCDN_ACCESSKEY="your-access-key" \
--QINIUCDN_SECRETKEY="your-secret-key" \
--QINIUCDN_DOMAIN="example.com"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
DomainMatchPattern: impl.DOMAIN_MATCH_PATTERN_EXACT,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -1,94 +0,0 @@
package qiniukodo
import (
"context"
"fmt"
"log/slog"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert"
qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// 七牛云 AccessKey。
AccessKey string `json:"accessKey"`
// 七牛云 SecretKey。
SecretKey string `json:"secretKey"`
// 存储桶名。暂时无用。
Bucket string `json:"bucket"`
// 自定义域名(不支持泛域名)。
Domain string `json:"domain"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *qiniusdk.KodoManager
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client := qiniusdk.NewKodoManager(auth.New(config.AccessKey, config.SecretKey))
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkCertmgr: pcertmgr,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 绑定空间域名证书
bindBucketCertResp, err := d.sdkClient.BindBucketCert(ctx, d.config.Domain, upres.CertId)
d.logger.Debug("sdk request 'kodo.BindCert'", slog.String("params.domain", d.config.Domain), slog.String("params.certId", upres.CertId), slog.Any("response", bindBucketCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'kodo.BindCert': %w", err)
}
return &DeployResult{}, nil
}
@@ -1,57 +0,0 @@
package qiniukodo_test
import (
"testing"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-kodo"
tester "github.com/certimate-go/certimate/pkg/core/deployer/testing"
)
var (
fp = tester.Args("QINIUKODO_")
fTestCertPath string
fTestKeyPath string
fAccessKey string
fSecretKey string
fBucket string
fDomain string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKey, "ACCESSKEY")
fp.DefineString(&fSecretKey, "SECRETKEY")
fp.DefineString(&fBucket, "BUCKET")
fp.DefineString(&fDomain, "DOMAIN")
}
/*
Shell command to run this test:
go test -v ./qiniu_kodo_test.go -args \
--QINIUKODO_TESTCERTPATH="/path/to/your-test-cert.pem" \
--QINIUKODO_TESTKEYPATH="/path/to/your-test-key.pem" \
--QINIUKODO_ACCESSKEY="your-access-key" \
--QINIUKODO_SECRETKEY="your-secret-key" \
--QINIUKODO_BUCKET="your-bucket" \
--QINIUKODO_DOMAIN="example.com"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
Bucket: fBucket,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -1,8 +0,0 @@
package qiniupili
const (
// 匹配模式:精确匹配。
DOMAIN_MATCH_PATTERN_EXACT = "exact"
// 匹配模式:证书 SAN 匹配。
DOMAIN_MATCH_PATTERN_CERTSAN = "certsan"
)
@@ -1,174 +0,0 @@
package qiniupili
import (
"context"
"fmt"
"log/slog"
"github.com/qiniu/go-sdk/v7/pili"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/qiniu-sslcert"
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// 七牛云 AccessKey。
AccessKey string `json:"accessKey"`
// 七牛云 SecretKey。
SecretKey string `json:"secretKey"`
// 直播空间名。
Hub string `json:"hub"`
// 域名匹配模式。
// 零值时默认值 [DOMAIN_MATCH_PATTERN_EXACT]。
DomainMatchPattern string `json:"domainMatchPattern,omitempty"`
// 直播流域名(不支持泛域名)。
Domain string `json:"domain"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *pili.Manager
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
manager := pili.NewManager(pili.ManagerConfig{AccessKey: config.AccessKey, SecretKey: config.SecretKey})
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
AccessKey: config.AccessKey,
SecretKey: config.SecretKey,
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: manager,
sdkCertmgr: pcertmgr,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
} else {
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
// 获取待部署的域名列表
var domains []string
switch d.config.DomainMatchPattern {
case "", DOMAIN_MATCH_PATTERN_EXACT:
{
if d.config.Domain == "" {
return nil, fmt.Errorf("config `domain` is required")
}
domains = []string{d.config.Domain}
}
case DOMAIN_MATCH_PATTERN_CERTSAN:
{
domainCandidates, err := d.getAllDomainsByHub(ctx, d.config.Hub)
if err != nil {
return nil, err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatchByCertificatePEM(certPEM, domain)
})
if len(domains) == 0 {
return nil, fmt.Errorf("could not find any domains matched by certificate")
}
}
default:
return nil, fmt.Errorf("unsupported domain match pattern: '%s'", d.config.DomainMatchPattern)
}
// 批量更新域名证书
if len(domains) == 0 {
d.logger.Info("no pili domains to deploy")
} else {
d.logger.Info("found pili domains to deploy", slog.Any("domains", domains))
if err := xloop.ForRangeAllWithContext(ctx, domains, func(ctx context.Context, domain string, _ int) error {
return d.updateDomainCertificate(ctx, d.config.Hub, domain, upres.CertName)
}); err != nil {
return nil, err
}
}
return &DeployResult{}, nil
}
func (d *Deployer) getAllDomainsByHub(ctx context.Context, hub string) ([]string, error) {
domains := make([]string, 0)
// 查询域名列表
// REF: https://developer.qiniu.com/pili/9910/pili-service-sdk#6
getDomainListReq := pili.GetDomainsListRequest{
Hub: hub,
}
getDomainListResp, err := d.sdkClient.GetDomainsList(ctx, getDomainListReq)
d.logger.Debug("sdk request 'pili.GetDomainsList'", slog.Any("request", getDomainListReq), slog.Any("response", getDomainListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'pili.GetDomainsList': %w", err)
}
for _, domainItem := range getDomainListResp.Domains {
domains = append(domains, domainItem.Domain)
}
return domains, nil
}
func (d *Deployer) updateDomainCertificate(ctx context.Context, hub string, domain string, cloudCertName string) error {
// 修改域名证书配置
// REF: https://developer.qiniu.com/pili/9910/pili-service-sdk#6
setDomainCertReq := pili.SetDomainCertRequest{
Hub: hub,
Domain: domain,
CertName: cloudCertName,
}
err := d.sdkClient.SetDomainCert(ctx, setDomainCertReq)
d.logger.Debug("sdk request 'pili.SetDomainCert'", slog.Any("request", setDomainCertReq))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'pili.SetDomainCert': %w", err)
}
return nil
}
@@ -1,57 +0,0 @@
package qiniupili_test
import (
"testing"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/qiniu-pili"
tester "github.com/certimate-go/certimate/pkg/core/deployer/testing"
)
var (
fp = tester.Args("QINIUPILI_")
fTestCertPath string
fTestKeyPath string
fAccessKey string
fSecretKey string
fHub string
fDomain string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKey, "ACCESSKEY")
fp.DefineString(&fSecretKey, "SECRETKEY")
fp.DefineString(&fHub, "HUB")
fp.DefineString(&fDomain, "DOMAIN")
}
/*
Shell command to run this test:
go test -v ./qiniu_pili_test.go -args \
--QINIUPILI_TESTCERTPATH="/path/to/your-test-cert.pem" \
--QINIUPILI_TESTKEYPATH="/path/to/your-test-key.pem" \
--QINIUPILI_ACCESSKEY="your-access-key" \
--QINIUPILI_SECRETKEY="your-secret-key" \
--QINIUPILI_HUB="your-hub-name" \
--QINIUPILI_DOMAIN="example.com"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
AccessKey: fAccessKey,
SecretKey: fSecretKey,
DomainMatchPattern: impl.DOMAIN_MATCH_PATTERN_EXACT,
Domain: fDomain,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -0,0 +1,226 @@
package webhook
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"log/slog"
"mime"
"net/http"
"net/url"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/pkg/core"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
xcertx509 "github.com/certimate-go/certimate/pkg/utils/cert/x509"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// Webhook 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"`
// 请求超时(单位:秒)。
// 零值时默认值 30。
Timeout int `json:"timeout,omitempty"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
httpClient *resty.Client
}
var _ Provider = (*Deployer)(nil)
const (
contentTypeJson = "application/json"
contentTypeForm = "application/x-www-form-urlencoded"
contentTypeMultipart = "multipart/form-data"
)
var allowedContentTypes = map[string]bool{
contentTypeJson: true,
contentTypeForm: true,
contentTypeMultipart: true,
}
var allowedMethods = map[string]bool{
http.MethodGet: true,
http.MethodPost: true,
http.MethodPut: true,
http.MethodPatch: true,
http.MethodDelete: true,
}
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client := resty.New().
SetTimeout(30 * time.Second).
SetRetryCount(3).
SetRetryWaitTime(5 * time.Second).
AddRetryCondition(func(resp *resty.Response, _ error) bool {
return resp == nil || resp.StatusCode() >= 500
})
if config.Timeout > 0 {
client.SetTimeout(time.Duration(config.Timeout) * time.Second)
}
if config.AllowInsecureConnections {
client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
}
return &Deployer{
config: config,
logger: slog.Default(),
httpClient: client,
}, nil
}
func (d *Deployer) SetLogger(logger *slog.Logger) {
if logger == nil {
d.logger = slog.New(slog.DiscardHandler)
} else {
d.logger = logger
}
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to parse x509: %w", err)
}
// 提取服务器证书和中间证书
serverCertPEM, issuerCertPEM, err := xcert.ExtractCertificatesFromPEM(certPEM)
if err != nil {
return nil, fmt.Errorf("failed to extract certs: %w", err)
}
// 处理 Webhook URL
webhookUrl, err := url.Parse(d.config.WebhookUrl)
if err != nil {
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(d.config.Method)
if webhookMethod == "" {
webhookMethod = http.MethodPost
} else if !allowedMethods[webhookMethod] {
return nil, fmt.Errorf("unsupported webhook request method '%s'", webhookMethod)
}
// 处理 Webhook 请求标头
webhookHeaders := make(http.Header)
for k, v := range d.config.Headers {
webhookHeaders.Set(k, v)
}
// 处理 Webhook 请求内容类型
webhookContentType := webhookHeaders.Get("Content-Type")
if webhookContentType == "" {
webhookContentType = contentTypeJson
webhookHeaders.Set("Content-Type", contentTypeJson)
} else if mediaType, _, err := mime.ParseMediaType(webhookContentType); err != nil || !allowedContentTypes[mediaType] {
return nil, fmt.Errorf("unsupported webhook content type '%s'", webhookContentType)
}
// 处理 Webhook 请求数据
var webhookData any
if d.config.WebhookData == "" {
webhookData = map[string]string{
"name": strings.Join(xcertx509.GetSubjectAltNames(certX509), ";"),
"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)
}
if webhookMethod == http.MethodGet || webhookContentType == contentTypeForm || webhookContentType == contentTypeMultipart {
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
}
}
}
// 替换变量值
webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${CERTIMATE_DEPLOYER_COMMONNAME}", url.PathEscape(xcertx509.GetSubjectCommonName(certX509)))
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIMATE_DEPLOYER_COMMONNAME}", xcertx509.GetSubjectCommonName(certX509))
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}", strings.Join(xcertx509.GetSubjectAltNames(certX509), ";"))
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIMATE_DEPLOYER_CERTIFICATE}", certPEM)
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}", serverCertPEM)
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}", issuerCertPEM)
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIMATE_DEPLOYER_PRIVATEKEY}", privkeyPEM)
// 兼容旧版变量
// TODO: remove in future version
webhookUrl.Path = strings.ReplaceAll(webhookUrl.Path, "${DOMAIN}", url.PathEscape(certX509.Subject.CommonName))
xmaps.DeepReplaceValueUnsafe(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
xmaps.DeepReplaceValueUnsafe(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
xmaps.DeepReplaceValueUnsafe(webhookData, "${CERTIFICATE}", certPEM)
xmaps.DeepReplaceValueUnsafe(webhookData, "${SERVER_CERTIFICATE}", serverCertPEM)
xmaps.DeepReplaceValueUnsafe(webhookData, "${INTERMEDIA_CERTIFICATE}", issuerCertPEM)
xmaps.DeepReplaceValueUnsafe(webhookData, "${PRIVATE_KEY}", privkeyPEM)
// 生成请求
// 其中 GET 请求需转换为查询参数
req := d.httpClient.R().SetHeaderMultiValues(webhookHeaders)
req.URL = webhookUrl.String()
req.Method = webhookMethod
if webhookMethod == http.MethodGet {
req.SetQueryParams(webhookData.(map[string]string))
} else {
switch webhookContentType {
case contentTypeJson:
req.SetBody(webhookData)
case contentTypeForm:
req.SetFormData(webhookData.(map[string]string))
case contentTypeMultipart:
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())
}
d.logger.Debug("webhook responded", slog.Any("response", resp.String()))
return &DeployResult{}, nil
}
@@ -0,0 +1,57 @@
package webhook_test
import (
"testing"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/webhook"
tester "github.com/certimate-go/certimate/pkg/core/deployer/testing"
)
var (
fp = tester.Args("WEBHOOK_")
fTestCertPath string
fTestKeyPath string
fWebhookUrl string
fWebhookContentType string
fWebhookData string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fWebhookUrl, "URL")
fp.DefineString(&fWebhookContentType, "CONTENTTYPE", "application/json")
fp.DefineString(&fWebhookData, "DATA")
}
/*
Shell command to run this test:
go test -v ./webhook_test.go -args \
--WEBHOOK_TESTCERTPATH="/path/to/your-test-cert.pem" \
--WEBHOOK_TESTKEYPATH="/path/to/your-test-key.pem" \
--WEBHOOK_URL="https://example.com/your-webhook-url" \
--WEBHOOK_CONTENTTYPE="application/json" \
--WEBHOOK_DATA="{\"certificate\":\"${CERTIFICATE}\",\"privateKey\":\"${PRIVATE_KEY}\"}"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
WebhookUrl: fWebhookUrl,
WebhookData: fWebhookData,
Method: "POST",
Headers: map[string]string{
"Content-Type": fWebhookContentType,
},
AllowInsecureConnections: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
-30
View File
@@ -1,30 +0,0 @@
package qiniu
import (
"net/http"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
)
type transport struct {
http.RoundTripper
mac *auth.Credentials
}
func newTransport(mac *auth.Credentials, tr http.RoundTripper) *transport {
if tr == nil {
tr = client.DefaultTransport
}
return &transport{tr, mac}
}
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
token, err := t.mac.SignRequestV2(req)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Qiniu "+token)
return t.RoundTripper.RoundTrip(req)
}
-130
View File
@@ -1,130 +0,0 @@
package qiniu
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
)
type CdnManager struct {
client *client.Client
}
func NewCdnManager(mac *auth.Credentials) *CdnManager {
if mac == nil {
mac = auth.Default()
}
client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}
return &CdnManager{client: client}
}
type GetDomainListResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
Marker string `json:"marker"`
Domains []*struct {
Name string `json:"name"`
Type string `json:"type"`
CName string `json:"cname"`
OperatingState string `json:"operatingState"`
OperatingStateDesc string `json:"operatingStateDesc"`
CreateAt string `json:"createAt"`
ModifyAt string `json:"modifyAt"`
} `json:"domains"`
}
func (m *CdnManager) GetDomainList(ctx context.Context, marker string, limit int) (*GetDomainListResponse, error) {
query := url.Values{}
if marker != "" {
query.Set("marker", marker)
}
if limit > 0 {
query.Set("limit", fmt.Sprintf("%d", limit))
}
resp := new(GetDomainListResponse)
if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain?%s", query.Encode()), nil); err != nil {
return nil, err
}
return resp, nil
}
type GetDomainInfoResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
CName string `json:"cname"`
Https *struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
} `json:"https"`
PareDomain string `json:"pareDomain"`
OperationType string `json:"operationType"`
OperatingState string `json:"operatingState"`
OperatingStateDesc string `json:"operatingStateDesc"`
CreateAt string `json:"createAt"`
ModifyAt string `json:"modifyAt"`
}
func (m *CdnManager) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) {
resp := new(GetDomainInfoResponse)
if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain/%s", domain), nil); err != nil {
return nil, err
}
return resp, nil
}
type ModifyDomainHttpsConfRequest struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
type ModifyDomainHttpsConfResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}
func (m *CdnManager) ModifyDomainHttpsConf(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) {
req := &ModifyDomainHttpsConfRequest{
CertID: certId,
ForceHttps: forceHttps,
Http2Enable: http2Enable,
}
resp := new(ModifyDomainHttpsConfResponse)
if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/httpsconf", domain), nil, req); err != nil {
return nil, err
}
return resp, nil
}
type EnableDomainHttpsRequest struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
type EnableDomainHttpsResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}
func (m *CdnManager) EnableDomainHttps(ctx context.Context, domain string, certId string, forceHttps bool, http2Enable bool) (*EnableDomainHttpsResponse, error) {
req := &EnableDomainHttpsRequest{
CertID: certId,
ForceHttps: forceHttps,
Http2Enable: http2Enable,
}
resp := new(EnableDomainHttpsResponse)
if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/sslize", domain), nil, req); err != nil {
return nil, err
}
return resp, nil
}
-3
View File
@@ -1,3 +0,0 @@
// An extension SDK client for Qiniu.
// Based on github.com/qiniu/go-sdk.
package qiniu
-44
View File
@@ -1,44 +0,0 @@
package qiniu
import (
"context"
"net/http"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
)
type KodoManager struct {
client *client.Client
}
func NewKodoManager(mac *auth.Credentials) *KodoManager {
if mac == nil {
mac = auth.Default()
}
client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}
return &KodoManager{client: client}
}
type BindBucketCertRequest struct {
CertID string `json:"certid"`
Domain string `json:"domain"`
}
type BindBucketCertResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}
func (m *KodoManager) BindBucketCert(ctx context.Context, domain string, certId string) (*BindBucketCertResponse, error) {
req := &BindBucketCertRequest{
CertID: certId,
Domain: domain,
}
resp := new(BindBucketCertResponse)
if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("cert/bind"), nil, req); err != nil {
return nil, err
}
return resp, nil
}
-80
View File
@@ -1,80 +0,0 @@
package qiniu
import (
"context"
"net/http"
"net/url"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
)
type SslCertManager struct {
client *client.Client
}
func NewSslCertManager(mac *auth.Credentials) *SslCertManager {
if mac == nil {
mac = auth.Default()
}
client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}
return &SslCertManager{client: client}
}
type GetSslCertListResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
Certs []*struct {
CertID string `json:"certid"`
Name string `json:"name"`
CommonName string `json:"common_name"`
DnsNames []string `json:"dnsnames"`
CreateTime int64 `json:"create_time"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
ProductType string `json:"product_type"`
ProductShortName string `json:"product_short_name,omitempty"`
OrderId string `json:"orderid,omitempty"`
CertType string `json:"cert_type"`
Encrypt string `json:"encrypt"`
EncryptParameter string `json:"encryptParameter,omitempty"`
Enable bool `json:"enable"`
} `json:"certs"`
Marker string `json:"marker"`
}
func (m *SslCertManager) GetSslCertList(ctx context.Context, marker string, limit int32) (*GetSslCertListResponse, error) {
resp := new(GetSslCertListResponse)
if err := m.client.Call(ctx, resp, http.MethodGet, urlf("sslcert?marker=%s&limit=%d", url.QueryEscape(marker), limit), nil); err != nil {
return nil, err
}
return resp, nil
}
type UploadSslCertRequest struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Certificate string `json:"ca"`
PrivateKey string `json:"pri"`
}
type UploadSslCertResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
CertID string `json:"certID"`
}
func (m *SslCertManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) {
req := &UploadSslCertRequest{
Name: name,
CommonName: commonName,
Certificate: certificate,
PrivateKey: privateKey,
}
resp := new(UploadSslCertResponse)
if err := m.client.CallWithJson(ctx, resp, http.MethodPost, urlf("sslcert"), nil, req); err != nil {
return nil, err
}
return resp, nil
}
-14
View File
@@ -1,14 +0,0 @@
package qiniu
import (
"fmt"
"strings"
)
const qiniuHost = "https://api.qiniu.com"
func urlf(pathf string, pathargs ...any) string {
path := fmt.Sprintf(pathf, pathargs...)
path = strings.TrimPrefix(path, "/")
return qiniuHost + "/" + path
}
@@ -90,7 +90,6 @@ import AccessConfigFieldsProviderPorkbun from "./AccessConfigFieldsProviderPorkb
import AccessConfigFieldsProviderPowerDNS from "./AccessConfigFieldsProviderPowerDNS";
import AccessConfigFieldsProviderProxmoxVE from "./AccessConfigFieldsProviderProxmoxVE";
import AccessConfigFieldsProviderQingCloud from "./AccessConfigFieldsProviderQingCloud";
import AccessConfigFieldsProviderQiniu from "./AccessConfigFieldsProviderQiniu";
import AccessConfigFieldsProviderRainYun from "./AccessConfigFieldsProviderRainYun";
import AccessConfigFieldsProviderRatPanel from "./AccessConfigFieldsProviderRatPanel";
import AccessConfigFieldsProviderRegru from "./AccessConfigFieldsProviderRegru";
@@ -215,7 +214,6 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.POWERDNS]: AccessConfigFieldsProviderPowerDNS,
[ACCESS_PROVIDERS.PROXMOXVE]: AccessConfigFieldsProviderProxmoxVE,
[ACCESS_PROVIDERS.QINGCLOUD]: AccessConfigFieldsProviderQingCloud,
[ACCESS_PROVIDERS.QINIU]: AccessConfigFieldsProviderQiniu,
[ACCESS_PROVIDERS.RAINYUN]: AccessConfigFieldsProviderRainYun,
[ACCESS_PROVIDERS.RATPANEL]: AccessConfigFieldsProviderRatPanel,
[ACCESS_PROVIDERS.REGRU]: AccessConfigFieldsProviderRegru,
@@ -287,7 +285,9 @@ const useComponent = (provider: string, { initProps, deps = [] }: { initProps?:
setComponent(envelope ? <SchemaConfigFields envelope={envelope} /> : initComponent());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, envelope]);
useEffect(() => setComponent(initComponent()), deps);
useEffect(() => {
setComponent(envelope ? <SchemaConfigFields envelope={envelope} /> : initComponent());
}, deps);
return component;
};
@@ -1,64 +0,0 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const AccessConfigFormFieldsProviderQiniu = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "accessKey"]}
initialValue={initialValues.accessKey}
label={t("access.form.qiniu_access_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.qiniu_access_key.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.qiniu_access_key.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "secretKey"]}
initialValue={initialValues.secretKey}
label={t("access.form.qiniu_secret_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.qiniu_secret_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.qiniu_secret_key.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
accessKey: "",
secretKey: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
accessKey: z.string().nonempty(),
secretKey: z.string().nonempty(),
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderQiniu, {
getInitialValues,
getSchema,
});
export default _default;
@@ -69,13 +69,13 @@ const DeploymentProviderPicker = forwardRef<DeploymentProviderPickerInstance, De
<Tooltip title={t(provider.name)} mouseEnterDelay={1}>
<Typography.Text>{t(provider.name) || "\u00A0"}</Typography.Text>
</Tooltip>
{provider.source === "plugin" ? (
<Tag className="ml-1" color="orange">
{t("provider.tag.plugin")}
</Tag>
) : null}
</div>
</div>
{provider.source === "plugin" ? (
<Tag className="shrink-0" color="orange">
{t("provider.tag.plugin")}
</Tag>
) : null}
</div>
</div>
</Card>
@@ -92,9 +92,6 @@ import BizDeployNodeConfigFieldsProviderNginxProxyManager from "./BizDeployNodeC
import BizDeployNodeConfigFieldsProviderOracleCloudCertificatesMgmt from "./BizDeployNodeConfigFieldsProviderOracleCloudCertificatesMgmt";
import BizDeployNodeConfigFieldsProviderProxmoxVE from "./BizDeployNodeConfigFieldsProviderProxmoxVE";
import BizDeployNodeConfigFieldsProviderQingCloudLB from "./BizDeployNodeConfigFieldsProviderQingCloudLB";
import BizDeployNodeConfigFieldsProviderQiniuCDN from "./BizDeployNodeConfigFieldsProviderQiniuCDN";
import BizDeployNodeConfigFieldsProviderQiniuKodo from "./BizDeployNodeConfigFieldsProviderQiniuKodo";
import BizDeployNodeConfigFieldsProviderQiniuPili from "./BizDeployNodeConfigFieldsProviderQiniuPili";
import BizDeployNodeConfigFieldsProviderRainYunRCDN from "./BizDeployNodeConfigFieldsProviderRainYunRCDN";
import BizDeployNodeConfigFieldsProviderRainYunSSLCenter from "./BizDeployNodeConfigFieldsProviderRainYunSSLCenter";
import BizDeployNodeConfigFieldsProviderRatPanel from "./BizDeployNodeConfigFieldsProviderRatPanel";
@@ -143,6 +140,7 @@ import BizDeployNodeConfigFieldsProviderVolcEngineWAF from "./BizDeployNodeConfi
import BizDeployNodeConfigFieldsProviderWangsuCDN from "./BizDeployNodeConfigFieldsProviderWangsuCDN";
import BizDeployNodeConfigFieldsProviderWangsuCDNPro from "./BizDeployNodeConfigFieldsProviderWangsuCDNPro";
import BizDeployNodeConfigFieldsProviderWangsuCertificate from "./BizDeployNodeConfigFieldsProviderWangsuCertificate";
import BizDeployNodeConfigFieldsProviderWebhook from "./BizDeployNodeConfigFieldsProviderWebhook";
import BizDeployNodeConfigFieldsProviderZenlayerCDN from "./BizDeployNodeConfigFieldsProviderZenlayerCDN";
import BizDeployNodeConfigFieldsProviderZenlayerGA from "./BizDeployNodeConfigFieldsProviderZenlayerGA";
import SchemaConfigFields from "./SchemaConfigFields";
@@ -241,9 +239,6 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
[DEPLOYMENT_PROVIDERS.ORACLECLOUD_CERTIFICATESMGMT]: BizDeployNodeConfigFieldsProviderOracleCloudCertificatesMgmt,
[DEPLOYMENT_PROVIDERS.PROXMOXVE]: BizDeployNodeConfigFieldsProviderProxmoxVE,
[DEPLOYMENT_PROVIDERS.QINGCLOUD_LB]: BizDeployNodeConfigFieldsProviderQingCloudLB,
[DEPLOYMENT_PROVIDERS.QINIU_CDN]: BizDeployNodeConfigFieldsProviderQiniuCDN,
[DEPLOYMENT_PROVIDERS.QINIU_KODO]: BizDeployNodeConfigFieldsProviderQiniuKodo,
[DEPLOYMENT_PROVIDERS.QINIU_PILI]: BizDeployNodeConfigFieldsProviderQiniuPili,
[DEPLOYMENT_PROVIDERS.RAINYUN_RCDN]: BizDeployNodeConfigFieldsProviderRainYunRCDN,
[DEPLOYMENT_PROVIDERS.RAINYUN_SSLCENTER]: BizDeployNodeConfigFieldsProviderRainYunSSLCenter,
[DEPLOYMENT_PROVIDERS.RATPANEL]: BizDeployNodeConfigFieldsProviderRatPanel,
@@ -292,6 +287,7 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
[DEPLOYMENT_PROVIDERS.WANGSU_CDN]: BizDeployNodeConfigFieldsProviderWangsuCDN,
[DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO]: BizDeployNodeConfigFieldsProviderWangsuCDNPro,
[DEPLOYMENT_PROVIDERS.WANGSU_CERTIFICATE]: BizDeployNodeConfigFieldsProviderWangsuCertificate,
[DEPLOYMENT_PROVIDERS.WEBHOOK]: BizDeployNodeConfigFieldsProviderWebhook,
[DEPLOYMENT_PROVIDERS.ZENLAYER_CDN]: BizDeployNodeConfigFieldsProviderZenlayerCDN,
[DEPLOYMENT_PROVIDERS.ZENLAYER_GA]: BizDeployNodeConfigFieldsProviderZenlayerGA,
};
@@ -333,7 +329,9 @@ const useComponent = (provider: string, { initProps, deps = [] }: { initProps?:
setComponent(envelope ? <SchemaConfigFields envelope={envelope} /> : initComponent());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provider, envelope]);
useEffect(() => setComponent(initComponent()), deps);
useEffect(() => {
setComponent(envelope ? <SchemaConfigFields envelope={envelope} /> : initComponent());
}, deps);
return component;
};
@@ -1,107 +0,0 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { isDomain } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const;
const DOMAIN_MATCH_PATTERN_WILDCARD = "wildcard" as const;
const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const;
const BizDeployNodeConfigFieldsProviderQiniuCDN = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const initialValues = getInitialValues();
const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true });
return (
<>
<Form.Item
name={[parentNamePath, "domainMatchPattern"]}
initialValue={initialValues.domainMatchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
extra={
fieldDomainMatchPattern === DOMAIN_MATCH_PATTERN_EXACT ? (
<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.shared_domain_match_pattern.option.exact.help.wildcard") }}></span>
) : (
void 0
)
}
rules={[formRule]}
>
<Radio.Group
options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_WILDCARD, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.qiniu_cdn_domain.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.qiniu_cdn_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,
domain: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z
.object({
domainMatchPattern: z.string().nonempty().default(DOMAIN_MATCH_PATTERN_EXACT),
domain: z.string().nullish(),
})
.superRefine((values, ctx) => {
if (values.domainMatchPattern) {
switch (values.domainMatchPattern) {
case DOMAIN_MATCH_PATTERN_EXACT:
case DOMAIN_MATCH_PATTERN_WILDCARD:
{
const scDomain = z.string().refine((v) => isDomain(v, { allowWildcard: true }), t("common.errmsg.domain_invalid"));
const spDomain = scDomain.safeParse(values.domain);
if (!spDomain.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spDomain.error).errors.join(),
path: ["domain"],
});
}
}
break;
}
}
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuCDN, {
getInitialValues,
getSchema,
});
export default _default;
@@ -1,64 +0,0 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { isDomain } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const BizDeployNodeConfigFieldsProviderQiniuKodo = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "bucket"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.qiniu_kodo_bucket.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.qiniu_kodo_bucket.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.qiniu_kodo_domain.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.qiniu_kodo_domain.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
bucket: "",
domain: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z.object({
bucket: z.string().nonempty(),
domain: z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid")),
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuKodo, {
getInitialValues,
getSchema,
});
export default _default;
@@ -1,110 +0,0 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { isDomain } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const DOMAIN_MATCH_PATTERN_EXACT = "exact" as const;
const DOMAIN_MATCH_PATTERN_CERTSAN = "certsan" as const;
const BizDeployNodeConfigFieldsProviderQiniuPili = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const initialValues = getInitialValues();
const fieldDomainMatchPattern = Form.useWatch([parentNamePath, "domainMatchPattern"], { form: formInst, preserve: true });
return (
<>
<Form.Item
name={[parentNamePath, "hub"]}
initialValue={initialValues.hub}
label={t("workflow_node.deploy.form.qiniu_pili_hub.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.qiniu_pili_hub.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.qiniu_pili_hub.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "domainMatchPattern"]}
initialValue={initialValues.domainMatchPattern}
label={t("workflow_node.deploy.form.shared_domain_match_pattern.label")}
rules={[formRule]}
>
<Radio.Group
options={[DOMAIN_MATCH_PATTERN_EXACT, DOMAIN_MATCH_PATTERN_CERTSAN].map((s) => ({
label: t(`workflow_node.deploy.form.shared_domain_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldDomainMatchPattern !== DOMAIN_MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "domain"]}
initialValue={initialValues.domain}
label={t("workflow_node.deploy.form.qiniu_pili_domain.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.deploy.form.qiniu_pili_domain.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
hub: "",
domainMatchPattern: DOMAIN_MATCH_PATTERN_EXACT,
domain: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z
.object({
hub: z.string().nonempty(),
domainMatchPattern: z.string().nonempty().default(DOMAIN_MATCH_PATTERN_EXACT),
domain: z.string().nullish(),
})
.superRefine((values, ctx) => {
if (values.domainMatchPattern) {
switch (values.domainMatchPattern) {
case DOMAIN_MATCH_PATTERN_EXACT:
{
const scDomain = z.string().refine((v) => isDomain(v), t("common.errmsg.domain_invalid"));
const spDomain = scDomain.safeParse(values.domain);
if (!spDomain.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spDomain.error).errors.join(),
path: ["domain"],
});
}
}
break;
}
}
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderQiniuPili, {
getInitialValues,
getSchema,
});
export default _default;
@@ -0,0 +1,94 @@
import { getI18n, useTranslation } from "react-i18next";
import { IconBulb } from "@tabler/icons-react";
import { Button, Form, Input, Popover } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import CodeTextInput from "@/components/CodeTextInput";
import { isJsonObject } from "@/utils/validator";
import { useFormNestedFieldsContext } from "./_context";
const BizDeployNodeConfigFieldsProviderWebhook = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance();
const initialValues = getInitialValues();
const handleWebhookDataBlur = () => {
const value = formInst.getFieldValue([parentNamePath, "webhookData"]);
try {
const json = JSON.stringify(JSON.parse(value), null, 2);
formInst.setFieldValue([parentNamePath, "webhookData"], json);
} catch {
return;
}
};
return (
<>
<Form.Item label={t("workflow_node.deploy.form.webhook_data.label")} extra={t("workflow_node.deploy.form.webhook_data.help")}>
<div className="absolute -top-1.5 right-0 -translate-y-full">
<Popover content={<div dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.vartips") }} />} mouseEnterDelay={1}>
<Button color="default" size="small" variant="link">
<IconBulb size="1.25em" />
</Button>
</Popover>
</div>
<Form.Item name={[parentNamePath, "webhookData"]} initialValue={initialValues.webhookData} noStyle rules={[formRule]}>
<CodeTextInput
lineWrapping={false}
height="auto"
minHeight="64px"
maxHeight="256px"
language="json"
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
onBlur={handleWebhookDataBlur}
/>
</Form.Item>
</Form.Item>
<Form.Item name={[parentNamePath, "timeout"]} label={t("workflow_node.deploy.form.webhook_timeout.label")} rules={[formRule]}>
<Input
type="number"
allowClear
min={0}
max={3600}
placeholder={t("workflow_node.deploy.form.webhook_timeout.placeholder")}
suffix={t("workflow_node.deploy.form.webhook_timeout.unit")}
/>
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z.object({
webhookData: z
.string()
.nullish()
.refine((v) => {
if (!v) return true;
return isJsonObject(v);
}, t("common.errmsg.json_invalid")),
timeout: z.coerce.number().int().gt(0).or(z.literal("")).nullish(),
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderWebhook, {
getInitialValues,
getSchema,
});
export default _default;
@@ -1,6 +1,7 @@
import { z } from "zod";
import type { ProviderSchemaEnvelope, SchemaColumn, SchemaCondition, SchemaValidationRule } from "@/api/providerschema";
import i18n from "@/i18n";
import {
isCron,
isDomain,
@@ -17,7 +18,7 @@ import {
const isEmpty = (v: unknown) => v == null || v === "";
export const matchCondition = (cond: SchemaCondition, value: unknown): boolean => {
const present = cond.values.includes(String(value));
const present = (cond.values ?? []).includes(String(value));
switch (cond.op) {
case "eq":
case "in":
@@ -98,7 +99,7 @@ const fieldBaseType = (col: SchemaColumn): z.ZodType => {
const applyRules = (schema: z.ZodType, rules: SchemaValidationRule[], allowEmpty: boolean): z.ZodType => {
for (const rule of rules) {
const ok = (v: unknown) => (allowEmpty && isEmpty(v)) || runValidator(rule.validator, rule.params, v);
schema = schema.refine(ok, { message: `common.errmsg.${rule.validator}_invalid` });
schema = schema.refine(ok, { message: i18n.t(`common.errmsg.${rule.validator}_invalid`) });
}
return schema;
};
@@ -159,13 +160,13 @@ const buildPlainObject = (columns: SchemaColumn[]): z.ZodType => {
for (const cond of col.requiredWhen ?? []) {
if (!matchCondition(cond, record[cond.field])) continue;
if (isEmpty(value)) {
ctx.addIssue({ code: "custom", message: `common.errmsg.required`, path: [col.name] });
ctx.addIssue({ code: "custom", message: i18n.t("common.errmsg.required"), path: [col.name] });
}
}
for (const rule of col.validateWhen ?? []) {
if (!matchCondition(rule, record[rule.field])) continue;
if (!isEmpty(value) && !runValidator(rule.validator, rule.params, value)) {
ctx.addIssue({ code: "custom", message: `common.errmsg.${rule.validator}_invalid`, path: [col.name] });
ctx.addIssue({ code: "custom", message: i18n.t(`common.errmsg.${rule.validator}_invalid`), path: [col.name] });
}
}
}
+2 -8
View File
@@ -108,7 +108,6 @@ export const ACCESS_PROVIDERS = Object.freeze({
POWERDNS: "powerdns",
PROXMOXVE: "proxmoxve",
QINGCLOUD: "qingcloud",
QINIU: "qiniu",
RAINYUN: "rainyun",
RATPANEL: "ratpanel",
REGRU: "regru",
@@ -201,7 +200,6 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.BAISHAN, "provider.baishan", "/imgs/providers/baishan.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.DOGECLOUD, "provider.dogecloud", "/imgs/providers/dogecloud.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.KSYUN, "provider.ksyun", "/imgs/providers/ksyun.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.QINIU, "provider.qiniu", "/imgs/providers/qiniu.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.UPYUN, "provider.upyun", "/imgs/providers/upyun.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.WANGSU, "provider.wangsu", "/imgs/providers/wangsu.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.MOHUA, "provider.mohua", "/imgs/providers/mohua.png", [ACCESS_USAGES.HOSTING]],
@@ -704,9 +702,6 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
ORACLECLOUD_CERTIFICATESMGMT: `${ACCESS_PROVIDERS.ORACLECLOUD}-certificatesmgmt`,
PROXMOXVE: `${ACCESS_PROVIDERS.PROXMOXVE}`,
QINGCLOUD_LB: `${ACCESS_PROVIDERS.QINGCLOUD}-lb`,
QINIU_CDN: `${ACCESS_PROVIDERS.QINIU}-cdn`,
QINIU_KODO: `${ACCESS_PROVIDERS.QINIU}-kodo`,
QINIU_PILI: `${ACCESS_PROVIDERS.QINIU}-pili`,
RAINYUN_RCDN: `${ACCESS_PROVIDERS.RAINYUN}-rcdn`,
RAINYUN_SSLCENTER: `${ACCESS_PROVIDERS.RAINYUN}-sslcenter`,
RATPANEL: `${ACCESS_PROVIDERS.RATPANEL}`,
@@ -757,6 +752,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
WANGSU_CDN: `${ACCESS_PROVIDERS.WANGSU}-cdn`,
WANGSU_CDNPRO: `${ACCESS_PROVIDERS.WANGSU}-cdnpro`,
WANGSU_CERTIFICATE: `${ACCESS_PROVIDERS.WANGSU}-certificate`,
WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`,
ZENLAYER_CDN: `${ACCESS_PROVIDERS.ZENLAYER}-cdn`,
ZENLAYER_GA: `${ACCESS_PROVIDERS.ZENLAYER}-ga`,
} as const);
@@ -795,6 +791,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.SSH, "provider.ssh", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.FTP, "provider.ftp", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET, "provider.kubernetes_secret", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.WEBHOOK, "provider.webhook", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.S3, "provider.s3_upload", DEPLOYMENT_CATEGORIES.STORAGE],
[DEPLOYMENT_PROVIDERS.ALIYUN_OSS, "provider.aliyun_oss", DEPLOYMENT_CATEGORIES.STORAGE],
[DEPLOYMENT_PROVIDERS.ALIYUN_CDN, "provider.aliyun_cdn", DEPLOYMENT_CATEGORIES.CDN],
@@ -909,9 +906,6 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.UCLOUD_UPATHX, "provider.ucloud_upathx", DEPLOYMENT_CATEGORIES.ACCELERATOR],
[DEPLOYMENT_PROVIDERS.BAISHAN_CDN, "provider.baishan_cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN, "provider.dogecloud_cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.QINIU_KODO, "provider.qiniu_kodo", DEPLOYMENT_CATEGORIES.STORAGE],
[DEPLOYMENT_PROVIDERS.QINIU_CDN, "provider.qiniu_cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.QINIU_PILI, "provider.qiniu_pili", DEPLOYMENT_CATEGORIES.AV],
[DEPLOYMENT_PROVIDERS.WANGSU_CDN, "provider.wangsu_cdn", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO, "provider.wangsu_cdnpro", DEPLOYMENT_CATEGORIES.CDN],
[DEPLOYMENT_PROVIDERS.WANGSU_CERTIFICATE, "provider.wangsu_certificate_upload", DEPLOYMENT_CATEGORIES.SSL],
-10
View File
@@ -1128,16 +1128,6 @@
"placeholder": "Please enter QingCloud SecretAccessKey",
"tooltip": "For more information, see <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>"
},
"qiniu_access_key": {
"label": "Qiniu AccessKey",
"placeholder": "Please enter Qiniu AccessKey",
"tooltip": "For more information, see <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>"
},
"qiniu_secret_key": {
"label": "Qiniu SecretKey",
"placeholder": "Please enter Qiniu SecretKey",
"tooltip": "For more information, see <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>"
},
"rainyun_api_key": {
"label": "Rain Yun API key",
"placeholder": "Please enter Rain Yun API key",
@@ -224,10 +224,6 @@
"qingcloud": "QingCloud",
"qingcloud_dns": "QingCloud - DNS",
"qingcloud_lb": "QingCloud - LBC (Load Balancer Cluster)",
"qiniu": "Qiniu",
"qiniu_cdn": "Qiniu - CDN (Content Delivery Network)",
"qiniu_kodo": "Qiniu - Kodo",
"qiniu_pili": "Qiniu - Pili",
"rainyun": "Rain Yun",
"rainyun_rcdn": "Rain Yun - RCDN (Content Delivery Network)",
"rainyun_sslcenter_upload": "Rain Yun - Upload to SSL Certificate Center",
@@ -457,6 +457,17 @@
}
},
"form": {
"webhook_data": {
"label": "Webhook data (Optional)",
"placeholder": "Please enter Webhook data",
"help": "Notes: Leave it blank to use the default Webhook data provided by the selected credential.",
"vartips": "Supported variables: <br><ol style=\"list-style: disc;\"><li><strong>${CERTIMATE_DEPLOYER_COMMONNAME}</strong>: <br>The primary domain or IP address of the certificate.</li><li><strong>${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}</strong>: <br>The domains or IP addresses of the certificate, separated by semicolons.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE}</strong>: <br>The PEM format content of the certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}</strong>: <br>The PEM format content of the server certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}</strong>: <br>The PEM format content of the intermediate CA certificate file.</li><li><strong>${CERTIMATE_DEPLOYER_PRIVATEKEY}</strong>: <br>The PEM format content of the private key file.</li></ol>"
},
"webhook_timeout": {
"label": "Webhook timeout (Optional)",
"placeholder": "Please enter Webhook timeout",
"unit": "seconds"
},
"certificate_output_node_id": {
"label": "Certificate to deploy",
"placeholder": "Please select certificate to deploy",
@@ -2049,27 +2060,6 @@
"placeholder": "Please enter QingCloud LBC listener ID",
"tooltip": "For more information, see <a href=\"https://console.qingcloud.com/pek3a/loadbalancers/\" target=\"_blank\">https://console.qingcloud.com/pek3a/loadbalancers/</a>"
},
"qiniu_cdn_domain": {
"label": "Qiniu CDN domain",
"placeholder": "Please enter Qiniu CDN domain name"
},
"qiniu_kodo_bucket": {
"label": "Qiniu Kodo bucket",
"placeholder": "Please enter Qiniu Kodo bucket name"
},
"qiniu_kodo_domain": {
"label": "Qiniu Kodo custom domain",
"placeholder": "Please enter Qiniu Kodo bucket custom domain name"
},
"qiniu_pili_hub": {
"label": "Qiniu Pili hub",
"placeholder": "Please enter Qiniu Pili hub name",
"tooltip": "For more information, see <a href=\"https://portal.qiniu.com/hub\" target=\"_blank\">https://portal.qiniu.com/hub</a>"
},
"qiniu_pili_domain": {
"label": "Qiniu Pili streaming domain",
"placeholder": "Please enter Qiniu Pili streaming domain name"
},
"rainyun_rcdn_instance_id": {
"label": "Rain Yun RCDN instance ID",
"placeholder": "Please enter Rain Yun RCDN instance ID",
-10
View File
@@ -1128,16 +1128,6 @@
"placeholder": "请输入青云 SecretAccessKey",
"tooltip": "这是什么?请参阅 <a href=\"https://console.qingcloud.com/access_keys/\" target=\"_blank\">https://console.qingcloud.com/access_keys/</a>"
},
"qiniu_access_key": {
"label": "七牛云 AccessKey",
"placeholder": "请输入七牛云 AccessKey",
"tooltip": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>"
},
"qiniu_secret_key": {
"label": "七牛云 SecretKey",
"placeholder": "请输入七牛云 SecretKey",
"tooltip": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/\" target=\"_blank\">https://portal.qiniu.com/</a>"
},
"rainyun_api_key": {
"label": "雨云 API 密钥",
"placeholder": "请输入雨云 API 密钥",
@@ -224,10 +224,6 @@
"qingcloud": "青云",
"qingcloud_dns": "青云 - 云解析 DNS",
"qingcloud_lb": "青云 - 负载均衡 LB",
"qiniu": "七牛云",
"qiniu_cdn": "七牛云 - 内容分发网络 CDN",
"qiniu_kodo": "七牛云 - 对象存储 Kodo",
"qiniu_pili": "七牛云 - 视频直播 Pili",
"rainyun": "雨云",
"rainyun_rcdn": "雨云 - 雨盾 CDN",
"rainyun_sslcenter_upload": "雨云 - 上传到 SSL 证书中心",
@@ -456,6 +456,17 @@
}
},
"form": {
"webhook_data": {
"label": "Webhook 回调数据(可选)",
"placeholder": "请输入 Webhook 回调数据以覆盖默认值",
"help": "提示:不填写时,将使用所选提供商授权的默认 Webhook 回调数据。",
"vartips": "支持的变量:<br><ol style=\"list-style: disc;\"><li><strong>${CERTIMATE_DEPLOYER_COMMONNAME}</strong>:<br>证书的主域名或 IP。</li><li><strong>${CERTIMATE_DEPLOYER_SUBJECTALTNAMES}</strong>:<br>证书的多域名或 IP,以半角分号隔开。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE}</strong>:<br>证书文件 PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_SERVER}</strong>:<br>证书文件(仅含服务器证书)PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_CERTIFICATE_INTERMEDIA}</strong>:<br>证书文件(仅含中间证书)PEM 格式内容。</li><li><strong>${CERTIMATE_DEPLOYER_PRIVATEKEY}</strong>:<br>私钥文件 PEM 格式内容。</li></ol>"
},
"webhook_timeout": {
"label": "Webhook 超时时间(可选)",
"placeholder": "请输入 Webhook 超时时间",
"unit": "秒"
},
"certificate_output_node_id": {
"label": "待部署证书",
"placeholder": "请选择待部署证书",
@@ -2048,27 +2059,6 @@
"placeholder": "请输入青云 LB 监听器 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://console.qingcloud.com/pek3a/loadbalancers/\" target=\"_blank\">https://console.qingcloud.com/pek3a/loadbalancers/</a>"
},
"qiniu_cdn_domain": {
"label": "七牛云 CDN 加速域名",
"placeholder": "请输入七牛云 CDN 加速域名"
},
"qiniu_kodo_bucket": {
"label": "七牛云对象存储桶名",
"placeholder": "请输入七牛云对象存储桶名"
},
"qiniu_kodo_domain": {
"label": "七牛云对象存储自定义域名",
"placeholder": "请输入七牛云对象存储自定义域名"
},
"qiniu_pili_hub": {
"label": "七牛云视频直播空间名",
"placeholder": "请输入七牛云视频直播空间名",
"tooltip": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/hub\" target=\"_blank\">https://portal.qiniu.com/hub</a>"
},
"qiniu_pili_domain": {
"label": "七牛云视频直播流域名",
"placeholder": "请输入七牛云视频直播流域名"
},
"rainyun_rcdn_instance_id": {
"label": "雨云 RCDN 实例 ID",
"placeholder": "请输入雨云 RCDN 实例 ID",