mirror of
https://github.com/certimate-go/certimate.git
synced 2026-07-20 21:01:41 +08:00
feat(provider): new deployment provider: samwaf console
This commit is contained in:
parent
0a696cd157
commit
5b6f59d444
27
internal/certmgmt/deployers/sp_samwaf_console.go
Normal file
27
internal/certmgmt/deployers/sp_samwaf_console.go
Normal 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
|
||||
})
|
||||
}
|
||||
@ -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")
|
||||
|
||||
@ -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 {
|
||||
|
||||
111
pkg/core/deployer/providers/samwaf-console/samwaf_console.go
Normal file
111
pkg/core/deployer/providers/samwaf-console/samwaf_console.go
Normal 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
|
||||
}
|
||||
@ -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})
|
||||
})
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
30
pkg/sdk3rd/samwaf/api_vipconfig_restartmanager.go
Normal file
30
pkg/sdk3rd/samwaf/api_vipconfig_restartmanager.go
Normal 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
|
||||
}
|
||||
35
pkg/sdk3rd/samwaf/api_vipconfig_updatesslenable.go
Normal file
35
pkg/sdk3rd/samwaf/api_vipconfig_updatesslenable.go
Normal 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
|
||||
}
|
||||
36
pkg/sdk3rd/samwaf/api_vipconfig_uploadsslcert.go
Normal file
36
pkg/sdk3rd/samwaf/api_vipconfig_uploadsslcert.go
Normal 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
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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],
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -233,6 +233,7 @@
|
||||
"s3_upload": "上传到 S3 兼容的对象存储",
|
||||
"safeline": "雷池",
|
||||
"samwaf": "SamWAF",
|
||||
"samwaf_console": "SamWAF - 面板自身",
|
||||
"sectigo": "Sectigo",
|
||||
"simplycom": "Simply.com",
|
||||
"slackbot": "Slack 机器人",
|
||||
|
||||
@ -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": "请输入上传文件前执行的命令"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user