Compare commits
43 Commits
v0.2.20
...
v0.2.24-be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42f9fba82c | ||
|
|
d653ab442f | ||
|
|
a924a2df77 | ||
|
|
7590bb5cbc | ||
|
|
5042930aaa | ||
|
|
9d36df211e | ||
|
|
cec56b6e49 | ||
|
|
dfd2fccc1e | ||
|
|
4ffb7ae969 | ||
|
|
302ddcdd07 | ||
|
|
65df759275 | ||
|
|
a842b6b925 | ||
|
|
4916757d59 | ||
|
|
30b66adc3b | ||
|
|
13582d1a7b | ||
|
|
0b9312b549 | ||
|
|
bde51d8d38 | ||
|
|
643a666853 | ||
|
|
a59184ae5f | ||
|
|
82807fcc1b | ||
|
|
a6c93ef9b8 | ||
|
|
6a151865f7 | ||
|
|
414d8d140e | ||
|
|
51fb9dca58 | ||
|
|
6367785b1b | ||
|
|
aa7fb7da06 | ||
|
|
26d11de249 | ||
|
|
0daa9f1882 | ||
|
|
56886dcfe9 | ||
|
|
81e1e4a7ff | ||
|
|
9b5256716f | ||
|
|
446bf80f1d | ||
|
|
6a80455c6c | ||
|
|
43b2ff7957 | ||
|
|
295b7779ee | ||
|
|
d1df088662 | ||
|
|
2b0f7aaf8a | ||
|
|
3265dd76ab | ||
|
|
d1d7b44303 | ||
|
|
56eced3813 | ||
|
|
9a75d2ac8f | ||
|
|
41bd321a4f | ||
|
|
952e9687d0 |
1
go.mod
1
go.mod
@@ -46,6 +46,7 @@ require (
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.46.0 // indirect
|
||||
github.com/blinkbean/dingtalk v1.1.3 // indirect
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-lark/lark v1.14.1 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -225,6 +225,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/blinkbean/dingtalk v1.1.3 h1:MbidFZYom7DTFHD/YIs+eaI7kRy52kmWE/sy0xjo6E4=
|
||||
github.com/blinkbean/dingtalk v1.1.3/go.mod h1:9BaLuGSBqY3vT5hstValh48DbsKO7vaHaJnG9pXwbto=
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35 h1:bM18V4iw9ylRc2LahQaq3k3gjEVJdyQYvptLVZaCa54=
|
||||
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.35/go.mod h1:7iCaE+dR9EycrJU0GQyMhptbInLbQhsKXiDKDjNi8Vs=
|
||||
github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c/go.mod h1:l/bIBLeOl9eX+wxJAzxS4TveKRtAqlyDpHjhkfO0MEI=
|
||||
github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
|
||||
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
|
||||
@@ -751,6 +753,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
@@ -1265,6 +1268,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
|
||||
@@ -20,10 +20,10 @@ func NewVolcengine(option *ApplyOption) Applicant {
|
||||
}
|
||||
|
||||
func (a *volcengine) Apply() (*Certificate, error) {
|
||||
access := &domain.VolcengineAccess{}
|
||||
access := &domain.VolcEngineAccess{}
|
||||
json.Unmarshal([]byte(a.option.Access), access)
|
||||
|
||||
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyID)
|
||||
os.Setenv("VOLC_ACCESSKEY", access.AccessKeyId)
|
||||
os.Setenv("VOLC_SECRETKEY", access.SecretAccessKey)
|
||||
os.Setenv("VOLC_PROPAGATION_TIMEOUT", fmt.Sprintf("%d", a.option.Timeout))
|
||||
dnsProvider, err := volcengineDns.NewDNSProvider()
|
||||
|
||||
116
internal/deployer/byteplus_cdn.go
Normal file
116
internal/deployer/byteplus_cdn.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
bytepluscdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
|
||||
|
||||
"github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
)
|
||||
|
||||
type ByteplusCDNDeployer struct {
|
||||
option *DeployerOption
|
||||
infos []string
|
||||
sdkClient *cdn.CDN
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
func NewByteplusCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.ByteplusAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
client := cdn.NewInstance()
|
||||
client.Client.SetAccessKey(access.AccessKey)
|
||||
client.Client.SetSecretKey(access.SecretKey)
|
||||
uploader, err := bytepluscdn.New(&bytepluscdn.ByteplusCDNUploaderConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
return &ByteplusCDNDeployer{
|
||||
option: option,
|
||||
infos: make([]string, 0),
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *ByteplusCDNDeployer) GetID() string {
|
||||
return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id)
|
||||
}
|
||||
|
||||
func (d *ByteplusCDNDeployer) GetInfos() []string {
|
||||
return d.infos
|
||||
}
|
||||
|
||||
func (d *ByteplusCDNDeployer) Deploy(ctx context.Context) error {
|
||||
apiCtx := context.Background()
|
||||
// 上传证书
|
||||
upres, err := d.sslUploader.Upload(apiCtx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.infos = append(d.infos, toStr("已上传证书", upres))
|
||||
|
||||
domains := make([]string, 0)
|
||||
configDomain := d.option.DeployConfig.GetConfigAsString("domain")
|
||||
if strings.HasPrefix(configDomain, "*.") {
|
||||
// 获取证书可以部署的域名
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
|
||||
describeCertConfigReq := &cdn.DescribeCertConfigRequest{
|
||||
CertId: upres.CertId,
|
||||
}
|
||||
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.CertNotConfig {
|
||||
// 当前未启用 HTTPS 的加速域名列表。
|
||||
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.OtherCertConfig {
|
||||
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联的证书不是您指定的证书。
|
||||
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
|
||||
}
|
||||
for i := range describeCertConfigResp.Result.SpecifiedCertConfig {
|
||||
// 已启用了 HTTPS 的加速域名列表。这些加速域名关联了您指定的证书。
|
||||
d.infos = append(d.infos, fmt.Sprintf("%s域名已配置该证书", describeCertConfigResp.Result.SpecifiedCertConfig[i].Domain))
|
||||
}
|
||||
if len(domains) == 0 {
|
||||
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
|
||||
// 所有匹配的域名都配置了该证书,跳过部署
|
||||
return nil
|
||||
} else {
|
||||
return xerrors.Errorf("未查询到匹配的域名: %s", configDomain)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, configDomain)
|
||||
}
|
||||
// 部署证书
|
||||
// REF: https://github.com/byteplus-sdk/byteplus-sdk-golang/blob/master/service/cdn/api_list.go#L306
|
||||
for i := range domains {
|
||||
batchDeployCertReq := &cdn.BatchDeployCertRequest{
|
||||
CertId: upres.CertId,
|
||||
Domain: domains[i],
|
||||
}
|
||||
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'cdn.BatchDeployCert'")
|
||||
} else {
|
||||
d.infos = append(d.infos, toStr(fmt.Sprintf("%s域名的证书已修改", domains[i]), batchDeployCertResp))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,21 +1,15 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pavlo-v-chernykh/keystore-go/v4"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
)
|
||||
|
||||
@@ -34,14 +28,15 @@ const (
|
||||
targetHuaweiCloudCDN = "huaweicloud-cdn"
|
||||
targetHuaweiCloudELB = "huaweicloud-elb"
|
||||
targetBaiduCloudCDN = "baiducloud-cdn"
|
||||
targetVolcEngineLive = "volcengine-live"
|
||||
targetVolcEngineCDN = "volcengine-cdn"
|
||||
targetBytePlusCDN = "byteplus-cdn"
|
||||
targetQiniuCdn = "qiniu-cdn"
|
||||
targetDogeCloudCdn = "dogecloud-cdn"
|
||||
targetLocal = "local"
|
||||
targetSSH = "ssh"
|
||||
targetWebhook = "webhook"
|
||||
targetK8sSecret = "k8s-secret"
|
||||
targetVolcengineLive = "volcengine-live"
|
||||
targetVolcengineCDN = "volcengine-cdn"
|
||||
)
|
||||
|
||||
type DeployerOption struct {
|
||||
@@ -152,10 +147,12 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep
|
||||
return NewWebhookDeployer(option)
|
||||
case targetK8sSecret:
|
||||
return NewK8sSecretDeployer(option)
|
||||
case targetVolcengineLive:
|
||||
case targetVolcEngineLive:
|
||||
return NewVolcengineLiveDeployer(option)
|
||||
case targetVolcengineCDN:
|
||||
case targetVolcEngineCDN:
|
||||
return NewVolcengineCDNDeployer(option)
|
||||
case targetBytePlusCDN:
|
||||
return NewByteplusCDNDeployer(option)
|
||||
}
|
||||
return nil, errors.New("unsupported deploy target")
|
||||
}
|
||||
@@ -167,57 +164,3 @@ func toStr(tag string, data any) string {
|
||||
byts, _ := json.Marshal(data)
|
||||
return tag + ":" + string(byts)
|
||||
}
|
||||
|
||||
func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) {
|
||||
cert, err := x509.ParseCertificateFromPEM(certificate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privkey, err := x509.ParsePKCS1PrivateKeyFromPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pfxData, nil
|
||||
}
|
||||
|
||||
func convertPEMToJKS(certificate string, privateKey string, alias string, keypass string, storepass string) ([]byte, error) {
|
||||
certBlock, _ := pem.Decode([]byte(certificate))
|
||||
if certBlock == nil {
|
||||
return nil, errors.New("failed to decode certificate PEM")
|
||||
}
|
||||
|
||||
privkeyBlock, _ := pem.Decode([]byte(privateKey))
|
||||
if privkeyBlock == nil {
|
||||
return nil, errors.New("failed to decode private key PEM")
|
||||
}
|
||||
|
||||
ks := keystore.New()
|
||||
entry := keystore.PrivateKeyEntry{
|
||||
CreationTime: time.Now(),
|
||||
PrivateKey: privkeyBlock.Bytes,
|
||||
CertificateChain: []keystore.Certificate{
|
||||
{
|
||||
Type: "X509",
|
||||
Content: certBlock.Bytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ks.SetPrivateKeyEntry(alias, entry, []byte(keypass)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := ks.Store(&buf, []byte(storepass)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
374
internal/deployer/factory.go
Normal file
374
internal/deployer/factory.go
Normal file
@@ -0,0 +1,374 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
providerAliyunAlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
|
||||
providerAliyunCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
|
||||
providerAliyunClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
|
||||
providerAliyunDcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
|
||||
providerAliyunNlb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
|
||||
providerAliyunOss "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
|
||||
providerBaiduCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
|
||||
providerBytePlusCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
|
||||
providerDogeCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
|
||||
providerHuaweiCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
|
||||
providerHuaweiCloudElb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
|
||||
providerK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
|
||||
providerLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
|
||||
providerQiniuCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
|
||||
providerSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
|
||||
providerTencentCloudCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
|
||||
providerTencentCloudClb "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
|
||||
providerTencentCloudCos "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
|
||||
providerTencentCloudEcdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
|
||||
providerTencentCloudTeo "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo"
|
||||
providerVolcEngineCdn "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
|
||||
providerVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
|
||||
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
// TODO: 该方法目前未实际使用,将在后续迭代中替换
|
||||
func createDeployer(target string, accessConfig string, deployConfig map[string]any) (deployer.Deployer, deployer.Logger, error) {
|
||||
logger := deployer.NewDefaultLogger()
|
||||
|
||||
switch target {
|
||||
case targetAliyunALB, targetAliyunCDN, targetAliyunCLB, targetAliyunDCDN, targetAliyunNLB, targetAliyunOSS:
|
||||
{
|
||||
access := &domain.AliyunAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetAliyunALB:
|
||||
deployer, err := providerAliyunAlb.NewWithLogger(&providerAliyunAlb.AliyunALBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerAliyunAlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunCDN:
|
||||
deployer, err := providerAliyunCdn.NewWithLogger(&providerAliyunCdn.AliyunCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunCLB:
|
||||
deployer, err := providerAliyunClb.NewWithLogger(&providerAliyunClb.AliyunCLBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerAliyunClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerPort: maps.GetValueAsInt32(deployConfig, "listenerPort"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunDCDN:
|
||||
deployer, err := providerAliyunDcdn.NewWithLogger(&providerAliyunDcdn.AliyunDCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunNLB:
|
||||
deployer, err := providerAliyunNlb.NewWithLogger(&providerAliyunNlb.AliyunNLBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerAliyunNlb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetAliyunOSS:
|
||||
deployer, err := providerAliyunOss.NewWithLogger(&providerAliyunOss.AliyunOSSDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.AccessKeySecret,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(deployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetBaiduCloudCDN:
|
||||
{
|
||||
access := &domain.BaiduCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerBaiduCloudCdn.NewWithLogger(&providerBaiduCloudCdn.BaiduCloudCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetBytePlusCDN:
|
||||
{
|
||||
access := &domain.ByteplusAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerBytePlusCdn.NewWithLogger(&providerBytePlusCdn.BytePlusCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetDogeCloudCdn:
|
||||
{
|
||||
access := &domain.DogeCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerDogeCdn.NewWithLogger(&providerDogeCdn.DogeCloudCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetHuaweiCloudCDN, targetHuaweiCloudELB:
|
||||
{
|
||||
access := &domain.HuaweiCloudAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetHuaweiCloudCDN:
|
||||
deployer, err := providerHuaweiCloudCdn.NewWithLogger(&providerHuaweiCloudCdn.HuaweiCloudCDNDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetHuaweiCloudELB:
|
||||
deployer, err := providerHuaweiCloudElb.NewWithLogger(&providerHuaweiCloudElb.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerHuaweiCloudElb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
CertificateId: maps.GetValueAsString(deployConfig, "certificateId"),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetLocal:
|
||||
{
|
||||
deployer, err := providerLocal.NewWithLogger(&providerLocal.LocalDeployerConfig{
|
||||
ShellEnv: providerLocal.ShellEnvType(maps.GetValueAsString(deployConfig, "shellEnv")),
|
||||
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
||||
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
||||
OutputFormat: providerLocal.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
|
||||
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
||||
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
||||
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
||||
JksAlias: maps.GetValueAsString(deployConfig, "jksAlias"),
|
||||
JksKeypass: maps.GetValueAsString(deployConfig, "jksKeypass"),
|
||||
JksStorepass: maps.GetValueAsString(deployConfig, "jksStorepass"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetK8sSecret:
|
||||
{
|
||||
access := &domain.KubernetesAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerK8sSecret.NewWithLogger(&providerK8sSecret.K8sSecretDeployerConfig{
|
||||
KubeConfig: access.KubeConfig,
|
||||
Namespace: maps.GetValueOrDefaultAsString(deployConfig, "namespace", "default"),
|
||||
SecretName: maps.GetValueAsString(deployConfig, "secretName"),
|
||||
SecretDataKeyForCrt: maps.GetValueOrDefaultAsString(deployConfig, "secretDataKeyForCrt", "tls.crt"),
|
||||
SecretDataKeyForKey: maps.GetValueOrDefaultAsString(deployConfig, "secretDataKeyForKey", "tls.key"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetQiniuCdn:
|
||||
{
|
||||
access := &domain.QiniuAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerQiniuCdn.NewWithLogger(&providerQiniuCdn.QiniuCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetSSH:
|
||||
{
|
||||
access := &domain.SSHAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
sshPort, _ := strconv.ParseInt(access.Port, 10, 32)
|
||||
deployer, err := providerSSH.NewWithLogger(&providerSSH.SshDeployerConfig{
|
||||
SshHost: access.Host,
|
||||
SshPort: int32(sshPort),
|
||||
SshUsername: access.Username,
|
||||
SshPassword: access.Password,
|
||||
SshKey: access.Key,
|
||||
SshKeyPassphrase: access.KeyPassphrase,
|
||||
PreCommand: maps.GetValueAsString(deployConfig, "preCommand"),
|
||||
PostCommand: maps.GetValueAsString(deployConfig, "postCommand"),
|
||||
OutputFormat: providerSSH.OutputFormatType(maps.GetValueOrDefaultAsString(deployConfig, "format", "PEM")),
|
||||
OutputCertPath: maps.GetValueAsString(deployConfig, "certPath"),
|
||||
OutputKeyPath: maps.GetValueAsString(deployConfig, "keyPath"),
|
||||
PfxPassword: maps.GetValueAsString(deployConfig, "pfxPassword"),
|
||||
JksAlias: maps.GetValueAsString(deployConfig, "jksAlias"),
|
||||
JksKeypass: maps.GetValueAsString(deployConfig, "jksKeypass"),
|
||||
JksStorepass: maps.GetValueAsString(deployConfig, "jksStorepass"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
|
||||
case targetTencentCDN, targetTencentCLB, targetTencentCOS, targetTencentECDN, targetTencentTEO:
|
||||
{
|
||||
access := &domain.TencentAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetTencentCDN:
|
||||
deployer, err := providerTencentCloudCdn.NewWithLogger(&providerTencentCloudCdn.TencentCloudCDNDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentCLB:
|
||||
deployer, err := providerTencentCloudClb.NewWithLogger(&providerTencentCloudClb.TencentCloudCLBDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
ResourceType: providerTencentCloudClb.DeployResourceType(maps.GetValueAsString(deployConfig, "resourceType")),
|
||||
LoadbalancerId: maps.GetValueAsString(deployConfig, "loadbalancerId"),
|
||||
ListenerId: maps.GetValueAsString(deployConfig, "listenerId"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentCOS:
|
||||
deployer, err := providerTencentCloudCos.NewWithLogger(&providerTencentCloudCos.TencentCloudCOSDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Region: maps.GetValueAsString(deployConfig, "region"),
|
||||
Bucket: maps.GetValueAsString(deployConfig, "bucket"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentECDN:
|
||||
deployer, err := providerTencentCloudEcdn.NewWithLogger(&providerTencentCloudEcdn.TencentCloudECDNDeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetTencentTEO:
|
||||
deployer, err := providerTencentCloudTeo.NewWithLogger(&providerTencentCloudTeo.TencentCloudTEODeployerConfig{
|
||||
SecretId: access.SecretId,
|
||||
SecretKey: access.SecretKey,
|
||||
ZoneId: maps.GetValueAsString(deployConfig, "zoneId"),
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetVolcEngineCDN, targetVolcEngineLive:
|
||||
{
|
||||
access := &domain.VolcEngineAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
switch target {
|
||||
case targetVolcEngineCDN:
|
||||
deployer, err := providerVolcEngineCdn.NewWithLogger(&providerVolcEngineCdn.VolcEngineCDNDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
case targetVolcEngineLive:
|
||||
deployer, err := providerVolcEngineLive.NewWithLogger(&providerVolcEngineLive.VolcEngineLiveDeployerConfig{
|
||||
AccessKey: access.AccessKey,
|
||||
SecretKey: access.SecretKey,
|
||||
Domain: maps.GetValueAsString(deployConfig, "domain"),
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
case targetWebhook:
|
||||
{
|
||||
access := &domain.WebhookAccess{}
|
||||
if err := json.Unmarshal([]byte(accessConfig), access); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to unmarshal access config: %w", err)
|
||||
}
|
||||
|
||||
deployer, err := providerWebhook.NewWithLogger(&providerWebhook.WebhookDeployerConfig{
|
||||
Url: access.Url,
|
||||
Variables: nil, // TODO: 尚未实现
|
||||
}, logger)
|
||||
return deployer, logger, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("unsupported deployer target: %s", target)
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
|
||||
@@ -17,6 +15,7 @@ import (
|
||||
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"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
@@ -163,9 +162,6 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r
|
||||
}
|
||||
|
||||
client := hcIam.NewIamClient(hcClient)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
request := &hcIamModel.KeystoneListProjectsRequest{
|
||||
Name: ®ion,
|
||||
@@ -352,11 +348,7 @@ func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context,
|
||||
newCertificate := showNewCertificateResp.Certificate
|
||||
|
||||
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
|
||||
oldCertificateSans := oldCertificate.SubjectAlternativeNames
|
||||
newCertificateSans := newCertificate.SubjectAlternativeNames
|
||||
sort.Strings(*oldCertificateSans)
|
||||
sort.Strings(*newCertificateSans)
|
||||
if strings.Join(*oldCertificateSans, ";") == strings.Join(*newCertificateSans, ";") {
|
||||
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type LocalDeployer struct {
|
||||
@@ -73,7 +74,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
|
||||
d.infos = append(d.infos, toStr("保存私钥成功", nil))
|
||||
|
||||
case certFormatPFX:
|
||||
pfxData, err := convertPEMToPFX(
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
|
||||
@@ -89,7 +90,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error {
|
||||
d.infos = append(d.infos, toStr("保存证书成功", nil))
|
||||
|
||||
case certFormatJKS:
|
||||
jksData, err := convertPEMToJKS(
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("jksAlias"),
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type SSHDeployer struct {
|
||||
@@ -78,7 +79,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
|
||||
d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil))
|
||||
|
||||
case certFormatPFX:
|
||||
pfxData, err := convertPEMToPFX(
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("pfxPassword"),
|
||||
@@ -94,7 +95,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error {
|
||||
d.infos = append(d.infos, toStr("SSH 上传证书成功", nil))
|
||||
|
||||
case certFormatJKS:
|
||||
jksData, err := convertPEMToJKS(
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(
|
||||
d.option.Certificate.Certificate,
|
||||
d.option.Certificate.PrivateKey,
|
||||
d.option.DeployConfig.GetConfigAsString("jksAlias"),
|
||||
|
||||
@@ -101,9 +101,9 @@ func (d *TencentCDNDeployer) Deploy(ctx context.Context) error {
|
||||
}
|
||||
|
||||
temp := make([]string, 0)
|
||||
for _, aliInstanceId := range tcInstanceIds {
|
||||
if !slices.Contains(deployedDomains, aliInstanceId) {
|
||||
temp = append(temp, aliInstanceId)
|
||||
for _, tcInstanceId := range tcInstanceIds {
|
||||
if !slices.Contains(deployedDomains, tcInstanceId) {
|
||||
temp = append(temp, tcInstanceId)
|
||||
}
|
||||
}
|
||||
tcInstanceIds = temp
|
||||
@@ -161,7 +161,7 @@ func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]strin
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeCertDomainsResp.Response.Domains == nil {
|
||||
if describeCertDomainsResp.Response.Domains != nil {
|
||||
for _, domain := range describeCertDomainsResp.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]stri
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeCertDomainsResp.Response.Domains == nil {
|
||||
if describeCertDomainsResp.Response.Domains != nil {
|
||||
for _, domain := range describeCertDomainsResp.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
|
||||
@@ -74,6 +74,12 @@ func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
|
||||
return xerrors.New("`zoneId` is required")
|
||||
}
|
||||
|
||||
tcDomain := strings.ReplaceAll(strings.TrimSpace(d.option.DeployConfig.GetConfigAsString("domain")), "\r", "")
|
||||
tcDomains := strings.Split(tcDomain, "\n")
|
||||
if len(tcDomains) == 0 {
|
||||
return xerrors.New("`domain` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
@@ -87,7 +93,7 @@ func (d *TencentTEODeployer) Deploy(ctx context.Context) error {
|
||||
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.Hosts = common.StringPtrs(tcDomains)
|
||||
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
|
||||
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,15 +22,15 @@ type VolcengineCDNDeployer struct {
|
||||
}
|
||||
|
||||
func NewVolcengineCDNDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.VolcengineAccess{}
|
||||
access := &domain.VolcEngineAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
client := cdn.NewInstance()
|
||||
client.Client.SetAccessKey(access.AccessKeyID)
|
||||
client.Client.SetAccessKey(access.AccessKeyId)
|
||||
client.Client.SetSecretKey(access.SecretAccessKey)
|
||||
uploader, err := volcenginecdn.New(&volcenginecdn.VolcengineCDNUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyID,
|
||||
uploader, err := volcenginecdn.New(&volcenginecdn.VolcEngineCDNUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -24,17 +24,17 @@ type VolcengineLiveDeployer struct {
|
||||
}
|
||||
|
||||
func NewVolcengineLiveDeployer(option *DeployerOption) (Deployer, error) {
|
||||
access := &domain.VolcengineAccess{}
|
||||
access := &domain.VolcEngineAccess{}
|
||||
if err := json.Unmarshal([]byte(option.Access), access); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to get access")
|
||||
}
|
||||
client := live.NewInstance()
|
||||
client.SetCredential(base.Credentials{
|
||||
AccessKeyID: access.AccessKeyID,
|
||||
AccessKeyID: access.AccessKeyId,
|
||||
SecretAccessKey: access.SecretAccessKey,
|
||||
})
|
||||
uploader, err := volcenginelive.New(&volcenginelive.VolcengineLiveUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyID,
|
||||
uploader, err := volcenginelive.New(&volcenginelive.VolcEngineLiveUploaderConfig{
|
||||
AccessKeyId: access.AccessKeyId,
|
||||
AccessKeySecret: access.SecretAccessKey,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,11 @@ type AliyunAccess struct {
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type ByteplusAccess struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type TencentAccess struct {
|
||||
SecretId string `json:"secretId"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
@@ -22,9 +27,9 @@ type BaiduCloudAccess struct {
|
||||
}
|
||||
|
||||
type AwsAccess struct {
|
||||
Region string `json:"region"`
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
Region string `json:"region"`
|
||||
HostedZoneId string `json:"hostedZoneId"`
|
||||
}
|
||||
|
||||
@@ -56,9 +61,14 @@ type PdnsAccess struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
}
|
||||
|
||||
type VolcengineAccess struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
type VolcEngineAccess struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
|
||||
// Deprecated: Use [AccessKey] and [SecretKey] instead in the future
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// Deprecated: Use [AccessKey] and [SecretKey] instead in the future
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
}
|
||||
|
||||
type HttpreqAccess struct {
|
||||
|
||||
@@ -41,7 +41,7 @@ func (dc *DeployConfig) GetConfigAsString(key string) string {
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回默认值。
|
||||
// - 配置项的值。如果配置项不存在、类型不是字符串或者值为零值,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue string) string {
|
||||
return maps.GetValueOrDefaultAsString(dc.Config, key, defaultValue)
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func (dc *DeployConfig) GetConfigAsInt32(key string) int32 {
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。
|
||||
// - 配置项的值。如果配置项不存在、类型不是 32 位整数或者值为零值,则返回默认值。
|
||||
func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 {
|
||||
return maps.GetValueOrDefaultAsInt32(dc.Config, key, defaultValue)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,22 @@ package domains
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/applicant"
|
||||
"github.com/usual2970/certimate/internal/deployer"
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/utils/app"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type Phase string
|
||||
@@ -20,6 +28,8 @@ const (
|
||||
deployPhase Phase = "deploy"
|
||||
)
|
||||
|
||||
const validityDuration = time.Hour * 24 * 10
|
||||
|
||||
func deploy(ctx context.Context, record *models.Record) error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -45,7 +55,10 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
cert := currRecord.GetString("certificate")
|
||||
expiredAt := currRecord.GetDateTime("expiredAt").Time()
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24*10 && currRecord.GetBool("deployed") {
|
||||
// 检查证书是否包含设置的所有域名
|
||||
changed := isCertChanged(cert, currRecord)
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > validityDuration && currRecord.GetBool("deployed") && !changed {
|
||||
app.GetApp().Logger().Info("证书在有效期内")
|
||||
history.record(checkPhase, "证书在有效期内且已部署,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
@@ -60,7 +73,7 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
// ############2.申请证书
|
||||
history.record(applyPhase, "开始申请", nil)
|
||||
|
||||
if cert != "" && time.Until(expiredAt) > time.Hour*24 {
|
||||
if cert != "" && time.Until(expiredAt) > validityDuration && !changed {
|
||||
history.record(applyPhase, "证书在有效期内,跳过", &RecordInfo{
|
||||
Info: []string{fmt.Sprintf("证书有效期至 %s", expiredAt.Format("2006-01-02"))},
|
||||
})
|
||||
@@ -121,3 +134,80 @@ func deploy(ctx context.Context, record *models.Record) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCertChanged(certificate string, record *models.Record) bool {
|
||||
// 如果证书为空,直接返回true
|
||||
if certificate == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 解析证书
|
||||
cert, err := x509.ParseCertificateFromPEM(certificate)
|
||||
if err != nil {
|
||||
app.GetApp().Logger().Error("解析证书失败", "err", err)
|
||||
return true
|
||||
}
|
||||
|
||||
// 遍历域名列表,检查是否都在证书中,找到第一个不存在证书中域名时提前返回true
|
||||
for _, domain := range strings.Split(record.GetString("domain"), ";") {
|
||||
if !slices.Contains(cert.DNSNames, domain) && !slices.Contains(cert.DNSNames, "*."+removeLastSubdomain(domain)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 解析applyConfig
|
||||
applyConfig := &domain.ApplyConfig{}
|
||||
record.UnmarshalJSONField("applyConfig", applyConfig)
|
||||
|
||||
// 检查证书加密算法是否变更
|
||||
switch pubkey := cert.PublicKey.(type) {
|
||||
case *rsa.PublicKey:
|
||||
bitSize := pubkey.N.BitLen()
|
||||
switch bitSize {
|
||||
case 2048:
|
||||
// RSA2048
|
||||
if applyConfig.KeyAlgorithm != "" && applyConfig.KeyAlgorithm != "RSA2048" {
|
||||
return true
|
||||
}
|
||||
case 3072:
|
||||
// RSA3072
|
||||
if applyConfig.KeyAlgorithm != "RSA3072" {
|
||||
return true
|
||||
}
|
||||
case 4096:
|
||||
// RSA4096
|
||||
if applyConfig.KeyAlgorithm != "RSA4096" {
|
||||
return true
|
||||
}
|
||||
case 8192:
|
||||
// RSA8192
|
||||
if applyConfig.KeyAlgorithm != "RSA8192" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case *ecdsa.PublicKey:
|
||||
bitSize := pubkey.Curve.Params().BitSize
|
||||
switch bitSize {
|
||||
case 256:
|
||||
// EC256
|
||||
if applyConfig.KeyAlgorithm != "EC256" {
|
||||
return true
|
||||
}
|
||||
case 384:
|
||||
// EC384
|
||||
if applyConfig.KeyAlgorithm != "EC384" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func removeLastSubdomain(domain string) string {
|
||||
parts := strings.Split(domain, ".")
|
||||
if len(parts) > 1 {
|
||||
return strings.Join(parts[1:], ".")
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
package notify
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/usual2970/certimate/internal/domain"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/notifier"
|
||||
notifierBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||
notifierDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
notifierLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||
notifierServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||
notifierTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||
notifierWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||
providerBark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||
providerDingTalk "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||
providerEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
providerLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||
providerServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||
providerTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||
providerWebhook "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/maps"
|
||||
)
|
||||
|
||||
func createNotifier(channel string, channelConfig map[string]any) (notifier.Notifier, error) {
|
||||
switch channel {
|
||||
case domain.NotifyChannelEmail:
|
||||
return notifierEmail.New(¬ifierEmail.EmailNotifierConfig{
|
||||
return providerEmail.New(&providerEmail.EmailNotifierConfig{
|
||||
SmtpHost: maps.GetValueAsString(channelConfig, "smtpHost"),
|
||||
SmtpPort: maps.GetValueAsInt32(channelConfig, "smtpPort"),
|
||||
SmtpTLS: maps.GetValueOrDefaultAsBool(channelConfig, "smtpTLS", true),
|
||||
@@ -29,38 +29,38 @@ func createNotifier(channel string, channelConfig map[string]any) (notifier.Noti
|
||||
})
|
||||
|
||||
case domain.NotifyChannelWebhook:
|
||||
return notifierWebhook.New(¬ifierWebhook.WebhookNotifierConfig{
|
||||
return providerWebhook.New(&providerWebhook.WebhookNotifierConfig{
|
||||
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelDingtalk:
|
||||
return notifierDingTalk.New(¬ifierDingTalk.DingTalkNotifierConfig{
|
||||
return providerDingTalk.New(&providerDingTalk.DingTalkNotifierConfig{
|
||||
AccessToken: maps.GetValueAsString(channelConfig, "accessToken"),
|
||||
Secret: maps.GetValueAsString(channelConfig, "secret"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelLark:
|
||||
return notifierLark.New(¬ifierLark.LarkNotifierConfig{
|
||||
return providerLark.New(&providerLark.LarkNotifierConfig{
|
||||
WebhookUrl: maps.GetValueAsString(channelConfig, "webhookUrl"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelTelegram:
|
||||
return notifierTelegram.New(¬ifierTelegram.TelegramNotifierConfig{
|
||||
return providerTelegram.New(&providerTelegram.TelegramNotifierConfig{
|
||||
ApiToken: maps.GetValueAsString(channelConfig, "apiToken"),
|
||||
ChatId: maps.GetValueAsInt64(channelConfig, "chatId"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelServerChan:
|
||||
return notifierServerChan.New(¬ifierServerChan.ServerChanNotifierConfig{
|
||||
return providerServerChan.New(&providerServerChan.ServerChanNotifierConfig{
|
||||
Url: maps.GetValueAsString(channelConfig, "url"),
|
||||
})
|
||||
|
||||
case domain.NotifyChannelBark:
|
||||
return notifierBark.New(¬ifierBark.BarkNotifierConfig{
|
||||
return providerBark.New(&providerBark.BarkNotifierConfig{
|
||||
DeviceKey: maps.GetValueAsString(channelConfig, "deviceKey"),
|
||||
ServerUrl: maps.GetValueAsString(channelConfig, "serverUrl"),
|
||||
})
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported notifier channel")
|
||||
return nil, fmt.Errorf("unsupported notifier channel: %s", channelConfig)
|
||||
}
|
||||
|
||||
24
internal/pkg/core/deployer/deployer.go
Normal file
24
internal/pkg/core/deployer/deployer.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package deployer
|
||||
|
||||
import "context"
|
||||
|
||||
// 表示定义证书部署器的抽象类型接口。
|
||||
// 注意与 `Uploader` 区分,“部署”通常为“上传”的后置操作。
|
||||
type Deployer interface {
|
||||
// 部署证书。
|
||||
//
|
||||
// 入参:
|
||||
// - ctx:上下文。
|
||||
// - certPem:证书 PEM 内容。
|
||||
// - privkeyPem:私钥 PEM 内容。
|
||||
//
|
||||
// 出参:
|
||||
// - res:部署结果。
|
||||
// - err: 错误。
|
||||
Deploy(ctx context.Context, certPem string, privkeyPem string) (res *DeployResult, err error)
|
||||
}
|
||||
|
||||
// 表示证书部署结果的数据结构。
|
||||
type DeployResult struct {
|
||||
DeploymentData map[string]any `json:"deploymentData,omitempty"`
|
||||
}
|
||||
117
internal/pkg/core/deployer/logger.go
Normal file
117
internal/pkg/core/deployer/logger.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package deployer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 表示定义证书部署器的日志记录器的抽象类型接口。
|
||||
type Logger interface {
|
||||
// 追加一条日志记录。
|
||||
// 该方法会将 `data` 以 JSON 序列化后拼接到 `tag` 结尾。
|
||||
//
|
||||
// 入参:
|
||||
// - tag:标签。
|
||||
// - data:数据。
|
||||
Logt(tag string, data ...any)
|
||||
|
||||
// 追加一条日志记录。
|
||||
// 该方法会将 `args` 以 `format` 格式化。
|
||||
//
|
||||
// 入参:
|
||||
// - format:格式化字符串。
|
||||
// - args:格式化参数。
|
||||
Logf(format string, args ...any)
|
||||
|
||||
// 获取所有日志记录。
|
||||
GetRecords() []string
|
||||
|
||||
// 清空所有日志记录。
|
||||
FlushRecords()
|
||||
}
|
||||
|
||||
// 表示默认的日志记录器类型。
|
||||
type DefaultLogger struct {
|
||||
records []string
|
||||
}
|
||||
|
||||
var _ Logger = (*DefaultLogger)(nil)
|
||||
|
||||
func (l *DefaultLogger) Logt(tag string, data ...any) {
|
||||
l.ensureInitialized()
|
||||
|
||||
temp := make([]string, len(data)+1)
|
||||
temp[0] = tag
|
||||
for i, v := range data {
|
||||
s := ""
|
||||
if v == nil {
|
||||
s = "<nil>"
|
||||
} else {
|
||||
switch reflect.ValueOf(v).Kind() {
|
||||
case reflect.String:
|
||||
s = v.(string)
|
||||
case reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64:
|
||||
s = fmt.Sprintf("%v", v)
|
||||
default:
|
||||
jsonData, _ := json.Marshal(v)
|
||||
s = string(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
temp[i+1] = s
|
||||
}
|
||||
|
||||
l.records = append(l.records, strings.Join(temp, ": "))
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) Logf(format string, args ...any) {
|
||||
l.ensureInitialized()
|
||||
|
||||
l.records = append(l.records, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) GetRecords() []string {
|
||||
l.ensureInitialized()
|
||||
|
||||
temp := make([]string, len(l.records))
|
||||
copy(temp, l.records)
|
||||
return temp
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) FlushRecords() {
|
||||
l.records = make([]string, 0)
|
||||
}
|
||||
|
||||
func (l *DefaultLogger) ensureInitialized() {
|
||||
if l.records == nil {
|
||||
l.records = make([]string, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func NewDefaultLogger() *DefaultLogger {
|
||||
return &DefaultLogger{
|
||||
records: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// 表示空的日志记录器类型。
|
||||
// 该日志记录器不会执行任何操作。
|
||||
type NilLogger struct{}
|
||||
|
||||
var _ Logger = (*NilLogger)(nil)
|
||||
|
||||
func (l *NilLogger) Logt(string, ...any) {}
|
||||
func (l *NilLogger) Logf(string, ...any) {}
|
||||
func (l *NilLogger) GetRecords() []string {
|
||||
return make([]string, 0)
|
||||
}
|
||||
func (l *NilLogger) FlushRecords() {}
|
||||
|
||||
func NewNilLogger() *NilLogger {
|
||||
return &NilLogger{}
|
||||
}
|
||||
56
internal/pkg/core/deployer/logger_test.go
Normal file
56
internal/pkg/core/deployer/logger_test.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package deployer_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v logger_test.go
|
||||
*/
|
||||
func TestLogger(t *testing.T) {
|
||||
t.Run("Logger_Appendt", func(t *testing.T) {
|
||||
logger := deployer.NewDefaultLogger()
|
||||
|
||||
logger.Logt("test")
|
||||
logger.Logt("test_nil", nil)
|
||||
logger.Logt("test_int", 1024)
|
||||
logger.Logt("test_string", "certimate")
|
||||
logger.Logt("test_map", map[string]interface{}{"key": "value"})
|
||||
logger.Logt("test_struct", struct{ Name string }{Name: "certimate"})
|
||||
logger.Logt("test_slice", []string{"certimate"})
|
||||
t.Log(logger.GetRecords())
|
||||
if len(logger.GetRecords()) != 7 {
|
||||
t.Errorf("expected 7 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
|
||||
logger.FlushRecords()
|
||||
if len(logger.GetRecords()) != 0 {
|
||||
t.Errorf("expected 0 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Logger_Appendf", func(t *testing.T) {
|
||||
logger := deployer.NewDefaultLogger()
|
||||
|
||||
logger.Logf("test")
|
||||
logger.Logf("test_nil: %v", nil)
|
||||
logger.Logf("test_int: %v", 1024)
|
||||
logger.Logf("test_string: %v", "certimate")
|
||||
logger.Logf("test_map: %v", map[string]interface{}{"key": "value"})
|
||||
logger.Logf("test_struct: %v", struct{ Name string }{Name: "certimate"})
|
||||
logger.Logf("test_slice: %v", []string{"certimate"})
|
||||
t.Log(logger.GetRecords())
|
||||
if len(logger.GetRecords()) != 7 {
|
||||
t.Errorf("expected 7 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
|
||||
logger.FlushRecords()
|
||||
if len(logger.GetRecords()) != 0 {
|
||||
t.Errorf("expected 0 records, got %d", len(logger.GetRecords()))
|
||||
}
|
||||
})
|
||||
}
|
||||
289
internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go
Normal file
289
internal/pkg/core/deployer/providers/aliyun-alb/aliyun_alb.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package aliyunalb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
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/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type AliyunALBDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
}
|
||||
|
||||
type AliyunALBDeployer struct {
|
||||
config *AliyunALBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunAlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunALBDeployer)(nil)
|
||||
|
||||
func New(config *AliyunALBDeployerConfig) (*AliyunALBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunALBDeployerConfig, logger deployer.Logger) (*AliyunALBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
aliyunCasRegion := config.Region
|
||||
if aliyunCasRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 ALB 服务的
|
||||
// 国内版固定接入点:华东一杭州
|
||||
// 国际版固定接入点:亚太东南一新加坡
|
||||
if !strings.HasPrefix(aliyunCasRegion, "cn-") {
|
||||
aliyunCasRegion = "ap-southeast-1"
|
||||
} else {
|
||||
aliyunCasRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
uploader, err := providerCas.New(&providerCas.AliyunCASUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
AccessKeySecret: config.AccessKeySecret,
|
||||
Region: aliyunCasRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunALBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CAS
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("HTTPS"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 HTTPS 监听", listenerIds)
|
||||
|
||||
// 查询 QUIC 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-listlisteners
|
||||
listListenersPage = 1
|
||||
listListenersToken = nil
|
||||
for {
|
||||
listListenersReq := &aliyunAlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("QUIC"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", listenerIds)
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ALB 监听配置", getListenerAttributeResp)
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{
|
||||
CertificateId: tea.String(cloudCertId),
|
||||
}},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ALB 监听配置", updateListenerAttributeResp)
|
||||
|
||||
// TODO: #347
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou"
|
||||
}
|
||||
|
||||
// 接入点一览 https://www.alibabacloud.com/help/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou-finance":
|
||||
endpoint = "alb.cn-hangzhou.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("alb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
client, err := aliyunAlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package aliyunalb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-alb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNALB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_alb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_LOADBALANCERID="your-alb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNALB_LISTENERID="your-alb-listener-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunALBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunALBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
10
internal/pkg/core/deployer/providers/aliyun-alb/defines.go
Normal file
10
internal/pkg/core/deployer/providers/aliyun-alb/defines.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package aliyunalb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
package aliyuncdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type AliyunCDNDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type AliyunCDNDeployer struct {
|
||||
config *AliyunCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunCdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunCDNDeployer)(nil)
|
||||
|
||||
func New(config *AliyunCDNDeployerConfig) (*AliyunCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunCDNDeployerConfig, logger deployer.Logger) (*AliyunCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 设置 CDN 域名域名证书
|
||||
// REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate
|
||||
setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{
|
||||
DomainName: tea.String(d.config.Domain),
|
||||
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(certPem),
|
||||
SSLPri: tea.String(privkeyPem),
|
||||
}
|
||||
setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) {
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String("cdn.aliyuncs.com"),
|
||||
}
|
||||
|
||||
client, err := aliyunCdn.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package aliyuncdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
291
internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go
Normal file
291
internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb.go
Normal file
@@ -0,0 +1,291 @@
|
||||
package aliyunclb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
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/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb"
|
||||
)
|
||||
|
||||
type AliyunCLBDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听端口。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerPort int32 `json:"listenerPort,omitempty"`
|
||||
}
|
||||
|
||||
type AliyunCLBDeployer struct {
|
||||
config *AliyunCLBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunSlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunCLBDeployer)(nil)
|
||||
|
||||
func New(config *AliyunCLBDeployerConfig) (*AliyunCLBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunCLBDeployerConfig, logger deployer.Logger) (*AliyunCLBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := providerSlb.New(&providerSlb.AliyunSLBUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
AccessKeySecret: config.AccessKeySecret,
|
||||
Region: config.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunCLBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SLB
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerPorts := make([]int32, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute
|
||||
describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 HTTPS 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerId: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("https"),
|
||||
}
|
||||
describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'")
|
||||
}
|
||||
|
||||
if describeLoadBalancerListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range describeLoadBalancerListenersResp.Body.Listeners {
|
||||
listenerPorts = append(listenerPorts, *listener.ListenerPort)
|
||||
}
|
||||
}
|
||||
|
||||
if len(describeLoadBalancerListenersResp.Body.Listeners) == 0 || describeLoadBalancerListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = describeLoadBalancerListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", listenerPorts)
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, listenerPort := range listenerPorts {
|
||||
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listenerPort, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
if d.config.ListenerPort == 0 {
|
||||
return errors.New("config `listenerPort` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerPort, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerPort int32, cloudCertId string) error {
|
||||
// 查询监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute
|
||||
describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{
|
||||
LoadBalancerId: tea.String(cloudLoadbalancerId),
|
||||
ListenerPort: tea.Int32(cloudListenerPort),
|
||||
}
|
||||
describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)
|
||||
|
||||
// 查询扩展域名
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions
|
||||
describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
LoadBalancerId: tea.String(cloudLoadbalancerId),
|
||||
ListenerPort: tea.Int32(cloudListenerPort),
|
||||
}
|
||||
describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 CLB 扩展域名", describeDomainExtensionsResp)
|
||||
|
||||
// 遍历修改扩展域名
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute
|
||||
//
|
||||
// 这里仅修改跟被替换证书一致的扩展域名
|
||||
if describeDomainExtensionsResp.Body.DomainExtensions != nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension != nil {
|
||||
for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension {
|
||||
if *domainExtension.ServerCertificateId != *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId {
|
||||
continue
|
||||
}
|
||||
|
||||
setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
DomainExtensionId: tea.String(*domainExtension.DomainExtensionId),
|
||||
ServerCertificateId: tea.String(cloudCertId),
|
||||
}
|
||||
_, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改监听配置
|
||||
// REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute
|
||||
//
|
||||
// 注意修改监听配置要放在修改扩展域名之后
|
||||
setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{
|
||||
RegionId: tea.String(d.config.Region),
|
||||
LoadBalancerId: tea.String(cloudLoadbalancerId),
|
||||
ListenerPort: tea.Int32(cloudListenerPort),
|
||||
ServerCertificateId: tea.String(cloudCertId),
|
||||
}
|
||||
setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
case
|
||||
"cn-hangzhou",
|
||||
"cn-hangzhou-finance",
|
||||
"cn-shanghai-finance-1",
|
||||
"cn-shenzhen-finance-1":
|
||||
endpoint = "slb.aliyuncs.com"
|
||||
default:
|
||||
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
client, err := aliyunSlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package aliyunclb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-clb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerPort int
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNCLB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.IntVar(&fListenerPort, argsPrefix+"LISTENERPORT", 443, "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_clb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNCLB_LISTENERPORT=443
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunCLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERPORT: %v", fListenerPort),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunCLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
ListenerPort: int32(fListenerPort),
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
10
internal/pkg/core/deployer/providers/aliyun-clb/defines.go
Normal file
10
internal/pkg/core/deployer/providers/aliyun-clb/defines.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package aliyunclb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
package aliyundcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"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/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type AliyunDCDNDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type AliyunDCDNDeployer struct {
|
||||
config *AliyunDCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunDcdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunDCDNDeployer)(nil)
|
||||
|
||||
func New(config *AliyunDCDNDeployerConfig) (*AliyunDCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunDCDNDeployerConfig, logger deployer.Logger) (*AliyunDCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunDCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunDCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// "*.example.com" → ".example.com",适配阿里云 DCDN 要求的泛域名格式
|
||||
domain := strings.TrimPrefix(d.config.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),
|
||||
CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
|
||||
CertType: tea.String("upload"),
|
||||
SSLProtocol: tea.String("on"),
|
||||
SSLPub: tea.String(certPem),
|
||||
SSLPri: tea.String(privkeyPem),
|
||||
}
|
||||
setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) {
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String("dcdn.aliyuncs.com"),
|
||||
}
|
||||
|
||||
client, err := aliyunDcdn.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package aliyundcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-dcdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_dcdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunDCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
251
internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go
Normal file
251
internal/pkg/core/deployer/providers/aliyun-nlb/aliyun_nlb.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package aliyunnlb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
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/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas"
|
||||
)
|
||||
|
||||
type AliyunNLBDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡实例 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
}
|
||||
|
||||
type AliyunNLBDeployer struct {
|
||||
config *AliyunNLBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *aliyunNlb.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunNLBDeployer)(nil)
|
||||
|
||||
func New(config *AliyunNLBDeployerConfig) (*AliyunNLBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunNLBDeployerConfig, logger deployer.Logger) (*AliyunNLBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
aliyunCasRegion := config.Region
|
||||
if aliyunCasRegion != "" {
|
||||
// 阿里云 CAS 服务接入点是独立于 NLB 服务的
|
||||
// 国内版固定接入点:华东一杭州
|
||||
// 国际版固定接入点:亚太东南一新加坡
|
||||
if !strings.HasPrefix(aliyunCasRegion, "cn-") {
|
||||
aliyunCasRegion = "ap-southeast-1"
|
||||
} else {
|
||||
aliyunCasRegion = "cn-hangzhou"
|
||||
}
|
||||
}
|
||||
uploader, err := providerCas.New(&providerCas.AliyunCASUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
AccessKeySecret: config.AccessKeySecret,
|
||||
Region: aliyunCasRegion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &AliyunNLBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CAS
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡实例的详细信息
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute
|
||||
getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{
|
||||
LoadBalancerId: tea.String(d.config.LoadbalancerId),
|
||||
}
|
||||
getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp)
|
||||
|
||||
// 查询 TCPSSL 监听列表
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-listlisteners
|
||||
listListenersPage := 1
|
||||
listListenersLimit := int32(100)
|
||||
var listListenersToken *string = nil
|
||||
for {
|
||||
listListenersReq := &aliyunNlb.ListListenersRequest{
|
||||
MaxResults: tea.Int32(listListenersLimit),
|
||||
NextToken: listListenersToken,
|
||||
LoadBalancerIds: []*string{tea.String(d.config.LoadbalancerId)},
|
||||
ListenerProtocol: tea.String("TCPSSL"),
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Body.Listeners != nil {
|
||||
for _, listener := range listListenersResp.Body.Listeners {
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
|
||||
if len(listListenersResp.Body.Listeners) == 0 || listListenersResp.Body.NextToken == nil {
|
||||
break
|
||||
} else {
|
||||
listListenersToken = listListenersResp.Body.NextToken
|
||||
listListenersPage += 1
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", listenerIds)
|
||||
|
||||
// 批量更新监听证书
|
||||
var errs []error
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 更新监听
|
||||
if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute
|
||||
getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
}
|
||||
getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 NLB 监听配置", getListenerAttributeResp)
|
||||
|
||||
// 修改监听的属性
|
||||
// REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute
|
||||
updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{
|
||||
ListenerId: tea.String(cloudListenerId),
|
||||
CertificateIds: []*string{tea.String(cloudCertId)},
|
||||
}
|
||||
updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 NLB 监听配置", updateListenerAttributeResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
default:
|
||||
endpoint = fmt.Sprintf("nlb.%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
config := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
Endpoint: tea.String(endpoint),
|
||||
}
|
||||
|
||||
client, err := aliyunNlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package aliyunnlb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-nlb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNNLB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_nlb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_LOADBALANCERID="your-nlb-instance-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNNLB_LISTENERID="your-nlb-listener-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunNLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunNLBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
10
internal/pkg/core/deployer/providers/aliyun-nlb/defines.go
Normal file
10
internal/pkg/core/deployer/providers/aliyun-nlb/defines.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package aliyunnlb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
112
internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go
Normal file
112
internal/pkg/core/deployer/providers/aliyun-oss/aliyun_oss.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package aliyunoss
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type AliyunOSSDeployerConfig struct {
|
||||
// 阿里云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 阿里云 AccessKeySecret。
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
// 阿里云地域。
|
||||
Region string `json:"region"`
|
||||
// 存储桶名。
|
||||
Bucket string `json:"bucket"`
|
||||
// 自定义域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type AliyunOSSDeployer struct {
|
||||
config *AliyunOSSDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *oss.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*AliyunOSSDeployer)(nil)
|
||||
|
||||
func New(config *AliyunOSSDeployerConfig) (*AliyunOSSDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *AliyunOSSDeployerConfig, logger deployer.Logger) (*AliyunOSSDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &AliyunOSSDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *AliyunOSSDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.Bucket == "" {
|
||||
return nil, errors.New("config `bucket` is required")
|
||||
}
|
||||
if d.config.Domain == "" {
|
||||
return nil, errors.New("config `domain` is required")
|
||||
}
|
||||
|
||||
// 为存储空间绑定自定义域名
|
||||
// REF: https://help.aliyun.com/zh/oss/developer-reference/putcname
|
||||
err := d.sdkClient.PutBucketCnameWithCertificate(d.config.Bucket, oss.PutBucketCname{
|
||||
Cname: d.config.Domain,
|
||||
CertificateConfiguration: &oss.CertificateConfiguration{
|
||||
Certificate: certPem,
|
||||
PrivateKey: privkeyPem,
|
||||
Force: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'")
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, accessKeySecret, region string) (*oss.Client, error) {
|
||||
// 接入点一览 https://help.aliyun.com/zh/oss/user-guide/regions-and-endpoints
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "":
|
||||
endpoint = "oss.aliyuncs.com"
|
||||
case
|
||||
"cn-hzjbp",
|
||||
"cn-hzjbp-a",
|
||||
"cn-hzjbp-b":
|
||||
endpoint = "oss-cn-hzjbp-a-internal.aliyuncs.com"
|
||||
case
|
||||
"cn-shanghai-finance-1",
|
||||
"cn-shenzhen-finance-1",
|
||||
"cn-beijing-finance-1",
|
||||
"cn-north-2-gov-1":
|
||||
endpoint = fmt.Sprintf("oss-%s-internal.aliyuncs.com", region)
|
||||
default:
|
||||
endpoint = fmt.Sprintf("oss-%s.aliyuncs.com", region)
|
||||
}
|
||||
|
||||
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package aliyunoss_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fAccessKeySecret string
|
||||
fRegion string
|
||||
fBucket string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_ALIYUNOSS_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v aliyun_oss_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_ACCESSKEYSECRET="your-access-key-secret" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_REGION="cn-hangzhou" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_BUCKET="your-oss-bucket" \
|
||||
--CERTIMATE_DEPLOYER_ALIYUNOSS_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("BUCKET: %v", fBucket),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.AliyunOSSDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
AccessKeySecret: fAccessKeySecret,
|
||||
Region: fRegion,
|
||||
Bucket: fBucket,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package baiducloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
bceCdn "github.com/baidubce/bce-sdk-go/services/cdn"
|
||||
bceCdnApi "github.com/baidubce/bce-sdk-go/services/cdn/api"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
)
|
||||
|
||||
type BaiduCloudCDNDeployerConfig struct {
|
||||
// 百度智能云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 百度智能云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type BaiduCloudCDNDeployer struct {
|
||||
config *BaiduCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *bceCdn.Client
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*BaiduCloudCDNDeployer)(nil)
|
||||
|
||||
func New(config *BaiduCloudCDNDeployerConfig) (*BaiduCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *BaiduCloudCDNDeployerConfig, logger deployer.Logger) (*BaiduCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
return &BaiduCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BaiduCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 修改域名证书
|
||||
// REF: https://cloud.baidu.com/doc/CDN/s/qjzuz2hp8
|
||||
putCertResp, err := d.sdkClient.PutCert(
|
||||
d.config.Domain,
|
||||
&bceCdnApi.UserCertificate{
|
||||
CertName: fmt.Sprintf("certimate-%d", time.Now().UnixMilli()),
|
||||
ServerData: certPem,
|
||||
PrivateData: privkeyPem,
|
||||
},
|
||||
"ON",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.PutCert'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已修改域名证书", putCertResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, secretAccessKey string) (*bceCdn.Client, error) {
|
||||
client, err := bceCdn.NewClient(accessKeyId, secretAccessKey, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package baiducloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fSecretAccessKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v baiducloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
|
||||
--CERTIMATE_DEPLOYER_BAIDUCLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.BaiduCloudCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package bytepluscdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
bpCdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/byteplus-cdn"
|
||||
)
|
||||
|
||||
type BytePlusCDNDeployerConfig struct {
|
||||
// BytePlus AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// BytePlus SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type BytePlusCDNDeployer struct {
|
||||
config *BytePlusCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *bpCdn.CDN
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*BytePlusCDNDeployer)(nil)
|
||||
|
||||
func New(config *BytePlusCDNDeployerConfig) (*BytePlusCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *BytePlusCDNDeployerConfig, logger deployer.Logger) (*BytePlusCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := bpCdn.NewInstance()
|
||||
client.Client.SetAccessKey(config.AccessKey)
|
||||
client.Client.SetSecretKey(config.SecretKey)
|
||||
|
||||
uploader, err := providerCdn.New(&providerCdn.ByteplusCDNUploaderConfig{
|
||||
AccessKey: config.AccessKey,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &BytePlusCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *BytePlusCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
domains := make([]string, 0)
|
||||
if strings.HasPrefix(d.config.Domain, "*.") {
|
||||
// 获取指定证书可关联的域名
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-describecertconfig-9ea17
|
||||
describeCertConfigReq := &bpCdn.DescribeCertConfigRequest{
|
||||
CertId: upres.CertId,
|
||||
}
|
||||
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
|
||||
}
|
||||
|
||||
if describeCertConfigResp.Result.CertNotConfig != nil {
|
||||
for i := range describeCertConfigResp.Result.CertNotConfig {
|
||||
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if describeCertConfigResp.Result.OtherCertConfig != nil {
|
||||
for i := range describeCertConfigResp.Result.OtherCertConfig {
|
||||
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
|
||||
// 所有可关联的域名都配置了该证书,跳过部署
|
||||
} else {
|
||||
return nil, xerrors.New("domain not found")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, d.config.Domain)
|
||||
}
|
||||
|
||||
if len(domains) > 0 {
|
||||
var errs []error
|
||||
|
||||
for _, domain := range domains {
|
||||
// 关联证书与加速域名
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-batchdeploycert
|
||||
batchDeployCertReq := &bpCdn.BatchDeployCertRequest{
|
||||
CertId: upres.CertId,
|
||||
Domain: domain,
|
||||
}
|
||||
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
d.logger.Logt(fmt.Sprintf("已关联证书到域名 %s", domain), batchDeployCertResp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package bytepluscdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_BYTEPLUSCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v byteplus_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_BYTEPLUSCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.BytePlusCDNDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package dogecloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerDoge "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/dogecloud"
|
||||
dogesdk "github.com/usual2970/certimate/internal/pkg/vendors/dogecloud-sdk"
|
||||
)
|
||||
|
||||
type DogeCloudCDNDeployerConfig struct {
|
||||
// 多吉云 AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// 多吉云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type DogeCloudCDNDeployer struct {
|
||||
config *DogeCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *dogesdk.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*DogeCloudCDNDeployer)(nil)
|
||||
|
||||
func New(config *DogeCloudCDNDeployerConfig) (*DogeCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *DogeCloudCDNDeployerConfig, logger deployer.Logger) (*DogeCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := dogesdk.NewClient(config.AccessKey, config.SecretKey)
|
||||
|
||||
uploader, err := providerDoge.New(&providerDoge.DogeCloudUploaderConfig{
|
||||
AccessKey: config.AccessKey,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &DogeCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DogeCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 绑定证书
|
||||
// REF: https://docs.dogecloud.com/cdn/api-cert-bind
|
||||
bindCdnCertId, _ := strconv.ParseInt(upres.CertId, 10, 64)
|
||||
bindCdnCertResp, err := d.sdkClient.BindCdnCertWithDomain(bindCdnCertId, d.config.Domain)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.BindCdnCert'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已绑定证书", bindCdnCertResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package dogecloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_DOGECLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v dogecloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_DOGECLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.DogeCloudCDNDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package huaweicloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"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/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
huaweicloudsdk "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk"
|
||||
)
|
||||
|
||||
type HuaweiCloudCDNDeployerConfig struct {
|
||||
// 华为云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 华为云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 华为云地域。
|
||||
Region string `json:"region"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type HuaweiCloudCDNDeployer struct {
|
||||
config *HuaweiCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *huaweicloudsdk.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*HuaweiCloudCDNDeployer)(nil)
|
||||
|
||||
func New(config *HuaweiCloudCDNDeployerConfig) (*HuaweiCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *HuaweiCloudCDNDeployerConfig, logger deployer.Logger) (*HuaweiCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(
|
||||
config.AccessKeyId,
|
||||
config.SecretAccessKey,
|
||||
config.Region,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := providerScm.New(&providerScm.HuaweiCloudSCMUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
SecretAccessKey: config.SecretAccessKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &HuaweiCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 查询加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
|
||||
showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{
|
||||
DomainName: d.config.Domain,
|
||||
}
|
||||
showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到加速域名配置", showDomainFullConfigResp)
|
||||
|
||||
// 更新加速域名配置
|
||||
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
|
||||
// REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html
|
||||
updateDomainMultiCertificatesReqBodyContent := &huaweicloudsdk.UpdateDomainMultiCertificatesExRequestBodyContent{}
|
||||
updateDomainMultiCertificatesReqBodyContent.DomainName = d.config.Domain
|
||||
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
|
||||
updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2)
|
||||
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId)
|
||||
updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName)
|
||||
updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs)
|
||||
updateDomainMultiCertificatesReq := &huaweicloudsdk.UpdateDomainMultiCertificatesExRequest{
|
||||
Body: &huaweicloudsdk.UpdateDomainMultiCertificatesExRequestBody{
|
||||
Https: updateDomainMultiCertificatesReqBodyContent,
|
||||
},
|
||||
}
|
||||
updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新加速域名配置", updateDomainMultiCertificatesResp)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, secretAccessKey, region string) (*huaweicloudsdk.Client, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-1" // CDN 服务默认区域:华北一北京
|
||||
}
|
||||
|
||||
auth, err := global.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcRegion, err := hcCdnRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcClient, err := hcCdn.CdnClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := huaweicloudsdk.NewClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package huaweicloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fSecretAccessKey string
|
||||
fRegion string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v huaweicloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_REGION="cn-north-1" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudCDNDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package huaweicloudelb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:替换指定证书。
|
||||
DEPLOY_RESOURCE_CERTIFICATE = DeployResourceType("certificate")
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
)
|
||||
@@ -0,0 +1,395 @@
|
||||
package huaweicloudelb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
|
||||
hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3"
|
||||
hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model"
|
||||
hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region"
|
||||
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"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
)
|
||||
|
||||
type HuaweiCloudELBDeployerConfig struct {
|
||||
// 华为云 AccessKeyId。
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
// 华为云 SecretAccessKey。
|
||||
SecretAccessKey string `json:"secretAccessKey"`
|
||||
// 华为云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 证书 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_CERTIFICATE] 时必填。
|
||||
CertificateId string `json:"certificateId,omitempty"`
|
||||
// 负载均衡器 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LOADBALANCER] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_LISTENER] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
}
|
||||
|
||||
type HuaweiCloudELBDeployer struct {
|
||||
config *HuaweiCloudELBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *hcElb.ElbClient
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*HuaweiCloudELBDeployer)(nil)
|
||||
|
||||
func New(config *HuaweiCloudELBDeployerConfig) (*HuaweiCloudELBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *HuaweiCloudELBDeployerConfig, logger deployer.Logger) (*HuaweiCloudELBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk client")
|
||||
}
|
||||
|
||||
uploader, err := providerElb.New(&providerElb.HuaweiCloudELBUploaderConfig{
|
||||
AccessKeyId: config.AccessKeyId,
|
||||
SecretAccessKey: config.SecretAccessKey,
|
||||
Region: config.Region,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &HuaweiCloudELBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_CERTIFICATE:
|
||||
if err := d.deployToCertificate(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, certPem, privkeyPem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context, certPem string, privkeyPem string) error {
|
||||
if d.config.CertificateId == "" {
|
||||
return errors.New("config `certificateId` is required")
|
||||
}
|
||||
|
||||
// 更新证书
|
||||
// REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html
|
||||
updateCertificateReq := &hcElbModel.UpdateCertificateRequest{
|
||||
CertificateId: d.config.CertificateId,
|
||||
Body: &hcElbModel.UpdateCertificateRequestBody{
|
||||
Certificate: &hcElbModel.UpdateCertificateOption{
|
||||
Certificate: cast.StringPtr(certPem),
|
||||
PrivateKey: cast.StringPtr(privkeyPem),
|
||||
},
|
||||
},
|
||||
}
|
||||
updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ELB 证书", updateCertificateResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context, certPem string, privkeyPem string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询负载均衡器详情
|
||||
// REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html
|
||||
showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{
|
||||
LoadbalancerId: d.config.LoadbalancerId,
|
||||
}
|
||||
showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ELB 负载均衡器", showLoadBalancerResp)
|
||||
|
||||
// 查询监听器列表
|
||||
// REF: https://support.huaweicloud.com/api-elb/ListListeners.html
|
||||
listListenersLimit := int32(2000)
|
||||
var listListenersMarker *string = nil
|
||||
for {
|
||||
listListenersReq := &hcElbModel.ListListenersRequest{
|
||||
Limit: cast.Int32Ptr(listListenersLimit),
|
||||
Marker: listListenersMarker,
|
||||
Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"},
|
||||
LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id},
|
||||
}
|
||||
listListenersResp, err := d.sdkClient.ListListeners(listListenersReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'")
|
||||
}
|
||||
|
||||
if listListenersResp.Listeners != nil {
|
||||
for _, listener := range *listListenersResp.Listeners {
|
||||
listenerIds = append(listenerIds, listener.Id)
|
||||
}
|
||||
}
|
||||
|
||||
if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) {
|
||||
break
|
||||
} else {
|
||||
listListenersMarker = listListenersResp.PageInfo.NextMarker
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ELB 负载均衡器下的监听器", listenerIds)
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 批量更新监听器证书
|
||||
var errs []error
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.modifyListenerCertificate(ctx, listenerId, upres.CertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context, certPem string, privkeyPem string) error {
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SCM
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 更新监听器证书
|
||||
if err := d.modifyListenerCertificate(ctx, d.config.ListenerId, upres.CertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
|
||||
// 查询监听器详情
|
||||
// REF: https://support.huaweicloud.com/api-elb/ShowListener.html
|
||||
showListenerReq := &hcElbModel.ShowListenerRequest{
|
||||
ListenerId: cloudListenerId,
|
||||
}
|
||||
showListenerResp, err := d.sdkClient.ShowListener(showListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到 ELB 监听器", showListenerResp)
|
||||
|
||||
// 更新监听器
|
||||
// REF: https://support.huaweicloud.com/api-elb/UpdateListener.html
|
||||
updateListenerReq := &hcElbModel.UpdateListenerRequest{
|
||||
ListenerId: cloudListenerId,
|
||||
Body: &hcElbModel.UpdateListenerRequestBody{
|
||||
Listener: &hcElbModel.UpdateListenerOption{
|
||||
DefaultTlsContainerRef: cast.StringPtr(cloudCertId),
|
||||
},
|
||||
},
|
||||
}
|
||||
if showListenerResp.Listener.SniContainerRefs != nil {
|
||||
if len(showListenerResp.Listener.SniContainerRefs) > 0 {
|
||||
// 如果开启 SNI,需替换同 SAN 的证书
|
||||
sniCertIds := make([]string, 0)
|
||||
sniCertIds = append(sniCertIds, cloudCertId)
|
||||
|
||||
listOldCertificateReq := &hcElbModel.ListCertificatesRequest{
|
||||
Id: &showListenerResp.Listener.SniContainerRefs,
|
||||
}
|
||||
listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'")
|
||||
}
|
||||
|
||||
showNewCertificateReq := &hcElbModel.ShowCertificateRequest{
|
||||
CertificateId: cloudCertId,
|
||||
}
|
||||
showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'")
|
||||
}
|
||||
|
||||
for _, certificate := range *listOldCertificateResp.Certificates {
|
||||
oldCertificate := certificate
|
||||
newCertificate := showNewCertificateResp.Certificate
|
||||
|
||||
if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil {
|
||||
if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if oldCertificate.Domain == newCertificate.Domain {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
sniCertIds = append(sniCertIds, certificate.Id)
|
||||
}
|
||||
|
||||
updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds
|
||||
}
|
||||
|
||||
if showListenerResp.Listener.SniMatchAlgo != "" {
|
||||
updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo)
|
||||
}
|
||||
}
|
||||
updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已更新 ELB 监听器", updateListenerResp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-4" // ELB 服务默认区域:华北四北京
|
||||
}
|
||||
|
||||
projectId, err := getSdkProjectId(accessKeyId, secretAccessKey, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth, err := basic.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
WithProjectId(projectId).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcRegion, err := hcElbRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hcClient, err := hcElb.ElbClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := hcElb.NewElbClient(hcClient)
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) {
|
||||
if region == "" {
|
||||
region = "cn-north-4" // IAM 服务默认区域:华北四北京
|
||||
}
|
||||
|
||||
auth, err := global.NewCredentialsBuilder().
|
||||
WithAk(accessKeyId).
|
||||
WithSk(secretAccessKey).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hcRegion, err := hcIamRegion.SafeValueOf(region)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hcClient, err := hcIam.IamClientBuilder().
|
||||
WithRegion(hcRegion).
|
||||
WithCredential(auth).
|
||||
SafeBuild()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := hcIam.NewIamClient(hcClient)
|
||||
|
||||
request := &hcIamModel.KeystoneListProjectsRequest{
|
||||
Name: ®ion,
|
||||
}
|
||||
response, err := client.KeystoneListProjects(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if response.Projects == nil || len(*response.Projects) == 0 {
|
||||
return "", errors.New("no project found")
|
||||
}
|
||||
|
||||
return (*response.Projects)[0].Id, nil
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package huaweicloudelb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKeyId string
|
||||
fSecretAccessKey string
|
||||
fRegion string
|
||||
fCertificateId string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_HUAWEICLOUDELB_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
|
||||
flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v huaweicloud_elb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_ACCESSKEYID="your-access-key-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_REGION="cn-north-1" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_CERTIFICATEID="your-elb-cert-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LOADBALANCERID="your-elb-loadbalancer-id" \
|
||||
--CERTIMATE_DEPLOYER_HUAWEICLOUDELB_LISTENERID="your-elb-listener-id"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_ToCertificate", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_CERTIFICATE,
|
||||
CertificateId: fCertificateId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListenerId", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId),
|
||||
fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.HuaweiCloudELBDeployerConfig{
|
||||
AccessKeyId: fAccessKeyId,
|
||||
SecretAccessKey: fSecretAccessKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
160
internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go
Normal file
160
internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package k8ssecret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type K8sSecretDeployerConfig struct {
|
||||
// kubeconfig 文件内容。
|
||||
KubeConfig string `json:"kubeConfig,omitempty"`
|
||||
// K8s 命名空间。
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
// K8s Secret 名称。
|
||||
SecretName string `json:"secretName"`
|
||||
// K8s Secret 中用于存放证书的 Key。
|
||||
SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"`
|
||||
// K8s Secret 中用于存放私钥的 Key。
|
||||
SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"`
|
||||
}
|
||||
|
||||
type K8sSecretDeployer struct {
|
||||
config *K8sSecretDeployerConfig
|
||||
logger deployer.Logger
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*K8sSecretDeployer)(nil)
|
||||
|
||||
func New(config *K8sSecretDeployerConfig) (*K8sSecretDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *K8sSecretDeployerConfig, logger deployer.Logger) (*K8sSecretDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
return &K8sSecretDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *K8sSecretDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.Namespace == "" {
|
||||
return nil, errors.New("config `namespace` is required")
|
||||
}
|
||||
if d.config.SecretName == "" {
|
||||
return nil, errors.New("config `secretName` is required")
|
||||
}
|
||||
if d.config.SecretDataKeyForCrt == "" {
|
||||
return nil, errors.New("config `secretDataKeyForCrt` is required")
|
||||
}
|
||||
if d.config.SecretDataKeyForKey == "" {
|
||||
return nil, errors.New("config `secretDataKeyForKey` is required")
|
||||
}
|
||||
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 连接
|
||||
client, err := createK8sClient(d.config.KubeConfig)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create k8s client")
|
||||
}
|
||||
|
||||
var secretPayload *k8sCore.Secret
|
||||
secretAnnotations := map[string]string{
|
||||
"certimate/common-name": certX509.Subject.CommonName,
|
||||
"certimate/subject-sn": certX509.Subject.SerialNumber,
|
||||
"certimate/subject-alt-names": strings.Join(certX509.DNSNames, ","),
|
||||
"certimate/issuer-sn": certX509.Issuer.SerialNumber,
|
||||
"certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","),
|
||||
}
|
||||
|
||||
// 获取 Secret 实例,如果不存在则创建
|
||||
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(context.TODO(), d.config.SecretName, k8sMeta.GetOptions{})
|
||||
if err != nil {
|
||||
secretPayload = &k8sCore.Secret{
|
||||
TypeMeta: k8sMeta.TypeMeta{
|
||||
Kind: "Secret",
|
||||
APIVersion: "v1",
|
||||
},
|
||||
ObjectMeta: k8sMeta.ObjectMeta{
|
||||
Name: d.config.SecretName,
|
||||
Annotations: secretAnnotations,
|
||||
},
|
||||
Type: k8sCore.SecretType("kubernetes.io/tls"),
|
||||
}
|
||||
secretPayload.Data = make(map[string][]byte)
|
||||
secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPem)
|
||||
secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPem)
|
||||
|
||||
_, err = client.CoreV1().Secrets(d.config.Namespace).Create(context.TODO(), secretPayload, k8sMeta.CreateOptions{})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create k8s secret")
|
||||
} else {
|
||||
d.logger.Logf("k8s secret created", secretPayload)
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 Secret 实例
|
||||
secretPayload.Type = k8sCore.SecretType("kubernetes.io/tls")
|
||||
if secretPayload.ObjectMeta.Annotations == nil {
|
||||
secretPayload.ObjectMeta.Annotations = secretAnnotations
|
||||
} else {
|
||||
for k, v := range secretAnnotations {
|
||||
secretPayload.ObjectMeta.Annotations[k] = v
|
||||
}
|
||||
}
|
||||
secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(context.TODO(), secretPayload, k8sMeta.UpdateOptions{})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to update k8s secret")
|
||||
}
|
||||
|
||||
d.logger.Logf("k8s secret updated", secretPayload)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) {
|
||||
var config *rest.Config
|
||||
var err error
|
||||
if kubeConfig == "" {
|
||||
config, err = rest.InClusterConfig()
|
||||
} else {
|
||||
kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config, err = kubeConfig.ClientConfig()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package k8ssecret_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fNamespace string
|
||||
fSecretName string
|
||||
fSecretDataKeyForCrt string
|
||||
fSecretDataKeyForKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_K8SSECRET_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fNamespace, argsPrefix+"NAMESPACE", "default", "")
|
||||
flag.StringVar(&fSecretName, argsPrefix+"SECRETNAME", "", "")
|
||||
flag.StringVar(&fSecretDataKeyForCrt, argsPrefix+"SECRETDATAKEYFORCRT", "tls.crt", "")
|
||||
flag.StringVar(&fSecretDataKeyForKey, argsPrefix+"SECRETDATAKEYFORKEY", "tls.key", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v k8s_secret_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_NAMESPACE="default" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETNAME="secret" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORCRT="tls.crt" \
|
||||
--CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORKEY="tls.key"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("NAMESPACE: %v", fNamespace),
|
||||
fmt.Sprintf("SECRETNAME: %v", fSecretName),
|
||||
fmt.Sprintf("SECRETDATAKEYFORCRT: %v", fSecretDataKeyForCrt),
|
||||
fmt.Sprintf("SECRETDATAKEYFORKEY: %v", fSecretDataKeyForKey),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.K8sSecretDeployerConfig{
|
||||
Namespace: fNamespace,
|
||||
SecretName: fSecretName,
|
||||
SecretDataKeyForCrt: fSecretDataKeyForCrt,
|
||||
SecretDataKeyForKey: fSecretDataKeyForKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
17
internal/pkg/core/deployer/providers/local/defines.go
Normal file
17
internal/pkg/core/deployer/providers/local/defines.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package local
|
||||
|
||||
type OutputFormatType string
|
||||
|
||||
const (
|
||||
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
|
||||
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
|
||||
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
|
||||
)
|
||||
|
||||
type ShellEnvType string
|
||||
|
||||
const (
|
||||
SHELL_ENV_SH = ShellEnvType("sh")
|
||||
SHELL_ENV_CMD = ShellEnvType("cmd")
|
||||
SHELL_ENV_POWERSHELL = ShellEnvType("powershell")
|
||||
)
|
||||
178
internal/pkg/core/deployer/providers/local/local.go
Normal file
178
internal/pkg/core/deployer/providers/local/local.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/fs"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type LocalDeployerConfig struct {
|
||||
// Shell 执行环境。
|
||||
// 零值时默认根据操作系统决定。
|
||||
ShellEnv ShellEnvType `json:"shellEnv,omitempty"`
|
||||
// 前置命令。
|
||||
PreCommand string `json:"preCommand,omitempty"`
|
||||
// 后置命令。
|
||||
PostCommand string `json:"postCommand,omitempty"`
|
||||
// 输出证书格式。
|
||||
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
|
||||
// 输出证书文件路径。
|
||||
OutputCertPath string `json:"outputCertPath,omitempty"`
|
||||
// 输出私钥文件路径。
|
||||
OutputKeyPath string `json:"outputKeyPath,omitempty"`
|
||||
// PFX 导出密码。
|
||||
// 证书格式为 PFX 时必填。
|
||||
PfxPassword string `json:"pfxPassword,omitempty"`
|
||||
// JKS 别名。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksAlias string `json:"jksAlias,omitempty"`
|
||||
// JKS 密钥密码。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksKeypass string `json:"jksKeypass,omitempty"`
|
||||
// JKS 存储密码。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksStorepass string `json:"jksStorepass,omitempty"`
|
||||
}
|
||||
|
||||
type LocalDeployer struct {
|
||||
config *LocalDeployerConfig
|
||||
logger deployer.Logger
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*LocalDeployer)(nil)
|
||||
|
||||
func New(config *LocalDeployerConfig) (*LocalDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *LocalDeployerConfig, logger deployer.Logger) (*LocalDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
return &LocalDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *LocalDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 执行前置命令
|
||||
if d.config.PreCommand != "" {
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PreCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("pre-command executed", stdout)
|
||||
}
|
||||
|
||||
// 写入证书和私钥文件
|
||||
switch d.config.OutputFormat {
|
||||
case OUTPUT_FORMAT_PEM:
|
||||
if err := fs.WriteFileString(d.config.OutputCertPath, certPem); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file saved")
|
||||
|
||||
if err := fs.WriteFileString(d.config.OutputKeyPath, privkeyPem); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save private key file")
|
||||
}
|
||||
|
||||
d.logger.Logt("private key file saved")
|
||||
|
||||
case OUTPUT_FORMAT_PFX:
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate transformed to PFX")
|
||||
|
||||
if err := fs.WriteFile(d.config.OutputCertPath, pfxData); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file saved")
|
||||
|
||||
case OUTPUT_FORMAT_JKS:
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate transformed to JKS")
|
||||
|
||||
if err := fs.WriteFile(d.config.OutputCertPath, jksData); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to save certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
|
||||
}
|
||||
|
||||
// 执行后置命令
|
||||
if d.config.PostCommand != "" {
|
||||
stdout, stderr, err := execCommand(d.config.ShellEnv, d.config.PostCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("post-command executed", stdout)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func execCommand(shellEnv ShellEnvType, command string) (string, string, error) {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
switch shellEnv {
|
||||
case SHELL_ENV_SH:
|
||||
cmd = exec.Command("sh", "-c", command)
|
||||
|
||||
case SHELL_ENV_CMD:
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
|
||||
case SHELL_ENV_POWERSHELL:
|
||||
cmd = exec.Command("powershell", "-Command", command)
|
||||
|
||||
case "":
|
||||
if runtime.GOOS == "windows" {
|
||||
cmd = exec.Command("cmd", "/C", command)
|
||||
} else {
|
||||
cmd = exec.Command("sh", "-c", command)
|
||||
}
|
||||
|
||||
default:
|
||||
return "", "", fmt.Errorf("unsupported shell env: %s", shellEnv)
|
||||
}
|
||||
|
||||
var stdoutBuf bytes.Buffer
|
||||
cmd.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
cmd.Stderr = &stderrBuf
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return "", "", xerrors.Wrap(err, "failed to execute shell command")
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
}
|
||||
186
internal/pkg/core/deployer/providers/local/local_test.go
Normal file
186
internal/pkg/core/deployer/providers/local/local_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package local_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fOutputCertPath string
|
||||
fOutputKeyPath string
|
||||
fPfxPassword string
|
||||
fJksAlias string
|
||||
fJksKeypass string
|
||||
fJksStorepass string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_LOCAL_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fPfxPassword, argsPrefix+"PFXPASSWORD", "", "")
|
||||
flag.StringVar(&fJksAlias, argsPrefix+"JKSALIAS", "", "")
|
||||
flag.StringVar(&fJksKeypass, argsPrefix+"JKSKEYPASS", "", "")
|
||||
flag.StringVar(&fJksStorepass, argsPrefix+"JKSSTOREPASS", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v local_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_OUTPUTCERTPATH="/path/to/your-output-cert" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_OUTPUTKEYPATH="/path/to/your-output-key" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_PFXPASSWORD="your-pfx-password" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSALIAS="your-jks-alias" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSKEYPASS="your-jks-keypass" \
|
||||
--CERTIMATE_DEPLOYER_LOCAL_JKSSTOREPASS="your-jks-storepass"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_PEM", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.LocalDeployerConfig{
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fstat1, err := os.Stat(fOutputCertPath)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
} else if fstat1.Size() == 0 {
|
||||
t.Errorf("err: empty output certificate file")
|
||||
return
|
||||
}
|
||||
|
||||
fstat2, err := os.Stat(fOutputKeyPath)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
} else if fstat2.Size() == 0 {
|
||||
t.Errorf("err: empty output private key file")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_PFX", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
fmt.Sprintf("PFXPASSWORD: %v", fPfxPassword),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.LocalDeployerConfig{
|
||||
OutputFormat: provider.OUTPUT_FORMAT_PFX,
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
PfxPassword: fPfxPassword,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fstat, err := os.Stat(fOutputCertPath)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
} else if fstat.Size() == 0 {
|
||||
t.Errorf("err: empty output certificate file")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_JKS", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
fmt.Sprintf("JKSALIAS: %v", fJksAlias),
|
||||
fmt.Sprintf("JKSKEYPASS: %v", fJksKeypass),
|
||||
fmt.Sprintf("JKSSTOREPASS: %v", fJksStorepass),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.LocalDeployerConfig{
|
||||
OutputFormat: provider.OUTPUT_FORMAT_JKS,
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
JksAlias: fJksAlias,
|
||||
JksKeypass: fJksKeypass,
|
||||
JksStorepass: fJksStorepass,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fstat, err := os.Stat(fOutputCertPath)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
} else if fstat.Size() == 0 {
|
||||
t.Errorf("err: empty output certificate file")
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
106
internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go
Normal file
106
internal/pkg/core/deployer/providers/qiniu-cdn/qiniu_cdn.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package qiniucdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/qiniu/go-sdk/v7/auth"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert"
|
||||
qiniusdk "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk"
|
||||
)
|
||||
|
||||
type QiniuCDNDeployerConfig struct {
|
||||
// 七牛云 AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// 七牛云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type QiniuCDNDeployer struct {
|
||||
config *QiniuCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *qiniusdk.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*QiniuCDNDeployer)(nil)
|
||||
|
||||
func New(config *QiniuCDNDeployerConfig) (*QiniuCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *QiniuCDNDeployerConfig, logger deployer.Logger) (*QiniuCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := qiniusdk.NewClient(auth.New(config.AccessKey, config.SecretKey))
|
||||
|
||||
uploader, err := providerQiniu.New(&providerQiniu.QiniuSSLCertUploaderConfig{
|
||||
AccessKey: config.AccessKey,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &QiniuCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *QiniuCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// "*.example.com" → ".example.com",适配七牛云 CDN 要求的泛域名格式
|
||||
domain := strings.TrimPrefix(d.config.Domain, "*")
|
||||
|
||||
// 获取域名信息
|
||||
// REF: https://developer.qiniu.com/fusion/4246/the-domain-name
|
||||
getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已获取域名信息", 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 nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已修改域名证书", modifyDomainHttpsConfResp)
|
||||
} else {
|
||||
enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已将域名升级为 HTTPS", enableDomainHttpsResp)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package qiniucdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_QINIUCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v qiniu_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_QINIUCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_QINIUCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_QINIUCDN_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_QINIUCDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_QINIUCDN_DOMAIN="example.com" \
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.QiniuCDNDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
9
internal/pkg/core/deployer/providers/ssh/defines.go
Normal file
9
internal/pkg/core/deployer/providers/ssh/defines.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package ssh
|
||||
|
||||
type OutputFormatType string
|
||||
|
||||
const (
|
||||
OUTPUT_FORMAT_PEM = OutputFormatType("PEM")
|
||||
OUTPUT_FORMAT_PFX = OutputFormatType("PFX")
|
||||
OUTPUT_FORMAT_JKS = OutputFormatType("JKS")
|
||||
)
|
||||
252
internal/pkg/core/deployer/providers/ssh/ssh.go
Normal file
252
internal/pkg/core/deployer/providers/ssh/ssh.go
Normal file
@@ -0,0 +1,252 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
"github.com/pkg/sftp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
)
|
||||
|
||||
type SshDeployerConfig struct {
|
||||
// SSH 主机。
|
||||
// 零值时默认为 "localhost"。
|
||||
SshHost string `json:"sshHost,omitempty"`
|
||||
// SSH 端口。
|
||||
// 零值时默认为 22。
|
||||
SshPort int32 `json:"sshPort,omitempty"`
|
||||
// SSH 登录用户名。
|
||||
SshUsername string `json:"sshUsername,omitempty"`
|
||||
// SSH 登录密码。
|
||||
SshPassword string `json:"sshPassword,omitempty"`
|
||||
// SSH 登录私钥。
|
||||
SshKey string `json:"sshKey,omitempty"`
|
||||
// SSH 登录私钥口令。
|
||||
SshKeyPassphrase string `json:"sshKeyPassphrase,omitempty"`
|
||||
// 前置命令。
|
||||
PreCommand string `json:"preCommand,omitempty"`
|
||||
// 后置命令。
|
||||
PostCommand string `json:"postCommand,omitempty"`
|
||||
// 输出证书格式。
|
||||
OutputFormat OutputFormatType `json:"outputFormat,omitempty"`
|
||||
// 输出证书文件路径。
|
||||
OutputCertPath string `json:"outputCertPath,omitempty"`
|
||||
// 输出私钥文件路径。
|
||||
OutputKeyPath string `json:"outputKeyPath,omitempty"`
|
||||
// PFX 导出密码。
|
||||
// 证书格式为 PFX 时必填。
|
||||
PfxPassword string `json:"pfxPassword,omitempty"`
|
||||
// JKS 别名。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksAlias string `json:"jksAlias,omitempty"`
|
||||
// JKS 密钥密码。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksKeypass string `json:"jksKeypass,omitempty"`
|
||||
// JKS 存储密码。
|
||||
// 证书格式为 JKS 时必填。
|
||||
JksStorepass string `json:"jksStorepass,omitempty"`
|
||||
}
|
||||
|
||||
type SshDeployer struct {
|
||||
config *SshDeployerConfig
|
||||
logger deployer.Logger
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*SshDeployer)(nil)
|
||||
|
||||
func New(config *SshDeployerConfig) (*SshDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *SshDeployerConfig, logger deployer.Logger) (*SshDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
return &SshDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *SshDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 连接
|
||||
client, err := createSshClient(
|
||||
d.config.SshHost,
|
||||
d.config.SshPort,
|
||||
d.config.SshUsername,
|
||||
d.config.SshPassword,
|
||||
d.config.SshKey,
|
||||
d.config.SshKeyPassphrase,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssh client")
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
d.logger.Logt("SSH connected")
|
||||
|
||||
// 执行前置命令
|
||||
if d.config.PreCommand != "" {
|
||||
stdout, stderr, err := execSshCommand(client, d.config.PreCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("SSH pre-command executed", stdout)
|
||||
}
|
||||
|
||||
// 上传证书和私钥文件
|
||||
switch d.config.OutputFormat {
|
||||
case OUTPUT_FORMAT_PEM:
|
||||
if err := writeSftpFileString(client, d.config.OutputCertPath, certPem); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded")
|
||||
|
||||
if err := writeSftpFileString(client, d.config.OutputKeyPath, privkeyPem); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload private key file")
|
||||
}
|
||||
|
||||
d.logger.Logt("private key file uploaded")
|
||||
|
||||
case OUTPUT_FORMAT_PFX:
|
||||
pfxData, err := x509.TransformCertificateFromPEMToPFX(certPem, privkeyPem, d.config.PfxPassword)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to transform certificate to PFX")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate transformed to PFX")
|
||||
|
||||
if err := writeSftpFile(client, d.config.OutputCertPath, pfxData); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded")
|
||||
|
||||
case OUTPUT_FORMAT_JKS:
|
||||
jksData, err := x509.TransformCertificateFromPEMToJKS(certPem, privkeyPem, d.config.JksAlias, d.config.JksKeypass, d.config.JksStorepass)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to transform certificate to JKS")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate transformed to JKS")
|
||||
|
||||
if err := writeSftpFile(client, d.config.OutputCertPath, jksData); err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded")
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported output format: %s", d.config.OutputFormat)
|
||||
}
|
||||
|
||||
// 执行后置命令
|
||||
if d.config.PostCommand != "" {
|
||||
stdout, stderr, err := execSshCommand(client, d.config.PostCommand)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr)
|
||||
}
|
||||
|
||||
d.logger.Logt("SSH post-command executed", stdout)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSshClient(host string, port int32, username string, password string, key string, keyPassphrase string) (*ssh.Client, error) {
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
|
||||
var authMethod ssh.AuthMethod
|
||||
if key != "" {
|
||||
var signer ssh.Signer
|
||||
var err error
|
||||
|
||||
if keyPassphrase != "" {
|
||||
signer, err = ssh.ParsePrivateKeyWithPassphrase([]byte(key), []byte(keyPassphrase))
|
||||
} else {
|
||||
signer, err = ssh.ParsePrivateKey([]byte(key))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
authMethod = ssh.PublicKeys(signer)
|
||||
} else {
|
||||
authMethod = ssh.Password(password)
|
||||
}
|
||||
|
||||
return ssh.Dial("tcp", fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []ssh.AuthMethod{authMethod},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
})
|
||||
}
|
||||
|
||||
func execSshCommand(sshCli *ssh.Client, command string) (string, string, error) {
|
||||
session, err := sshCli.NewSession()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
var stdoutBuf bytes.Buffer
|
||||
session.Stdout = &stdoutBuf
|
||||
var stderrBuf bytes.Buffer
|
||||
session.Stderr = &stderrBuf
|
||||
err = session.Run(command)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return stdoutBuf.String(), stderrBuf.String(), nil
|
||||
}
|
||||
|
||||
func writeSftpFileString(sshCli *ssh.Client, path string, content string) error {
|
||||
return writeSftpFile(sshCli, path, []byte(content))
|
||||
}
|
||||
|
||||
func writeSftpFile(sshCli *ssh.Client, path string, data []byte) error {
|
||||
sftpCli, err := sftp.NewClient(sshCli)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to create sftp client")
|
||||
}
|
||||
defer sftpCli.Close()
|
||||
|
||||
if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil {
|
||||
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 xerrors.Wrap(err, "failed to open remote file")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, err = file.Write(data)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to write to remote file")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
90
internal/pkg/core/deployer/providers/ssh/ssh_test.go
Normal file
90
internal/pkg/core/deployer/providers/ssh/ssh_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package ssh_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fSshHost string
|
||||
fSshPort int
|
||||
fSshUsername string
|
||||
fSshPassword string
|
||||
fOutputCertPath string
|
||||
fOutputKeyPath string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_SSH_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fSshHost, argsPrefix+"SSHHOST", "", "")
|
||||
flag.IntVar(&fSshPort, argsPrefix+"SSHPORT", 0, "")
|
||||
flag.StringVar(&fSshUsername, argsPrefix+"SSHUSERNAME", "", "")
|
||||
flag.StringVar(&fSshPassword, argsPrefix+"SSHPASSWORD", "", "")
|
||||
flag.StringVar(&fOutputCertPath, argsPrefix+"OUTPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fOutputKeyPath, argsPrefix+"OUTPUTKEYPATH", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v ssh_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_SSH_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_SSH_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_SSH_SSHHOST="localhost" \
|
||||
--CERTIMATE_DEPLOYER_SSH_SSHPORT=22 \
|
||||
--CERTIMATE_DEPLOYER_SSH_SSHUSERNAME="root" \
|
||||
--CERTIMATE_DEPLOYER_SSH_SSHPASSWORD="password" \
|
||||
--CERTIMATE_DEPLOYER_SSH_OUTPUTCERTPATH="/path/to/your-output-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_SSH_OUTPUTKEYPATH="/path/to/your-output-key.pem"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SSHHOST: %v", fSshHost),
|
||||
fmt.Sprintf("SSHPORT: %v", fSshPort),
|
||||
fmt.Sprintf("SSHUSERNAME: %v", fSshUsername),
|
||||
fmt.Sprintf("SSHPASSWORD: %v", fSshPassword),
|
||||
fmt.Sprintf("OUTPUTCERTPATH: %v", fOutputCertPath),
|
||||
fmt.Sprintf("OUTPUTKEYPATH: %v", fOutputKeyPath),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.SshDeployerConfig{
|
||||
SshHost: fSshHost,
|
||||
SshPort: int32(fSshPort),
|
||||
SshUsername: fSshUsername,
|
||||
SshPassword: fSshPassword,
|
||||
OutputCertPath: fOutputCertPath,
|
||||
OutputKeyPath: fOutputKeyPath,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package tencentcloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCloudCDNDeployerConfig struct {
|
||||
// 腾讯云 SecretId。
|
||||
SecretId string `json:"secretId"`
|
||||
// 腾讯云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type TencentCloudCDNDeployer struct {
|
||||
config *TencentCloudCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClients *tencentCloudCDNDeployerSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*TencentCloudCDNDeployer)(nil)
|
||||
|
||||
type tencentCloudCDNDeployerSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
cdn *tcCdn.Client
|
||||
}
|
||||
|
||||
func New(config *TencentCloudCDNDeployerConfig) (*TencentCloudCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *TencentCloudCDNDeployerConfig, logger deployer.Logger) (*TencentCloudCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
clients, err := createSdkClients(config.SecretId, config.SecretKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: config.SecretId,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCloudCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 获取待部署的 CDN 实例
|
||||
// 如果是泛域名,根据证书匹配 CDN 实例
|
||||
instanceIds := make([]string, 0)
|
||||
if strings.HasPrefix(d.config.Domain, "*.") {
|
||||
domains, err := d.getDomainsByCertificateId(upres.CertId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instanceIds = domains
|
||||
} else {
|
||||
instanceIds = append(instanceIds, d.config.Domain)
|
||||
}
|
||||
|
||||
// 跳过已部署的 CDN 实例
|
||||
if len(instanceIds) > 0 {
|
||||
deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
temp := make([]string, 0)
|
||||
for _, instanceId := range instanceIds {
|
||||
if !slices.Contains(deployedDomains, instanceId) {
|
||||
temp = append(temp, instanceId)
|
||||
}
|
||||
}
|
||||
instanceIds = temp
|
||||
}
|
||||
|
||||
if len(instanceIds) == 0 {
|
||||
d.logger.Logt("已部署过或没有要部署的 CDN 实例")
|
||||
} else {
|
||||
// 证书部署到 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(instanceIds)
|
||||
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCDNDeployer) getDomainsByCertificateId(cloudCertId string) ([]string, error) {
|
||||
// 获取证书中的可用域名
|
||||
// REF: https://cloud.tencent.com/document/product/228/42491
|
||||
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
|
||||
describeCertDomainsReq.CertId = common.StringPtr(cloudCertId)
|
||||
describeCertDomainsReq.Product = common.StringPtr("cdn")
|
||||
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeCertDomainsResp.Response.Domains != nil {
|
||||
for _, domain := range describeCertDomainsResp.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCDNDeployer) getDeployedDomainsByCertificateId(cloudCertId string) ([]string, error) {
|
||||
// 根据证书查询关联 CDN 域名
|
||||
// REF: https://cloud.tencent.com/document/product/400/62674
|
||||
describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest()
|
||||
describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{cloudCertId})
|
||||
describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn")
|
||||
describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'")
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeDeployedResourcesResp.Response.DeployedResources != nil {
|
||||
for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources {
|
||||
for _, resource := range deployedResource.Resources {
|
||||
domains = append(domains, *resource)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func createSdkClients(secretId, secretKey string) (*tencentCloudCDNDeployerSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tencentCloudCDNDeployerSdkClients{
|
||||
ssl: sslClient,
|
||||
cdn: cdnClient,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package tencentcloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fSecretId string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v tencentcloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_SECRETID="your-secret-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudCDNDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package tencentcloudclb
|
||||
|
||||
type DeployResourceType string
|
||||
|
||||
const (
|
||||
// 资源类型:通过 SSL 服务部署到云资源实例。
|
||||
DEPLOY_RESOURCE_USE_SSLDEPLOY = DeployResourceType("ssl-deploy")
|
||||
// 资源类型:部署到指定负载均衡器。
|
||||
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
|
||||
// 资源类型:部署到指定监听器。
|
||||
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
|
||||
// 资源类型:部署到指定转发规则域名。
|
||||
DEPLOY_RESOURCE_RULEDOMAIN = DeployResourceType("ruledomain")
|
||||
)
|
||||
@@ -0,0 +1,305 @@
|
||||
package tencentcloudclb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCloudCLBDeployerConfig struct {
|
||||
// 腾讯云 SecretId。
|
||||
SecretId string `json:"secretId"`
|
||||
// 腾讯云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 腾讯云地域。
|
||||
Region string `json:"region"`
|
||||
// 部署资源类型。
|
||||
ResourceType DeployResourceType `json:"resourceType"`
|
||||
// 负载均衡器 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY]、[DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
|
||||
LoadbalancerId string `json:"loadbalancerId,omitempty"`
|
||||
// 负载均衡监听 ID。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY]、[DEPLOY_RESOURCE_LOADBALANCER]、[DEPLOY_RESOURCE_LISTENER]、[DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
|
||||
ListenerId string `json:"listenerId,omitempty"`
|
||||
// SNI 域名或七层转发规则域名(支持泛域名)。
|
||||
// 部署资源类型为 [DEPLOY_RESOURCE_SSLDEPLOY] 时选填;部署资源类型为 [DEPLOY_RESOURCE_RULEDOMAIN] 时必填。
|
||||
Domain string `json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
type TencentCloudCLBDeployer struct {
|
||||
config *TencentCloudCLBDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClients *wSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*TencentCloudCLBDeployer)(nil)
|
||||
|
||||
type wSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
clb *tcClb.Client
|
||||
}
|
||||
|
||||
func New(config *TencentCloudCLBDeployerConfig) (*TencentCloudCLBDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *TencentCloudCLBDeployerConfig, logger deployer.Logger) (*TencentCloudCLBDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
clients, err := createSdkClients(config.SecretId, config.SecretKey, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: config.SecretId,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCloudCLBDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCLBDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 根据部署资源类型决定部署方式
|
||||
switch d.config.ResourceType {
|
||||
case DEPLOY_RESOURCE_USE_SSLDEPLOY:
|
||||
if err := d.deployToInstanceUseSsl(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LOADBALANCER:
|
||||
if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_LISTENER:
|
||||
if err := d.deployToListener(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
case DEPLOY_RESOURCE_RULEDOMAIN:
|
||||
if err := d.deployToRuleDomain(ctx, upres.CertId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCLBDeployer) deployToInstanceUseSsl(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 证书部署到 CLB 实例
|
||||
// REF: https://cloud.tencent.com/document/product/400/91667
|
||||
deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest()
|
||||
deployCertificateInstanceReq.CertificateId = common.StringPtr(cloudCertId)
|
||||
deployCertificateInstanceReq.ResourceType = common.StringPtr("clb")
|
||||
deployCertificateInstanceReq.Status = common.Int64Ptr(1)
|
||||
if d.config.Domain == "" {
|
||||
// 未开启 SNI,只需指定到监听器
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", d.config.LoadbalancerId, d.config.ListenerId)})
|
||||
} else {
|
||||
// 开启 SNI,需指定到域名(支持泛域名)
|
||||
deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", d.config.LoadbalancerId, d.config.ListenerId, d.config.Domain)})
|
||||
}
|
||||
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCLBDeployer) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
|
||||
listenerIds := make([]string, 0)
|
||||
|
||||
// 查询监听器列表
|
||||
// REF: https://cloud.tencent.com/document/api/214/30686
|
||||
describeListenersReq := tcClb.NewDescribeListenersRequest()
|
||||
describeListenersReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)
|
||||
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
|
||||
}
|
||||
|
||||
listenerIds = append(listenerIds, *listener.ListenerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到负载均衡器下的监听器", listenerIds)
|
||||
|
||||
// 批量更新监听器证书
|
||||
if len(listenerIds) > 0 {
|
||||
var errs []error
|
||||
|
||||
for _, listenerId := range listenerIds {
|
||||
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, listenerId, cloudCertId); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCLBDeployer) deployToListener(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
|
||||
// 更新监听器证书
|
||||
if err := d.modifyListenerCertificate(ctx, d.config.LoadbalancerId, d.config.ListenerId, cloudCertId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCLBDeployer) deployToRuleDomain(ctx context.Context, cloudCertId string) error {
|
||||
if d.config.LoadbalancerId == "" {
|
||||
return errors.New("config `loadbalancerId` is required")
|
||||
}
|
||||
if d.config.ListenerId == "" {
|
||||
return errors.New("config `listenerId` is required")
|
||||
}
|
||||
if d.config.Domain == "" {
|
||||
return errors.New("config `domain` is required")
|
||||
}
|
||||
|
||||
// 修改负载均衡七层监听器转发规则的域名级别属性
|
||||
// REF: https://cloud.tencent.com/document/api/214/38092
|
||||
modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest()
|
||||
modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(d.config.LoadbalancerId)
|
||||
modifyDomainAttributesReq.ListenerId = common.StringPtr(d.config.ListenerId)
|
||||
modifyDomainAttributesReq.Domain = common.StringPtr(d.config.Domain)
|
||||
modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{
|
||||
SSLMode: common.StringPtr("UNIDIRECTIONAL"),
|
||||
CertId: common.StringPtr(cloudCertId),
|
||||
}
|
||||
modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq)
|
||||
if err != nil {
|
||||
return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCLBDeployer) modifyListenerCertificate(ctx context.Context, cloudLoadbalancerId, cloudListenerId, cloudCertId string) error {
|
||||
// 查询监听器列表
|
||||
// REF: https://cloud.tencent.com/document/api/214/30686
|
||||
describeListenersReq := tcClb.NewDescribeListenersRequest()
|
||||
describeListenersReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)
|
||||
describeListenersReq.ListenerIds = common.StringPtrs([]string{cloudListenerId})
|
||||
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 {
|
||||
return errors.New("listener not found")
|
||||
}
|
||||
|
||||
d.logger.Logt("已查询到监听器属性", describeListenersResp.Response)
|
||||
|
||||
// 修改监听器属性
|
||||
// REF: https://cloud.tencent.com/document/product/214/30681
|
||||
modifyListenerReq := tcClb.NewModifyListenerRequest()
|
||||
modifyListenerReq.LoadBalancerId = common.StringPtr(cloudLoadbalancerId)
|
||||
modifyListenerReq.ListenerId = common.StringPtr(cloudListenerId)
|
||||
modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(cloudCertId)}
|
||||
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.logger.Logt("已修改监听器属性", modifyListenerResp.Response)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSdkClients(secretId, secretKey, region string) (*wSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
// 注意虽然官方文档中地域无需指定,但实际需要部署到 CLB 时必传
|
||||
sslClient, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wSdkClients{
|
||||
ssl: sslClient,
|
||||
clb: clbClient,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package tencentcloudclb_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-clb"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fSecretId string
|
||||
fSecretKey string
|
||||
fRegion string
|
||||
fLoadbalancerId string
|
||||
fListenerId string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
|
||||
flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v tencentcloud_clb_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_SECRETID="your-secret-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_REGION="ap-guangzhou" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_LOADBALANCERID="your-clb-lb-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_LISTENERID="your-clb-lbl-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCLB_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy_UseSslDeploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_USE_SSLDEPLOY,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
ListenerId: fListenerId,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToLoadbalancer", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LOADBALANCER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToListener", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_LISTENER,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
ListenerId: fListenerId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
|
||||
t.Run("Deploy_ToRuleDomain", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
|
||||
fmt.Sprintf("LISTENERID: %v", fListenerId),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudCLBDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Region: fRegion,
|
||||
ResourceType: provider.DEPLOY_RESOURCE_RULEDOMAIN,
|
||||
LoadbalancerId: fLoadbalancerId,
|
||||
ListenerId: fListenerId,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package tencentcloudcdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
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/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCloudCOSDeployerConfig struct {
|
||||
// 腾讯云 SecretId。
|
||||
SecretId string `json:"secretId"`
|
||||
// 腾讯云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 腾讯云地域。
|
||||
Region string `json:"region"`
|
||||
// 存储桶名。
|
||||
Bucket string `json:"bucket"`
|
||||
// 自定义域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type TencentCloudCOSDeployer struct {
|
||||
config *TencentCloudCOSDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *tcSsl.Client
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*TencentCloudCOSDeployer)(nil)
|
||||
|
||||
func New(config *TencentCloudCOSDeployerConfig) (*TencentCloudCOSDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *TencentCloudCOSDeployerConfig, logger deployer.Logger) (*TencentCloudCOSDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client, err := createSdkClient(config.SecretId, config.SecretKey, config.Region)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: config.SecretId,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCloudCOSDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudCOSDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.Bucket == "" {
|
||||
return nil, errors.New("config `bucket` is required")
|
||||
}
|
||||
if d.config.Domain == "" {
|
||||
return nil, errors.New("config `domain` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", 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", d.config.Region, d.config.Bucket, d.config.Domain)})
|
||||
deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClient(secretId, secretKey, region string) (*tcSsl.Client, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package tencentcloudcdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cos"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fSecretId string
|
||||
fSecretKey string
|
||||
fRegion string
|
||||
fBucket string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fRegion, argsPrefix+"REGION", "", "")
|
||||
flag.StringVar(&fBucket, argsPrefix+"BUCKET", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v tencentcloud_cos_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_SECRETID="your-secret-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_REGION="ap-guangzhou" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_BUCKET="your-cos-bucket" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDCOS_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("REGION: %v", fRegion),
|
||||
fmt.Sprintf("BUCKET: %v", fBucket),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudCOSDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Region: fRegion,
|
||||
Bucket: fBucket,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package tencentcloudecdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
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"
|
||||
tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCloudECDNDeployerConfig struct {
|
||||
// 腾讯云 SecretId。
|
||||
SecretId string `json:"secretId"`
|
||||
// 腾讯云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type TencentCloudECDNDeployer struct {
|
||||
config *TencentCloudECDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClients *wSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*TencentCloudECDNDeployer)(nil)
|
||||
|
||||
type wSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
cdn *tcCdn.Client
|
||||
}
|
||||
|
||||
func New(config *TencentCloudECDNDeployerConfig) (*TencentCloudECDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *TencentCloudECDNDeployerConfig, logger deployer.Logger) (*TencentCloudECDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
clients, err := createSdkClients(config.SecretId, config.SecretKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: config.SecretId,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCloudECDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudECDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 获取待部署的 CDN 实例
|
||||
// 如果是泛域名,根据证书匹配 CDN 实例
|
||||
instanceIds := make([]string, 0)
|
||||
if strings.HasPrefix(d.config.Domain, "*.") {
|
||||
domains, err := d.getDomainsByCertificateId(upres.CertId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instanceIds = domains
|
||||
} else {
|
||||
instanceIds = append(instanceIds, d.config.Domain)
|
||||
}
|
||||
|
||||
if len(instanceIds) == 0 {
|
||||
d.logger.Logt("已部署过或没有要部署的 ECDN 实例")
|
||||
} else {
|
||||
// 证书部署到 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(instanceIds)
|
||||
deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已部署证书到云资源实例", deployCertificateInstanceResp.Response)
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudECDNDeployer) getDomainsByCertificateId(cloudCertId string) ([]string, error) {
|
||||
// 获取证书中的可用域名
|
||||
// REF: https://cloud.tencent.com/document/product/228/42491
|
||||
describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest()
|
||||
describeCertDomainsReq.CertId = common.StringPtr(cloudCertId)
|
||||
describeCertDomainsReq.Product = common.StringPtr("ecdn")
|
||||
describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'")
|
||||
}
|
||||
|
||||
domains := make([]string, 0)
|
||||
if describeCertDomainsResp.Response.Domains != nil {
|
||||
for _, domain := range describeCertDomainsResp.Response.Domains {
|
||||
domains = append(domains, *domain)
|
||||
}
|
||||
}
|
||||
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func createSdkClients(secretId, secretKey string) (*wSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wSdkClients{
|
||||
ssl: sslClient,
|
||||
cdn: cdnClient,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package tencentcloudecdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fSecretId string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v tencentcloud_ecdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_SECRETID="your-secret-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDECDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudECDNDeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package tencentcloudeteo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
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"
|
||||
tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
|
||||
)
|
||||
|
||||
type TencentCloudTEODeployerConfig struct {
|
||||
// 腾讯云 SecretId。
|
||||
SecretId string `json:"secretId"`
|
||||
// 腾讯云 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 站点 ID。
|
||||
ZoneId string `json:"zoneId"`
|
||||
// 加速域名(不支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type TencentCloudTEODeployer struct {
|
||||
config *TencentCloudTEODeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClients *wSdkClients
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*TencentCloudTEODeployer)(nil)
|
||||
|
||||
type wSdkClients struct {
|
||||
ssl *tcSsl.Client
|
||||
teo *tcTeo.Client
|
||||
}
|
||||
|
||||
func New(config *TencentCloudTEODeployerConfig) (*TencentCloudTEODeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *TencentCloudTEODeployerConfig, logger deployer.Logger) (*TencentCloudTEODeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
clients, err := createSdkClients(config.SecretId, config.SecretKey)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create sdk clients")
|
||||
}
|
||||
|
||||
uploader, err := providerSsl.New(&providerSsl.TencentCloudSSLUploaderConfig{
|
||||
SecretId: config.SecretId,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &TencentCloudTEODeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClients: clients,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *TencentCloudTEODeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
if d.config.ZoneId == "" {
|
||||
return nil, xerrors.New("config `zoneId` is required")
|
||||
}
|
||||
|
||||
// 上传证书到 SSL
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
// 配置域名证书
|
||||
// REF: https://cloud.tencent.com/document/product/1552/80764
|
||||
modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest()
|
||||
modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId)
|
||||
modifyHostsCertificateReq.Mode = common.StringPtr("sslcert")
|
||||
modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{d.config.Domain})
|
||||
modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}}
|
||||
modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'")
|
||||
}
|
||||
|
||||
d.logger.Logt("已配置域名证书", modifyHostsCertificateResp.Response)
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
|
||||
func createSdkClients(secretId, secretKey string) (*wSdkClients, error) {
|
||||
credential := common.NewCredential(secretId, secretKey)
|
||||
|
||||
sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &wSdkClients{
|
||||
ssl: sslClient,
|
||||
teo: teoClient,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package tencentcloudeteo_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-teo"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fSecretId string
|
||||
fSecretKey string
|
||||
fZoneId string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v tencentcloud_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETID="your-secret-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_ZONEID="your-zone-id" \
|
||||
--CERTIMATE_DEPLOYER_TENCENTCLOUDETEO_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("SECRETID: %v", fSecretId),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("ZONEID: %v", fZoneId),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.TencentCloudTEODeployerConfig{
|
||||
SecretId: fSecretId,
|
||||
SecretKey: fSecretKey,
|
||||
ZoneId: fZoneId,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package volcenginecdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
veCdn "github.com/volcengine/volc-sdk-golang/service/cdn"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerCdn "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-cdn"
|
||||
)
|
||||
|
||||
type VolcEngineCDNDeployerConfig struct {
|
||||
// 火山引擎 AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// 火山引擎 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type VolcEngineCDNDeployer struct {
|
||||
config *VolcEngineCDNDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *veCdn.CDN
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*VolcEngineCDNDeployer)(nil)
|
||||
|
||||
func New(config *VolcEngineCDNDeployerConfig) (*VolcEngineCDNDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *VolcEngineCDNDeployerConfig, logger deployer.Logger) (*VolcEngineCDNDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := veCdn.NewInstance()
|
||||
client.Client.SetAccessKey(config.AccessKey)
|
||||
client.Client.SetSecretKey(config.SecretKey)
|
||||
|
||||
uploader, err := providerCdn.New(&providerCdn.VolcEngineCDNUploaderConfig{
|
||||
AccessKeyId: config.AccessKey,
|
||||
AccessKeySecret: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &VolcEngineCDNDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *VolcEngineCDNDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 CDN
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
domains := make([]string, 0)
|
||||
if strings.HasPrefix(d.config.Domain, "*.") {
|
||||
// 获取指定证书可关联的域名
|
||||
// REF: https://www.volcengine.com/docs/6454/125711
|
||||
describeCertConfigReq := &veCdn.DescribeCertConfigRequest{
|
||||
CertId: upres.CertId,
|
||||
}
|
||||
describeCertConfigResp, err := d.sdkClient.DescribeCertConfig(describeCertConfigReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertConfig'")
|
||||
}
|
||||
|
||||
if describeCertConfigResp.Result.CertNotConfig != nil {
|
||||
for i := range describeCertConfigResp.Result.CertNotConfig {
|
||||
domains = append(domains, describeCertConfigResp.Result.CertNotConfig[i].Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if describeCertConfigResp.Result.OtherCertConfig != nil {
|
||||
for i := range describeCertConfigResp.Result.OtherCertConfig {
|
||||
domains = append(domains, describeCertConfigResp.Result.OtherCertConfig[i].Domain)
|
||||
}
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
if len(describeCertConfigResp.Result.SpecifiedCertConfig) > 0 {
|
||||
// 所有可关联的域名都配置了该证书,跳过部署
|
||||
} else {
|
||||
return nil, xerrors.New("domain not found")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, d.config.Domain)
|
||||
}
|
||||
|
||||
if len(domains) > 0 {
|
||||
var errs []error
|
||||
|
||||
for _, domain := range domains {
|
||||
// 关联证书与加速域名
|
||||
// REF: https://www.volcengine.com/docs/6454/125712
|
||||
batchDeployCertReq := &veCdn.BatchDeployCertRequest{
|
||||
CertId: upres.CertId,
|
||||
Domain: domain,
|
||||
}
|
||||
batchDeployCertResp, err := d.sdkClient.BatchDeployCert(batchDeployCertReq)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
d.logger.Logt(fmt.Sprintf("已关联证书到域名 %s", domain), batchDeployCertResp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package volcenginecdn_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_VOLCENGINECDN_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v volcengine_cdn_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINECDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINECDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINECDN_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINECDN_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINECDN_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.VolcEngineCDNDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package volcenginelive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
veLive "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
providerLive "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-live"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
)
|
||||
|
||||
type VolcEngineLiveDeployerConfig struct {
|
||||
// 火山引擎 AccessKey。
|
||||
AccessKey string `json:"accessKey"`
|
||||
// 火山引擎 SecretKey。
|
||||
SecretKey string `json:"secretKey"`
|
||||
// 加速域名(支持泛域名)。
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
type VolcEngineLiveDeployer struct {
|
||||
config *VolcEngineLiveDeployerConfig
|
||||
logger deployer.Logger
|
||||
sdkClient *veLive.Live
|
||||
sslUploader uploader.Uploader
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*VolcEngineLiveDeployer)(nil)
|
||||
|
||||
func New(config *VolcEngineLiveDeployerConfig) (*VolcEngineLiveDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *VolcEngineLiveDeployerConfig, logger deployer.Logger) (*VolcEngineLiveDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
client := veLive.NewInstance()
|
||||
client.SetAccessKey(config.AccessKey)
|
||||
client.SetSecretKey(config.SecretKey)
|
||||
|
||||
uploader, err := providerLive.New(&providerLive.VolcEngineLiveUploaderConfig{
|
||||
AccessKeyId: config.AccessKey,
|
||||
AccessKeySecret: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to create ssl uploader")
|
||||
}
|
||||
|
||||
return &VolcEngineLiveDeployer{
|
||||
logger: logger,
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
sslUploader: uploader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *VolcEngineLiveDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
// 上传证书到 Live
|
||||
upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to upload certificate file")
|
||||
}
|
||||
|
||||
d.logger.Logt("certificate file uploaded", upres)
|
||||
|
||||
domains := make([]string, 0)
|
||||
if strings.HasPrefix(d.config.Domain, "*.") {
|
||||
listDomainDetailPageNum := int32(1)
|
||||
listDomainDetailPageSize := int32(1000)
|
||||
listDomainDetailTotal := 0
|
||||
for {
|
||||
// 查询域名列表
|
||||
// REF: https://www.volcengine.com/docs/6469/1186277#%E6%9F%A5%E8%AF%A2%E5%9F%9F%E5%90%8D%E5%88%97%E8%A1%A8
|
||||
listDomainDetailReq := &veLive.ListDomainDetailBody{
|
||||
PageNum: listDomainDetailPageNum,
|
||||
PageSize: listDomainDetailPageSize,
|
||||
}
|
||||
listDomainDetailResp, err := d.sdkClient.ListDomainDetail(ctx, listDomainDetailReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListDomainDetail'")
|
||||
}
|
||||
|
||||
if listDomainDetailResp.Result.DomainList != nil {
|
||||
for _, item := range listDomainDetailResp.Result.DomainList {
|
||||
// 仅匹配泛域名的下一级子域名
|
||||
wildcardDomain := strings.TrimPrefix(d.config.Domain, "*")
|
||||
if strings.HasSuffix(item.Domain, wildcardDomain) && !strings.Contains(strings.TrimSuffix(item.Domain, wildcardDomain), ".") {
|
||||
domains = append(domains, item.Domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listDomainDetailLen := len(listDomainDetailResp.Result.DomainList)
|
||||
if listDomainDetailLen < int(listDomainDetailPageSize) || int(listDomainDetailResp.Result.Total) <= listDomainDetailTotal+listDomainDetailLen {
|
||||
break
|
||||
} else {
|
||||
listDomainDetailPageNum++
|
||||
listDomainDetailTotal += listDomainDetailLen
|
||||
}
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
return nil, xerrors.Errorf("未查询到匹配的域名: %s", d.config.Domain)
|
||||
}
|
||||
} else {
|
||||
domains = append(domains, d.config.Domain)
|
||||
}
|
||||
|
||||
if len(domains) > 0 {
|
||||
var errs []error
|
||||
|
||||
for _, domain := range domains {
|
||||
// 绑定证书
|
||||
// REF: https://www.volcengine.com/docs/6469/1186278#%E7%BB%91%E5%AE%9A%E8%AF%81%E4%B9%A6
|
||||
bindCertReq := &veLive.BindCertBody{
|
||||
ChainID: upres.CertId,
|
||||
Domain: domain,
|
||||
HTTPS: cast.BoolPtr(true),
|
||||
}
|
||||
bindCertResp, err := d.sdkClient.BindCert(ctx, bindCertReq)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
d.logger.Logt(fmt.Sprintf("已绑定证书到域名 %s", domain), bindCertResp)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errors.Join(errs...)
|
||||
}
|
||||
}
|
||||
|
||||
return &deployer.DeployResult{}, nil
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package volcenginelive_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fAccessKey string
|
||||
fSecretKey string
|
||||
fDomain string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_VOLCENGINELIVE_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "")
|
||||
flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
|
||||
flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v volcengine_live_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_ACCESSKEY="your-access-key" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_SECRETKEY="your-secret-key" \
|
||||
--CERTIMATE_DEPLOYER_VOLCENGINELIVE_DOMAIN="example.com"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("ACCESSKEY: %v", fAccessKey),
|
||||
fmt.Sprintf("SECRETKEY: %v", fSecretKey),
|
||||
fmt.Sprintf("DOMAIN: %v", fDomain),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.VolcEngineLiveDeployerConfig{
|
||||
AccessKey: fAccessKey,
|
||||
SecretKey: fSecretKey,
|
||||
Domain: fDomain,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
85
internal/pkg/core/deployer/providers/webhook/webhook.go
Normal file
85
internal/pkg/core/deployer/providers/webhook/webhook.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package webhook
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/deployer"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
xhttp "github.com/usual2970/certimate/internal/utils/http"
|
||||
)
|
||||
|
||||
type WebhookDeployerConfig struct {
|
||||
// Webhook URL。
|
||||
Url string `json:"url"`
|
||||
// Webhook 变量字典。
|
||||
Variables map[string]string `json:"variables,omitempty"`
|
||||
}
|
||||
|
||||
type WebhookDeployer struct {
|
||||
config *WebhookDeployerConfig
|
||||
logger deployer.Logger
|
||||
}
|
||||
|
||||
var _ deployer.Deployer = (*WebhookDeployer)(nil)
|
||||
|
||||
func New(config *WebhookDeployerConfig) (*WebhookDeployer, error) {
|
||||
return NewWithLogger(config, deployer.NewNilLogger())
|
||||
}
|
||||
|
||||
func NewWithLogger(config *WebhookDeployerConfig, logger deployer.Logger) (*WebhookDeployer, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
if logger == nil {
|
||||
return nil, errors.New("logger is nil")
|
||||
}
|
||||
|
||||
return &WebhookDeployer{
|
||||
config: config,
|
||||
logger: logger,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type webhookData struct {
|
||||
SubjectAltNames string `json:"subjectAltNames"`
|
||||
Certificate string `json:"certificate"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
Variables map[string]string `json:"variables"`
|
||||
}
|
||||
|
||||
func (d *WebhookDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) {
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to parse x509")
|
||||
}
|
||||
|
||||
data := &webhookData{
|
||||
SubjectAltNames: strings.Join(certX509.DNSNames, ","),
|
||||
Certificate: certPem,
|
||||
PrivateKey: privkeyPem,
|
||||
Variables: d.config.Variables,
|
||||
}
|
||||
body, _ := json.Marshal(data)
|
||||
resp, err := xhttp.Req(d.config.Url, http.MethodPost, bytes.NewReader(body), map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to send webhook request")
|
||||
}
|
||||
|
||||
d.logger.Logt("Webhook Response", string(resp))
|
||||
|
||||
return &deployer.DeployResult{
|
||||
DeploymentData: map[string]any{
|
||||
"responseText": string(resp),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
65
internal/pkg/core/deployer/providers/webhook/webhook_test.go
Normal file
65
internal/pkg/core/deployer/providers/webhook/webhook_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package webhook_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook"
|
||||
)
|
||||
|
||||
var (
|
||||
fInputCertPath string
|
||||
fInputKeyPath string
|
||||
fUrl string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_DEPLOYER_WEBHOOK_"
|
||||
|
||||
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
|
||||
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
|
||||
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v webhook_test.go -args \
|
||||
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTCERTPATH="/path/to/your-input-cert.pem" \
|
||||
--CERTIMATE_DEPLOYER_WEBHOOK_INPUTKEYPATH="/path/to/your-input-key.pem" \
|
||||
--CERTIMATE_DEPLOYER_WEBHOOK_URL="https://example.com/your-webhook-url"
|
||||
*/
|
||||
func TestDeploy(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Deploy", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
|
||||
fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
|
||||
fmt.Sprintf("URL: %v", fUrl),
|
||||
}, "\n"))
|
||||
|
||||
deployer, err := provider.New(&provider.WebhookDeployerConfig{
|
||||
Url: fUrl,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
fInputCertData, _ := os.ReadFile(fInputCertPath)
|
||||
fInputKeyData, _ := os.ReadFile(fInputKeyPath)
|
||||
res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
64
internal/pkg/core/notifier/providers/bark/bark_test.go
Normal file
64
internal/pkg/core/notifier/providers/bark/bark_test.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package bark_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/bark"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fServerUrl string
|
||||
fDeviceKey string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_BARK_"
|
||||
|
||||
flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
|
||||
flag.StringVar(&fDeviceKey, argsPrefix+"DEVICEKEY", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v bark_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_BARK_SERVERURL="https://example.com/your-server-url" \
|
||||
--CERTIMATE_NOTIFIER_BARK_DEVICEKEY="your-device-key"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("SERVERURL: %v", fServerUrl),
|
||||
fmt.Sprintf("DEVICEKEY: %v", fDeviceKey),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.New(&provider.BarkNotifierConfig{
|
||||
ServerUrl: fServerUrl,
|
||||
DeviceKey: fDeviceKey,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package dingtalk_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/dingtalk"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fAccessToken string
|
||||
fSecret string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_DINGTALK_"
|
||||
|
||||
flag.StringVar(&fAccessToken, argsPrefix+"ACCESSTOKEN", "", "")
|
||||
flag.StringVar(&fSecret, argsPrefix+"SECRET", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v dingtalk_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_DINGTALK_URL="https://example.com/your-webhook-url"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("ACCESSTOKEN: %v", fAccessToken),
|
||||
fmt.Sprintf("SECRET: %v", fSecret),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.New(&provider.DingTalkNotifierConfig{
|
||||
AccessToken: fAccessToken,
|
||||
Secret: fSecret,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -1,51 +1,89 @@
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
notifierEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fSmtpHost string
|
||||
fSmtpPort int
|
||||
fSmtpTLS bool
|
||||
fUsername string
|
||||
fPassword string
|
||||
fSenderAddress string
|
||||
fReceiverAddress string
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_EMAIL_"
|
||||
|
||||
flag.StringVar(&fSmtpHost, argsPrefix+"SMTPHOST", "", "")
|
||||
flag.IntVar(&fSmtpPort, argsPrefix+"SMTPPORT", 0, "")
|
||||
flag.BoolVar(&fSmtpTLS, argsPrefix+"SMTPTLS", false, "")
|
||||
flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "")
|
||||
flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "")
|
||||
flag.StringVar(&fSenderAddress, argsPrefix+"SENDERADDRESS", "", "")
|
||||
flag.StringVar(&fReceiverAddress, argsPrefix+"RECEIVERADDRESS", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
|
||||
CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
|
||||
CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
|
||||
CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
|
||||
CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
|
||||
CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
|
||||
CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com" \
|
||||
go test -v -run TestNotify email_test.go
|
||||
go test -v email_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_SMTPHOST="smtp.example.com" \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_SMTPPORT=465 \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_SMTPTLS=true \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_USERNAME="your-username" \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_PASSWORD="your-password" \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS="sender@example.com" \
|
||||
--CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS="receiver@example.com"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
smtpPort, err := strconv.ParseInt(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPPORT"), 10, 32)
|
||||
if err != nil {
|
||||
t.Errorf("invalid envvar: %+v", err)
|
||||
panic(err)
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
smtpTLS, err := strconv.ParseBool(os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPTLS"))
|
||||
if err != nil {
|
||||
t.Errorf("invalid envvar: %+v", err)
|
||||
panic(err)
|
||||
}
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("SMTPHOST: %v", fSmtpHost),
|
||||
fmt.Sprintf("SMTPPORT: %v", fSmtpPort),
|
||||
fmt.Sprintf("SMTPTLS: %v", fSmtpTLS),
|
||||
fmt.Sprintf("USERNAME: %v", fUsername),
|
||||
fmt.Sprintf("PASSWORD: %v", fPassword),
|
||||
fmt.Sprintf("SENDERADDRESS: %v", fSenderAddress),
|
||||
fmt.Sprintf("RECEIVERADDRESS: %v", fReceiverAddress),
|
||||
}, "\n"))
|
||||
|
||||
res, err := notifierEmail.New(¬ifierEmail.EmailNotifierConfig{
|
||||
SmtpHost: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SMTPHOST"),
|
||||
SmtpPort: int32(smtpPort),
|
||||
SmtpTLS: smtpTLS,
|
||||
Username: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_USERNAME"),
|
||||
Password: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_PASSWORD"),
|
||||
SenderAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_SENDERADDRESS"),
|
||||
ReceiverAddress: os.Getenv("CERTIMATE_NOTIFIER_EMAIL_RECEIVERADDRESS"),
|
||||
notifier, err := provider.New(&provider.EmailNotifierConfig{
|
||||
SmtpHost: fSmtpHost,
|
||||
SmtpPort: int32(fSmtpPort),
|
||||
SmtpTLS: fSmtpTLS,
|
||||
Username: fUsername,
|
||||
Password: fPassword,
|
||||
SenderAddress: fSenderAddress,
|
||||
ReceiverAddress: fReceiverAddress,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("invalid envvar: %+v", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
t.Logf("notify result: %v", res)
|
||||
}
|
||||
|
||||
57
internal/pkg/core/notifier/providers/lark/lark_test.go
Normal file
57
internal/pkg/core/notifier/providers/lark/lark_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package lark_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var fWebhookUrl string
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_LARK_"
|
||||
|
||||
flag.StringVar(&fWebhookUrl, argsPrefix+"WEBHOOKURL", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v lark_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_LARK_WEBHOOKURL="https://example.com/your-webhook-url"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("WEBHOOKURL: %v", fWebhookUrl),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.New(&provider.LarkNotifierConfig{
|
||||
WebhookUrl: fWebhookUrl,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package serverchan_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var fUrl string
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_SERVERCHAN_"
|
||||
|
||||
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v serverchan_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_SERVERCHAN_URL="https://example.com/your-webhook-url" \
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("URL: %v", fUrl),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.New(&provider.ServerChanNotifierConfig{
|
||||
Url: fUrl,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package telegram_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var (
|
||||
fApiToken string
|
||||
fChartId int64
|
||||
)
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_TELEGRAM_"
|
||||
|
||||
flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "")
|
||||
flag.Int64Var(&fChartId, argsPrefix+"CHATID", 0, "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v telegram_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_TELEGRAM_APITOKEN="your-api-token" \
|
||||
--CERTIMATE_NOTIFIER_TELEGRAM_CHATID=123456
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("APITOKEN: %v", fApiToken),
|
||||
fmt.Sprintf("CHATID: %v", fChartId),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.New(&provider.TelegramNotifierConfig{
|
||||
ApiToken: fApiToken,
|
||||
ChatId: fChartId,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
57
internal/pkg/core/notifier/providers/webhook/webhook_test.go
Normal file
57
internal/pkg/core/notifier/providers/webhook/webhook_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package webhook_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/webhook"
|
||||
)
|
||||
|
||||
const (
|
||||
mockSubject = "test_subject"
|
||||
mockMessage = "test_message"
|
||||
)
|
||||
|
||||
var fUrl string
|
||||
|
||||
func init() {
|
||||
argsPrefix := "CERTIMATE_NOTIFIER_WEBHOOK_"
|
||||
|
||||
flag.StringVar(&fUrl, argsPrefix+"URL", "", "")
|
||||
}
|
||||
|
||||
/*
|
||||
Shell command to run this test:
|
||||
|
||||
go test -v webhook_test.go -args \
|
||||
--CERTIMATE_NOTIFIER_WEBHOOK_URL="https://example.com/your-webhook-url"
|
||||
*/
|
||||
func TestNotify(t *testing.T) {
|
||||
flag.Parse()
|
||||
|
||||
t.Run("Notify", func(t *testing.T) {
|
||||
t.Log(strings.Join([]string{
|
||||
"args:",
|
||||
fmt.Sprintf("URL: %v", fUrl),
|
||||
}, "\n"))
|
||||
|
||||
notifier, err := provider.New(&provider.WebhookNotifierConfig{
|
||||
Url: fUrl,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res, err := notifier.Notify(context.Background(), mockSubject, mockMessage)
|
||||
if err != nil {
|
||||
t.Errorf("err: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("ok: %v", res)
|
||||
})
|
||||
}
|
||||
@@ -144,11 +144,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Cl
|
||||
region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/ssl-certificate/developer-reference/endpoints
|
||||
var endpoint string
|
||||
switch region {
|
||||
case "cn-hangzhou":
|
||||
@@ -156,9 +152,14 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Cl
|
||||
default:
|
||||
endpoint = fmt.Sprintf("cas.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := aliyunCas.NewClient(aConfig)
|
||||
config := &aliyunOpen.Config{
|
||||
Endpoint: tea.String(endpoint),
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
client, err := aliyunCas.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -121,11 +121,7 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Cl
|
||||
region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州
|
||||
}
|
||||
|
||||
aConfig := &aliyunOpen.Config{
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
// 接入点一览 https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-endpoint
|
||||
var endpoint string
|
||||
switch region {
|
||||
case
|
||||
@@ -137,9 +133,14 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Cl
|
||||
default:
|
||||
endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region)
|
||||
}
|
||||
aConfig.Endpoint = tea.String(endpoint)
|
||||
|
||||
client, err := aliyunSlb.NewClient(aConfig)
|
||||
config := &aliyunOpen.Config{
|
||||
Endpoint: tea.String(endpoint),
|
||||
AccessKeyId: tea.String(accessKeyId),
|
||||
AccessKeySecret: tea.String(accessKeySecret),
|
||||
}
|
||||
|
||||
client, err := aliyunSlb.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package bytepluscdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bpCdn "github.com/byteplus-sdk/byteplus-sdk-golang/service/cdn"
|
||||
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"
|
||||
)
|
||||
|
||||
type ByteplusCDNUploaderConfig struct {
|
||||
AccessKey string `json:"accessKey"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
}
|
||||
|
||||
type ByteplusCDNUploader struct {
|
||||
config *ByteplusCDNUploaderConfig
|
||||
sdkClient *bpCdn.CDN
|
||||
}
|
||||
|
||||
var _ uploader.Uploader = (*ByteplusCDNUploader)(nil)
|
||||
|
||||
func New(config *ByteplusCDNUploaderConfig) (*ByteplusCDNUploader, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
client := bpCdn.NewInstance()
|
||||
client.Client.SetAccessKey(config.AccessKey)
|
||||
client.Client.SetSecretKey(config.SecretKey)
|
||||
|
||||
return &ByteplusCDNUploader{
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *ByteplusCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询证书列表,避免重复上传
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-listcertinfo
|
||||
listCertInfoPageNum := int64(1)
|
||||
listCertInfoPageSize := int64(100)
|
||||
listCertInfoTotal := 0
|
||||
listCertInfoReq := &bpCdn.ListCertInfoRequest{
|
||||
PageNum: cast.Int64Ptr(listCertInfoPageNum),
|
||||
PageSize: cast.Int64Ptr(listCertInfoPageSize),
|
||||
Source: cast.StringPtr("cert_center"),
|
||||
}
|
||||
for {
|
||||
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.ListCertInfo'")
|
||||
}
|
||||
|
||||
if listCertInfoResp.Result.CertInfo != nil {
|
||||
for _, certDetail := range listCertInfoResp.Result.CertInfo {
|
||||
fingerprintSha1 := sha1.Sum(certX509.Raw)
|
||||
fingerprintSha256 := sha256.Sum256(certX509.Raw)
|
||||
isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certDetail.CertFingerprint.Sha1) &&
|
||||
strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certDetail.CertFingerprint.Sha256)
|
||||
// 如果已存在相同证书,直接返回已有的证书信息
|
||||
if isSameCert {
|
||||
return &uploader.UploadResult{
|
||||
CertId: certDetail.CertId,
|
||||
CertName: certDetail.Desc,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listCertInfoLen := len(listCertInfoResp.Result.CertInfo)
|
||||
if listCertInfoLen < int(listCertInfoPageSize) || int(listCertInfoResp.Result.Total) <= listCertInfoTotal+listCertInfoLen {
|
||||
break
|
||||
} else {
|
||||
listCertInfoPageNum++
|
||||
listCertInfoTotal += listCertInfoLen
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新证书名(需符合 BytePlus 命名规则)
|
||||
var certId, certName string
|
||||
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
|
||||
|
||||
// 上传新证书
|
||||
// REF: https://docs.byteplus.com/en/docs/byteplus-cdn/reference-addcertificate
|
||||
addCertificateReq := &bpCdn.AddCertificateRequest{
|
||||
Certificate: certPem,
|
||||
PrivateKey: privkeyPem,
|
||||
Source: cast.StringPtr("cert_center"),
|
||||
Desc: cast.StringPtr(certName),
|
||||
}
|
||||
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.AddCertificate'")
|
||||
}
|
||||
|
||||
certId = addCertificateResp.Result.CertId
|
||||
return &uploader.UploadResult{
|
||||
CertId: certId,
|
||||
CertName: certName,
|
||||
}, nil
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error)
|
||||
|
||||
func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
newCert, err := x509.ParseCertificateFromPEM(certPem)
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -83,12 +83,12 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri
|
||||
if certDetail.Certificate == certPem {
|
||||
isSameCert = true
|
||||
} else {
|
||||
cert, err := x509.ParseCertificateFromPEM(certDetail.Certificate)
|
||||
oldCertX509, err := x509.ParseCertificateFromPEM(certDetail.Certificate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
isSameCert = x509.EqualCertificate(cert, newCert)
|
||||
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
|
||||
}
|
||||
|
||||
// 如果已存在相同证书,直接返回已有的证书信息
|
||||
@@ -205,9 +205,6 @@ func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error
|
||||
}
|
||||
|
||||
client := hcIam.NewIamClient(hcClient)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
request := &hcIamModel.KeystoneListProjectsRequest{
|
||||
Name: ®ion,
|
||||
|
||||
@@ -92,12 +92,12 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri
|
||||
if *exportCertificateResp.Certificate == certPem {
|
||||
isSameCert = true
|
||||
} else {
|
||||
cert, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
|
||||
oldCertX509, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
isSameCert = x509.EqualCertificate(certX509, cert)
|
||||
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
|
||||
}
|
||||
|
||||
// 如果已存在相同证书,直接返回已有的证书信息
|
||||
|
||||
@@ -2,6 +2,7 @@ package volcenginecdn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
@@ -10,56 +11,57 @@ import (
|
||||
"time"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
veCdn "github.com/volcengine/volc-sdk-golang/service/cdn"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
"github.com/volcengine/volc-sdk-golang/service/cdn"
|
||||
)
|
||||
|
||||
type VolcengineCDNUploaderConfig struct {
|
||||
type VolcEngineCDNUploaderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type VolcengineCDNUploader struct {
|
||||
config *VolcengineCDNUploaderConfig
|
||||
sdkClient *cdn.CDN
|
||||
type VolcEngineCDNUploader struct {
|
||||
config *VolcEngineCDNUploaderConfig
|
||||
sdkClient *veCdn.CDN
|
||||
}
|
||||
|
||||
var _ uploader.Uploader = (*VolcengineCDNUploader)(nil)
|
||||
var _ uploader.Uploader = (*VolcEngineCDNUploader)(nil)
|
||||
|
||||
func New(config *VolcengineCDNUploaderConfig) (*VolcengineCDNUploader, error) {
|
||||
func New(config *VolcEngineCDNUploaderConfig) (*VolcEngineCDNUploader, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
instance := cdn.NewInstance()
|
||||
client := instance.Client
|
||||
client.SetAccessKey(config.AccessKeyId)
|
||||
client.SetSecretKey(config.AccessKeySecret)
|
||||
client := veCdn.NewInstance()
|
||||
client.Client.SetAccessKey(config.AccessKeyId)
|
||||
client.Client.SetSecretKey(config.AccessKeySecret)
|
||||
|
||||
return &VolcengineCDNUploader{
|
||||
return &VolcEngineCDNUploader{
|
||||
config: config,
|
||||
sdkClient: instance,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
func (u *VolcEngineCDNUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询证书列表,避免重复上传
|
||||
// REF: https://www.volcengine.com/docs/6454/125709
|
||||
pageNum := int64(1)
|
||||
pageSize := int64(100)
|
||||
certSource := "volc_cert_center"
|
||||
listCertInfoReq := &cdn.ListCertInfoRequest{
|
||||
PageNum: &pageNum,
|
||||
PageSize: &pageSize,
|
||||
Source: certSource,
|
||||
listCertInfoPageNum := int64(1)
|
||||
listCertInfoPageSize := int64(100)
|
||||
listCertInfoTotal := 0
|
||||
listCertInfoReq := &veCdn.ListCertInfoRequest{
|
||||
PageNum: cast.Int64Ptr(listCertInfoPageNum),
|
||||
PageSize: cast.Int64Ptr(listCertInfoPageSize),
|
||||
Source: "volc_cert_center",
|
||||
}
|
||||
searchTotal := 0
|
||||
for {
|
||||
listCertInfoResp, err := u.sdkClient.ListCertInfo(listCertInfoReq)
|
||||
if err != nil {
|
||||
@@ -68,8 +70,10 @@ func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, priv
|
||||
|
||||
if listCertInfoResp.Result.CertInfo != nil {
|
||||
for _, certDetail := range listCertInfoResp.Result.CertInfo {
|
||||
hash := sha256.Sum256(certX509.Raw)
|
||||
isSameCert := strings.EqualFold(hex.EncodeToString(hash[:]), certDetail.CertFingerprint.Sha256)
|
||||
fingerprintSha1 := sha1.Sum(certX509.Raw)
|
||||
fingerprintSha256 := sha256.Sum256(certX509.Raw)
|
||||
isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certDetail.CertFingerprint.Sha1) &&
|
||||
strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certDetail.CertFingerprint.Sha256)
|
||||
// 如果已存在相同证书,直接返回已有的证书信息
|
||||
if isSameCert {
|
||||
return &uploader.UploadResult{
|
||||
@@ -80,24 +84,26 @@ func (u *VolcengineCDNUploader) Upload(ctx context.Context, certPem string, priv
|
||||
}
|
||||
}
|
||||
|
||||
searchTotal += len(listCertInfoResp.Result.CertInfo)
|
||||
if int(listCertInfoResp.Result.Total) > searchTotal {
|
||||
pageNum++
|
||||
} else {
|
||||
listCertInfoLen := len(listCertInfoResp.Result.CertInfo)
|
||||
if listCertInfoLen < int(listCertInfoPageSize) || int(listCertInfoResp.Result.Total) <= listCertInfoTotal+listCertInfoLen {
|
||||
break
|
||||
} else {
|
||||
listCertInfoPageNum++
|
||||
listCertInfoTotal += listCertInfoLen
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 生成新证书名(需符合火山引擎命名规则)
|
||||
var certId, certName string
|
||||
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
|
||||
|
||||
// 上传新证书
|
||||
// REF: https://www.volcengine.com/docs/6454/1245763
|
||||
addCertificateReq := &cdn.AddCertificateRequest{
|
||||
addCertificateReq := &veCdn.AddCertificateRequest{
|
||||
Certificate: certPem,
|
||||
PrivateKey: privkeyPem,
|
||||
Source: &certSource,
|
||||
Desc: &certName,
|
||||
Source: cast.StringPtr("volc_cert_center"),
|
||||
Desc: cast.StringPtr(certName),
|
||||
}
|
||||
addCertificateResp, err := u.sdkClient.AddCertificate(addCertificateReq)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,77 +8,79 @@ import (
|
||||
"time"
|
||||
|
||||
xerrors "github.com/pkg/errors"
|
||||
veLive "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
|
||||
|
||||
"github.com/usual2970/certimate/internal/pkg/core/uploader"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/cast"
|
||||
"github.com/usual2970/certimate/internal/pkg/utils/x509"
|
||||
live "github.com/volcengine/volc-sdk-golang/service/live/v20230101"
|
||||
)
|
||||
|
||||
type VolcengineLiveUploaderConfig struct {
|
||||
type VolcEngineLiveUploaderConfig struct {
|
||||
AccessKeyId string `json:"accessKeyId"`
|
||||
AccessKeySecret string `json:"accessKeySecret"`
|
||||
}
|
||||
|
||||
type VolcengineLiveUploader struct {
|
||||
config *VolcengineLiveUploaderConfig
|
||||
sdkClient *live.Live
|
||||
type VolcEngineLiveUploader struct {
|
||||
config *VolcEngineLiveUploaderConfig
|
||||
sdkClient *veLive.Live
|
||||
}
|
||||
|
||||
var _ uploader.Uploader = (*VolcengineLiveUploader)(nil)
|
||||
var _ uploader.Uploader = (*VolcEngineLiveUploader)(nil)
|
||||
|
||||
func New(config *VolcengineLiveUploaderConfig) (*VolcengineLiveUploader, error) {
|
||||
func New(config *VolcEngineLiveUploaderConfig) (*VolcEngineLiveUploader, error) {
|
||||
if config == nil {
|
||||
return nil, errors.New("config is nil")
|
||||
}
|
||||
|
||||
client := live.NewInstance()
|
||||
client := veLive.NewInstance()
|
||||
client.SetAccessKey(config.AccessKeyId)
|
||||
client.SetSecretKey(config.AccessKeySecret)
|
||||
|
||||
return &VolcengineLiveUploader{
|
||||
return &VolcEngineLiveUploader{
|
||||
config: config,
|
||||
sdkClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (u *VolcengineLiveUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
func (u *VolcEngineLiveUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
|
||||
// 解析证书内容
|
||||
certX509, err := x509.ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 查询证书列表,避免重复上传
|
||||
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E8%AF%A2%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8
|
||||
listCertReq := &live.ListCertV2Body{}
|
||||
listCertReq := &veLive.ListCertV2Body{}
|
||||
listCertResp, err := u.sdkClient.ListCertV2(ctx, listCertReq)
|
||||
if err != nil {
|
||||
return nil, xerrors.Wrap(err, "failed to execute sdk request 'live.ListCertV2'")
|
||||
}
|
||||
|
||||
if listCertResp.Result.CertList != nil {
|
||||
for _, certDetail := range listCertResp.Result.CertList {
|
||||
|
||||
describeCertDetailSecretReq := &live.DescribeCertDetailSecretV2Body{
|
||||
ChainID: cast.StringPtr(certDetail.ChainID),
|
||||
}
|
||||
// 查询证书详细信息
|
||||
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85
|
||||
describeCertDetailSecretResp, detailErr := u.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)
|
||||
if detailErr != nil {
|
||||
describeCertDetailSecretReq := &veLive.DescribeCertDetailSecretV2Body{
|
||||
ChainID: cast.StringPtr(certDetail.ChainID),
|
||||
}
|
||||
describeCertDetailSecretResp, err := u.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var isSameCert bool
|
||||
certificate := strings.Join(describeCertDetailSecretResp.Result.SSL.Chain, "\n\n")
|
||||
if certificate == certPem {
|
||||
isSameCert = true
|
||||
} else {
|
||||
cert, err := x509.ParseCertificateFromPEM(certificate)
|
||||
oldCertX509, err := x509.ParseCertificateFromPEM(certificate)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
isSameCert = x509.EqualCertificate(cert, certX509)
|
||||
isSameCert = x509.EqualCertificate(certX509, oldCertX509)
|
||||
}
|
||||
|
||||
// 如果已存在相同证书,直接返回已有的证书信息
|
||||
if isSameCert {
|
||||
return &uploader.UploadResult{
|
||||
@@ -92,13 +94,14 @@ func (u *VolcengineLiveUploader) Upload(ctx context.Context, certPem string, pri
|
||||
// 生成新证书名(需符合火山引擎命名规则)
|
||||
var certId, certName string
|
||||
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
|
||||
|
||||
// 上传新证书
|
||||
// REF: https://www.volcengine.com/docs/6469/1186278#%E6%B7%BB%E5%8A%A0%E8%AF%81%E4%B9%A6
|
||||
createCertReq := &live.CreateCertBody{
|
||||
createCertReq := &veLive.CreateCertBody{
|
||||
CertName: &certName,
|
||||
UseWay: "https",
|
||||
ProjectName: cast.StringPtr("default"),
|
||||
Rsa: live.CreateCertBodyRsa{
|
||||
Rsa: veLive.CreateCertBodyRsa{
|
||||
Prikey: privkeyPem,
|
||||
Pubkey: certPem,
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ func GetValueAsString(dict map[string]any, key string) string {
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 字典中键对应的值。如果指定键不存在或者值的类型不是字符串,则返回默认值。
|
||||
// - 字典中键对应的值。如果指定键不存在、值的类型不是字符串或者值为零值,则返回默认值。
|
||||
func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue string) string {
|
||||
if dict == nil {
|
||||
return defaultValue
|
||||
@@ -30,7 +30,9 @@ func GetValueOrDefaultAsString(dict map[string]any, key string, defaultValue str
|
||||
|
||||
if value, ok := dict[key]; ok {
|
||||
if result, ok := value.(string); ok {
|
||||
return result
|
||||
if result != "" {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +59,7 @@ func GetValueAsInt32(dict map[string]any, key string) int32 {
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 32 位整数,则返回默认值。
|
||||
// - 字典中键对应的值。如果指定键不存在、值的类型不是 32 位整数或者值为零值,则返回默认值。
|
||||
func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int32) int32 {
|
||||
if dict == nil {
|
||||
return defaultValue
|
||||
@@ -65,13 +67,17 @@ func GetValueOrDefaultAsInt32(dict map[string]any, key string, defaultValue int3
|
||||
|
||||
if value, ok := dict[key]; ok {
|
||||
if result, ok := value.(int32); ok {
|
||||
return result
|
||||
if result != 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容字符串类型的值
|
||||
if str, ok := value.(string); ok {
|
||||
if result, err := strconv.ParseInt(str, 10, 32); err == nil {
|
||||
return int32(result)
|
||||
if result != 0 {
|
||||
return int32(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,7 +105,7 @@ func GetValueAsInt64(dict map[string]any, key string) int64 {
|
||||
// - defaultValue: 默认值。
|
||||
//
|
||||
// 出参:
|
||||
// - 字典中键对应的值。如果指定键不存在或者值的类型不是 64 位整数,则返回默认值。
|
||||
// - 字典中键对应的值。如果指定键不存在、值的类型不是 64 位整数或者值为零值,则返回默认值。
|
||||
func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int64) int64 {
|
||||
if dict == nil {
|
||||
return defaultValue
|
||||
@@ -107,13 +113,17 @@ func GetValueOrDefaultAsInt64(dict map[string]any, key string, defaultValue int6
|
||||
|
||||
if value, ok := dict[key]; ok {
|
||||
if result, ok := value.(int64); ok {
|
||||
return result
|
||||
if result != 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// 兼容字符串类型的值
|
||||
if str, ok := value.(string); ok {
|
||||
if result, err := strconv.ParseInt(str, 10, 64); err == nil {
|
||||
return result
|
||||
if result != 0 {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
87
internal/pkg/utils/x509/transformer.go
Normal file
87
internal/pkg/utils/x509/transformer.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package x509
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/pavlo-v-chernykh/keystore-go/v4"
|
||||
"software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
// 将 PEM 编码的证书字符串转换为 PFX 格式。
|
||||
//
|
||||
// 入参:
|
||||
// - certPem: 证书 PEM 内容。
|
||||
// - privkeyPem: 私钥 PEM 内容。
|
||||
// - pfxPassword: PFX 导出密码。
|
||||
//
|
||||
// 出参:
|
||||
// - data: PFX 格式的证书数据。
|
||||
// - err: 错误。
|
||||
func TransformCertificateFromPEMToPFX(certPem string, privkeyPem string, pfxPassword string) ([]byte, error) {
|
||||
cert, err := ParseCertificateFromPEM(certPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privkey, err := ParsePKCS1PrivateKeyFromPEM(privkeyPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pfxData, nil
|
||||
}
|
||||
|
||||
// 将 PEM 编码的证书字符串转换为 JKS 格式。
|
||||
//
|
||||
// 入参:
|
||||
// - certPem: 证书 PEM 内容。
|
||||
// - privkeyPem: 私钥 PEM 内容。
|
||||
// - jksAlias: JKS 别名。
|
||||
// - jksKeypass: JKS 密钥密码。
|
||||
// - jksStorepass: JKS 存储密码。
|
||||
//
|
||||
// 出参:
|
||||
// - data: JKS 格式的证书数据。
|
||||
// - err: 错误。
|
||||
func TransformCertificateFromPEMToJKS(certPem string, privkeyPem string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) {
|
||||
certBlock, _ := pem.Decode([]byte(certPem))
|
||||
if certBlock == nil {
|
||||
return nil, errors.New("failed to decode certificate PEM")
|
||||
}
|
||||
|
||||
privkeyBlock, _ := pem.Decode([]byte(privkeyPem))
|
||||
if privkeyBlock == nil {
|
||||
return nil, errors.New("failed to decode private key PEM")
|
||||
}
|
||||
|
||||
ks := keystore.New()
|
||||
entry := keystore.PrivateKeyEntry{
|
||||
CreationTime: time.Now(),
|
||||
PrivateKey: privkeyBlock.Bytes,
|
||||
CertificateChain: []keystore.Certificate{
|
||||
{
|
||||
Type: "X509",
|
||||
Content: certBlock.Bytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := ks.Store(&buf, []byte(jksStorepass)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import (
|
||||
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core"
|
||||
hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
|
||||
hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -21,6 +22,7 @@ func (c *Client) UploadDomainMultiCertificatesEx(request *UpdateDomainMultiCerti
|
||||
if resp, err := c.HcClient.Sync(request, requestDef); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return resp.(*UpdateDomainMultiCertificatesExResponse), nil
|
||||
temp := resp.(*hcCdnModel.UpdateDomainMultiCertificatesResponse)
|
||||
return &UpdateDomainMultiCertificatesExResponse{UpdateDomainMultiCertificatesResponse: *temp}, nil
|
||||
}
|
||||
}
|
||||
|
||||
107
migrations/1731872250_add_byteplus.go
Normal file
107
migrations/1731872250_add_byteplus.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
edit_configType := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "hwy7m03o",
|
||||
"name": "configType",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"aliyun",
|
||||
"tencent",
|
||||
"huaweicloud",
|
||||
"qiniu",
|
||||
"aws",
|
||||
"cloudflare",
|
||||
"namesilo",
|
||||
"godaddy",
|
||||
"pdns",
|
||||
"httpreq",
|
||||
"local",
|
||||
"ssh",
|
||||
"webhook",
|
||||
"k8s",
|
||||
"baiducloud",
|
||||
"dogecloud",
|
||||
"volcengine",
|
||||
"byteplus"
|
||||
]
|
||||
}
|
||||
}`), edit_configType); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(edit_configType)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("4yzbv8urny5ja1e")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
edit_configType := &schema.SchemaField{}
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "hwy7m03o",
|
||||
"name": "configType",
|
||||
"type": "select",
|
||||
"required": false,
|
||||
"presentable": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"maxSelect": 1,
|
||||
"values": [
|
||||
"aliyun",
|
||||
"tencent",
|
||||
"huaweicloud",
|
||||
"qiniu",
|
||||
"aws",
|
||||
"cloudflare",
|
||||
"namesilo",
|
||||
"godaddy",
|
||||
"pdns",
|
||||
"httpreq",
|
||||
"local",
|
||||
"ssh",
|
||||
"webhook",
|
||||
"k8s",
|
||||
"baiducloud",
|
||||
"dogecloud",
|
||||
"volcengine"
|
||||
]
|
||||
}
|
||||
}`), edit_configType); err != nil {
|
||||
return err
|
||||
}
|
||||
collection.Schema.AddField(edit_configType)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
4
ui/public/imgs/providers/byteplus.svg
Normal file
4
ui/public/imgs/providers/byteplus.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
|
||||
<path d="M0 0 C0.33 12.87 0.66 25.74 1 39 C4.63 36.03 8.26 33.06 12 30 C12.99 29.67 13.98 29.34 15 29 C15 28.34 15 27.68 15 27 C16.6953125 25.4765625 16.6953125 25.4765625 19.125 23.625 C24.36818245 19.55364629 29.38002727 15.25969115 34.35205078 10.86181641 C38.78276777 7 38.78276777 7 41 7 C41 6.34 41 5.68 41 5 C45.35294118 1 45.35294118 1 49 1 C49.02312269 12.19573244 49.04091471 23.39145924 49.05181217 34.58721066 C49.05704116 39.78528115 49.0641391 44.98333536 49.07543945 50.18139648 C49.08626776 55.19342268 49.09227671 60.20543318 49.09487724 65.21747017 C49.09673159 67.13412282 49.1003524 69.05077457 49.10573006 70.96742058 C49.11293975 73.64228072 49.11399097 76.31707514 49.11352539 78.99194336 C49.11712067 79.7925116 49.12071594 80.59307983 49.12442017 81.41790771 C49.11405216 86.88594784 49.11405216 86.88594784 48 88 C46.23140807 88.09946453 44.45888549 88.13080217 42.6875 88.1328125 C41.61242187 88.13410156 40.53734375 88.13539063 39.4296875 88.13671875 C38.29789062 88.13285156 37.16609375 88.12898438 36 88.125 C34.86820312 88.12886719 33.73640625 88.13273437 32.5703125 88.13671875 C31.49523437 88.13542969 30.42015625 88.13414062 29.3125 88.1328125 C28.31863281 88.13168457 27.32476562 88.13055664 26.30078125 88.12939453 C24 88 24 88 23 87 C22.84174742 85.00217874 22.74880829 82.99911166 22.68359375 80.99609375 C22.64169922 79.78115234 22.59980469 78.56621094 22.55664062 77.31445312 C22.51732422 76.03505859 22.47800781 74.75566406 22.4375 73.4375 C22.39431641 72.15423828 22.35113281 70.87097656 22.30664062 69.54882812 C22.200152 66.36600147 22.09814038 63.18309326 22 60 C21.24074219 60.63808594 20.48148437 61.27617188 19.69921875 61.93359375 C18.70535156 62.75988281 17.71148437 63.58617187 16.6875 64.4375 C15.70136719 65.26121094 14.71523437 66.08492188 13.69921875 66.93359375 C11 69 11 69 8 70 C8 70.66 8 71.32 8 72 C6.51261765 73.35382149 4.95569449 74.63139886 3.375 75.875 C-1.2643158 79.58405196 -5.81455578 83.3707187 -10.3125 87.25 C-11.99985624 88.70517299 -13.68754171 90.15975891 -15.38134766 91.60742188 C-16.47861169 92.55143304 -17.5666265 93.50628271 -18.64599609 94.47070312 C-22.67291886 98 -22.67291886 98 -26 98 C-26 84.8 -26 71.6 -26 58 C-29.3 60.64 -32.6 63.28 -36 66 C-36.66 66 -37.32 66 -38 66 C-38 66.66 -38 67.32 -38 68 C-39.71332754 69.58533702 -41.49689047 71.09541215 -43.3125 72.5625 C-44.29863281 73.36816406 -45.28476563 74.17382812 -46.30078125 75.00390625 C-49 77 -49 77 -52 78 C-52 78.66 -52 79.32 -52 80 C-53.42720409 81.26928798 -54.91983776 82.46538593 -56.4375 83.625 C-60.85617551 87.04712316 -64.96051883 90.6632284 -68.9921875 94.53125 C-71 96 -71 96 -75 96 C-75.02312283 84.93465142 -75.04091478 73.86930855 -75.05181217 62.80394077 C-75.05704113 57.66640434 -75.06413901 52.52888437 -75.07543945 47.39135742 C-75.08626787 42.4376859 -75.09227674 37.48403026 -75.09487724 32.53034782 C-75.09673157 30.63602384 -75.10035234 28.74170076 -75.10573006 26.8473835 C-75.11293989 24.20365614 -75.11399097 21.55999529 -75.11352539 18.91625977 C-75.11712067 18.12504227 -75.12071594 17.33382477 -75.12442017 16.51863098 C-75.11405216 11.11405216 -75.11405216 11.11405216 -74 10 C-72.21928754 9.91273777 -70.4351686 9.89300959 -68.65234375 9.90234375 C-67.57275391 9.90556641 -66.49316406 9.90878906 -65.38085938 9.91210938 C-63.67639648 9.92467773 -63.67639648 9.92467773 -61.9375 9.9375 C-60.22723633 9.94426758 -60.22723633 9.94426758 -58.48242188 9.95117188 C-55.65489649 9.96300253 -52.8274736 9.9794859 -50 10 C-48.65893595 12.6821281 -48.78549581 14.88398032 -48.68359375 17.8828125 C-48.64169922 19.04941406 -48.59980469 20.21601562 -48.55664062 21.41796875 C-48.51732422 22.64128906 -48.47800781 23.86460938 -48.4375 25.125 C-48.39431641 26.35605469 -48.35113281 27.58710937 -48.30664062 28.85546875 C-48.20031296 31.90352853 -48.09828219 34.9516719 -48 38 C-46.515 37.505 -46.515 37.505 -45 37 C-45 36.34 -45 35.68 -45 35 C-43.28667246 33.41466298 -41.50310953 31.90458785 -39.6875 30.4375 C-38.70136719 29.63183594 -37.71523437 28.82617188 -36.69921875 27.99609375 C-34 26 -34 26 -31 25 C-31 24.34 -31 23.68 -31 23 C-29.3046875 21.4765625 -29.3046875 21.4765625 -26.875 19.625 C-21.63181755 15.55364629 -16.61997273 11.25969115 -11.64794922 6.86181641 C-7.21723223 3 -7.21723223 3 -5 3 C-5 2.34 -5 1.68 -5 1 C-3 0 -3 0 0 0 Z " fill="#0260F9" transform="translate(113,51)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
194
ui/src/components/certimate/AccessByteplusForm.tsx
Normal file
194
ui/src/components/certimate/AccessByteplusForm.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import z from "zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PbErrorData } from "@/domain/base";
|
||||
import { accessProvidersMap, accessTypeFormSchema, type Access, type ByteplusConfig } from "@/domain/access";
|
||||
import { save } from "@/repository/access";
|
||||
import { useConfigContext } from "@/providers/config";
|
||||
|
||||
type AccessByteplusFormProps = {
|
||||
op: "add" | "edit" | "copy";
|
||||
data?: Access;
|
||||
onAfterReq: () => void;
|
||||
};
|
||||
|
||||
const AccessByteplusForm = ({ data, op, onAfterReq }: AccessByteplusFormProps) => {
|
||||
const { addAccess, updateAccess } = useConfigContext();
|
||||
const { t } = useTranslation();
|
||||
const formSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, "access.authorization.form.name.placeholder")
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
configType: accessTypeFormSchema,
|
||||
accessKey: z
|
||||
.string()
|
||||
.min(1, "access.authorization.form.access_key.placeholder")
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
secretKey: z
|
||||
.string()
|
||||
.min(1, "access.authorization.form.secret_key.placeholder")
|
||||
.max(64, t("common.errmsg.string_max", { max: 64 })),
|
||||
});
|
||||
|
||||
let config: ByteplusConfig = {
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
};
|
||||
if (data) config = data.config as ByteplusConfig;
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
id: data?.id,
|
||||
name: data?.name || "",
|
||||
configType: "byteplus",
|
||||
accessKey: config.accessKey,
|
||||
secretKey: config.secretKey,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (data: z.infer<typeof formSchema>) => {
|
||||
const req: Access = {
|
||||
id: data.id as string,
|
||||
name: data.name,
|
||||
configType: data.configType,
|
||||
usage: accessProvidersMap.get(data.configType)!.usage,
|
||||
config: {
|
||||
accessKey: data.accessKey,
|
||||
secretKey: data.secretKey,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
req.id = op == "copy" ? "" : req.id;
|
||||
const rs = await save(req);
|
||||
|
||||
onAfterReq();
|
||||
|
||||
req.id = rs.id;
|
||||
req.created = rs.created;
|
||||
req.updated = rs.updated;
|
||||
if (data.id && op == "edit") {
|
||||
updateAccess(req);
|
||||
return;
|
||||
}
|
||||
addAccess(req);
|
||||
} catch (e) {
|
||||
const err = e as ClientResponseError;
|
||||
|
||||
Object.entries(err.response.data as PbErrorData).forEach(([key, value]) => {
|
||||
form.setError(key as keyof z.infer<typeof formSchema>, {
|
||||
type: "manual",
|
||||
message: value.message,
|
||||
});
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.stopPropagation();
|
||||
form.handleSubmit(onSubmit)(e);
|
||||
}}
|
||||
className="space-y-8"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("access.authorization.form.name.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("access.authorization.form.name.placeholder")} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="id"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="configType"
|
||||
render={({ field }) => (
|
||||
<FormItem className="hidden">
|
||||
<FormLabel>{t("access.authorization.form.config.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="accessKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("access.authorization.form.access_key.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("access.authorization.form.access_key.placeholder")} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="secretKey"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("access.authorization.form.secret_key.label")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("access.authorization.form.secret_key.placeholder")} {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">{t("common.save")}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccessByteplusForm;
|
||||
@@ -22,6 +22,7 @@ import AccessSSHForm from "./AccessSSHForm";
|
||||
import AccessWebhookForm from "./AccessWebhookForm";
|
||||
import AccessKubernetesForm from "./AccessKubernetesForm";
|
||||
import AccessVolcengineForm from "./AccessVolcengineForm";
|
||||
import AccessByteplusForm from "./AccessByteplusForm";
|
||||
import { Access } from "@/domain/access";
|
||||
import { AccessTypeSelect } from "./AccessTypeSelect";
|
||||
|
||||
@@ -228,6 +229,17 @@ const AccessEditDialog = ({ trigger, op, data, className }: AccessEditProps) =>
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "byteplus":
|
||||
childComponent = (
|
||||
<AccessByteplusForm
|
||||
data={data}
|
||||
op={op}
|
||||
onAfterReq={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,305 +1,309 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
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, type DeployEditContext as DeployEditContextType } from "./DeployEdit";
|
||||
import DeployToAliyunOSS from "./DeployToAliyunOSS";
|
||||
import DeployToAliyunCDN from "./DeployToAliyunCDN";
|
||||
import DeployToAliyunCLB from "./DeployToAliyunCLB";
|
||||
import DeployToAliyunALB from "./DeployToAliyunALB";
|
||||
import DeployToAliyunNLB from "./DeployToAliyunNLB";
|
||||
import DeployToTencentCDN from "./DeployToTencentCDN";
|
||||
import DeployToTencentCLB from "./DeployToTencentCLB";
|
||||
import DeployToTencentCOS from "./DeployToTencentCOS";
|
||||
import DeployToTencentTEO from "./DeployToTencentTEO";
|
||||
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
|
||||
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
|
||||
import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN";
|
||||
import DeployToQiniuCDN from "./DeployToQiniuCDN";
|
||||
import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN";
|
||||
import DeployToLocal from "./DeployToLocal";
|
||||
import DeployToSSH from "./DeployToSSH";
|
||||
import DeployToWebhook from "./DeployToWebhook";
|
||||
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
|
||||
import DeployToVolcengineLive from "./DeployToVolcengineLive"
|
||||
import DeployToVolcengineCDN from "./DeployToVolcengineCDN"
|
||||
import { deployTargetsMap, type DeployConfig } from "@/domain/domain";
|
||||
import { accessProvidersMap } from "@/domain/access";
|
||||
import { useConfigContext } from "@/providers/config";
|
||||
|
||||
type DeployEditDialogProps = {
|
||||
trigger: React.ReactNode;
|
||||
deployConfig?: DeployConfig;
|
||||
onSave: (deploy: DeployConfig) => void;
|
||||
};
|
||||
|
||||
const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
config: { accesses },
|
||||
} = useConfigContext();
|
||||
|
||||
const [deployType, setDeployType] = useState("");
|
||||
|
||||
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
|
||||
access: "",
|
||||
type: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (deployConfig) {
|
||||
setLocDeployConfig({ ...deployConfig });
|
||||
} else {
|
||||
setLocDeployConfig({
|
||||
access: "",
|
||||
type: "",
|
||||
});
|
||||
}
|
||||
}, [deployConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
setDeployType(locDeployConfig.type);
|
||||
setErrors({});
|
||||
}, [locDeployConfig.type]);
|
||||
|
||||
const setConfig = useCallback(
|
||||
(deploy: DeployConfig) => {
|
||||
if (deploy.type !== locDeployConfig.type) {
|
||||
setLocDeployConfig({ ...deploy, access: "", config: {} });
|
||||
} else {
|
||||
setLocDeployConfig({ ...deploy });
|
||||
}
|
||||
},
|
||||
[locDeployConfig.type]
|
||||
);
|
||||
|
||||
const targetAccesses = accesses.filter((item) => {
|
||||
if (item.usage == "apply") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (locDeployConfig.type == "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider;
|
||||
});
|
||||
|
||||
const handleSaveClick = () => {
|
||||
// 验证数据
|
||||
const newError = { ...errors };
|
||||
newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : "";
|
||||
newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : "";
|
||||
setErrors(newError);
|
||||
if (Object.values(newError).some((e) => !!e)) return;
|
||||
|
||||
// 保存数据
|
||||
onSave(locDeployConfig);
|
||||
|
||||
// 清理数据
|
||||
setLocDeployConfig({
|
||||
access: "",
|
||||
type: "",
|
||||
});
|
||||
setErrors({});
|
||||
|
||||
// 关闭弹框
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
let childComponent = <></>;
|
||||
switch (deployType) {
|
||||
case "aliyun-oss":
|
||||
childComponent = <DeployToAliyunOSS />;
|
||||
break;
|
||||
case "aliyun-cdn":
|
||||
case "aliyun-dcdn":
|
||||
childComponent = <DeployToAliyunCDN />;
|
||||
break;
|
||||
case "aliyun-clb":
|
||||
childComponent = <DeployToAliyunCLB />;
|
||||
break;
|
||||
case "aliyun-alb":
|
||||
childComponent = <DeployToAliyunALB />;
|
||||
break;
|
||||
case "aliyun-nlb":
|
||||
childComponent = <DeployToAliyunNLB />;
|
||||
break;
|
||||
case "tencent-cdn":
|
||||
case "tencent-ecdn":
|
||||
childComponent = <DeployToTencentCDN />;
|
||||
break;
|
||||
case "tencent-clb":
|
||||
childComponent = <DeployToTencentCLB />;
|
||||
break;
|
||||
case "tencent-cos":
|
||||
childComponent = <DeployToTencentCOS />;
|
||||
break;
|
||||
case "tencent-teo":
|
||||
childComponent = <DeployToTencentTEO />;
|
||||
break;
|
||||
case "huaweicloud-cdn":
|
||||
childComponent = <DeployToHuaweiCloudCDN />;
|
||||
break;
|
||||
case "huaweicloud-elb":
|
||||
childComponent = <DeployToHuaweiCloudELB />;
|
||||
break;
|
||||
case "baiducloud-cdn":
|
||||
childComponent = <DeployToBaiduCloudCDN />;
|
||||
break;
|
||||
case "qiniu-cdn":
|
||||
childComponent = <DeployToQiniuCDN />;
|
||||
break;
|
||||
case "dogecloud-cdn":
|
||||
childComponent = <DeployToDogeCloudCDN />;
|
||||
break;
|
||||
case "local":
|
||||
childComponent = <DeployToLocal />;
|
||||
break;
|
||||
case "ssh":
|
||||
childComponent = <DeployToSSH />;
|
||||
break;
|
||||
case "webhook":
|
||||
childComponent = <DeployToWebhook />;
|
||||
break;
|
||||
case "k8s-secret":
|
||||
childComponent = <DeployToKubernetesSecret />;
|
||||
break;
|
||||
case "volcengine-live":
|
||||
childComponent = <DeployToVolcengineLive />;
|
||||
break;
|
||||
case "volcengine-cdn":
|
||||
childComponent = <DeployToVolcengineCDN />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<DeployEditContext.Provider
|
||||
value={{
|
||||
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}>
|
||||
<DialogTrigger>{trigger}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="dark:text-stone-200"
|
||||
onInteractOutside={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[80vh]">
|
||||
<div className="container py-3">
|
||||
{/* 部署方式 */}
|
||||
<div>
|
||||
<Label>{t("domain.deployment.form.type.label")}</Label>
|
||||
|
||||
<Select
|
||||
value={locDeployConfig.type}
|
||||
onValueChange={(val: string) => {
|
||||
setConfig({ ...locDeployConfig, type: val });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
|
||||
{Array.from(deployTargetsMap.entries()).map(([key, target]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img className="w-6" src={target.icon} />
|
||||
<div>{t(target.name)}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="text-red-500 text-sm mt-1">{errors.type}</div>
|
||||
</div>
|
||||
|
||||
{/* 授权配置 */}
|
||||
<div className="mt-8">
|
||||
<Label className="flex justify-between">
|
||||
<div>{t("domain.deployment.form.access.label")}</div>
|
||||
<AccessEditDialog
|
||||
trigger={
|
||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||
<Plus size={14} />
|
||||
{t("common.add")}
|
||||
</div>
|
||||
}
|
||||
op="add"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={locDeployConfig.access}
|
||||
onValueChange={(val: string) => {
|
||||
setConfig({ ...locDeployConfig, access: val });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
|
||||
{targetAccesses.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
|
||||
<div>{item.name}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="text-red-500 text-sm mt-1">{errors.access}</div>
|
||||
</div>
|
||||
|
||||
{/* 其他参数 */}
|
||||
<div className="mt-8">{childComponent}</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DeployEditContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeployEditDialog;
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Plus } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
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, type DeployEditContext as DeployEditContextType } from "./DeployEdit";
|
||||
import DeployToAliyunOSS from "./DeployToAliyunOSS";
|
||||
import DeployToAliyunCDN from "./DeployToAliyunCDN";
|
||||
import DeployToAliyunCLB from "./DeployToAliyunCLB";
|
||||
import DeployToAliyunALB from "./DeployToAliyunALB";
|
||||
import DeployToAliyunNLB from "./DeployToAliyunNLB";
|
||||
import DeployToTencentCDN from "./DeployToTencentCDN";
|
||||
import DeployToTencentCLB from "./DeployToTencentCLB";
|
||||
import DeployToTencentCOS from "./DeployToTencentCOS";
|
||||
import DeployToTencentTEO from "./DeployToTencentTEO";
|
||||
import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN";
|
||||
import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB";
|
||||
import DeployToBaiduCloudCDN from "./DeployToBaiduCloudCDN";
|
||||
import DeployToQiniuCDN from "./DeployToQiniuCDN";
|
||||
import DeployToDogeCloudCDN from "./DeployToDogeCloudCDN";
|
||||
import DeployToLocal from "./DeployToLocal";
|
||||
import DeployToSSH from "./DeployToSSH";
|
||||
import DeployToWebhook from "./DeployToWebhook";
|
||||
import DeployToKubernetesSecret from "./DeployToKubernetesSecret";
|
||||
import DeployToVolcengineLive from "./DeployToVolcengineLive";
|
||||
import DeployToVolcengineCDN from "./DeployToVolcengineCDN";
|
||||
import DeployToByteplusCDN from "./DeployToByteplusCDN";
|
||||
import { deployTargetsMap, type DeployConfig } from "@/domain/domain";
|
||||
import { accessProvidersMap } from "@/domain/access";
|
||||
import { useConfigContext } from "@/providers/config";
|
||||
|
||||
type DeployEditDialogProps = {
|
||||
trigger: React.ReactNode;
|
||||
deployConfig?: DeployConfig;
|
||||
onSave: (deploy: DeployConfig) => void;
|
||||
};
|
||||
|
||||
const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
config: { accesses },
|
||||
} = useConfigContext();
|
||||
|
||||
const [deployType, setDeployType] = useState("");
|
||||
|
||||
const [locDeployConfig, setLocDeployConfig] = useState<DeployConfig>({
|
||||
access: "",
|
||||
type: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string | undefined>>({});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (deployConfig) {
|
||||
setLocDeployConfig({ ...deployConfig });
|
||||
} else {
|
||||
setLocDeployConfig({
|
||||
access: "",
|
||||
type: "",
|
||||
});
|
||||
}
|
||||
}, [deployConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
setDeployType(locDeployConfig.type);
|
||||
setErrors({});
|
||||
}, [locDeployConfig.type]);
|
||||
|
||||
const setConfig = useCallback(
|
||||
(deploy: DeployConfig) => {
|
||||
if (deploy.type !== locDeployConfig.type) {
|
||||
setLocDeployConfig({ ...deploy, access: "", config: {} });
|
||||
} else {
|
||||
setLocDeployConfig({ ...deploy });
|
||||
}
|
||||
},
|
||||
[locDeployConfig.type]
|
||||
);
|
||||
|
||||
const targetAccesses = accesses.filter((item) => {
|
||||
if (item.usage == "apply") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (locDeployConfig.type == "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider;
|
||||
});
|
||||
|
||||
const handleSaveClick = () => {
|
||||
// 验证数据
|
||||
const newError = { ...errors };
|
||||
newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : "";
|
||||
newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : "";
|
||||
setErrors(newError);
|
||||
if (Object.values(newError).some((e) => !!e)) return;
|
||||
|
||||
// 保存数据
|
||||
onSave(locDeployConfig);
|
||||
|
||||
// 清理数据
|
||||
setLocDeployConfig({
|
||||
access: "",
|
||||
type: "",
|
||||
});
|
||||
setErrors({});
|
||||
|
||||
// 关闭弹框
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
let childComponent = <></>;
|
||||
switch (deployType) {
|
||||
case "aliyun-oss":
|
||||
childComponent = <DeployToAliyunOSS />;
|
||||
break;
|
||||
case "aliyun-cdn":
|
||||
case "aliyun-dcdn":
|
||||
childComponent = <DeployToAliyunCDN />;
|
||||
break;
|
||||
case "aliyun-clb":
|
||||
childComponent = <DeployToAliyunCLB />;
|
||||
break;
|
||||
case "aliyun-alb":
|
||||
childComponent = <DeployToAliyunALB />;
|
||||
break;
|
||||
case "aliyun-nlb":
|
||||
childComponent = <DeployToAliyunNLB />;
|
||||
break;
|
||||
case "tencent-cdn":
|
||||
case "tencent-ecdn":
|
||||
childComponent = <DeployToTencentCDN />;
|
||||
break;
|
||||
case "tencent-clb":
|
||||
childComponent = <DeployToTencentCLB />;
|
||||
break;
|
||||
case "tencent-cos":
|
||||
childComponent = <DeployToTencentCOS />;
|
||||
break;
|
||||
case "tencent-teo":
|
||||
childComponent = <DeployToTencentTEO />;
|
||||
break;
|
||||
case "huaweicloud-cdn":
|
||||
childComponent = <DeployToHuaweiCloudCDN />;
|
||||
break;
|
||||
case "huaweicloud-elb":
|
||||
childComponent = <DeployToHuaweiCloudELB />;
|
||||
break;
|
||||
case "baiducloud-cdn":
|
||||
childComponent = <DeployToBaiduCloudCDN />;
|
||||
break;
|
||||
case "qiniu-cdn":
|
||||
childComponent = <DeployToQiniuCDN />;
|
||||
break;
|
||||
case "dogecloud-cdn":
|
||||
childComponent = <DeployToDogeCloudCDN />;
|
||||
break;
|
||||
case "local":
|
||||
childComponent = <DeployToLocal />;
|
||||
break;
|
||||
case "ssh":
|
||||
childComponent = <DeployToSSH />;
|
||||
break;
|
||||
case "webhook":
|
||||
childComponent = <DeployToWebhook />;
|
||||
break;
|
||||
case "k8s-secret":
|
||||
childComponent = <DeployToKubernetesSecret />;
|
||||
break;
|
||||
case "volcengine-live":
|
||||
childComponent = <DeployToVolcengineLive />;
|
||||
break;
|
||||
case "volcengine-cdn":
|
||||
childComponent = <DeployToVolcengineCDN />;
|
||||
break;
|
||||
case "byteplus-cdn":
|
||||
childComponent = <DeployToByteplusCDN />;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<DeployEditContext.Provider
|
||||
value={{
|
||||
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}>
|
||||
<DialogTrigger>{trigger}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="dark:text-stone-200"
|
||||
onInteractOutside={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("domain.deployment.tab")}</DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[80vh]">
|
||||
<div className="container py-3">
|
||||
{/* 部署方式 */}
|
||||
<div>
|
||||
<Label>{t("domain.deployment.form.type.label")}</Label>
|
||||
|
||||
<Select
|
||||
value={locDeployConfig.type}
|
||||
onValueChange={(val: string) => {
|
||||
setConfig({ ...locDeployConfig, type: val });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder={t("domain.deployment.form.type.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("domain.deployment.form.type.list")}</SelectLabel>
|
||||
{Array.from(deployTargetsMap.entries()).map(([key, target]) => (
|
||||
<SelectItem key={key} value={key}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img className="w-6" src={target.icon} />
|
||||
<div>{t(target.name)}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="text-red-500 text-sm mt-1">{errors.type}</div>
|
||||
</div>
|
||||
|
||||
{/* 授权配置 */}
|
||||
<div className="mt-8">
|
||||
<Label className="flex justify-between">
|
||||
<div>{t("domain.deployment.form.access.label")}</div>
|
||||
<AccessEditDialog
|
||||
trigger={
|
||||
<div className="font-normal text-primary hover:underline cursor-pointer flex items-center">
|
||||
<Plus size={14} />
|
||||
{t("common.add")}
|
||||
</div>
|
||||
}
|
||||
op="add"
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<Select
|
||||
value={locDeployConfig.access}
|
||||
onValueChange={(val: string) => {
|
||||
setConfig({ ...locDeployConfig, access: val });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder={t("domain.deployment.form.access.placeholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>{t("domain.deployment.form.access.list")}</SelectLabel>
|
||||
{targetAccesses.map((item) => (
|
||||
<SelectItem key={item.id} value={item.id}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img className="w-6" src={accessProvidersMap.get(item.configType)?.icon} />
|
||||
<div>{item.name}</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<div className="text-red-500 text-sm mt-1">{errors.access}</div>
|
||||
</div>
|
||||
|
||||
{/* 其他参数 */}
|
||||
<div className="mt-8">{childComponent}</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSaveClick();
|
||||
}}
|
||||
>
|
||||
{t("common.save")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</DeployEditContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeployEditDialog;
|
||||
|
||||
68
ui/src/components/certimate/DeployToByteplusCDN.tsx
Normal file
68
ui/src/components/certimate/DeployToByteplusCDN.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
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 DeployToByteplusCDNConfigParams = {
|
||||
domain?: string;
|
||||
};
|
||||
|
||||
const DeployToByteplusCDN = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { config, setConfig, errors, setErrors } = useDeployEditContext<DeployToByteplusCDNConfigParams>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!config.id) {
|
||||
setConfig({
|
||||
...config,
|
||||
config: {},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors({});
|
||||
}, []);
|
||||
|
||||
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>
|
||||
<Label>{t("domain.deployment.form.domain.label.wildsupported")}</Label>
|
||||
<Input
|
||||
placeholder={t("domain.deployment.form.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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeployToByteplusCDN;
|
||||
@@ -33,9 +33,17 @@ const DeployToTencentTEO = () => {
|
||||
|
||||
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"),
|
||||
}),
|
||||
domain: z.string().refine(
|
||||
(v) =>
|
||||
!!v &&
|
||||
v
|
||||
.trim()
|
||||
.split("\n")
|
||||
.every((s) => /^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/.test(s)),
|
||||
{
|
||||
message: t("common.errmsg.domain_invalid"),
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const accessProvidersMap: Map<AccessProvider["type"], AccessProvider> = n
|
||||
["qiniu", "common.provider.qiniu", "/imgs/providers/qiniu.svg", "deploy", "七牛云:qiniu"],
|
||||
["dogecloud", "common.provider.dogecloud", "/imgs/providers/dogecloud.svg", "deploy", "多吉云:doge cloud"],
|
||||
["volcengine", "common.provider.volcengine", "/imgs/providers/volcengine.svg", "all", "火山引擎"],
|
||||
["byteplus", "common.provider.byteplus", "/imgs/providers/byteplus.svg", "all", "BytePlus"],
|
||||
["aws", "common.provider.aws", "/imgs/providers/aws.svg", "apply", "亚马逊:amazon:aws"],
|
||||
["cloudflare", "common.provider.cloudflare", "/imgs/providers/cloudflare.svg", "apply", "cloudflare:cf:cloud flare"],
|
||||
["namesilo", "common.provider.namesilo", "/imgs/providers/namesilo.svg", "apply", "namesilo"],
|
||||
@@ -51,6 +52,7 @@ export const accessTypeFormSchema = z.union(
|
||||
z.literal("webhook"),
|
||||
z.literal("k8s"),
|
||||
z.literal("volcengine"),
|
||||
z.literal("byteplus"),
|
||||
],
|
||||
{ message: "access.authorization.form.type.placeholder" }
|
||||
);
|
||||
@@ -77,7 +79,8 @@ export type Access = {
|
||||
| SSHConfig
|
||||
| WebhookConfig
|
||||
| KubernetesConfig
|
||||
| VolcengineConfig;
|
||||
| VolcengineConfig
|
||||
| ByteplusConfig;
|
||||
deleted?: string;
|
||||
created?: string;
|
||||
updated?: string;
|
||||
@@ -170,3 +173,8 @@ export type VolcengineConfig = {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
};
|
||||
|
||||
export type ByteplusConfig = {
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
};
|
||||
|
||||
@@ -94,5 +94,6 @@ export const deployTargetsMap: Map<DeployTarget["type"], DeployTarget> = new Map
|
||||
["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"],
|
||||
["volcengine-live", "common.provider.volcengine.live", "/imgs/providers/volcengine.svg"],
|
||||
["volcengine-cdn", "common.provider.volcengine.cdn", "/imgs/providers/volcengine.svg"],
|
||||
["byteplus-cdn", "common.provider.byteplus.cdn", "/imgs/providers/byteplus.svg"],
|
||||
].map(([type, name, icon]) => [type, { type, provider: type.split("-")[0], name, icon }])
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user