diff --git a/internal/deployer/local.go b/internal/deployer/local.go index 1f1ef488..ccda2306 100644 --- a/internal/deployer/local.go +++ b/internal/deployer/local.go @@ -155,5 +155,5 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) { return "", "", xerrors.Wrap(err, "failed to execute shell script") } - return stdoutBuf.String(), stderrBuf.String(), err + return stdoutBuf.String(), stderrBuf.String(), nil } diff --git a/internal/deployer/ssh.go b/internal/deployer/ssh.go index 98c4de48..a94c9782 100644 --- a/internal/deployer/ssh.go +++ b/internal/deployer/ssh.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" + xerrors "github.com/pkg/errors" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" @@ -55,7 +56,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { if preCommand != "" { stdout, stderr, err := d.sshExecCommand(client, preCommand) if err != nil { - return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr) } d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout)) @@ -65,13 +66,13 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { case certFormatPEM: if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { - return fmt.Errorf("failed to upload certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { - return fmt.Errorf("failed to upload private key file: %w", err) + return err } d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil)) @@ -83,11 +84,11 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { d.option.DeployConfig.GetConfigAsString("pfxPassword"), ) if err != nil { - return fmt.Errorf("failed to convert pem to pfx %w", err) + return err } if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { - return fmt.Errorf("failed to upload certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) @@ -101,11 +102,11 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { d.option.DeployConfig.GetConfigAsString("jksStorepass"), ) if err != nil { - return fmt.Errorf("failed to convert pem to pfx %w", err) + return err } if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { - return fmt.Errorf("failed to save certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("保存证书成功", nil)) @@ -116,7 +117,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { if command != "" { stdout, stderr, err := d.sshExecCommand(client, command) if err != nil { - return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr) } d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout)) @@ -158,7 +159,7 @@ func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, er func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string, string, error) { session, err := client.NewSession() if err != nil { - return "", "", fmt.Errorf("failed to create ssh session: %w", err) + return "", "", xerrors.Wrap(err, "failed to create ssh session") } defer session.Close() @@ -167,7 +168,11 @@ func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string var stderrBuf bytes.Buffer session.Stderr = &stderrBuf err = session.Run(command) - return stdoutBuf.String(), stderrBuf.String(), err + if err != nil { + return "", "", xerrors.Wrap(err, "failed to execute ssh script") + } + + return stdoutBuf.String(), stderrBuf.String(), nil } func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, content string) error { @@ -177,23 +182,23 @@ func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, conte func (d *SSHDeployer) writeSftpFile(client *ssh.Client, path string, data []byte) error { sftpCli, err := sftp.NewClient(client) if err != nil { - return fmt.Errorf("failed to create sftp client: %w", err) + return xerrors.Wrap(err, "failed to create sftp client") } defer sftpCli.Close() if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil { - return fmt.Errorf("failed to create remote directory: %w", err) + return xerrors.Wrap(err, "failed to create remote directory") } file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) if err != nil { - return fmt.Errorf("failed to open remote file: %w", err) + return xerrors.Wrap(err, "failed to open remote file") } defer file.Close() _, err = file.Write(data) if err != nil { - return fmt.Errorf("failed to write to remote file: %w", err) + return xerrors.Wrap(err, "failed to write to remote file") } return nil diff --git a/internal/deployer/tencent_cdn.go b/internal/deployer/tencent_cdn.go index 26c9c252..d89cb819 100644 --- a/internal/deployer/tencent_cdn.go +++ b/internal/deployer/tencent_cdn.go @@ -5,38 +5,58 @@ import ( "encoding/json" "fmt" "strings" - "golang.org/x/exp/slices" - cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" + xerrors "github.com/pkg/errors" + tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + "golang.org/x/exp/slices" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" ) type TencentCDNDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentCDNDeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentCDNDeployerSdkClients struct { + ssl *tcSsl.Client + cdn *tcCdn.Client } func NewTencentCDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentCDNDeployer{}).createSdkClients( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploader.NewTencentCloudSSLUploader(&uploader.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentCDNDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -49,141 +69,124 @@ func (d *TencentCDNDeployer) GetInfo() []string { } func (d *TencentCDNDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + return err } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 获取待部署的 CDN 实例 + // 如果是泛域名,根据证书匹配 CDN 实例 + aliInstanceIds := make([]string, 0) + domain := d.option.DeployConfig.GetConfigAsString("domain") + if strings.HasPrefix(domain, "*") { + domains, err := d.getDomainsByCertificateId(upres.CertId) + if err != nil { + return err + } + + aliInstanceIds = domains + } else { + aliInstanceIds = append(aliInstanceIds, domain) } + // 跳过已部署的 CDN 实例 + if len(aliInstanceIds) > 0 { + deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId) + if err != nil { + return err + } + + temp := make([]string, 0) + for _, aliInstanceId := range aliInstanceIds { + if !slices.Contains(deployedDomains, aliInstanceId) { + temp = append(temp, aliInstanceId) + } + } + aliInstanceIds = temp + } + if len(aliInstanceIds) == 0 { + d.infos = append(d.infos, "已部署过或没有要部署的 CDN 实例") + return nil + } + + // 证书部署到 CDN 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds) + deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) + return nil } -func (d *TencentCDNDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile()) + if err != nil { + return nil, err + } + + return &tencentCDNDeployerSdkClients{ + ssl: sslClient, + cdn: cdnClient, + }, nil } -func (d *TencentCDNDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, "", cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("cdn") - request.Status = common.Int64Ptr(1) - - // 如果是泛域名就从cdn列表下获取SSL证书中的可用域名 - domain := getDeployString(d.option.DeployConfig, "domain") - if strings.Contains(domain, "*") { - list, errGetList := d.getDomainList(certId) - if errGetList != nil { - return fmt.Errorf("failed to get certificate domain list: %w", errGetList) - } - if len(list) == 0 { - d.infos = append(d.infos, "没有需要部署的实例") - return nil - } - request.InstanceIdList = common.StringPtrs(list) - } else { // 否则直接使用传入的域名 - deployed, _ := d.isDomainDeployed(certId, domain) - if(deployed){ - d.infos = append(d.infos, "域名已部署") - return nil - }else{ - request.InstanceIdList = common.StringPtrs([]string{domain}) - } - } - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) +func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) { + // 获取证书中的可用域名 + // REF: https://cloud.tencent.com/document/product/228/42491 + describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest() + describeCertDomainsReq.CertId = common.StringPtr(tcCertId) + describeCertDomainsReq.Product = common.StringPtr("cdn") + describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) - } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil -} - -func (d *TencentCDNDeployer) getDomainList(certId string) ([]string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com" - client, _ := cdn.NewClient(d.credential, "", cpf) - - request := cdn.NewDescribeCertDomainsRequest() - - request.CertId = common.StringPtr(certId) - - response, err := client.DescribeCertDomains(request) - if err != nil { - return nil, fmt.Errorf("failed to get domain list: %w", err) - } - - deployedDomains, err := d.getDeployedDomainList(certId) - if err != nil { - return nil, fmt.Errorf("failed to get deployed domain list: %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'") } domains := make([]string, 0) - for _, domain := range response.Response.Domains { - domainStr := *domain - if(slices.Contains(deployedDomains, domainStr)){ - domains = append(domains, domainStr) + if describeCertDomainsResp.Response.Domains == nil { + for _, domain := range describeCertDomainsResp.Response.Domains { + domains = append(domains, *domain) } } return domains, nil } -func (d *TencentCDNDeployer) isDomainDeployed(certId, domain string) (bool, error) { - deployedDomains, err := d.getDeployedDomainList(certId) - if(err != nil){ - return false, err - } - - return slices.Contains(deployedDomains, domain), nil -} - -func (d *TencentCDNDeployer) getDeployedDomainList(certId string) ([]string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewDescribeDeployedResourcesRequest() - request.CertificateIds = common.StringPtrs([]string{certId}) - request.ResourceType = common.StringPtr("cdn") - - response, err := client.DescribeDeployedResources(request) +func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) { + // 根据证书查询关联 CDN 域名 + // REF: https://cloud.tencent.com/document/product/400/62674 + describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest() + describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId}) + describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn") + describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq) if err != nil { - return nil, fmt.Errorf("failed to get deployed domain list: %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'") } domains := make([]string, 0) - for _, domain := range response.Response.DeployedResources[0].Resources { - domains = append(domains, *domain) + if describeDeployedResourcesResp.Response.DeployedResources != nil { + for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources { + for _, resource := range deployedResource.Resources { + domains = append(domains, *resource) + } + } } return domains, nil diff --git a/internal/deployer/tencent_clb.go b/internal/deployer/tencent_clb.go index ba97e840..45ccb993 100644 --- a/internal/deployer/tencent_clb.go +++ b/internal/deployer/tencent_clb.go @@ -3,37 +3,58 @@ package deployer import ( "context" "encoding/json" + "errors" "fmt" + xerrors "github.com/pkg/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" ) type TencentCLBDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentCLBDeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentCLBDeployerSdkClients struct { + ssl *tcSsl.Client } func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentCLBDeployer{}).createSdkClients( access.SecretId, access.SecretKey, + option.DeployConfig.GetConfigAsString("region"), ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploader.NewTencentCloudSSLUploader(&uploader.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentCLBDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -46,72 +67,68 @@ func (d *TencentCLBDeployer) GetInfo() []string { } func (d *TencentCLBDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() - if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) - } - d.infos = append(d.infos, toStr("上传证书", certId)) + // TODO: 直接部署方式 - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + // 通过 SSL 服务部署到云资源实例 + err := d.deployToInstanceUseSsl(ctx) + if err != nil { + return err } return nil } -func (d *TencentCLBDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + return &tencentCLBDeployerSdkClients{ + ssl: sslClient, + }, nil } -func (d *TencentCLBDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("clb") - request.Status = common.Int64Ptr(1) - - clbId := getDeployString(d.option.DeployConfig, "clbId") - lsnId := getDeployString(d.option.DeployConfig, "lsnId") - domain := getDeployString(d.option.DeployConfig, "domain") - - if(domain == ""){ - // 未开启SNI,只需要精确到监听器 - request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", clbId, lsnId)}) - }else{ - // 开启SNI,需要精确到域名,支持泛域名 - request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", clbId, lsnId, domain)}) +func (d *TencentCLBDeployer) deployToInstanceUseSsl(ctx context.Context) error { + tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("clbId") + tcListenerId := d.option.DeployConfig.GetConfigAsString("lsnId") + tcDomain := d.option.DeployConfig.GetConfigAsString("domain") + if tcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + if tcListenerId == "" { + return errors.New("`listenerId` is required") } - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) + return err } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) + + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 证书部署到 CLB 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("clb") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + if tcDomain == "" { + // 未开启 SNI,只需指定到监听器 + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", tcLoadbalancerId, tcListenerId)}) + } else { + // 开启 SNI,需指定到域名(支持泛域名) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", tcLoadbalancerId, tcListenerId, tcDomain)}) + } + deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) + return nil -} \ No newline at end of file +} diff --git a/internal/deployer/tencent_cos.go b/internal/deployer/tencent_cos.go index 2ea8c7d6..749f5249 100644 --- a/internal/deployer/tencent_cos.go +++ b/internal/deployer/tencent_cos.go @@ -3,37 +3,53 @@ package deployer import ( "context" "encoding/json" + "errors" "fmt" + xerrors "github.com/pkg/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" ) type TencentCOSDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClient *tcSsl.Client + sslUploader uploader.Uploader } func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + client, err := (&TencentCOSDeployer{}).createSdkClient( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploader.NewTencentCloudSSLUploader(&uploader.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentCOSDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, }, nil } @@ -46,63 +62,44 @@ func (d *TencentCOSDeployer) GetInfo() []string { } func (d *TencentCOSDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() - if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + tcRegion := d.option.DeployConfig.GetConfigAsString("region") + tcBucket := d.option.DeployConfig.GetConfigAsString("bucket") + tcDomain := d.option.DeployConfig.GetConfigAsString("domain") + if tcBucket == "" { + return errors.New("`bucket` is required") } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err } + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 证书部署到 CLB 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("cos") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", tcRegion, tcBucket, tcDomain)}) + deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) + return nil } -// 上传证书,与CDN部署的上传方法一致。 -func (d *TencentCOSDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) +func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) { + credential := common.NewCredential(secretId, secretKey) + client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + return client, nil } - -func (d *TencentCOSDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("cos") - request.Status = common.Int64Ptr(1) - - domain := getDeployString(d.option.DeployConfig, "domain") - request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", getDeployString(d.option.DeployConfig, "region"), getDeployString(d.option.DeployConfig, "bucket"), domain)}) - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) - if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) - } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil -} \ No newline at end of file diff --git a/internal/deployer/tencent_ecdn.go b/internal/deployer/tencent_ecdn.go index ea52794e..0458c74d 100644 --- a/internal/deployer/tencent_ecdn.go +++ b/internal/deployer/tencent_ecdn.go @@ -2,41 +2,60 @@ package deployer import ( "context" - "encoding/base64" "encoding/json" "fmt" "strings" - cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" + xerrors "github.com/pkg/errors" + tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" ) type TencentECDNDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentECDNDeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentECDNDeployerSdkClients struct { + ssl *tcSsl.Client + cdn *tcCdn.Client } func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentECDNDeployer{}).createSdkClients( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploader.NewTencentCloudSSLUploader(&uploader.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentECDNDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -49,97 +68,85 @@ func (d *TencentECDNDeployer) GetInfo() []string { } func (d *TencentECDNDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + return err } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 获取待部署的 ECDN 实例 + // 如果是泛域名,根据证书匹配 ECDN 实例 + aliInstanceIds := make([]string, 0) + domain := d.option.DeployConfig.GetConfigAsString("domain") + if strings.HasPrefix(domain, "*") { + domains, err := d.getDomainsByCertificateId(upres.CertId) + if err != nil { + return err + } + + aliInstanceIds = domains + } else { + aliInstanceIds = append(aliInstanceIds, domain) } + if len(aliInstanceIds) == 0 { + d.infos = append(d.infos, "没有要部署的 ECDN 实例") + return nil + } + + // 证书部署到 ECDN 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds) + deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) return nil } -func (d *TencentECDNDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentECDNDeployer) createSdkClients(secretId, secretKey string) (*tencentECDNDeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile()) + if err != nil { + return nil, err + } + + return &tencentECDNDeployerSdkClients{ + ssl: sslClient, + cdn: cdnClient, + }, nil } -func (d *TencentECDNDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, "", cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("ecdn") - request.Status = common.Int64Ptr(1) - - // 如果是泛域名就从cdn列表下获取SSL证书中的可用域名 - domain := getDeployString(d.option.DeployConfig, "domain") - if strings.Contains(domain, "*") { - list, errGetList := d.getDomainList() - if errGetList != nil { - return fmt.Errorf("failed to get certificate domain list: %w", errGetList) - } - if list == nil || len(list) == 0 { - return fmt.Errorf("failed to get certificate domain list: empty list.") - } - request.InstanceIdList = common.StringPtrs(list) - } else { // 否则直接使用传入的域名 - request.InstanceIdList = common.StringPtrs([]string{domain}) - } - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) +func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) { + // 获取证书中的可用域名 + // REF: https://cloud.tencent.com/document/product/228/42491 + describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest() + describeCertDomainsReq.CertId = common.StringPtr(tcCertId) + describeCertDomainsReq.Product = common.StringPtr("ecdn") + describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) - } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil -} - -func (d *TencentECDNDeployer) getDomainList() ([]string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com" - client, _ := cdn.NewClient(d.credential, "", cpf) - - request := cdn.NewDescribeCertDomainsRequest() - - cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate)) - request.Cert = &cert - request.Product = common.StringPtr("ecdn") - - response, err := client.DescribeCertDomains(request) - if err != nil { - return nil, fmt.Errorf("failed to get domain list: %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'") } domains := make([]string, 0) - for _, domain := range response.Response.Domains { - domains = append(domains, *domain) + if describeCertDomainsResp.Response.Domains == nil { + for _, domain := range describeCertDomainsResp.Response.Domains { + domains = append(domains, *domain) + } } return domains, nil diff --git a/internal/deployer/tencent_teo.go b/internal/deployer/tencent_teo.go index f31aee8d..83eca55e 100644 --- a/internal/deployer/tencent_teo.go +++ b/internal/deployer/tencent_teo.go @@ -6,36 +6,56 @@ import ( "fmt" "strings" - teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" + xerrors "github.com/pkg/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" ) type TencentTEODeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentTEODeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentTEODeployerSdkClients struct { + ssl *tcSsl.Client + teo *tcTeo.Client } func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentTEODeployer{}).createSdkClients( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploader.NewTencentCloudSSLUploader(&uploader.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentTEODeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -48,64 +68,51 @@ func (d *TencentTEODeployer) GetInfo() []string { } func (d *TencentTEODeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() - if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + tcZoneId := d.option.DeployConfig.GetConfigAsString("zoneId") + if tcZoneId == "" { + return xerrors.New("`zoneId` is required") } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err } + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 配置域名证书 + // REF: https://cloud.tencent.com/document/product/1552/80764 + modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest() + modifyHostsCertificateReq.ZoneId = common.StringPtr(tcZoneId) + modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") + modifyHostsCertificateReq.Hosts = common.StringPtrs(strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"), "\n")) + modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} + modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'") + } + + d.infos = append(d.infos, toStr("已配置域名证书", modifyHostsCertificateResp.Response)) + return nil } -func (d *TencentTEODeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentTEODeployer) createSdkClients(secretId, secretKey string) (*tencentTEODeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil -} - -func (d *TencentTEODeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := teo.NewClient(d.credential, "", cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := teo.NewModifyHostsCertificateRequest() - - request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId")) - request.Mode = common.StringPtr("sslcert") - request.ServerCertInfo = []*teo.ServerCertInfo{{ - CertId: common.StringPtr(certId), - }} - - domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n") - request.Hosts = common.StringPtrs(domains) - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.ModifyHostsCertificate(request) + teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) + return nil, err } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil + + return &tencentTEODeployerSdkClients{ + ssl: sslClient, + teo: teoClient, + }, nil } diff --git a/internal/deployer/webhook.go b/internal/deployer/webhook.go index 522705d4..bc227f0d 100644 --- a/internal/deployer/webhook.go +++ b/internal/deployer/webhook.go @@ -7,6 +7,8 @@ import ( "fmt" "net/http" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/domain" xhttp "github.com/usual2970/certimate/internal/utils/http" ) @@ -41,7 +43,7 @@ type webhookData struct { func (d *WebhookDeployer) Deploy(ctx context.Context) error { access := &domain.WebhookAccess{} if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return fmt.Errorf("failed to parse hook access config: %w", err) + return xerrors.Wrap(err, "failed to get access") } data := &webhookData{ @@ -50,17 +52,15 @@ func (d *WebhookDeployer) Deploy(ctx context.Context) error { PrivateKey: d.option.Certificate.PrivateKey, Variables: getDeployVariables(d.option.DeployConfig), } - body, _ := json.Marshal(data) - resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{ "Content-Type": "application/json", }) if err != nil { - return fmt.Errorf("failed to send hook request: %w", err) + return xerrors.Wrap(err, "failed to send webhook request") } - d.infos = append(d.infos, toStr("webhook response", string(resp))) + d.infos = append(d.infos, toStr("Webhook Response", string(resp))) return nil }