mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-13 07:59:52 +00:00
feat: support template variables in webhook deployment
This commit is contained in:
parent
e695c8ee5c
commit
90058b2dae
@ -367,23 +367,9 @@ func createDeployer(target string, accessConfig string, deployConfig map[string]
|
|||||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
variables := make(map[string]string)
|
|
||||||
if deployConfig != nil {
|
|
||||||
value, ok := deployConfig["variables"]
|
|
||||||
if ok {
|
|
||||||
kvs := make([]domain.KV, 0)
|
|
||||||
bts, _ := json.Marshal(value)
|
|
||||||
if err := json.Unmarshal(bts, &kvs); err == nil {
|
|
||||||
for _, kv := range kvs {
|
|
||||||
variables[kv.Key] = kv.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{
|
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{
|
||||||
Url: access.Url,
|
WebhookUrl: access.Url,
|
||||||
Variables: variables,
|
WebhookData: maps.GetValueAsString(deployConfig, "webhookData"),
|
||||||
}, logger)
|
}, logger)
|
||||||
return deployer, logger, err
|
return deployer, logger, err
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,3 @@ type DeployConfig struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Config map[string]any `json:"config"`
|
Config map[string]any `json:"config"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: TODO: 即将废弃
|
|
||||||
type KV struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
@ -209,8 +209,8 @@ func execSshCommand(sshCli *ssh.Client, command string) (string, string, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer session.Close()
|
defer session.Close()
|
||||||
|
|
||||||
var stdoutBuf bytes.Buffer
|
var stdoutBuf bytes.Buffer
|
||||||
session.Stdout = &stdoutBuf
|
session.Stdout = &stdoutBuf
|
||||||
var stderrBuf bytes.Buffer
|
var stderrBuf bytes.Buffer
|
||||||
|
@ -19,9 +19,9 @@ import (
|
|||||||
|
|
||||||
type WebhookDeployerConfig struct {
|
type WebhookDeployerConfig struct {
|
||||||
// Webhook URL。
|
// Webhook URL。
|
||||||
Url string `json:"url"`
|
WebhookUrl string `json:"webhookUrl"`
|
||||||
// Webhook 变量字典。
|
// Webhook 回调数据(JSON 格式)。
|
||||||
Variables map[string]string `json:"variables,omitempty"`
|
WebhookData string `json:"webhookData,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebhookDeployer struct {
|
type WebhookDeployer struct {
|
||||||
@ -67,19 +67,25 @@ func (d *WebhookDeployer) Deploy(ctx context.Context, certPem string, privkeyPem
|
|||||||
return nil, xerrors.Wrap(err, "failed to parse x509")
|
return nil, xerrors.Wrap(err, "failed to parse x509")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 自定义回调数据
|
var webhookData interface{}
|
||||||
reqBody, _ := json.Marshal(&webhookData{
|
err = json.Unmarshal([]byte(d.config.WebhookData), &webhookData)
|
||||||
SubjectAltNames: strings.Join(certX509.DNSNames, ","),
|
if err != nil {
|
||||||
Certificate: certPem,
|
return nil, xerrors.Wrap(err, "failed to unmarshall webhook data")
|
||||||
PrivateKey: privkeyPem,
|
}
|
||||||
Variables: d.config.Variables,
|
|
||||||
})
|
replaceJsonValueRecursively(webhookData, "${DOMAIN}", certX509.Subject.CommonName)
|
||||||
resp, err := d.httpClient.Post(d.config.Url, bytes.NewReader(reqBody), map[string][]string{"Content-Type": {"application/json"}})
|
replaceJsonValueRecursively(webhookData, "${DOMAINS}", strings.Join(certX509.DNSNames, ";"))
|
||||||
|
replaceJsonValueRecursively(webhookData, "${SUBJECT_ALT_NAMES}", strings.Join(certX509.DNSNames, ";"))
|
||||||
|
replaceJsonValueRecursively(webhookData, "${CERTIFICATE}", certPem)
|
||||||
|
replaceJsonValueRecursively(webhookData, "${PRIVATE_KEY}", privkeyPem)
|
||||||
|
|
||||||
|
reqBody, _ := json.Marshal(&webhookData)
|
||||||
|
resp, err := d.httpClient.Post(d.config.WebhookUrl, bytes.NewReader(reqBody), map[string][]string{"Content-Type": {"application/json"}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Wrap(err, "failed to send webhook request")
|
return nil, xerrors.Wrap(err, "failed to send webhook request")
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
respBody, err := io.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerrors.Wrap(err, "failed to read response body")
|
return nil, xerrors.Wrap(err, "failed to read response body")
|
||||||
@ -93,3 +99,19 @@ func (d *WebhookDeployer) Deploy(ctx context.Context, certPem string, privkeyPem
|
|||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func replaceJsonValueRecursively(data interface{}, oldStr, newStr string) interface{} {
|
||||||
|
switch v := data.(type) {
|
||||||
|
case map[string]interface{}:
|
||||||
|
for k, val := range v {
|
||||||
|
v[k] = replaceJsonValueRecursively(val, oldStr, newStr)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
for i, val := range v {
|
||||||
|
v[i] = replaceJsonValueRecursively(val, oldStr, newStr)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
return strings.ReplaceAll(v, oldStr, newStr)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
@ -14,7 +14,8 @@ import (
|
|||||||
var (
|
var (
|
||||||
fInputCertPath string
|
fInputCertPath string
|
||||||
fInputKeyPath string
|
fInputKeyPath string
|
||||||
fUrl string
|
fWebhookUrl string
|
||||||
|
fWebhookData string
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -22,7 +23,8 @@ func init() {
|
|||||||
|
|
||||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||||
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
|
flag.StringVar(&fWebhookUrl, argsPrefix+"URL", "", "")
|
||||||
|
flag.StringVar(&fWebhookData, argsPrefix+"DATA", "", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -31,7 +33,8 @@ Shell command to run this test:
|
|||||||
go test -v webhook_test.go -args \
|
go test -v webhook_test.go -args \
|
||||||
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||||
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||||
--CERTIMATE_DEPLOYER_WEBHOOK_URL="https://example.com/your-webhook-url"
|
--CERTIMATE_DEPLOYER_WEBHOOK_URL="https://example.com/your-webhook-url" \
|
||||||
|
--CERTIMATE_DEPLOYER_WEBHOOK_DATA="{\"certificate\":\"${Certificate}\",\"privateKey\":\"${PrivateKey}\"}"
|
||||||
*/
|
*/
|
||||||
func TestDeploy(t *testing.T) {
|
func TestDeploy(t *testing.T) {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@ -41,11 +44,13 @@ func TestDeploy(t *testing.T) {
|
|||||||
"args:",
|
"args:",
|
||||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||||
fmt.Sprintf("URL: %v", fUrl),
|
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
|
||||||
|
fmt.Sprintf("WEBHOOKDATA: %v", fWebhookData),
|
||||||
}, "\n"))
|
}, "\n"))
|
||||||
|
|
||||||
deployer, err := provider.New(&provider.WebhookDeployerConfig{
|
deployer, err := provider.New(&provider.WebhookDeployerConfig{
|
||||||
Url: fUrl,
|
WebhookUrl: fWebhookUrl,
|
||||||
|
WebhookData: fWebhookData,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("err: %+v", err)
|
t.Errorf("err: %+v", err)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Form, Input } from "antd";
|
import { Alert, Button, Form, Input } from "antd";
|
||||||
import { createSchemaFieldRule } from "antd-zod";
|
import { createSchemaFieldRule } from "antd-zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@ -14,11 +14,23 @@ const DeployNodeFormWebhookFields = () => {
|
|||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}, t("workflow_node.deploy.form.webhook_data.placeholder")),
|
}, t("workflow_node.deploy.form.webhook_data.errmsg.json_invalid")),
|
||||||
});
|
});
|
||||||
const formRule = createSchemaFieldRule(formSchema);
|
const formRule = createSchemaFieldRule(formSchema);
|
||||||
const formInst = Form.useFormInstance();
|
const formInst = Form.useFormInstance();
|
||||||
|
|
||||||
|
const initialValues: Partial<z.infer<typeof formSchema>> = {
|
||||||
|
webhookData: JSON.stringify(
|
||||||
|
{
|
||||||
|
name: "${DOMAINS}",
|
||||||
|
cert: "${CERTIFICATE}",
|
||||||
|
privkey: "${PRIVATE_KEY}",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
const handleWebhookDataBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
try {
|
try {
|
||||||
@ -29,19 +41,34 @@ const DeployNodeFormWebhookFields = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePresetDataClick = () => {
|
||||||
|
formInst.setFieldValue("webhookData", initialValues.webhookData);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form.Item
|
<Form.Item className="mb-0">
|
||||||
name="webhookData"
|
<label className="mb-1 block">
|
||||||
label={t("workflow_node.deploy.form.webhook_data.label")}
|
<div className="flex w-full items-center justify-between gap-4">
|
||||||
rules={[formRule]}
|
<div className="max-w-full grow truncate">{t("workflow_node.deploy.form.webhook_data.label")}</div>
|
||||||
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.tooltip") }}></span>}
|
<div className="text-right">
|
||||||
>
|
<Button size="small" type="link" onClick={handlePresetDataClick}>
|
||||||
<Input.TextArea
|
{t("workflow_node.deploy.form.webhook_data_preset.button")}
|
||||||
autoSize={{ minRows: 3, maxRows: 10 }}
|
</Button>
|
||||||
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
|
</div>
|
||||||
onBlur={handleWebhookDataBlur}
|
</div>
|
||||||
/>
|
</label>
|
||||||
|
<Form.Item name="webhookData" rules={[formRule]} initialValue={initialValues.webhookData}>
|
||||||
|
<Input.TextArea
|
||||||
|
autoSize={{ minRows: 3, maxRows: 10 }}
|
||||||
|
placeholder={t("workflow_node.deploy.form.webhook_data.placeholder")}
|
||||||
|
onBlur={handleWebhookDataBlur}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item>
|
||||||
|
<Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.webhook_data.guide") }}></span>} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -19,10 +19,10 @@
|
|||||||
"settings.notification.template.card.title": "Template",
|
"settings.notification.template.card.title": "Template",
|
||||||
"settings.notification.template.form.subject.label": "Subject",
|
"settings.notification.template.form.subject.label": "Subject",
|
||||||
"settings.notification.template.form.subject.placeholder": "Please enter notification subject",
|
"settings.notification.template.form.subject.placeholder": "Please enter notification subject",
|
||||||
"settings.notification.template.form.subject.extra": "Optional variables (${COUNT}: number of expiring soon)",
|
"settings.notification.template.form.subject.extra": "Supported variables (${COUNT}: number of expiring soon)",
|
||||||
"settings.notification.template.form.message.label": "Message",
|
"settings.notification.template.form.message.label": "Message",
|
||||||
"settings.notification.template.form.message.placeholder": "Please enter notification message",
|
"settings.notification.template.form.message.placeholder": "Please enter notification message",
|
||||||
"settings.notification.template.form.message.extra": "Optional variables (${COUNT}: number of expiring soon. ${DOMAINS}: Domain list)",
|
"settings.notification.template.form.message.extra": "Supported variables (${COUNT}: number of expiring soon. ${DOMAINS}: Domain list)",
|
||||||
"settings.notification.channels.card.title": "Channels",
|
"settings.notification.channels.card.title": "Channels",
|
||||||
"settings.notification.channel.enabled.on": "On",
|
"settings.notification.channel.enabled.on": "On",
|
||||||
"settings.notification.channel.enabled.off": "Off",
|
"settings.notification.channel.enabled.off": "Off",
|
||||||
|
@ -279,6 +279,9 @@
|
|||||||
"workflow_node.deploy.form.volcengine_live_domain.tooltip": "For more information, see <a href=\"https://console.volcengine.com/live\" target=\"_blank\">https://console.volcengine.com/live</a>",
|
"workflow_node.deploy.form.volcengine_live_domain.tooltip": "For more information, see <a href=\"https://console.volcengine.com/live\" target=\"_blank\">https://console.volcengine.com/live</a>",
|
||||||
"workflow_node.deploy.form.webhook_data.label": "Webhook data (JSON format)",
|
"workflow_node.deploy.form.webhook_data.label": "Webhook data (JSON format)",
|
||||||
"workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data",
|
"workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data",
|
||||||
|
"workflow_node.deploy.form.webhook_data.guide": "Tips: The Webhook data should be a key-value pair in JSON format. The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL. <br><br>Supported variables: <br><strong>${DOMAIN}</strong>: The primary domain of the certificate (<i>CommonName</i>).<br><strong>${DOMAINS}</strong>: The domain list of the certificate (<i>SubjectAltNames</i>).<br><strong>${CERTIFICATE}</strong>: The PEM format content of the certificate file.<br><strong>${PRIVATE_KEY}</strong>: The PEM format content of the private key file.",
|
||||||
|
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
|
||||||
|
"workflow_node.deploy.form.webhook_data_preset.button": "Use preset template",
|
||||||
|
|
||||||
"workflow_node.notify.label": "Notification",
|
"workflow_node.notify.label": "Notification",
|
||||||
"workflow_node.notify.form.subject.label": "Subject",
|
"workflow_node.notify.form.subject.label": "Subject",
|
||||||
|
@ -19,10 +19,10 @@
|
|||||||
"settings.notification.template.card.title": "通知模板",
|
"settings.notification.template.card.title": "通知模板",
|
||||||
"settings.notification.template.form.subject.label": "通知主题",
|
"settings.notification.template.form.subject.label": "通知主题",
|
||||||
"settings.notification.template.form.subject.placeholder": "请输入通知主题",
|
"settings.notification.template.form.subject.placeholder": "请输入通知主题",
|
||||||
"settings.notification.template.form.subject.extra": "可选的变量(${COUNT}: 即将过期张数)",
|
"settings.notification.template.form.subject.extra": "支持的变量(${COUNT}: 即将过期张数)",
|
||||||
"settings.notification.template.form.message.label": "通知内容",
|
"settings.notification.template.form.message.label": "通知内容",
|
||||||
"settings.notification.template.form.message.placeholder": "请输入通知内容",
|
"settings.notification.template.form.message.placeholder": "请输入通知内容",
|
||||||
"settings.notification.template.form.message.extra": "可选的变量(${COUNT}: 即将过期张数;${DOMAINS}: 域名列表)",
|
"settings.notification.template.form.message.extra": "支持的变量(${COUNT}: 即将过期张数;${DOMAINS}: 域名列表)",
|
||||||
"settings.notification.channels.card.title": "通知渠道",
|
"settings.notification.channels.card.title": "通知渠道",
|
||||||
"settings.notification.channel.enabled.on": "启用",
|
"settings.notification.channel.enabled.on": "启用",
|
||||||
"settings.notification.channel.enabled.off": "未启用",
|
"settings.notification.channel.enabled.off": "未启用",
|
||||||
|
@ -279,6 +279,9 @@
|
|||||||
"workflow_node.deploy.form.volcengine_live_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/live\" target=\"_blank\">https://console.volcengine.com/live</a>",
|
"workflow_node.deploy.form.volcengine_live_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.volcengine.com/live\" target=\"_blank\">https://console.volcengine.com/live</a>",
|
||||||
"workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(JSON 格式)",
|
"workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(JSON 格式)",
|
||||||
"workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据",
|
"workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据",
|
||||||
|
"workflow_node.deploy.form.webhook_data.guide": "小贴士:回调数据是一个 JSON 格式的键值对。其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。<br><br>支持的变量:<br><strong>${DOMAIN}</strong>:证书的主域名(即 <i>CommonName</i>)<br><strong>${DOMAINS}</strong>:证书的多域名列表(即 <i>SubjectAltNames</i>)<br><strong>${CERTIFICATE}</strong>:证书文件 PEM 格式内容<br><strong>${PRIVATE_KEY}</strong>:私钥文件 PEM 格式内容",
|
||||||
|
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串",
|
||||||
|
"workflow_node.deploy.form.webhook_data_preset.button": "使用预设模板",
|
||||||
|
|
||||||
"workflow_node.notify.label": "通知",
|
"workflow_node.notify.label": "通知",
|
||||||
"workflow_node.notify.form.subject.label": "通知主题",
|
"workflow_node.notify.form.subject.label": "通知主题",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user