feat(provider): new deployment provider: samwaf console

This commit is contained in:
Fu Diwei 2026-07-13 15:06:38 +08:00
parent 0a696cd157
commit 5b6f59d444
16 changed files with 369 additions and 2 deletions

View File

@ -0,0 +1,27 @@
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/samwaf-console"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.DeploymentProviderTypeSamWAFConsole, func(options *ProviderFactoryOptions) (core.Deployer, error) {
credentials := domain.AccessConfigForSamWAF{}
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{
ServerUrl: credentials.ServerUrl,
ApiKey: credentials.ApiKey,
AllowInsecureConnections: credentials.AllowInsecureConnections,
AutoRestart: xmaps.GetBool(options.ProviderExtendedConfig, "autoRestart"),
})
return provider, err
})
}

View File

@ -420,6 +420,7 @@ const (
DeploymentProviderTypeS3 = DeploymentProviderType(AccessProviderTypeS3)
DeploymentProviderTypeSafeLine = DeploymentProviderType(AccessProviderTypeSafeLine)
DeploymentProviderTypeSamWAF = DeploymentProviderType(AccessProviderTypeSamWAF)
DeploymentProviderTypeSamWAFConsole = DeploymentProviderType(AccessProviderTypeSamWAF + "-console")
DeploymentProviderTypeSSH = DeploymentProviderType(AccessProviderTypeSSH)
DeploymentProviderTypeSynologyDSM = DeploymentProviderType(AccessProviderTypeSynologyDSM)
DeploymentProviderTypeTencentCloudCDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-cdn")

View File

@ -63,7 +63,7 @@ func (d *Deployer) SetLogger(logger *slog.Logger) {
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
// 根据部署目标决定业务流程``
// 根据部署目标决定业务流程
switch d.config.DeployTarget {
case DEPLOY_TARGET_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {

View File

@ -0,0 +1,111 @@
package samwafconsole
import (
"context"
"crypto/tls"
"fmt"
"log/slog"
"github.com/certimate-go/certimate/pkg/core"
samwafsdk "github.com/certimate-go/certimate/pkg/sdk3rd/samwaf"
)
type (
Provider = core.Deployer
DeployResult = core.DeployerDeployResult
)
type DeployerConfig struct {
// SamWAF 服务地址。
ServerUrl string `json:"serverUrl"`
// SamWAF API Key。
ApiKey string `json:"apiKey"`
// 是否允许不安全的连接。
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
// 是否自动重启。
AutoRestart bool `json:"autoRestart"`
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *samwafsdk.Client
}
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, err := createSDKClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: 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) {
// 上传管理端 SSL 证书
// REF: https://doc.samwaf.com/api/
vipConfigUploadSslCertReq := &samwafsdk.VipConfigUploadSslCertRequest{
CertContent: certPEM,
KeyContent: privkeyPEM,
}
vipConfigUploadSslCertResp, err := d.sdkClient.VipConfigUploadSslCertWithContext(ctx, vipConfigUploadSslCertReq)
d.logger.Debug("sdk request 'vipconfig.UploadSslCert'", slog.Any("request", vipConfigUploadSslCertReq), slog.Any("response", vipConfigUploadSslCertResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'vipconfig.UploadSslCert': %w", err)
}
// 更新管理端 SSL 启用状态
// REF: https://doc.samwaf.com/api/
vipConfigUpdateSslEnableReq := &samwafsdk.VipConfigUpdateSslEnableRequest{
SslEnable: true,
}
vipConfigUpdateSslEnableResp, err := d.sdkClient.VipConfigUpdateSslEnableWithContext(ctx, vipConfigUpdateSslEnableReq)
d.logger.Debug("sdk request 'vipconfig.UpdateSslEnable'", slog.Any("request", vipConfigUpdateSslEnableReq), slog.Any("response", vipConfigUpdateSslEnableResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'vipconfig.UpdateSslEnable': %w", err)
}
if d.config.AutoRestart {
// 重启管理端
vipConfigRestartManagerResp, err := d.sdkClient.VipConfigRestartManagerWithContext(ctx)
d.logger.Debug("sdk request 'vipconfig.RestartManager'", slog.Any("response", vipConfigRestartManagerResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'vipconfig.RestartManager': %w", err)
}
}
return &DeployResult{}, nil
}
func createSDKClient(serverUrl, apiKey string, skipTlsVerify bool) (*samwafsdk.Client, error) {
client, err := samwafsdk.NewClient(serverUrl,
samwafsdk.WithApiKey(apiKey),
)
if err != nil {
return nil, err
}
if skipTlsVerify {
client.SetTLSConfig(&tls.Config{InsecureSkipVerify: true})
}
return client, nil
}

View File

@ -0,0 +1,52 @@
package samwafconsole_test
import (
"testing"
"github.com/certimate-go/certimate/pkg/core/deployer/internal/tester"
impl "github.com/certimate-go/certimate/pkg/core/deployer/providers/samwaf-console"
)
var (
fp = tester.Args("SAMWAFCONSOLE_")
fTestCertPath string
fTestKeyPath string
fServerUrl string
fApiKey string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fServerUrl, "SERVERURL")
fp.DefineString(&fApiKey, "APIKEY")
}
/*
Shell command to run this test:
go test -v ./samwaf_console_test.go -args \
--SAMWAFCONSOLE_TESTCERTPATH="/path/to/your-test-cert.pem" \
--SAMWAFCONSOLE_TESTKEYPATH="/path/to/your-test-key.pem" \
--SAMWAFCONSOLE_SERVERURL="http://127.0.0.1:26666" \
--SAMWAFCONSOLE_APIKEY="your-api-key" \
--SAMWAFCONSOLE_CERTIFICATEID="your-certificate-id"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Deploy", func(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
ServerUrl: fServerUrl,
ApiKey: fApiKey,
AllowInsecureConnections: true,
AutoRestart: true,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestDeploy(t, provider, tester.TestDeployArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}

View File

@ -63,7 +63,7 @@ func (d *Deployer) SetLogger(logger *slog.Logger) {
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
// 根据部署目标决定业务流程``
// 根据部署目标决定业务流程
switch d.config.DeployTarget {
case DEPLOY_TARGET_CERTIFICATE:
if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil {

View File

@ -0,0 +1,30 @@
package samwaf
import (
"context"
"net/http"
)
type VipConfigRestartManagerResponse struct {
sdkResponseBase
}
func (c *Client) VipConfigRestartManager() (*VipConfigRestartManagerResponse, error) {
return c.VipConfigRestartManagerWithContext(context.Background())
}
func (c *Client) VipConfigRestartManagerWithContext(ctx context.Context) (*VipConfigRestartManagerResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/vipconfig/restartManager")
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &VipConfigRestartManagerResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -0,0 +1,35 @@
package samwaf
import (
"context"
"net/http"
)
type VipConfigUpdateSslEnableRequest struct {
SslEnable bool `json:"ssl_enable"`
}
type VipConfigUpdateSslEnableResponse struct {
sdkResponseBase
}
func (c *Client) VipConfigUpdateSslEnable(req *VipConfigUpdateSslEnableRequest) (*VipConfigUpdateSslEnableResponse, error) {
return c.VipConfigUpdateSslEnableWithContext(context.Background(), req)
}
func (c *Client) VipConfigUpdateSslEnableWithContext(ctx context.Context, req *VipConfigUpdateSslEnableRequest) (*VipConfigUpdateSslEnableResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/vipconfig/updateSslEnable")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &VipConfigUpdateSslEnableResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -0,0 +1,36 @@
package samwaf
import (
"context"
"net/http"
)
type VipConfigUploadSslCertRequest struct {
CertContent string `json:"cert_content"`
KeyContent string `json:"key_content"`
}
type VipConfigUploadSslCertResponse struct {
sdkResponseBase
}
func (c *Client) VipConfigUploadSslCert(req *VipConfigUploadSslCertRequest) (*VipConfigUploadSslCertResponse, error) {
return c.VipConfigUploadSslCertWithContext(context.Background(), req)
}
func (c *Client) VipConfigUploadSslCertWithContext(ctx context.Context, req *VipConfigUploadSslCertRequest) (*VipConfigUploadSslCertResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/vipconfig/uploadSslCert")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &VipConfigUploadSslCertResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -101,6 +101,7 @@ import BizDeployNodeConfigFieldsProviderRatPanel from "./BizDeployNodeConfigFiel
import BizDeployNodeConfigFieldsProviderS3 from "./BizDeployNodeConfigFieldsProviderS3";
import BizDeployNodeConfigFieldsProviderSafeLine from "./BizDeployNodeConfigFieldsProviderSafeLine";
import BizDeployNodeConfigFieldsProviderSamWAF from "./BizDeployNodeConfigFieldsProviderSamWAF";
import BizDeployNodeConfigFieldsProviderSamWAFConsole from "./BizDeployNodeConfigFieldsProviderSamWAFConsole";
import BizDeployNodeConfigFieldsProviderSSH from "./BizDeployNodeConfigFieldsProviderSSH";
import BizDeployNodeConfigFieldsProviderSynologyDSM from "./BizDeployNodeConfigFieldsProviderSynologyDSM";
import BizDeployNodeConfigFieldsProviderTencentCloudCDN from "./BizDeployNodeConfigFieldsProviderTencentCloudCDN";
@ -249,6 +250,7 @@ const providerComponentMap: Partial<Record<DeploymentProviderType, React.Compone
[DEPLOYMENT_PROVIDERS.S3]: BizDeployNodeConfigFieldsProviderS3,
[DEPLOYMENT_PROVIDERS.SAFELINE]: BizDeployNodeConfigFieldsProviderSafeLine,
[DEPLOYMENT_PROVIDERS.SAMWAF]: BizDeployNodeConfigFieldsProviderSamWAF,
[DEPLOYMENT_PROVIDERS.SAMWAF_CONSOLE]: BizDeployNodeConfigFieldsProviderSamWAFConsole,
[DEPLOYMENT_PROVIDERS.SSH]: BizDeployNodeConfigFieldsProviderSSH,
[DEPLOYMENT_PROVIDERS.SYNOLOGYDSM]: BizDeployNodeConfigFieldsProviderSynologyDSM,
[DEPLOYMENT_PROVIDERS.TENCENTCLOUD_CDN]: BizDeployNodeConfigFieldsProviderTencentCloudCDN,

View File

@ -0,0 +1,57 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Switch } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Tips from "@/components/Tips";
import { useFormNestedFieldsContext } from "./_context";
const BizDeployNodeConfigFieldsProviderSamWAFConsole = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item>
<Tips message={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.samwaf_console.guide") }}></span>} />
</Form.Item>
<Form.Item
name={[parentNamePath, "autoRestart"]}
initialValue={initialValues.autoRestart}
label={t("workflow_node.deploy.form.samwaf_console_auto_restart.label")}
rules={[formRule]}
>
<Switch />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
autoRestart: true,
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
autoRestart: z.boolean().nullish(),
});
};
const _default = Object.assign(BizDeployNodeConfigFieldsProviderSamWAFConsole, {
getInitialValues,
getSchema,
});
export default _default;

View File

@ -710,6 +710,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
S3: `${ACCESS_PROVIDERS.S3}`,
SAFELINE: `${ACCESS_PROVIDERS.SAFELINE}`,
SAMWAF: `${ACCESS_PROVIDERS.SAMWAF}`,
SAMWAF_CONSOLE: `${ACCESS_PROVIDERS.SAMWAF}-console`,
SSH: `${ACCESS_PROVIDERS.SSH}`,
SYNOLOGYDSM: `${ACCESS_PROVIDERS.SYNOLOGYDSM}`,
TENCENTCLOUD_CDN: `${ACCESS_PROVIDERS.TENCENTCLOUD}-cdn`,
@ -932,6 +933,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE, "provider.baotawaf_console", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.SAMWAF, "provider.samwaf", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.SAMWAF_CONSOLE, "provider.samwaf_console", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.APISIX, "provider.apisix", DEPLOYMENT_CATEGORIES.APIGATEWAY],
[DEPLOYMENT_PROVIDERS.KONG, "provider.kong", DEPLOYMENT_CATEGORIES.APIGATEWAY],
[DEPLOYMENT_PROVIDERS.CPANEL, "provider.cpanel", DEPLOYMENT_CATEGORIES.WEBSITE],

View File

@ -233,6 +233,7 @@
"s3_upload": "Upload to S3-compatible object storage",
"safeline": "SafeLine",
"samwaf": "SamWAF",
"samwaf_console": "SamWAF - Console itself",
"sectigo": "Sectigo",
"simplycom": "Simply.com",
"slackbot": "Slack Bot",

View File

@ -2172,6 +2172,12 @@
"placeholder": "Please enter SamWAF certificate ID",
"tooltip": "You can find it on SamWAF dashboard."
},
"samwaf_console": {
"guide": "Requires SamWAF v1.3.21 or higher."
},
"samwaf_console_auto_restart": {
"label": "Auto restart SamWAF after deployment"
},
"ssh_pre_command": {
"label": "Pre-command (Optional)",
"placeholder": "Please enter command to be executed before uploading files"

View File

@ -233,6 +233,7 @@
"s3_upload": "上传到 S3 兼容的对象存储",
"safeline": "雷池",
"samwaf": "SamWAF",
"samwaf_console": "SamWAF - 面板自身",
"sectigo": "Sectigo",
"simplycom": "Simply.com",
"slackbot": "Slack 机器人",

View File

@ -2171,6 +2171,12 @@
"placeholder": "请输入 SamWAF 证书 ID",
"tooltip": "请登录 SamWAF 控制台查看"
},
"samwaf_console": {
"guide": "需要 SamWAF v1.3.21 或更高版本。"
},
"samwaf_console_auto_restart": {
"label": "部署后自动重启 SamWAF 服务"
},
"ssh_pre_command": {
"label": "前置命令(可选)",
"placeholder": "请输入上传文件前执行的命令"