Merge pull request #276 from fudiwei/feat/cloud-load-balance

feat: tencent clb deployer
This commit is contained in:
usual2970 2024-11-02 09:46:42 +08:00 committed by GitHub
commit c41f34c352
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 2728 additions and 2607 deletions

7
go.mod
View File

@ -12,7 +12,6 @@ require (
github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3
github.com/alibabacloud-go/slb-20140515/v4 v4.0.9
github.com/alibabacloud-go/tea v1.2.2
github.com/alibabacloud-go/tea-utils/v2 v2.0.6
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/go-acme/lego/v4 v4.19.2
github.com/gojek/heimdall/v7 v7.0.3
@ -25,7 +24,8 @@ require (
github.com/pocketbase/pocketbase v0.22.18
github.com/qiniu/go-sdk/v7 v7.22.0
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030
golang.org/x/crypto v0.28.0
@ -41,6 +41,7 @@ require (
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect
github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect
github.com/blinkbean/dingtalk v1.1.3 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
@ -141,7 +142,7 @@ require (
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nrdcg/namesilo v0.2.1 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/errors v0.9.1
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cobra v1.8.1 // indirect

7
go.sum
View File

@ -204,8 +204,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I=
github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s=
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw=
@ -460,11 +458,14 @@ github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQ
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 h1:OymmfmyFkvHirY3WHsoRT3cdTEsqygLbMn8jM41erK4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017/go.mod h1:gnLxGXlLmF+jDqWR1/RVoF/UUwxQxomQhkc0oN7KeuI=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031 h1:/eVMCl+jadCex6HxNN6/hFbC0iWl+e8s4PSIcI8aqS4=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031/go.mod h1:8Km0fRIaDS7PssuyxDFvRRFBUFmECqG+ICpViCs/Vak=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031 h1:3ouglYKE5cwhx2vwICGeW7pAlwyCLnpQd7O0l3hCSTg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg=

View File

@ -6,25 +6,29 @@ import (
"errors"
"fmt"
alb20200616 "github.com/alibabacloud-go/alb-20200616/v2/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
)
type AliyunALBDeployer struct {
option *DeployerOption
infos []string
sdkClient *alb20200616.Client
sdkClient *aliyunAlb.Client
sslUploader uploader.Uploader
}
func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunALBDeployer{}).createSdkClient(
access.AccessKeyId,
@ -32,16 +36,16 @@ func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) {
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: option.DeployConfig.GetConfigAsString("region"),
})
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunALBDeployer{
@ -56,7 +60,7 @@ func (d *AliyunALBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunALBDeployer) GetInfo() []string {
func (d *AliyunALBDeployer) GetInfos() []string {
return d.infos
}
@ -77,12 +81,12 @@ func (d *AliyunALBDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*alb20200616.Client, error) {
func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州
}
aConfig := &openapi.Config{
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
@ -96,7 +100,7 @@ func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region
}
aConfig.Endpoint = tea.String(endpoint)
client, err := alb20200616.NewClient(aConfig)
client, err := aliyunAlb.NewClient(aConfig)
if err != nil {
return nil, err
}
@ -114,12 +118,12 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
getLoadBalancerAttributeReq := &alb20200616.GetLoadBalancerAttributeRequest{
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(aliLoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp))
@ -130,7 +134,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
listListenersReq := &alb20200616.ListListenersRequest{
listListenersReq := &aliyunAlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
@ -138,7 +142,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
@ -162,7 +166,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
listListenersPage = 1
listListenersToken = nil
for {
listListenersReq := &alb20200616.ListListenersRequest{
listListenersReq := &aliyunAlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
@ -170,7 +174,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
@ -190,17 +194,17 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error {
d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", aliListenerIds))
// 上传证书到 SSL
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听证书
var errs []error
for _, aliListenerId := range aliListenerIds {
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil {
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
@ -218,15 +222,15 @@ func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error {
}
// 上传证书到 SSL
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil {
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
return err
}
@ -236,27 +240,27 @@ func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error {
func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
getListenerAttributeReq := &alb20200616.GetListenerAttributeRequest{
getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 ALB 监听配置", getListenerAttributeResp))
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
updateListenerAttributeReq := &alb20200616.UpdateListenerAttributeRequest{
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
Certificates: []*alb20200616.UpdateListenerAttributeRequestCertificates{{
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
CertificateId: tea.String(aliCertId),
}},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
}
d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp))

View File

@ -4,39 +4,41 @@ import (
"context"
"encoding/json"
"fmt"
"time"
cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v5/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
)
type AliyunCDNDeployer struct {
client *cdn20180510.Client
option *DeployerOption
infos []string
sdkClient *aliyunCdn.Client
}
func NewAliyunCDNDeployer(option *DeployerOption) (*AliyunCDNDeployer, error) {
func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
d := &AliyunCDNDeployer{
option: option,
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
client, err := (&AliyunCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunCDNDeployer{
client: client,
option: option,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
@ -44,41 +46,43 @@ func (d *AliyunCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunCDNDeployer) GetInfo() []string {
func (d *AliyunCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunCDNDeployer) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6))
setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(getDeployString(d.option.DeployConfig, "domain")),
CertName: tea.String(certName),
// 设置 CDN 域名域名证书
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{
DomainName: tea.String(d.option.DeployConfig.GetConfigAsString("domain")),
CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
}
runtime := &util.RuntimeOptions{}
resp, err := d.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime)
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
if err != nil {
return err
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
}
d.infos = append(d.infos, toStr("cdn设置证书", resp))
d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp))
return nil
}
func (d *AliyunCDNDeployer) createClient(accessKeyId, accessKeySecret string) (_result *cdn20180510.Client, _err error) {
config := &openapi.Config{
func (d *AliyunCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) {
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("cdn.aliyuncs.com"),
}
config.Endpoint = tea.String("cdn.aliyuncs.com")
_result = &cdn20180510.Client{}
_result, _err = cdn20180510.NewClient(config)
return _result, _err
client, err := aliyunCdn.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -6,25 +6,29 @@ import (
"errors"
"fmt"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderAliyunSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb"
)
type AliyunCLBDeployer struct {
option *DeployerOption
infos []string
sdkClient *slb20140515.Client
sdkClient *aliyunSlb.Client
sslUploader uploader.Uploader
}
func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunCLBDeployer{}).createSdkClient(
access.AccessKeyId,
@ -32,16 +36,16 @@ func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) {
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploader.NewAliyunSLBUploader(&uploader.AliyunSLBUploaderConfig{
uploader, err := uploaderAliyunSlb.New(&uploaderAliyunSlb.AliyunSLBUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: option.DeployConfig.GetConfigAsString("region"),
})
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunCLBDeployer{
@ -56,7 +60,7 @@ func (d *AliyunCLBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunCLBDeployer) GetInfo() []string {
func (d *AliyunCLBDeployer) GetInfos() []string {
return d.infos
}
@ -77,12 +81,12 @@ func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) {
func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
}
aConfig := &openapi.Config{
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
@ -99,7 +103,7 @@ func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region
}
aConfig.Endpoint = tea.String(endpoint)
client, err := slb20140515.NewClient(aConfig)
client, err := aliyunSlb.NewClient(aConfig)
if err != nil {
return nil, err
}
@ -109,21 +113,20 @@ func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region
func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
aliListenerPorts := make([]int32, 0)
if aliLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
aliListenerPorts := make([]int32, 0)
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
describeLoadBalancerAttributeReq := &slb20140515.DescribeLoadBalancerAttributeRequest{
describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
LoadBalancerId: tea.String(aliLoadbalancerId),
}
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp))
@ -134,7 +137,7 @@ func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
describeLoadBalancerListenersReq := &slb20140515.DescribeLoadBalancerListenersRequest{
describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
@ -143,7 +146,7 @@ func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
}
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'")
}
if describeLoadBalancerListenersResp.Body.Listeners != nil {
@ -163,17 +166,17 @@ func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", aliListenerPorts))
// 上传证书到 SLB
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听证书
var errs []error
for _, aliListenerPort := range aliListenerPorts {
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil {
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil {
errs = append(errs, err)
}
}
@ -196,15 +199,15 @@ func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error {
}
// 上传证书到 SLB
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil {
if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil {
return err
}
@ -214,27 +217,27 @@ func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error {
func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error {
// 查询监听配置
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
describeLoadBalancerHTTPSListenerAttributeReq := &slb20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest{
describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
LoadBalancerId: tea.String(aliLoadbalancerId),
ListenerPort: tea.Int32(aliListenerPort),
}
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp))
// 查询扩展域名
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
describeDomainExtensionsReq := &slb20140515.DescribeDomainExtensionsRequest{
describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
LoadBalancerId: tea.String(aliLoadbalancerId),
ListenerPort: tea.Int32(aliListenerPort),
}
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'")
}
d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp))
@ -249,14 +252,14 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo
break
}
setDomainExtensionAttributeReq := &slb20140515.SetDomainExtensionAttributeRequest{
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
ServerCertificateId: tea.String(aliCertId),
}
_, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'")
}
}
}
@ -265,7 +268,7 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
//
// 注意修改监听配置要放在修改扩展域名之后
setLoadBalancerHTTPSListenerAttributeReq := &slb20140515.SetLoadBalancerHTTPSListenerAttributeRequest{
setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{
RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")),
LoadBalancerId: tea.String(aliLoadbalancerId),
ListenerPort: tea.Int32(aliListenerPort),
@ -273,7 +276,7 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo
}
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'")
}
d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp))

View File

@ -0,0 +1,95 @@
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type AliyunDCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *aliyunDcdn.Client
}
func NewAliyunDCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunDCDNDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunDCDNDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *AliyunDCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunDCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunDCDNDeployer) Deploy(ctx context.Context) error {
// 支持泛解析域名,在 Aliyun DCDN 中泛解析域名表示为 .example.com
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
// 配置域名证书
// REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate
setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")),
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
}
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'")
}
d.infos = append(d.infos, toStr("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp))
return nil
}
func (d *AliyunDCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) {
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
Endpoint: tea.String("dcdn.aliyuncs.com"),
}
client, err := aliyunDcdn.NewClient(aConfig)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -1,97 +0,0 @@
/*
* @Author: Bin
* @Date: 2024-09-17
* @FilePath: /certimate/internal/deployer/aliyun_esa.go
*/
package deployer
import (
"context"
"encoding/json"
"fmt"
"strings"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
)
type AliyunESADeployer struct {
client *dcdn20180115.Client
option *DeployerOption
infos []string
}
func NewAliyunESADeployer(option *DeployerOption) (*AliyunESADeployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
d := &AliyunESADeployer{
option: option,
}
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
if err != nil {
return nil, err
}
return &AliyunESADeployer{
client: client,
option: option,
infos: make([]string, 0),
}, nil
}
func (d *AliyunESADeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunESADeployer) GetInfo() []string {
return d.infos
}
func (d *AliyunESADeployer) Deploy(ctx context.Context) error {
certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6))
// 支持泛解析域名,在 Aliyun DCND 中泛解析域名表示为 .example.com
domain := getDeployString(d.option.DeployConfig, "domain")
if strings.HasPrefix(domain, "*") {
domain = strings.TrimPrefix(domain, "*")
}
setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{
DomainName: tea.String(domain),
CertName: tea.String(certName),
CertType: tea.String("upload"),
SSLProtocol: tea.String("on"),
SSLPub: tea.String(d.option.Certificate.Certificate),
SSLPri: tea.String(d.option.Certificate.PrivateKey),
CertRegion: tea.String("cn-hangzhou"),
}
runtime := &util.RuntimeOptions{}
resp, err := d.client.SetDcdnDomainSSLCertificateWithOptions(setDcdnDomainSSLCertificateRequest, runtime)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("dcdn设置证书", resp))
return nil
}
func (d *AliyunESADeployer) createClient(accessKeyId, accessKeySecret string) (_result *dcdn20180115.Client, _err error) {
config := &openapi.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
config.Endpoint = tea.String("dcdn.aliyuncs.com")
_result = &dcdn20180115.Client{}
_result, _err = dcdn20180115.NewClient(config)
return _result, _err
}

View File

@ -6,25 +6,29 @@ import (
"errors"
"fmt"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
nlb20220430 "github.com/alibabacloud-go/nlb-20220430/v2/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
)
type AliyunNLBDeployer struct {
option *DeployerOption
infos []string
sdkClient *nlb20220430.Client
sdkClient *aliyunNlb.Client
sslUploader uploader.Uploader
}
func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&AliyunNLBDeployer{}).createSdkClient(
access.AccessKeyId,
@ -32,16 +36,16 @@ func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) {
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{
uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{
AccessKeyId: access.AccessKeyId,
AccessKeySecret: access.AccessKeySecret,
Region: option.DeployConfig.GetConfigAsString("region"),
})
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &AliyunNLBDeployer{
@ -56,7 +60,7 @@ func (d *AliyunNLBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunNLBDeployer) GetInfo() []string {
func (d *AliyunNLBDeployer) GetInfos() []string {
return d.infos
}
@ -77,12 +81,12 @@ func (d *AliyunNLBDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*nlb20220430.Client, error) {
func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
}
aConfig := &openapi.Config{
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
@ -94,7 +98,7 @@ func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region
}
aConfig.Endpoint = tea.String(endpoint)
client, err := nlb20220430.NewClient(aConfig)
client, err := aliyunNlb.NewClient(aConfig)
if err != nil {
return nil, err
}
@ -112,12 +116,12 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error {
// 查询负载均衡实例的详细信息
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
getLoadBalancerAttributeReq := &nlb20220430.GetLoadBalancerAttributeRequest{
getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{
LoadBalancerId: tea.String(aliLoadbalancerId),
}
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp))
@ -128,7 +132,7 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error {
listListenersLimit := int32(100)
var listListenersToken *string = nil
for {
listListenersReq := &nlb20220430.ListListenersRequest{
listListenersReq := &aliyunNlb.ListListenersRequest{
MaxResults: tea.Int32(listListenersLimit),
NextToken: listListenersToken,
LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)},
@ -136,7 +140,7 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error {
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'")
}
if listListenersResp.Body.Listeners != nil {
@ -156,17 +160,17 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error {
d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", aliListenerIds))
// 上传证书到 SSL
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听证书
var errs []error
for _, aliListenerId := range aliListenerIds {
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil {
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
@ -184,15 +188,15 @@ func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error {
}
// 上传证书到 SSL
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听
if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil {
if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil {
return err
}
@ -202,25 +206,25 @@ func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error {
func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error {
// 查询监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
getListenerAttributeReq := &nlb20220430.GetListenerAttributeRequest{
getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
}
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'")
}
d.infos = append(d.infos, toStr("已查询到 NLB 监听配置", getListenerAttributeResp))
// 修改监听的属性
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
updateListenerAttributeReq := &nlb20220430.UpdateListenerAttributeRequest{
updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{
ListenerId: tea.String(aliListenerId),
CertificateIds: []*string{tea.String(aliCertId)},
}
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'")
}
d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp))

View File

@ -3,48 +3,62 @@ package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
)
type AliyunOSSDeployer struct {
client *oss.Client
option *DeployerOption
infos []string
sdkClient *oss.Client
}
func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.AliyunAccess{}
json.Unmarshal([]byte(option.Access), access)
d := &AliyunOSSDeployer{
option: option,
infos: make([]string, 0),
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret)
client, err := (&AliyunOSSDeployer{}).createSdkClient(
access.AccessKeyId,
access.AccessKeySecret,
option.DeployConfig.GetConfigAsString("endpoint"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
d.client = client
return d, nil
return &AliyunOSSDeployer{
option: option,
infos: make([]string, 0),
sdkClient: client,
}, nil
}
func (d *AliyunOSSDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *AliyunOSSDeployer) GetInfo() []string {
func (d *AliyunOSSDeployer) GetInfos() []string {
return d.infos
}
func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error {
err := d.client.PutBucketCnameWithCertificate(getDeployString(d.option.DeployConfig, "bucket"), oss.PutBucketCname{
Cname: getDeployString(d.option.DeployConfig, "domain"),
aliBucket := d.option.DeployConfig.GetConfigAsString("bucket")
if aliBucket == "" {
return errors.New("`bucket` is required")
}
// 为存储空间绑定自定义域名
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
err := d.sdkClient.PutBucketCnameWithCertificate(aliBucket, oss.PutBucketCname{
Cname: d.option.DeployConfig.GetConfigAsString("domain"),
CertificateConfiguration: &oss.CertificateConfiguration{
Certificate: d.option.Certificate.Certificate,
PrivateKey: d.option.Certificate.PrivateKey,
@ -52,19 +66,21 @@ func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error {
},
})
if err != nil {
return fmt.Errorf("deploy aliyun oss error: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
}
return nil
}
func (d *AliyunOSSDeployer) createClient(accessKeyId, accessKeySecret string) (*oss.Client, error) {
client, err := oss.New(
getDeployString(d.option.DeployConfig, "endpoint"),
accessKeyId,
accessKeySecret,
)
if err != nil {
return nil, fmt.Errorf("create aliyun client error: %w", err)
func (d *AliyunOSSDeployer) createSdkClient(accessKeyId, accessKeySecret, endpoint string) (*oss.Client, error) {
if endpoint == "" {
endpoint = "oss.aliyuncs.com"
}
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -22,7 +22,7 @@ import (
const (
targetAliyunOSS = "aliyun-oss"
targetAliyunCDN = "aliyun-cdn"
targetAliyunESA = "aliyun-dcdn"
targetAliyunDCDN = "aliyun-dcdn"
targetAliyunCLB = "aliyun-clb"
targetAliyunALB = "aliyun-alb"
targetAliyunNLB = "aliyun-nlb"
@ -52,7 +52,7 @@ type DeployerOption struct {
type Deployer interface {
Deploy(ctx context.Context) error
GetInfo() []string
GetInfos() []string
GetID() string
}
@ -112,8 +112,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
return NewAliyunOSSDeployer(option)
case targetAliyunCDN:
return NewAliyunCDNDeployer(option)
case targetAliyunESA:
return NewAliyunESADeployer(option)
case targetAliyunDCDN:
return NewAliyunDCDNDeployer(option)
case targetAliyunCLB:
return NewAliyunCLBDeployer(option)
case targetAliyunALB:
@ -156,41 +156,6 @@ func toStr(tag string, data any) string {
return tag + "" + string(byts)
}
func getDeployString(conf domain.DeployConfig, key string) string {
if _, ok := conf.Config[key]; !ok {
return ""
}
val, ok := conf.Config[key].(string)
if !ok {
return ""
}
return val
}
func getDeployVariables(conf domain.DeployConfig) map[string]string {
rs := make(map[string]string)
data, ok := conf.Config["variables"]
if !ok {
return rs
}
bts, _ := json.Marshal(data)
kvData := make([]domain.KV, 0)
if err := json.Unmarshal(bts, &kvData); err != nil {
return rs
}
for _, kv := range kvData {
rs[kv.Key] = kv.Value
}
return rs
}
func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
cert, err := x509.ParseCertificateFromPEM(certificate)
if err != nil {
@ -204,7 +169,7 @@ func convertPEMToPFX(certificate string, privateKey string, password string) ([]
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password)
if err != nil {
return nil, fmt.Errorf("failed to encode as pfx %w", err)
return nil, err
}
return pfxData, nil

View File

@ -4,30 +4,32 @@ import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderHcScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
hcCdnEx "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk"
)
type HuaweiCloudCDNDeployer struct {
option *DeployerOption
infos []string
sdkClient *hcCdn.CdnClient
sdkClient *hcCdnEx.Client
sslUploader uploader.Uploader
}
func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.HuaweiCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient(
@ -36,17 +38,16 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) {
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
// TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版
uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{
uploader, err := uploaderHcScm.New(&uploaderHcScm.HuaweiCloudSCMUploaderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: "",
})
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudCDNDeployer{
@ -61,11 +62,19 @@ func (d *HuaweiCloudCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *HuaweiCloudCDNDeployer) GetInfo() []string {
func (d *HuaweiCloudCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 查询加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
@ -73,7 +82,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
}
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
if err != nil {
return err
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
}
d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp))
@ -81,37 +90,21 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
// 更新加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
updateDomainMultiCertificatesReqBodyContent := &huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent{}
updateDomainMultiCertificatesReqBodyContent := &hcCdnEx.UpdateDomainMultiCertificatesExRequestBodyContent{}
updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain")
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
var updateDomainMultiCertificatesResp *hcCdnModel.UpdateDomainMultiCertificatesResponse
if d.option.DeployConfig.GetConfigAsBool("useSCM") {
// 上传证书到 SCM
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(uploadResult.CertId)
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(uploadResult.CertName)
} else {
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(0)
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli()))
updateDomainMultiCertificatesReqBodyContent.Certificate = cast.StringPtr(d.option.Certificate.Certificate)
updateDomainMultiCertificatesReqBodyContent.PrivateKey = cast.StringPtr(d.option.Certificate.PrivateKey)
}
updateDomainMultiCertificatesReqBodyContent = mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, updateDomainMultiCertificatesReqBodyContent)
updateDomainMultiCertificatesReq := &huaweicloudCDNUpdateDomainMultiCertificatesRequest{
Body: &huaweicloudCDNUpdateDomainMultiCertificatesRequestBody{
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId)
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName)
updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs)
updateDomainMultiCertificatesReq := &hcCdnEx.UpdateDomainMultiCertificatesExRequest{
Body: &hcCdnEx.UpdateDomainMultiCertificatesExRequestBody{
Https: updateDomainMultiCertificatesReqBodyContent,
},
}
updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(d.sdkClient, updateDomainMultiCertificatesReq)
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
if err != nil {
return err
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
}
d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp))
@ -119,7 +112,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdn.CdnClient, error) {
func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdnEx.Client, error) {
if region == "" {
region = "cn-north-1" // CDN 服务默认区域:华北一北京
}
@ -145,69 +138,6 @@ func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, r
return nil, err
}
client := hcCdn.NewCdnClient(hcClient)
client := hcCdnEx.NewClient(hcClient)
return client, nil
}
type huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent struct {
hcCdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"`
SCMCertificateId *string `json:"scm_certificate_id,omitempty"`
}
type huaweicloudCDNUpdateDomainMultiCertificatesRequestBody struct {
Https *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent `json:"https,omitempty"`
}
type huaweicloudCDNUpdateDomainMultiCertificatesRequest struct {
Body *huaweicloudCDNUpdateDomainMultiCertificatesRequestBody `json:"body,omitempty"`
}
func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *hcCdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*hcCdnModel.UpdateDomainMultiCertificatesResponse, error) {
// 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求
// 可能需要等之后 SDK 更新
requestDef := hcCdn.GenReqDefForUpdateDomainMultiCertificates()
if resp, err := client.HcClient.Sync(request, requestDef); err != nil {
return nil, err
} else {
return resp.(*hcCdnModel.UpdateDomainMultiCertificatesResponse), nil
}
}
func mergeHuaweiCloudCDNConfig(src *hcCdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent {
if src == nil {
return dest
}
// 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去
// 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化
if *src.OriginProtocol == "follow" {
dest.AccessOriginWay = cast.Int32Ptr(1)
} else if *src.OriginProtocol == "http" {
dest.AccessOriginWay = cast.Int32Ptr(2)
} else if *src.OriginProtocol == "https" {
dest.AccessOriginWay = cast.Int32Ptr(3)
}
if src.ForceRedirect != nil {
dest.ForceRedirectConfig = &hcCdnModel.ForceRedirect{}
if src.ForceRedirect.Status == "on" {
dest.ForceRedirectConfig.Switch = 1
dest.ForceRedirectConfig.RedirectType = src.ForceRedirect.Type
} else {
dest.ForceRedirectConfig.Switch = 0
}
}
if src.Https != nil {
if *src.Https.Http2Status == "on" {
dest.Http2 = cast.Int32Ptr(1)
}
}
return dest
}

View File

@ -16,9 +16,11 @@ import (
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderHcElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
@ -33,7 +35,7 @@ type HuaweiCloudELBDeployer struct {
func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.HuaweiCloudAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&HuaweiCloudELBDeployer{}).createSdkClient(
@ -42,16 +44,16 @@ func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) {
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{
uploader, err := uploaderHcElb.New(&uploaderHcElb.HuaweiCloudELBUploaderConfig{
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
Region: option.DeployConfig.GetConfigAsString("region"),
})
if err != nil {
return nil, err
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &HuaweiCloudELBDeployer{
@ -66,21 +68,24 @@ func (d *HuaweiCloudELBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *HuaweiCloudELBDeployer) GetInfo() []string {
func (d *HuaweiCloudELBDeployer) GetInfos() []string {
return d.infos
}
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "certificate":
// 部署到指定证书
if err := d.deployToCertificate(ctx); err != nil {
return err
}
case "loadbalancer":
// 部署到指定负载均衡器
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
// 部署到指定监听器
if err := d.deployToListener(ctx); err != nil {
return err
}
@ -169,7 +174,7 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r
if err != nil {
return "", err
} else if response.Projects == nil || len(*response.Projects) == 0 {
return "", fmt.Errorf("no project found")
return "", errors.New("no project found")
}
return (*response.Projects)[0].Id, nil
@ -194,7 +199,7 @@ func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error
}
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.UpdateCertificate': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
}
d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp))
@ -217,7 +222,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
}
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
}
d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp))
@ -235,7 +240,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
}
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
}
if listListenersResp.Listeners != nil {
@ -254,17 +259,17 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error
d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds))
// 上传证书到 SCM
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听器证书
var errs []error
for _, hcListenerId := range hcListenerIds {
if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil {
if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
@ -282,22 +287,22 @@ func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error {
}
// 上传证书到 SCM
uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听器证书
if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil {
if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error {
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error {
// 查询监听器详情
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
showListenerReq := &hcElbModel.ShowListenerRequest{
@ -305,7 +310,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context,
}
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
}
d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp))
@ -331,7 +336,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context,
}
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
}
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
@ -339,7 +344,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context,
}
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
}
for _, certificate := range *listOldCertificateResp.Certificates {
@ -372,7 +377,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context,
}
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
}
d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp))

View File

@ -3,11 +3,13 @@ package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
k8sMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
xerrors "github.com/pkg/errors"
k8sCore "k8s.io/api/core/v1"
k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
@ -19,12 +21,25 @@ import (
type K8sSecretDeployer struct {
option *DeployerOption
infos []string
k8sClient *kubernetes.Clientset
}
func NewK8sSecretDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.KubernetesAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&K8sSecretDeployer{}).createK8sClient(access)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create k8s client")
}
return &K8sSecretDeployer{
option: option,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
k8sClient: client,
}, nil
}
@ -32,75 +47,53 @@ func (d *K8sSecretDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *K8sSecretDeployer) GetInfo() []string {
func (d *K8sSecretDeployer) GetInfos() []string {
return d.infos
}
func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
access := &domain.KubernetesAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
client, err := d.createClient(access)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("kubeClient create success.", nil))
namespace := getDeployString(d.option.DeployConfig, "namespace")
namespace := d.option.DeployConfig.GetConfigAsString("namespace")
secretName := d.option.DeployConfig.GetConfigAsString("secretName")
secretDataKeyForCrt := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForCrt", "tls.crt")
secretDataKeyForKey := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForKey", "tls.key")
if namespace == "" {
namespace = "default"
}
secretName := getDeployString(d.option.DeployConfig, "secretName")
if secretName == "" {
return fmt.Errorf("k8s secret name is empty")
return errors.New("`secretName` is required")
}
secretDataKeyForCrt := getDeployString(d.option.DeployConfig, "secretDataKeyForCrt")
if secretDataKeyForCrt == "" {
namespace = "tls.crt"
}
secretDataKeyForKey := getDeployString(d.option.DeployConfig, "secretDataKeyForKey")
if secretDataKeyForKey == "" {
namespace = "tls.key"
}
certificate, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate)
certX509, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate)
if err != nil {
return fmt.Errorf("failed to parse certificate: %w", err)
return err
}
secretPayload := corev1.Secret{
TypeMeta: k8sMetaV1.TypeMeta{
secretPayload := k8sCore.Secret{
TypeMeta: k8sMeta.TypeMeta{
Kind: "Secret",
APIVersion: "v1",
},
ObjectMeta: k8sMetaV1.ObjectMeta{
ObjectMeta: k8sMeta.ObjectMeta{
Name: secretName,
Annotations: map[string]string{
"certimate/domains": d.option.Domain,
"certimate/alt-names": strings.Join(certificate.DNSNames, ","),
"certimate/common-name": certificate.Subject.CommonName,
"certimate/issuer-organization": strings.Join(certificate.Issuer.Organization, ","),
"certimate/alt-names": strings.Join(certX509.DNSNames, ","),
"certimate/common-name": certX509.Subject.CommonName,
"certimate/issuer-organization": strings.Join(certX509.Issuer.Organization, ","),
},
},
Type: corev1.SecretType("kubernetes.io/tls"),
Type: k8sCore.SecretType("kubernetes.io/tls"),
}
secretPayload.Data = make(map[string][]byte)
secretPayload.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate)
secretPayload.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey)
// 获取 Secret 实例
_, err = client.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMetaV1.GetOptions{})
_, err = d.k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMeta.GetOptions{})
if err != nil {
_, err = client.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMetaV1.CreateOptions{})
_, err = d.k8sClient.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMeta.CreateOptions{})
if err != nil {
return fmt.Errorf("failed to create k8s secret: %w", err)
return xerrors.Wrap(err, "failed to create k8s secret")
} else {
d.infos = append(d.infos, toStr("Certificate has been created in K8s Secret", nil))
return nil
@ -108,9 +101,9 @@ func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
}
// 更新 Secret 实例
_, err = client.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMetaV1.UpdateOptions{})
_, err = d.k8sClient.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMeta.UpdateOptions{})
if err != nil {
return fmt.Errorf("failed to update k8s secret: %w", err)
return xerrors.Wrap(err, "failed to update k8s secret")
}
d.infos = append(d.infos, toStr("Certificate has been updated to K8s Secret", nil))
@ -118,7 +111,7 @@ func (d *K8sSecretDeployer) Deploy(ctx context.Context) error {
return nil
}
func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) {
func (d *K8sSecretDeployer) createK8sClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) {
var config *rest.Config
var err error
if access.KubeConfig == "" {
@ -129,7 +122,6 @@ func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kube
return nil, err
}
config, err = kubeConfig.ClientConfig()
}
if err != nil {
return nil, err
@ -139,5 +131,6 @@ func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kube
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -3,12 +3,13 @@ package deployer
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os/exec"
"runtime"
"github.com/usual2970/certimate/internal/domain"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/utils/fs"
)
@ -40,22 +41,17 @@ func (d *LocalDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *LocalDeployer) GetInfo() []string {
func (d *LocalDeployer) GetInfos() []string {
return []string{}
}
func (d *LocalDeployer) Deploy(ctx context.Context) error {
access := &domain.LocalAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return err
}
// 执行前置命令
preCommand := d.option.DeployConfig.GetConfigAsString("preCommand")
if preCommand != "" {
stdout, stderr, err := d.execCommand(preCommand)
if err != nil {
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
return xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("执行前置命令成功", stdout))
@ -65,13 +61,13 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case certFormatPEM:
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
return fmt.Errorf("failed to save private key file: %w", err)
return err
}
d.infos = append(d.infos, toStr("保存私钥成功", nil))
@ -83,11 +79,11 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
@ -101,11 +97,11 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
@ -116,7 +112,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
if command != "" {
stdout, stderr, err := d.execCommand(command)
if err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("执行命令成功", stdout))
@ -146,7 +142,7 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) {
}
default:
return "", "", fmt.Errorf("unsupported shell")
return "", "", errors.New("unsupported shell")
}
var stdoutBuf bytes.Buffer
@ -156,8 +152,8 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) {
err := cmd.Run()
if err != nil {
return "", "", fmt.Errorf("failed to execute script: %w", err)
return "", "", xerrors.Wrap(err, "failed to execute shell script")
}
return stdoutBuf.String(), stderrBuf.String(), err
return stdoutBuf.String(), stderrBuf.String(), nil
}

View File

@ -1,36 +1,54 @@
package deployer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/domain"
xhttp "github.com/usual2970/certimate/internal/utils/http"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert"
qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
)
const qiniuGateway = "http://api.qiniu.com"
type QiniuCDNDeployer struct {
option *DeployerOption
info []string
credentials *auth.Credentials
option *DeployerOption
infos []string
sdkClient *qiniuEx.Client
sslUploader uploader.Uploader
}
func NewQiniuCDNDeployer(option *DeployerOption) (*QiniuCDNDeployer, error) {
func NewQiniuCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.QiniuAccess{}
json.Unmarshal([]byte(option.Access), access)
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, xerrors.Wrap(err, "failed to get access")
}
client, err := (&QiniuCDNDeployer{}).createSdkClient(
access.AccessKey,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
uploader, err := uploaderQiniu.New(&uploaderQiniu.QiniuSSLCertUploaderConfig{
AccessKey: access.AccessKey,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &QiniuCDNDeployer{
option: option,
info: make([]string, 0),
credentials: auth.New(access.AccessKey, access.SecretKey),
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
@ -38,177 +56,52 @@ func (d *QiniuCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *QiniuCDNDeployer) GetInfo() []string {
return d.info
func (d *QiniuCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return fmt.Errorf("uploadCert failed: %w", err)
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 获取域名信息
domainInfo, err := d.getDomainInfo()
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
domain := d.option.DeployConfig.GetConfigAsString("domain")
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
if err != nil {
return fmt.Errorf("getDomainInfo failed: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
}
// 判断域名是否启用 https
if domainInfo.Https != nil && domainInfo.Https.CertID != "" {
// 启用了 https
// 修改域名证书
err = d.modifyDomainCert(certId, domainInfo.Https.ForceHttps, domainInfo.Https.Http2Enable)
d.infos = append(d.infos, toStr("已获取域名信息", getDomainInfoResp))
// 判断域名是否已启用 HTTPS。如果已启用修改域名证书否则启用 HTTPS
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" {
modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable)
if err != nil {
return fmt.Errorf("modifyDomainCert failed: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
}
d.infos = append(d.infos, toStr("已修改域名证书", modifyDomainHttpsConfResp))
} else {
// 没启用 https
// 启用 https
err = d.enableHttps(certId)
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
if err != nil {
return fmt.Errorf("enableHttps failed: %w", err)
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
}
d.infos = append(d.infos, toStr("已将域名升级为 HTTPS", enableDomainHttpsResp))
}
return nil
}
func (d *QiniuCDNDeployer) enableHttps(certId string) error {
domain := d.option.DeployConfig.GetDomain()
path := fmt.Sprintf("/domain/%s/sslize", domain)
body := &qiniuModifyDomainCertReq{
CertID: certId,
ForceHttps: true,
Http2Enable: true,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
_, err = d.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("enable https failed: %w", err)
}
return nil
}
type qiniuDomainInfo struct {
Https *qiniuModifyDomainCertReq `json:"https"`
}
func (d *QiniuCDNDeployer) getDomainInfo() (*qiniuDomainInfo, error) {
domain := d.option.DeployConfig.GetDomain()
path := fmt.Sprintf("/domain/%s", domain)
res, err := d.req(qiniuGateway+path, http.MethodGet, nil)
if err != nil {
return nil, fmt.Errorf("req failed: %w", err)
}
resp := &qiniuDomainInfo{}
err = json.Unmarshal(res, resp)
if err != nil {
return nil, fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp, nil
}
type qiniuUploadCertReq struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Pri string `json:"pri"`
Ca string `json:"ca"`
}
type qiniuUploadCertResp struct {
CertID string `json:"certID"`
}
func (d *QiniuCDNDeployer) uploadCert() (string, error) {
path := "/sslcert"
body := &qiniuUploadCertReq{
Name: getDeployString(d.option.DeployConfig, "domain"),
CommonName: getDeployString(d.option.DeployConfig, "domain"),
Pri: d.option.Certificate.PrivateKey,
Ca: d.option.Certificate.Certificate,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return "", fmt.Errorf("json.Marshal failed: %w", err)
}
res, err := d.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes))
if err != nil {
return "", fmt.Errorf("req failed: %w", err)
}
resp := &qiniuUploadCertResp{}
err = json.Unmarshal(res, resp)
if err != nil {
return "", fmt.Errorf("json.Unmarshal failed: %w", err)
}
return resp.CertID, nil
}
type qiniuModifyDomainCertReq struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
func (d *QiniuCDNDeployer) modifyDomainCert(certId string, forceHttps, http2Enable bool) error {
domain := d.option.DeployConfig.GetDomain()
path := fmt.Sprintf("/domain/%s/httpsconf", domain)
body := &qiniuModifyDomainCertReq{
CertID: certId,
ForceHttps: forceHttps,
Http2Enable: http2Enable,
}
bodyBytes, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("json.Marshal failed: %w", err)
}
_, err = d.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes))
if err != nil {
return fmt.Errorf("req failed: %w", err)
}
return nil
}
func (d *QiniuCDNDeployer) req(url, method string, body io.Reader) ([]byte, error) {
req := xhttp.BuildReq(url, method, body, map[string]string{
"Content-Type": "application/json",
})
if err := d.credentials.AddToken(auth.TokenQBox, req); err != nil {
return nil, fmt.Errorf("credentials.AddToken failed: %w", err)
}
respBody, err := xhttp.ToRequest(req)
if err != nil {
return nil, fmt.Errorf("ToRequest failed: %w", err)
}
defer respBody.Close()
res, err := io.ReadAll(respBody)
if err != nil {
return nil, fmt.Errorf("io.ReadAll failed: %w", err)
}
return res, nil
func (u *QiniuCDNDeployer) createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) {
credential := auth.New(accessKey, secretKey)
client := qiniuEx.NewClient(credential)
return client, nil
}

View File

@ -1,85 +0,0 @@
package deployer
import (
"testing"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/applicant"
)
func Test_qiuniu_uploadCert(t *testing.T) {
type fields struct {
option *DeployerOption
}
tests := []struct {
name string
fields fields
want string
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "example.com",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
Certificate: applicant.Certificate{
Certificate: "",
PrivateKey: "",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiniuCDNDeployer(tt.fields.option)
got, err := q.uploadCert()
if (err != nil) != tt.wantErr {
t.Errorf("qiuniu.uploadCert() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("qiuniu.uploadCert() = %v, want %v", got, tt.want)
}
})
}
}
func Test_qiuniu_modifyDomainCert(t *testing.T) {
type fields struct {
option *DeployerOption
info []string
credentials *auth.Credentials
}
type args struct {
certId string
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "test",
fields: fields{
option: &DeployerOption{
DomainId: "1",
Domain: "jt1.ikit.fun",
Access: `{"bucket":"test","accessKey":"","secretKey":""}`,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q, _ := NewQiniuCDNDeployer(tt.fields.option)
if err := q.modifyDomainCert(tt.args.certId, true, true); (err != nil) != tt.wantErr {
t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
"github.com/pkg/sftp"
"golang.org/x/crypto/ssh"
@ -31,7 +32,7 @@ func (d *SSHDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *SSHDeployer) GetInfo() []string {
func (d *SSHDeployer) GetInfos() []string {
return d.infos
}
@ -55,7 +56,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
if preCommand != "" {
stdout, stderr, err := d.sshExecCommand(client, preCommand)
if err != nil {
return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout))
@ -65,13 +66,13 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) {
case certFormatPEM:
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil {
return fmt.Errorf("failed to upload private key file: %w", err)
return err
}
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
@ -83,11 +84,11 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
)
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
return err
}
if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil {
return fmt.Errorf("failed to upload certificate file: %w", err)
return err
}
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
@ -101,11 +102,11 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
d.option.DeployConfig.GetConfigAsString("jksStorepass"),
)
if err != nil {
return fmt.Errorf("failed to convert pem to pfx %w", err)
return err
}
if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil {
return fmt.Errorf("failed to save certificate file: %w", err)
return err
}
d.infos = append(d.infos, toStr("保存证书成功", nil))
@ -116,7 +117,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
if command != "" {
stdout, stderr, err := d.sshExecCommand(client, command)
if err != nil {
return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr)
return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
}
d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout))
@ -158,7 +159,7 @@ func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, er
func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string, string, error) {
session, err := client.NewSession()
if err != nil {
return "", "", fmt.Errorf("failed to create ssh session: %w", err)
return "", "", xerrors.Wrap(err, "failed to create ssh session")
}
defer session.Close()
@ -167,7 +168,11 @@ func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string
var stderrBuf bytes.Buffer
session.Stderr = &stderrBuf
err = session.Run(command)
return stdoutBuf.String(), stderrBuf.String(), err
if err != nil {
return "", "", xerrors.Wrap(err, "failed to execute ssh script")
}
return stdoutBuf.String(), stderrBuf.String(), nil
}
func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, content string) error {
@ -177,23 +182,23 @@ func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, conte
func (d *SSHDeployer) writeSftpFile(client *ssh.Client, path string, data []byte) error {
sftpCli, err := sftp.NewClient(client)
if err != nil {
return fmt.Errorf("failed to create sftp client: %w", err)
return xerrors.Wrap(err, "failed to create sftp client")
}
defer sftpCli.Close()
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
return fmt.Errorf("failed to create remote directory: %w", err)
return xerrors.Wrap(err, "failed to create remote directory")
}
file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
if err != nil {
return fmt.Errorf("failed to open remote file: %w", err)
return xerrors.Wrap(err, "failed to open remote file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return fmt.Errorf("failed to write to remote file: %w", err)
return xerrors.Wrap(err, "failed to write to remote file")
}
return nil

View File

@ -6,38 +6,58 @@ import (
"fmt"
"strings"
"golang.org/x/exp/slices"
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
xerrors "github.com/pkg/errors"
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCDNDeployer struct {
option *DeployerOption
credential *common.Credential
infos []string
option *DeployerOption
infos []string
sdkClients *tencentCDNDeployerSdkClients
sslUploader uploader.Uploader
}
type tencentCDNDeployerSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func NewTencentCDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
return nil, xerrors.Wrap(err, "failed to get access")
}
credential := common.NewCredential(
clients, err := (&TencentCDNDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCDNDeployer{
option: option,
credential: credential,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
@ -45,146 +65,129 @@ func (d *TencentCDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCDNDeployer) GetInfo() []string {
func (d *TencentCDNDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
return err
}
d.infos = append(d.infos, toStr("上传证书", certId))
if err := d.deploy(certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
d.infos = append(d.infos, toStr("已上传证书", upres))
// 获取待部署的 CDN 实例
// 如果是泛域名,根据证书匹配 CDN 实例
tcInstanceIds := make([]string, 0)
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return err
}
tcInstanceIds = domains
} else {
tcInstanceIds = append(tcInstanceIds, domain)
}
// 跳过已部署的 CDN 实例
if len(tcInstanceIds) > 0 {
deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId)
if err != nil {
return err
}
temp := make([]string, 0)
for _, aliInstanceId := range tcInstanceIds {
if !slices.Contains(deployedDomains, aliInstanceId) {
temp = append(temp, aliInstanceId)
}
}
tcInstanceIds = temp
}
if len(tcInstanceIds) == 0 {
d.infos = append(d.infos, "已部署过或没有要部署的 CDN 实例")
return nil
}
// 证书部署到 CDN 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(tcInstanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
func (d *TencentCDNDeployer) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentCDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}
func (d *TencentCDNDeployer) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(d.credential, "", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.ResourceType = common.StringPtr("cdn")
request.Status = common.Int64Ptr(1)
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
domain := getDeployString(d.option.DeployConfig, "domain")
if strings.Contains(domain, "*") {
list, errGetList := d.getDomainList(certId)
if errGetList != nil {
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
}
if len(list) == 0 {
d.infos = append(d.infos, "没有需要部署的实例")
return nil
}
request.InstanceIdList = common.StringPtrs(list)
} else { // 否则直接使用传入的域名
deployed, _ := d.isDomainDeployed(certId, domain)
if deployed {
d.infos = append(d.infos, "域名已部署")
return nil
} else {
request.InstanceIdList = common.StringPtrs([]string{domain})
}
}
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
describeCertDomainsReq.Product = common.StringPtr("cdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
}
d.infos = append(d.infos, toStr("部署证书", resp.Response))
return nil
}
func (d *TencentCDNDeployer) getDomainList(certId string) ([]string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
client, _ := cdn.NewClient(d.credential, "", cpf)
request := cdn.NewDescribeCertDomainsRequest()
request.CertId = common.StringPtr(certId)
response, err := client.DescribeCertDomains(request)
if err != nil {
return nil, fmt.Errorf("failed to get domain list: %w", err)
}
deployedDomains, err := d.getDeployedDomainList(certId)
if err != nil {
return nil, fmt.Errorf("failed to get deployed domain list: %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
for _, domain := range response.Response.Domains {
domainStr := *domain
if !slices.Contains(deployedDomains, domainStr) {
domains = append(domains, domainStr)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil
}
func (d *TencentCDNDeployer) isDomainDeployed(certId, domain string) (bool, error) {
deployedDomains, err := d.getDeployedDomainList(certId)
func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) {
// 根据证书查询关联 CDN 域名
// REF: https://cloud.tencent.com/document/product/400/62674
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId})
describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn")
describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq)
if err != nil {
return false, err
}
return slices.Contains(deployedDomains, domain), nil
}
func (d *TencentCDNDeployer) getDeployedDomainList(certId string) ([]string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewDescribeDeployedResourcesRequest()
request.CertificateIds = common.StringPtrs([]string{certId})
request.ResourceType = common.StringPtr("cdn")
response, err := client.DescribeDeployedResources(request)
if err != nil {
return nil, fmt.Errorf("failed to get deployed domain list: %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'")
}
domains := make([]string, 0)
for _, domain := range response.Response.DeployedResources[0].Resources {
domains = append(domains, *domain)
if describeDeployedResourcesResp.Response.DeployedResources != nil {
for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources {
for _, resource := range deployedResource.Resources {
domains = append(domains, *resource)
}
}
}
return domains, nil

View File

@ -3,37 +3,61 @@ package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
xerrors "github.com/pkg/errors"
tcClb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCLBDeployer struct {
option *DeployerOption
credential *common.Credential
infos []string
option *DeployerOption
infos []string
sdkClients *tencentCLBDeployerSdkClients
sslUploader uploader.Uploader
}
type tencentCLBDeployerSdkClients struct {
ssl *tcSsl.Client
clb *tcClb.Client
}
func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
return nil, xerrors.Wrap(err, "failed to get access")
}
credential := common.NewCredential(
clients, err := (&TencentCLBDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
option.DeployConfig.GetConfigAsString("region"),
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCLBDeployer{
option: option,
credential: credential,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
@ -41,77 +65,266 @@ func (d *TencentCLBDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCLBDeployer) GetInfo() []string {
func (d *TencentCLBDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCLBDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
}
d.infos = append(d.infos, toStr("上传证书", certId))
// TODO: 直接部署方式
if err := d.deploy(certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
switch d.option.DeployConfig.GetConfigAsString("resourceType") {
case "ssl-deploy":
// 通过 SSL 服务部署到云资源实例
err := d.deployToInstanceUseSsl(ctx)
if err != nil {
return err
}
case "loadbalancer":
// 部署到指定负载均衡器
if err := d.deployToLoadbalancer(ctx); err != nil {
return err
}
case "listener":
// 部署到指定监听器
if err := d.deployToListener(ctx); err != nil {
return err
}
case "ruledomain":
// 部署到指定七层监听转发规则域名
if err := d.deployToRuleDomain(ctx); err != nil {
return err
}
default:
return errors.New("unsupported resource type")
}
return nil
}
func (d *TencentCLBDeployer) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentCLBDeployerSdkClients{
ssl: sslClient,
clb: clbClient,
}, nil
}
func (d *TencentCLBDeployer) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.ResourceType = common.StringPtr("clb")
request.Status = common.Int64Ptr(1)
clbId := getDeployString(d.option.DeployConfig, "clbId")
lsnId := getDeployString(d.option.DeployConfig, "lsnId")
domain := getDeployString(d.option.DeployConfig, "domain")
if(domain == ""){
// 未开启SNI只需要精确到监听器
request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", clbId, lsnId)})
}else{
// 开启SNI需要精确到域名支持泛域名
request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", clbId, lsnId, domain)})
func (d *TencentCLBDeployer) deployToInstanceUseSsl(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
if tcListenerId == "" {
return errors.New("`listenerId` is required")
}
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
return err
}
d.infos = append(d.infos, toStr("部署证书", resp.Response))
d.infos = append(d.infos, toStr("已上传证书", upres))
// 证书部署到 CLB 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("clb")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
if tcDomain == "" {
// 未开启 SNI只需指定到监听器
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", tcLoadbalancerId, tcListenerId)})
} else {
// 开启 SNI需指定到域名支持泛域名
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", tcLoadbalancerId, tcListenerId, tcDomain)})
}
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
}
func (d *TencentCLBDeployer) deployToLoadbalancer(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerIds := make([]string, 0)
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
// 查询负载均衡器详细信息
// REF: https://cloud.tencent.com/document/api/214/46916
describeLoadBalancersDetailReq := tcClb.NewDescribeLoadBalancersDetailRequest()
describeLoadBalancersDetailResp, err := d.sdkClients.clb.DescribeLoadBalancersDetail(describeLoadBalancersDetailReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeLoadBalancersDetail'")
}
d.infos = append(d.infos, toStr("已查询到负载均衡详细信息", describeLoadBalancersDetailResp))
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
} else {
if describeListenersResp.Response.Listeners != nil {
for _, listener := range describeListenersResp.Response.Listeners {
if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") {
continue
}
tcListenerIds = append(tcListenerIds, *listener.ListenerId)
}
}
}
d.infos = append(d.infos, toStr("已查询到负载均衡器下的监听器", tcListenerIds))
// 上传证书到 SCM
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 批量更新监听器证书
var errs []error
for _, tcListenerId := range tcListenerIds {
if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (d *TencentCLBDeployer) deployToListener(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
if tcListenerId == "" {
return errors.New("`listenerId` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 更新监听器证书
if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil {
return err
}
return nil
}
func (d *TencentCLBDeployer) deployToRuleDomain(ctx context.Context) error {
tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId")
tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId")
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
if tcLoadbalancerId == "" {
return errors.New("`loadbalancerId` is required")
}
if tcListenerId == "" {
return errors.New("`listenerId` is required")
}
if tcDomain == "" {
return errors.New("`domain` is required")
}
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 修改负载均衡七层监听器转发规则的域名级别属性
// REF: https://cloud.tencent.com/document/api/214/38092
modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest()
modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
modifyDomainAttributesReq.ListenerId = common.StringPtr(tcListenerId)
modifyDomainAttributesReq.Domain = common.StringPtr(tcDomain)
modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{
SSLMode: common.StringPtr("UNIDIRECTIONAL"),
CertId: common.StringPtr(upres.CertId),
}
modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'")
}
d.infos = append(d.infos, toStr("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response))
return nil
}
func (d *TencentCLBDeployer) modifyListenerCertificate(ctx context.Context, tcLoadbalancerId, tcListenerId, tcCertId string) error {
// 查询监听器列表
// REF: https://cloud.tencent.com/document/api/214/30686
describeListenersReq := tcClb.NewDescribeListenersRequest()
describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
describeListenersReq.ListenerIds = common.StringPtrs([]string{tcListenerId})
describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'")
}
if len(describeListenersResp.Response.Listeners) == 0 {
d.infos = append(d.infos, toStr("未找到监听器", nil))
return errors.New("listener not found")
}
d.infos = append(d.infos, toStr("已查询到监听器属性", describeListenersResp.Response))
// 修改监听器属性
// REF: https://cloud.tencent.com/document/product/214/30681
modifyListenerReq := tcClb.NewModifyListenerRequest()
modifyListenerReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId)
modifyListenerReq.ListenerId = common.StringPtr(tcListenerId)
modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(tcCertId)}
if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil {
modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode
modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId
} else {
modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL")
}
modifyListenerResp, err := d.sdkClients.clb.ModifyListener(modifyListenerReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyListener'")
}
d.infos = append(d.infos, toStr("已修改监听器属性", modifyListenerResp.Response))
return nil
}

View File

@ -3,37 +3,54 @@ package deployer
import (
"context"
"encoding/json"
"errors"
"fmt"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentCOSDeployer struct {
option *DeployerOption
credential *common.Credential
infos []string
option *DeployerOption
infos []string
sdkClient *tcSsl.Client
sslUploader uploader.Uploader
}
func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
return nil, xerrors.Wrap(err, "failed to get access")
}
credential := common.NewCredential(
client, err := (&TencentCOSDeployer{}).createSdkClient(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentCOSDeployer{
option: option,
credential: credential,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClient: client,
sslUploader: uploader,
}, nil
}
@ -41,68 +58,49 @@ func (d *TencentCOSDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentCOSDeployer) GetInfo() []string {
func (d *TencentCOSDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentCOSDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
tcRegion := d.option.DeployConfig.GetConfigAsString("region")
tcBucket := d.option.DeployConfig.GetConfigAsString("bucket")
tcDomain := d.option.DeployConfig.GetConfigAsString("domain")
if tcBucket == "" {
return errors.New("`bucket` is required")
}
d.infos = append(d.infos, toStr("上传证书", certId))
if err := d.deploy(certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 证书部署到 COS 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("cos")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", tcRegion, tcBucket, tcDomain)})
deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
// 上传证书与CDN部署的上传方法一致。
func (d *TencentCOSDeployer) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
return client, nil
}
func (d *TencentCOSDeployer) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.ResourceType = common.StringPtr("cos")
request.Status = common.Int64Ptr(1)
domain := getDeployString(d.option.DeployConfig, "domain")
request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", getDeployString(d.option.DeployConfig, "region"), getDeployString(d.option.DeployConfig, "bucket"), domain)})
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
}
d.infos = append(d.infos, toStr("部署证书", resp.Response))
return nil
}

View File

@ -2,41 +2,61 @@ package deployer
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
xerrors "github.com/pkg/errors"
tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentECDNDeployer struct {
option *DeployerOption
credential *common.Credential
infos []string
option *DeployerOption
infos []string
sdkClients *tencentECDNDeployerSdkClients
sslUploader uploader.Uploader
}
type tencentECDNDeployerSdkClients struct {
ssl *tcSsl.Client
cdn *tcCdn.Client
}
func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
return nil, xerrors.Wrap(err, "failed to get access")
}
credential := common.NewCredential(
clients, err := (&TencentECDNDeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentECDNDeployer{
option: option,
credential: credential,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
@ -44,102 +64,90 @@ func (d *TencentECDNDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentECDNDeployer) GetInfo() []string {
func (d *TencentECDNDeployer) GetInfos() []string {
return d.infos
}
func (d *TencentECDNDeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
return err
}
d.infos = append(d.infos, toStr("上传证书", certId))
if err := d.deploy(certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
d.infos = append(d.infos, toStr("已上传证书", upres))
// 获取待部署的 ECDN 实例
// 如果是泛域名,根据证书匹配 ECDN 实例
aliInstanceIds := make([]string, 0)
domain := d.option.DeployConfig.GetConfigAsString("domain")
if strings.HasPrefix(domain, "*") {
domains, err := d.getDomainsByCertificateId(upres.CertId)
if err != nil {
return err
}
aliInstanceIds = domains
} else {
aliInstanceIds = append(aliInstanceIds, domain)
}
if len(aliInstanceIds) == 0 {
d.infos = append(d.infos, "没有要部署的 ECDN 实例")
return nil
}
// 证书部署到 ECDN 实例
// REF: https://cloud.tencent.com/document/product/400/91667
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId)
deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn")
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds)
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
}
d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response))
return nil
}
func (d *TencentECDNDeployer) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
func (d *TencentECDNDeployer) createSdkClients(secretId, secretKey string) (*tencentECDNDeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return &tencentECDNDeployerSdkClients{
ssl: sslClient,
cdn: cdnClient,
}, nil
}
func (d *TencentECDNDeployer) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := ssl.NewClient(d.credential, "", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := ssl.NewDeployCertificateInstanceRequest()
request.CertificateId = common.StringPtr(certId)
request.ResourceType = common.StringPtr("ecdn")
request.Status = common.Int64Ptr(1)
// 如果是泛域名就从cdn列表下获取SSL证书中的可用域名
domain := getDeployString(d.option.DeployConfig, "domain")
if strings.Contains(domain, "*") {
list, errGetList := d.getDomainList()
if errGetList != nil {
return fmt.Errorf("failed to get certificate domain list: %w", errGetList)
}
if list == nil || len(list) == 0 {
return fmt.Errorf("failed to get certificate domain list: empty list.")
}
request.InstanceIdList = common.StringPtrs(list)
} else { // 否则直接使用传入的域名
request.InstanceIdList = common.StringPtrs([]string{domain})
}
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.DeployCertificateInstance(request)
func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) {
// 获取证书中的可用域名
// REF: https://cloud.tencent.com/document/product/228/42491
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
describeCertDomainsReq.CertId = common.StringPtr(tcCertId)
describeCertDomainsReq.Product = common.StringPtr("ecdn")
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
}
d.infos = append(d.infos, toStr("部署证书", resp.Response))
return nil
}
func (d *TencentECDNDeployer) getDomainList() ([]string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com"
client, _ := cdn.NewClient(d.credential, "", cpf)
request := cdn.NewDescribeCertDomainsRequest()
cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate))
request.Cert = &cert
request.Product = common.StringPtr("ecdn")
response, err := client.DescribeCertDomains(request)
if err != nil {
return nil, fmt.Errorf("failed to get domain list: %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
}
domains := make([]string, 0)
for _, domain := range response.Response.Domains {
domains = append(domains, *domain)
if describeCertDomainsResp.Response.Domains == nil {
for _, domain := range describeCertDomainsResp.Response.Domains {
domains = append(domains, *domain)
}
}
return domains, nil

View File

@ -6,36 +6,57 @@ import (
"fmt"
"strings"
teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/rand"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
)
type TencentTEODeployer struct {
option *DeployerOption
credential *common.Credential
infos []string
option *DeployerOption
infos []string
sdkClients *tencentTEODeployerSdkClients
sslUploader uploader.Uploader
}
type tencentTEODeployerSdkClients struct {
ssl *tcSsl.Client
teo *tcTeo.Client
}
func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) {
access := &domain.TencentAccess{}
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err)
return nil, xerrors.Wrap(err, "failed to get access")
}
credential := common.NewCredential(
clients, err := (&TencentTEODeployer{}).createSdkClients(
access.SecretId,
access.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk clients")
}
uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{
SecretId: access.SecretId,
SecretKey: access.SecretKey,
})
if err != nil {
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
}
return &TencentTEODeployer{
option: option,
credential: credential,
infos: make([]string, 0),
option: option,
infos: make([]string, 0),
sdkClients: clients,
sslUploader: uploader,
}, nil
}
@ -43,69 +64,56 @@ func (d *TencentTEODeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *TencentTEODeployer) GetInfo() []string {
func (d *TencentTEODeployer) GetInfos() []string {
return d.infos
}
func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
// 上传证书
certId, err := d.uploadCert()
if err != nil {
return fmt.Errorf("failed to upload certificate: %w", err)
tcZoneId := d.option.DeployConfig.GetConfigAsString("zoneId")
if tcZoneId == "" {
return xerrors.New("`zoneId` is required")
}
d.infos = append(d.infos, toStr("上传证书", certId))
if err := d.deploy(certId); err != nil {
return fmt.Errorf("failed to deploy: %w", err)
// 上传证书到 SSL
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", upres))
// 配置域名证书
// REF: https://cloud.tencent.com/document/product/1552/80764
modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest()
modifyHostsCertificateReq.ZoneId = common.StringPtr(tcZoneId)
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
modifyHostsCertificateReq.Hosts = common.StringPtrs(strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"), "\n"))
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
if err != nil {
return xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'")
}
d.infos = append(d.infos, toStr("已配置域名证书", modifyHostsCertificateResp.Response))
return nil
}
func (d *TencentTEODeployer) uploadCert() (string, error) {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com"
func (d *TencentTEODeployer) createSdkClients(secretId, secretKey string) (*tencentTEODeployerSdkClients, error) {
credential := common.NewCredential(secretId, secretKey)
client, _ := ssl.NewClient(d.credential, "", cpf)
request := ssl.NewUploadCertificateRequest()
request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate)
request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey)
request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6))
request.Repeatable = common.BoolPtr(false)
response, err := client.UploadCertificate(request)
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return "", fmt.Errorf("failed to upload certificate: %w", err)
return nil, err
}
return *response.Response.CertificateId, nil
}
func (d *TencentTEODeployer) deploy(certId string) error {
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com"
// 实例化要请求产品的client对象,clientProfile是可选的
client, _ := teo.NewClient(d.credential, "", cpf)
// 实例化一个请求对象,每个接口都会对应一个request对象
request := teo.NewModifyHostsCertificateRequest()
request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId"))
request.Mode = common.StringPtr("sslcert")
request.ServerCertInfo = []*teo.ServerCertInfo{{
CertId: common.StringPtr(certId),
}}
domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n")
request.Hosts = common.StringPtrs(domains)
// 返回的resp是一个DeployCertificateInstanceResponse的实例与请求对象对应
resp, err := client.ModifyHostsCertificate(request)
teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return fmt.Errorf("failed to deploy certificate: %w", err)
return nil, err
}
d.infos = append(d.infos, toStr("部署证书", resp.Response))
return nil
return &tencentTEODeployerSdkClients{
ssl: sslClient,
teo: teoClient,
}, nil
}

View File

@ -7,6 +7,8 @@ import (
"fmt"
"net/http"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/domain"
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
@ -27,7 +29,7 @@ func (d *WebhookDeployer) GetID() string {
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
}
func (d *WebhookDeployer) GetInfo() []string {
func (d *WebhookDeployer) GetInfos() []string {
return d.infos
}
@ -41,26 +43,24 @@ type webhookData struct {
func (d *WebhookDeployer) Deploy(ctx context.Context) error {
access := &domain.WebhookAccess{}
if err := json.Unmarshal([]byte(d.option.Access), access); err != nil {
return fmt.Errorf("failed to parse hook access config: %w", err)
return xerrors.Wrap(err, "failed to get access")
}
data := &webhookData{
Domain: d.option.Domain,
Certificate: d.option.Certificate.Certificate,
PrivateKey: d.option.Certificate.PrivateKey,
Variables: getDeployVariables(d.option.DeployConfig),
Variables: d.option.DeployConfig.GetConfigAsVariables(),
}
body, _ := json.Marshal(data)
resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
"Content-Type": "application/json",
})
if err != nil {
return fmt.Errorf("failed to send hook request: %w", err)
return xerrors.Wrap(err, "failed to send webhook request")
}
d.infos = append(d.infos, toStr("webhook response", string(resp)))
d.infos = append(d.infos, toStr("Webhook Response", string(resp)))
return nil
}

View File

@ -1,6 +1,9 @@
package domain
import "strings"
import (
"encoding/json"
"strings"
)
type ApplyConfig struct {
Email string `json:"email"`
@ -117,6 +120,33 @@ func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool)
return defaultValue
}
// 以变量字典形式获取配置项。
//
// 出参:
// - 变量字典。
func (dc *DeployConfig) GetConfigAsVariables() map[string]string {
rs := make(map[string]string)
if dc.Config != nil {
value, ok := dc.Config["variables"]
if !ok {
return rs
}
kvs := make([]KV, 0)
bts, _ := json.Marshal(value)
if err := json.Unmarshal(bts, &kvs); err != nil {
return rs
}
for _, kv := range kvs {
rs[kv.Key] = kv.Value
}
}
return rs
}
// GetDomain returns the domain from the deploy config
// if the domain is a wildcard domain, and wildcard is true, return the wildcard domain
func (dc *DeployConfig) GetDomain(wildcard ...bool) string {

View File

@ -105,11 +105,11 @@ func deploy(ctx context.Context, record *models.Record) error {
if err = deployer.Deploy(ctx); err != nil {
app.GetApp().Logger().Error("部署失败", "err", err)
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()})
history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfos()})
return err
}
history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{
Info: deployer.GetInfo(),
Info: deployer.GetInfos(),
}, false)
}

View File

@ -1,4 +1,4 @@
package uploader
package aliyuncas
import (
"context"
@ -6,11 +6,12 @@ import (
"strings"
"time"
cas20200407 "github.com/alibabacloud-go/cas-20200407/v3/client"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
aliyunCas "github.com/alibabacloud-go/cas-20200407/v3/client"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
@ -21,29 +22,27 @@ type AliyunCASUploaderConfig struct {
}
type AliyunCASUploader struct {
config *AliyunCASUploaderConfig
sdkClient *cas20200407.Client
sdkRuntime *util.RuntimeOptions
config *AliyunCASUploaderConfig
sdkClient *aliyunCas.Client
}
func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (Uploader, error) {
client, err := (&AliyunCASUploader{}).createSdkClient(
func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) {
client, err := createSdkClient(
config.AccessKeyId,
config.AccessKeySecret,
config.Region,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunCASUploader{
config: config,
sdkClient: client,
sdkRuntime: &util.RuntimeOptions{},
config: config,
sdkClient: client,
}, nil
}
func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) {
func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
@ -56,25 +55,25 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP
listUserCertificateOrderPage := int64(1)
listUserCertificateOrderLimit := int64(50)
for {
listUserCertificateOrderReq := &cas20200407.ListUserCertificateOrderRequest{
listUserCertificateOrderReq := &aliyunCas.ListUserCertificateOrderRequest{
CurrentPage: tea.Int64(listUserCertificateOrderPage),
ShowSize: tea.Int64(listUserCertificateOrderLimit),
OrderType: tea.String("CERT"),
}
listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrderWithOptions(listUserCertificateOrderReq, u.sdkRuntime)
listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrder(listUserCertificateOrderReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cas.ListUserCertificateOrder': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.ListUserCertificateOrder'")
}
if listUserCertificateOrderResp.Body.CertificateOrderList != nil {
for _, certDetail := range listUserCertificateOrderResp.Body.CertificateOrderList {
if strings.EqualFold(certX509.SerialNumber.Text(16), *certDetail.SerialNo) {
getUserCertificateDetailReq := &cas20200407.GetUserCertificateDetailRequest{
getUserCertificateDetailReq := &aliyunCas.GetUserCertificateDetailRequest{
CertId: certDetail.CertificateId,
}
getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetailWithOptions(getUserCertificateDetailReq, u.sdkRuntime)
getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetail(getUserCertificateDetailReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.GetUserCertificateDetail'")
}
var isSameCert bool
@ -91,7 +90,7 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &UploadResult{
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)),
CertName: *certDetail.Name,
}, nil
@ -116,29 +115,29 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP
// 上传新证书
// REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate
uploadUserCertificateReq := &cas20200407.UploadUserCertificateRequest{
uploadUserCertificateReq := &aliyunCas.UploadUserCertificateRequest{
Name: tea.String(certName),
Cert: tea.String(certPem),
Key: tea.String(privkeyPem),
}
uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificateWithOptions(uploadUserCertificateReq, u.sdkRuntime)
uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificate(uploadUserCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cas.UploadUserCertificate': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.UploadUserCertificate'")
}
certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId))
return &UploadResult{
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*cas20200407.Client, error) {
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Client, error) {
if region == "" {
region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州
}
aConfig := &openapi.Config{
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
@ -152,7 +151,7 @@ func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region
}
aConfig.Endpoint = tea.String(endpoint)
client, err := cas20200407.NewClient(aConfig)
client, err := aliyunCas.NewClient(aConfig)
if err != nil {
return nil, err
}

View File

@ -1,4 +1,4 @@
package uploader
package aliyunslb
import (
"context"
@ -8,11 +8,12 @@ import (
"strings"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client"
"github.com/alibabacloud-go/tea/tea"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
@ -23,29 +24,27 @@ type AliyunSLBUploaderConfig struct {
}
type AliyunSLBUploader struct {
config *AliyunSLBUploaderConfig
sdkClient *slb20140515.Client
sdkRuntime *util.RuntimeOptions
config *AliyunSLBUploaderConfig
sdkClient *aliyunSlb.Client
}
func NewAliyunSLBUploader(config *AliyunSLBUploaderConfig) (Uploader, error) {
client, err := (&AliyunSLBUploader{}).createSdkClient(
func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) {
client, err := createSdkClient(
config.AccessKeyId,
config.AccessKeySecret,
config.Region,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &AliyunSLBUploader{
config: config,
sdkClient: client,
sdkRuntime: &util.RuntimeOptions{},
config: config,
sdkClient: client,
}, nil
}
func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) {
func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
@ -54,12 +53,12 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP
// 查询证书列表,避免重复上传
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates
describeServerCertificatesReq := &slb20140515.DescribeServerCertificatesRequest{
describeServerCertificatesReq := &aliyunSlb.DescribeServerCertificatesRequest{
RegionId: tea.String(u.config.Region),
}
describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificatesWithOptions(describeServerCertificatesReq, u.sdkRuntime)
describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificates(describeServerCertificatesReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'slb.DescribeServerCertificates': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeServerCertificates'")
}
if describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil {
@ -71,7 +70,7 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP
strings.EqualFold(certX509.Subject.CommonName, *certDetail.CommonName)
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &UploadResult{
return &uploader.UploadResult{
CertId: *certDetail.ServerCertificateId,
CertName: *certDetail.ServerCertificateName,
}, nil
@ -85,30 +84,30 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP
// 上传新证书
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate
uploadServerCertificateReq := &slb20140515.UploadServerCertificateRequest{
uploadServerCertificateReq := &aliyunSlb.UploadServerCertificateRequest{
RegionId: tea.String(u.config.Region),
ServerCertificateName: tea.String(certName),
ServerCertificate: tea.String(certPem),
PrivateKey: tea.String(privkeyPem),
}
uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificateWithOptions(uploadServerCertificateReq, u.sdkRuntime)
uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificate(uploadServerCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'slb.UploadServerCertificate': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'slb.UploadServerCertificate'")
}
certId = *uploadServerCertificateResp.Body.ServerCertificateId
return &UploadResult{
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func (u *AliyunSLBUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) {
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
if region == "" {
region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州
}
aConfig := &openapi.Config{
aConfig := &aliyunOpen.Config{
AccessKeyId: tea.String(accessKeyId),
AccessKeySecret: tea.String(accessKeySecret),
}
@ -125,7 +124,7 @@ func (u *AliyunSLBUploader) createSdkClient(accessKeyId, accessKeySecret, region
}
aConfig.Endpoint = tea.String(endpoint)
client, err := slb20140515.NewClient(aConfig)
client, err := aliyunSlb.NewClient(aConfig)
if err != nil {
return nil, err
}

View File

@ -1,7 +1,8 @@
package uploader
package huaweicloudelb
import (
"context"
"errors"
"fmt"
"time"
@ -13,7 +14,9 @@ import (
hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3"
hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model"
hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
@ -29,14 +32,14 @@ type HuaweiCloudELBUploader struct {
sdkClient *hcElb.ElbClient
}
func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader, error) {
client, err := (&HuaweiCloudELBUploader{}).createSdkClient(
func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) {
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,
config.Region,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
return nil, xerrors.Wrap(err, "failed to create sdk client: %w")
}
return &HuaweiCloudELBUploader{
@ -45,7 +48,7 @@ func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader,
}, nil
}
func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) {
func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
newCert, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
@ -65,7 +68,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
}
listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
}
if listCertificatesResp.Certificates != nil {
@ -84,7 +87,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &UploadResult{
return &uploader.UploadResult{
CertId: certDetail.Id,
CertName: certDetail.Name,
}, nil
@ -105,9 +108,9 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
// 获取项目 ID
// REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html
projectId, err := u.getSdkProjectId(u.config.Region, u.config.AccessKeyId, u.config.SecretAccessKey)
projectId, err := getSdkProjectId(u.config.AccessKeyId, u.config.SecretAccessKey, u.config.Region)
if err != nil {
return nil, fmt.Errorf("failed to get SDK project id: %w", err)
return nil, xerrors.Wrap(err, "failed to get SDK project id")
}
// 生成新证书名(需符合华为云命名规则)
@ -128,18 +131,18 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
}
createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'elb.CreateCertificate'")
}
certId = createCertificateResp.Certificate.Id
certName = createCertificateResp.Certificate.Name
return &UploadResult{
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
if region == "" {
region = "cn-north-4" // ELB 服务默认区域:华北四北京
}
@ -169,7 +172,7 @@ func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, r
return client, nil
}
func (u *HuaweiCloudELBUploader) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
if region == "" {
region = "cn-north-4" // IAM 服务默认区域:华北四北京
}
@ -207,7 +210,7 @@ func (u *HuaweiCloudELBUploader) getSdkProjectId(accessKeyId, secretAccessKey, r
if err != nil {
return "", err
} else if response.Projects == nil || len(*response.Projects) == 0 {
return "", fmt.Errorf("no project found")
return "", errors.New("no project found")
}
return (*response.Projects)[0].Id, nil

View File

@ -1,4 +1,4 @@
package uploader
package huaweicloudscm
import (
"context"
@ -9,7 +9,9 @@ import (
hcScm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3"
hcScmModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model"
hcScmRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
)
@ -25,14 +27,14 @@ type HuaweiCloudSCMUploader struct {
sdkClient *hcScm.ScmClient
}
func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader, error) {
client, err := (&HuaweiCloudSCMUploader{}).createSdkClient(
func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
client, err := createSdkClient(
config.AccessKeyId,
config.SecretAccessKey,
config.Region,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &HuaweiCloudSCMUploader{
@ -41,7 +43,7 @@ func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader,
}, nil
}
func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) {
func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
@ -63,7 +65,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
}
listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'scm.ListCertificates': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ListCertificates'")
}
if listCertificatesResp.Certificates != nil {
@ -76,7 +78,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
if exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 {
continue
}
return nil, fmt.Errorf("failed to execute sdk request 'scm.ExportCertificate': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ExportCertificate'")
}
var isSameCert bool
@ -93,7 +95,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
return &UploadResult{
return &uploader.UploadResult{
CertId: certDetail.Id,
CertName: certDetail.Name,
}, nil
@ -127,17 +129,17 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
}
importCertificateResp, err := u.sdkClient.ImportCertificate(importCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'scm.ImportCertificate': %w", err)
return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ImportCertificate'")
}
certId = *importCertificateResp.CertificateId
return &UploadResult{
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func (u *HuaweiCloudSCMUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) {
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) {
if region == "" {
region = "cn-north-4" // SCM 服务默认区域:华北四北京
}

View File

@ -0,0 +1,70 @@
package qiniusslcert
import (
"context"
"fmt"
"time"
xerrors "github.com/pkg/errors"
"github.com/qiniu/go-sdk/v7/auth"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/x509"
qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
)
type QiniuSSLCertUploaderConfig struct {
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
}
type QiniuSSLCertUploader struct {
config *QiniuSSLCertUploaderConfig
sdkClient *qiniuEx.Client
}
func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) {
client, err := createSdkClient(
config.AccessKey,
config.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &QiniuSSLCertUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 生成新证书名(需符合七牛云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate
uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, privkeyPem, certPem)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'")
}
certId = uploadSslCertResp.CertID
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) {
credential := auth.New(accessKey, secretKey)
client := qiniuEx.NewClient(credential)
return client, nil
}

View File

@ -0,0 +1,66 @@
package tencentcloudssl
import (
"context"
xerrors "github.com/pkg/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
)
type TencentCloudSSLUploaderConfig struct {
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}
type TencentCloudSSLUploader struct {
config *TencentCloudSSLUploaderConfig
sdkClient *tcSsl.Client
}
func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) {
client, err := createSdkClient(
config.SecretId,
config.SecretKey,
)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &TencentCloudSSLUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 上传新证书
// REF: https://cloud.tencent.com/document/product/400/41665
uploadCertificateReq := tcSsl.NewUploadCertificateRequest()
uploadCertificateReq.CertificatePublicKey = common.StringPtr(certPem)
uploadCertificateReq.CertificatePrivateKey = common.StringPtr(privkeyPem)
uploadCertificateReq.Repeatable = common.BoolPtr(false)
uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.UploadCertificate'")
}
certId := *uploadCertificateResp.Response.CertificateId
return &uploader.UploadResult{
CertId: certId,
CertName: "",
}, nil
}
func createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) {
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -1,92 +0,0 @@
package uploader
import (
"context"
"fmt"
"time"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type TencentCloudSSLUploaderConfig struct {
Region string `json:"region"`
SecretId string `json:"secretId"`
SecretKey string `json:"secretKey"`
}
type TencentCloudSSLUploader struct {
config *TencentCloudSSLUploaderConfig
sdkClient *tcSsl.Client
}
func NewTencentCloudSSLUploader(config *TencentCloudSSLUploaderConfig) (Uploader, error) {
client, err := (&TencentCloudSSLUploader{}).createSdkClient(
config.Region,
config.SecretId,
config.SecretKey,
)
if err != nil {
return nil, fmt.Errorf("failed to create sdk client: %w", err)
}
return &TencentCloudSSLUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) {
// 生成新证书名(需符合腾讯云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://cloud.tencent.com/document/product/400/41665
uploadCertificateReq := &tcSsl.UploadCertificateRequest{
Alias: cast.StringPtr(certName),
CertificatePublicKey: cast.StringPtr(certPem),
CertificatePrivateKey: cast.StringPtr(privkeyPem),
Repeatable: cast.BoolPtr(false),
}
uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.UploadCertificate': %w", err)
}
// 获取证书详情
// REF: https://cloud.tencent.com/document/api/400/41673
//
// P.S. 上传重复证书会返回上一次的证书 ID这里需要重新获取一遍证书名https://github.com/usual2970/certimate/pull/227
describeCertificateDetailReq := &tcSsl.DescribeCertificateDetailRequest{
CertificateId: uploadCertificateResp.Response.CertificateId,
}
describeCertificateDetailResp, err := u.sdkClient.DescribeCertificateDetail(describeCertificateDetailReq)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeCertificateDetail': %w", err)
}
certId = *describeCertificateDetailResp.Response.CertificateId
certName = *describeCertificateDetailResp.Response.Alias
return &UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func (u *TencentCloudSSLUploader) createSdkClient(region, secretId, secretKey string) (*tcSsl.Client, error) {
if region == "" {
region = "ap-guangzhou" // SSL 服务默认区域:广州
}
credential := common.NewCredential(secretId, secretKey)
client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -1,9 +1,10 @@
package fs
import (
"fmt"
"os"
"path/filepath"
xerrors "github.com/pkg/errors"
)
// 与 [WriteFile] 类似,但写入的是字符串内容。
@ -33,18 +34,18 @@ func WriteFile(path string, data []byte) error {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
return xerrors.Wrap(err, "failed to create directory")
}
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
return xerrors.Wrap(err, "failed to create file")
}
defer file.Close()
_, err = file.Write(data)
if err != nil {
return fmt.Errorf("failed to write file: %w", err)
return xerrors.Wrap(err, "failed to write file")
}
return nil

View File

@ -0,0 +1,22 @@
package x509
import (
"crypto/x509"
)
// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。
// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。
//
// 入参:
// - a: 待比较的第一个 x509.Certificate 对象。
// - b: 待比较的第二个 x509.Certificate 对象。
//
// 出参:
// - 是否相同。
func EqualCertificate(a, b *x509.Certificate) bool {
return string(a.Signature) == string(b.Signature) &&
a.SignatureAlgorithm == b.SignatureAlgorithm &&
a.SerialNumber.String() == b.SerialNumber.String() &&
a.Issuer.SerialNumber == b.Issuer.SerialNumber &&
a.Subject.SerialNumber == b.Subject.SerialNumber
}

View File

@ -0,0 +1,31 @@
package x509
import (
"crypto/ecdsa"
"crypto/x509"
"encoding/pem"
xerrors "github.com/pkg/errors"
)
// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。
//
// 入参:
// - privkey: ecdsa.PrivateKey 对象。
//
// 出参:
// - privkeyPem: 私钥 PEM 内容。
// - err: 错误。
func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPem string, err error) {
data, err := x509.MarshalECPrivateKey(privkey)
if err != nil {
return "", xerrors.Wrap(err, "failed to marshal EC private key")
}
block := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: data,
}
return string(pem.EncodeToMemory(block)), nil
}

View File

@ -0,0 +1,83 @@
package x509
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
xerrors "github.com/pkg/errors"
)
// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。
//
// 入参:
// - certPem: 证书 PEM 内容。
//
// 出参:
// - cert: x509.Certificate 对象。
// - err: 错误。
func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) {
pemData := []byte(certPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse certificate")
}
return cert, nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: ecdsa.PrivateKey 对象。
// - err: 错误。
func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err error) {
pemData := []byte(privkeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
privkey, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse private key")
}
return privkey, nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 rsa.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: rsa.PrivateKey 对象。
// - err: 错误。
func ParsePKCS1PrivateKeyFromPEM(privkeyPem string) (privkey *rsa.PrivateKey, err error) {
pemData := []byte(privkeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to decode PEM block")
}
privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, xerrors.Wrap(err, "failed to parse private key")
}
return privkey, nil
}

View File

@ -1,120 +0,0 @@
package x509
import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
)
// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。
// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。
//
// 入参:
// - a: 待比较的第一个 x509.Certificate 对象。
// - b: 待比较的第二个 x509.Certificate 对象。
//
// 出参:
// - 是否相同。
func EqualCertificate(a, b *x509.Certificate) bool {
return string(a.Signature) == string(b.Signature) &&
a.SignatureAlgorithm == b.SignatureAlgorithm &&
a.SerialNumber.String() == b.SerialNumber.String() &&
a.Issuer.SerialNumber == b.Issuer.SerialNumber &&
a.Subject.SerialNumber == b.Subject.SerialNumber
}
// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。
//
// 入参:
// - certPem: 证书 PEM 内容。
//
// 出参:
// - cert: x509.Certificate 对象。
// - err: 错误。
func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) {
pemData := []byte(certPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
cert, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
return cert, nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: ecdsa.PrivateKey 对象。
// - err: 错误。
func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err error) {
pemData := []byte(privkeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
privkey, err = x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
return privkey, nil
}
// 从 PEM 编码的私钥字符串解析并返回一个 rsa.PrivateKey 对象。
//
// 入参:
// - privkeyPem: 私钥 PEM 内容。
//
// 出参:
// - privkey: rsa.PrivateKey 对象。
// - err: 错误。
func ParsePKCS1PrivateKeyFromPEM(privkeyPem string) (privkey *rsa.PrivateKey, err error) {
pemData := []byte(privkeyPem)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse private key: %w", err)
}
return privkey, nil
}
// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。
//
// 入参:
// - privkey: ecdsa.PrivateKey 对象。
//
// 出参:
// - privkeyPem: 私钥 PEM 内容。
// - err: 错误。
func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPem string, err error) {
data, err := x509.MarshalECPrivateKey(privkey)
if err != nil {
return "", fmt.Errorf("failed to marshal EC private key: %w", err)
}
block := &pem.Block{
Type: "EC PRIVATE KEY",
Bytes: data,
}
return string(pem.EncodeToMemory(block)), nil
}

View File

@ -0,0 +1,26 @@
package huaweicloudcdnsdk
import (
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core"
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
)
type Client struct {
hcCdn.CdnClient
}
func NewClient(hcClient *core.HcHttpClient) *Client {
return &Client{
CdnClient: *hcCdn.NewCdnClient(hcClient),
}
}
func (c *Client) UploadDomainMultiCertificatesEx(request *UpdateDomainMultiCertificatesExRequest) (*UpdateDomainMultiCertificatesExResponse, error) {
requestDef := hcCdn.GenReqDefForUpdateDomainMultiCertificates()
if resp, err := c.HcClient.Sync(request, requestDef); err != nil {
return nil, err
} else {
return resp.(*UpdateDomainMultiCertificatesExResponse), nil
}
}

View File

@ -0,0 +1,62 @@
package huaweicloudcdnsdk
import (
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
"github.com/usual2970/certimate/internal/pkg/utils/cast"
)
type UpdateDomainMultiCertificatesExRequestBodyContent struct {
hcCdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"`
// 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求,可能需要等之后 SDK 更新。
SCMCertificateId *string `json:"scm_certificate_id,omitempty"`
}
type UpdateDomainMultiCertificatesExRequestBody struct {
Https *UpdateDomainMultiCertificatesExRequestBodyContent `json:"https,omitempty"`
}
type UpdateDomainMultiCertificatesExRequest struct {
Body *UpdateDomainMultiCertificatesExRequestBody `json:"body,omitempty"`
}
type UpdateDomainMultiCertificatesExResponse struct {
hcCdnModel.UpdateDomainMultiCertificatesResponse
}
func (m *UpdateDomainMultiCertificatesExRequestBodyContent) MergeConfig(src *hcCdnModel.ConfigsGetBody) *UpdateDomainMultiCertificatesExRequestBodyContent {
if src == nil {
return m
}
// 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去。
// 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化。
if *src.OriginProtocol == "follow" {
m.AccessOriginWay = cast.Int32Ptr(1)
} else if *src.OriginProtocol == "http" {
m.AccessOriginWay = cast.Int32Ptr(2)
} else if *src.OriginProtocol == "https" {
m.AccessOriginWay = cast.Int32Ptr(3)
}
if src.ForceRedirect != nil {
m.ForceRedirectConfig = &hcCdnModel.ForceRedirect{}
if src.ForceRedirect.Status == "on" {
m.ForceRedirectConfig.Switch = 1
m.ForceRedirectConfig.RedirectType = src.ForceRedirect.Type
} else {
m.ForceRedirectConfig.Switch = 0
}
}
if src.Https != nil {
if *src.Https.Http2Status == "on" {
m.Http2 = cast.Int32Ptr(1)
}
}
return m
}

160
internal/pkg/vendors/qiniu-sdk/client.go vendored Normal file
View File

@ -0,0 +1,160 @@
package qiniusdk
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/qiniu/go-sdk/v7/auth"
xhttp "github.com/usual2970/certimate/internal/utils/http"
)
const qiniuHost = "http://api.qiniu.com"
type Client struct {
mac *auth.Credentials
}
func NewClient(mac *auth.Credentials) *Client {
if mac == nil {
mac = auth.Default()
}
return &Client{mac: mac}
}
func (c *Client) GetDomainInfo(domain string) (*GetDomainInfoResponse, error) {
respBytes, err := c.sendReq(http.MethodGet, fmt.Sprintf("domain/%s", domain), nil)
if err != nil {
return nil, err
}
resp := &GetDomainInfoResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) {
req := &ModifyDomainHttpsConfRequest{
DomainInfoHttpsData: DomainInfoHttpsData{
CertID: certId,
ForceHttps: forceHttps,
Http2Enable: http2Enable,
},
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/httpsconf", domain), bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
resp := &ModifyDomainHttpsConfResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enable bool) (*EnableDomainHttpsResponse, error) {
req := &EnableDomainHttpsRequest{
DomainInfoHttpsData: DomainInfoHttpsData{
CertID: certId,
ForceHttps: forceHttps,
Http2Enable: http2Enable,
},
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/sslize", domain), bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
resp := &EnableDomainHttpsResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) UploadSslCert(name, commonName, pri, ca string) (*UploadSslCertResponse, error) {
req := &UploadSslCertRequest{
Name: name,
CommonName: commonName,
Pri: pri,
Ca: ca,
}
reqBytes, err := json.Marshal(req)
if err != nil {
return nil, err
}
respBytes, err := c.sendReq(http.MethodPost, "sslcert", bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
resp := &UploadSslCertResponse{}
err = json.Unmarshal(respBytes, resp)
if err != nil {
return nil, err
}
if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 {
return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error)
}
return resp, nil
}
func (c *Client) sendReq(method string, path string, body io.Reader) ([]byte, error) {
req := xhttp.BuildReq(fmt.Sprintf("%s/%s", qiniuHost, path), method, body, map[string]string{
"Content-Type": "application/json",
})
if err := c.mac.AddToken(auth.TokenQBox, req); err != nil {
return nil, err
}
respBody, err := xhttp.ToRequest(req)
if err != nil {
return nil, err
}
defer respBody.Close()
res, err := io.ReadAll(respBody)
if err != nil {
return nil, err
}
return res, nil
}

View File

@ -0,0 +1,53 @@
package qiniusdk
type UploadSslCertRequest struct {
Name string `json:"name"`
CommonName string `json:"common_name"`
Pri string `json:"pri"`
Ca string `json:"ca"`
}
type UploadSslCertResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
CertID string `json:"certID"`
}
type DomainInfoHttpsData struct {
CertID string `json:"certId"`
ForceHttps bool `json:"forceHttps"`
Http2Enable bool `json:"http2Enable"`
}
type GetDomainInfoResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
Name string `json:"name"`
Type string `json:"type"`
CName string `json:"cname"`
Https *DomainInfoHttpsData `json:"https"`
PareDomain string `json:"pareDomain"`
OperationType string `json:"operationType"`
OperatingState string `json:"operatingState"`
OperatingStateDesc string `json:"operatingStateDesc"`
CreateAt string `json:"createAt"`
ModifyAt string `json:"modifyAt"`
}
type ModifyDomainHttpsConfRequest struct {
DomainInfoHttpsData
}
type ModifyDomainHttpsConfResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}
type EnableDomainHttpsRequest struct {
DomainInfoHttpsData
}
type EnableDomainHttpsResponse struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}

View File

@ -5,7 +5,8 @@ import (
"time"
)
// RandStr 随机生成指定长度字符串
// Deprecated: this will be removed in the future.
// 随机生成指定长度字符串
func RandStr(n int) string {
seed := time.Now().UnixNano()
source := rand.NewSource(seed)

View File

@ -1,16 +1,17 @@
import { createContext, useContext } from "react";
import { createContext, useContext, type Context as ReactContext } from "react";
import { DeployConfig } from "@/domain/domain";
import { type DeployConfig } from "@/domain/domain";
type DeployEditContext = {
deploy: DeployConfig;
error: Record<string, string | undefined>;
setDeploy: (deploy: DeployConfig) => void;
setError: (error: Record<string, string | undefined>) => void;
export type DeployEditContext<T extends DeployConfig["config"] = DeployConfig["config"]> = {
config: Omit<DeployConfig, "config"> & { config: T };
setConfig: (config: Omit<DeployConfig, "config"> & { config: T }) => void;
errors: { [K in keyof T]?: string };
setErrors: (error: { [K in keyof T]?: string }) => void;
};
export const Context = createContext<DeployEditContext>({} as DeployEditContext);
export const useDeployEditContext = () => {
return useContext(Context);
};
export function useDeployEditContext<T extends DeployConfig["config"] = DeployConfig["config"]>() {
return useContext<DeployEditContext<T>>(Context as unknown as ReactContext<DeployEditContext<T>>);
}

View File

@ -8,7 +8,7 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ScrollArea } from "@/components/ui/scroll-area";
import AccessEditDialog from "./AccessEditDialog";
import { Context as DeployEditContext } from "./DeployEdit";
import { Context as DeployEditContext, type DeployEditContext as DeployEditContextType } from "./DeployEdit";
import DeployToAliyunOSS from "./DeployToAliyunOSS";
import DeployToAliyunCDN from "./DeployToAliyunCDN";
import DeployToAliyunCLB from "./DeployToAliyunCLB";
@ -49,7 +49,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
type: "",
});
const [error, setError] = useState<Record<string, string | undefined>>({});
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
const [open, setOpen] = useState(false);
@ -66,10 +66,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
useEffect(() => {
setDeployType(locDeployConfig.type);
setError({});
setErrors({});
}, [locDeployConfig.type]);
const setDeploy = useCallback(
const setConfig = useCallback(
(deploy: DeployConfig) => {
if (deploy.type !== locDeployConfig.type) {
setLocDeployConfig({ ...deploy, access: "", config: {} });
@ -94,10 +94,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
const handleSaveClick = () => {
// 验证数据
const newError = { ...error };
const newError = { ...errors };
newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : "";
newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : "";
setError(newError);
setErrors(newError);
if (Object.values(newError).some((e) => !!e)) return;
// 保存数据
@ -108,7 +108,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
access: "",
type: "",
});
setError({});
setErrors({});
// 关闭弹框
setOpen(false);
@ -171,10 +171,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
return (
<DeployEditContext.Provider
value={{
deploy: locDeployConfig,
error: error,
setDeploy: setDeploy,
setError: setError,
config: locDeployConfig as DeployEditContextType["config"],
setConfig: setConfig as DeployEditContextType["setConfig"],
errors: errors as DeployEditContextType["errors"],
setErrors: setErrors as DeployEditContextType["setErrors"],
}}
>
<Dialog open={open} onOpenChange={setOpen}>
@ -199,7 +199,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
<Select
value={locDeployConfig.type}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, type: val });
setConfig({ ...locDeployConfig, type: val });
}}
>
<SelectTrigger className="mt-2">
@ -220,7 +220,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.type}</div>
<div className="text-red-500 text-sm mt-1">{errors.type}</div>
</div>
{/* 授权配置 */}
@ -241,7 +241,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
<Select
value={locDeployConfig.access}
onValueChange={(val: string) => {
setDeploy({ ...locDeployConfig, access: val });
setConfig({ ...locDeployConfig, access: val });
}}
>
<SelectTrigger className="mt-2">
@ -262,7 +262,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro
</SelectContent>
</Select>
<div className="text-red-500 text-sm mt-1">{error.access}</div>
<div className="text-red-500 text-sm mt-1">{errors.access}</div>
</div>
{/* 其他参数 */}

View File

@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
import DeployEditDialog from "./DeployEditDialog";
import { DeployConfig } from "@/domain/domain";
import { accessProvidersMap } from "@/domain/access";
import { deployTargetsMap } from "@/domain/domain";
import { useConfigContext } from "@/providers/config";
type DeployItemProps = {
@ -18,10 +19,11 @@ type DeployItemProps = {
};
const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => {
const { t } = useTranslation();
const {
config: { accesses },
} = useConfigContext();
const { t } = useTranslation();
const access = accesses.find((access) => access.id === item.access);
@ -34,11 +36,7 @@ const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => {
};
const getTypeName = () => {
if (!access) {
return "";
}
return t(accessProvidersMap.get(access.configType)?.name || "");
return t(deployTargetsMap.get(item.type)?.name || "");
};
return (

View File

@ -8,27 +8,31 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useDeployEditContext } from "./DeployEdit";
type DeployToAliyunALBConfigParams = {
region?: string;
resourceType?: string;
loadbalancerId?: string;
listenerId?: string;
};
const DeployToAliyunALB = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToAliyunALBConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
region: "cn-hangzhou",
resourceType: "",
loadbalancerId: "",
listenerId: "",
},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z
@ -50,25 +54,15 @@ const DeployToAliyunALB = () => {
});
useEffect(() => {
const res = formSchema.safeParse(data.config);
if (!res.success) {
setError({
...error,
region: res.error.errors.find((e) => e.path[0] === "region")?.message,
resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message,
});
} else {
setError({
...error,
region: undefined,
resourceType: undefined,
loadbalancerId: undefined,
listenerId: undefined,
});
}
}, [data]);
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -77,28 +71,28 @@ const DeployToAliyunALB = () => {
<Input
placeholder={t("domain.deployment.form.aliyun_alb_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_alb_resource_type.label")}</Label>
<Select
value={data?.config?.resourceType}
value={config?.config?.resourceType}
onValueChange={(value) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
>
<SelectTrigger>
@ -111,46 +105,46 @@ const DeployToAliyunALB = () => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
{data?.config?.resourceType === "loadbalancer" ? (
{config?.config?.resourceType === "loadbalancer" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_alb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={data?.config?.loadbalancerId}
value={config?.config?.loadbalancerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
) : (
<></>
)}
{data?.config?.resourceType === "listener" ? (
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_alb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_alb_listener_id.placeholder")}
className="w-full mt-1"
value={data?.config?.listenerId}
value={config?.config?.listenerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.listenerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>

View File

@ -7,45 +7,42 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToAliyunCDNConfigParams = {
domain?: string;
};
const DeployToAliyunCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToAliyunCDNConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
domain: "",
},
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
@ -53,33 +50,16 @@ const DeployToAliyunCDN = () => {
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -8,19 +8,24 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useDeployEditContext } from "./DeployEdit";
type DeployToAliyunCLBConfigParams = {
region?: string;
resourceType?: string;
loadbalancerId?: string;
listenerPort?: string;
};
const DeployToAliyunCLB = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToAliyunCLBConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
region: "cn-hangzhou",
resourceType: "",
loadbalancerId: "",
listenerPort: "443",
},
});
@ -28,7 +33,7 @@ const DeployToAliyunCLB = () => {
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z
@ -50,25 +55,15 @@ const DeployToAliyunCLB = () => {
});
useEffect(() => {
const res = formSchema.safeParse(data.config);
if (!res.success) {
setError({
...error,
region: res.error.errors.find((e) => e.path[0] === "region")?.message,
resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerPort: res.error.errors.find((e) => e.path[0] === "listenerPort")?.message,
});
} else {
setError({
...error,
region: undefined,
resourceType: undefined,
loadbalancerId: undefined,
listenerPort: undefined,
});
}
}, [data]);
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerPort: res.error?.errors?.find((e) => e.path[0] === "listenerPort")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -77,28 +72,28 @@ const DeployToAliyunCLB = () => {
<Input
placeholder={t("domain.deployment.form.aliyun_clb_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_clb_resource_type.label")}</Label>
<Select
value={data?.config?.resourceType}
value={config?.config?.resourceType}
onValueChange={(value) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value?.trim();
draft.config.resourceType = value;
});
setDeploy(newData);
setConfig(nv);
}}
>
<SelectTrigger>
@ -111,7 +106,7 @@ const DeployToAliyunCLB = () => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
<div>
@ -119,34 +114,34 @@ const DeployToAliyunCLB = () => {
<Input
placeholder={t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={data?.config?.loadbalancerId}
value={config?.config?.loadbalancerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
{data?.config?.resourceType === "listener" ? (
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_clb_listener_port.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_clb_listener_port.placeholder")}
className="w-full mt-1"
value={data?.config?.listenerPort}
value={config?.config?.listenerPort}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerPort = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.listenerPort}</div>
<div className="text-red-600 text-sm mt-1">{errors?.listenerPort}</div>
</div>
) : (
<></>

View File

@ -8,27 +8,31 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useDeployEditContext } from "./DeployEdit";
type DeployToAliyunNLBConfigParams = {
region?: string;
resourceType?: string;
loadbalancerId?: string;
listenerId?: string;
};
const DeployToAliyunNLB = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToAliyunNLBConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
region: "cn-hangzhou",
resourceType: "",
loadbalancerId: "",
listenerId: "",
},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z
@ -50,25 +54,15 @@ const DeployToAliyunNLB = () => {
});
useEffect(() => {
const res = formSchema.safeParse(data.config);
if (!res.success) {
setError({
...error,
region: res.error.errors.find((e) => e.path[0] === "region")?.message,
resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message,
});
} else {
setError({
...error,
region: undefined,
resourceType: undefined,
loadbalancerId: undefined,
listenerId: undefined,
});
}
}, [data]);
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -77,28 +71,28 @@ const DeployToAliyunNLB = () => {
<Input
placeholder={t("domain.deployment.form.aliyun_nlb_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_resource_type.label")}</Label>
<Select
value={data?.config?.resourceType}
value={config?.config?.resourceType}
onValueChange={(value) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value?.trim();
draft.config.resourceType = value;
});
setDeploy(newData);
setConfig(nv);
}}
>
<SelectTrigger>
@ -111,46 +105,46 @@ const DeployToAliyunNLB = () => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
{data?.config?.resourceType === "loadbalancer" ? (
{config?.config?.resourceType === "loadbalancer" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={data?.config?.loadbalancerId}
value={config?.config?.loadbalancerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
) : (
<></>
)}
{data?.config?.resourceType === "listener" ? (
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.aliyun_nlb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.aliyun_nlb_listener_id.placeholder")}
className="w-full mt-1"
value={data?.config?.listenerId}
value={config?.config?.listenerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.listenerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>

View File

@ -7,65 +7,53 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToAliyunOSSConfigParams = {
endpoint?: string;
bucket?: string;
domain?: string;
};
const DeployToAliyunOSS = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToAliyunOSSConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
endpoint: "oss-cn-hangzhou.aliyuncs.com",
bucket: "",
domain: "",
endpoint: "oss.aliyuncs.com",
},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
useEffect(() => {
const resp = bucketSchema.safeParse(data.config?.bucket);
if (!resp.success) {
setError({
...error,
bucket: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
const formSchema = z.object({
endpoint: z.string().min(1, {
message: t("domain.deployment.form.aliyun_oss_endpoint.placeholder"),
}),
bucket: z.string().min(1, {
message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"),
}),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
const bucketSchema = z.string().min(1, {
message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
endpoint: res.error?.errors?.find((e) => e.path[0] === "endpoint")?.message,
bucket: res.error?.errors?.find((e) => e.path[0] === "bucket")?.message,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -74,20 +62,16 @@ const DeployToAliyunOSS = () => {
<Input
placeholder={t("domain.deployment.form.aliyun_oss_endpoint.placeholder")}
className="w-full mt-1"
value={data?.config?.endpoint}
value={config?.config?.endpoint}
onChange={(e) => {
const temp = e.target.value;
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.endpoint = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.endpoint = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.endpoint}</div>
<div className="text-red-600 text-sm mt-1">{errors?.endpoint}</div>
</div>
<div>
@ -95,33 +79,16 @@ const DeployToAliyunOSS = () => {
<Input
placeholder={t("domain.deployment.form.aliyun_oss_bucket.placeholder")}
className="w-full mt-1"
value={data?.config?.bucket}
value={config?.config?.bucket}
onChange={(e) => {
const temp = e.target.value;
const resp = bucketSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
bucket: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.bucket = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.bucket = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.bucket}</div>
<div className="text-red-600 text-sm mt-1">{errors?.bucket}</div>
</div>
<div>
@ -129,33 +96,16 @@ const DeployToAliyunOSS = () => {
<Input
placeholder={t("domain.deployment.form.domain.label")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -7,63 +7,66 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToHuaweiCloudCDNConfigParams = {
region?: string;
domain?: string;
};
const DeployToHuaweiCloudCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToHuaweiCloudCDNConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
region: "cn-north-1",
domain: "",
},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
const formSchema = z.object({
region: z.string().min(1, {
message: t("domain.deployment.form.huaweicloud_cdn_region.placeholder"),
}),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_region.label")}</Label>
<Label>{t("domain.deployment.form.huaweicloud_cdn_region.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_region.placeholder")}
placeholder={t("domain.deployment.form.huaweicloud_cdn_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
@ -71,16 +74,16 @@ const DeployToHuaweiCloudCDN = () => {
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -8,34 +8,40 @@ import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useDeployEditContext } from "./DeployEdit";
const DeployToHuaweiCloudCDN = () => {
type DeployToHuaweiCloudELBConfigParams = {
region?: string;
resourceType?: string;
certificateId?: string;
loadbalancerId?: string;
listenerId?: string;
};
const DeployToHuaweiCloudELB = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToHuaweiCloudELBConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
region: "cn-north-1",
resourceType: "",
certificateId: "",
loadbalancerId: "",
listenerId: "",
},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z
.object({
region: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_region.placeholder")),
resourceType: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder")),
resourceType: z.union([z.literal("certificate"), z.literal("loadbalancer"), z.literal("listener")], {
message: t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder"),
}),
certificateId: z.string().optional(),
loadbalancerId: z.string().optional(),
listenerId: z.string().optional(),
@ -54,27 +60,16 @@ const DeployToHuaweiCloudCDN = () => {
});
useEffect(() => {
const res = formSchema.safeParse(data.config);
if (!res.success) {
setError({
...error,
region: res.error.errors.find((e) => e.path[0] === "region")?.message,
resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message,
certificateId: res.error.errors.find((e) => e.path[0] === "certificateId")?.message,
loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message,
});
} else {
setError({
...error,
region: undefined,
resourceType: undefined,
certificateId: undefined,
loadbalancerId: undefined,
listenerId: undefined,
});
}
}, [data]);
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message,
certificateId: res.error?.errors?.find((e) => e.path[0] === "certificateId")?.message,
loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -83,28 +78,28 @@ const DeployToHuaweiCloudCDN = () => {
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_resource_type.label")}</Label>
<Select
value={data?.config?.resourceType}
value={config?.config?.resourceType}
onValueChange={(value) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value;
});
setDeploy(newData);
setConfig(nv);
}}
>
<SelectTrigger>
@ -118,67 +113,67 @@ const DeployToHuaweiCloudCDN = () => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{error?.resourceType}</div>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
{data?.config?.resourceType === "certificate" ? (
{config?.config?.resourceType === "certificate" ? (
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_certificate_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder")}
className="w-full mt-1"
value={data?.config?.certificateId}
value={config?.config?.certificateId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.certificateId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.certificateId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.certificateId}</div>
</div>
) : (
<></>
)}
{data?.config?.resourceType === "loadbalancer" ? (
{config?.config?.resourceType === "loadbalancer" ? (
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={data?.config?.loadbalancerId}
value={config?.config?.loadbalancerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.loadbalancerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
) : (
<></>
)}
{data?.config?.resourceType === "listener" ? (
{config?.config?.resourceType === "listener" ? (
<div>
<Label>{t("domain.deployment.form.huaweicloud_elb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.huaweicloud_elb_listener_id.placeholder")}
className="w-full mt-1"
value={data?.config?.listenerId}
value={config?.config?.listenerId}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.listenerId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>
@ -187,4 +182,4 @@ const DeployToHuaweiCloudCDN = () => {
);
};
export default DeployToHuaweiCloudCDN;
export default DeployToHuaweiCloudELB;

View File

@ -1,23 +1,30 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToKubernetesSecretConfigParams = {
namespace?: string;
secretName?: string;
secretDataKeyForCrt?: string;
secretDataKeyForKey?: string;
};
const DeployToKubernetesSecret = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToKubernetesSecretConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
namespace: "default",
secretName: "",
secretDataKeyForCrt: "tls.crt",
secretDataKeyForKey: "tls.key",
},
@ -26,9 +33,35 @@ const DeployToKubernetesSecret = () => {
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z.object({
namespace: z.string().min(1, {
message: t("domain.deployment.form.k8s_namespace.placeholder"),
}),
secretName: z.string().min(1, {
message: t("domain.deployment.form.k8s_secret_name.placeholder"),
}),
secretDataKeyForCrt: z.string().min(1, {
message: t("domain.deployment.form.k8s_secret_data_key_for_crt.placeholder"),
}),
secretDataKeyForKey: z.string().min(1, {
message: t("domain.deployment.form.k8s_secret_data_key_for_key.placeholder"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
namespace: res.error?.errors?.find((e) => e.path[0] === "namespace")?.message,
secretName: res.error?.errors?.find((e) => e.path[0] === "secretName")?.message,
secretDataKeyForCrt: res.error?.errors?.find((e) => e.path[0] === "secretDataKeyForCrt")?.message,
secretDataKeyForKey: res.error?.errors?.find((e) => e.path[0] === "secretDataKeyForKey")?.message,
});
}, [config]);
return (
<>
<div className="flex flex-col space-y-8">
@ -37,13 +70,13 @@ const DeployToKubernetesSecret = () => {
<Input
placeholder={t("domain.deployment.form.k8s_namespace.label")}
className="w-full mt-1"
value={data?.config?.namespace}
value={config?.config?.namespace}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.namespace = e.target.value;
draft.config.namespace = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
</div>
@ -53,13 +86,13 @@ const DeployToKubernetesSecret = () => {
<Input
placeholder={t("domain.deployment.form.k8s_secret_name.label")}
className="w-full mt-1"
value={data?.config?.secretName}
value={config?.config?.secretName}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.secretName = e.target.value;
draft.config.secretName = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
</div>
@ -69,13 +102,13 @@ const DeployToKubernetesSecret = () => {
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_crt.label")}
className="w-full mt-1"
value={data?.config?.secretDataKeyForCrt}
value={config?.config?.secretDataKeyForCrt}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForCrt = e.target.value;
draft.config.secretDataKeyForCrt = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
</div>
@ -85,13 +118,13 @@ const DeployToKubernetesSecret = () => {
<Input
placeholder={t("domain.deployment.form.k8s_secret_data_key_for_key.label")}
className="w-full mt-1"
value={data?.config?.secretDataKeyForKey}
value={config?.config?.secretDataKeyForKey}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.secretDataKeyForKey = e.target.value;
draft.config.secretDataKeyForKey = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
</div>

View File

@ -12,33 +12,40 @@ import { Textarea } from "@/components/ui/textarea";
import { useDeployEditContext } from "./DeployEdit";
import { cn } from "@/lib/utils";
type DeployToLocalConfigParams = {
format?: string;
certPath?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
jksKeypass?: string;
jksStorepass?: string;
shell?: string;
preCommand?: string;
command?: string;
};
const DeployToLocal = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToLocalConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
format: "pem",
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
pfxPassword: "",
jksAlias: "",
jksKeypass: "",
jksStorepass: "",
shell: "sh",
preCommand: "",
command: "sudo service nginx reload",
},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z
@ -86,68 +93,55 @@ const DeployToLocal = () => {
});
useEffect(() => {
const res = formSchema.safeParse(data.config);
if (!res.success) {
setError({
...error,
format: res.error.errors.find((e) => e.path[0] === "format")?.message,
certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message,
pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message,
jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message,
jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message,
jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message,
shell: res.error.errors.find((e) => e.path[0] === "shell")?.message,
preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message,
command: res.error.errors.find((e) => e.path[0] === "command")?.message,
});
} else {
setError({
...error,
format: undefined,
certPath: undefined,
keyPath: undefined,
pfxPassword: undefined,
jksAlias: undefined,
jksKeypass: undefined,
jksStorepass: undefined,
shell: undefined,
preCommand: undefined,
command: undefined,
});
}
}, [data]);
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
format: res.error?.errors?.find((e) => e.path[0] === "format")?.message,
certPath: res.error?.errors?.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error?.errors?.find((e) => e.path[0] === "keyPath")?.message,
pfxPassword: res.error?.errors?.find((e) => e.path[0] === "pfxPassword")?.message,
jksAlias: res.error?.errors?.find((e) => e.path[0] === "jksAlias")?.message,
jksKeypass: res.error?.errors?.find((e) => e.path[0] === "jksKeypass")?.message,
jksStorepass: res.error?.errors?.find((e) => e.path[0] === "jksStorepass")?.message,
shell: res.error?.errors?.find((e) => e.path[0] === "shell")?.message,
preCommand: res.error?.errors?.find((e) => e.path[0] === "preCommand")?.message,
command: res.error?.errors?.find((e) => e.path[0] === "command")?.message,
});
}, [config]);
useEffect(() => {
if (data.config?.format === "pem") {
if (/(.pfx|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt");
});
setDeploy(newData);
if (config.config?.format === "pem") {
if (/(.pfx|.jks)$/.test(config.config.certPath!)) {
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = config.config!.certPath!.replace(/(.pfx|.jks)$/, ".crt");
})
);
}
} else if (data.config?.format === "pfx") {
if (/(.crt|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx");
});
setDeploy(newData);
} else if (config.config?.format === "pfx") {
if (/(.crt|.jks)$/.test(config.config.certPath!)) {
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = config.config!.certPath!.replace(/(.crt|.jks)$/, ".pfx");
})
);
}
} else if (data.config?.format === "jks") {
if (/(.crt|.pfx)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks");
});
setDeploy(newData);
} else if (config.config?.format === "jks") {
if (/(.crt|.pfx)$/.test(config.config.certPath!)) {
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = config.config!.certPath!.replace(/(.crt|.pfx)$/, ".jks");
})
);
}
}
}, [data.config?.format]);
}, [config.config?.format]);
const getOptionCls = (val: string) => {
if (data.config?.shell === val) {
if (config.config?.shell === val) {
return "border-primary dark:border-primary";
}
@ -158,21 +152,23 @@ const DeployToLocal = () => {
switch (key) {
case "reload_nginx":
{
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.shell = "sh";
draft.config.command = "sudo service nginx reload";
});
setDeploy(newData);
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.shell = "sh";
draft.config.command = "sudo service nginx reload";
})
);
}
break;
case "binding_iis":
{
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.shell = "powershell";
draft.config.command = `
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.shell = "powershell";
draft.config.command = `
#
$pfxPath = "<your-pfx-path>" # PFX
$pfxPassword = "<your-pfx-password>" # PFX
@ -201,17 +197,18 @@ $binding.AddSslCertificate($thumbprint, "My")
#
Remove-Item -Path "$pfxPath" -Force
`.trim();
});
setDeploy(newData);
})
);
}
break;
case "binding_netsh":
{
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.shell = "powershell";
draft.config.command = `
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.shell = "powershell";
draft.config.command = `
#
$pfxPath = "<your-pfx-path>" # PFX
$pfxPassword = "<your-pfx-password>" # PFX
@ -232,8 +229,8 @@ netsh http add sslcert ipport=$addr certhash=$thumbprint
#
Remove-Item -Path "$pfxPath" -Force
`.trim();
});
setDeploy(newData);
})
);
}
break;
}
@ -245,13 +242,13 @@ Remove-Item -Path "$pfxPath" -Force
<div>
<Label>{t("domain.deployment.form.file_format.label")}</Label>
<Select
value={data?.config?.format}
value={config?.config?.format}
onValueChange={(value) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.format = value;
});
setDeploy(newData);
setConfig(nv);
}}
>
<SelectTrigger>
@ -265,7 +262,7 @@ Remove-Item -Path "$pfxPath" -Force
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{error?.format}</div>
<div className="text-red-600 text-sm mt-1">{errors?.format}</div>
</div>
<div>
@ -273,77 +270,77 @@ Remove-Item -Path "$pfxPath" -Force
<Input
placeholder={t("domain.deployment.form.file_cert_path.label")}
className="w-full mt-1"
value={data?.config?.certPath}
value={config?.config?.certPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.certPath}</div>
<div className="text-red-600 text-sm mt-1">{errors?.certPath}</div>
</div>
{data.config?.format === "pem" ? (
{config.config?.format === "pem" ? (
<div>
<Label>{t("domain.deployment.form.file_key_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_key_path.placeholder")}
className="w-full mt-1"
value={data?.config?.keyPath}
value={config?.config?.keyPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.keyPath = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.keyPath}</div>
<div className="text-red-600 text-sm mt-1">{errors?.keyPath}</div>
</div>
) : (
<></>
)}
{data.config?.format === "pfx" ? (
{config.config?.format === "pfx" ? (
<div>
<Label>{t("domain.deployment.form.file_pfx_password.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_pfx_password.placeholder")}
className="w-full mt-1"
value={data?.config?.pfxPassword}
value={config?.config?.pfxPassword}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.pfxPassword = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.pfxPassword}</div>
<div className="text-red-600 text-sm mt-1">{errors?.pfxPassword}</div>
</div>
) : (
<></>
)}
{data.config?.format === "jks" ? (
{config.config?.format === "jks" ? (
<>
<div>
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
className="w-full mt-1"
value={data?.config?.jksAlias}
value={config?.config?.jksAlias}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksAlias = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksAlias}</div>
<div className="text-red-600 text-sm mt-1">{errors?.jksAlias}</div>
</div>
<div>
@ -351,16 +348,16 @@ Remove-Item -Path "$pfxPath" -Force
<Input
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksKeypass}
value={config?.config?.jksKeypass}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksKeypass = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksKeypass}</div>
<div className="text-red-600 text-sm mt-1">{errors?.jksKeypass}</div>
</div>
<div>
@ -368,16 +365,16 @@ Remove-Item -Path "$pfxPath" -Force
<Input
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksStorepass}
value={config?.config?.jksStorepass}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksStorepass = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksStorepass}</div>
<div className="text-red-600 text-sm mt-1">{errors?.jksStorepass}</div>
</div>
</>
) : (
@ -388,13 +385,13 @@ Remove-Item -Path "$pfxPath" -Force
<Label>{t("domain.deployment.form.shell.label")}</Label>
<RadioGroup
className="flex mt-1"
value={data?.config?.shell}
value={config?.config?.shell}
onValueChange={(val) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.shell = val;
});
setDeploy(newData);
setConfig(nv);
}}
>
<div className="flex items-center space-x-2">
@ -422,24 +419,24 @@ Remove-Item -Path "$pfxPath" -Force
</Label>
</div>
</RadioGroup>
<div className="text-red-600 text-sm mt-1">{error?.shell}</div>
<div className="text-red-600 text-sm mt-1">{errors?.shell}</div>
</div>
<div>
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.preCommand}
value={config?.config?.preCommand}
placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.preCommand = e.target.value;
});
setDeploy(newData);
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{error?.preCommand}</div>
<div className="text-red-600 text-sm mt-1">{errors?.preCommand}</div>
</div>
<div>
@ -464,17 +461,17 @@ Remove-Item -Path "$pfxPath" -Force
</div>
<Textarea
className="mt-1"
value={data?.config?.command}
value={config?.config?.command}
placeholder={t("domain.deployment.form.shell_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.command = e.target.value;
});
setDeploy(newData);
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{error?.command}</div>
<div className="text-red-600 text-sm mt-1">{errors?.command}</div>
</div>
</div>
</>

View File

@ -7,45 +7,42 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToQiniuCDNConfigParams = {
domain?: string;
};
const DeployToQiniuCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToQiniuCDNConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
config: {
domain: "",
},
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
@ -53,33 +50,16 @@ const DeployToQiniuCDN = () => {
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -9,24 +9,32 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa
import { Textarea } from "@/components/ui/textarea";
import { useDeployEditContext } from "./DeployEdit";
type DeployToSSHConfigParams = {
format?: string;
certPath?: string;
keyPath?: string;
pfxPassword?: string;
jksAlias?: string;
jksKeypass?: string;
jksStorepass?: string;
shell?: string;
preCommand?: string;
command?: string;
};
const DeployToSSH = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToSSHConfigParams>();
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
format: "pem",
certPath: "/etc/nginx/ssl/nginx.crt",
keyPath: "/etc/nginx/ssl/nginx.key",
pfxPassword: "",
jksAlias: "",
jksKeypass: "",
jksStorepass: "",
preCommand: "",
command: "sudo service nginx reload",
},
});
@ -34,7 +42,7 @@ const DeployToSSH = () => {
}, []);
useEffect(() => {
setError({});
setErrors({});
}, []);
const formSchema = z
@ -79,63 +87,51 @@ const DeployToSSH = () => {
});
useEffect(() => {
const res = formSchema.safeParse(data.config);
if (!res.success) {
setError({
...error,
format: res.error.errors.find((e) => e.path[0] === "format")?.message,
certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message,
pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message,
jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message,
jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message,
jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message,
preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message,
command: res.error.errors.find((e) => e.path[0] === "command")?.message,
});
} else {
setError({
...error,
format: undefined,
certPath: undefined,
keyPath: undefined,
pfxPassword: undefined,
jksAlias: undefined,
jksKeypass: undefined,
jksStorepass: undefined,
preCommand: undefined,
command: undefined,
});
}
}, [data]);
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
format: res.error?.errors?.find((e) => e.path[0] === "format")?.message,
certPath: res.error?.errors?.find((e) => e.path[0] === "certPath")?.message,
keyPath: res.error?.errors?.find((e) => e.path[0] === "keyPath")?.message,
pfxPassword: res.error?.errors?.find((e) => e.path[0] === "pfxPassword")?.message,
jksAlias: res.error?.errors?.find((e) => e.path[0] === "jksAlias")?.message,
jksKeypass: res.error?.errors?.find((e) => e.path[0] === "jksKeypass")?.message,
jksStorepass: res.error?.errors?.find((e) => e.path[0] === "jksStorepass")?.message,
preCommand: res.error?.errors?.find((e) => e.path[0] === "preCommand")?.message,
command: res.error?.errors?.find((e) => e.path[0] === "command")?.message,
});
}, [config]);
useEffect(() => {
if (data.config?.format === "pem") {
if (/(.pfx|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt");
});
setDeploy(newData);
if (config.config?.format === "pem") {
if (/(.pfx|.jks)$/.test(config.config.certPath!)) {
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = config.config!.certPath!.replace(/(.pfx|.jks)$/, ".crt");
})
);
}
} else if (data.config?.format === "pfx") {
if (/(.crt|.jks)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx");
});
setDeploy(newData);
} else if (config.config?.format === "pfx") {
if (/(.crt|.jks)$/.test(config.config.certPath!)) {
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = config.config!.certPath!.replace(/(.crt|.jks)$/, ".pfx");
})
);
}
} else if (data.config?.format === "jks") {
if (/(.crt|.pfx)$/.test(data.config.certPath)) {
const newData = produce(data, (draft) => {
draft.config ??= {};
draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks");
});
setDeploy(newData);
} else if (config.config?.format === "jks") {
if (/(.crt|.pfx)$/.test(config.config.certPath!)) {
setConfig(
produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = config.config!.certPath!.replace(/(.crt|.pfx)$/, ".jks");
})
);
}
}
}, [data.config?.format]);
}, [config.config?.format]);
return (
<>
@ -143,13 +139,13 @@ const DeployToSSH = () => {
<div>
<Label>{t("domain.deployment.form.file_format.label")}</Label>
<Select
value={data?.config?.format}
value={config?.config?.format}
onValueChange={(value) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.format = value;
});
setDeploy(newData);
setConfig(nv);
}}
>
<SelectTrigger>
@ -163,7 +159,7 @@ const DeployToSSH = () => {
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{error?.format}</div>
<div className="text-red-600 text-sm mt-1">{errors?.format}</div>
</div>
<div>
@ -171,77 +167,77 @@ const DeployToSSH = () => {
<Input
placeholder={t("domain.deployment.form.file_cert_path.label")}
className="w-full mt-1"
value={data?.config?.certPath}
value={config?.config?.certPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.certPath = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.certPath}</div>
<div className="text-red-600 text-sm mt-1">{errors?.certPath}</div>
</div>
{data.config?.format === "pem" ? (
{config.config?.format === "pem" ? (
<div>
<Label>{t("domain.deployment.form.file_key_path.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_key_path.placeholder")}
className="w-full mt-1"
value={data?.config?.keyPath}
value={config?.config?.keyPath}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.keyPath = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.keyPath}</div>
<div className="text-red-600 text-sm mt-1">{errors?.keyPath}</div>
</div>
) : (
<></>
)}
{data.config?.format === "pfx" ? (
{config.config?.format === "pfx" ? (
<div>
<Label>{t("domain.deployment.form.file_pfx_password.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_pfx_password.placeholder")}
className="w-full mt-1"
value={data?.config?.pfxPassword}
value={config?.config?.pfxPassword}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.pfxPassword = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.pfxPassword}</div>
<div className="text-red-600 text-sm mt-1">{errors?.pfxPassword}</div>
</div>
) : (
<></>
)}
{data.config?.format === "jks" ? (
{config.config?.format === "jks" ? (
<>
<div>
<Label>{t("domain.deployment.form.file_jks_alias.label")}</Label>
<Input
placeholder={t("domain.deployment.form.file_jks_alias.placeholder")}
className="w-full mt-1"
value={data?.config?.jksAlias}
value={config?.config?.jksAlias}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksAlias = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksAlias}</div>
<div className="text-red-600 text-sm mt-1">{errors?.jksAlias}</div>
</div>
<div>
@ -249,16 +245,16 @@ const DeployToSSH = () => {
<Input
placeholder={t("domain.deployment.form.file_jks_keypass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksKeypass}
value={config?.config?.jksKeypass}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksKeypass = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksKeypass}</div>
<div className="text-red-600 text-sm mt-1">{errors?.jksKeypass}</div>
</div>
<div>
@ -266,16 +262,16 @@ const DeployToSSH = () => {
<Input
placeholder={t("domain.deployment.form.file_jks_storepass.placeholder")}
className="w-full mt-1"
value={data?.config?.jksStorepass}
value={config?.config?.jksStorepass}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.jksStorepass = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.jksStorepass}</div>
<div className="text-red-600 text-sm mt-1">{errors?.jksStorepass}</div>
</div>
</>
) : (
@ -286,34 +282,34 @@ const DeployToSSH = () => {
<Label>{t("domain.deployment.form.shell_pre_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.preCommand}
value={config?.config?.preCommand}
placeholder={t("domain.deployment.form.shell_pre_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.preCommand = e.target.value;
});
setDeploy(newData);
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{error?.preCommand}</div>
<div className="text-red-600 text-sm mt-1">{errors?.preCommand}</div>
</div>
<div>
<Label>{t("domain.deployment.form.shell_command.label")}</Label>
<Textarea
className="mt-1"
value={data?.config?.command}
value={config?.config?.command}
placeholder={t("domain.deployment.form.shell_command.placeholder")}
onChange={(e) => {
const newData = produce(data, (draft) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.command = e.target.value;
});
setDeploy(newData);
setConfig(nv);
}}
></Textarea>
<div className="text-red-600 text-sm mt-1">{error?.command}</div>
<div className="text-red-600 text-sm mt-1">{errors?.command}</div>
</div>
</div>
</>

View File

@ -7,34 +7,42 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
type DeployToTencentCDNParams = {
domain?: string;
};
const DeployToTencentCDN = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToTencentCDNParams>();
useEffect(() => {
setError({});
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
setErrors({});
}, []);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
const formSchema = z.object({
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
<div>
@ -42,33 +50,16 @@ const DeployToTencentCDN = () => {
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -5,106 +5,80 @@ import { produce } from "immer";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useDeployEditContext } from "./DeployEdit";
const DeployToTencentCLB = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
type DeployToTencentCLBParams = {
region?: string;
resourceType?: string;
loadbalancerId?: string;
listenerId?: string;
domain?: string;
};
const DeployToTencentCLB = () => {
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToTencentCLBParams>();
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
useEffect(() => {
const clbIdresp = clbIdSchema.safeParse(data.config?.clbId);
if (!clbIdresp.success) {
setError({
...error,
clbId: JSON.parse(clbIdresp.error.message)[0].message,
});
} else {
setError({
...error,
clbId: "",
});
}
}, [data]);
useEffect(() => {
const lsnIdresp = lsnIdSchema.safeParse(data.config?.lsnId);
if (!lsnIdresp.success) {
setError({
...error,
lsnId: JSON.parse(lsnIdresp.error.message)[0].message,
});
} else {
setError({
...error,
lsnId: "",
});
}
}, [data]);
useEffect(() => {
const regionResp = regionSchema.safeParse(data.config?.region);
if (!regionResp.success) {
setError({
...error,
region: JSON.parse(regionResp.error.message)[0].message,
});
} else {
setError({
...error,
region: "",
});
}
}, []);
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
lsnId: "",
clbId: "",
domain: "",
region: "",
region: "ap-guangzhou",
},
});
}
}, []);
const regionSchema = z.string().regex(/^ap-[a-z]+$/, {
message: t("domain.deployment.form.tencent_clb_region.placeholder"),
});
useEffect(() => {
setErrors({});
}, []);
const domainSchema = z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
const formSchema = z
.object({
region: z.string().min(1, t("domain.deployment.form.tencent_clb_region.placeholder")),
resourceType: z.union([z.literal("ssl-deploy"), z.literal("loadbalancer"), z.literal("listener"), z.literal("ruledomain")], {
message: t("domain.deployment.form.tencent_clb_resource_type.placeholder"),
}),
loadbalancerId: z.string().min(1, t("domain.deployment.form.tencent_clb_loadbalancer_id.placeholder")),
listenerId: z.string().optional(),
domain: z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
})
.refine(
(data) => {
switch (data.resourceType) {
case "ssl-deploy":
case "listener":
case "ruledomain":
return !!data.listenerId?.trim();
}
return true;
},
{
message: t("domain.deployment.form.tencent_clb_listener_id.placeholder"),
path: ["listenerId"],
}
)
.refine((data) => (data.resourceType === "ruledomain" ? !!data.domain?.trim() : true), {
message: t("domain.deployment.form.tencent_clb_ruledomain.placeholder"),
path: ["domain"],
});
const clbIdSchema = z.string().regex(/^lb-[a-zA-Z0-9]{8}$/, {
message: t("domain.deployment.form.tencent_clb_id.placeholder"),
});
const lsnIdSchema = z.string().regex(/^lbl-.{8}$/, {
message: t("domain.deployment.form.tencent_clb_listener.placeholder"),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message,
loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message,
listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -113,136 +87,124 @@ const DeployToTencentCLB = () => {
<Input
placeholder={t("domain.deployment.form.tencent_clb_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const temp = e.target.value;
const resp = regionSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
region: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
region: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.region = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_clb_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_id.placeholder")}
className="w-full mt-1"
value={data?.config?.clbId}
onChange={(e) => {
const temp = e.target.value;
const resp = clbIdSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
clbId: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
clbId: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.clbId = temp;
<Label>{t("domain.deployment.form.tencent_clb_resource_type.label")}</Label>
<Select
value={config?.config?.resourceType}
onValueChange={(value) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.resourceType = value;
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.clbId}</div>
>
<SelectTrigger>
<SelectValue placeholder={t("domain.deployment.form.tencent_clb_resource_type.placeholder")} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="ssl-deploy">{t("domain.deployment.form.tencent_clb_resource_type.option.ssl_deploy.label")}</SelectItem>
<SelectItem value="loadbalancer">{t("domain.deployment.form.tencent_clb_resource_type.option.loadbalancer.label")}</SelectItem>
<SelectItem value="listener">{t("domain.deployment.form.tencent_clb_resource_type.option.listener.label")}</SelectItem>
<SelectItem value="ruledomain">{t("domain.deployment.form.tencent_clb_resource_type.option.ruledomain.label")}</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<div className="text-red-600 text-sm mt-1">{errors?.resourceType}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_clb_listener.label")}</Label>
<Label>{t("domain.deployment.form.tencent_clb_loadbalancer_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_listener.placeholder")}
placeholder={t("domain.deployment.form.tencent_clb_loadbalancer_id.placeholder")}
className="w-full mt-1"
value={data?.config?.lsnId}
value={config?.config?.loadbalancerId}
onChange={(e) => {
const temp = e.target.value;
const resp = lsnIdSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
lsnId: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
lsnId: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.lsnId = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.loadbalancerId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.lsnId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.loadbalancerId}</div>
</div>
<div>
<Label>{t("domain.deployment.form.tencent_clb_domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
{config?.config?.resourceType === "ssl-deploy" || config?.config?.resourceType === "listener" || config?.config?.resourceType === "ruledomain" ? (
<div>
<Label>{t("domain.deployment.form.tencent_clb_listener_id.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_listener_id.placeholder")}
className="w-full mt-1"
value={config?.config?.listenerId}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.listenerId = e.target.value?.trim();
});
} else {
setError({
...error,
domain: "",
});
}
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.listenerId}</div>
</div>
) : (
<></>
)}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
});
setDeploy(newData);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
</div>
{config?.config?.resourceType === "ssl-deploy" ? (
<div>
<Label>{t("domain.deployment.form.tencent_clb_domain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_domain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
) : (
<></>
)}
{config?.config?.resourceType === "ruledomain" ? (
<div>
<Label>{t("domain.deployment.form.tencent_clb_ruledomain.label")}</Label>
<Input
placeholder={t("domain.deployment.form.tencent_clb_ruledomain.placeholder")}
className="w-full mt-1"
value={config?.config?.domain}
onChange={(e) => {
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
) : (
<></>
)}
</div>
);
};

View File

@ -7,84 +7,49 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useDeployEditContext } from "./DeployEdit";
const DeployToTencentCOS = () => {
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
type DeployToTencentCOSParams = {
region?: string;
bucket?: string;
domain?: string;
};
const DeployToTencentCOS = () => {
const { t } = useTranslation();
useEffect(() => {
setError({});
}, []);
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToTencentCOSParams>();
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
useEffect(() => {
const bucketResp = bucketSchema.safeParse(data.config?.bucket);
if (!bucketResp.success) {
setError({
...error,
bucket: JSON.parse(bucketResp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
}, []);
useEffect(() => {
const regionResp = regionSchema.safeParse(data.config?.region);
if (!regionResp.success) {
setError({
...error,
region: JSON.parse(regionResp.error.message)[0].message,
});
} else {
setError({
...error,
region: "",
});
}
}, []);
useEffect(() => {
if (!data.id) {
setDeploy({
...data,
if (!config.id) {
setConfig({
...config,
config: {
region: "",
bucket: "",
domain: "",
region: "ap-guangzhou",
},
});
}
}, []);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
useEffect(() => {
setErrors({});
}, []);
const formSchema = z.object({
region: z.string().min(1, t("domain.deployment.form.tencent_cos_region.placeholder")),
bucket: z.string().min(1, t("domain.deployment.form.tencent_cos_bucket.placeholder")),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
const regionSchema = z.string().regex(/^ap-[a-z]+$/, {
message: t("domain.deployment.form.tencent_cos_region.placeholder"),
});
const bucketSchema = z.string().regex(/^.+-\d+$/, {
message: t("domain.deployment.form.tencent_cos_bucket.placeholder"),
});
useEffect(() => {
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
region: res.error?.errors?.find((e) => e.path[0] === "region")?.message,
bucket: res.error?.errors?.find((e) => e.path[0] === "bucket")?.message,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -93,33 +58,16 @@ const DeployToTencentCOS = () => {
<Input
placeholder={t("domain.deployment.form.tencent_cos_region.placeholder")}
className="w-full mt-1"
value={data?.config?.region}
value={config?.config?.region}
onChange={(e) => {
const temp = e.target.value;
const resp = regionSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
region: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
region: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.region = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.region = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.region}</div>
<div className="text-red-600 text-sm mt-1">{errors?.region}</div>
</div>
<div>
@ -127,33 +75,16 @@ const DeployToTencentCOS = () => {
<Input
placeholder={t("domain.deployment.form.tencent_cos_bucket.placeholder")}
className="w-full mt-1"
value={data?.config?.bucket}
value={config?.config?.bucket}
onChange={(e) => {
const temp = e.target.value;
const resp = bucketSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
bucket: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
bucket: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.bucket = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.bucket = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.bucket}</div>
<div className="text-red-600 text-sm mt-1">{errors?.bucket}</div>
</div>
<div>
@ -161,33 +92,16 @@ const DeployToTencentCOS = () => {
<Input
placeholder={t("domain.deployment.form.domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -8,52 +8,44 @@ import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { useDeployEditContext } from "./DeployEdit";
type DeployToTencentTEOParams = {
zoneId?: string;
domain?: string;
};
const DeployToTencentTEO = () => {
const { t } = useTranslation();
const { deploy: data, setDeploy, error, setError } = useDeployEditContext();
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToTencentTEOParams>();
useEffect(() => {
setError({});
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
const resp = domainSchema.safeParse(data.config?.domain);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
}, [data]);
setErrors({});
}, []);
const formSchema = z.object({
zoneId: z.string().min(1, t("domain.deployment.form.tencent_teo_zone_id.placeholder")),
domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
}),
});
useEffect(() => {
const resp = zoneIdSchema.safeParse(data.config?.zoneId);
if (!resp.success) {
setError({
...error,
zoneId: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
zoneId: "",
});
}
}, [data]);
const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, {
message: t("common.errmsg.domain_invalid"),
});
const zoneIdSchema = z.string().regex(/^zone-[0-9a-zA-Z]{9}$/, {
message: t("common.errmsg.zoneid_invalid"),
});
const res = formSchema.safeParse(config.config);
setErrors({
...errors,
zoneId: res.error?.errors?.find((e) => e.path[0] === "zoneId")?.message,
domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message,
});
}, [config]);
return (
<div className="flex flex-col space-y-8">
@ -62,33 +54,16 @@ const DeployToTencentTEO = () => {
<Input
placeholder={t("domain.deployment.form.tencent_teo_zone_id.placeholder")}
className="w-full mt-1"
value={data?.config?.zoneId}
value={config?.config?.zoneId}
onChange={(e) => {
const temp = e.target.value;
const resp = zoneIdSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
zoneId: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
zoneId: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.zoneId = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.zoneId = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.zoneId}</div>
<div className="text-red-600 text-sm mt-1">{errors?.zoneId}</div>
</div>
<div>
@ -96,33 +71,16 @@ const DeployToTencentTEO = () => {
<Textarea
placeholder={t("domain.deployment.form.tencent_teo_domain.placeholder")}
className="w-full mt-1"
value={data?.config?.domain}
value={config?.config?.domain}
onChange={(e) => {
const temp = e.target.value;
const resp = domainSchema.safeParse(temp);
if (!resp.success) {
setError({
...error,
domain: JSON.parse(resp.error.message)[0].message,
});
} else {
setError({
...error,
domain: "",
});
}
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
draft.config.domain = temp;
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.domain = e.target.value?.trim();
});
setDeploy(newData);
setConfig(nv);
}}
/>
<div className="text-red-600 text-sm mt-1">{error?.domain}</div>
<div className="text-red-600 text-sm mt-1">{errors?.domain}</div>
</div>
</div>
);

View File

@ -6,26 +6,31 @@ import KVList from "./KVList";
import { type KVType } from "@/domain/domain";
const DeployToWebhook = () => {
const { deploy: data, setDeploy } = useDeployEditContext();
const { setError } = useDeployEditContext();
const { config, setConfig, setErrors } = useDeployEditContext();
useEffect(() => {
setError({});
if (!config.id) {
setConfig({
...config,
config: {},
});
}
}, []);
useEffect(() => {
setErrors({});
}, []);
return (
<>
<KVList
variables={data?.config?.variables}
variables={config?.config?.variables}
onValueChange={(variables: KVType[]) => {
const newData = produce(data, (draft) => {
if (!draft.config) {
draft.config = {};
}
const nv = produce(config, (draft) => {
draft.config ??= {};
draft.config.variables = variables;
});
setDeploy(newData);
setConfig(nv);
}}
/>
</>

View File

@ -51,7 +51,6 @@
"common.errmsg.host_invalid": "Please enter the correct domain name or IP",
"common.errmsg.ip_invalid": "Please enter IP",
"common.errmsg.url_invalid": "Please enter a valid URL",
"common.errmsg.zoneid_invalid": "Please enter Zone ID",
"common.provider.aliyun": "Alibaba Cloud",
"common.provider.aliyun.oss": "Alibaba Cloud - OSS",
@ -65,7 +64,7 @@
"common.provider.tencent.ecdn": "Tencent Cloud - ECDN",
"common.provider.tencent.clb": "Tencent Cloud - CLB",
"common.provider.tencent.cos": "Tencent Cloud - COS",
"common.provider.tencent.teo": "Tencent Cloud - TEO",
"common.provider.tencent.teo": "Tencent Cloud - EdgeOne",
"common.provider.huaweicloud": "Huawei Cloud",
"common.provider.huaweicloud.cdn": "Huawei Cloud - CDN",
"common.provider.huaweicloud.elb": "Huawei Cloud - ELB",

View File

@ -95,14 +95,23 @@
"domain.deployment.form.tencent_cos_region.placeholder": "Please enter region (e.g. ap-guangzhou)",
"domain.deployment.form.tencent_cos_bucket.label": "Bucket",
"domain.deployment.form.tencent_cos_bucket.placeholder": "Please enter bucket",
"domain.deployment.form.tencent_cdn_region.label": "Region",
"domain.deployment.form.tencent_cdn_region.placeholder": "Please enter region (e.g. ap-guangzhou)",
"domain.deployment.form.tencent_clb_region.label": "Region",
"domain.deployment.form.tencent_clb_region.placeholder": "Please enter region (e.g. ap-guangzhou)",
"domain.deployment.form.tencent_clb_id.label": "CLB ID",
"domain.deployment.form.tencent_clb_id.placeholder": "Please enter CLB ID (e.g. lb-xxxxxxxx)",
"domain.deployment.form.tencent_clb_listener.label": "Listener ID",
"domain.deployment.form.tencent_clb_listener.placeholder": "Please enter listener ID (e.g. lbl-xxxxxxxx). The specific listener should have set the corresponding domain HTTPS forwarding, and the original certificate domain should be consistent with the certificate to be deployed.",
"domain.deployment.form.tencent_clb_resource_type.label": "Resource Type",
"domain.deployment.form.tencent_clb_resource_type.placeholder": "Please select CLB resource type",
"domain.deployment.form.tencent_clb_resource_type.option.ssl_deploy.label": "Through SSL Deploy",
"domain.deployment.form.tencent_clb_resource_type.option.loadbalancer.label": "CLB LoadBalancer",
"domain.deployment.form.tencent_clb_resource_type.option.listener.label": "CLB Listener",
"domain.deployment.form.tencent_clb_loadbalancer_id.label": "Loadbalancer ID",
"domain.deployment.form.tencent_clb_loadbalancer_id.placeholder": "Please enter Loadbalancer ID",
"domain.deployment.form.tencent_clb_listener_id.label": "Listener ID",
"domain.deployment.form.tencent_clb_listener_id.placeholder": "Please enter listener ID. The specific listener should have set the corresponding domain HTTPS forwarding, and the original certificate domain should be consistent with the certificate to be deployed.",
"domain.deployment.form.tencent_clb_domain.label": "Deploy to domain (Wildcard domain is also supported)",
"domain.deployment.form.tencent_clb_domain.placeholder": "Please enter domain to be deployed. If SNI is not enabled, you can leave it blank.",
"domain.deployment.form.tencent_clb_ruledomain.label": "Rule Domain",
"domain.deployment.form.tencent_clb_ruledomain.placeholder": "Please enter rule domain",
"domain.deployment.form.tencent_teo_zone_id.label": "Zone ID",
"domain.deployment.form.tencent_teo_zone_id.placeholder": "Please enter zone id, e.g. zone-xxxxxxxxx",
"domain.deployment.form.tencent_teo_domain.label": "Deploy to domain (Wildcard domain is also supported, but should be same as the config on server, one domain each line)",

View File

@ -51,7 +51,6 @@
"common.errmsg.host_invalid": "请输入正确的域名或 IP 地址",
"common.errmsg.ip_invalid": "请输入正确的 IP 地址",
"common.errmsg.url_invalid": "请输入正确的 URL",
"common.errmsg.zoneid_invalid": "请输入正确的 Zone ID",
"common.provider.aliyun": "阿里云",
"common.provider.aliyun.oss": "阿里云 - 对象存储 OSS",
@ -65,7 +64,7 @@
"common.provider.tencent.cdn": "腾讯云 - 内容分发网络 CDN",
"common.provider.tencent.ecdn": "腾讯云 - 全站加速网络 ECDN",
"common.provider.tencent.clb": "腾讯云 - 负载均衡 CLB",
"common.provider.tencent.teo": "腾讯云 - 边缘安全加速平台 EO",
"common.provider.tencent.teo": "腾讯云 - 边缘安全加速平台 EdgeOne",
"common.provider.huaweicloud": "华为云",
"common.provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN",
"common.provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB",
@ -88,4 +87,3 @@
"common.provider.lark": "飞书",
"common.provider.mail": "电子邮件"
}

View File

@ -65,7 +65,7 @@
"domain.deployment.form.aliyun_clb_region.placeholder": "请输入地域(如 cn-hangzhou",
"domain.deployment.form.aliyun_clb_resource_type.label": "替换方式",
"domain.deployment.form.aliyun_clb_resource_type.placeholder": "请选择替换方式",
"domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书(仅支持 HTTPS 监听)",
"domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听的证书",
"domain.deployment.form.aliyun_clb_resource_type.option.listener.label": "替换指定负载均衡监听的证书",
"domain.deployment.form.aliyun_clb_loadbalancer_id.label": "负载均衡器 ID",
"domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
@ -75,8 +75,8 @@
"domain.deployment.form.aliyun_alb_region.placeholder": "请输入地域(如 cn-hangzhou",
"domain.deployment.form.aliyun_alb_resource_type.label": "替换方式",
"domain.deployment.form.aliyun_alb_resource_type.placeholder": "请选择替换方式",
"domain.deployment.form.aliyun_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书(仅支持 HTTPS/QUIC 监听)",
"domain.deployment.form.aliyun_alb_resource_type.option.listener.label": "替换指定监听器的证书",
"domain.deployment.form.aliyun_alb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/QUIC 监听的证书",
"domain.deployment.form.aliyun_alb_resource_type.option.listener.label": "替换指定负载均衡监听器的证书",
"domain.deployment.form.aliyun_alb_loadbalancer_id.label": "负载均衡器 ID",
"domain.deployment.form.aliyun_alb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
"domain.deployment.form.aliyun_alb_listener_id.label": "监听器 ID",
@ -85,8 +85,8 @@
"domain.deployment.form.aliyun_nlb_region.placeholder": "请输入地域(如 cn-hangzhou",
"domain.deployment.form.aliyun_nlb_resource_type.label": "替换方式",
"domain.deployment.form.aliyun_nlb_resource_type.placeholder": "请选择替换方式",
"domain.deployment.form.aliyun_nlb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书(仅支持 TCPSSL 监听)",
"domain.deployment.form.aliyun_nlb_resource_type.option.listener.label": "替换指定监听器的证书",
"domain.deployment.form.aliyun_nlb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 TCPSSL 监听的证书",
"domain.deployment.form.aliyun_nlb_resource_type.option.listener.label": "替换指定负载均衡监听器的证书",
"domain.deployment.form.aliyun_nlb_loadbalancer_id.label": "负载均衡器 ID",
"domain.deployment.form.aliyun_nlb_loadbalancer_id.placeholder": "请输入负载均衡器 ID",
"domain.deployment.form.aliyun_nlb_listener_id.label": "监听器 ID",
@ -97,23 +97,33 @@
"domain.deployment.form.tencent_cos_bucket.placeholder": "请输入存储桶名",
"domain.deployment.form.tencent_clb_region.label": "地域",
"domain.deployment.form.tencent_clb_region.placeholder": "请输入地域(如 ap-guangzhou",
"domain.deployment.form.tencent_clb_id.label": "负载均衡器 ID",
"domain.deployment.form.tencent_clb_id.placeholder": "请输入负载均衡器实例 ID如 lb-xxxxxxxx",
"domain.deployment.form.tencent_clb_listener.label": "监听器 ID对应监听器应已设置对应域名 HTTPS 转发, 且原证书对应域名应与待部署证书的一致)",
"domain.deployment.form.tencent_clb_listener.placeholder": "请输入监听器 ID如 lb-xxxxxxxx",
"domain.deployment.form.tencent_clb_resource_type.label": "替换方式",
"domain.deployment.form.tencent_clb_resource_type.placeholder": "请选择替换方式",
"domain.deployment.form.tencent_clb_resource_type.option.ssl_deploy.label": "通过 SSL 服务部署到云资源实例",
"domain.deployment.form.tencent_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/TCPSSL/QUIC 监听器的证书",
"domain.deployment.form.tencent_clb_resource_type.option.listener.label": "替换指定负载均衡监听器的证书",
"domain.deployment.form.tencent_clb_resource_type.option.ruledomain.label": "替换指定七层监听转发规则域名的证书",
"domain.deployment.form.tencent_clb_loadbalancer_id.label": "负载均衡器 ID",
"domain.deployment.form.tencent_clb_loadbalancer_id.placeholder": "请输入负载均衡器实例 ID",
"domain.deployment.form.tencent_clb_listener_id.label": "监听器 ID",
"domain.deployment.form.tencent_clb_listener_id.placeholder": "请输入监听器 ID",
"domain.deployment.form.tencent_clb_domain.label": "部署到域名(支持泛域名)",
"domain.deployment.form.tencent_clb_domain.placeholder": "请输入部署到的域名, 如未开启 SNI, 可置空忽略此项",
"domain.deployment.form.tencent_clb_ruledomain.label": "转发域名",
"domain.deployment.form.tencent_clb_ruledomain.placeholder": "请输入七层监听转发规则域名",
"domain.deployment.form.tencent_teo_zone_id.label": "Zone ID",
"domain.deployment.form.tencent_teo_zone_id.placeholder": "请输入 Zone ID",
"domain.deployment.form.tencent_teo_domain.label": "部署到域名(支持泛域名, 应与服务器上配置的域名完全一致, 每行一个域名)",
"domain.deployment.form.tencent_teo_domain.placeholder": "请输入部署到的域名",
"domain.deployment.form.huaweicloud_cdn_region.label": "地域",
"domain.deployment.form.huaweicloud_cdn_region.placeholder": "请输入地域(如 cn-north-1",
"domain.deployment.form.huaweicloud_elb_region.label": "地域",
"domain.deployment.form.huaweicloud_elb_region.placeholder": "请输入地域(如 cn-north-1",
"domain.deployment.form.huaweicloud_elb_resource_type.label": "替换方式",
"domain.deployment.form.huaweicloud_elb_resource_type.placeholder": "请选择替换方式",
"domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label": "替换指定证书",
"domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听器的证书(仅支持 HTTPS 监听)",
"domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "替换指定监听器",
"domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听器的证书",
"domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "替换指定监听器的证书",
"domain.deployment.form.huaweicloud_elb_certificate_id.label": "证书 ID",
"domain.deployment.form.huaweicloud_elb_certificate_id.placeholder": "请输入证书 ID",
"domain.deployment.form.huaweicloud_elb_loadbalancer_id.label": "负载均衡器 ID",
@ -143,7 +153,7 @@
"domain.deployment.form.shell_preset_scripts.trigger": "使用预设脚本",
"domain.deployment.form.shell_preset_scripts.option.reload_nginx.label": "Bash - 重启 nginx",
"domain.deployment.form.shell_preset_scripts.option.binding_iis.label": "PowerShell - 导入并绑定到 IIS需管理员权限",
"domain.deployment.form.shell_preset_scripts.option.binding_netsh.label": "PowerShell - 导入并绑定到netsh需管理员权限",
"domain.deployment.form.shell_preset_scripts.option.binding_netsh.label": "PowerShell - 导入并绑定到 netsh需管理员权限",
"domain.deployment.form.k8s_namespace.label": "命名空间",
"domain.deployment.form.k8s_namespace.placeholder": "请输入 K8S 命名空间",
"domain.deployment.form.k8s_secret_name.label": "Secret 名称",