feat: support using scm service on deployment to huaweicloud cdn

This commit is contained in:
Fu Diwei 2024-10-20 16:42:05 +08:00
parent 17f72eb9cb
commit 88e64717cd
5 changed files with 388 additions and 25 deletions

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global"
cdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2"
@ -11,7 +12,7 @@ import (
cdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region"
"certimate/internal/domain"
"certimate/internal/utils/rand"
uploaderImpl "certimate/internal/pkg/core/uploader/impl"
)
type HuaweiCloudCDNDeployer struct {
@ -45,11 +46,12 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
return err
}
d.infos = append(d.infos, toStr("HuaweiCloudCdnClient 创建成功", nil))
d.infos = append(d.infos, toStr("SDK 客户端创建成功", nil))
// 查询加速域名配置
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
showDomainFullConfigReq := &cdnModel.ShowDomainFullConfigRequest{
DomainName: getDeployString(d.option.DeployConfig, "domain"),
DomainName: d.option.DeployConfig.GetConfigAsString("domain"),
}
showDomainFullConfigResp, err := client.ShowDomainFullConfig(showDomainFullConfigReq)
if err != nil {
@ -59,19 +61,45 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error {
d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp))
// 更新加速域名配置
certName := fmt.Sprintf("%s-%s", d.option.DomainId, rand.RandStr(12))
updateDomainMultiCertificatesReq := &cdnModel.UpdateDomainMultiCertificatesRequest{
Body: &cdnModel.UpdateDomainMultiCertificatesRequestBody{
Https: mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, &cdnModel.UpdateDomainMultiCertificatesRequestBodyContent{
DomainName: getDeployString(d.option.DeployConfig, "domain"),
HttpsSwitch: 1,
CertName: &certName,
Certificate: &d.option.Certificate.Certificate,
PrivateKey: &d.option.Certificate.PrivateKey,
}),
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
updateDomainMultiCertificatesReqBodyContent := &huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent{}
updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain")
updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1
var updateDomainMultiCertificatesResp *cdnModel.UpdateDomainMultiCertificatesResponse
if d.option.DeployConfig.GetConfigAsBool("useSCM") {
uploader, err := uploaderImpl.NewHuaweiCloudSCMUploader(&uploaderImpl.HuaweiCloudSCMUploaderConfig{
Region: "", // TODO: SCM 服务与 CDN 服务的区域不一致,这里暂时不传而是使用默认值,仅支持华为云国内版
AccessKeyId: access.AccessKeyId,
SecretAccessKey: access.SecretAccessKey,
})
if err != nil {
return err
}
// 上传证书到 SCM
uploadResult, err := uploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey)
if err != nil {
return err
}
d.infos = append(d.infos, toStr("已上传证书", uploadResult))
updateDomainMultiCertificatesReqBodyContent.CertificateType = int32Ptr(2)
updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = stringPtr(uploadResult.CertId)
updateDomainMultiCertificatesReqBodyContent.CertName = stringPtr(uploadResult.CertName)
} else {
updateDomainMultiCertificatesReqBodyContent.CertificateType = int32Ptr(0)
updateDomainMultiCertificatesReqBodyContent.CertName = stringPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli()))
updateDomainMultiCertificatesReqBodyContent.Certificate = stringPtr(d.option.Certificate.Certificate)
updateDomainMultiCertificatesReqBodyContent.PrivateKey = stringPtr(d.option.Certificate.PrivateKey)
}
updateDomainMultiCertificatesReqBodyContent = mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, updateDomainMultiCertificatesReqBodyContent)
updateDomainMultiCertificatesReq := &huaweicloudCDNUpdateDomainMultiCertificatesRequest{
Body: &huaweicloudCDNUpdateDomainMultiCertificatesRequestBody{
Https: updateDomainMultiCertificatesReqBodyContent,
},
}
updateDomainMultiCertificatesResp, err := client.UpdateDomainMultiCertificates(updateDomainMultiCertificatesReq)
updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(client, updateDomainMultiCertificatesReq)
if err != nil {
return err
}
@ -107,25 +135,47 @@ func (d *HuaweiCloudCDNDeployer) createClient(access *domain.HuaweiCloudAccess)
return client, nil
}
func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *cdnModel.UpdateDomainMultiCertificatesRequestBodyContent) *cdnModel.UpdateDomainMultiCertificatesRequestBodyContent {
type huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent struct {
cdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"`
SCMCertificateId *string `json:"scm_certificate_id,omitempty"`
}
type huaweicloudCDNUpdateDomainMultiCertificatesRequestBody struct {
Https *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent `json:"https,omitempty"`
}
type huaweicloudCDNUpdateDomainMultiCertificatesRequest struct {
Body *huaweicloudCDNUpdateDomainMultiCertificatesRequestBody `json:"body,omitempty"`
}
func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *cdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*cdnModel.UpdateDomainMultiCertificatesResponse, error) {
// 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求
// 可能需要等之后 SDK 更新
requestDef := cdn.GenReqDefForUpdateDomainMultiCertificates()
if resp, err := client.HcClient.Sync(request, requestDef); err != nil {
return nil, err
} else {
return resp.(*cdnModel.UpdateDomainMultiCertificatesResponse), nil
}
}
func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent {
if src == nil {
return dest
}
// 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去
// 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化
// REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html
// REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html
if *src.OriginProtocol == "follow" {
accessOriginWay := int32(1)
dest.AccessOriginWay = &accessOriginWay
dest.AccessOriginWay = int32Ptr(1)
} else if *src.OriginProtocol == "http" {
accessOriginWay := int32(2)
dest.AccessOriginWay = &accessOriginWay
dest.AccessOriginWay = int32Ptr(2)
} else if *src.OriginProtocol == "https" {
accessOriginWay := int32(3)
dest.AccessOriginWay = &accessOriginWay
dest.AccessOriginWay = int32Ptr(3)
}
if src.ForceRedirect != nil {
@ -141,10 +191,17 @@ func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *cdnModel.Upda
if src.Https != nil {
if *src.Https.Http2Status == "on" {
http2 := int32(1)
dest.Http2 = &http2
dest.Http2 = int32Ptr(1)
}
}
return dest
}
func int32Ptr(i int32) *int32 {
return &i
}
func stringPtr(s string) *string {
return &s
}

View File

@ -15,6 +15,48 @@ type DeployConfig struct {
Config map[string]any `json:"config"`
}
// 以字符串形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是字符串,则返回空字符串。
func (dc *DeployConfig) GetConfigAsString(key string) string {
if dc.Config == nil {
return ""
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(string); ok {
return result
}
}
return ""
}
// 以布尔形式获取配置项。
//
// 入参:
// - key: 配置项的键。
//
// 出参:
// - 配置项的值。如果配置项不存在或者类型不是布尔,则返回 false。
func (dc *DeployConfig) GetConfigAsBool(key string) bool {
if dc.Config == nil {
return false
}
if value, ok := dc.Config[key]; ok {
if result, ok := value.(bool); ok {
return result
}
}
return false
}
type KV struct {
Key string `json:"key"`
Value string `json:"value"`

View File

@ -0,0 +1,189 @@
package impl
import (
"context"
"fmt"
"time"
"github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic"
scm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3"
scmModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model"
scmRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region"
"certimate/internal/pkg/core/uploader"
"certimate/internal/pkg/utils/x509"
)
type HuaweiCloudSCMUploaderConfig struct {
Region string `json:"region"`
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
}
type HuaweiCloudSCMUploader struct {
client *scm.ScmClient
}
func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) {
client, err := createClient(config.Region, config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("failed to create client: %w", err)
}
return &HuaweiCloudSCMUploader{
client: client,
}, nil
}
func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
newCert, err := x509.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历查询已有证书,避免重复上传
// REF: https://support.huaweicloud.com/api-ccm/ListCertificates.html
// REF: https://support.huaweicloud.com/api-ccm/ExportCertificate_0.html
listCertificatesLimit := int32(50)
listCertificatesOffset := int32(0)
for {
listCertificatesReq := &scmModel.ListCertificatesRequest{
<<<<<<< HEAD
Limit: int32Ptr(listCertificatesLimit),
Offset: int32Ptr(listCertificatesOffset),
SortDir: stringPtr("DESC"),
SortKey: stringPtr("certExpiredTime"),
=======
Limit: int32Ptr(listCertificatesLimit),
Offset: int32Ptr(listCertificatesOffset),
>>>>>>> 1ff10bf989afaa505a3fc2fda668ffcded815d09
}
listCertificatesResp, err := u.client.ListCertificates(listCertificatesReq)
if err != nil {
return nil, fmt.Errorf("failed to execute request 'scm.ListCertificates': %w", err)
}
if listCertificatesResp.Certificates != nil {
for _, certDetail := range *listCertificatesResp.Certificates {
exportCertificateReq := &scmModel.ExportCertificateRequest{
CertificateId: certDetail.Id,
}
exportCertificateResp, err := u.client.ExportCertificate(exportCertificateReq)
if err != nil {
if exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 {
continue
}
return nil, fmt.Errorf("failed to execute request 'scm.ExportCertificate': %w", err)
}
<<<<<<< HEAD
var isSameCert bool
if *exportCertificateResp.Certificate == certPem {
isSameCert = true
} else {
cert, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
if err != nil {
continue
}
isSameCert = x509.EqualCertificate(cert, newCert)
}
// 如果已存在相同证书,直接返回已有的证书信息
if isSameCert {
=======
cert, err := x509.ParseCertificateFromPEM(*exportCertificateResp.Certificate)
if err != nil {
continue
}
if x509.EqualCertificate(cert, newCert) {
// 如果已存在相同证书,直接返回已有的证书信息
>>>>>>> 1ff10bf989afaa505a3fc2fda668ffcded815d09
return &uploader.UploadResult{
CertId: certDetail.Id,
CertName: certDetail.Name,
}, nil
}
}
}
if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) {
break
}
listCertificatesOffset += listCertificatesLimit
<<<<<<< HEAD
if listCertificatesOffset >= 999 { // 避免无限获取
break
}
=======
>>>>>>> 1ff10bf989afaa505a3fc2fda668ffcded815d09
}
// 生成证书名(需符合华为云命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 上传新证书
// REF: https://support.huaweicloud.com/api-ccm/ImportCertificate.html
importCertificateReq := &scmModel.ImportCertificateRequest{
Body: &scmModel.ImportCertificateRequestBody{
Name: certName,
Certificate: certPem,
PrivateKey: privkeyPem,
},
}
importCertificateResp, err := u.client.ImportCertificate(importCertificateReq)
if err != nil {
return nil, fmt.Errorf("failed to execute request 'scm.ImportCertificate': %w", err)
}
certId = *importCertificateResp.CertificateId
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
}, nil
}
func createClient(region, accessKeyId, secretAccessKey string) (*scm.ScmClient, error) {
auth, err := basic.NewCredentialsBuilder().
WithAk(accessKeyId).
WithSk(secretAccessKey).
SafeBuild()
if err != nil {
return nil, err
}
if region == "" {
region = "cn-north-4" // SCM 服务默认区域:华北北京四
}
hcRegion, err := scmRegion.SafeValueOf(region)
if err != nil {
return nil, err
}
hcClient, err := scm.ScmClientBuilder().
WithRegion(hcRegion).
WithCredential(auth).
SafeBuild()
if err != nil {
return nil, err
}
client := scm.NewScmClient(hcClient)
return client, nil
}
func int32Ptr(i int32) *int32 {
return &i
}
<<<<<<< HEAD
func stringPtr(s string) *string {
return &s
}
=======
>>>>>>> 1ff10bf989afaa505a3fc2fda668ffcded815d09

View File

@ -0,0 +1,27 @@
package uploader
import "context"
// 表示定义证书上传者的抽象类型接口。
// 云服务商通常会提供 SSL 证书管理服务,可供用户集中管理证书。
// 注意与 `Deployer` 区分,“上传”通常为“部署”的前置操作。
type Uploader interface {
// 上传证书。
//
// 入参:
// - ctx
// - certPem证书 PEM 内容
// - privkeyPem私钥 PEM 内容
//
// 出参:
// - res
// - err
Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error)
}
// 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。
type UploadResult struct {
CertId string `json:"certId"`
CertName string `json:"certName"`
CertData map[string]interface{} `json:"certData,omitempty"`
}

View File

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