From 6d296af02044407197cb37dc180d1b43d02a3afe Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 19 Nov 2025 20:58:50 +0800 Subject: [PATCH] feat(provider): support configuring website match pattern in deployment to 1panel site --- .../providers/1panel-site/1panel_site.go | 298 +++++++++++++++--- .../providers/1panel-site/1panel_site_test.go | 1 + .../deployer/providers/1panel-site/consts.go | 7 + pkg/sdk3rd/1panel/api_website_get.go | 64 ++++ pkg/sdk3rd/1panel/api_website_https_get.go | 11 +- pkg/sdk3rd/1panel/api_website_https_post.go | 21 +- pkg/sdk3rd/1panel/api_website_search.go | 58 ++++ pkg/sdk3rd/1panel/v2/api_website_get.go | 64 ++++ pkg/sdk3rd/1panel/v2/api_website_https_get.go | 12 +- .../1panel/v2/api_website_https_post.go | 22 +- pkg/sdk3rd/1panel/v2/api_website_search.go | 58 ++++ ...ployNodeConfigFieldsProvider1PanelSite.tsx | 65 +++- .../i18n/locales/en/nls.workflow.nodes.json | 7 +- .../i18n/locales/zh/nls.workflow.nodes.json | 5 + 14 files changed, 594 insertions(+), 99 deletions(-) create mode 100644 pkg/sdk3rd/1panel/api_website_get.go create mode 100644 pkg/sdk3rd/1panel/api_website_search.go create mode 100644 pkg/sdk3rd/1panel/v2/api_website_get.go create mode 100644 pkg/sdk3rd/1panel/v2/api_website_search.go diff --git a/pkg/core/deployer/providers/1panel-site/1panel_site.go b/pkg/core/deployer/providers/1panel-site/1panel_site.go index de1a1f49..b91bc670 100644 --- a/pkg/core/deployer/providers/1panel-site/1panel_site.go +++ b/pkg/core/deployer/providers/1panel-site/1panel_site.go @@ -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" diff --git a/pkg/core/deployer/providers/1panel-site/1panel_site_test.go b/pkg/core/deployer/providers/1panel-site/1panel_site_test.go index ef17d61b..fe93f09d 100644 --- a/pkg/core/deployer/providers/1panel-site/1panel_site_test.go +++ b/pkg/core/deployer/providers/1panel-site/1panel_site_test.go @@ -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 { diff --git a/pkg/core/deployer/providers/1panel-site/consts.go b/pkg/core/deployer/providers/1panel-site/consts.go index ab403c50..91e1b430 100644 --- a/pkg/core/deployer/providers/1panel-site/consts.go +++ b/pkg/core/deployer/providers/1panel-site/consts.go @@ -6,3 +6,10 @@ const ( // 资源类型:替换指定证书。 RESOURCE_TYPE_CERTIFICATE = "certificate" ) + +const ( + // 匹配模式:指定 ID。 + WEBSITE_MATCH_PATTERN_SPECIFIED = "specified" + // 匹配模式:证书 SAN 匹配。 + WEBSITE_MATCH_PATTERN_CERTSAN = "certsan" +) diff --git a/pkg/sdk3rd/1panel/api_website_get.go b/pkg/sdk3rd/1panel/api_website_get.go new file mode 100644 index 00000000..6841c88e --- /dev/null +++ b/pkg/sdk3rd/1panel/api_website_get.go @@ -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 +} diff --git a/pkg/sdk3rd/1panel/api_website_https_get.go b/pkg/sdk3rd/1panel/api_website_https_get.go index 25f00eed..d09b683a 100644 --- a/pkg/sdk3rd/1panel/api_website_https_get.go +++ b/pkg/sdk3rd/1panel/api_website_https_get.go @@ -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"` } diff --git a/pkg/sdk3rd/1panel/api_website_https_post.go b/pkg/sdk3rd/1panel/api_website_https_post.go index 998fbd4b..fa4de62f 100644 --- a/pkg/sdk3rd/1panel/api_website_https_post.go +++ b/pkg/sdk3rd/1panel/api_website_https_post.go @@ -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 { diff --git a/pkg/sdk3rd/1panel/api_website_search.go b/pkg/sdk3rd/1panel/api_website_search.go new file mode 100644 index 00000000..e2263d52 --- /dev/null +++ b/pkg/sdk3rd/1panel/api_website_search.go @@ -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 +} diff --git a/pkg/sdk3rd/1panel/v2/api_website_get.go b/pkg/sdk3rd/1panel/v2/api_website_get.go new file mode 100644 index 00000000..9681533f --- /dev/null +++ b/pkg/sdk3rd/1panel/v2/api_website_get.go @@ -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 +} diff --git a/pkg/sdk3rd/1panel/v2/api_website_https_get.go b/pkg/sdk3rd/1panel/v2/api_website_https_get.go index 757077f9..a18a2c13 100644 --- a/pkg/sdk3rd/1panel/v2/api_website_https_get.go +++ b/pkg/sdk3rd/1panel/v2/api_website_https_get.go @@ -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"` } diff --git a/pkg/sdk3rd/1panel/v2/api_website_https_post.go b/pkg/sdk3rd/1panel/v2/api_website_https_post.go index de05a08c..1b96e409 100644 --- a/pkg/sdk3rd/1panel/v2/api_website_https_post.go +++ b/pkg/sdk3rd/1panel/v2/api_website_https_post.go @@ -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 { diff --git a/pkg/sdk3rd/1panel/v2/api_website_search.go b/pkg/sdk3rd/1panel/v2/api_website_search.go new file mode 100644 index 00000000..7bc5706b --- /dev/null +++ b/pkg/sdk3rd/1panel/v2/api_website_search.go @@ -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 +} diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1PanelSite.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1PanelSite.tsx index 76e2d9b7..7567d7ea 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1PanelSite.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider1PanelSite.tsx @@ -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 = () => { + ) : ( + void 0 + ) + } rules={[formRule]} - tooltip={} > - + ({ + key: s, + label: t(`workflow_node.deploy.form.1panel_site_website_match_pattern.option.${s}.label`), + value: s, + }))} + /> + + + } + > + + + @@ -81,6 +109,8 @@ const BizDeployNodeConfigFieldsProvider1PanelSite = () => { const getInitialValues = (): Nullish>> => { return { resourceType: RESOURCE_TYPE_WEBSITE, + websiteMatchPattern: WEBSITE_MATCH_PATTERN_SPECIFIED, + websiteId: "", }; }; @@ -91,6 +121,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) .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 }) 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"], }); } } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 5d081d57..b0b93d1f 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -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 https://www.alibabacloud.com/help/en/slb/application-load-balancer/product-overview/supported-regions-and-zones", "workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label": "ALB load balancer", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 167ea5d2..5cc2f784 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -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 面板查看",