diff --git a/pkg/core/certifier/challengers/dns01/aws-lightsail/internal/lego.go b/pkg/core/certifier/challengers/dns01/aws-lightsail/internal/lego.go index 9f36596b9..2e3b02bc6 100644 --- a/pkg/core/certifier/challengers/dns01/aws-lightsail/internal/lego.go +++ b/pkg/core/certifier/challengers/dns01/aws-lightsail/internal/lego.go @@ -6,14 +6,13 @@ import ( "strconv" "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/service/lightsail" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lightsail/types" "github.com/go-acme/lego/v5/challenge" "github.com/go-acme/lego/v5/challenge/dns01" "github.com/go-acme/lego/v5/platform/env" + + awslightsailsdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/lightsail" ) const ( @@ -25,8 +24,6 @@ const ( EnvPollingInterval = envNamespace + "POLLING_INTERVAL" ) -const maxRetries = 5 - var _ challenge.ProviderTimeout = (*DNSProvider)(nil) type Config struct { @@ -49,7 +46,7 @@ func NewDefaultConfig() *Config { // 这里有意不使用 lego 提供的 lightsail 实现, // 因为它只支持单个域,无法签发多域名证书。 type DNSProvider struct { - client *lightsail.Client + client *awslightsailsdk.Client config *Config } @@ -65,10 +62,9 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return nil, fmt.Errorf("lightsail: the configuration of the DNS provider is nil") } - ctx := context.Background() - cfg, err := awscfg.LoadDefaultConfig(ctx, - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(config.AccessKeyID, config.SecretAccessKey, config.SessionToken)), - awscfg.WithRegion(config.Region), + client, err := awslightsailsdk.NewClient( + awslightsailsdk.WithAkSk(config.AccessKeyID, config.SecretAccessKey), + awslightsailsdk.WithRegion(config.Region), ) if err != nil { return nil, err @@ -76,7 +72,7 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { return &DNSProvider{ config: config, - client: lightsail.NewFromConfig(cfg), + client: client, }, nil } @@ -88,7 +84,8 @@ func (d *DNSProvider) Present(ctx context.Context, domain, _, keyAuth string) er return fmt.Errorf("lightsail: could not find zone for domain %q: %w", domain, err) } - if _, err := d.client.CreateDomainEntry(ctx, &lightsail.CreateDomainEntryInput{ + // REF: https://docs.aws.amazon.com/lightsail/2016-11-28/api-reference/API_CreateDomainEntry.html + if _, err := d.client.CreateDomainEntryWithContext(ctx, &awslightsailsdk.CreateDomainEntryRequest{ DomainName: aws.String(dns01.UnFqdn(authZone)), DomainEntry: &types.DomainEntry{ Type: aws.String("TXT"), @@ -110,7 +107,8 @@ func (d *DNSProvider) CleanUp(ctx context.Context, domain, _, keyAuth string) er return fmt.Errorf("lightsail: could not find zone for domain %q: %w", domain, err) } - if _, err := d.client.DeleteDomainEntry(ctx, &lightsail.DeleteDomainEntryInput{ + // REF: https://docs.aws.amazon.com/lightsail/2016-11-28/api-reference/API_DeleteDomainEntry.html + if _, err := d.client.DeleteDomainEntryWithContext(ctx, &awslightsailsdk.DeleteDomainEntryRequest{ DomainName: aws.String(dns01.UnFqdn(authZone)), DomainEntry: &types.DomainEntry{ Type: aws.String("TXT"), diff --git a/pkg/core/certmgr/providers/aws-acm/aws_acm.go b/pkg/core/certmgr/providers/aws-acm/aws_acm.go index da6084b1a..3707de997 100644 --- a/pkg/core/certmgr/providers/aws-acm/aws_acm.go +++ b/pkg/core/certmgr/providers/aws-acm/aws_acm.go @@ -8,12 +8,11 @@ import ( "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/service/acm" + "github.com/aws/aws-sdk-go-v2/service/acm/types" "github.com/aws/smithy-go" "github.com/certimate-go/certimate/pkg/core" + awsacmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/acm" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) @@ -35,7 +34,7 @@ type CertmgrConfig struct { type Certmgr struct { config *CertmgrConfig logger *slog.Logger - sdkClient *acm.Client + sdkClient *awsacmsdk.Client } var _ Provider = (*Certmgr)(nil) @@ -89,34 +88,37 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo default: } - listCertificatesReq := &acm.ListCertificatesInput{ + listCertificatesReq := &awsacmsdk.ListCertificatesRequest{ NextToken: listCertificatesNextToken, MaxItems: aws.Int32(1000), + SortBy: types.SortByCreatedAt, + SortOrder: types.SortOrderDescending, } - listCertificatesResp, err := c.sdkClient.ListCertificates(ctx, listCertificatesReq) + listCertificatesResp, err := c.sdkClient.ListCertificatesWithContext(ctx, listCertificatesReq) c.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 !strings.EqualFold(strings.Join(certX509.DNSNames, ","), strings.Join(certItem.SubjectAlternativeNameSummaries, ",")) { + // 对比证书通用名称 + // 注意,虽然文档中描述为包含了备用名称字段,但实际值不完整,因此不能用于判断证书是否相同 + if certItem.DomainName == nil || !strings.EqualFold(certX509.Subject.CommonName, *certItem.DomainName) { continue } // 对比证书有效期 - if certItem.NotBefore == nil || !certX509.NotBefore.Equal(*certItem.NotBefore) { + if certItem.NotBefore == nil || certX509.NotBefore.Unix() != certItem.NotBefore.Unix() { continue - } else if certItem.NotAfter == nil || !certX509.NotAfter.Equal(*certItem.NotAfter) { + } else if certItem.NotAfter == nil || certX509.NotAfter.Unix() != certItem.NotAfter.Unix() { continue } // 对比证书内容 - getCertificateReq := &acm.GetCertificateInput{ + getCertificateReq := &awsacmsdk.GetCertificateRequest{ CertificateArn: certItem.CertificateArn, } - getCertificateResp, err := c.sdkClient.GetCertificate(ctx, getCertificateReq) + getCertificateResp, err := c.sdkClient.GetCertificateWithContext(ctx, getCertificateReq) if err != nil { var sdkErr smithy.APIError if errors.As(err, &sdkErr) { @@ -151,12 +153,12 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo // 导入证书 // REF: https://docs.aws.amazon.com/acm/latest/APIReference/API_ImportCertificate.html - importCertificateReq := &acm.ImportCertificateInput{ + importCertificateReq := &awsacmsdk.ImportCertificateRequest{ Certificate: ([]byte)(serverCertPEM), CertificateChain: ([]byte)(issuerCertPEM), PrivateKey: ([]byte)(privkeyPEM), } - importCertificateResp, err := c.sdkClient.ImportCertificate(ctx, importCertificateReq) + importCertificateResp, err := c.sdkClient.ImportCertificateWithContext(ctx, importCertificateReq) c.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err) @@ -179,13 +181,13 @@ func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, pri // 导入证书 // REF: https://docs.aws.amazon.com/acm/latest/APIReference/API_ImportCertificate.html - importCertificateReq := &acm.ImportCertificateInput{ + importCertificateReq := &awsacmsdk.ImportCertificateRequest{ CertificateArn: aws.String(certIdOrName), Certificate: ([]byte)(serverCertPEM), CertificateChain: ([]byte)(issuerCertPEM), PrivateKey: ([]byte)(privkeyPEM), } - importCertificateResp, err := c.sdkClient.ImportCertificate(ctx, importCertificateReq) + importCertificateResp, err := c.sdkClient.ImportCertificateWithContext(ctx, importCertificateReq) c.logger.Debug("sdk request 'acm.ImportCertificate'", slog.Any("request", importCertificateReq), slog.Any("response", importCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'acm.ImportCertificate': %w", err) @@ -194,15 +196,14 @@ func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, pri return &ReplaceResult{}, nil } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*acm.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsacmsdk.Client, error) { + client, err := awsacmsdk.NewClient( + awsacmsdk.WithAkSk(accessKeyId, secretAccessKey), + awsacmsdk.WithRegion(region), ) if err != nil { return nil, err } - client := acm.NewFromConfig(cfg) return client, nil } diff --git a/pkg/core/certmgr/providers/aws-iam/aws_iam.go b/pkg/core/certmgr/providers/aws-iam/aws_iam.go index 08a88329c..4dbd4a9da 100644 --- a/pkg/core/certmgr/providers/aws-iam/aws_iam.go +++ b/pkg/core/certmgr/providers/aws-iam/aws_iam.go @@ -9,13 +9,12 @@ import ( "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/service/iam" "github.com/aws/smithy-go" "github.com/samber/lo" "github.com/certimate-go/certimate/pkg/core" + awsiamsdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/iam" xcert "github.com/certimate-go/certimate/pkg/utils/cert" ) @@ -40,7 +39,7 @@ type CertmgrConfig struct { type Certmgr struct { config *CertmgrConfig logger *slog.Logger - sdkClient *iam.Client + sdkClient *awsiamsdk.Client } var _ Provider = (*Certmgr)(nil) @@ -99,7 +98,7 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo Marker: listServerCertificatesMarker, MaxItems: aws.Int32(1000), } - listServerCertificatesResp, err := c.sdkClient.ListServerCertificates(ctx, listServerCertificatesReq) + listServerCertificatesResp, err := c.sdkClient.ListServerCertificatesWithContext(ctx, listServerCertificatesReq) c.logger.Debug("sdk request 'iam.ListServerCertificates'", slog.Any("request", listServerCertificatesReq), slog.Any("response", listServerCertificatesResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'iam.ListServerCertificates': %w", err) @@ -112,15 +111,15 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo } // 对比证书有效期 - if certItem.Expiration == nil || !certX509.NotAfter.Equal(*certItem.Expiration) { + if certItem.Expiration == nil || certX509.NotAfter.Unix() != certItem.Expiration.Unix() { continue } // 对比证书内容 - getServerCertificateReq := &iam.GetServerCertificateInput{ + getServerCertificateReq := &awsiamsdk.GetServerCertificateRequest{ ServerCertificateName: certItem.ServerCertificateName, } - getServerCertificateResp, err := c.sdkClient.GetServerCertificate(ctx, getServerCertificateReq) + getServerCertificateResp, err := c.sdkClient.GetServerCertificateWithContext(ctx, getServerCertificateReq) if err != nil { var sdkErr smithy.APIError if errors.As(err, &sdkErr) { @@ -160,14 +159,14 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo // 导入证书 // REF: https://docs.aws.amazon.com/IAM/latest/APIReference/API_UploadServerCertificate.html - uploadServerCertificateReq := &iam.UploadServerCertificateInput{ + uploadServerCertificateReq := &awsiamsdk.UploadServerCertificateRequest{ ServerCertificateName: aws.String(certName), Path: aws.String(cmp.Or(c.config.CertificatePath, "/")), CertificateBody: aws.String(serverCertPEM), CertificateChain: aws.String(issuerCertPEM), PrivateKey: aws.String(privkeyPEM), } - uploadServerCertificateResp, err := c.sdkClient.UploadServerCertificate(ctx, uploadServerCertificateReq) + uploadServerCertificateResp, err := c.sdkClient.UploadServerCertificateWithContext(ctx, uploadServerCertificateReq) c.logger.Debug("sdk request 'iam.UploadServerCertificate'", slog.Any("request", uploadServerCertificateReq), slog.Any("response", uploadServerCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'iam.UploadServerCertificate': %w", err) @@ -187,15 +186,14 @@ func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, pri return nil, core.ErrUnsupported } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*iam.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsiamsdk.Client, error) { + client, err := awsiamsdk.NewClient( + awsiamsdk.WithAkSk(accessKeyId, secretAccessKey), + awsiamsdk.WithRegion(region), ) if err != nil { return nil, err } - client := iam.NewFromConfig(cfg) return client, nil } diff --git a/pkg/core/deployer/providers/aws-alb/aws_alb.go b/pkg/core/deployer/providers/aws-alb/aws_alb.go index e117c7084..d35b9874a 100644 --- a/pkg/core/deployer/providers/aws-alb/aws_alb.go +++ b/pkg/core/deployer/providers/aws-alb/aws_alb.go @@ -6,14 +6,12 @@ import ( "log/slog" 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/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" "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" + awselbsdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/elasticloadbalancingv2" ) type ( @@ -42,7 +40,7 @@ type DeployerConfig struct { type Deployer struct { config *DeployerConfig logger *slog.Logger - sdkClient *elasticloadbalancingv2.Client + sdkClient *awselbsdk.Client sdkCertmgr core.Certmgr } @@ -121,10 +119,10 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 查询负载均衡器 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html - describeLoadBalancersReq := &elasticloadbalancingv2.DescribeLoadBalancersInput{ + describeLoadBalancersReq := &awselbsdk.DescribeLoadBalancersRequest{ LoadBalancerArns: []string{d.config.LoadbalancerArn}, } - describeLoadBalancersResp, err := d.sdkClient.DescribeLoadBalancers(ctx, describeLoadBalancersReq) + describeLoadBalancersResp, err := d.sdkClient.DescribeLoadBalancersWithContext(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) @@ -134,11 +132,11 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 查询侦听器 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeListeners.html - describeListenersReq := &elasticloadbalancingv2.DescribeListenersInput{ + describeListenersReq := &awselbsdk.DescribeListenersRequest{ LoadBalancerArn: aws.String(d.config.LoadbalancerArn), ListenerArns: []string{d.config.ListenerArn}, } - describeListenersResp, err := d.sdkClient.DescribeListeners(ctx, describeListenersReq) + describeListenersResp, err := d.sdkClient.DescribeListenersWithContext(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) @@ -183,7 +181,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudListenerArn string, cloudCertArn string) error { // 更新 HTTPS 侦听器 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_ModifyListener.html - modifyListenerReq := &elasticloadbalancingv2.ModifyListenerInput{ + modifyListenerReq := &awselbsdk.ModifyListenerRequest{ ListenerArn: aws.String(cloudListenerArn), Certificates: []types.Certificate{ { @@ -191,7 +189,7 @@ func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudLi }, }, } - modifyListenerResp, err := d.sdkClient.ModifyListener(ctx, modifyListenerReq) + modifyListenerResp, err := d.sdkClient.ModifyListenerWithContext(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) @@ -203,7 +201,7 @@ func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudLi func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListenerArn string, cloudCertArn string) error { // 将证书添加到证书列表 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_AddListenerCertificates.html - addListenerCertificatesReq := &elasticloadbalancingv2.AddListenerCertificatesInput{ + addListenerCertificatesReq := &awselbsdk.AddListenerCertificatesRequest{ ListenerArn: aws.String(cloudListenerArn), Certificates: []types.Certificate{ { @@ -211,7 +209,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen }, }, } - addListenerCertificatesResp, err := d.sdkClient.AddListenerCertificates(ctx, addListenerCertificatesReq) + addListenerCertificatesResp, err := d.sdkClient.AddListenerCertificatesWithContext(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) @@ -220,15 +218,14 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen return nil } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*elasticloadbalancingv2.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awselbsdk.Client, error) { + client, err := awselbsdk.NewClient( + awselbsdk.WithAkSk(accessKeyId, secretAccessKey), + awselbsdk.WithRegion(region), ) if err != nil { return nil, err } - client := elasticloadbalancingv2.NewFromConfig(cfg) return client, nil } diff --git a/pkg/core/deployer/providers/aws-amplify/aws_amplify.go b/pkg/core/deployer/providers/aws-amplify/aws_amplify.go index 0e299792d..11a9da5e3 100644 --- a/pkg/core/deployer/providers/aws-amplify/aws_amplify.go +++ b/pkg/core/deployer/providers/aws-amplify/aws_amplify.go @@ -6,13 +6,11 @@ import ( "log/slog" 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/service/amplify" "github.com/aws/aws-sdk-go-v2/service/amplify/types" "github.com/certimate-go/certimate/pkg/core" cmgrimplacm "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm" + awsamplifysdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/amplify" ) type ( @@ -39,7 +37,7 @@ type DeployerConfig struct { type Deployer struct { config *DeployerConfig logger *slog.Logger - sdkClient *amplify.Client + sdkClient *awsamplifysdk.Client sdkCertmgr core.Certmgr } @@ -107,7 +105,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 更新域名关联 // REF: https://docs.aws.amazon.com/amplify/latest/APIReference/API_UpdateDomainAssociation.html - updateDomainAssociationReq := &lify.UpdateDomainAssociationInput{ + updateDomainAssociationReq := &awsamplifysdk.UpdateDomainAssociationRequest{ AppId: aws.String(d.config.AppId), DomainName: aws.String(d.config.Domain), CertificateSettings: &types.CertificateSettings{ @@ -115,7 +113,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep CustomCertificateArn: aws.String(upres.ExtendedData["Arn"].(string)), }, } - updateDomainAssociationResp, err := d.sdkClient.UpdateDomainAssociation(ctx, updateDomainAssociationReq) + updateDomainAssociationResp, err := d.sdkClient.UpdateDomainAssociationWithContext(ctx, updateDomainAssociationReq) d.logger.Debug("sdk request 'amplify.UpdateDomainAssociation'", slog.Any("request", updateDomainAssociationReq), slog.Any("response", updateDomainAssociationResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'amplify.UpdateDomainAssociation': %w", err) @@ -124,15 +122,14 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep return &DeployResult{}, nil } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*amplify.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsamplifysdk.Client, error) { + client, err := awsamplifysdk.NewClient( + awsamplifysdk.WithAkSk(accessKeyId, secretAccessKey), + awsamplifysdk.WithRegion(region), ) if err != nil { return nil, err } - client := amplify.NewFromConfig(cfg) return client, nil } diff --git a/pkg/core/deployer/providers/aws-amplify/aws_amplify_test.go b/pkg/core/deployer/providers/aws-amplify/aws_amplify_test.go index 279464938..df62261c6 100644 --- a/pkg/core/deployer/providers/aws-amplify/aws_amplify_test.go +++ b/pkg/core/deployer/providers/aws-amplify/aws_amplify_test.go @@ -48,6 +48,7 @@ func TestProvider(t *testing.T) { AccessKeyId: fAccessKeyId, SecretAccessKey: fSecretAccessKey, Region: fRegion, + AppId: fAppId, Domain: fDomain, CertificateSource: impl.CERTIFICATE_SOURCE_ACM, }) diff --git a/pkg/core/deployer/providers/aws-apigateway/aws_apigateway.go b/pkg/core/deployer/providers/aws-apigateway/aws_apigateway.go index 469715e71..3aae15c06 100644 --- a/pkg/core/deployer/providers/aws-apigateway/aws_apigateway.go +++ b/pkg/core/deployer/providers/aws-apigateway/aws_apigateway.go @@ -6,13 +6,11 @@ import ( "log/slog" 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/service/apigatewayv2" "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types" "github.com/certimate-go/certimate/pkg/core" cmgrimplacm "github.com/certimate-go/certimate/pkg/core/certmgr/providers/aws-acm" + awsapigatewaysdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/apigatewayv2" ) type ( @@ -37,7 +35,7 @@ type DeployerConfig struct { type Deployer struct { config *DeployerConfig logger *slog.Logger - sdkClient *apigatewayv2.Client + sdkClient *awsapigatewaysdk.Client sdkCertmgr core.Certmgr } @@ -102,7 +100,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 更新自定义域名 // REF: https://docs.aws.amazon.com/apigateway/latest/api/API_UpdateDomainName.html - updateDomainNameReq := &apigatewayv2.UpdateDomainNameInput{ + updateDomainNameReq := &awsapigatewaysdk.UpdateDomainNameRequest{ DomainName: aws.String(d.config.Domain), DomainNameConfigurations: []types.DomainNameConfiguration{ { @@ -110,7 +108,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep }, }, } - updateDomainNameResp, err := d.sdkClient.UpdateDomainName(ctx, updateDomainNameReq) + updateDomainNameResp, err := d.sdkClient.UpdateDomainNameWithContext(ctx, updateDomainNameReq) d.logger.Debug("sdk request 'apigatewayv2.UpdateDomainName'", slog.Any("request", updateDomainNameReq), slog.Any("response", updateDomainNameResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'apigatewayv2.UpdateDomainName': %w", err) @@ -119,15 +117,14 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep return &DeployResult{}, nil } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*apigatewayv2.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awsapigatewaysdk.Client, error) { + client, err := awsapigatewaysdk.NewClient( + awsapigatewaysdk.WithAkSk(accessKeyId, secretAccessKey), + awsapigatewaysdk.WithRegion(region), ) if err != nil { return nil, err } - client := apigatewayv2.NewFromConfig(cfg) return client, nil } diff --git a/pkg/core/deployer/providers/aws-clb/aws_clb.go b/pkg/core/deployer/providers/aws-clb/aws_clb.go index e8643c2d5..fa8a5ba31 100644 --- a/pkg/core/deployer/providers/aws-clb/aws_clb.go +++ b/pkg/core/deployer/providers/aws-clb/aws_clb.go @@ -6,13 +6,11 @@ import ( "log/slog" 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/service/elasticloadbalancing" "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" + awselbsdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/elasticloadbalancing" ) type ( @@ -39,7 +37,7 @@ type DeployerConfig struct { type Deployer struct { config *DeployerConfig logger *slog.Logger - sdkClient *elasticloadbalancing.Client + sdkClient *awselbsdk.Client sdkCertmgr core.Certmgr } @@ -118,12 +116,12 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 替换 HTTPS 侦听器 SSL 证书 // REF: https://docs.aws.amazon.com/elasticloadbalancing/2012-06-01/APIReference/API_SetLoadBalancerListenerSSLCertificate.html - setLoadBalancerListenerSSLCertificateReq := &elasticloadbalancing.SetLoadBalancerListenerSSLCertificateInput{ + setLoadBalancerListenerSSLCertificateReq := &awselbsdk.SetLoadBalancerListenerSSLCertificateRequest{ LoadBalancerName: aws.String(d.config.LoadbalancerName), LoadBalancerPort: d.config.LoadbalancerPort, SSLCertificateId: aws.String(upres.ExtendedData["Arn"].(string)), } - setLoadBalancerListenerSSLCertificateResp, err := d.sdkClient.SetLoadBalancerListenerSSLCertificate(ctx, setLoadBalancerListenerSSLCertificateReq) + setLoadBalancerListenerSSLCertificateResp, err := d.sdkClient.SetLoadBalancerListenerSSLCertificateWithContext(ctx, setLoadBalancerListenerSSLCertificateReq) d.logger.Debug("sdk request 'elasticloadbalancing.SetLoadBalancerListenerSSLCertificate'", slog.Any("request", setLoadBalancerListenerSSLCertificateReq), slog.Any("response", setLoadBalancerListenerSSLCertificateResp)) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'elasticloadbalancing.SetLoadBalancerListenerSSLCertificate': %w", err) @@ -132,15 +130,14 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep return &DeployResult{}, nil } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*elasticloadbalancing.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awselbsdk.Client, error) { + client, err := awselbsdk.NewClient( + awselbsdk.WithAkSk(accessKeyId, secretAccessKey), + awselbsdk.WithRegion(region), ) if err != nil { return nil, err } - client := elasticloadbalancing.NewFromConfig(cfg) return client, nil } diff --git a/pkg/core/deployer/providers/aws-nlb/aws_nlb.go b/pkg/core/deployer/providers/aws-nlb/aws_nlb.go index fcb67ae24..419653a6e 100644 --- a/pkg/core/deployer/providers/aws-nlb/aws_nlb.go +++ b/pkg/core/deployer/providers/aws-nlb/aws_nlb.go @@ -6,14 +6,12 @@ import ( "log/slog" 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/service/elasticloadbalancingv2" "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" "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" + awselbsdk "github.com/certimate-go/certimate/pkg/sdk3rd/aws/elasticloadbalancingv2" ) type ( @@ -42,7 +40,7 @@ type DeployerConfig struct { type Deployer struct { config *DeployerConfig logger *slog.Logger - sdkClient *elasticloadbalancingv2.Client + sdkClient *awselbsdk.Client sdkCertmgr core.Certmgr } @@ -121,10 +119,10 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 查询负载均衡器 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeLoadBalancers.html - describeLoadBalancersReq := &elasticloadbalancingv2.DescribeLoadBalancersInput{ + describeLoadBalancersReq := &awselbsdk.DescribeLoadBalancersRequest{ LoadBalancerArns: []string{d.config.LoadbalancerArn}, } - describeLoadBalancersResp, err := d.sdkClient.DescribeLoadBalancers(ctx, describeLoadBalancersReq) + describeLoadBalancersResp, err := d.sdkClient.DescribeLoadBalancersWithContext(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) @@ -134,11 +132,11 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep // 查询侦听器 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_DescribeListeners.html - describeListenersReq := &elasticloadbalancingv2.DescribeListenersInput{ + describeListenersReq := &awselbsdk.DescribeListenersRequest{ LoadBalancerArn: aws.String(d.config.LoadbalancerArn), ListenerArns: []string{d.config.ListenerArn}, } - describeListenersResp, err := d.sdkClient.DescribeListeners(ctx, describeListenersReq) + describeListenersResp, err := d.sdkClient.DescribeListenersWithContext(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) @@ -183,7 +181,7 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudListenerArn string, cloudCertArn string) error { // 更新 HTTPS 侦听器 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_ModifyListener.html - modifyListenerReq := &elasticloadbalancingv2.ModifyListenerInput{ + modifyListenerReq := &awselbsdk.ModifyListenerRequest{ ListenerArn: aws.String(cloudListenerArn), Certificates: []types.Certificate{ { @@ -191,7 +189,7 @@ func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudLi }, }, } - modifyListenerResp, err := d.sdkClient.ModifyListener(ctx, modifyListenerReq) + modifyListenerResp, err := d.sdkClient.ModifyListenerWithContext(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) @@ -203,7 +201,7 @@ func (d *Deployer) updateListenerDefaultCertificate(ctx context.Context, cloudLi func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListenerArn string, cloudCertArn string) error { // 将证书添加到证书列表 // REF: https://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_AddListenerCertificates.html - addListenerCertificatesReq := &elasticloadbalancingv2.AddListenerCertificatesInput{ + addListenerCertificatesReq := &awselbsdk.AddListenerCertificatesRequest{ ListenerArn: aws.String(cloudListenerArn), Certificates: []types.Certificate{ { @@ -211,7 +209,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen }, }, } - addListenerCertificatesResp, err := d.sdkClient.AddListenerCertificates(ctx, addListenerCertificatesReq) + addListenerCertificatesResp, err := d.sdkClient.AddListenerCertificatesWithContext(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) @@ -220,15 +218,14 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudListen return nil } -func createSDKClient(accessKeyId, secretAccessKey, region string) (*elasticloadbalancingv2.Client, error) { - cfg, err := awscfg.LoadDefaultConfig(context.Background(), - awscfg.WithCredentialsProvider(awscred.NewStaticCredentialsProvider(accessKeyId, secretAccessKey, "")), - awscfg.WithRegion(region), +func createSDKClient(accessKeyId, secretAccessKey, region string) (*awselbsdk.Client, error) { + client, err := awselbsdk.NewClient( + awselbsdk.WithAkSk(accessKeyId, secretAccessKey), + awselbsdk.WithRegion(region), ) if err != nil { return nil, err } - client := elasticloadbalancingv2.NewFromConfig(cfg) return client, nil } diff --git a/pkg/sdk3rd/aws/acm/README.md b/pkg/sdk3rd/aws/acm/README.md new file mode 100644 index 000000000..1d408ff58 --- /dev/null +++ b/pkg/sdk3rd/aws/acm/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/acm`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/acm/api_get_certificate.go b/pkg/sdk3rd/aws/acm/api_get_certificate.go new file mode 100644 index 000000000..7a841d1e5 --- /dev/null +++ b/pkg/sdk3rd/aws/acm/api_get_certificate.go @@ -0,0 +1,32 @@ +package acm + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/acm" +) + +type GetCertificateRequest = acm.GetCertificateInput + +type GetCertificateResponse = acm.GetCertificateOutput + +func (c *Client) GetCertificate(req *GetCertificateRequest) (*GetCertificateResponse, error) { + return c.GetCertificateWithContext(context.Background(), req) +} + +func (c *Client) GetCertificateWithContext(ctx context.Context, req *GetCertificateRequest) (*GetCertificateResponse, error) { + httpreq, err := c.newRequest(buildAmzTarget("GetCertificate")) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &GetCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/acm/api_import_certificate.go b/pkg/sdk3rd/aws/acm/api_import_certificate.go new file mode 100644 index 000000000..83c8ac44a --- /dev/null +++ b/pkg/sdk3rd/aws/acm/api_import_certificate.go @@ -0,0 +1,32 @@ +package acm + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/acm" +) + +type ImportCertificateRequest = acm.ImportCertificateInput + +type ImportCertificateResponse = acm.ImportCertificateOutput + +func (c *Client) ImportCertificate(req *ImportCertificateRequest) (*ImportCertificateResponse, error) { + return c.ImportCertificateWithContext(context.Background(), req) +} + +func (c *Client) ImportCertificateWithContext(ctx context.Context, req *ImportCertificateRequest) (*ImportCertificateResponse, error) { + httpreq, err := c.newRequest(buildAmzTarget("ImportCertificate")) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ImportCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/acm/api_list_certificates.go b/pkg/sdk3rd/aws/acm/api_list_certificates.go new file mode 100644 index 000000000..e35fb5080 --- /dev/null +++ b/pkg/sdk3rd/aws/acm/api_list_certificates.go @@ -0,0 +1,32 @@ +package acm + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/acm" +) + +type ListCertificatesRequest = acm.ListCertificatesInput + +type ListCertificatesResponse = acm.ListCertificatesOutput + +func (c *Client) ListCertificates(req *ListCertificatesRequest) (*ListCertificatesResponse, error) { + return c.ListCertificatesWithContext(context.Background(), req) +} + +func (c *Client) ListCertificatesWithContext(ctx context.Context, req *ListCertificatesRequest) (*ListCertificatesResponse, error) { + httpreq, err := c.newRequest(buildAmzTarget("ListCertificates")) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ListCertificatesResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/acm/client.go b/pkg/sdk3rd/aws/acm/client.go new file mode 100644 index 000000000..2a49c1b96 --- /dev/null +++ b/pkg/sdk3rd/aws/acm/client.go @@ -0,0 +1,116 @@ +// A simple SDK client for AWS ACM. +// API documentation: https://docs.aws.amazon.com/acm/ +package acm + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocoljson "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "acm" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(baseUrl). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/x-amz-json-1.1"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(xAmzTarget string) (*resty.Request, error) { + req := c.rc.R() + req.Method = http.MethodPost + req.URL = "/" + if xAmzTarget != "" { + req.Header.Set("X-Amz-Target", xAmzTarget) + } + + // WARN: + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocoljson.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + dezer := smithyprotocoljson.NewDeserializer() + dezer.UseEpochTime() + if err := dezer.Deserialize(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/acm/options.go b/pkg/sdk3rd/aws/acm/options.go new file mode 100644 index 000000000..35510fb3c --- /dev/null +++ b/pkg/sdk3rd/aws/acm/options.go @@ -0,0 +1,18 @@ +package acm + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/acm/utils.go b/pkg/sdk3rd/aws/acm/utils.go new file mode 100644 index 000000000..07b53ae8b --- /dev/null +++ b/pkg/sdk3rd/aws/acm/utils.go @@ -0,0 +1,5 @@ +package acm + +func buildAmzTarget(action string) string { + return "CertificateManager." + action +} diff --git a/pkg/sdk3rd/aws/amplify/README.md b/pkg/sdk3rd/aws/amplify/README.md new file mode 100644 index 000000000..00e3721d4 --- /dev/null +++ b/pkg/sdk3rd/aws/amplify/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/amplify`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/amplify/api_update_domain_association.go b/pkg/sdk3rd/aws/amplify/api_update_domain_association.go new file mode 100644 index 000000000..3f84c7acd --- /dev/null +++ b/pkg/sdk3rd/aws/amplify/api_update_domain_association.go @@ -0,0 +1,47 @@ +package amplify + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/aws/aws-sdk-go-v2/service/amplify" +) + +type UpdateDomainAssociationRequest = amplify.UpdateDomainAssociationInput + +type UpdateDomainAssociationResponse = amplify.UpdateDomainAssociationOutput + +func (c *Client) UpdateDomainAssociation(req *UpdateDomainAssociationRequest) (*UpdateDomainAssociationResponse, error) { + return c.UpdateDomainAssociationWithContext(context.Background(), req) +} + +func (c *Client) UpdateDomainAssociationWithContext(ctx context.Context, req *UpdateDomainAssociationRequest) (*UpdateDomainAssociationResponse, error) { + if req.AppId == nil { + return nil, fmt.Errorf("sdkerr: bad request: unset appId") + } + if req.DomainName == nil { + return nil, fmt.Errorf("sdkerr: bad request: unset domainName") + } + + path := fmt.Sprintf("/apps/%s/domains/%s", url.PathEscape(*req.AppId), url.PathEscape(*req.DomainName)) + httpreq, err := c.newRequest(http.MethodPost, path, req) + if err != nil { + return nil, err + } else { + if m, ok := httpreq.Body.(map[string]any); ok { + delete(m, "appId") + delete(m, "domainName") + } + + httpreq.SetContext(ctx) + } + + result := &UpdateDomainAssociationResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/amplify/client.go b/pkg/sdk3rd/aws/amplify/client.go new file mode 100644 index 000000000..1dd931f1e --- /dev/null +++ b/pkg/sdk3rd/aws/amplify/client.go @@ -0,0 +1,129 @@ +// A simple SDK client for AWS Amplify. +// API documentation: https://docs.aws.amazon.com/amplify/ +package amplify + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocoljson "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "amplify" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(baseUrl). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + req := c.rc.R() + req.Method = method + req.URL = path + + if params != nil { + sezer := smithyprotocoljson.NewSerializer() + sezer.UseCamelCaseNamePolicy() + sezer.UseOmitEmptyValue() + paramsMap, _ := sezer.SerializeToMap(params) + + req.SetBody(paramsMap) + } + + // WARN: + // DO NOT CALL `req.SetBody` or `req.SetFormData` LATER! USE `newRequest` INSTEAD. + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocoljson.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := json.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/amplify/options.go b/pkg/sdk3rd/aws/amplify/options.go new file mode 100644 index 000000000..97ed9e7c6 --- /dev/null +++ b/pkg/sdk3rd/aws/amplify/options.go @@ -0,0 +1,18 @@ +package amplify + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/apigatewayv2/README.md b/pkg/sdk3rd/aws/apigatewayv2/README.md new file mode 100644 index 000000000..4b0e4aebf --- /dev/null +++ b/pkg/sdk3rd/aws/apigatewayv2/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/apigatewayv2`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/apigatewayv2/api_update_domain_name.go b/pkg/sdk3rd/aws/apigatewayv2/api_update_domain_name.go new file mode 100644 index 000000000..ac06f37c7 --- /dev/null +++ b/pkg/sdk3rd/aws/apigatewayv2/api_update_domain_name.go @@ -0,0 +1,43 @@ +package apigatewayv2 + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/aws/aws-sdk-go-v2/service/apigatewayv2" +) + +type UpdateDomainNameRequest = apigatewayv2.UpdateDomainNameInput + +type UpdateDomainNameResponse = apigatewayv2.UpdateDomainNameOutput + +func (c *Client) UpdateDomainName(req *UpdateDomainNameRequest) (*UpdateDomainNameResponse, error) { + return c.UpdateDomainNameWithContext(context.Background(), req) +} + +func (c *Client) UpdateDomainNameWithContext(ctx context.Context, req *UpdateDomainNameRequest) (*UpdateDomainNameResponse, error) { + if req.DomainName == nil { + return nil, fmt.Errorf("sdkerr: bad request: unset domainName") + } + + path := fmt.Sprintf("/domainnames/%s", url.PathEscape(*req.DomainName)) + httpreq, err := c.newRequest(http.MethodPatch, path, req) + if err != nil { + return nil, err + } else { + if m, ok := httpreq.Body.(map[string]any); ok { + delete(m, "domainName") + } + + httpreq.SetContext(ctx) + } + + result := &UpdateDomainNameResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/apigatewayv2/client.go b/pkg/sdk3rd/aws/apigatewayv2/client.go new file mode 100644 index 000000000..f331761aa --- /dev/null +++ b/pkg/sdk3rd/aws/apigatewayv2/client.go @@ -0,0 +1,129 @@ +// A simple SDK client for AWS API Gateway v2. +// API documentation: https://docs.aws.amazon.com/apigateway/ +package apigatewayv2 + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocoljson "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "apigateway" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(strings.TrimSuffix(baseUrl, "/")+"/v2"). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) { + if method == "" { + return nil, fmt.Errorf("sdkerr: unset method") + } + if path == "" { + return nil, fmt.Errorf("sdkerr: unset path") + } + + req := c.rc.R() + req.Method = method + req.URL = path + + if params != nil { + sezer := smithyprotocoljson.NewSerializer() + sezer.UseCamelCaseNamePolicy() + sezer.UseOmitEmptyValue() + paramsMap, _ := sezer.SerializeToMap(params) + + req.SetBody(paramsMap) + } + + // WARN: + // DO NOT CALL `req.SetBody` or `req.SetFormData` LATER! USE `newRequest` INSTEAD. + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocoljson.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + if err := json.Unmarshal(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/apigatewayv2/options.go b/pkg/sdk3rd/aws/apigatewayv2/options.go new file mode 100644 index 000000000..b37a1af0c --- /dev/null +++ b/pkg/sdk3rd/aws/apigatewayv2/options.go @@ -0,0 +1,18 @@ +package apigatewayv2 + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancing/README.md b/pkg/sdk3rd/aws/elasticloadbalancing/README.md new file mode 100644 index 000000000..9691b6934 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancing/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/elasticloadbalancing/api_set_load_balancer_listener_ssl_certificate.go b/pkg/sdk3rd/aws/elasticloadbalancing/api_set_load_balancer_listener_ssl_certificate.go new file mode 100644 index 000000000..76f53bad4 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancing/api_set_load_balancer_listener_ssl_certificate.go @@ -0,0 +1,42 @@ +package elasticloadbalancing + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing" +) + +type SetLoadBalancerListenerSSLCertificateRequest = elasticloadbalancing.SetLoadBalancerListenerSSLCertificateInput + +type SetLoadBalancerListenerSSLCertificateResponse = elasticloadbalancing.SetLoadBalancerListenerSSLCertificateOutput + +func (c *Client) SetLoadBalancerListenerSSLCertificate(req *SetLoadBalancerListenerSSLCertificateRequest) (*SetLoadBalancerListenerSSLCertificateResponse, error) { + return c.SetLoadBalancerListenerSSLCertificateWithContext(context.Background(), req) +} + +func (c *Client) SetLoadBalancerListenerSSLCertificateWithContext(ctx context.Context, req *SetLoadBalancerListenerSSLCertificateRequest) (*SetLoadBalancerListenerSSLCertificateResponse, error) { + params := &struct { + SetLoadBalancerListenerSSLCertificateRequest `json:",inline"` + Action string + Version string + }{ + SetLoadBalancerListenerSSLCertificateRequest: *req, + Action: "SetLoadBalancerListenerSSLCertificate", + Version: "2012-06-01", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &SetLoadBalancerListenerSSLCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancing/client.go b/pkg/sdk3rd/aws/elasticloadbalancing/client.go new file mode 100644 index 000000000..7f8a86118 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancing/client.go @@ -0,0 +1,133 @@ +// A simple SDK client for AWS Elastic Load Balancing. +// API documentation: https://docs.aws.amazon.com/elasticloadbalancing/ +package elasticloadbalancing + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocolquery "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "elasticloadbalancing" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(baseUrl). + SetHeader("Accept", "application/xml"). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(params any) (*resty.Request, error) { + req := c.rc.R() + req.Method = http.MethodPost + req.URL = "/" + + if params != nil { + sezer := smithyprotocolquery.NewSerializer() + sezer.UseOmitEmptyValue() + paramsMap, _ := sezer.SerializeToMap(params) + + if paramsMap["Action"] == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset action in params") + } + if paramsMap["Version"] == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset version in params") + } + + req.SetFormData(paramsMap) + } + + // WARN: + // DO NOT CALL `req.SetBody` or `req.SetFormData` AGAIN! USE `newRequest` INSTEAD. + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocolquery.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + action := "" + if req.FormData != nil { + action = req.FormData.Get("Action") + } + + dezer := smithyprotocolquery.NewDeserializer(action) + if err := dezer.Deserialize(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancing/options.go b/pkg/sdk3rd/aws/elasticloadbalancing/options.go new file mode 100644 index 000000000..f9460b0c4 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancing/options.go @@ -0,0 +1,18 @@ +package elasticloadbalancing + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/README.md b/pkg/sdk3rd/aws/elasticloadbalancingv2/README.md new file mode 100644 index 000000000..5dfc98357 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/api_add_listener_certificates.go b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_add_listener_certificates.go new file mode 100644 index 000000000..edf0afb02 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_add_listener_certificates.go @@ -0,0 +1,42 @@ +package elasticloadbalancingv2 + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" +) + +type AddListenerCertificatesRequest = elasticloadbalancingv2.AddListenerCertificatesInput + +type AddListenerCertificatesResponse = elasticloadbalancingv2.AddListenerCertificatesOutput + +func (c *Client) AddListenerCertificates(req *AddListenerCertificatesRequest) (*AddListenerCertificatesResponse, error) { + return c.AddListenerCertificatesWithContext(context.Background(), req) +} + +func (c *Client) AddListenerCertificatesWithContext(ctx context.Context, req *AddListenerCertificatesRequest) (*AddListenerCertificatesResponse, error) { + params := &struct { + AddListenerCertificatesRequest `json:",inline"` + Action string + Version string + }{ + AddListenerCertificatesRequest: *req, + Action: "AddListenerCertificates", + Version: "2015-12-01", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &AddListenerCertificatesResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/api_describe_listeners.go b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_describe_listeners.go new file mode 100644 index 000000000..e27a80c15 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_describe_listeners.go @@ -0,0 +1,42 @@ +package elasticloadbalancingv2 + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" +) + +type DescribeListenersRequest = elasticloadbalancingv2.DescribeListenersInput + +type DescribeListenersResponse = elasticloadbalancingv2.DescribeListenersOutput + +func (c *Client) DescribeListeners(req *DescribeListenersRequest) (*DescribeListenersResponse, error) { + return c.DescribeListenersWithContext(context.Background(), req) +} + +func (c *Client) DescribeListenersWithContext(ctx context.Context, req *DescribeListenersRequest) (*DescribeListenersResponse, error) { + params := &struct { + DescribeListenersRequest `json:",inline"` + Action string + Version string + }{ + DescribeListenersRequest: *req, + Action: "DescribeListeners", + Version: "2015-12-01", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &DescribeListenersResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/api_describe_load_balancers.go b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_describe_load_balancers.go new file mode 100644 index 000000000..e858a9c2f --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_describe_load_balancers.go @@ -0,0 +1,42 @@ +package elasticloadbalancingv2 + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" +) + +type DescribeLoadBalancersRequest = elasticloadbalancingv2.DescribeLoadBalancersInput + +type DescribeLoadBalancersResponse = elasticloadbalancingv2.DescribeLoadBalancersOutput + +func (c *Client) DescribeLoadBalancers(req *DescribeLoadBalancersRequest) (*DescribeLoadBalancersResponse, error) { + return c.DescribeLoadBalancersWithContext(context.Background(), req) +} + +func (c *Client) DescribeLoadBalancersWithContext(ctx context.Context, req *DescribeLoadBalancersRequest) (*DescribeLoadBalancersResponse, error) { + params := &struct { + DescribeLoadBalancersRequest `json:",inline"` + Action string + Version string + }{ + DescribeLoadBalancersRequest: *req, + Action: "DescribeLoadBalancers", + Version: "2015-12-01", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &DescribeLoadBalancersResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/api_modify_listener.go b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_modify_listener.go new file mode 100644 index 000000000..9ff80d154 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/api_modify_listener.go @@ -0,0 +1,42 @@ +package elasticloadbalancingv2 + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" +) + +type ModifyListenerRequest = elasticloadbalancingv2.ModifyListenerInput + +type ModifyListenerResponse = elasticloadbalancingv2.ModifyListenerOutput + +func (c *Client) ModifyListener(req *ModifyListenerRequest) (*ModifyListenerResponse, error) { + return c.ModifyListenerWithContext(context.Background(), req) +} + +func (c *Client) ModifyListenerWithContext(ctx context.Context, req *ModifyListenerRequest) (*ModifyListenerResponse, error) { + params := &struct { + ModifyListenerRequest `json:",inline"` + Action string + Version string + }{ + ModifyListenerRequest: *req, + Action: "ModifyListener", + Version: "2015-12-01", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ModifyListenerResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/client.go b/pkg/sdk3rd/aws/elasticloadbalancingv2/client.go new file mode 100644 index 000000000..8b2a8b231 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/client.go @@ -0,0 +1,133 @@ +// A simple SDK client for AWS Elastic Load Balancing v2. +// API documentation: https://docs.aws.amazon.com/elasticloadbalancing/ +package elasticloadbalancingv2 + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocolquery "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "elasticloadbalancing" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(baseUrl). + SetHeader("Accept", "application/xml"). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(params any) (*resty.Request, error) { + req := c.rc.R() + req.Method = http.MethodPost + req.URL = "/" + + if params != nil { + sezer := smithyprotocolquery.NewSerializer() + sezer.UseOmitEmptyValue() + paramsMap, _ := sezer.SerializeToMap(params) + + if paramsMap["Action"] == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset action in params") + } + if paramsMap["Version"] == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset version in params") + } + + req.SetFormData(paramsMap) + } + + // WARN: + // DO NOT CALL `req.SetBody` or `req.SetFormData` AGAIN! USE `newRequest` INSTEAD. + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocolquery.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + action := "" + if req.FormData != nil { + action = req.FormData.Get("Action") + } + + dezer := smithyprotocolquery.NewDeserializer(action) + if err := dezer.Deserialize(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/elasticloadbalancingv2/options.go b/pkg/sdk3rd/aws/elasticloadbalancingv2/options.go new file mode 100644 index 000000000..84ec368c6 --- /dev/null +++ b/pkg/sdk3rd/aws/elasticloadbalancingv2/options.go @@ -0,0 +1,18 @@ +package elasticloadbalancingv2 + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/iam/README.md b/pkg/sdk3rd/aws/iam/README.md new file mode 100644 index 000000000..596b73248 --- /dev/null +++ b/pkg/sdk3rd/aws/iam/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/iam`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/iam/api_get_server_certificate.go b/pkg/sdk3rd/aws/iam/api_get_server_certificate.go new file mode 100644 index 000000000..387f45e54 --- /dev/null +++ b/pkg/sdk3rd/aws/iam/api_get_server_certificate.go @@ -0,0 +1,42 @@ +package iam + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" +) + +type GetServerCertificateRequest = iam.GetServerCertificateInput + +type GetServerCertificateResponse = iam.GetServerCertificateOutput + +func (c *Client) GetServerCertificate(req *GetServerCertificateRequest) (*GetServerCertificateResponse, error) { + return c.GetServerCertificateWithContext(context.Background(), req) +} + +func (c *Client) GetServerCertificateWithContext(ctx context.Context, req *GetServerCertificateRequest) (*GetServerCertificateResponse, error) { + params := &struct { + GetServerCertificateRequest `json:",inline"` + Action string + Version string + }{ + GetServerCertificateRequest: *req, + Action: "GetServerCertificate", + Version: "2010-05-08", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &GetServerCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/iam/api_list_server_certificates.go b/pkg/sdk3rd/aws/iam/api_list_server_certificates.go new file mode 100644 index 000000000..2f51dbb38 --- /dev/null +++ b/pkg/sdk3rd/aws/iam/api_list_server_certificates.go @@ -0,0 +1,42 @@ +package iam + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" +) + +type ListServerCertificatesRequest = iam.ListServerCertificatesInput + +type ListServerCertificatesResponse = iam.ListServerCertificatesOutput + +func (c *Client) ListServerCertificates(req *ListServerCertificatesRequest) (*ListServerCertificatesResponse, error) { + return c.ListServerCertificatesWithContext(context.Background(), req) +} + +func (c *Client) ListServerCertificatesWithContext(ctx context.Context, req *ListServerCertificatesRequest) (*ListServerCertificatesResponse, error) { + params := &struct { + ListServerCertificatesRequest `json:",inline"` + Action string + Version string + }{ + ListServerCertificatesRequest: *req, + Action: "ListServerCertificates", + Version: "2010-05-08", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ListServerCertificatesResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/iam/api_upload_server_certificate.go b/pkg/sdk3rd/aws/iam/api_upload_server_certificate.go new file mode 100644 index 000000000..b43cd43e3 --- /dev/null +++ b/pkg/sdk3rd/aws/iam/api_upload_server_certificate.go @@ -0,0 +1,42 @@ +package iam + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/iam" +) + +type UploadServerCertificateRequest = iam.UploadServerCertificateInput + +type UploadServerCertificateResponse = iam.UploadServerCertificateOutput + +func (c *Client) UploadServerCertificate(req *UploadServerCertificateRequest) (*UploadServerCertificateResponse, error) { + return c.UploadServerCertificateWithContext(context.Background(), req) +} + +func (c *Client) UploadServerCertificateWithContext(ctx context.Context, req *UploadServerCertificateRequest) (*UploadServerCertificateResponse, error) { + params := &struct { + UploadServerCertificateRequest `json:",inline"` + Action string + Version string + }{ + UploadServerCertificateRequest: *req, + Action: "UploadServerCertificate", + Version: "2010-05-08", + } + + httpreq, err := c.newRequest(params) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &UploadServerCertificateResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/iam/client.go b/pkg/sdk3rd/aws/iam/client.go new file mode 100644 index 000000000..8d691557d --- /dev/null +++ b/pkg/sdk3rd/aws/iam/client.go @@ -0,0 +1,133 @@ +// A simple SDK client for AWS IAM. +// API documentation: https://docs.aws.amazon.com/iam/ +package iam + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocolquery "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "iam" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(baseUrl). + SetHeader("Accept", "application/xml"). + SetHeader("Content-Type", "application/x-www-form-urlencoded"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(params any) (*resty.Request, error) { + req := c.rc.R() + req.Method = http.MethodPost + req.URL = "/" + + if params != nil { + sezer := smithyprotocolquery.NewSerializer() + sezer.UseOmitEmptyValue() + paramsMap, _ := sezer.SerializeToMap(params) + + if paramsMap["Action"] == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset action in params") + } + if paramsMap["Version"] == "" { + return nil, fmt.Errorf("sdkerr: bad request: unset version in params") + } + + req.SetFormData(paramsMap) + } + + // WARN: + // DO NOT CALL `req.SetBody` or `req.SetFormData` AGAIN! USE `newRequest` INSTEAD. + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocolquery.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + action := "" + if req.FormData != nil { + action = req.FormData.Get("Action") + } + + dezer := smithyprotocolquery.NewDeserializer(action) + if err := dezer.Deserialize(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/iam/options.go b/pkg/sdk3rd/aws/iam/options.go new file mode 100644 index 000000000..56c9419fa --- /dev/null +++ b/pkg/sdk3rd/aws/iam/options.go @@ -0,0 +1,18 @@ +package iam + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/lightsail/README.md b/pkg/sdk3rd/aws/lightsail/README.md new file mode 100644 index 000000000..c7f735a72 --- /dev/null +++ b/pkg/sdk3rd/aws/lightsail/README.md @@ -0,0 +1,5 @@ +本模块存在相应的官方 SDK:`github.com/aws/aws-sdk-go-v2/service/lightsail`。 + +但会使得依赖体积显著膨胀,故没有使用。 + +具体细节见 [PR #1388](https://github.com/certimate-go/certimate/pull/1388)。 diff --git a/pkg/sdk3rd/aws/lightsail/api_create_domain_entry.go b/pkg/sdk3rd/aws/lightsail/api_create_domain_entry.go new file mode 100644 index 000000000..dfad6c8c9 --- /dev/null +++ b/pkg/sdk3rd/aws/lightsail/api_create_domain_entry.go @@ -0,0 +1,32 @@ +package lightsail + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/lightsail" +) + +type CreateDomainEntryRequest = lightsail.CreateDomainEntryInput + +type CreateDomainEntryResponse = lightsail.CreateDomainEntryOutput + +func (c *Client) CreateDomainEntry(req *CreateDomainEntryRequest) (*CreateDomainEntryResponse, error) { + return c.CreateDomainEntryWithContext(context.Background(), req) +} + +func (c *Client) CreateDomainEntryWithContext(ctx context.Context, req *CreateDomainEntryRequest) (*CreateDomainEntryResponse, error) { + httpreq, err := c.newRequest(buildAmzTarget("CreateDomainEntry")) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &CreateDomainEntryResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/lightsail/api_delete_domain_entry.go b/pkg/sdk3rd/aws/lightsail/api_delete_domain_entry.go new file mode 100644 index 000000000..3272b474f --- /dev/null +++ b/pkg/sdk3rd/aws/lightsail/api_delete_domain_entry.go @@ -0,0 +1,32 @@ +package lightsail + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/lightsail" +) + +type DeleteDomainEntryRequest = lightsail.DeleteDomainEntryInput + +type DeleteDomainEntryResponse = lightsail.DeleteDomainEntryOutput + +func (c *Client) DeleteDomainEntry(req *DeleteDomainEntryRequest) (*DeleteDomainEntryResponse, error) { + return c.DeleteDomainEntryWithContext(context.Background(), req) +} + +func (c *Client) DeleteDomainEntryWithContext(ctx context.Context, req *DeleteDomainEntryRequest) (*DeleteDomainEntryResponse, error) { + httpreq, err := c.newRequest(buildAmzTarget("DeleteDomainEntry")) + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &DeleteDomainEntryResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/sdk3rd/aws/lightsail/client.go b/pkg/sdk3rd/aws/lightsail/client.go new file mode 100644 index 000000000..1306b52ab --- /dev/null +++ b/pkg/sdk3rd/aws/lightsail/client.go @@ -0,0 +1,116 @@ +// A simple SDK client for AWS Lightsail. +// API documentation: https://docs.aws.amazon.com/lightsail/ +package lightsail + +import ( + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" + + "github.com/certimate-go/certimate/internal/app" + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" + smithyprotocoljson "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson" +) + +type Client struct { + rc *resty.Client +} + +func NewClient(optFns ...OptionsFunc) (*Client, error) { + options := &Options{} + for _, fn := range optFns { + fn(options) + } + + if options.AccessKeyId == "" { + return nil, fmt.Errorf("sdkerr: unset accessKeyId") + } + if options.SecretAccessKey == "" { + return nil, fmt.Errorf("sdkerr: unset secretAccessKey") + } + + service := "lightsail" + region := strings.TrimSpace(options.Region) + baseUrl, err := common.ResolveBaseEndpoint(service, region, common.EndpointVariantNone) + if err != nil { + return nil, fmt.Errorf("sdkerr: %w", err) + } + + signer := common.NewSigner(options.AccessKeyId, options.SecretAccessKey, service, region) + httper := resty.New(). + SetBaseURL(baseUrl). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/x-amz-json-1.1"). + SetHeader("User-Agent", app.AppUserAgent). + SetPreRequestHook(func(_ *resty.Client, req *http.Request) error { + if err := signer.Sign(req); err != nil { + return fmt.Errorf("sdkerr: sign error: %w", err) + } + + return nil + }) + + return &Client{rc: httper}, nil +} + +func (c *Client) SetTimeout(timeout time.Duration) *Client { + c.rc.SetTimeout(timeout) + return c +} + +func (c *Client) newRequest(xAmzTarget string) (*resty.Request, error) { + req := c.rc.R() + req.Method = http.MethodPost + req.URL = "/" + if xAmzTarget != "" { + req.Header.Set("X-Amz-Target", xAmzTarget) + } + + // WARN: + // DO NOT CALL `req.SetResult` or `req.SetError` LATER! USE `doRequestWithResult` INSTEAD. + return req, nil +} + +func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("sdkerr: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String()) + } + + return resp, nil +} + +func (c *Client) doRequestWithResult(req *resty.Request, res any) (*resty.Response, error) { + if req == nil { + return nil, fmt.Errorf("sdkerr: nil request") + } + + resp, err := c.doRequest(req) + if err != nil { + if resp != nil { + if sdkErr, _ := smithyprotocoljson.GetAPIErrorWithRawResponse(resp.Body(), resp.RawResponse); sdkErr != nil { + return resp, sdkErr + } + } + return resp, err + } + + if len(resp.Body()) != 0 { + dezer := smithyprotocoljson.NewDeserializer() + dezer.UseEpochTime() + if err := dezer.Deserialize(resp.Body(), &res); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String()) + } + } + + return resp, nil +} diff --git a/pkg/sdk3rd/aws/lightsail/options.go b/pkg/sdk3rd/aws/lightsail/options.go new file mode 100644 index 000000000..660b9122b --- /dev/null +++ b/pkg/sdk3rd/aws/lightsail/options.go @@ -0,0 +1,18 @@ +package lightsail + +import ( + common "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-common" +) + +type ( + Options = common.Options + OptionsFunc = common.OptionsFunc +) + +func WithAkSk(ak, sk string) OptionsFunc { + return common.WithAkSk(ak, sk) +} + +func WithRegion(region string) OptionsFunc { + return common.WithRegion(region) +} diff --git a/pkg/sdk3rd/aws/lightsail/utils.go b/pkg/sdk3rd/aws/lightsail/utils.go new file mode 100644 index 000000000..3c3f76aac --- /dev/null +++ b/pkg/sdk3rd/aws/lightsail/utils.go @@ -0,0 +1,5 @@ +package lightsail + +func buildAmzTarget(action string) string { + return "Lightsail_20161128." + action +} diff --git a/pkg/sdk3rd/aws/zz-shared-common/endpoint.go b/pkg/sdk3rd/aws/zz-shared-common/endpoint.go new file mode 100644 index 000000000..33c2bef04 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-common/endpoint.go @@ -0,0 +1,133 @@ +package common + +import ( + "fmt" + "regexp" + "strings" +) + +type EndpointVariant int + +const ( + EndpointVariantNone EndpointVariant = iota + EndpointVariantFIPS + EndpointVariantDualStack +) + +type EndpointPartition struct { + ID string + RegionRegex *regexp.Regexp + Templates map[EndpointVariant]string +} + +var endpointPartitions = []EndpointPartition{ + { + ID: "aws", + RegionRegex: regexp.MustCompile(`^(us|eu|ap|sa|ca|me|af|il|mx)-\w+-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.amazonaws.com", + EndpointVariantFIPS: "https://{service}-fips.{region}.amazonaws.com", + EndpointVariantDualStack: "https://{service}.{region}.api.aws", + EndpointVariantFIPS | EndpointVariantDualStack: "https://{service}-fips.{region}.api.aws", + }, + }, + { + ID: "aws-cn", + RegionRegex: regexp.MustCompile(`^cn-\w+-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.amazonaws.com.cn", + EndpointVariantFIPS: "https://{service}-fips.{region}.amazonaws.com.cn", + EndpointVariantDualStack: "https://{service}.{region}.api.amazonwebservices.com.cn", + EndpointVariantFIPS | EndpointVariantDualStack: "https://{service}-fips.{region}.api.amazonwebservices.com.cn", + }, + }, + { + ID: "aws-eusc", + RegionRegex: regexp.MustCompile(`^eusc\-(de)\-\w+\-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.amazonaws.eu", + EndpointVariantFIPS: "https://{service}-fips.{region}.amazonaws.eu", + EndpointVariantDualStack: "https://{service}.{region}.api.amazonwebservices.eu", + EndpointVariantFIPS | EndpointVariantDualStack: "https://{service}-fips.{region}.api.amazonwebservices.eu", + }, + }, + { + ID: "aws-iso", + RegionRegex: regexp.MustCompile(`^us\-iso\-\w+\-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.c2s.ic.gov", + EndpointVariantFIPS: "https://{service}-fips.{region}.c2s.ic.gov", + }, + }, + { + ID: "aws-iso-b", + RegionRegex: regexp.MustCompile(`^us\-isob\-\w+\-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.sc2s.sgov.gov", + EndpointVariantFIPS: "https://{service}-fips.{region}.sc2s.sgov.gov", + }, + }, + { + ID: "aws-iso-e", + RegionRegex: regexp.MustCompile(`^us\-isoe\-\w+\-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.cloud.adc-e.uk", + EndpointVariantFIPS: "https://{service}-fips.{region}.cloud.adc-e.uk", + }, + }, + { + ID: "aws-iso-f", + RegionRegex: regexp.MustCompile(`^us\-isof\-\w+\-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.csp.hci.ic.gov", + EndpointVariantFIPS: "https://{service}-fips.{region}.csp.hci.ic.gov", + }, + }, + { + ID: "aws-us-gov", + RegionRegex: regexp.MustCompile(`^us-gov-\w+-\d+$`), + Templates: map[EndpointVariant]string{ + EndpointVariantNone: "https://{service}.{region}.amazonaws.com", + EndpointVariantFIPS: "https://{service}-fips.{region}.amazonaws.com", + EndpointVariantDualStack: "https://{service}.{region}.api.aws", + }, + }, +} + +var deprecatedFIPSRegions = map[string]string{ + "us-east-1-fips": "us-east-1", + "us-east-2-fips": "us-east-2", + "us-west-1-fips": "us-west-1", + "us-west-2-fips": "us-west-2", + "ca-central-1-fips": "ca-central-1", + "ca-west-1-fips": "ca-west-1", +} + +func ResolveBaseEndpoint(service, region string, variant EndpointVariant) (string, error) { + if service == "" { + return "", fmt.Errorf("service could not be empty") + } + + if r, ok := deprecatedFIPSRegions[region]; ok { + region = r + variant |= EndpointVariantFIPS + } + + for _, p := range endpointPartitions { + if p.RegionRegex.MatchString(region) { + tpl, ok := p.Templates[variant] + if ok { + endpoint := tpl + endpoint = strings.Replace(endpoint, "{service}", service, -1) + if region == "" { + endpoint = strings.Replace(endpoint, ".{region}.", ".", -1) + } else { + endpoint = strings.Replace(endpoint, "{region}", region, -1) + } + return endpoint, nil + } + } + } + + return "", fmt.Errorf("unable to resolve endpoint for service %s in region %s", service, region) +} diff --git a/pkg/sdk3rd/aws/zz-shared-common/options.go b/pkg/sdk3rd/aws/zz-shared-common/options.go new file mode 100644 index 000000000..db82e9e4f --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-common/options.go @@ -0,0 +1,22 @@ +package common + +type Options struct { + AccessKeyId string + SecretAccessKey string + Region string +} + +type OptionsFunc func(*Options) + +func WithAkSk(ak, sk string) OptionsFunc { + return func(o *Options) { + o.AccessKeyId = ak + o.SecretAccessKey = sk + } +} + +func WithRegion(region string) OptionsFunc { + return func(o *Options) { + o.Region = region + } +} diff --git a/pkg/sdk3rd/aws/zz-shared-common/signer.go b/pkg/sdk3rd/aws/zz-shared-common/signer.go new file mode 100644 index 000000000..273eb98b9 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-common/signer.go @@ -0,0 +1,57 @@ +package common + +import ( + "bytes" + "crypto/sha256" + "encoding/base64" + "io" + "net/http" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + sigv4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" +) + +type signer struct { + accessKeyId string + secretAccessKey string + service string + region string +} + +func NewSigner(ak, sk, service, region string) *signer { + return &signer{ + accessKeyId: ak, + secretAccessKey: sk, + service: service, + region: region, + } +} + +func (s *signer) Sign(req *http.Request) error { + // API 签名机制: + // https://github.com/aws/smithy-go/blob/a4c9efcda6aa54c75d1a130d1320a2709eebf51d/aws-http-auth/sigv4/sigv4.go + + payload := ([]byte)(nil) + if req.Body != nil { + payloadb, err := io.ReadAll(req.Body) + if err != nil { + return err + } + + payload = payloadb + req.Body = io.NopCloser(bytes.NewReader(payloadb)) + } + + payloadHash := sha256.Sum256(payload) + payloadHashEncoded := base64.StdEncoding.EncodeToString(payloadHash[:]) + + ctx := req.Context() + cred := aws.Credentials{AccessKeyID: s.accessKeyId, SecretAccessKey: s.secretAccessKey} + now := time.Now() + if err := sigv4.NewSigner().SignHTTP(ctx, cred, req, payloadHashEncoded, s.service, s.region, now); err != nil { + return err + } + + return nil +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/amzjson.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/amzjson.go new file mode 100644 index 000000000..f27eefc37 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/amzjson.go @@ -0,0 +1,4 @@ +package amzjson + +// AWS JSON 1.1 protocol: +// https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/amzjson_test.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/amzjson_test.go new file mode 100644 index 000000000..584e1edaa --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/amzjson_test.go @@ -0,0 +1,51 @@ +package amzjson_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/service/acm" + acmtypes "github.com/aws/aws-sdk-go-v2/service/acm/types" + "github.com/stretchr/testify/assert" + + "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson" +) + +func TestProtocol(t *testing.T) { + t.Run("GetAPIError", func(t *testing.T) { + sdkErr, err := amzjson.GetAPIInfo([]byte(`{"__type":"InvalidParameterException","message":"The parameter is invalid."}`)) + + assert.NoError(t, err) + assert.Equal(t, "InvalidParameterException", sdkErr.ErrorCode()) + assert.Equal(t, "The parameter is invalid.", sdkErr.ErrorMessage()) + }) + + t.Run("Deserialize_[acm.ListCertificatesOutput]", func(t *testing.T) { + deserializer := amzjson.NewDeserializer() + deserializer.UseEpochTime() + + var output *acm.ListCertificatesOutput + err := deserializer.Deserialize([]byte(`{"CertificateSummaryList":[{"CertificateArn":"arn:aws:acm:us-east-1:000000000000:certificate/2717bc82-c2e4-4377-a3df-0de62c5de348","DomainName":"*.example.com","SubjectAlternativeNameSummaries":["*.example.com","example.com"],"HasAdditionalSubjectAlternativeNames":false,"Status":"ISSUED","Type":"IMPORTED","KeyAlgorithm":"EC_prime256v1","KeyUsages":["DIGITAL_SIGNATURE","KEY_ENCIPHERMENT"],"ExtendedKeyUsages":["TLS_WEB_SERVER_AUTHENTICATION","TLS_WEB_CLIENT_AUTHENTICATION"],"InUse":false,"Exported":false,"RenewalEligibility":"INELIGIBLE","NotBefore":1782835200,"NotAfter":1785513599,"ImportedAt":1784055845},{"CertificateArn":"arn:aws:acm:us-east-1:000000000000:certificate/2717bc82-c2e4-4377-a3df-0de62c5de349","DomainName":"*.example.com","SubjectAlternativeNameSummaries":["*.isafe-tech.com","example.com"],"HasAdditionalSubjectAlternativeNames":false,"Status":"ISSUED","Type":"IMPORTED","KeyAlgorithm":"EC-prime256v1","KeyUsages":["DIGITAL_SIGNATURE","KEY_ENCIPHERMENT"],"ExtendedKeyUsages":["TLS_WEB_SERVER_AUTHENTICATION","TLS_WEB_CLIENT_AUTHENTICATION"],"InUse":false,"Exported":false,"RenewalEligibility":"INELIGIBLE","NotBefore":1782835200.0,"NotAfter":1785513599.0,"ImportedAt":1.784055846E9}]}`), &output) + + assert.NoError(t, err) + assert.Len(t, output.CertificateSummaryList, 2) + assert.Equal(t, "arn:aws:acm:us-east-1:000000000000:certificate/2717bc82-c2e4-4377-a3df-0de62c5de348", *output.CertificateSummaryList[0].CertificateArn) + assert.Equal(t, "*.example.com", *output.CertificateSummaryList[0].DomainName) + assert.Equal(t, []string{"*.example.com", "example.com"}, output.CertificateSummaryList[0].SubjectAlternativeNameSummaries) + assert.Equal(t, false, *output.CertificateSummaryList[0].HasAdditionalSubjectAlternativeNames) + assert.Equal(t, acmtypes.CertificateStatusIssued, output.CertificateSummaryList[0].Status) + assert.Equal(t, acmtypes.CertificateTypeImported, output.CertificateSummaryList[0].Type) + assert.Equal(t, acmtypes.KeyAlgorithmEcPrime256v1, output.CertificateSummaryList[0].KeyAlgorithm) + assert.Equal(t, []acmtypes.KeyUsageName{acmtypes.KeyUsageNameDigitalSignature, acmtypes.KeyUsageNameKeyEncipherment}, output.CertificateSummaryList[0].KeyUsages) + assert.Equal(t, []acmtypes.ExtendedKeyUsageName{acmtypes.ExtendedKeyUsageNameTlsWebServerAuthentication, acmtypes.ExtendedKeyUsageNameTlsWebClientAuthentication}, output.CertificateSummaryList[0].ExtendedKeyUsages) + assert.Equal(t, false, *output.CertificateSummaryList[0].InUse) + assert.Equal(t, false, *output.CertificateSummaryList[0].Exported) + assert.Equal(t, acmtypes.RenewalEligibilityIneligible, output.CertificateSummaryList[0].RenewalEligibility) + assert.Equal(t, int64(1782835200), output.CertificateSummaryList[0].NotBefore.Unix()) + assert.Equal(t, int64(1785513599), output.CertificateSummaryList[0].NotAfter.Unix()) + assert.Equal(t, int64(1784055845), output.CertificateSummaryList[0].ImportedAt.Unix()) + assert.Equal(t, "arn:aws:acm:us-east-1:000000000000:certificate/2717bc82-c2e4-4377-a3df-0de62c5de349", *output.CertificateSummaryList[1].CertificateArn) + assert.Equal(t, int64(1782835200), output.CertificateSummaryList[1].NotBefore.Unix()) + assert.Equal(t, int64(1785513599), output.CertificateSummaryList[1].NotAfter.Unix()) + assert.Equal(t, int64(1784055846), output.CertificateSummaryList[1].ImportedAt.Unix()) + }) +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/deserializer.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/deserializer.go new file mode 100644 index 000000000..3ae95a791 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/deserializer.go @@ -0,0 +1,229 @@ +package amzjson + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" + "time" + + smithytime "github.com/aws/smithy-go/time" + + xreflect "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/reflect" +) + +type deserializer struct { + useEpochTime bool +} + +func NewDeserializer() *deserializer { + return &deserializer{} +} + +func (d *deserializer) UseEpochTime() { + d.useEpochTime = true +} + +func (d *deserializer) Deserialize(data []byte, v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return &json.InvalidUnmarshalError{Type: reflect.TypeOf(v)} + } + + elem := xreflect.Indirect(rv) + if elem.Kind() != reflect.Struct { + return json.Unmarshal(data, v) + } + + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + + var raw map[string]any + if err := dec.Decode(&raw); err != nil { + return err + } + return d.decodeStruct(raw, elem) +} + +func (d *deserializer) decodeStruct(dataM map[string]any, rv reflect.Value) error { + t := rv.Type() + + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) + if field.PkgPath != "" { + continue + } + + key := field.Name + val, ok := dataM[key] + if !ok { + continue + } + + fv := rv.Field(i) + if err := d.decodeValue(val, fv); err != nil { + return fmt.Errorf("failed to set field %s: %w", key, err) + } + } + + return nil +} + +func (d *deserializer) decodeValue(dataV any, rv reflect.Value) error { + if dataV == nil { + return nil + } + + if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + return d.decodeValue(dataV, rv.Elem()) + } + + switch rv.Kind() { + case reflect.Struct: + { + if rv.Type() == reflect.TypeOf(time.Time{}) { + t, err := d.toTime(dataV) + if err != nil { + return fmt.Errorf("expected time.Time, got %T: %w", dataV, err) + } + + rv.Set(reflect.ValueOf(t)) + return nil + } + + m, ok := dataV.(map[string]any) + if !ok { + return fmt.Errorf("expected object for struct, got %T", dataV) + } + + return d.decodeStruct(m, rv) + } + + case reflect.Slice: + { + arr, ok := dataV.([]any) + if !ok { + return fmt.Errorf("expected array for slice, got %T", dataV) + } + + sl := reflect.MakeSlice(rv.Type(), len(arr), len(arr)) + for i := range arr { + if err := d.decodeValue(arr[i], sl.Index(i)); err != nil { + return err + } + } + + rv.Set(sl) + return nil + } + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + { + n, err := d.toInt64(dataV) + if err != nil { + return fmt.Errorf("expected integer, got %T: %w", dataV, err) + } + + rv.SetInt(n) + return nil + } + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + { + n, err := d.toUint64(dataV) + if err != nil { + return fmt.Errorf("expected unsigned integer, got %T: %w", dataV, err) + } + + rv.SetUint(n) + return nil + } + + case reflect.Float32, reflect.Float64: + { + n, err := d.toFloat64(dataV) + if err != nil { + return fmt.Errorf("expected float, got %T: %w", dataV, err) + } + + rv.SetFloat(n) + return nil + } + + case reflect.String: + { + s, ok := dataV.(string) + if !ok { + return fmt.Errorf("expected string, got %T", dataV) + } + + rv.SetString(s) + return nil + } + + case reflect.Bool: + { + b, ok := dataV.(bool) + if !ok { + return fmt.Errorf("expected bool, got %T", dataV) + } + + rv.SetBool(b) + return nil + } + + default: + return fmt.Errorf("unsupported kind: %s", rv.Kind()) + } +} + +func (d *deserializer) toInt64(val any) (int64, error) { + switch t := val.(type) { + case json.Number: + return t.Int64() + case float64: + return int64(t), nil + default: + return 0, fmt.Errorf("cannot convert %T to int64", val) + } +} + +func (d *deserializer) toUint64(val any) (uint64, error) { + switch t := val.(type) { + case json.Number: + i, err := t.Int64() + return uint64(i), err + case float64: + return uint64(t), nil + default: + return 0, fmt.Errorf("cannot convert %T to uint64", val) + } +} + +func (d *deserializer) toFloat64(val any) (float64, error) { + switch t := val.(type) { + case json.Number: + return t.Float64() + case float64: + return t, nil + default: + return 0, fmt.Errorf("cannot convert %T to float64", val) + } +} + +func (d *deserializer) toTime(val any) (time.Time, error) { + switch t := val.(type) { + case string: + return smithytime.ParseDateTime(t) + default: + if d.useEpochTime { + if sec, err := d.toFloat64(val); err == nil { + return smithytime.ParseEpochSeconds(sec), nil + } + } + return time.Time{}, fmt.Errorf("cannot convert %T to time.Time", val) + } +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/errors.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/errors.go new file mode 100644 index 000000000..383b31a1d --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/amzjson/errors.go @@ -0,0 +1,68 @@ +package amzjson + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/aws/aws-sdk-go-v2/aws/protocol/restjson" + "github.com/aws/smithy-go" +) + +func GetAPIInfo(data []byte) (smithy.APIError, error) { + return GetAPIErrorWithRawResponse(data, nil) +} + +func GetAPIErrorWithRawResponse(data []byte, rawResp *http.Response) (smithy.APIError, error) { + errorCode, errorMessage, err := parseAPIErrorInfo(data) + if err != nil { + return nil, err + } + + apiError := &smithy.GenericAPIError{ + Code: errorCode, + Message: errorMessage, + } + if rawResp != nil { + switch rawStatus := rawResp.StatusCode; { + case rawStatus >= http.StatusBadRequest && rawStatus < http.StatusInternalServerError: + apiError.Fault = smithy.FaultClient + case rawStatus >= http.StatusInternalServerError: + apiError.Fault = smithy.FaultServer + } + } + + return apiError, nil +} + +func parseAPIErrorInfo(data []byte) (errorCode string, errorMessage string, err error) { + var errInfo struct { + Code string + Type string `json:"__type"` + Message string + } + + err = json.Unmarshal(data, &errInfo) + if err != nil { + if err == io.EOF { + return errorCode, errorMessage, nil + } + return errorCode, errorMessage, err + } + + if len(errInfo.Code) != 0 { + errorCode = errInfo.Code + } else if len(errInfo.Type) != 0 { + errorCode = errInfo.Type + } + + if len(errInfo.Message) != 0 { + errorMessage = errInfo.Message + } + + if len(errorCode) != 0 { + errorCode = restjson.SanitizeErrorCode(errorCode) + } + + return errorCode, errorMessage, nil +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/deserializer.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/deserializer.go new file mode 100644 index 000000000..668980bc2 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/deserializer.go @@ -0,0 +1,292 @@ +package query + +import ( + "bytes" + "encoding/xml" + "fmt" + "io" + "reflect" + "strconv" + "time" + + smithytime "github.com/aws/smithy-go/time" + + xreflect "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/reflect" +) + +type deserializer struct { + action string +} + +func NewDeserializer(action string) *deserializer { + return &deserializer{action: action} +} + +func (d *deserializer) Deserialize(data []byte, v any) error { + rv := reflect.ValueOf(v) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return xml.UnmarshalError("xml: Unmarshal(nil)") + } + + elem := xreflect.Indirect(rv) + if elem.Kind() != reflect.Struct { + return xml.Unmarshal(data, v) + } + + dec := xml.NewDecoder(bytes.NewReader(data)) + + if d.action != "" { + _, err := d.advanceStartElement(dec, d.action+"Response") + if err != nil { + return fmt.Errorf("cannot find root element: <%sResponse>", d.action) + } + + start, err := d.advanceStartElement(dec, d.action+"Result") + if err != nil { + return fmt.Errorf("cannot find start element: <%sResult>", d.action) + } + + if err := d.decodeStruct(dec, start, elem); err != nil { + return err + } + + return nil + } + + for { + tok, err := dec.Token() + if err != nil { + if err == io.EOF { + break + } + return err + } + + if se, ok := tok.(xml.StartElement); ok { + if err := d.decodeStruct(dec, se, elem); err != nil { + return err + } + } + } + + return nil +} + +func (d *deserializer) advanceStartElement(dec *xml.Decoder, name string) (xml.StartElement, error) { + for { + tok, err := dec.Token() + if err != nil { + return xml.StartElement{}, err + } + + switch t := tok.(type) { + case xml.StartElement: + if t.Name.Local == name { + return t, nil + } + } + } +} + +func (d *deserializer) decodeStruct(dec *xml.Decoder, start xml.StartElement, rv reflect.Value) error { + if rv.Kind() == reflect.Ptr { + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + return d.decodeStruct(dec, start, rv.Elem()) + } + + if rv.Kind() != reflect.Struct { + return fmt.Errorf("expected struct, got %s", rv.Kind()) + } + + fields := map[string]reflect.Value{} + for i := 0; i < rv.NumField(); i++ { + ft := rv.Type().Field(i) + name := ft.Name + fields[name] = rv.Field(i) + } + + for { + tok, err := dec.Token() + if err != nil { + return err + } + + switch t := tok.(type) { + case xml.StartElement: + field, ok := fields[t.Name.Local] + if !ok { + if err := dec.Skip(); err != nil { + return err + } + continue + } + + if err := d.decodeValue(dec, t, field); err != nil { + return err + } + + case xml.EndElement: + if t.Name.Local == start.Name.Local { + return nil + } + if d.action != "" && (t.Name.Local == d.action+"Result" || t.Name.Local == d.action+"Response") { + return nil + } + } + } +} + +func (d *deserializer) decodeValue(dec *xml.Decoder, start xml.StartElement, rv reflect.Value) error { + switch rv.Kind() { + case reflect.Ptr: + { + if rv.IsNil() { + rv.Set(reflect.New(rv.Type().Elem())) + } + return d.decodeValue(dec, start, rv.Elem()) + } + + case reflect.Struct: + { + if rv.Type() == reflect.TypeOf(time.Time{}) { + s, err := d.decodeCharData(dec) + if err != nil { + return err + } + + t, err := smithytime.ParseDateTime(s) + if err != nil { + return fmt.Errorf("cannot convert '%s' to time.Time: %w", s, err) + } + + rv.Set(reflect.ValueOf(t)) + return nil + } + + return d.decodeStruct(dec, start, rv) + } + + case reflect.Slice: + { + return d.decodeSlice(dec, start, rv) + } + + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s, err := d.decodeCharData(dec) + if err != nil { + return err + } + + i, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return fmt.Errorf("cannot convert '%s' to int64: %w", s, err) + } + + rv.SetInt(i) + return nil + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + s, err := d.decodeCharData(dec) + if err != nil { + return err + } + + u, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return fmt.Errorf("cannot convert '%s' to uint64: %w", s, err) + } + + rv.SetUint(u) + return nil + + case reflect.Float32, reflect.Float64: + s, err := d.decodeCharData(dec) + if err != nil { + return err + } + + f, err := strconv.ParseFloat(s, 64) + if err != nil { + return fmt.Errorf("cannot convert '%s' to float: %w", s, err) + } + + rv.SetFloat(f) + return nil + + case reflect.String: + { + s, err := d.decodeCharData(dec) + if err != nil { + return err + } + + rv.SetString(s) + return nil + } + + case reflect.Bool: + { + s, err := d.decodeCharData(dec) + if err != nil { + return err + } + + b, err := strconv.ParseBool(s) + if err != nil { + return fmt.Errorf("cannot convert '%s' to bool: %w", s, err) + } + + rv.SetBool(b) + return nil + } + + default: + return fmt.Errorf("unsupported kind: %s", rv.Kind()) + } +} + +func (d *deserializer) decodeSlice(dec *xml.Decoder, start xml.StartElement, rv reflect.Value) error { + elemType := rv.Type().Elem() + + for { + tok, err := dec.Token() + if err != nil { + return err + } + + switch t := tok.(type) { + case xml.EndElement: + if t.Name.Local == start.Name.Local { + return nil + } + + case xml.StartElement: + if t.Name.Local != "member" { + return fmt.Errorf("expected , got <%s>", t.Name.Local) + } + + newElem := reflect.New(elemType).Elem() + if err := d.decodeValue(dec, t, newElem); err != nil { + return err + } + + rv.Set(reflect.Append(rv, newElem)) + } + } +} + +func (d *deserializer) decodeCharData(dec *xml.Decoder) (string, error) { + tok, err := dec.Token() + if err != nil { + return "", err + } + + cd, ok := tok.(xml.CharData) + if !ok { + return "", fmt.Errorf("expected char data") + } + + return string(cd), nil +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/errors.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/errors.go new file mode 100644 index 000000000..5ed8f1d67 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/errors.go @@ -0,0 +1,17 @@ +package query + +import ( + "net/http" + + "github.com/aws/smithy-go" + + "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml" +) + +func GetAPIError(data []byte) (smithy.APIError, error) { + return GetAPIErrorWithRawResponse(data, nil) +} + +func GetAPIErrorWithRawResponse(data []byte, rawResp *http.Response) (smithy.APIError, error) { + return restxml.GetAPIErrorWithRawResponse(data, rawResp) +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/query.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/query.go new file mode 100644 index 000000000..87efa60d6 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/query.go @@ -0,0 +1,4 @@ +package query + +// AWS query protocol: +// https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/query_test.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/query_test.go new file mode 100644 index 000000000..03759da3b --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/query_test.go @@ -0,0 +1,93 @@ +package query_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" + elasticloadbalancingv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/stretchr/testify/assert" + + "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query" +) + +func TestProtocol(t *testing.T) { + t.Run("GetAPIError", func(t *testing.T) { + sdkErr, err := query.GetAPIError([]byte(`SenderInvalidParameterExceptionThe parameter is invalid.645a617f-73b5-4882-bb23-d68d22d16a76`)) + + assert.NoError(t, err) + assert.Equal(t, "InvalidParameterException", sdkErr.ErrorCode()) + assert.Equal(t, "The parameter is invalid.", sdkErr.ErrorMessage()) + }) + + t.Run("Deserialize_[iam.ListServerCertificatesOutput]", func(t *testing.T) { + deserializer := query.NewDeserializer("ListServerCertificates") + + var output *iam.ListServerCertificatesOutput + err := deserializer.Deserialize([]byte(`falseProdServerCert/company/servercerts/arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert2010-05-08T01:02:03.004ZASCACKCEVSQ6CEXAMPLE12012-05-08T01:02:03.004ZBetaServerCert/company/servercerts/arn:aws:iam::123456789012:server-certificate/company/servercerts/BetaServerCert2010-05-08T02:03:01.004ZASCACKCEVSQ6CEXAMPLE22012-05-08T02:03:01.004ZTestServerCert/company/servercerts/arn:aws:iam::123456789012:server-certificate/company/servercerts/TestServerCert2010-05-08T03:01:02.004ZASCACKCEVSQ6CEXAMPLE32012-05-08T03:01:02.004Z7a62c49f-347e-4fc4-9331-6e8eEXAMPLE`), &output) + + assert.NoError(t, err) + assert.Len(t, output.ServerCertificateMetadataList, 3) + assert.Equal(t, "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert", *output.ServerCertificateMetadataList[0].Arn) + assert.Equal(t, "ASCACKCEVSQ6CEXAMPLE1", *output.ServerCertificateMetadataList[0].ServerCertificateId) + assert.Equal(t, "ProdServerCert", *output.ServerCertificateMetadataList[0].ServerCertificateName) + assert.Equal(t, "/company/servercerts/", *output.ServerCertificateMetadataList[0].Path) + assert.Equal(t, int64(1273280523), output.ServerCertificateMetadataList[0].UploadDate.Unix()) + assert.Equal(t, int64(1336438923004), output.ServerCertificateMetadataList[0].Expiration.UnixMilli()) + }) + + t.Run("Deserialize_[iam.ListServerCertificatesOutput]_panic", func(t *testing.T) { + assert.NotPanics(t, func() { + deserializer := query.NewDeserializer("") + + var output *iam.ListServerCertificatesOutput + err := deserializer.Deserialize([]byte(`falseProdServerCert/company/servercerts/arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert2010-05-08T01:02:03.004ZASCACKCEVSQ6CEXAMPLE12012-05-08T01:02:03.004ZBetaServerCert/company/servercerts/arn:aws:iam::123456789012:server-certificate/company/servercerts/BetaServerCert2010-05-08T02:03:01.004ZASCACKCEVSQ6CEXAMPLE22012-05-08T02:03:01.004ZTestServerCert/company/servercerts/arn:aws:iam::123456789012:server-certificate/company/servercerts/TestServerCert2010-05-08T03:01:02.004ZASCACKCEVSQ6CEXAMPLE32012-05-08T03:01:02.004Z7a62c49f-347e-4fc4-9331-6e8eEXAMPLE`), &output) + + assert.NoError(t, err) + assert.Len(t, output.ServerCertificateMetadataList, 0) + }) + }) + + t.Run("SerializeToMap_[elasticloadbalancingv2.DescribeLoadBalancers]", func(t *testing.T) { + serializer := query.NewSerializer() + serializer.UseOmitEmptyValue() + + input := &elasticloadbalancingv2.DescribeLoadBalancersInput{ + LoadBalancerArns: []string{"arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188"}, + } + paramsMap, err := serializer.SerializeToMap(input) + + assert.NoError(t, err) + assert.Len(t, paramsMap, 1) + assert.Equal(t, "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188", paramsMap["LoadBalancerArns.member.1"]) + }) + + t.Run("SerializeToMap_[elasticloadbalancingv2.ModifyListener]", func(t *testing.T) { + serializer := query.NewSerializer() + serializer.UseOmitEmptyValue() + + input := &elasticloadbalancingv2.ModifyListenerInput{ + ListenerArn: aws.String("arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2"), + DefaultActions: []elasticloadbalancingv2types.Action{ + { + Type: elasticloadbalancingv2types.ActionTypeEnumForward, + TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-new-targets/2453ed029918f21e"), + }, + { + Type: elasticloadbalancingv2types.ActionTypeEnumRedirect, + TargetGroupArn: aws.String("arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-new-targets/2453ed029918f21f"), + }, + }, + } + paramsMap, err := serializer.SerializeToMap(input) + + assert.NoError(t, err) + assert.Len(t, paramsMap, 5) + assert.Equal(t, "arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/app/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2", paramsMap["ListenerArn"]) + assert.Equal(t, "forward", paramsMap["DefaultActions.member.1.Type"]) + assert.Equal(t, "arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-new-targets/2453ed029918f21e", paramsMap["DefaultActions.member.1.TargetGroupArn"]) + assert.Equal(t, "redirect", paramsMap["DefaultActions.member.2.Type"]) + assert.Equal(t, "arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-new-targets/2453ed029918f21f", paramsMap["DefaultActions.member.2.TargetGroupArn"]) + }) +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/serializer.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/serializer.go new file mode 100644 index 000000000..186fd2fe6 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/query/serializer.go @@ -0,0 +1,111 @@ +package query + +import ( + "encoding/json" + "fmt" + "net/url" + "strconv" +) + +type serializer struct { + omitEmpty bool +} + +func NewSerializer() *serializer { + return &serializer{} +} + +func (s *serializer) UseOmitEmptyValue() { + s.omitEmpty = true +} + +func (s *serializer) Serialize(v any) ([]byte, error) { + m, err := s.SerializeToMap(v) + if err != nil { + return nil, err + } + + values := url.Values{} + for k, v := range m { + values.Set(k, v) + } + + return []byte(values.Encode()), nil +} + +func (s *serializer) SerializeToMap(v any) (map[string]string, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + + raw := make(map[string]any) + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + s.tidy(raw) + + m := make(map[string]string) + s.flatten("", raw, m) + return m, nil +} + +func (s *serializer) flatten(prefix string, val any, out map[string]string) { + switch v := val.(type) { + case map[string]any: + for k, sub := range v { + key := k + if prefix != "" { + key = prefix + "." + k + } + s.flatten(key, sub, out) + } + + case []any: + for i, elem := range v { + index := strconv.Itoa(i + 1) + key := prefix + ".member." + index + s.flatten(key, elem, out) + } + + default: + out[prefix] = fmt.Sprintf("%v", v) + } +} + +func (s *serializer) tidy(m map[string]any) { + for k, v := range m { + if v == nil { + delete(m, k) + continue + } + + switch val := v.(type) { + case string: + if s.omitEmpty && len(val) == 0 { + delete(m, k) + } + + case map[string]any: + if s.omitEmpty && len(val) == 0 { + delete(m, k) + } else { + s.tidy(val) + } + + case []any: + if s.omitEmpty && len(val) == 0 { + delete(m, k) + } else { + for _, item := range val { + if item == nil { + continue + } + if subm, ok := item.(map[string]any); ok { + s.tidy(subm) + } + } + } + } + } +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/errors.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/errors.go new file mode 100644 index 000000000..ad58dacb2 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/errors.go @@ -0,0 +1,56 @@ +package restjson + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/aws/smithy-go" +) + +func GetAPIError(data []byte) (smithy.APIError, error) { + return GetAPIErrorWithRawResponse(data, nil) +} + +func GetAPIErrorWithRawResponse(data []byte, rawResp *http.Response) (smithy.APIError, error) { + errorMessage, err := parseAPIErrorInfo(data) + if err != nil { + return nil, err + } + + apiError := &smithy.GenericAPIError{ + Message: errorMessage, + } + if rawResp != nil { + apiError.Code = rawResp.Header.Get("X-Amzn-ErrorType") + + switch rawStatus := rawResp.StatusCode; { + case rawStatus >= http.StatusBadRequest && rawStatus < http.StatusInternalServerError: + apiError.Fault = smithy.FaultClient + case rawStatus >= http.StatusInternalServerError: + apiError.Fault = smithy.FaultServer + } + } + + return apiError, nil +} + +func parseAPIErrorInfo(data []byte) (errorMessage string, err error) { + var errInfo struct { + Message string + } + + err = json.Unmarshal(data, &errInfo) + if err != nil { + if err == io.EOF { + return errorMessage, nil + } + return errorMessage, err + } + + if len(errInfo.Message) != 0 { + errorMessage = errInfo.Message + } + + return errorMessage, nil +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/restjson.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/restjson.go new file mode 100644 index 000000000..8dfad924c --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/restjson.go @@ -0,0 +1,4 @@ +package restjson + +// AWS restJson1 protocol: +// https://smithy.io/2.0/aws/protocols/aws-restjson1-protocol.html diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/restjson_test.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/restjson_test.go new file mode 100644 index 000000000..caedd8ac0 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/restjson_test.go @@ -0,0 +1,51 @@ +package restjson_test + +import ( + "net/http" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/amplify" + amplifytypes "github.com/aws/aws-sdk-go-v2/service/amplify/types" + "github.com/stretchr/testify/assert" + + "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson" +) + +func TestProtocol(t *testing.T) { + t.Run("GetErrorInfo", func(t *testing.T) { + mockResponse := &http.Response{Header: http.Header{}} + mockResponse.StatusCode = http.StatusBadRequest + mockResponse.Header.Set("x-amzn-ErrorType", "InvalidParameterException") + sdkErr, err := restjson.GetAPIErrorWithRawResponse([]byte(`{"message":"The parameter is invalid."}`), mockResponse) + + assert.NoError(t, err) + assert.Equal(t, "InvalidParameterException", sdkErr.ErrorCode()) + assert.Equal(t, "The parameter is invalid.", sdkErr.ErrorMessage()) + }) + + t.Run("Serialize_[amplify.UpdateDomainAssociationInput]", func(t *testing.T) { + serializer := restjson.NewSerializer() + serializer.UseCamelCaseNamePolicy() + + intput := amplify.UpdateDomainAssociationInput{ + AppId: aws.String("a1b2c3d4e5"), + DomainName: aws.String("example.com"), + CertificateSettings: &lifytypes.CertificateSettings{ + Type: amplifytypes.CertificateTypeCustom, + CustomCertificateArn: aws.String("arn:aws:acm:us-east-1:000000000000:certificate/2717bc82-c2e4-4377-a3df-0de62c5de348"), + }, + } + jsonb, err := serializer.Serialize(intput) + + assert.NoError(t, err) + assert.NotContains(t, string(jsonb), "AppId") + assert.NotContains(t, string(jsonb), "DomainName") + assert.NotContains(t, string(jsonb), "CertificateSettings") + assert.NotContains(t, string(jsonb), "CustomCertificateArn") + assert.Contains(t, string(jsonb), "appId") + assert.Contains(t, string(jsonb), "domainName") + assert.Contains(t, string(jsonb), "certificateSettings") + assert.Contains(t, string(jsonb), "customCertificateArn") + }) +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/serializer.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/serializer.go new file mode 100644 index 000000000..f2f528c67 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restjson/serializer.go @@ -0,0 +1,150 @@ +package restjson + +import ( + "encoding/json" + "fmt" + "reflect" + "unicode" +) + +type namePolicy int + +const ( + namePolicyNone namePolicy = iota + namePolicyCamelCase +) + +type serializer struct { + namePolicy namePolicy + omitEmpty bool +} + +func NewSerializer() *serializer { + return &serializer{} +} + +func (s *serializer) UseDefaultNamePolicy() { + s.namePolicy = namePolicyNone +} + +func (s *serializer) UseCamelCaseNamePolicy() { + s.namePolicy = namePolicyCamelCase +} + +func (s *serializer) UseOmitEmptyValue() { + s.omitEmpty = true +} + +func (s *serializer) Serialize(v any) ([]byte, error) { + m, err := s.SerializeToMap(v) + if err != nil { + return nil, err + } + + return json.Marshal(m) +} + +func (s *serializer) SerializeToMap(v any) (map[string]any, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + + raw := make(map[string]any) + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + s.tidy(raw) + + switch s.namePolicy { + case namePolicyNone: + return raw, nil + + case namePolicyCamelCase: + raw = s.deepCamelize(raw).(map[string]any) + return raw, nil + + default: + return nil, fmt.Errorf("unsupported name policy: %v", s.namePolicy) + } +} + +func (s *serializer) deepCamelize(v any) any { + val := reflect.ValueOf(v) + + switch val.Kind() { + case reflect.Map: + { + if val.Type().Key().Kind() != reflect.String { + return v + } + + newMap := make(map[string]any) + for _, key := range val.MapKeys() { + nkey := s.toCamelCase(key.String()) + elem := val.MapIndex(key).Interface() + newMap[nkey] = s.deepCamelize(elem) + } + return newMap + } + + case reflect.Slice, reflect.Array: + { + newSlice := make([]any, val.Len()) + for i := 0; i < val.Len(); i++ { + newSlice[i] = s.deepCamelize(val.Index(i).Interface()) + } + return newSlice + } + + default: + return v + } +} + +func (s *serializer) toCamelCase(str string) string { + if str == "" { + return str + } + + r := []rune(str) + r[0] = unicode.ToLower(r[0]) + return string(r) +} + +func (s *serializer) tidy(m map[string]any) { + for k, v := range m { + if v == nil { + delete(m, k) + continue + } + + switch val := v.(type) { + case string: + if s.omitEmpty && len(val) == 0 { + delete(m, k) + } + + case map[string]any: + if s.omitEmpty && len(val) == 0 { + delete(m, k) + } else { + s.tidy(val) + } + + case []any: + if s.omitEmpty && len(val) == 0 { + delete(m, k) + } else { + for _, item := range val { + if item == nil { + continue + } + if subm, ok := item.(map[string]any); ok { + s.tidy(subm) + } + } + } + } + } +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/errors.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/errors.go new file mode 100644 index 000000000..f614b0dfe --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/errors.go @@ -0,0 +1,60 @@ +package restxml + +import ( + "encoding/xml" + "io" + "net/http" + + "github.com/aws/smithy-go" +) + +func GetAPIError(data []byte) (smithy.APIError, error) { + return GetAPIErrorWithRawResponse(data, nil) +} + +func GetAPIErrorWithRawResponse(data []byte, rawResp *http.Response) (smithy.APIError, error) { + errorType, errorMessage, err := parseAPIErrorInfo(data) + if err != nil { + return nil, err + } + + apiError := &smithy.GenericAPIError{ + Code: errorType, + Message: errorMessage, + } + if rawResp != nil { + switch rawStatus := rawResp.StatusCode; { + case rawStatus >= http.StatusBadRequest && rawStatus < http.StatusInternalServerError: + apiError.Fault = smithy.FaultClient + case rawStatus >= http.StatusInternalServerError: + apiError.Fault = smithy.FaultServer + } + } + + return apiError, nil +} + +func parseAPIErrorInfo(data []byte) (errorCode string, errorMessage string, err error) { + var errInfo struct { + Code string `xml:"Error>Code"` + Message string `xml:"Error>Message"` + } + + err = xml.Unmarshal(data, &errInfo) + if err != nil { + if err == io.EOF { + return errorCode, errorMessage, nil + } + return errorCode, errorMessage, err + } + + if len(errInfo.Code) != 0 { + errorCode = errInfo.Code + } + + if len(errInfo.Message) != 0 { + errorMessage = errInfo.Message + } + + return errorCode, errorMessage, nil +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/restxml.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/restxml.go new file mode 100644 index 000000000..a7888b8af --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/restxml.go @@ -0,0 +1,4 @@ +package restxml + +// AWS query protocol: +// https://smithy.io/2.0/aws/protocols/aws-json-1_1-protocol.html diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/restxml_test.go b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/restxml_test.go new file mode 100644 index 000000000..880f92f0e --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml/restxml_test.go @@ -0,0 +1,19 @@ +package restxml_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/certimate-go/certimate/pkg/sdk3rd/aws/zz-shared-smithy/protocol/restxml" +) + +func TestProtocol(t *testing.T) { + t.Run("GetAPIError", func(t *testing.T) { + sdkErr, err := restxml.GetAPIError([]byte(`SenderInvalidParameterExceptionThe parameter is invalid.645a617f-73b5-4882-bb23-d68d22d16a76`)) + + assert.NoError(t, err) + assert.Equal(t, "InvalidParameterException", sdkErr.ErrorCode()) + assert.Equal(t, "The parameter is invalid.", sdkErr.ErrorMessage()) + }) +} diff --git a/pkg/sdk3rd/aws/zz-shared-smithy/reflect/reflect.go b/pkg/sdk3rd/aws/zz-shared-smithy/reflect/reflect.go new file mode 100644 index 000000000..62d593d32 --- /dev/null +++ b/pkg/sdk3rd/aws/zz-shared-smithy/reflect/reflect.go @@ -0,0 +1,24 @@ +package reflect + +import ( + "reflect" +) + +func Indirect(v reflect.Value) reflect.Value { + for { + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + case reflect.Interface: + if v.IsNil() { + return v + } + v = v.Elem() + default: + return v + } + } +}