feat(provider): new purge provider: aws acm

This commit is contained in:
Fu Diwei
2026-08-27 17:51:40 +08:00
parent ded9cad881
commit 83bb9a7eae
12 changed files with 787 additions and 16 deletions
+27
View File
@@ -0,0 +1,27 @@
package purgers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
pgrimpl "github.com/certimate-go/certimate/pkg/core/purger/providers/aws-acm"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.PurgeProviderTypeAWSACM, func(options *ProviderFactoryOptions) (core.Purger, error) {
credentials := domain.AccessConfigForAWS{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
provider, err := pgrimpl.NewPurger(&pgrimpl.PurgerConfig{
AuthMethod: credentials.AuthMethod,
AccessKeyId: credentials.AccessKeyId,
SecretAccessKey: credentials.SecretAccessKey,
Region: xmaps.GetString(options.ProviderExtendedConfig, "region"),
})
return provider, err
})
}
+1
View File
@@ -495,6 +495,7 @@ const (
PurgeProviderTypeAliyunCAS = PurgeProviderType(AccessProviderTypeAliyun + "-cas")
PurgeProviderTypeAliyunCLB = PurgeProviderType(AccessProviderTypeAliyun + "-clb")
PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa")
PurgeProviderTypeAWSACM = PurgeProviderType(AccessProviderTypeAWS + "-acm")
PurgeProviderTypeAWSIAM = PurgeProviderType(AccessProviderTypeAWS + "-iam")
PurgeProviderTypeAzureKeyVault = PurgeProviderType(AccessProviderTypeAzure + "-keyvault")
PurgeProviderTypeBaiduCloudCert = PurgeProviderType(AccessProviderTypeBaiduCloud + "-cert")
+224 -8
View File
@@ -2,19 +2,26 @@ package awsalb
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/smithy-go"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimplacm "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm"
cmgrimpliam "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type (
@@ -47,18 +54,34 @@ type DeployerConfig struct {
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *elasticloadbalancingv2.Client
sdkClients *wSDKClients
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
type wSDKClients struct {
ACM *acm.Client
IAM *iam.Client
ELB *elasticloadbalancingv2.Client
}
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
clientELB, err := createSDKClientELB(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
clientACM, err := createSDKClientACM(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
clientIAM, err := createSDKClientIAM(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
@@ -95,7 +118,7 @@ func NewDeployer(config *DeployerConfig) (*Deployer, error) {
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkClients: &wSDKClients{ELB: clientELB, ACM: clientACM, IAM: clientIAM},
sdkCertmgr: pcertmgr,
}, nil
}
@@ -118,6 +141,12 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
return nil, fmt.Errorf("config `listenerArn` is required")
}
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
@@ -131,7 +160,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
describeLoadBalancersReq := &elasticloadbalancingv2.DescribeLoadBalancersInput{
LoadBalancerArns: []string{d.config.LoadbalancerArn},
}
describeLoadBalancersResp, err := d.sdkClient.DescribeLoadBalancers(ctx, describeLoadBalancersReq)
describeLoadBalancersResp, err := d.sdkClients.ELB.DescribeLoadBalancers(ctx, describeLoadBalancersReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.DescribeLoadBalancers'", slog.Any("request", describeLoadBalancersReq), slog.Any("response", describeLoadBalancersResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.DescribeLoadBalancers': %w", err)
@@ -145,7 +174,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
LoadBalancerArn: aws.String(d.config.LoadbalancerArn),
ListenerArns: []string{d.config.ListenerArn},
}
describeListenersResp, err := d.sdkClient.DescribeListeners(ctx, describeListenersReq)
describeListenersResp, err := d.sdkClients.ELB.DescribeListeners(ctx, describeListenersReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.DescribeListeners': %w", err)
@@ -182,6 +211,10 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
if err := d.updateListenerSniCertificate(ctx, *listenerInfo.ListenerArn, certArn); err != nil {
return nil, err
}
if err := d.pruneListenerSniCertificate(ctx, *listenerInfo.ListenerArn, certArn, certX509.DNSNames); err != nil {
return nil, err
}
}
return &DeployResult{}, nil
@@ -198,7 +231,7 @@ func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudLi
},
},
}
modifyListenerResp, err := d.sdkClient.ModifyListener(ctx, modifyListenerReq)
modifyListenerResp, err := d.sdkClients.ELB.ModifyListener(ctx, modifyListenerReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.ModifyListener'", slog.Any("request", modifyListenerReq), slog.Any("response", modifyListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.ModifyListener': %w", err)
@@ -218,7 +251,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
},
},
}
addListenerCertificatesResp, err := d.sdkClient.AddListenerCertificates(ctx, addListenerCertificatesReq)
addListenerCertificatesResp, err := d.sdkClients.ELB.AddListenerCertificates(ctx, addListenerCertificatesReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.AddListenerCertificates'", slog.Any("request", addListenerCertificatesReq), slog.Any("response", addListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.AddListenerCertificates': %w", err)
@@ -227,7 +260,132 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
return nil
}
func createSDKClient(authMethod, accessKeyId, secretAccessKey, region string) (*elasticloadbalancingv2.Client, error) {
func (d *Deployer) pruneListenerSniCertificate(ctx context.Context, cloudListenerArn string, cloudCertArn string, cloudCertSANs []string) error {
const acmArnPrefix = "arn:aws:acm:"
const iamArnPrefix = "arn:aws:iam:"
certArnsToDelete := make([]string, 0)
// 查询侦听器证书列表,并找出需要删除绑定的同域名证书
// REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeListenerCertificates.html
// REF: https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetServerCertificate.html
// REF: https://docs.aws.amazon.com/acm/latest/APIReference/API_GetCertificate.html
describeListenerCertificatesMarker := (*string)(nil)
describeListenerCertificatesPageSize := 100
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeListenerCertificatesReq := &elasticloadbalancingv2.DescribeListenerCertificatesInput{
ListenerArn: aws.String(cloudListenerArn),
Marker: describeListenerCertificatesMarker,
PageSize: aws.Int32(int32(describeListenerCertificatesPageSize)),
}
describeListenerCertificatesResp, err := d.sdkClients.ELB.DescribeListenerCertificates(ctx, describeListenerCertificatesReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.DescribeListenerCertificates'", slog.Any("request", describeListenerCertificatesReq), slog.Any("response", describeListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.DescribeListenerCertificates': %w", err)
}
for _, certItem := range describeListenerCertificatesResp.Certificates {
if aws.ToString(certItem.CertificateArn) == cloudCertArn {
continue
}
if aws.ToBool(certItem.IsDefault) {
continue
}
switch {
case strings.HasPrefix(aws.ToString(certItem.CertificateArn), acmArnPrefix):
{
getCertificateReq := &acm.GetCertificateInput{
CertificateArn: certItem.CertificateArn,
}
getCertificateResp, err := d.sdkClients.ACM.GetCertificate(ctx, getCertificateReq)
if err != nil {
var sdkerr smithy.APIError
if errors.As(err, &sdkerr) {
if sdkErrCode := sdkerr.ErrorCode(); sdkErrCode == "InvalidArnException" || sdkErrCode == "ResourceNotFoundException" {
continue
}
}
return fmt.Errorf("failed to execute sdk request 'acm.GetCertificate': %w", err)
}
certX509InACM, err := xcert.ParseCertificateFromPEM(aws.ToString(getCertificateResp.Certificate))
if err != nil {
continue
}
if lo.ElementsMatch(cloudCertSANs, certX509InACM.DNSNames) {
certArnsToDelete = append(certArnsToDelete, aws.ToString(certItem.CertificateArn))
}
}
case strings.HasPrefix(aws.ToString(certItem.CertificateArn), iamArnPrefix):
{
getServerCertificateReq := &iam.GetServerCertificateInput{
ServerCertificateName: certItem.CertificateArn,
}
getServerCertificateResp, err := d.sdkClients.IAM.GetServerCertificate(ctx, getServerCertificateReq)
if err != nil {
var sdkerr smithy.APIError
if errors.As(err, &sdkerr) {
if sdkerr.ErrorCode() == "NoSuchEntity" {
continue
}
}
return fmt.Errorf("failed to execute sdk request 'iam.GetServerCertificate': %w", err)
}
certX509InIAM, err := xcert.ParseCertificateFromPEM(aws.ToString(getServerCertificateResp.ServerCertificate.CertificateBody))
if err != nil {
continue
}
if lo.ElementsMatch(cloudCertSANs, certX509InIAM.DNSNames) {
certArnsToDelete = append(certArnsToDelete, aws.ToString(certItem.CertificateArn))
}
}
}
}
if len(describeListenerCertificatesResp.Certificates) < describeListenerCertificatesPageSize || describeListenerCertificatesResp.NextMarker == nil {
break
}
describeListenerCertificatesMarker = describeListenerCertificatesResp.NextMarker
}
// 从证书列表删除证书
// REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_RemoveListenerCertificates.html
if len(certArnsToDelete) > 0 {
d.logger.Info("found alb listener certificates to delete", slog.Any("certificateArns", certArnsToDelete))
removeListenerCertificatesReq := &elasticloadbalancingv2.RemoveListenerCertificatesInput{
ListenerArn: aws.String(d.config.ListenerArn),
Certificates: lo.Map(certArnsToDelete, func(certArn string, _ int) types.Certificate {
return types.Certificate{
CertificateArn: aws.String(certArn),
}
}),
}
removeListenerCertificatesResp, err := d.sdkClients.ELB.RemoveListenerCertificates(ctx, removeListenerCertificatesReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.RemoveListenerCertificates'", slog.Any("request", removeListenerCertificatesReq), slog.Any("response", removeListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.RemoveListenerCertificates': %w", err)
}
}
return nil
}
func createSDKClientELB(authMethod, accessKeyId, secretAccessKey, region string) (*elasticloadbalancingv2.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
@@ -255,3 +413,61 @@ func createSDKClient(authMethod, accessKeyId, secretAccessKey, region string) (*
client := elasticloadbalancingv2.NewFromConfig(cfg)
return client, nil
}
func createSDKClientACM(authMethod, accessKeyId, secretAccessKey, region string) (*acm.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
staticCredsProvider := awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")
imdsCredsProvider := aws.NewCredentialsCache(ec2rolecreds.New())
switch authMethod {
case "":
if accessKeyId != "" && secretAccessKey != "" {
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
}
case AUTH_METHOD_ACCESSKEY:
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
case AUTH_METHOD_IMDS:
opts = append(opts, awscfg.WithCredentialsProvider(imdsCredsProvider))
default:
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
cfg, err := awscfg.LoadDefaultConfig(context.Background(), opts...)
if err != nil {
return nil, err
}
client := acm.NewFromConfig(cfg)
return client, nil
}
func createSDKClientIAM(authMethod, accessKeyId, secretAccessKey, region string) (*iam.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
staticCredsProvider := awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")
imdsCredsProvider := aws.NewCredentialsCache(ec2rolecreds.New())
switch authMethod {
case "":
if accessKeyId != "" && secretAccessKey != "" {
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
}
case AUTH_METHOD_ACCESSKEY:
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
case AUTH_METHOD_IMDS:
opts = append(opts, awscfg.WithCredentialsProvider(imdsCredsProvider))
default:
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
cfg, err := awscfg.LoadDefaultConfig(context.Background(), opts...)
if err != nil {
return nil, err
}
client := iam.NewFromConfig(cfg)
return client, nil
}
+224 -8
View File
@@ -2,19 +2,26 @@ package awsnlb
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
"github.com/aws/aws-sdk-go-v2/service/iam"
"github.com/aws/smithy-go"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimplacm "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm"
cmgrimpliam "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type (
@@ -47,18 +54,34 @@ type DeployerConfig struct {
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *elasticloadbalancingv2.Client
sdkClients *wSDKClients
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
type wSDKClients struct {
ACM *acm.Client
IAM *iam.Client
ELB *elasticloadbalancingv2.Client
}
func NewDeployer(config *DeployerConfig) (*Deployer, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
clientELB, err := createSDKClientELB(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
clientACM, err := createSDKClientACM(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
clientIAM, err := createSDKClientIAM(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
@@ -95,7 +118,7 @@ func NewDeployer(config *DeployerConfig) (*Deployer, error) {
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkClients: &wSDKClients{ELB: clientELB, ACM: clientACM, IAM: clientIAM},
sdkCertmgr: pcertmgr,
}, nil
}
@@ -118,6 +141,12 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
return nil, fmt.Errorf("config `listenerArn` is required")
}
// 解析证书内容
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
// 上传证书
upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
@@ -131,7 +160,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
describeLoadBalancersReq := &elasticloadbalancingv2.DescribeLoadBalancersInput{
LoadBalancerArns: []string{d.config.LoadbalancerArn},
}
describeLoadBalancersResp, err := d.sdkClient.DescribeLoadBalancers(ctx, describeLoadBalancersReq)
describeLoadBalancersResp, err := d.sdkClients.ELB.DescribeLoadBalancers(ctx, describeLoadBalancersReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.DescribeLoadBalancers'", slog.Any("request", describeLoadBalancersReq), slog.Any("response", describeLoadBalancersResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.DescribeLoadBalancers': %w", err)
@@ -145,7 +174,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
LoadBalancerArn: aws.String(d.config.LoadbalancerArn),
ListenerArns: []string{d.config.ListenerArn},
}
describeListenersResp, err := d.sdkClient.DescribeListeners(ctx, describeListenersReq)
describeListenersResp, err := d.sdkClients.ELB.DescribeListeners(ctx, describeListenersReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.DescribeListeners'", slog.Any("request", describeListenersReq), slog.Any("response", describeListenersResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.DescribeListeners': %w", err)
@@ -182,6 +211,10 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
if err := d.updateListenerSniCertificate(ctx, *listenerInfo.ListenerArn, certArn); err != nil {
return nil, err
}
if err := d.pruneListenerSniCertificates(ctx, *listenerInfo.ListenerArn, certArn, certX509.DNSNames); err != nil {
return nil, err
}
}
return &DeployResult{}, nil
@@ -198,7 +231,7 @@ func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudLi
},
},
}
modifyListenerResp, err := d.sdkClient.ModifyListener(ctx, modifyListenerReq)
modifyListenerResp, err := d.sdkClients.ELB.ModifyListener(ctx, modifyListenerReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.ModifyListener'", slog.Any("request", modifyListenerReq), slog.Any("response", modifyListenerResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.ModifyListener': %w", err)
@@ -218,7 +251,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
},
},
}
addListenerCertificatesResp, err := d.sdkClient.AddListenerCertificates(ctx, addListenerCertificatesReq)
addListenerCertificatesResp, err := d.sdkClients.ELB.AddListenerCertificates(ctx, addListenerCertificatesReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.AddListenerCertificates'", slog.Any("request", addListenerCertificatesReq), slog.Any("response", addListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.AddListenerCertificates': %w", err)
@@ -227,7 +260,132 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen
return nil
}
func createSDKClient(authMethod, accessKeyId, secretAccessKey, region string) (*elasticloadbalancingv2.Client, error) {
func (d *Deployer) pruneListenerSniCertificates(ctx context.Context, cloudListenerArn string, cloudCertArn string, cloudCertSANs []string) error {
const acmArnPrefix = "arn:aws:acm:"
const iamArnPrefix = "arn:aws:iam:"
certArnsToDelete := make([]string, 0)
// 查询侦听器证书列表,并找出需要删除绑定的同域名证书
// REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeListenerCertificates.html
// REF: https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetServerCertificate.html
// REF: https://docs.aws.amazon.com/acm/latest/APIReference/API_GetCertificate.html
describeListenerCertificatesMarker := (*string)(nil)
describeListenerCertificatesPageSize := 100
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
describeListenerCertificatesReq := &elasticloadbalancingv2.DescribeListenerCertificatesInput{
ListenerArn: aws.String(cloudListenerArn),
Marker: describeListenerCertificatesMarker,
PageSize: aws.Int32(int32(describeListenerCertificatesPageSize)),
}
describeListenerCertificatesResp, err := d.sdkClients.ELB.DescribeListenerCertificates(ctx, describeListenerCertificatesReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.DescribeListenerCertificates'", slog.Any("request", describeListenerCertificatesReq), slog.Any("response", describeListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.DescribeListenerCertificates': %w", err)
}
for _, certItem := range describeListenerCertificatesResp.Certificates {
if aws.ToString(certItem.CertificateArn) == cloudCertArn {
continue
}
if aws.ToBool(certItem.IsDefault) {
continue
}
switch {
case strings.HasPrefix(aws.ToString(certItem.CertificateArn), acmArnPrefix):
{
getCertificateReq := &acm.GetCertificateInput{
CertificateArn: certItem.CertificateArn,
}
getCertificateResp, err := d.sdkClients.ACM.GetCertificate(ctx, getCertificateReq)
if err != nil {
var sdkerr smithy.APIError
if errors.As(err, &sdkerr) {
if sdkErrCode := sdkerr.ErrorCode(); sdkErrCode == "InvalidArnException" || sdkErrCode == "ResourceNotFoundException" {
continue
}
}
return fmt.Errorf("failed to execute sdk request 'acm.GetCertificate': %w", err)
}
certX509InACM, err := xcert.ParseCertificateFromPEM(aws.ToString(getCertificateResp.Certificate))
if err != nil {
continue
}
if lo.ElementsMatch(cloudCertSANs, certX509InACM.DNSNames) {
certArnsToDelete = append(certArnsToDelete, aws.ToString(certItem.CertificateArn))
}
}
case strings.HasPrefix(aws.ToString(certItem.CertificateArn), iamArnPrefix):
{
getServerCertificateReq := &iam.GetServerCertificateInput{
ServerCertificateName: certItem.CertificateArn,
}
getServerCertificateResp, err := d.sdkClients.IAM.GetServerCertificate(ctx, getServerCertificateReq)
if err != nil {
var sdkerr smithy.APIError
if errors.As(err, &sdkerr) {
if sdkerr.ErrorCode() == "NoSuchEntity" {
continue
}
}
return fmt.Errorf("failed to execute sdk request 'iam.GetServerCertificate': %w", err)
}
certX509InIAM, err := xcert.ParseCertificateFromPEM(aws.ToString(getServerCertificateResp.ServerCertificate.CertificateBody))
if err != nil {
continue
}
if lo.ElementsMatch(cloudCertSANs, certX509InIAM.DNSNames) {
certArnsToDelete = append(certArnsToDelete, aws.ToString(certItem.CertificateArn))
}
}
}
}
if len(describeListenerCertificatesResp.Certificates) < describeListenerCertificatesPageSize || describeListenerCertificatesResp.NextMarker == nil {
break
}
describeListenerCertificatesMarker = describeListenerCertificatesResp.NextMarker
}
// 从证书列表删除证书
// REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_RemoveListenerCertificates.html
if len(certArnsToDelete) > 0 {
d.logger.Info("found nlb listener certificates to delete", slog.Any("certificateArns", certArnsToDelete))
removeListenerCertificatesReq := &elasticloadbalancingv2.RemoveListenerCertificatesInput{
ListenerArn: aws.String(d.config.ListenerArn),
Certificates: lo.Map(certArnsToDelete, func(certArn string, _ int) types.Certificate {
return types.Certificate{
CertificateArn: aws.String(certArn),
}
}),
}
removeListenerCertificatesResp, err := d.sdkClients.ELB.RemoveListenerCertificates(ctx, removeListenerCertificatesReq)
d.logger.Debug("sdk request 'elasticloadbalancingv2.RemoveListenerCertificates'", slog.Any("request", removeListenerCertificatesReq), slog.Any("response", removeListenerCertificatesResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'elasticloadbalancingv2.RemoveListenerCertificates': %w", err)
}
}
return nil
}
func createSDKClientELB(authMethod, accessKeyId, secretAccessKey, region string) (*elasticloadbalancingv2.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
@@ -255,3 +413,61 @@ func createSDKClient(authMethod, accessKeyId, secretAccessKey, region string) (*
client := elasticloadbalancingv2.NewFromConfig(cfg)
return client, nil
}
func createSDKClientACM(authMethod, accessKeyId, secretAccessKey, region string) (*acm.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
staticCredsProvider := awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")
imdsCredsProvider := aws.NewCredentialsCache(ec2rolecreds.New())
switch authMethod {
case "":
if accessKeyId != "" && secretAccessKey != "" {
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
}
case AUTH_METHOD_ACCESSKEY:
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
case AUTH_METHOD_IMDS:
opts = append(opts, awscfg.WithCredentialsProvider(imdsCredsProvider))
default:
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
cfg, err := awscfg.LoadDefaultConfig(context.Background(), opts...)
if err != nil {
return nil, err
}
client := acm.NewFromConfig(cfg)
return client, nil
}
func createSDKClientIAM(authMethod, accessKeyId, secretAccessKey, region string) (*iam.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
staticCredsProvider := awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")
imdsCredsProvider := aws.NewCredentialsCache(ec2rolecreds.New())
switch authMethod {
case "":
if accessKeyId != "" && secretAccessKey != "" {
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
}
case AUTH_METHOD_ACCESSKEY:
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
case AUTH_METHOD_IMDS:
opts = append(opts, awscfg.WithCredentialsProvider(imdsCredsProvider))
default:
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
cfg, err := awscfg.LoadDefaultConfig(context.Background(), opts...)
if err != nil {
return nil, err
}
client := iam.NewFromConfig(cfg)
return client, nil
}
@@ -0,0 +1,48 @@
//go:build tester
package awsacm_test
import (
"testing"
"github.com/stretchr/testify/require"
tester "github.com/certimate-go/certimate/pkg/core/purger/providers-tester"
impl "github.com/certimate-go/certimate/pkg/core/purger/providers/aws-acm"
)
var (
fp = tester.InitArgs("AWSACM_")
fAccessKeyId string
fSecretAccessKey string
fRegion string
)
func init() {
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY")
fp.DefineString(&fRegion, "REGION")
}
/*
Shell command to run this test:
go test -tags=tester -v ./aws_acm_test.go -args \
--AWSACM_ACCESSKEYID="your-access-key-id" \
--AWSACM_SECRETACCESSKEY="your-access-key-secret" \
--AWSACM_REGION="us-east-1"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Purge", func(t *testing.T) {
provider, err := impl.NewPurger(&impl.PurgerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
})
require.NoError(t, err)
tester.Purge(t, provider, tester.PurgeInput{})
})
}
@@ -0,0 +1,187 @@
package awsacm
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
aws "github.com/aws/aws-sdk-go-v2/aws"
awscfg "github.com/aws/aws-sdk-go-v2/config"
awscred "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/aws/aws-sdk-go-v2/service/acm/types"
"github.com/aws/smithy-go"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
xwait "github.com/certimate-go/certimate/pkg/utils/wait"
)
type (
Provider = core.Purger
PurgeResult = core.PurgerPurgeResult
)
type PurgerConfig struct {
// AWS API 认证方式。
// 可取值 "accesskey"、"imds"。
// 零值时默认值 [AUTH_METHOD_ACCESSKEY]。
AuthMethod string `json:"authMethod,omitempty"`
// AWS AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// AWS SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// AWS 区域。
Region string `json:"region"`
}
type Purger struct {
config *PurgerConfig
logger *slog.Logger
sdkClient *acm.Client
}
var _ Provider = (*Purger)(nil)
func NewPurger(config *PurgerConfig) (*Purger, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the purger provider is nil")
}
client, err := createSDKClient(config.AuthMethod, config.AccessKeyId, config.SecretAccessKey, config.Region)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
return &Purger{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (p *Purger) SetLogger(logger *slog.Logger) {
if logger == nil {
p.logger = slog.New(slog.DiscardHandler)
} else {
p.logger = logger
}
}
func (p *Purger) Purge(ctx context.Context, expiry time.Duration) (*PurgeResult, error) {
purgingCertArns := make([]string, 0)
// 获取证书列表,避免重复上传
// REF: https://docs.aws.amazon.com/acm/latest/APIReference/API_ListCertificates.html
listCertificatesNextToken := (*string)(nil)
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
listCertificatesReq := &acm.ListCertificatesInput{
NextToken: listCertificatesNextToken,
MaxItems: aws.Int32(1000),
}
listCertificatesResp, err := p.sdkClient.ListCertificates(ctx, listCertificatesReq)
p.logger.Debug("sdk request 'acm.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'acm.ListCertificates': %w", err)
}
for _, certItem := range listCertificatesResp.CertificateSummaryList {
if certItem.CertificateKeyPairOrigin == types.CertificateKeyPairOriginAcme {
continue
}
if certItem.NotAfter == nil {
continue
}
certNotAfter := lo.FromPtr(certItem.NotAfter)
if time.Since(certNotAfter) >= expiry {
purgingCertArns = append(purgingCertArns, *certItem.CertificateArn)
}
}
if len(listCertificatesResp.CertificateSummaryList) == 0 || listCertificatesResp.NextToken == nil {
break
}
listCertificatesNextToken = listCertificatesResp.NextToken
}
// 删除服务器证书
// REF: https://docs.aws.amazon.com/acm/latest/APIReference/API_DeleteCertificate.html
purgedCount := 0
if len(purgingCertArns) > 0 {
p.logger.Info("found certificates to purge", slog.Any("certArns", purgingCertArns))
if err := xloop.ForRangeAllWithContext(ctx, purgingCertArns, func(ctx context.Context, certArn string, i int) error {
if i > 0 {
if err := xwait.DelayWithContext(ctx, 1*time.Second); err != nil {
return err
}
}
deleteServerCertificateReq := &acm.DeleteCertificateInput{
CertificateArn: aws.String(certArn),
}
deleteServerCertificateResp, err := p.sdkClient.DeleteCertificate(ctx, deleteServerCertificateReq)
p.logger.Debug("sdk request 'acm.DeleteCertificate'", slog.Any("request", deleteServerCertificateReq), slog.Any("response", deleteServerCertificateResp))
if err != nil {
var sdkErr smithy.APIError
if errors.As(err, &sdkErr) {
if sdkErrCode := sdkErr.ErrorCode(); sdkErrCode == "ResourceNotFoundException" || sdkErrCode == "ResourceInUseException" {
return nil
}
}
return fmt.Errorf("failed to execute sdk request 'acm.DeleteCertificate': %w", err)
}
purgedCount++
return nil
}); err != nil {
return nil, err
}
}
return &PurgeResult{
Count: purgedCount,
}, nil
}
func createSDKClient(authMethod, accessKeyId, secretAccessKey, region string) (*acm.Client, error) {
opts := []func(options *awscfg.LoadOptions) error{
awscfg.WithRegion(region),
}
staticCredsProvider := awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")
imdsCredsProvider := aws.NewCredentialsCache(ec2rolecreds.New())
switch authMethod {
case "":
if accessKeyId != "" && secretAccessKey != "" {
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
}
case AUTH_METHOD_ACCESSKEY:
opts = append(opts, awscfg.WithCredentialsProvider(staticCredsProvider))
case AUTH_METHOD_IMDS:
opts = append(opts, awscfg.WithCredentialsProvider(imdsCredsProvider))
default:
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
cfg, err := awscfg.LoadDefaultConfig(context.Background(), opts...)
if err != nil {
return nil, err
}
client := acm.NewFromConfig(cfg)
return client, nil
}
@@ -0,0 +1,10 @@
package awsacm
import (
iam "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-iam"
)
const (
AUTH_METHOD_ACCESSKEY = iam.AUTH_METHOD_ACCESSKEY
AUTH_METHOD_IMDS = iam.AUTH_METHOD_IMDS
)
@@ -5,6 +5,7 @@ import { PURGE_PROVIDERS, type PurgeProviderType } from "@/domain/provider";
import BizPurgeNodeConfigFieldsProviderAliyunCAS from "./BizPurgeNodeConfigFieldsProviderAliyunCAS";
import BizPurgeNodeConfigFieldsProviderAliyunCLB from "./BizPurgeNodeConfigFieldsProviderAliyunCLB";
import BizPurgeNodeConfigFieldsProviderAliyunESA from "./BizPurgeNodeConfigFieldsProviderAliyunESA";
import BizPurgeNodeConfigFieldsProviderAWSACM from "./BizPurgeNodeConfigFieldsProviderAWSACM";
import BizPurgeNodeConfigFieldsProviderAWSIAM from "./BizPurgeNodeConfigFieldsProviderAWSIAM";
import BizPurgeNodeConfigFieldsProviderAzureKeyVault from "./BizPurgeNodeConfigFieldsProviderAzureKeyVault";
import BizPurgeNodeConfigFieldsProviderHuaweiCloudELB from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudELB";
@@ -23,6 +24,7 @@ const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentTyp
[PURGE_PROVIDERS.ALIYUN_CAS]: BizPurgeNodeConfigFieldsProviderAliyunCAS,
[PURGE_PROVIDERS.ALIYUN_CLB]: BizPurgeNodeConfigFieldsProviderAliyunCLB,
[PURGE_PROVIDERS.ALIYUN_ESA]: BizPurgeNodeConfigFieldsProviderAliyunESA,
[PURGE_PROVIDERS.AWS_ACM]: BizPurgeNodeConfigFieldsProviderAWSACM,
[PURGE_PROVIDERS.AWS_IAM]: BizPurgeNodeConfigFieldsProviderAWSIAM,
[PURGE_PROVIDERS.AZURE_KEYVAULT]: BizPurgeNodeConfigFieldsProviderAzureKeyVault,
[PURGE_PROVIDERS.HUAWEICLOUD_ELB]: BizPurgeNodeConfigFieldsProviderHuaweiCloudELB,
@@ -0,0 +1,52 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import { useFormNestedFieldsContext } from "./_context";
const BizPurgeNodeConfigFieldsProviderAWSACM = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "region"]}
initialValue={initialValues.region}
label={t("workflow_node.purge.form.aws_acm_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.purge.form.aws_acm_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.purge.form.aws_acm_region.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
region: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
region: z.string().nonempty(),
});
};
const _default = Object.assign(BizPurgeNodeConfigFieldsProviderAWSACM, {
getInitialValues,
getSchema,
});
export default _default;
+2
View File
@@ -989,6 +989,7 @@ export const PURGE_PROVIDERS = Object.freeze(
ALIYUN_CAS: `${ACCESS_PROVIDERS.ALIYUN}-cas`,
ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`,
ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`,
AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`,
AWS_IAM: `${ACCESS_PROVIDERS.AWS}-iam`,
AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,
BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,
@@ -1025,6 +1026,7 @@ export const purgeProvidersMap: Map<PurgeProvider["type"] | string, PurgeProvide
[PURGE_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud_elb"],
[PURGE_PROVIDERS.HUAWEICLOUD_WAF, "provider.huaweicloud_waf"],
[PURGE_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine_certcenter"],
[PURGE_PROVIDERS.AWS_ACM, "provider.aws_acm"],
[PURGE_PROVIDERS.AWS_IAM, "provider.aws_iam"],
[PURGE_PROVIDERS.AZURE_KEYVAULT, "provider.azure_keyvault"],
[PURGE_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud_cert"],
@@ -3052,6 +3052,11 @@
"placeholder": "Please enter Alibaba Cloud ESA site ID",
"tooltip": "For more information, see <a href=\"https://esa.console.aliyun.com/siteManage/list\" target=\"_blank\">https://esa.console.aliyun.com/siteManage/list</a>"
},
"aws_acm_region": {
"label": "AWS Region",
"placeholder": "Please enter AWS ACM region (e.g. us-east-1)",
"tooltip": "For more information, see <a href=\"https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints</a>"
},
"aws_iam_region": {
"label": "AWS Region",
"placeholder": "Please enter AWS IAM region (e.g. us-east-1)",
@@ -3051,6 +3051,11 @@
"placeholder": "请输入阿里云 ESA 站点 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://esa.console.aliyun.com/siteManage/list\" target=\"_blank\">https://esa.console.aliyun.com/siteManage/list</a>"
},
"aws_acm_region": {
"label": "AWS 服务区域",
"placeholder": "请输入 AWS ACM 服务区域(例如:us-east-1)",
"tooltip": "这是什么?请参阅 <a href=\"https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints\" target=\"_blank\">https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints</a>"
},
"aws_iam_region": {
"label": "AWS 服务区域",
"placeholder": "请输入 AWS IAM 服务区域(例如:us-east-1)",