diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 421ab2f3..6b1ffa25 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -109,7 +109,9 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { ApiUrl: access.ApiUrl, ApiKey: access.ApiKey, AllowInsecureConnections: access.AllowInsecureConnections, + ResourceType: p1PanelSite.ResourceType(maputil.GetOrDefaultString(options.ProviderDeployConfig, "resourceType", string(p1PanelSite.RESOURCE_TYPE_WEBSITE))), WebsiteId: maputil.GetInt64(options.ProviderDeployConfig, "websiteId"), + CertificateId: maputil.GetInt64(options.ProviderDeployConfig, "certificateId"), }) return deployer, err @@ -474,7 +476,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { ApiUrl: access.ApiUrl, ApiKey: access.ApiKey, ApiSecret: access.ApiSecret, - ResourceType: pCdnfly.ResourceType(maputil.GetString(options.ProviderDeployConfig, "resourceType")), + ResourceType: pCdnfly.ResourceType(maputil.GetOrDefaultString(options.ProviderDeployConfig, "resourceType", string(pCdnfly.RESOURCE_TYPE_SITE))), SiteId: maputil.GetString(options.ProviderDeployConfig, "siteId"), CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"), }) diff --git a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go index 6aa34607..24f5daa3 100644 --- a/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go +++ b/internal/pkg/core/deployer/providers/1panel-site/1panel_site.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" "log/slog" "net/url" "strconv" @@ -23,8 +24,14 @@ type DeployerConfig struct { ApiKey string `json:"apiKey"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` // 网站 ID。 - WebsiteId int64 `json:"websiteId"` + // 部署资源类型为 [RESOURCE_TYPE_WEBSITE] 时必填。 + WebsiteId int64 `json:"websiteId,omitempty"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId int64 `json:"certificateId,omitempty"` } type DeployerProvider struct { @@ -73,6 +80,30 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_WEBSITE: + if err := d.deployToWebsite(ctx, certPem, privkeyPem); err != nil { + return nil, err + } + + case RESOURCE_TYPE_CERTIFICATE: + if err := d.deployToCertificate(ctx, certPem, privkeyPem); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *DeployerProvider) deployToWebsite(ctx context.Context, certPem string, privkeyPem string) error { + if d.config.WebsiteId == 0 { + return errors.New("config `websiteId` is required") + } + // 获取网站 HTTPS 配置 getHttpsConfReq := &opsdk.GetHttpsConfRequest{ WebsiteID: d.config.WebsiteId, @@ -80,13 +111,13 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe getHttpsConfResp, err := d.sdkClient.GetHttpsConf(getHttpsConfReq) d.logger.Debug("sdk request '1panel.GetHttpsConf'", slog.Any("request", getHttpsConfReq), slog.Any("response", getHttpsConfResp)) if err != nil { - return nil, xerrors.Wrap(err, "failed to execute sdk request '1panel.GetHttpsConf'") + return xerrors.Wrap(err, "failed to execute sdk request '1panel.GetHttpsConf'") } // 上传证书到面板 upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) if err != nil { - return nil, xerrors.Wrap(err, "failed to upload certificate file") + return xerrors.Wrap(err, "failed to upload certificate file") } else { d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) } @@ -106,10 +137,42 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe updateHttpsConfResp, err := d.sdkClient.UpdateHttpsConf(updateHttpsConfReq) d.logger.Debug("sdk request '1panel.UpdateHttpsConf'", slog.Any("request", updateHttpsConfReq), slog.Any("response", updateHttpsConfResp)) if err != nil { - return nil, xerrors.Wrap(err, "failed to execute sdk request '1panel.UpdateHttpsConf'") + return xerrors.Wrap(err, "failed to execute sdk request '1panel.UpdateHttpsConf'") } - return &deployer.DeployResult{}, nil + return nil +} + +func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPem string, privkeyPem string) error { + if d.config.CertificateId == 0 { + return errors.New("config `certificateId` is required") + } + + // 获取证书详情 + getWebsiteSSLReq := &opsdk.GetWebsiteSSLRequest{ + SSLID: d.config.CertificateId, + } + getWebsiteSSLResp, err := d.sdkClient.GetWebsiteSSL(getWebsiteSSLReq) + d.logger.Debug("sdk request '1panel.GetWebsiteSSL'", slog.Any("request", getWebsiteSSLReq), slog.Any("response", getWebsiteSSLResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request '1panel.GetWebsiteSSL'") + } + + // 更新证书 + uploadWebsiteSSLReq := &opsdk.UploadWebsiteSSLRequest{ + Type: "paste", + SSLID: d.config.CertificateId, + Description: getWebsiteSSLResp.Data.Description, + Certificate: certPem, + PrivateKey: privkeyPem, + } + uploadWebsiteSSLResp, err := d.sdkClient.UploadWebsiteSSL(uploadWebsiteSSLReq) + d.logger.Debug("sdk request '1panel.UploadWebsiteSSL'", slog.Any("request", uploadWebsiteSSLReq), slog.Any("response", uploadWebsiteSSLResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request '1panel.UploadWebsiteSSL'") + } + + return nil } func createSdkClient(apiUrl, apiKey string, allowInsecure bool) (*opsdk.Client, error) { diff --git a/internal/pkg/core/deployer/providers/1panel-site/1panel_site_test.go b/internal/pkg/core/deployer/providers/1panel-site/1panel_site_test.go index 1be2444d..5815fd5e 100644 --- a/internal/pkg/core/deployer/providers/1panel-site/1panel_site_test.go +++ b/internal/pkg/core/deployer/providers/1panel-site/1panel_site_test.go @@ -20,7 +20,7 @@ var ( ) func init() { - argsPrefix := "CERTIMATE_DEPLOYER_1PANELCONSOLE_" + argsPrefix := "CERTIMATE_DEPLOYER_1PANELSITE_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") @@ -32,12 +32,12 @@ func init() { /* Shell command to run this test: - go test -v ./1panel_console_test.go -args \ - --CERTIMATE_DEPLOYER_1PANELCONSOLE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ - --CERTIMATE_DEPLOYER_1PANELCONSOLE_INPUTKEYPATH="/path/to/your-input-key.pem" \ - --CERTIMATE_DEPLOYER_1PANELCONSOLE_APIURL="http://127.0.0.1:20410" \ - --CERTIMATE_DEPLOYER_1PANELCONSOLE_APIKEY="your-api-key" \ - --CERTIMATE_DEPLOYER_1PANELCONSOLE_WEBSITEID="your-website-id" + go test -v ./1panel_site_test.go -args \ + --CERTIMATE_DEPLOYER_1PANELSITE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_1PANELSITE_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_1PANELSITE_APIURL="http://127.0.0.1:20410" \ + --CERTIMATE_DEPLOYER_1PANELSITE_APIKEY="your-api-key" \ + --CERTIMATE_DEPLOYER_1PANELSITE_WEBSITEID="your-website-id" */ func TestDeploy(t *testing.T) { flag.Parse() @@ -55,8 +55,9 @@ func TestDeploy(t *testing.T) { deployer, err := provider.NewDeployer(&provider.DeployerConfig{ ApiUrl: fApiUrl, ApiKey: fApiKey, - WebsiteId: fWebsiteId, AllowInsecureConnections: true, + ResourceType: provider.RESOURCE_TYPE_WEBSITE, + WebsiteId: fWebsiteId, }) if err != nil { t.Errorf("err: %+v", err) diff --git a/internal/pkg/core/deployer/providers/1panel-site/consts.go b/internal/pkg/core/deployer/providers/1panel-site/consts.go new file mode 100644 index 00000000..caba1d5c --- /dev/null +++ b/internal/pkg/core/deployer/providers/1panel-site/consts.go @@ -0,0 +1,10 @@ +package onepanelsite + +type ResourceType string + +const ( + // 资源类型:替换指定网站的证书。 + RESOURCE_TYPE_WEBSITE = ResourceType("website") + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate") +) diff --git a/internal/pkg/vendors/1panel-sdk/api.go b/internal/pkg/vendors/1panel-sdk/api.go index 8b0a6d09..8fa15393 100644 --- a/internal/pkg/vendors/1panel-sdk/api.go +++ b/internal/pkg/vendors/1panel-sdk/api.go @@ -17,6 +17,12 @@ func (c *Client) SearchWebsiteSSL(req *SearchWebsiteSSLRequest) (*SearchWebsiteS return resp, err } +func (c *Client) GetWebsiteSSL(req *GetWebsiteSSLRequest) (*GetWebsiteSSLResponse, error) { + resp := &GetWebsiteSSLResponse{} + err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/websites/ssl/%d", req.SSLID), req, resp) + return resp, err +} + func (c *Client) UploadWebsiteSSL(req *UploadWebsiteSSLRequest) (*UploadWebsiteSSLResponse, error) { resp := &UploadWebsiteSSLResponse{} err := c.sendRequestWithResult(http.MethodPost, "/websites/ssl/upload", req, resp) diff --git a/internal/pkg/vendors/1panel-sdk/models.go b/internal/pkg/vendors/1panel-sdk/models.go index af1bdbe9..13f144d9 100644 --- a/internal/pkg/vendors/1panel-sdk/models.go +++ b/internal/pkg/vendors/1panel-sdk/models.go @@ -59,6 +59,28 @@ type SearchWebsiteSSLResponse struct { } `json:"data,omitempty"` } +type GetWebsiteSSLRequest struct { + SSLID int64 `json:"-"` +} + +type GetWebsiteSSLResponse struct { + baseResponse + Data *struct { + ID int64 `json:"id"` + Provider string `json:"provider"` + Description string `json:"description"` + PrimaryDomain string `json:"primaryDomain"` + Domains string `json:"domains"` + Type string `json:"type"` + Organization string `json:"organization"` + Status string `json:"status"` + StartDate string `json:"startDate"` + ExpireDate string `json:"expireDate"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"data,omitempty"` +} + type UploadWebsiteSSLRequest struct { Type string `json:"type"` SSLID int64 `json:"sslID"` diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm1PanelSiteConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm1PanelSiteConfig.tsx index f5a26450..94a9bce2 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm1PanelSiteConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm1PanelSiteConfig.tsx @@ -1,10 +1,14 @@ import { useTranslation } from "react-i18next"; -import { Form, type FormInstance, Input } from "antd"; +import { Form, type FormInstance, Input, Select } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import Show from "@/components/Show"; + type DeployNodeConfigForm1PanelSiteConfigFieldValues = Nullish<{ - websiteId: string | number; + resourceType: string; + websiteId?: string | number; + certificateId?: string | number; }>; export type DeployNodeConfigForm1PanelSiteConfigProps = { @@ -15,8 +19,13 @@ export type DeployNodeConfigForm1PanelSiteConfigProps = { onValuesChange?: (values: DeployNodeConfigForm1PanelSiteConfigFieldValues) => void; }; +const RESOURCE_TYPE_WEBSITE = "website" as const; +const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; + const initFormModel = (): DeployNodeConfigForm1PanelSiteConfigFieldValues => { - return {}; + return { + resourceType: RESOURCE_TYPE_WEBSITE, + }; }; const DeployNodeConfigForm1PanelSiteConfig = ({ @@ -29,12 +38,28 @@ const DeployNodeConfigForm1PanelSiteConfig = ({ const { t } = useTranslation(); const formSchema = z.object({ - websiteId: z.union([z.string(), z.number()]).refine((v) => { - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.1panel_site_website_id.placeholder")), + resourceType: z.union([z.literal(RESOURCE_TYPE_WEBSITE), z.literal(RESOURCE_TYPE_CERTIFICATE)], { + message: t("workflow_node.deploy.form.1panel_site_resource_type.placeholder"), + }), + websiteId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_WEBSITE) return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.1panel_site_website_id.placeholder")), + certificateId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_CERTIFICATE) return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.1panel_site_certificate_id.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); + const fieldResourceType = Form.useWatch("resourceType", formInst); + const handleFormChange = (_: unknown, values: z.infer) => { onValuesChange?.(values); }; @@ -48,14 +73,38 @@ const DeployNodeConfigForm1PanelSiteConfig = ({ name={formName} onValuesChange={handleFormChange} > - } - > - + + + + + } + > + + + + + + } + > + + + ); }; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx index 4ce459ac..45662e75 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormCdnflyConfig.tsx @@ -7,6 +7,7 @@ import Show from "@/components/Show"; type DeployNodeConfigFormCdnflyConfigFieldValues = Nullish<{ resourceType: string; + siteId?: string | number; certificateId?: string | number; }>; @@ -34,10 +35,13 @@ const DeployNodeConfigFormCdnflyConfig = ({ form: formInst, formName, disabled, resourceType: z.union([z.literal(RESOURCE_TYPE_SITE), z.literal(RESOURCE_TYPE_CERTIFICATE)], { message: t("workflow_node.deploy.form.cdnfly_resource_type.placeholder"), }), - siteId: z.union([z.string(), z.number().int()]).refine((v) => { - if (fieldResourceType !== RESOURCE_TYPE_SITE) return true; - return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.cdnfly_site_id.placeholder")), + siteId: z + .union([z.string(), z.number().int()]) + .nullish() + .refine((v) => { + if (fieldResourceType !== RESOURCE_TYPE_SITE) return true; + return /^\d+$/.test(v + "") && +v! > 0; + }, t("workflow_node.deploy.form.cdnfly_site_id.placeholder")), certificateId: z .union([z.string(), z.number().int()]) .nullish() diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index cf44e2c5..887ff208 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -103,9 +103,16 @@ "workflow_node.deploy.form.certificate.tooltip": "The certificate to be deployed comes from the previous nodes of application or upload.", "workflow_node.deploy.form.params_config.label": "Parameter settings", "workflow_node.deploy.form.1panel_console_auto_restart.label": "Auto restart after deployment", + "workflow_node.deploy.form.1panel_site_resource_type.label": "Resource type", + "workflow_node.deploy.form.1panel_site_resource_type.placeholder": "Please select resource type", + "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_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 WebUI.", + "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 WebUI.", "workflow_node.deploy.form.aliyun_alb_resource_type.label": "Resource type", "workflow_node.deploy.form.aliyun_alb_resource_type.placeholder": "Please select resource type", "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 03ce4875..50a93795 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -102,9 +102,16 @@ "workflow_node.deploy.form.certificate.tooltip": "待部署证书来自之前的申请或上传节点。如果选项为空请先确保前序节点配置正确。", "workflow_node.deploy.form.params_config.label": "参数设置", "workflow_node.deploy.form.1panel_console_auto_restart.label": "部署后自动重启面板服务", + "workflow_node.deploy.form.1panel_site_resource_type.label": "证书替换方式", + "workflow_node.deploy.form.1panel_site_resource_type.placeholder": "请选择证书替换方式", + "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_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 管理面板查看。", + "workflow_node.deploy.form.1panel_site_certificate_id.label": "1Panel 证书 ID", + "workflow_node.deploy.form.1panel_site_certificate_id.placeholder": "请输入 1Panel 证书 ID", + "workflow_node.deploy.form.1panel_site_certificate_id.tooltip": "请在 1Panel 管理面板查看。", "workflow_node.deploy.form.aliyun_alb_resource_type.label": "证书替换方式", "workflow_node.deploy.form.aliyun_alb_resource_type.placeholder": "请选择证书替换方式", "workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/QUIC 监听的证书",