feat: implement more Deployer

This commit is contained in:
Fu Diwei
2024-11-20 21:02:29 +08:00
parent a59184ae5f
commit 643a666853
23 changed files with 1778 additions and 17 deletions

View 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"
upAliyunSlb "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 := upAliyunSlb.New(&upAliyunSlb.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.Appendt("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.Appendt("已查询到 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 describeLoadBalancerListenersResp.Body.NextToken == nil {
break
} else {
listListenersToken = describeLoadBalancerListenersResp.Body.NextToken
listListenersPage += 1
}
}
d.logger.Appendt("已查询到 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.Appendt("已查询到 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.Appendt("已查询到 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.Appendt("已更新 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
}

View File

@@ -0,0 +1,120 @@
package aliyunclb_test
import (
"context"
"flag"
"fmt"
"os"
"strings"
"testing"
dpAliyunClb "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 Test(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 := dpAliyunClb.New(&dpAliyunClb.AliyunCLBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: dpAliyunClb.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 := dpAliyunClb.New(&dpAliyunClb.AliyunCLBDeployerConfig{
AccessKeyId: fAccessKeyId,
AccessKeySecret: fAccessKeySecret,
Region: fRegion,
ResourceType: dpAliyunClb.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)
})
}

View File

@@ -0,0 +1,10 @@
package aliyunclb
type DeployResourceType string
const (
// 资源类型:部署到指定负载均衡器。
DEPLOY_RESOURCE_LOADBALANCER = DeployResourceType("loadbalancer")
// 资源类型:部署到指定监听器。
DEPLOY_RESOURCE_LISTENER = DeployResourceType("listener")
)