feat(provider): support configuring website match pattern in deployment to 1panel site

This commit is contained in:
Fu Diwei 2025-11-19 20:58:50 +08:00
parent c25458439f
commit 6d296af020
14 changed files with 594 additions and 99 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/certimate-go/certimate/pkg/core/deployer"
onepanelsdk "github.com/certimate-go/certimate/pkg/sdk3rd/1panel"
onepanelsdk2 "github.com/certimate-go/certimate/pkg/sdk3rd/1panel/v2"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type DeployerConfig struct {
@ -30,8 +31,11 @@ type DeployerConfig struct {
NodeName string `json:"nodeName,omitempty"`
// 部署资源类型。
ResourceType string `json:"resourceType"`
// 域名匹配模式。
// 零值时默认值 [WEBSITE_MATCH_PATTERN_EXACT]。
WebsiteMatchPattern string `json:"websiteMatchPattern,omitempty"`
// 网站 ID。
// 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。
// 部署资源类型为 [RESOURCE_TYPE_WEBSITE]、且匹配模式非 [WEBSITE_MATCH_PATTERN_CERTSAN] 时必填。
WebsiteId int64 `json:"websiteId,omitempty"`
// 证书 ID。
// 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
@ -106,10 +110,6 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*dep
}
func (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM string) error {
if d.config.WebsiteId == 0 {
return errors.New("config `websiteId` is required")
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
@ -118,65 +118,54 @@ func (d *Deployer) deployToWebsite(ctx context.Context, certPEM, privkeyPEM stri
d.logger.Info("ssl certificate uploaded", slog.Any("result", upres))
}
switch sdkClient := d.sdkClient.(type) {
case *onepanelsdk.Client:
// 获取待部署的网站列表
var websiteIds []int64
switch d.config.WebsiteMatchPattern {
case "", WEBSITE_MATCH_PATTERN_SPECIFIED:
{
// 获取网站 HTTPS 配置
websiteHttpsGetResp, err := sdkClient.WebsiteHttpsGet(d.config.WebsiteId)
d.logger.Debug("sdk request '1panel.WebsiteHttpsGet'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("response", websiteHttpsGetResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsGet': %w", err)
if d.config.WebsiteId == 0 {
return errors.New("config `websiteId` is required")
}
// 修改网站 HTTPS 配置
sslId, _ := strconv.ParseInt(upres.CertId, 10, 64)
websiteHttpsPostReq := &onepanelsdk.WebsiteHttpsPostRequest{
WebsiteID: d.config.WebsiteId,
Type: "existed",
WebsiteSSLID: sslId,
Enable: websiteHttpsGetResp.Data.Enable,
HttpConfig: websiteHttpsGetResp.Data.HttpConfig,
SSLProtocol: websiteHttpsGetResp.Data.SSLProtocol,
Algorithm: websiteHttpsGetResp.Data.Algorithm,
Hsts: websiteHttpsGetResp.Data.Hsts,
}
websiteHttpsPostResp, err := sdkClient.WebsiteHttpsPost(d.config.WebsiteId, websiteHttpsPostReq)
d.logger.Debug("sdk request '1panel.WebsiteHttpsPost'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("request", websiteHttpsPostReq), slog.Any("response", websiteHttpsPostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsPost': %w", err)
}
websiteIds = []int64{d.config.WebsiteId}
}
case *onepanelsdk2.Client:
case WEBSITE_MATCH_PATTERN_CERTSAN:
{
// 获取网站 HTTPS 配置
websiteHttpsGetResp, err := sdkClient.WebsiteHttpsGet(d.config.WebsiteId)
d.logger.Debug("sdk request '1panel.WebsiteHttpsGet'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("response", websiteHttpsGetResp))
websiteIdCandidates, err := d.getMatchedWebsiteIdsByCertificate(ctx, certPEM)
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsGet': %w", err)
return err
}
// 修改网站 HTTPS 配置
sslId, _ := strconv.ParseInt(upres.CertId, 10, 64)
websiteHttpsPostReq := &onepanelsdk2.WebsiteHttpsPostRequest{
WebsiteID: d.config.WebsiteId,
Type: "existed",
WebsiteSSLID: sslId,
Enable: websiteHttpsGetResp.Data.Enable,
HttpConfig: websiteHttpsGetResp.Data.HttpConfig,
SSLProtocol: websiteHttpsGetResp.Data.SSLProtocol,
Algorithm: websiteHttpsGetResp.Data.Algorithm,
Hsts: websiteHttpsGetResp.Data.Hsts,
}
websiteHttpsPostResp, err := sdkClient.WebsiteHttpsPost(d.config.WebsiteId, websiteHttpsPostReq)
d.logger.Debug("sdk request '1panel.WebsiteHttpsPost'", slog.Int64("websiteId", d.config.WebsiteId), slog.Any("request", websiteHttpsPostReq), slog.Any("response", websiteHttpsPostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsPost': %w", err)
}
websiteIds = websiteIdCandidates
}
default:
panic("unreachable")
return fmt.Errorf("unsupported website match pattern: '%s'", d.config.WebsiteMatchPattern)
}
// 遍历更新网站证书
if len(websiteIds) == 0 {
d.logger.Info("no websites to deploy")
} else {
d.logger.Info("found websites to deploy", slog.Any("websiteIds", websiteIds))
var errs []error
websiteSSLId, _ := strconv.ParseInt(upres.CertId, 10, 64)
for _, websiteId := range websiteIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateWebsiteCertificate(ctx, websiteId, websiteSSLId); err != nil {
errs = append(errs, err)
}
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
}
return nil
@ -198,6 +187,211 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
return nil
}
func (d *Deployer) getMatchedWebsiteIdsByCertificate(ctx context.Context, certPEM string) ([]int64, error) {
var websiteIds []int64
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
switch sdkClient := d.sdkClient.(type) {
case *onepanelsdk.Client:
{
websiteSearchPage := 1
websiteSearchPageSize := 100
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
websiteSearchReq := &onepanelsdk.WebsiteSearchRequest{
Order: "ascending",
OrderBy: "primary_domain",
Page: int32(websiteSearchPage),
PageSize: int32(websiteSearchPageSize),
}
websiteSearchResp, err := sdkClient.WebsiteSearch(websiteSearchReq)
d.logger.Debug("sdk request '1panel.WebsiteSearch'", slog.Any("request", websiteSearchReq), slog.Any("response", websiteSearchResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSearch': %w", err)
}
if websiteSearchResp.Data == nil {
break
}
for _, websiteItem := range websiteSearchResp.Data.Items {
if certX509.VerifyHostname(websiteItem.PrimaryDomain) != nil {
continue
}
websiteGetResp, err := sdkClient.WebsiteGet(websiteItem.ID)
d.logger.Debug("sdk request '1panel.WebsiteGet'", slog.Int64("websiteId", websiteItem.ID), slog.Any("response", websiteGetResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteGet': %w", err)
}
for _, domainInfo := range websiteGetResp.Data.Domains {
if domainInfo.SSL {
websiteIds = append(websiteIds, websiteItem.ID)
break
}
}
}
if len(websiteSearchResp.Data.Items) < websiteSearchPageSize {
break
}
websiteSearchPage++
}
}
case *onepanelsdk2.Client:
{
websiteSearchPage := 1
websiteSearchPageSize := 100
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
websiteSearchReq := &onepanelsdk2.WebsiteSearchRequest{
Order: "ascending",
OrderBy: "primary_domain",
Page: int32(websiteSearchPage),
PageSize: int32(websiteSearchPageSize),
}
websiteSearchResp, err := sdkClient.WebsiteSearch(websiteSearchReq)
d.logger.Debug("sdk request '1panel.WebsiteSearch'", slog.Any("request", websiteSearchReq), slog.Any("response", websiteSearchResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteSearch': %w", err)
}
if websiteSearchResp.Data == nil {
break
}
for _, websiteItem := range websiteSearchResp.Data.Items {
if certX509.VerifyHostname(websiteItem.PrimaryDomain) != nil {
continue
}
websiteGetResp, err := sdkClient.WebsiteGet(websiteItem.ID)
d.logger.Debug("sdk request '1panel.WebsiteGet'", slog.Int64("websiteId", websiteItem.ID), slog.Any("response", websiteGetResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request '1panel.WebsiteGet': %w", err)
}
for _, domainInfo := range websiteGetResp.Data.Domains {
if domainInfo.SSL {
websiteIds = append(websiteIds, websiteItem.ID)
break
}
}
}
if len(websiteSearchResp.Data.Items) < websiteSearchPageSize {
break
}
websiteSearchPage++
}
}
default:
panic("unreachable")
}
if len(websiteIds) == 0 {
return nil, errors.New("could not find any websites matched by certificate")
}
return websiteIds, nil
}
func (d *Deployer) updateWebsiteCertificate(ctx context.Context, websiteId int64, websiteSSLId int64) error {
switch sdkClient := d.sdkClient.(type) {
case *onepanelsdk.Client:
{
// 获取网站 HTTPS 配置
websiteHttpsGetResp, err := sdkClient.WebsiteHttpsGet(websiteId)
d.logger.Debug("sdk request '1panel.WebsiteHttpsGet'", slog.Int64("websiteId", websiteId), slog.Any("response", websiteHttpsGetResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsGet': %w", err)
} else {
if websiteHttpsGetResp.Data.Enable && websiteHttpsGetResp.Data.WebsiteSSLID == websiteSSLId {
return nil
}
}
// 修改网站 HTTPS 配置
websiteHttpsPostReq := &onepanelsdk.WebsiteHttpsPostRequest{
WebsiteID: websiteId,
Type: "existed",
WebsiteSSLID: websiteSSLId,
Enable: true,
HttpConfig: websiteHttpsGetResp.Data.HttpConfig,
SSLProtocol: websiteHttpsGetResp.Data.SSLProtocol,
Algorithm: websiteHttpsGetResp.Data.Algorithm,
Hsts: websiteHttpsGetResp.Data.Hsts,
}
if websiteHttpsPostReq.HttpConfig == "" {
websiteHttpsPostReq.HttpConfig = "HTTPToHTTPS"
}
websiteHttpsPostResp, err := sdkClient.WebsiteHttpsPost(websiteId, websiteHttpsPostReq)
d.logger.Debug("sdk request '1panel.WebsiteHttpsPost'", slog.Int64("websiteId", websiteId), slog.Any("request", websiteHttpsPostReq), slog.Any("response", websiteHttpsPostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsPost': %w", err)
}
}
case *onepanelsdk2.Client:
{
// 获取网站 HTTPS 配置
websiteHttpsGetResp, err := sdkClient.WebsiteHttpsGet(websiteId)
d.logger.Debug("sdk request '1panel.WebsiteHttpsGet'", slog.Int64("websiteId", websiteId), slog.Any("response", websiteHttpsGetResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsGet': %w", err)
} else {
if websiteHttpsGetResp.Data.Enable && websiteHttpsGetResp.Data.WebsiteSSLID == websiteSSLId {
return nil
}
}
// 修改网站 HTTPS 配置
websiteHttpsPostReq := &onepanelsdk2.WebsiteHttpsPostRequest{
WebsiteID: websiteId,
Type: "existed",
WebsiteSSLID: websiteSSLId,
Enable: true,
HttpConfig: websiteHttpsGetResp.Data.HttpConfig,
SSLProtocol: websiteHttpsGetResp.Data.SSLProtocol,
Algorithm: websiteHttpsGetResp.Data.Algorithm,
Hsts: websiteHttpsGetResp.Data.Hsts,
Http3: websiteHttpsGetResp.Data.Http3,
}
if websiteHttpsPostReq.HttpConfig == "" {
websiteHttpsPostReq.HttpConfig = "HTTPToHTTPS"
}
websiteHttpsPostResp, err := sdkClient.WebsiteHttpsPost(websiteId, websiteHttpsPostReq)
d.logger.Debug("sdk request '1panel.WebsiteHttpsPost'", slog.Int64("websiteId", websiteId), slog.Any("request", websiteHttpsPostReq), slog.Any("response", websiteHttpsPostResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request '1panel.WebsiteHttpsPost': %w", err)
}
}
default:
panic("unreachable")
}
return nil
}
const (
sdkVersionV1 = "v1"
sdkVersionV2 = "v2"

View File

@ -62,6 +62,7 @@ func TestDeploy(t *testing.T) {
ApiKey: fApiKey,
AllowInsecureConnections: true,
ResourceType: provider.RESOURCE_TYPE_WEBSITE,
WebsiteMatchPattern: provider.WEBSITE_MATCH_PATTERN_SPECIFIED,
WebsiteId: fWebsiteId,
})
if err != nil {

View File

@ -6,3 +6,10 @@ const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = "certificate"
)
const (
// 匹配模式:指定 ID。
WEBSITE_MATCH_PATTERN_SPECIFIED = "specified"
// 匹配模式:证书 SAN 匹配。
WEBSITE_MATCH_PATTERN_CERTSAN = "certsan"
)

View File

@ -0,0 +1,64 @@
package onepanel
import (
"context"
"fmt"
"net/http"
)
type WebsiteGetRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Page int32 `json:"page"`
PageSize int32 `json:"pageSize"`
}
type WebsiteGetResponse struct {
apiResponseBase
Data *struct {
ID int64 `json:"id"`
Alias string `json:"alias"`
PrimaryDomain string `json:"primaryDomain"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Status string `json:"status"`
SitePath string `json:"sitePath"`
Remark string `json:"remark"`
Domains []*struct {
ID int64 `json:"id"`
Domain string `json:"domain"`
Port int32 `json:"port"`
SSL bool `json:"ssl"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
} `json:"domains"`
WebsiteSSLId int64 `json:"webSiteSSLId"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
} `json:"data,omitempty"`
}
func (c *Client) WebsiteGet(websiteId int64) (*WebsiteGetResponse, error) {
return c.WebsiteGetWithContext(context.Background(), websiteId)
}
func (c *Client) WebsiteGetWithContext(ctx context.Context, websiteId int64) (*WebsiteGetResponse, error) {
if websiteId == 0 {
return nil, fmt.Errorf("sdkerr: unset websiteId")
}
httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/%d", websiteId))
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &WebsiteGetResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -10,11 +10,12 @@ type WebsiteHttpsGetResponse struct {
apiResponseBase
Data *struct {
Enable bool `json:"enable"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
Enable bool `json:"enable"`
WebsiteSSLID int64 `json:"websiteSSLId"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
} `json:"data,omitempty"`
}

View File

@ -7,19 +7,14 @@ import (
)
type WebsiteHttpsPostRequest struct {
WebsiteID int64 `json:"websiteId"`
Enable bool `json:"enable"`
Type string `json:"type"`
WebsiteSSLID int64 `json:"websiteSSLId"`
PrivateKey string `json:"privateKey"`
Certificate string `json:"certificate"`
PrivateKeyPath string `json:"privateKeyPath"`
CertificatePath string `json:"certificatePath"`
ImportType string `json:"importType"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
WebsiteID int64 `json:"websiteId"`
Enable bool `json:"enable"`
Type string `json:"type"`
WebsiteSSLID int64 `json:"websiteSSLId"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
}
type WebsiteHttpsPostResponse struct {

View File

@ -0,0 +1,58 @@
package onepanel
import (
"context"
"net/http"
)
type WebsiteSearchRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Order string `json:"order"`
OrderBy string `json:"orderBy"`
Page int32 `json:"page"`
PageSize int32 `json:"pageSize"`
}
type WebsiteSearchResponse struct {
apiResponseBase
Data *struct {
Items []*struct {
ID int64 `json:"id"`
Alias string `json:"alias"`
PrimaryDomain string `json:"primaryDomain"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Status string `json:"status"`
SitePath string `json:"sitePath"`
Remark string `json:"remark"`
SSLStatus string `json:"sslStatus"`
SSLExpireDate string `json:"sslExpireDate"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
} `json:"items"`
Total int32 `json:"total"`
} `json:"data,omitempty"`
}
func (c *Client) WebsiteSearch(req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {
return c.WebsiteSearchWithContext(context.Background(), req)
}
func (c *Client) WebsiteSearchWithContext(ctx context.Context, req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/websites/search")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &WebsiteSearchResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -0,0 +1,64 @@
package v2
import (
"context"
"fmt"
"net/http"
)
type WebsiteGetRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Page int32 `json:"page"`
PageSize int32 `json:"pageSize"`
}
type WebsiteGetResponse struct {
apiResponseBase
Data *struct {
ID int64 `json:"id"`
Alias string `json:"alias"`
PrimaryDomain string `json:"primaryDomain"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Status string `json:"status"`
SitePath string `json:"sitePath"`
Remark string `json:"remark"`
Domains []*struct {
ID int64 `json:"id"`
Domain string `json:"domain"`
Port int32 `json:"port"`
SSL bool `json:"ssl"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
} `json:"domains,omitempty"`
WebsiteSSLId int64 `json:"webSiteSSLId"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
} `json:"data,omitempty"`
}
func (c *Client) WebsiteGet(websiteId int64) (*WebsiteGetResponse, error) {
return c.WebsiteGetWithContext(context.Background(), websiteId)
}
func (c *Client) WebsiteGetWithContext(ctx context.Context, websiteId int64) (*WebsiteGetResponse, error) {
if websiteId == 0 {
return nil, fmt.Errorf("sdkerr: unset websiteId")
}
httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("/websites/%d", websiteId))
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &WebsiteGetResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -10,11 +10,13 @@ type WebsiteHttpsGetResponse struct {
apiResponseBase
Data *struct {
Enable bool `json:"enable"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
Enable bool `json:"enable"`
HttpConfig string `json:"httpConfig"`
WebsiteSSLID int64 `json:"websiteSSLId"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
Http3 bool `json:"http3"`
} `json:"data,omitempty"`
}

View File

@ -7,19 +7,15 @@ import (
)
type WebsiteHttpsPostRequest struct {
WebsiteID int64 `json:"websiteId"`
Enable bool `json:"enable"`
Type string `json:"type"`
WebsiteSSLID int64 `json:"websiteSSLId"`
PrivateKey string `json:"privateKey"`
Certificate string `json:"certificate"`
PrivateKeyPath string `json:"privateKeyPath"`
CertificatePath string `json:"certificatePath"`
ImportType string `json:"importType"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
WebsiteID int64 `json:"websiteId"`
Enable bool `json:"enable"`
Type string `json:"type"`
WebsiteSSLID int64 `json:"websiteSSLId"`
HttpConfig string `json:"httpConfig"`
SSLProtocol []string `json:"SSLProtocol"`
Algorithm string `json:"algorithm"`
Hsts bool `json:"hsts"`
Http3 bool `json:"http3"`
}
type WebsiteHttpsPostResponse struct {

View File

@ -0,0 +1,58 @@
package v2
import (
"context"
"net/http"
)
type WebsiteSearchRequest struct {
Name string `json:"name"`
Type string `json:"type"`
Order string `json:"order"`
OrderBy string `json:"orderBy"`
Page int32 `json:"page"`
PageSize int32 `json:"pageSize"`
}
type WebsiteSearchResponse struct {
apiResponseBase
Data *struct {
Items []*struct {
ID int64 `json:"id"`
Alias string `json:"alias"`
PrimaryDomain string `json:"primaryDomain"`
Protocol string `json:"protocol"`
Type string `json:"type"`
Status string `json:"status"`
SitePath string `json:"sitePath"`
Remark string `json:"remark"`
SSLStatus string `json:"sslStatus"`
SSLExpireDate string `json:"sslExpireDate"`
UpdatedAt string `json:"updatedAt"`
CreatedAt string `json:"createdAt"`
} `json:"items"`
Total int32 `json:"total"`
} `json:"data,omitempty"`
}
func (c *Client) WebsiteSearch(req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {
return c.WebsiteSearchWithContext(context.Background(), req)
}
func (c *Client) WebsiteSearchWithContext(ctx context.Context, req *WebsiteSearchRequest) (*WebsiteSearchResponse, error) {
httpreq, err := c.newRequest(http.MethodPost, "/websites/search")
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &WebsiteSearchResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}

View File

@ -1,5 +1,5 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { Form, Input, Radio, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
@ -10,6 +10,9 @@ import { useFormNestedFieldsContext } from "./_context";
const RESOURCE_TYPE_WEBSITE = "website" as const;
const RESOURCE_TYPE_CERTIFICATE = "certificate" as const;
const WEBSITE_MATCH_PATTERN_SPECIFIED = "specified" as const;
const WEBSITE_MATCH_PATTERN_CERTSAN = "certsan" as const;
const BizDeployNodeConfigFieldsProvider1PanelSite = () => {
const { i18n, t } = useTranslation();
@ -22,6 +25,7 @@ const BizDeployNodeConfigFieldsProvider1PanelSite = () => {
const initialValues = getInitialValues();
const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst);
const fieldWebsiteMatchPattern = Form.useWatch([parentNamePath, "websiteMatchPattern"], { form: formInst, preserve: true });
return (
<>
@ -53,14 +57,38 @@ const BizDeployNodeConfigFieldsProvider1PanelSite = () => {
<Show when={fieldResourceType === RESOURCE_TYPE_WEBSITE}>
<Form.Item
name={[parentNamePath, "websiteId"]}
initialValue={initialValues.websiteId}
label={t("workflow_node.deploy.form.1panel_site_website_id.label")}
name={[parentNamePath, "websiteMatchPattern"]}
initialValue={initialValues.websiteMatchPattern}
label={t("workflow_node.deploy.form.1panel_site_website_match_pattern.label")}
extra={
fieldWebsiteMatchPattern === WEBSITE_MATCH_PATTERN_CERTSAN ? (
<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.1panel_site_website_match_pattern.help_certsan") }}></span>
) : (
void 0
)
}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.1panel_site_website_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("workflow_node.deploy.form.1panel_site_website_id.placeholder")} />
<Radio.Group
options={[WEBSITE_MATCH_PATTERN_SPECIFIED, WEBSITE_MATCH_PATTERN_CERTSAN].map((s) => ({
key: s,
label: t(`workflow_node.deploy.form.1panel_site_website_match_pattern.option.${s}.label`),
value: s,
}))}
/>
</Form.Item>
<Show when={fieldWebsiteMatchPattern !== WEBSITE_MATCH_PATTERN_CERTSAN}>
<Form.Item
name={[parentNamePath, "websiteId"]}
initialValue={initialValues.websiteId}
label={t("workflow_node.deploy.form.1panel_site_website_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.1panel_site_website_id.tooltip") }}></span>}
>
<Input type="number" placeholder={t("workflow_node.deploy.form.1panel_site_website_id.placeholder")} />
</Form.Item>
</Show>
</Show>
<Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>
@ -81,6 +109,8 @@ const BizDeployNodeConfigFieldsProvider1PanelSite = () => {
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
resourceType: RESOURCE_TYPE_WEBSITE,
websiteMatchPattern: WEBSITE_MATCH_PATTERN_SPECIFIED,
websiteId: "",
};
};
@ -91,6 +121,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
.object({
nodeName: z.string().nullish(),
resourceType: z.literal([RESOURCE_TYPE_WEBSITE, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")),
websiteMatchPattern: z.string().nullish(),
websiteId: z.union([z.string(), z.number()]).nullish(),
certificateId: z.union([z.string(), z.number()]).nullish(),
})
@ -98,12 +129,26 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
switch (values.resourceType) {
case RESOURCE_TYPE_WEBSITE:
{
const res = z.preprocess((v) => Number(v), z.number().int().positive()).safeParse(values.websiteId);
if (!res.success) {
if (values.websiteMatchPattern) {
switch (values.websiteMatchPattern) {
case WEBSITE_MATCH_PATTERN_SPECIFIED:
{
const res = z.preprocess((v) => Number(v), z.number().int().positive()).safeParse(values.websiteId);
if (!res.success) {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.1panel_site_website_id.placeholder"),
path: ["websiteId"],
});
}
}
break;
}
} else {
ctx.addIssue({
code: "custom",
message: t("workflow_node.deploy.form.1panel_site_website_id.placeholder"),
path: ["websiteId"],
message: t("workflow_node.deploy.form.1panel_site_website_match_pattern.placeholder"),
path: ["websiteMatchPattern"],
});
}
}

View File

@ -203,15 +203,20 @@
"workflow_node.deploy.form.1panel_site_node_name.placeholder": "Please enter 1Panel node name",
"workflow_node.deploy.form.1panel_site_node_name.help": "Notes: It is only used for 1Panel v2+.",
"workflow_node.deploy.form.1panel_site_node_name.tooltip": "You can find it on 1Panel dashboard.",
"workflow_node.deploy.form.aliyun_alb_region.label": "Alibaba Cloud region",
"workflow_node.deploy.form.1panel_site_resource_type.option.website.label": "Website",
"workflow_node.deploy.form.1panel_site_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.1panel_site_website_match_pattern.label": "Website match pattern",
"workflow_node.deploy.form.1panel_site_website_match_pattern.placeholder": "Please select website match pattern",
"workflow_node.deploy.form.1panel_site_website_match_pattern.option.specified.label": "Specified ID",
"workflow_node.deploy.form.1panel_site_website_match_pattern.option.certsan.label": "via Certificate",
"workflow_node.deploy.form.1panel_site_website_match_pattern.help_certsan": "Notes: The website name should be a domain name and include SSL configurations.",
"workflow_node.deploy.form.1panel_site_website_id.label": "1Panel website ID",
"workflow_node.deploy.form.1panel_site_website_id.placeholder": "Please enter 1Panel website ID",
"workflow_node.deploy.form.1panel_site_website_id.tooltip": "You can find it on 1Panel dashboard.",
"workflow_node.deploy.form.1panel_site_certificate_id.label": "1Panel certificate ID",
"workflow_node.deploy.form.1panel_site_certificate_id.placeholder": "Please enter 1Panel certificate ID",
"workflow_node.deploy.form.1panel_site_certificate_id.tooltip": "You can find it on 1Panel dashboard.",
"workflow_node.deploy.form.aliyun_alb_region.label": "Alibaba Cloud region",
"workflow_node.deploy.form.aliyun_alb_region.placeholder": "Please enter Alibaba Cloud ALB region (e.g. cn-hangzhou)",
"workflow_node.deploy.form.aliyun_alb_region.tooltip": "For more information, see <a href=\"https://www.alibabacloud.com/help/en/slb/application-load-balancer/product-overview/supported-regions-and-zones\" target=\"_blank\">https://www.alibabacloud.com/help/en/slb/application-load-balancer/product-overview/supported-regions-and-zones</a>",
"workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label": "ALB load balancer",

View File

@ -204,6 +204,11 @@
"workflow_node.deploy.form.1panel_site_node_name.tooltip": "请登录 1Panel 面板查看",
"workflow_node.deploy.form.1panel_site_resource_type.option.website.label": "部署到指定网站",
"workflow_node.deploy.form.1panel_site_resource_type.option.certificate.label": "替换指定证书",
"workflow_node.deploy.form.1panel_site_website_match_pattern.label": "网站匹配模式",
"workflow_node.deploy.form.1panel_site_website_match_pattern.placeholder": "请选择部署网站匹配模式",
"workflow_node.deploy.form.1panel_site_website_match_pattern.option.specified.label": "指定 ID",
"workflow_node.deploy.form.1panel_site_website_match_pattern.option.certsan.label": "根据证书自动匹配",
"workflow_node.deploy.form.1panel_site_website_match_pattern.help_certsan": "注意:网站名称需要为域名、且包含开启了 SSL 的域名配置。",
"workflow_node.deploy.form.1panel_site_website_id.label": "1Panel 网站 ID",
"workflow_node.deploy.form.1panel_site_website_id.placeholder": "请输入 1Panel 网站 ID",
"workflow_node.deploy.form.1panel_site_website_id.tooltip": "请登录 1Panel 面板查看",