From b43fcc3b613e0317c7e64d87bc8ac3f625c74e75 Mon Sep 17 00:00:00 2001
From: JiangJam <39955422+JiangJamm@users.noreply.github.com>
Date: Mon, 30 Jun 2025 15:56:50 +0800
Subject: [PATCH 1/8] fix: wrong path when uploading certificates from Windows
to Unix
fix: https://github.com/certimate-go/certimate/issues/805
---
pkg/core/ssl-deployer/providers/ssh/ssh.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/core/ssl-deployer/providers/ssh/ssh.go b/pkg/core/ssl-deployer/providers/ssh/ssh.go
index 94d0b6bf9..7cbe7c8f3 100644
--- a/pkg/core/ssl-deployer/providers/ssh/ssh.go
+++ b/pkg/core/ssl-deployer/providers/ssh/ssh.go
@@ -419,7 +419,7 @@ func writeFileWithSFTP(sshCli *ssh.Client, path string, data []byte) error {
}
defer sftpCli.Close()
- if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
+ if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
}
From 1e4cd2b9d5fb417abe1daac59b35b711d74dfd62 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=90=83=E7=93=9C=E7=9A=84=E6=98=9F=E6=A0=B8=E7=B2=BE?=
<129462191+cgxhj@users.noreply.github.com>
Date: Tue, 8 Jul 2025 21:22:29 +0800
Subject: [PATCH 2/8] feat: support configuring multiple domains deployment to
tencentcloud edgeone
---
internal/deployer/providers.go | 2 +-
migrations/1742209200_upgrade.go | 10 +-
migrations/1751961600_upgrade.go | 112 ++++++++++++++++++
.../tencentcloud-eo/tencentcloud_eo.go | 10 +-
.../tencentcloud-eo/tencentcloud_eo_test.go | 10 +-
...ployNodeConfigFormTencentCloudEOConfig.tsx | 29 +++--
.../i18n/locales/en/nls.workflow.nodes.json | 8 +-
.../i18n/locales/zh/nls.workflow.nodes.json | 8 +-
8 files changed, 159 insertions(+), 30 deletions(-)
create mode 100644 migrations/1751961600_upgrade.go
diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go
index 0a48d6ca7..7eb583726 100644
--- a/internal/deployer/providers.go
+++ b/internal/deployer/providers.go
@@ -1202,7 +1202,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy
SecretKey: access.SecretKey,
Endpoint: xmaps.GetString(options.ProviderServiceConfig, "endpoint"),
ZoneId: xmaps.GetString(options.ProviderServiceConfig, "zoneId"),
- Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"),
+ Domains: xslices.Filter(strings.Split(xmaps.GetString(options.ProviderServiceConfig, "domains"), ";"), func(s string) bool { return s != "" }),
})
return deployer, err
diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go
index 1bc0d4827..1cc05cf3f 100644
--- a/migrations/1742209200_upgrade.go
+++ b/migrations/1742209200_upgrade.go
@@ -270,12 +270,12 @@ func init() {
Id string `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
- Config map[string]any `json:"config"`
- Inputs []map[string]any `json:"inputs"`
- Outputs []map[string]any `json:"outputs"`
+ Config map[string]any `json:"config,omitempty"`
+ Inputs []map[string]any `json:"inputs,omitempty"`
+ Outputs []map[string]any `json:"outputs,omitempty"`
Next *dWorkflowNode `json:"next,omitempty"`
- Branches []dWorkflowNode `json:"branches,omitempty"`
- Validated bool `json:"validated"`
+ Branches []*dWorkflowNode `json:"branches,omitempty"`
+ Validated bool `json:"validated,omitempty"`
}
for _, workflowRun := range workflowRuns {
diff --git a/migrations/1751961600_upgrade.go b/migrations/1751961600_upgrade.go
new file mode 100644
index 000000000..91ac39d26
--- /dev/null
+++ b/migrations/1751961600_upgrade.go
@@ -0,0 +1,112 @@
+package migrations
+
+import (
+ "github.com/pocketbase/pocketbase/core"
+ m "github.com/pocketbase/pocketbase/migrations"
+)
+
+func init() {
+ m.Register(func(app core.App) error {
+ tracer := NewTracer("(v0.3)1751961600")
+ tracer.Printf("go ...")
+
+ // migrate data
+ {
+ workflows, err := app.FindAllRecords("workflow")
+ if err != nil {
+ return err
+ }
+
+ type dWorkflowNode struct {
+ Id string `json:"id"`
+ Type string `json:"type"`
+ Name string `json:"name"`
+ Config map[string]any `json:"config,omitempty"`
+ Inputs []map[string]any `json:"inputs,omitempty"`
+ Outputs []map[string]any `json:"outputs,omitempty"`
+ Next *dWorkflowNode `json:"next,omitempty"`
+ Branches []*dWorkflowNode `json:"branches,omitempty"`
+ Validated bool `json:"validated,omitempty"`
+ }
+
+ deepChangeFn := func(node *dWorkflowNode) bool {
+ stack := []*dWorkflowNode{node}
+
+ for len(stack) > 0 {
+ current := stack[len(stack)-1]
+ stack = stack[:len(stack)-1]
+
+ if current.Type == "deploy" {
+ configMap := current.Config
+ if configMap != nil {
+ if provider, ok := configMap["provider"]; ok {
+ if provider.(string) == "tencentcloud-eo" {
+ if providerConfig, ok := configMap["providerConfig"]; ok {
+ if providerConfigMap, ok := providerConfig.(map[string]any); ok {
+ if _, ok := providerConfigMap["domain"]; ok {
+ providerConfigMap["domains"] = providerConfigMap["domain"]
+ delete(providerConfigMap, "domain")
+ configMap["providerConfig"] = providerConfigMap
+ return true
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if current.Next != nil {
+ stack = append(stack, current.Next)
+ }
+
+ if current.Branches != nil {
+ for i := len(current.Branches) - 1; i >= 0; i-- {
+ stack = append(stack, current.Branches[i])
+ }
+ }
+ }
+
+ return false
+ }
+
+ for _, workflow := range workflows {
+ changed := false
+
+ rootNodeContent := &dWorkflowNode{}
+ if err := workflow.UnmarshalJSONField("content", rootNodeContent); err != nil {
+ return err
+ } else {
+ if deepChangeFn(rootNodeContent) {
+ workflow.Set("content", rootNodeContent)
+ changed = true
+ }
+ }
+
+ rootNodeDraft := &dWorkflowNode{}
+ if err := workflow.UnmarshalJSONField("draft", rootNodeDraft); err != nil {
+ return err
+ } else {
+ if deepChangeFn(rootNodeDraft) {
+ workflow.Set("draft", rootNodeDraft)
+ changed = true
+ }
+ }
+
+ if changed {
+ err = app.Save(workflow)
+ if err != nil {
+ return err
+ }
+
+ tracer.Printf("record #%s in collection '%s' updated", workflow.Id, workflow.Collection().Name)
+ }
+ }
+ }
+
+ tracer.Printf("done")
+ return nil
+ }, func(app core.App) error {
+ return nil
+ })
+}
diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go
index 5540c8c84..e2fcaf7d4 100644
--- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go
+++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go
@@ -25,8 +25,8 @@ type SSLDeployerProviderConfig struct {
Endpoint string `json:"endpoint,omitempty"`
// 站点 ID。
ZoneId string `json:"zoneId"`
- // 加速域名(支持泛域名)。
- Domain string `json:"domain"`
+ // 加速域名列表(支持泛域名)。
+ Domains []string `json:"domains"`
}
type SSLDeployerProvider struct {
@@ -82,8 +82,8 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke
if d.config.ZoneId == "" {
return nil, errors.New("config `zoneId` is required")
}
- if d.config.Domain == "" {
- return nil, errors.New("config `domain` is required")
+ if len(d.config.Domains) == 0 {
+ return nil, errors.New("config `domains` is required")
}
// 上传证书
@@ -99,7 +99,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke
modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
- modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{d.config.Domain})
+ modifyHostsCertificateReq.Hosts = common.StringPtrs(d.config.Domains)
modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq)
d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp))
diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go
index cbca3fe3a..51a440c1b 100644
--- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go
+++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go
@@ -17,7 +17,7 @@ var (
fSecretId string
fSecretKey string
fZoneId string
- fDomain string
+ fDomains string
)
func init() {
@@ -28,7 +28,7 @@ func init() {
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "")
- flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+ flag.StringVar(&fDomains, argsPrefix+"DOMAINS", "", "")
}
/*
@@ -40,7 +40,7 @@ Shell command to run this test:
--CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_SECRETID="your-secret-id" \
--CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_SECRETKEY="your-secret-key" \
--CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_ZONEID="your-zone-id" \
- --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_DOMAIN="example.com"
+ --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_DOMAINS="example.com"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
@@ -53,14 +53,14 @@ func TestDeploy(t *testing.T) {
fmt.Sprintf("SECRETID: %v", fSecretId),
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
fmt.Sprintf("ZONEID: %v", fZoneId),
- fmt.Sprintf("DOMAIN: %v", fDomain),
+ fmt.Sprintf("DOMAINS: %v", fDomains),
}, "\n"))
deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
SecretId: fSecretId,
SecretKey: fSecretKey,
ZoneId: fZoneId,
- Domain: fDomain,
+ Domains: strings.Split(fDomains, ";"),
})
if err != nil {
t.Errorf("err: %+v", err)
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx
index b5daf7158..7702c39ae 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudEOConfig.tsx
@@ -4,11 +4,12 @@ import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod/v4";
import { validDomainName } from "@/utils/validators";
+import MultipleSplitValueInput from "@/components/MultipleSplitValueInput";
type DeployNodeConfigFormTencentCloudEOConfigFieldValues = Nullish<{
endpoint?: string;
zoneId: string;
- domain: string;
+ domains: string;
}>;
export type DeployNodeConfigFormTencentCloudEOConfigProps = {
@@ -23,6 +24,8 @@ const initFormModel = (): DeployNodeConfigFormTencentCloudEOConfigFieldValues =>
return {};
};
+const MULTIPLE_INPUT_SEPARATOR = ";";
+
const DeployNodeConfigFormTencentCloudEOConfig = ({
form: formInst,
formName,
@@ -37,9 +40,14 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({
zoneId: z
.string(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder"))
.nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")),
- domain: z
- .string(t("workflow_node.deploy.form.tencentcloud_eo_domain.placeholder"))
- .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
+ domains: z
+ .string(t("workflow_node.deploy.form.tencentcloud_eo_domains.placeholder"))
+ .refine((v) => {
+ if (!v) return false;
+ return String(v)
+ .split(MULTIPLE_INPUT_SEPARATOR)
+ .every((e) => validDomainName(e, { allowWildcard: true }));
+ }, t("common.errmsg.domain_invalid")),
});
const formRule = createSchemaFieldRule(formSchema);
@@ -75,12 +83,17 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({
}
+ tooltip={}
>
-
+
);
diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json
index 122542213..df7f19152 100644
--- a/ui/src/i18n/locales/en/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json
@@ -727,9 +727,11 @@
"workflow_node.deploy.form.tencentcloud_eo_zone_id.label": "Tencent Cloud EdgeOne zone ID",
"workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID",
"workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip": "For more information, see https://console.tencentcloud.com/edgeone",
- "workflow_node.deploy.form.tencentcloud_eo_domain.label": "Tencent Cloud EdgeOne domain",
- "workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "Please enter Tencent Cloud EdgeOne domain name",
- "workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "For more information, see https://console.tencentcloud.com/edgeone",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.label": "Tencent Cloud EdgeOne domains",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.placeholder": "Please enter Tencent Cloud EdgeOne domain names (separated by semicolons)",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.tooltip": "For more information, see https://console.tencentcloud.com/edgeone",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "Change Tencent Cloud EdgeOne domain",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "Please enter Tencent Cloud EdgeOne domain name",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "Tencent Cloud GAAP API endpoint (Optional)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "- gaap.intl.tencentcloudapi.com for Tencent Cloud International
- gaap.tencentcloudapi.com for Tencent Cloud in China
",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
index a3f4278e7..061189dfd 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
@@ -725,9 +725,11 @@
"workflow_node.deploy.form.tencentcloud_eo_zone_id.label": "腾讯云 EdgeOne 站点 ID",
"workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID",
"workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone",
- "workflow_node.deploy.form.tencentcloud_eo_domain.label": "腾讯云 EdgeOne 加速域名",
- "workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)",
- "workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.label": "腾讯云 EdgeOne 加速域名",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名;多个值请用半角分号隔开)",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "修改腾讯云 EdgeOne 加速域名",
+ "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "腾讯云 GAAP 接口端点(可选)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com)",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/608/36934
国际站用户请填写 gaap.intl.tencentcloudapi.com。",
From b6c59c57ded445c0e8916a7ff5e63f6759c13bf7 Mon Sep 17 00:00:00 2001
From: Fu Diwei
Date: Sun, 6 Jul 2025 22:47:13 +0800
Subject: [PATCH 3/8] fix: #852
---
pkg/sdk3rd/baishan/client.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pkg/sdk3rd/baishan/client.go b/pkg/sdk3rd/baishan/client.go
index fb9a1df79..2978cd33e 100644
--- a/pkg/sdk3rd/baishan/client.go
+++ b/pkg/sdk3rd/baishan/client.go
@@ -22,7 +22,7 @@ func NewClient(apiToken string) (*Client, error) {
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", "certimate").
- SetHeader("Token", apiToken)
+ SetQueryParam("token", apiToken)
return &Client{client}, nil
}
From 72cfe921d3004544679c9bdfd2ac497ebcb87204 Mon Sep 17 00:00:00 2001
From: Fu Diwei
Date: Sun, 6 Jul 2025 23:07:07 +0800
Subject: [PATCH 4/8] fix: #849
---
pkg/sdk3rd/upyun/console/client.go | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/pkg/sdk3rd/upyun/console/client.go b/pkg/sdk3rd/upyun/console/client.go
index 7af3e7ae7..0c54ea613 100644
--- a/pkg/sdk3rd/upyun/console/client.go
+++ b/pkg/sdk3rd/upyun/console/client.go
@@ -103,8 +103,11 @@ func (c *Client) doRequestWithResult(req *resty.Request, res apiResponse) (*rest
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err)
} else {
- if tdata := res.GetData(); tdata == nil {
- return resp, fmt.Errorf("sdkerr: empty data")
+ tresp := &apiResponseBase{}
+ if err := json.Unmarshal(resp.Body(), &tresp); err != nil {
+ return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err)
+ } else if tdata := tresp.GetData(); tdata == nil {
+ return resp, fmt.Errorf("sdkerr: received empty data")
} else if terrcode := tdata.GetErrorCode(); terrcode != 0 {
return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", terrcode, tdata.GetMessage())
}
From 424e2e7710f2c47f6f1dd54ce31a3f0c8655a079 Mon Sep 17 00:00:00 2001
From: Fu Diwei
Date: Mon, 7 Jul 2025 10:04:14 +0800
Subject: [PATCH 5/8] build: fix README release path
---
.github/workflows/release.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ed10b5ee0..4f8efd8b0 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -189,6 +189,8 @@ jobs:
cp ../README.md "$tmpdir/README_zhCN.md"
cp ../README_EN.md "$tmpdir/README_enUS.md"
cp ../CHANGELOG.md "$tmpdir/CHANGELOG.md"
+ sed -i 's/README_EN\.md/README_enUS.md/g' "$tmpdir/README_zhCN.md"
+ sed -i 's/README\.md/README_zhCN.md/g' "$tmpdir/README_enUS.md"
if [[ "$bin" == *".exe" ]]; then
zip -j "${bin%.exe}.zip" "$tmpdir"/*
From 7d272a6c524daf05c770161967fc5c1b4d8099ca Mon Sep 17 00:00:00 2001
From: Fu Diwei
Date: Mon, 7 Jul 2025 17:27:04 +0800
Subject: [PATCH 6/8] fix: missing pfx certificiate chains
---
pkg/utils/cert/common.go | 16 +++++++++
pkg/utils/cert/extractor.go | 26 +++++++--------
pkg/utils/cert/transformer.go | 62 +++++++++++++++++++++++------------
3 files changed, 68 insertions(+), 36 deletions(-)
diff --git a/pkg/utils/cert/common.go b/pkg/utils/cert/common.go
index 567031252..bdc863673 100644
--- a/pkg/utils/cert/common.go
+++ b/pkg/utils/cert/common.go
@@ -2,6 +2,7 @@ package cert
import (
"crypto/x509"
+ "encoding/pem"
)
// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。
@@ -24,3 +25,18 @@ func EqualCertificate(a, b *x509.Certificate) bool {
a.Issuer.SerialNumber == b.Issuer.SerialNumber &&
a.Subject.SerialNumber == b.Subject.SerialNumber
}
+
+func decodePEM(data []byte) []*pem.Block {
+ blocks := make([]*pem.Block, 0)
+ for {
+ block, rest := pem.Decode(data)
+ if block == nil {
+ break
+ }
+
+ blocks = append(blocks, block)
+ data = rest
+ }
+
+ return blocks
+}
diff --git a/pkg/utils/cert/extractor.go b/pkg/utils/cert/extractor.go
index 1e116b1fd..b9e4607f1 100644
--- a/pkg/utils/cert/extractor.go
+++ b/pkg/utils/cert/extractor.go
@@ -3,6 +3,7 @@ package cert
import (
"encoding/pem"
"errors"
+ "fmt"
)
// 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。
@@ -15,32 +16,27 @@ import (
// - intermediaCertPEM: 中间证书的 PEM 内容。
// - err: 错误。
func ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) {
- pemBlocks := make([]*pem.Block, 0)
- pemData := []byte(certPEM)
- for {
- block, rest := pem.Decode(pemData)
- if block == nil || block.Type != "CERTIFICATE" {
- break
+ blocks := decodePEM([]byte(certPEM))
+ for i, block := range blocks {
+ if block.Type != "CERTIFICATE" {
+ return "", "", fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type)
}
-
- pemBlocks = append(pemBlocks, block)
- pemData = rest
}
serverCertPEM := ""
intermediaCertPEM := ""
- if len(pemBlocks) == 0 {
+ if len(blocks) == 0 {
return "", "", errors.New("failed to decode PEM block")
}
- if len(pemBlocks) > 0 {
- serverCertPEM = string(pem.EncodeToMemory(pemBlocks[0]))
+ if len(blocks) > 0 {
+ serverCertPEM = string(pem.EncodeToMemory(blocks[0]))
}
- if len(pemBlocks) > 1 {
- for i := 1; i < len(pemBlocks); i++ {
- intermediaCertPEM += string(pem.EncodeToMemory(pemBlocks[i]))
+ if len(blocks) > 1 {
+ for i := 1; i < len(blocks); i++ {
+ intermediaCertPEM += string(pem.EncodeToMemory(blocks[i]))
}
}
diff --git a/pkg/utils/cert/transformer.go b/pkg/utils/cert/transformer.go
index bf467efa7..690ae19fd 100644
--- a/pkg/utils/cert/transformer.go
+++ b/pkg/utils/cert/transformer.go
@@ -2,8 +2,9 @@ package cert
import (
"bytes"
- "encoding/pem"
+ "crypto/x509"
"errors"
+ "fmt"
"time"
"github.com/pavlo-v-chernykh/keystore-go/v4"
@@ -21,9 +22,19 @@ import (
// - data: PFX 格式的证书数据。
// - err: 错误。
func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPassword string) ([]byte, error) {
- cert, err := ParseCertificateFromPEM(certPEM)
- if err != nil {
- return nil, err
+ blocks := decodePEM([]byte(certPEM))
+
+ certs := make([]*x509.Certificate, 0, len(blocks))
+ for i, block := range blocks {
+ if block.Type != "CERTIFICATE" {
+ return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type)
+ }
+
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ certs = append(certs, cert)
}
privkey, err := ParsePrivateKeyFromPEM(privkeyPEM)
@@ -31,12 +42,16 @@ func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPass
return nil, err
}
- pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword)
- if err != nil {
- return nil, err
+ var pfxData []byte
+ if len(certs) == 0 {
+ return nil, errors.New("failed to decode certificate PEM")
+ } else if len(certs) == 1 {
+ pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], nil, pfxPassword)
+ } else {
+ pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], certs[1:], pfxPassword)
}
- return pfxData, nil
+ return pfxData, err
}
// 将 PEM 编码的证书字符串转换为 JKS 格式。
@@ -52,28 +67,33 @@ func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPass
// - data: JKS 格式的证书数据。
// - err: 错误。
func TransformCertificateFromPEMToJKS(certPEM string, privkeyPEM string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) {
- certBlock, _ := pem.Decode([]byte(certPEM))
- if certBlock == nil {
+ certBlocks := decodePEM([]byte(certPEM))
+ if len(certBlocks) == 0 {
return nil, errors.New("failed to decode certificate PEM")
}
- privkeyBlock, _ := pem.Decode([]byte(privkeyPEM))
- if privkeyBlock == nil {
+ privkeyBlocks := decodePEM([]byte(privkeyPEM))
+ if len(privkeyBlocks) == 0 {
return nil, errors.New("failed to decode private key PEM")
}
- ks := keystore.New()
entry := keystore.PrivateKeyEntry{
- CreationTime: time.Now(),
- PrivateKey: privkeyBlock.Bytes,
- CertificateChain: []keystore.Certificate{
- {
- Type: "X509",
- Content: certBlock.Bytes,
- },
- },
+ CreationTime: time.Now(),
+ PrivateKey: privkeyBlocks[0].Bytes,
+ CertificateChain: make([]keystore.Certificate, len(certBlocks)),
+ }
+ for i, certBlock := range certBlocks {
+ if certBlock.Type != "CERTIFICATE" {
+ return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, certBlock.Type)
+ }
+
+ entry.CertificateChain[i] = keystore.Certificate{
+ Type: "X509",
+ Content: certBlock.Bytes,
+ }
}
+ ks := keystore.New()
if err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil {
return nil, err
}
From 2f551dd3e3db9b5ff417f6b2ee1ff64f7e36af4a Mon Sep 17 00:00:00 2001
From: Fu Diwei
Date: Tue, 8 Jul 2025 10:25:44 +0800
Subject: [PATCH 7/8] fix: #854
---
internal/deployer/providers.go | 11 ++-
.../providers/qiniu-kodo/qiniu_kodo.go | 88 +++++++++++++++++++
.../providers/qiniu-kodo/qiniu_kodo_test.go | 75 ++++++++++++++++
.../providers/qiniu-sslcert/qiniu_sslcert.go | 81 +++++++++++++++--
.../qiniu-sslcert/qiniu_sslcert_test.go | 72 +++++++++++++++
pkg/sdk3rd/qiniu/cdn.go | 45 +---------
pkg/sdk3rd/qiniu/kodo.go | 44 ++++++++++
pkg/sdk3rd/qiniu/sslcert.go | 80 +++++++++++++++++
pkg/sdk3rd/qiniu/util.go | 14 +++
.../i18n/locales/en/nls.workflow.nodes.json | 28 +++---
.../i18n/locales/zh/nls.workflow.nodes.json | 8 +-
11 files changed, 481 insertions(+), 65 deletions(-)
create mode 100644 pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go
create mode 100644 pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go
create mode 100644 pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go
create mode 100644 pkg/sdk3rd/qiniu/kodo.go
create mode 100644 pkg/sdk3rd/qiniu/sslcert.go
create mode 100644 pkg/sdk3rd/qiniu/util.go
diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go
index 7eb583726..61ae87859 100644
--- a/internal/deployer/providers.go
+++ b/internal/deployer/providers.go
@@ -69,6 +69,7 @@ import (
pNetlifySite "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/netlify-site"
pProxmoxVE "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/proxmoxve"
pQiniuCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-cdn"
+ pQiniuKodo "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-kodo"
pQiniuPili "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-pili"
pRainYunRCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/rainyun-rcdn"
pRatPanelConsole "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/ratpanel-console"
@@ -1001,7 +1002,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy
}
switch options.Provider {
- case domain.DeploymentProviderTypeQiniuCDN, domain.DeploymentProviderTypeQiniuKodo:
+ case domain.DeploymentProviderTypeQiniuCDN:
deployer, err := pQiniuCDN.NewSSLDeployerProvider(&pQiniuCDN.SSLDeployerProviderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
@@ -1009,6 +1010,14 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy
})
return deployer, err
+ case domain.DeploymentProviderTypeQiniuKodo:
+ deployer, err := pQiniuKodo.NewSSLDeployerProvider(&pQiniuKodo.SSLDeployerProviderConfig{
+ AccessKey: access.AccessKey,
+ SecretKey: access.SecretKey,
+ Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"),
+ })
+ return deployer, err
+
case domain.DeploymentProviderTypeQiniuPili:
deployer, err := pQiniuPili.NewSSLDeployerProvider(&pQiniuPili.SSLDeployerProviderConfig{
AccessKey: access.AccessKey,
diff --git a/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go
new file mode 100644
index 000000000..7dcec1724
--- /dev/null
+++ b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go
@@ -0,0 +1,88 @@
+package qiniukodo
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/qiniu/go-sdk/v7/auth"
+
+ "github.com/certimate-go/certimate/pkg/core"
+ sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/qiniu-sslcert"
+ qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu"
+)
+
+type SSLDeployerProviderConfig struct {
+ // 七牛云 AccessKey。
+ AccessKey string `json:"accessKey"`
+ // 七牛云 SecretKey。
+ SecretKey string `json:"secretKey"`
+ // 自定义域名(不支持泛域名)。
+ Domain string `json:"domain"`
+}
+
+type SSLDeployerProvider struct {
+ config *SSLDeployerProviderConfig
+ logger *slog.Logger
+ sdkClient *qiniusdk.KodoManager
+ sslManager core.SSLManager
+}
+
+var _ core.SSLDeployer = (*SSLDeployerProvider)(nil)
+
+func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) {
+ if config == nil {
+ return nil, errors.New("the configuration of the ssl deployer provider is nil")
+ }
+
+ client := qiniusdk.NewKodoManager(auth.New(config.AccessKey, config.SecretKey))
+
+ sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{
+ AccessKey: config.AccessKey,
+ SecretKey: config.SecretKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("could not create ssl manager: %w", err)
+ }
+
+ return &SSLDeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslManager: sslmgr,
+ }, nil
+}
+
+func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+
+ d.sslManager.SetLogger(logger)
+}
+
+func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) {
+ if d.config.Domain == "" {
+ return nil, fmt.Errorf("config `domain` is required")
+ }
+
+ // 上传证书
+ upres, err := d.sslManager.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(context.TODO(), d.config.Domain, upres.CertId)
+ d.logger.Debug("sdk request 'kodo.BindCert'", slog.String("request.domain", d.config.Domain), slog.String("request.certId", upres.CertId), slog.Any("response", bindBucketCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'kodo.BindCert': %w", err)
+ }
+
+ return &core.SSLDeployResult{}, nil
+}
diff --git a/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go
new file mode 100644
index 000000000..3dfcf4561
--- /dev/null
+++ b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go
@@ -0,0 +1,75 @@
+package qiniukodo_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-kodo"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKey string
+ fSecretKey string
+ fDomain string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_SSLDEPLOYER_QINIUKODO_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
+ flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
+ flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./qiniu_kodo_test.go -args \
+ --CERTIMATE_SSLDEPLOYER_QINIUKODO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_SSLDEPLOYER_QINIUKODO_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_SSLDEPLOYER_QINIUKODO_ACCESSKEY="your-access-key" \
+ --CERTIMATE_SSLDEPLOYER_QINIUKODO_SECRETKEY="your-secret-key" \
+ --CERTIMATE_SSLDEPLOYER_QINIUKODO_DOMAIN="example.com"
+*/
+func TestDeploy(t *testing.T) {
+ flag.Parse()
+
+ t.Run("Deploy", func(t *testing.T) {
+ t.Log(strings.Join([]string{
+ "args:",
+ fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
+ fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
+ fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
+ fmt.Sprintf("SECRETKEY: %v", fSecretKey),
+ fmt.Sprintf("DOMAIN: %v", fDomain),
+ }, "\n"))
+
+ deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{
+ AccessKey: fAccessKey,
+ SecretKey: fSecretKey,
+ Domain: fDomain,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go
index 07775b21c..dcbe26b03 100644
--- a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go
+++ b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go
@@ -2,9 +2,12 @@ package qiniusslcert
import (
"context"
+ "crypto/x509"
"errors"
"fmt"
"log/slog"
+ "slices"
+ "strings"
"time"
"github.com/qiniu/go-sdk/v7/auth"
@@ -24,7 +27,7 @@ type SSLManagerProviderConfig struct {
type SSLManagerProvider struct {
config *SSLManagerProviderConfig
logger *slog.Logger
- sdkClient *qiniusdk.CdnManager
+ sdkClient *qiniusdk.SslCertManager
}
var _ core.SSLManager = (*SSLManagerProvider)(nil)
@@ -64,12 +67,80 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey
// 生成新证书名(需符合七牛云命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
+ // 遍历查询已有证书,避免重复上传
+ getSslCertListMarker := ""
+ getSslCertListLimit := int32(200)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ getSslCertListResp, err := m.sdkClient.GetSslCertList(context.TODO(), getSslCertListMarker, getSslCertListLimit)
+ m.logger.Debug("sdk request 'sslcert.GetList'", slog.Any("request.marker", getSslCertListMarker), slog.Any("response", getSslCertListResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'sslcert.GetList': %w", err)
+ }
+
+ if getSslCertListResp.Certs != nil {
+ for _, sslCert := range getSslCertListResp.Certs {
+ // 先对比证书通用名称
+ if !strings.EqualFold(certX509.Subject.CommonName, sslCert.CommonName) {
+ continue
+ }
+
+ // 再对比证书多域名
+ if !slices.Equal(certX509.DNSNames, sslCert.DnsNames) {
+ continue
+ }
+
+ // 再对比证书有效期
+ if certX509.NotBefore.Unix() != sslCert.NotBefore || certX509.NotAfter.Unix() != sslCert.NotAfter {
+ continue
+ }
+
+ // 最后对比证书公钥算法
+ switch certX509.PublicKeyAlgorithm {
+ case x509.RSA:
+ if !strings.EqualFold(sslCert.Encrypt, "RSA") {
+ continue
+ }
+ case x509.ECDSA:
+ if !strings.EqualFold(sslCert.Encrypt, "ECDSA") {
+ continue
+ }
+ case x509.Ed25519:
+ if !strings.EqualFold(sslCert.Encrypt, "ED25519") {
+ continue
+ }
+ default:
+ // 未知算法,跳过
+ continue
+ }
+
+ // 如果以上信息都一致,则视为已存在相同证书,直接返回
+ m.logger.Info("ssl certificate already exists")
+ return &core.SSLManageUploadResult{
+ CertId: sslCert.CertID,
+ CertName: sslCert.Name,
+ }, nil
+ }
+ }
+
+ if len(getSslCertListResp.Certs) < int(getSslCertListLimit) || getSslCertListResp.Marker == "" {
+ break
+ } else {
+ getSslCertListMarker = getSslCertListResp.Marker
+ }
+ }
+
// 上传新证书
// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate
uploadSslCertResp, err := m.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPEM, privkeyPEM)
- m.logger.Debug("sdk request 'cdn.UploadSslCert'", slog.Any("response", uploadSslCertResp))
+ m.logger.Debug("sdk request 'sslcert.Upload'", slog.Any("response", uploadSslCertResp))
if err != nil {
- return nil, fmt.Errorf("failed to execute sdk request 'cdn.UploadSslCert': %w", err)
+ return nil, fmt.Errorf("failed to execute sdk request 'sslcert.Upload': %w", err)
}
return &core.SSLManageUploadResult{
@@ -78,7 +149,7 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey
}, nil
}
-func createSDKClient(accessKey, secretKey string) (*qiniusdk.CdnManager, error) {
+func createSDKClient(accessKey, secretKey string) (*qiniusdk.SslCertManager, error) {
if secretKey == "" {
return nil, errors.New("invalid qiniu access key")
}
@@ -88,6 +159,6 @@ func createSDKClient(accessKey, secretKey string) (*qiniusdk.CdnManager, error)
}
credential := auth.New(accessKey, secretKey)
- client := qiniusdk.NewCdnManager(credential)
+ client := qiniusdk.NewSslCertManager(credential)
return client, nil
}
diff --git a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go
new file mode 100644
index 000000000..87e14a08a
--- /dev/null
+++ b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go
@@ -0,0 +1,72 @@
+package qiniusslcert_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/qiniu-sslcert"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKey string
+ fSecretKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_SSLMANAGER_QINIUSSLCERT_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
+ flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./qiniu_sslcert_test.go -args \
+ --CERTIMATE_SSLMANAGER_QINIUSSLCERT_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_SSLMANAGER_QINIUSSLCERT_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_SSLMANAGER_QINIUSSLCERT_ACCESSKEY="your-access-key" \
+ --CERTIMATE_SSLMANAGER_QINIUSSLCERT_SECRETKEY="your-secret-key"
+*/
+func TestDeploy(t *testing.T) {
+ flag.Parse()
+
+ t.Run("Deploy", func(t *testing.T) {
+ t.Log(strings.Join([]string{
+ "args:",
+ fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
+ fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
+ fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
+ fmt.Sprintf("SECRETKEY: %v", fSecretKey),
+ }, "\n"))
+
+ sslmanager, err := provider.NewSSLManagerProvider(&provider.SSLManagerProviderConfig{
+ AccessKey: fAccessKey,
+ SecretKey: fSecretKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := sslmanager.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/pkg/sdk3rd/qiniu/cdn.go b/pkg/sdk3rd/qiniu/cdn.go
index 54a56517d..74745d0ba 100644
--- a/pkg/sdk3rd/qiniu/cdn.go
+++ b/pkg/sdk3rd/qiniu/cdn.go
@@ -2,16 +2,12 @@ package qiniu
import (
"context"
- "fmt"
"net/http"
- "strings"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/qiniu/go-sdk/v7/client"
)
-const qiniuHost = "https://api.qiniu.com"
-
type CdnManager struct {
client *client.Client
}
@@ -21,16 +17,10 @@ func NewCdnManager(mac *auth.Credentials) *CdnManager {
mac = auth.Default()
}
- client := &client.Client{&http.Client{Transport: newTransport(mac, nil)}}
+ client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}}
return &CdnManager{client: client}
}
-func (m *CdnManager) urlf(pathf string, pathargs ...any) string {
- path := fmt.Sprintf(pathf, pathargs...)
- path = strings.TrimPrefix(path, "/")
- return qiniuHost + "/" + path
-}
-
type GetDomainInfoResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
@@ -52,7 +42,7 @@ type GetDomainInfoResponse struct {
func (m *CdnManager) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) {
resp := new(GetDomainInfoResponse)
- if err := m.client.Call(ctx, resp, http.MethodGet, m.urlf("domain/%s", domain), nil); err != nil {
+ if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain/%s", domain), nil); err != nil {
return nil, err
}
return resp, nil
@@ -76,7 +66,7 @@ func (m *CdnManager) ModifyDomainHttpsConf(ctx context.Context, domain string, c
Http2Enable: http2Enable,
}
resp := new(ModifyDomainHttpsConfResponse)
- if err := m.client.CallWithJson(ctx, resp, http.MethodPut, m.urlf("domain/%s/httpsconf", domain), nil, req); err != nil {
+ if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/httpsconf", domain), nil, req); err != nil {
return nil, err
}
return resp, nil
@@ -100,34 +90,7 @@ func (m *CdnManager) EnableDomainHttps(ctx context.Context, domain string, certI
Http2Enable: http2Enable,
}
resp := new(EnableDomainHttpsResponse)
- if err := m.client.CallWithJson(ctx, resp, http.MethodPut, m.urlf("domain/%s/sslize", domain), nil, req); 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 *CdnManager) 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, m.urlf("sslcert"), nil, req); err != nil {
+ if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/sslize", domain), nil, req); err != nil {
return nil, err
}
return resp, nil
diff --git a/pkg/sdk3rd/qiniu/kodo.go b/pkg/sdk3rd/qiniu/kodo.go
new file mode 100644
index 000000000..6a3245a28
--- /dev/null
+++ b/pkg/sdk3rd/qiniu/kodo.go
@@ -0,0 +1,44 @@
+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
+}
diff --git a/pkg/sdk3rd/qiniu/sslcert.go b/pkg/sdk3rd/qiniu/sslcert.go
new file mode 100644
index 000000000..f9784270f
--- /dev/null
+++ b/pkg/sdk3rd/qiniu/sslcert.go
@@ -0,0 +1,80 @@
+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
+}
diff --git a/pkg/sdk3rd/qiniu/util.go b/pkg/sdk3rd/qiniu/util.go
new file mode 100644
index 000000000..0957a3102
--- /dev/null
+++ b/pkg/sdk3rd/qiniu/util.go
@@ -0,0 +1,14 @@
+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
+}
diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json
index df7f19152..bc5a16a1b 100644
--- a/ui/src/i18n/locales/en/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json
@@ -268,8 +268,8 @@
"workflow_node.deploy.form.aliyun_oss_bucket.label": "Alibaba Cloud OSS bucket",
"workflow_node.deploy.form.aliyun_oss_bucket.placeholder": "Please enter Alibaba Cloud OSS bucket name",
"workflow_node.deploy.form.aliyun_oss_bucket.tooltip": "For more information, see https://oss.console.aliyun.com",
- "workflow_node.deploy.form.aliyun_oss_domain.label": "Alibaba Cloud OSS domain",
- "workflow_node.deploy.form.aliyun_oss_domain.placeholder": "Please enter Alibaba Cloud OSS domain name",
+ "workflow_node.deploy.form.aliyun_oss_domain.label": "Alibaba Cloud OSS custom domain",
+ "workflow_node.deploy.form.aliyun_oss_domain.placeholder": "Please enter Alibaba Cloud OSS bucket custom domain name",
"workflow_node.deploy.form.aliyun_oss_domain.tooltip": "For more information, see https://oss.console.aliyun.com",
"workflow_node.deploy.form.aliyun_vod_region.label": "Alibaba Cloud VOD region",
"workflow_node.deploy.form.aliyun_vod_region.placeholder": "Please enter Alibaba Cloud VOD region (e.g. cn-hangzhou)",
@@ -601,8 +601,8 @@
"workflow_node.deploy.form.qiniu_cdn_domain.label": "Qiniu CDN domain",
"workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "Please enter Qiniu CDN domain name",
"workflow_node.deploy.form.qiniu_cdn_domain.tooltip": "For more information, see https://portal.qiniu.com/cdn",
- "workflow_node.deploy.form.qiniu_kodo_domain.label": "Qiniu Kodo bucket domain",
- "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "Please enter Qiniu Kodo bucket domain name",
+ "workflow_node.deploy.form.qiniu_kodo_domain.label": "Qiniu Kodo custom domain",
+ "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "Please enter Qiniu Kodo bucket custom domain name",
"workflow_node.deploy.form.qiniu_kodo_domain.tooltip": "For more information, see https://portal.qiniu.com/kodo",
"workflow_node.deploy.form.qiniu_pili_hub.label": "Qiniu Pili hub",
"workflow_node.deploy.form.qiniu_pili_hub.placeholder": "Please enter Qiniu Pili hub name",
@@ -706,8 +706,8 @@
"workflow_node.deploy.form.tencentcloud_cos_bucket.label": "Tencent Cloud COS bucket",
"workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder": "Please enter Tencent Cloud COS bucket name",
"workflow_node.deploy.form.tencentcloud_cos_bucket.tooltip": "For more information, see https://console.tencentcloud.com/cos",
- "workflow_node.deploy.form.tencentcloud_cos_domain.label": "Tencent Cloud COS domain",
- "workflow_node.deploy.form.tencentcloud_cos_domain.placeholder": "Please enter Tencent Cloud COS domain name",
+ "workflow_node.deploy.form.tencentcloud_cos_domain.label": "Tencent Cloud COS custom domain",
+ "workflow_node.deploy.form.tencentcloud_cos_domain.placeholder": "Please enter Tencent Cloud COS bucket custom domain name",
"workflow_node.deploy.form.tencentcloud_cos_domain.tooltip": "For more information, see https://console.tencentcloud.com/cos",
"workflow_node.deploy.form.tencentcloud_css_endpoint.label": "Tencent Cloud CSS API endpoint (Optional)",
"workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder": "Please enter Tencent Cloud CSS API endpoint (e.g. live.intl.tencentcloudapi.com)",
@@ -824,8 +824,8 @@
"workflow_node.deploy.form.ucloud_us3_bucket.label": "UCloud US3 bucket",
"workflow_node.deploy.form.ucloud_us3_bucket.placeholder": "Please enter UCloud US3 bucket name",
"workflow_node.deploy.form.ucloud_us3_bucket.tooltip": "For more information, see https://console.ucloud-global.com/ufile",
- "workflow_node.deploy.form.ucloud_us3_domain.label": "UCloud US3 domain",
- "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "Please enter UCloud US3 domain name",
+ "workflow_node.deploy.form.ucloud_us3_domain.label": "UCloud US3 custom domain",
+ "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "Please enter UCloud US3 bucket custom domain name",
"workflow_node.deploy.form.ucloud_us3_domain.tooltip": "For more information, see https://console.ucloud-global.com/ufile",
"workflow_node.deploy.form.unicloud_webhost.guide": "Tips: This uses webpage simulator login and does not guarantee stability. If there are any changes to the uniCloud, please create a GitHub Issue.",
"workflow_node.deploy.form.unicloud_webhost_space_provider.label": "uniCloud space provider",
@@ -842,8 +842,8 @@
"workflow_node.deploy.form.upyun_cdn_domain.placeholder": "Please enter UPYUN CDN domain name",
"workflow_node.deploy.form.upyun_cdn_domain.tooltip": "For more information, see https://console.upyun.com/services/cdn/",
"workflow_node.deploy.form.upyun_file.guide": "Tips: This uses webpage simulator login and does not guarantee stability. If there are any changes to the UPYUN, please create a GitHub Issue.",
- "workflow_node.deploy.form.upyun_file_domain.label": "UPYUN bucket domain",
- "workflow_node.deploy.form.upyun_file_domain.placeholder": "Please enter UPYUN bucket domain name",
+ "workflow_node.deploy.form.upyun_file_domain.label": "UPYUN USS custom domain",
+ "workflow_node.deploy.form.upyun_file_domain.placeholder": "Please enter UPYUN USS bucket custom domain name",
"workflow_node.deploy.form.upyun_file_domain.tooltip": "For more information, see https://console.upyun.com/services/file/",
"workflow_node.deploy.form.volcengine_alb_region.label": "VolcEngine ALB region",
"workflow_node.deploy.form.volcengine_alb_region.placeholder": "Please enter VolcEngine ALB region (e.g. cn-beijing)",
@@ -888,8 +888,8 @@
"workflow_node.deploy.form.volcengine_imagex_service_id.label": "VolcEngine ImageX service ID",
"workflow_node.deploy.form.volcengine_imagex_service_id.placeholder": "Please enter VolcEngine ImageX service ID",
"workflow_node.deploy.form.volcengine_imagex_service_id.tooltip": "For more information, see https://console.volcengine.com/imagex",
- "workflow_node.deploy.form.volcengine_imagex_domain.label": "VolcEngine ImageX domain",
- "workflow_node.deploy.form.volcengine_imagex_domain.placeholder": "Please enter VolcEngine ImageX domain name",
+ "workflow_node.deploy.form.volcengine_imagex_domain.label": "VolcEngine ImageX custom domain",
+ "workflow_node.deploy.form.volcengine_imagex_domain.placeholder": "Please enter VolcEngine ImageX custom domain name",
"workflow_node.deploy.form.volcengine_imagex_domain.tooltip": "For more information, see https://console.volcengine.com/imagex",
"workflow_node.deploy.form.volcengine_live_domain.label": "VolcEngine Live streaming domain",
"workflow_node.deploy.form.volcengine_live_domain.placeholder": "Please enter VolcEngine Live streaming domain name",
@@ -900,8 +900,8 @@
"workflow_node.deploy.form.volcengine_tos_bucket.label": "VolcEngine TOS bucket",
"workflow_node.deploy.form.volcengine_tos_bucket.placeholder": "Please enter VolcEngine TOS bucket name",
"workflow_node.deploy.form.volcengine_tos_bucket.tooltip": "For more information, see https://console.volcengine.com/tos",
- "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain",
- "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name",
+ "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS custom domain",
+ "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS bucket custom domain name",
"workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see https://console.volcengine.com/tos",
"workflow_node.deploy.form.wangsu_cdn_domains.label": "Wangsu Cloud CDN domains",
"workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "Please enter Wangsu Cloud CDN domain names (separated by semicolons)",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
index 061189dfd..09fc27e95 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
@@ -599,8 +599,8 @@
"workflow_node.deploy.form.qiniu_cdn_domain.label": "七牛云 CDN 加速域名",
"workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "请输入七牛云 CDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.qiniu_cdn_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/cdn",
- "workflow_node.deploy.form.qiniu_kodo_domain.label": "七牛云对象存储加速域名",
- "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "请输入七牛云对象存储加速域名",
+ "workflow_node.deploy.form.qiniu_kodo_domain.label": "七牛云对象存储自定义域名",
+ "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "请输入七牛云对象存储自定义域名",
"workflow_node.deploy.form.qiniu_kodo_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/kodo",
"workflow_node.deploy.form.qiniu_pili_hub.label": "七牛云视频直播空间名",
"workflow_node.deploy.form.qiniu_pili_hub.placeholder": "请输入七牛云视频直播空间名",
@@ -840,8 +840,8 @@
"workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/cdn/",
"workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。",
- "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名",
- "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名",
+ "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储自定义域名",
+ "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储自定义域名",
"workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/",
"workflow_node.deploy.form.volcengine_alb_resource_type.label": "证书部署方式",
"workflow_node.deploy.form.volcengine_alb_resource_type.placeholder": "请选择证书部署方式",
From 07d5c2051cceb65e38f931de98f1fb123561f8ac Mon Sep 17 00:00:00 2001
From: Fu Diwei
Date: Tue, 8 Jul 2025 10:37:44 +0800
Subject: [PATCH 8/8] fix: missing valid from field
---
.../workflow/node/DeployNodeConfigFormSSHConfig.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
index ebeb1cff9..132de43f0 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
@@ -152,9 +152,11 @@ chmod 755 "$fnCertPath"
chmod 755 "$fnKeyPath"
# 更新数据库
+NEW_EFFECT_DATE=$(openssl x509 -startdate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/")
+NEW_EFFECT_TIMESTAMP=$(date -d "$NEW_EFFECT_DATE" +%s%3N)
NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/")
NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N)
-psql -U postgres -d trim_connect -c "UPDATE cert SET valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'"
+psql -U postgres -d trim_connect -c "UPDATE cert SET valid_from=$NEW_EFFECT_TIMESTAMP, valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'"
# 重启服务
systemctl restart webdav.service